diff --git a/etc/soft/nvim/+ftplugin/haskell.vim b/etc/soft/nvim/+ftplugin/haskell.vim index 5b2f721..adc5fab 100644 --- a/etc/soft/nvim/+ftplugin/haskell.vim +++ b/etc/soft/nvim/+ftplugin/haskell.vim @@ -6,8 +6,6 @@ set shiftwidth=8 set formatprg=stylish-haskell -" runtime! syntax/custom/HaskellConceal.vim - " ============================================================================= " Descriptions: Provide a function providing folding information for haskell " files. diff --git a/etc/soft/nvim/+ftplugin/help.vim b/etc/soft/nvim/+ftplugin/help.vim new file mode 100644 index 0000000..eb93bea --- /dev/null +++ b/etc/soft/nvim/+ftplugin/help.vim @@ -0,0 +1 @@ +autocmd BufWinEnter wincmd L diff --git a/etc/soft/nvim/+ftplugin/man.vim b/etc/soft/nvim/+ftplugin/man.vim new file mode 100644 index 0000000..eb93bea --- /dev/null +++ b/etc/soft/nvim/+ftplugin/man.vim @@ -0,0 +1 @@ +autocmd BufWinEnter wincmd L diff --git a/etc/soft/nvim/+ftplugin/markdown.vim b/etc/soft/nvim/+ftplugin/markdown.vim index 10461d3..3a8db4e 100644 --- a/etc/soft/nvim/+ftplugin/markdown.vim +++ b/etc/soft/nvim/+ftplugin/markdown.vim @@ -1,2 +1,4 @@ -" setlocal syntax=off -" set re=1 +setlocal spell +setlocal spelllang=ru,en + +set filetype=markdown.pandoc diff --git a/etc/soft/nvim/+layouts/coptic.vim b/etc/soft/nvim/+layouts/coptic.vim deleted file mode 100644 index 22b34dd..0000000 --- a/etc/soft/nvim/+layouts/coptic.vim +++ /dev/null @@ -1,57 +0,0 @@ -" Ввод команд в коптской раскладке - -map ⲑ q -map ⲱ w -map ⲉ e -map ⲣ r -map ⲧ t -map ⲯ y -map ⲩ u -map ⲓ i -map ⲟ o -map ⲡ p -map ⲁ a -map ⲥ s -map ⲇ d -map ⲫ f -map ⲅ g -map ⲅⲅ gg -map ⲏ h -map ϫ j -map ⲕ k -map ⲗ l -map ⲍ z -map ⲝ x -map ⲭ c -map ϣ v -map ⲃ b -map ⲛ n -map ⲙ m -map ~ ~ -map Ⲑ Q -map Ⲱ W -map Ⲉ E -map Ⲣ R -map Ⲧ T -map Ⲯ Y -map Ⲩ U -map Ⲓ I -map Ⲟ O -map Ⲡ P -map Ⲁ A -map Ⲥ S -map Ⲇ D -map Ⲫ F -map Ⲅ G -map Ⲏ H -map Ϫ J -map Ⲕ K -map Ⲗ L -map Ⲍ Z -map Ⲝ X -map Ⲭ C -map Ϣ V -map Ⲃ B -map Ⲛ N -map Ⲙ M - diff --git a/etc/soft/nvim/+layouts/greek.vim b/etc/soft/nvim/+layouts/greek.vim deleted file mode 100644 index 6a75383..0000000 --- a/etc/soft/nvim/+layouts/greek.vim +++ /dev/null @@ -1,55 +0,0 @@ -" Ввод команд в древнегреческой раскладке - -map ς w -map ε e -map ρ r -map τ t -map υ y -map θ u -map ι i -map ο o -map π p -map α a -map σ s -map δ d -map φ f -map γ g -map γγ gg -map η h -map ξ j -map κ k -map λ l -map ζ z -map χ x -map ψ c -map ω v -map β b -map ν n -map μ m -map ~ ~ -map Σ W -map Ε E -map Ρ R -map Τ T -map Υ Y -map Θ U -map Ι I -map Ο O -map Π P -map Α A -map Σ S -map Δ D -map Φ F -map Γ G -map Η H -map Ξ J -map Κ K -map Λ L -map Ζ Z -map Χ X -map Ψ C -map Ω V -map Β B -map Ν N -map Μ M - diff --git a/etc/soft/nvim/+layouts/russian.vim b/etc/soft/nvim/+layouts/russian.vim deleted file mode 100644 index bb63092..0000000 --- a/etc/soft/nvim/+layouts/russian.vim +++ /dev/null @@ -1,70 +0,0 @@ -" Ввод команд в русской раскладке - -map ё ` -map й q -map ц w -map у e -map к r -map е t -map н y -map г u -map ш i -map щ o -map з p -map х [ -map ъ ] -map ф a -map ы s -map в d -map а f -map п g -map р h -map о j -map л k -map д l -map ж ; -map э ' -map я z -map ч x -map с c -map м v -map и b -map т n -map ь m -map б , -map ю . -map Ё ~ -map Й Q -map Ц W -map У E -map К R -map Е T -map Н Y -map Г U -map Ш I -map Щ O -map З P -map Х { -map Ъ } -map Ф A -map Ы S -map В D -map А F -map П G -map Р H -map О J -map Л K -map Д L -map Ж : -map Э " -map Я Z -map Ч X -map С C -map М V -map И B -map Т N -map Ь M -map Б < -map Ю > -map , / - diff --git a/etc/soft/nvim/+plugins/cmd-parser.nvim/README.md b/etc/soft/nvim/+plugins/cmd-parser.nvim/README.md deleted file mode 100644 index effea15..0000000 --- a/etc/soft/nvim/+plugins/cmd-parser.nvim/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# cmd-parser.nvim - -I built this plugin to help other plugin authors to easily parse the command inputted by users and do awesome tricks with it. - -Input - -```lua -local parse_cmd = require'cmd-parser'.parse_cmd -parse_cmd("10+2++,/hello/-3d") -``` - -Output - -```lua -{ ["start_increment_number"] = 4,["end_increment"] = -3,["command"] = d,["start_range"] = 10,["end_increment_number"] = -3,["start_increment"] = +2++,["end_range"] = /hello/,} -``` - -## Installtion - -### `Paq.nvim` - -```lua -paq{'winston0410/cmd-parser.nvim'} -``` - -## Testing - -This plugin is well tested. To run the test case or help with testing, you need to install lester - -```shell -luarocks install lester -``` diff --git a/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.lua b/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.lua deleted file mode 100644 index c4b06ed..0000000 --- a/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.lua +++ /dev/null @@ -1,88 +0,0 @@ -local number_range = "^(%d+)" -local mark_range = "^('[%l><])" -local forward_search_range = "^(/.*/)" -local backward_search_range = "^(?.*?)" -local special_range = "^([%%%.$])" - -local command_pattern = "^(%l+)" -local range_patterns = { - special_range, number_range, mark_range, forward_search_range, - backward_search_range -} -local range_patterns_type = { - "special", "number", "mark", "forward_search", "backward_search" -} - -local function get_range(index, cmd) - local range, type - for i = 1, #range_patterns do - local _, end_index, result = string.find(cmd, range_patterns[i], index) - if end_index then - index = end_index + 1 - range = result - type = range_patterns_type[i] - break - end - end - if type == "special" then type = range end - return range, type, index -end - -local function update_increment(operator, increment, acc_text, acc_num) - local inc_str = acc_text .. operator .. increment - if increment == "" then increment = 1 end - return inc_str, acc_num + tonumber(operator .. increment) -end - -local function get_increment(index, cmd) - local pattern, inc_text, total, done = "([+-])(%d*)", "", 0, false - while not done do - local _, end_index, operator, increment = - string.find(cmd, pattern, index) - if not end_index then - done = true - break - end - inc_text, total = update_increment(operator, increment, inc_text, total) - index = end_index + 1 - end - - return inc_text, total, index -end - -local function parse_cmd(cmd) - local result, next_index, comma_index, _ = {}, 1, nil, nil - local start_range_text - - result.start_range, result.start_range_type, next_index = get_range(1, cmd) - - comma_index, _, result.separator = string.find(cmd, '[(;,)]', next_index) - - if comma_index then - if not result.start_range then - result.start_range = "." - result.start_range_type = "." - end - start_range_text = string.sub(cmd, 1, comma_index) - else - start_range_text = cmd - end - result.start_increment, result.start_increment_number, next_index = - get_increment(next_index, start_range_text) - if comma_index then - -- To offset the comma_index - next_index = next_index + 1 - result.end_range, result.end_range_type, next_index = - get_range(next_index, cmd) - result.end_increment, result.end_increment_number, next_index = - get_increment(next_index, cmd) - end - - _, _, result.command = string.find(cmd, command_pattern, next_index) - - return result -end - -local function setup() end - -return {setup = setup, parse_cmd = parse_cmd} diff --git a/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.test.lua b/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.test.lua deleted file mode 100644 index bd1c9ca..0000000 --- a/etc/soft/nvim/+plugins/cmd-parser.nvim/lua/cmd-parser/init.test.lua +++ /dev/null @@ -1,199 +0,0 @@ -local lester = require 'lester' -local describe, it, expect = lester.describe, lester.it, lester.expect -local parse_cmd = require'init'.parse_cmd - --- customize lester configuration. -lester.show_traceback = false - -describe('when parse_cmd is called', function() - local result = parse_cmd("") - it('should return a table', - function() expect.equal(type(result), "table") end) -end) - -describe('when it is called without range', function() - local result = parse_cmd("d") - it('should return nil start range', - function() expect.equal(result.start_range, nil) end) - it('should return nil start range type', - function() expect.equal(result.start_range_type, nil) end) -end) - -describe('when it is called with shorthand range', function () - local result = parse_cmd(",20d") - it('should return start range as .', function () - expect.equal(result.start_range, ".") - end) - it('should return start range type', function () - expect.equal(result.start_range_type, ".") - end) -end) - -describe('when it is called with single number range', function() - local result = parse_cmd("23d") - it('should return the start range', - function() expect.equal(result.start_range, "23") end) - it('should return the start range type', - function() expect.equal(result.start_range_type, "number") end) - it('should return the command', - function() expect.equal(result.command, "d") end) -end) --- -describe('when it is called with single number range', function() - describe('when it has increment', function() - local result = parse_cmd("23+5d") - it('should return the command', - function() expect.equal(result.command, "d") end) - it('should return the start increment', - function() expect.equal(result.start_increment, "+5") end) - it('should return the start increment number', - function() expect.equal(result.start_increment_number, 5) end) - end) -end) - -describe('when it is called with a complete number range', function() - local result = parse_cmd("10,20d") - it('should return the start range', - function() expect.equal(result.start_range, "10") end) - it('should return the end range', - function() expect.equal(result.end_range, "20") end) - it('should return the end range type', - function() expect.equal(result.end_range_type, "number") end) - it('should return the command', - function() expect.equal(result.command, "d") end) -end) - -describe('when it is called with a complete number range', function() - local result = parse_cmd("10+3+++-,20d-8+") - it('should return the start increment', - function() expect.equal(result.start_increment, "+3+++-") end) - it('should return the start increment number', - function() expect.equal(result.start_increment_number, 5) end) - it('should return the end increment', - function() expect.equal(result.end_increment, "-8+") end) - it('should return the end increment number', - function() expect.equal(result.end_increment_number, -7) end) -end) - -describe('when it is called with single mark range', function() - local result = parse_cmd("'ad") - it('should return the start range', - function() expect.equal(result.start_range, "'a") end) - it('should return the start range type', - function() expect.equal(result.start_range_type, "mark") end) - it('should return the command', - function() expect.equal(result.command, "d") end) -end) - -describe('when it is called with single mark range', function() - describe('when it has increments', function() - local result = parse_cmd("'a+2-5+3d") - it('should return the start range increment', - function() expect.equal(result.start_increment, "+2-5+3") end) - it('should return the start range increment number', - function() expect.equal(result.start_increment_number, 0) end) - end) -end) - -describe('when it is called with a complete mark range', function() - local result = parse_cmd("'a,'bt") - it('should return the start range', - function() expect.equal(result.start_range, "'a") end) - it('should return the end range', - function() expect.equal(result.end_range, "'b") end) - it('should return the end range type', - function() expect.equal(result.end_range_type, "mark") end) - it('should return the command', - function() expect.equal(result.command, "t") end) -end) - -describe('when it is called with a complete mark range', function() - describe('when it has increment', function() - local result = parse_cmd("'a+++-5,'b+20-t") - it('should return the start range increment', - function() expect.equal(result.start_increment, "+++-5") end) - it('should return the start range increment number', - function() expect.equal(result.start_increment_number, -2) end) - it('should return the end range increment', - function() expect.equal(result.end_increment, "+20-") end) - it('should return the end range increment number', - function() expect.equal(result.end_increment_number, 19) end) - end) -end) - -describe('when it is called with single forward search range', function() - local result = parse_cmd("/hello/d") - it('should return the start range', - function() expect.equal(result.start_range, "/hello/") end) - it('should return the start range type', - function() expect.equal(result.start_range_type, "forward_search") end) - it('should return the command', - function() expect.equal(result.command, "d") end) -end) - -describe('when it is called with single forward search range', function() - describe('when it has increments', function() - local result = parse_cmd("/hello/+10-2d") - it('should return the start increment', - function() expect.equal(result.start_increment, "+10-2") end) - it('should return the start increment number', - function() expect.equal(result.start_increment_number, 8) end) - end) -end) - -describe('when it is called with single backward search range', function() - local result = parse_cmd("?hello?pu") - it('should return the start range', - function() expect.equal(result.start_range, "?hello?") end) - it('should return the start range type', - function() expect.equal(result.start_range_type, "backward_search") end) - it('should return the command', - function() expect.equal(result.command, "pu") end) -end) - -describe('when it is called with single backward search range', function() - describe('when it has increments', function() - local result = parse_cmd("?hello?+10-3pu") - it('should return the start increment', - function() expect.equal(result.start_increment, "+10-3") end) - it('should return the start increment number', - function() expect.equal(result.start_increment_number, 7) end) - end) -end) - -describe('when it is called with single special range', function() - local result = parse_cmd("%y") - it('should return the start range', - function() expect.equal(result.start_range, "%") end) - it('should return the start range type', - function() expect.equal(result.start_range_type, "%") end) - it('should return the command', - function() expect.equal(result.command, "y") end) -end) - -describe('when it is called with single special range', function() - describe('when it has increment', function() - local result = parse_cmd("%y+20---") - it('should return the start increment', - function() expect.equal(result.start_increment, "+20---") end) - it('should return the command', - function() expect.equal(result.start_increment_number, 17) end) - end) -end) - --- Bug test: mark range '<,'> cannot be detected -describe('when it is called with complete mark range', function () - local result = parse_cmd("'<,'>") - it('should return the start range type', function () - expect.equal(result.start_range_type, "mark") - end) - it('should return the start range', function() - expect.equal(result.start_range, "'<") - end) - it('should return the end range', function() - expect.equal(result.end_range, "'>") - end) -end) - -lester.report() -- Print overall statistic of the tests run. -lester.exit() -- Exit with success if all tests passed. diff --git a/etc/soft/nvim/+plugins/cmdline_increment/plugin/cmdline-increment.vim b/etc/soft/nvim/+plugins/cmdline_increment/plugin/cmdline-increment.vim deleted file mode 100644 index 5c25341..0000000 --- a/etc/soft/nvim/+plugins/cmdline_increment/plugin/cmdline-increment.vim +++ /dev/null @@ -1,178 +0,0 @@ -scriptencoding utf-8 -if &cp || exists('g:loaded_cmdline_increment') - finish -endif -let g:loaded_cmdline_increment = 1 - -" escape user configuration -let s:save_cpo = &cpo -set cpo&vim - -" mapping -if !hasmapto('IncrementCommandLineNumber', 'c') - cmap IncrementCommandLineNumber -endif -cnoremap IncrementCommandLineNumber ":call g:IncrementCommandLineNumbering(1):=g:IncrementedCommandLine() -if !hasmapto('DecrementCommandLineNumber', 'c') - cmap DecrementCommandLineNumber -endif -cnoremap DecrementCommandLineNumber ":call g:IncrementCommandLineNumbering(-1):=g:IncrementedCommandLine() - -" script sharing variables. -" updated command line will be stored in g:IncrementCommandLineNumbering(). -let s:updatedcommandline = '' - -" increment, or decrement last appearing number. -function! g:IncrementCommandLineNumbering(plus) - " when continuous increment is done, because @: is not updated, - " plugin can not increment correctly. - " so add one entry to command history, and use there flag. - " last command is start with '"' is first try, - " last command is not start with '"' is second try. - let l:lastcommand = histget(':', -1) - let l:firstcommandchar = strpart(l:lastcommand, 0, 1) - - " l:command is '"' starting text. - if l:firstcommandchar ==# '"' - let l:command = l:lastcommand - else - let l:command = '"' . l:lastcommand - endif - - " check input - let l:matchtest = match(l:command, '^".\{-\}-\?\d\+\D*$') - " command do not contain number - if l:matchtest < 0 - " remove first char '"' - let s:updatedcommandline = substitute(l:command, '^"\(.*\)$', '\1', '') - return - endif - - " update numbering - let l:numpattern = substitute(l:command, '^".\{-\}\(\d\+\)\D*$', '\1', '') - let l:updatednumberpattern = s:IncrementedText(l:numpattern, a:plus) - - " create new command line strings - let l:p1 = substitute(l:command, '^"\(.\{-\}\)\d\+\D*$', '\1', '') - let l:p2 = l:updatednumberpattern - let l:p3 = substitute(l:command, '^".\{-\}\d\+\(\D*\)$', '\1', '') - " set l register - let s:updatedcommandline = l:p1 . l:p2 . l:p3 - - " delete '"' command (dummy command) history - call histdel(':', -1) - " wrong command history is added. limitation. - call histadd(':', s:updatedcommandline) -endfunction - -" return incremented command line text. -function! g:IncrementedCommandLine() - return s:updatedcommandline -endfunction - -" return incremented pattern text. -" -" number + - -" 0 -> 1, 0 -" 0000 -> 0001, 0000 -" 127 -> 128, 126 -" 0127 -> 0128, 0126 -" 00127 -> 00128, 00126 -function! s:IncrementedText(pattern, plus) - " 0 - if match(a:pattern, '^0$') >= 0 - if a:plus > 0 - return a:pattern + a:plus - else - " not supported - return a:pattern - endif - endif - - " 123 - if match(a:pattern, '^[^0]\d*$') >= 0 - return a:pattern + a:plus - endif - - " 00000 - if match(a:pattern, '^0\+$') >= 0 - if a:plus > 0 - let l:numlength = strlen(a:pattern) - return printf('%0' .l:numlength. 'd', a:plus) - else - " not supported - return a:pattern - endif - endif - - " 00123 - if match(a:pattern, '^0\d*$') >= 0 - echo a:pattern + a:plus - let l:numlength = strlen(a:pattern) - let l:number = substitute(a:pattern, '^0\+\(\d\+\)$', '\1', '') - return printf('%0' .l:numlength. 'd', l:number + a:plus) - endif - - throw 'unknow numbering pattern is found.' -endfunction - -" recover user configuration -let &cpo = s:save_cpo -finish - -============================================================================== -cmdline-increment.vim : increment, decrement for commandline number. ------------------------------------------------------------------------------- -$VIMRUNTIMEPATH/plugin/cmdline-increment.vim -============================================================================== -author : OMI TAKU -url : http://nanasi.jp/ -email : mail@nanasi.jp -version : 2009/09/19 03:00:00 -============================================================================== -Increment last appearing number in commandline-mode command with Control-a , -and decrement with Control-x . - - increment commandline last appearing number. - decrement commandline last appearing number. - - ------------------------------------------------------------------------------- -[Usage] - -1. Enter commandline mode. -2. Enter next command. - - :edit workfile_1.txt - -3. And Press Control-a , or press Control-x . - - ------------------------------------------------------------------------------- -[Customized Mapping] - -If you will customize increment, decrement mapping, -add put these code to your vimrc . - - " (for example) - " increment with Shift-Up - cmap IncrementCommandLineNumber - " decrement with Shift-Down - cmap DecrementCommandLineNumber - - ------------------------------------------------------------------------------- -[history] -2009/09/17 - - initial version. - -2009/09/18 - - plugin is now not using 'l' register. - - below 0 number decrement is newly not supported. - minial value is 0. - - default mapping is switched to , . - - custom mapping is supported. - - -============================================================================== -" vim: set ff=unix et ft=vim nowrap : diff --git a/etc/soft/nvim/+plugins/dial.nvim/HISTORY.md b/etc/soft/nvim/+plugins/dial.nvim/HISTORY.md new file mode 100644 index 0000000..8ea02f0 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/HISTORY.md @@ -0,0 +1,60 @@ +# Changelog + +## Unreleased + +### New Features + +* add options to augend 'date' + * `custom_date_elements` + * `clamp` + * `end_sensitive` + +## 0.4.0 + +### New Features + +* add augend: paren ([#15](https://github.com/monaqa/dial.nvim/pull/15)) +* add augend: case ([#26](https://github.com/monaqa/dial.nvim/pull/26), [#33](https://github.com/monaqa/dial.nvim/pull/33)) +* support comma-separated number or other ([#16](https://github.com/monaqa/dial.nvim/pull/16)) +* re-implement augend markdown_header ([#21](https://github.com/monaqa/dial.nvim/pull/21)) +* add alias: German date formats and weekdays ([#24](https://github.com/monaqa/dial.nvim/pull/24), by @f1rstlady) +* add public config API for 'date' augend ([#35](https://github.com/monaqa/dial.nvim/pull/35)): + * pattern + * default_kind + * only_valid + * word + +### Fixes + +* Fix document ([#22](https://github.com/monaqa/dial.nvim/pull/22), by @ktakayama) + +### Deprecates + +* `augend.date.alias["%Y/%m/%d"]` +* `augend.date.alias["%m/%d/%Y"]` +* `augend.date.alias["%d/%m/%Y"]` +* `augend.date.alias["%m/%d/%y"]` +* `augend.date.alias["%d/%m/%y"]` +* `augend.date.alias["%m/%d"]` +* `augend.date.alias["%-m/%-d"]` +* `augend.date.alias["%Y-%m-%d"]` +* `augend.date.alias["%Y年%-m月%-d日"]` +* `augend.date.alias["%Y年%-m月%-d日(%ja)"]` +* `augend.date.alias["%H:%M:%S"]` +* `augend.date.alias["%H:%M"]` + +## 0.3.0 + +* **[BREAKING CHANGE]** change overall interface +* support dot repeating +* support specifying augends with expression register + +## 0.2.0 + +* **[BREAKING CHANGE]** rename all augends +* **[BREAKING CHANGE]** change the directory structure +* add help file + +## 0.1.0 + +* first release diff --git a/etc/soft/nvim/+plugins/dial.nvim/LICENSE b/etc/soft/nvim/+plugins/dial.nvim/LICENSE new file mode 100644 index 0000000..99e65d8 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mogami Shinichi + +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. diff --git a/etc/soft/nvim/+plugins/dial.nvim/README.md b/etc/soft/nvim/+plugins/dial.nvim/README.md new file mode 100644 index 0000000..3f50b68 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/README.md @@ -0,0 +1,357 @@ +# dial.nvim + +**NOTICE: This plugin is work-in-progress yet. User interface is subject to change without notice.** + +## FOR USERS OF THE PREVIOUS VERSION (v0.2.0) + +This plugin was released v0.3.0 on 2022/02/20 and is no longer compatible with the old interface. +If you have configured the settings for previous versions, please refer to [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) and reconfigure them. + +## Abstract + +Extended increment/decrement plugin for [Neovim](https://github.com/neovim/neovim). Written in Lua. + +![demo.gif](https://github.com/monaqa/dial.nvim/wiki/fig/dial-demo-2.gif) + +## Features + +* Increment/decrement based on various type of rules + * n-ary (`2 <= n <= 36`) integers + * date and time + * constants (an ordered set of specific strings, such as a keyword or operator) + * `true` ⇄ `false` + * `&&` ⇄ `||` + * `a` ⇄ `b` ⇄ ... ⇄ `z` + * hex colors + * semantic version +* Support `` / `` / `g` / `g` in VISUAL mode +* Flexible configuration of increment/decrement targets + * Rules that are valid only in specific FileType + * Rules that are valid only in VISUAL mode +* Support counter +* Support dot repeat (without overriding the behavior of `.`) + +## Similar plugins + +* [tpope/vim-speeddating](https://github.com/tpope/vim-speeddating) +* [Cycle.vim](https://github.com/zef/vim-cycle) +* [AndrewRadev/switch.vim](https://github.com/AndrewRadev/switch.vim) + +## Installation + +`dial.nvim` requires Neovim `>=0.5.0` (`>=0.6.1` is recommended). +You can install `dial.nvim` by following the instructions of your favorite package manager. + +## Usage + +This plugin does not provide or override any default key-mappings. +To use this plugin, you need to assign the plugin key-mapping to the key you like, as shown below: + +```vim +nmap (dial-increment) +nmap (dial-decrement) +vmap (dial-increment) +vmap (dial-decrement) +vmap g g(dial-increment) +vmap g g(dial-decrement) +``` + +Or you can configure it with Lua as follows: + +```lua +vim.api.nvim_set_keymap("n", "", require("dial.map").inc_normal(), {noremap = true}) +vim.api.nvim_set_keymap("n", "", require("dial.map").dec_normal(), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").inc_visual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").dec_visual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "g", require("dial.map").inc_gvisual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "g", require("dial.map").dec_gvisual(), {noremap = true}) +``` + +## Configuration + +In this plugin, flexible increment/decrement rules can be set by using **augend** and **group**, +where **augend** represents the target of the increment/decrement operation, +and **group** represents a group of multiple augends. + +```lua +local augend = require("dial.augend") +require("dial.config").augends:register_group{ + -- default augends used when no group name is specified + default = { + augend.integer.alias.decimal, -- nonnegative decimal number (0, 1, 2, 3, ...) + augend.integer.alias.hex, -- nonnegative hex number (0x01, 0x1a1f, etc.) + augend.date.alias["%Y/%m/%d"], -- date (2022/02/19, etc.) + }, + + -- augends used when group with name `mygroup` is specified + mygroup = { + augend.integer.alias.decimal, + augend.constant.alias.bool, -- boolean value (true <-> false) + augend.date.alias["%m/%d/%Y"], -- date (02/19/2022, etc.) + } +} +``` + +* To define a group, use the `augends:register_group` function in the `"dial.config"` module. + The arguments is a dictionary whose keys are the group names and whose values are the list of augends. +* Various augends are defined `"dial.augend"` by default. + +To specify the group of augends, you can use **expression register** ([`:h @=`](https://neovim.io/doc/user/change.html#quote_=)) as follows: + +``` +"=mygroup +``` + +If it is tedious to specify the expression register for each operation, you can "map" it: + +```vim +nmap a "=mygroup(dial-increment) +``` + +Alternatively, you can set the same mapping without expression register: + +```lua +vim.api.nvim_set_keymap("n", "a", require("dial.map").inc_normal("mygroup"), {noremap = true}) +``` + +When you don't specify any group name in the way described above, the addends in the `default` group is used instead. + +### Example Configuration + +```vim +lua << EOF +local augend = require("dial.augend") +require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + }, + typescript = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.constant.new{ elements = {"let", "const"} }, + }, + visual = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + augend.constant.alias.alpha, + augend.constant.alias.Alpha, + }, +} + +-- change augends in VISUAL mode +vim.api.nvim_set_keymap("v", "", require("dial.map").inc_normal("visual"), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").dec_normal("visual"), {noremap = true}) +EOF + +" enable only for specific FileType +autocmd FileType typescript lua vim.api.nvim_buf_set_keymap(0, "n", "", require("dial.map").inc_normal("typescript"), {noremap = true}) +``` + +## List of Augends + +For simplicity, we define the variable `augend` as follows. + +```lua +local augend = require("dial.augend") +``` + +### `integer` + +`n`-based integer (`2 <= n <= 36`). You can use this rule with `augend.integer.new{ ...opts }`. + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.integer.new{ + radix = 16, + prefix = "0x", + natural = true, + case = "upper", + }, + }, +} +``` + +## `date` + +Date and time. + +```lua +require("dial.config").augends:register_group{ + default = { + -- date with format `yyyy/mm/dd` + augend.date.new{ + pattern = "%Y/%m/%d", + default_kind = "day", + -- if true, it does not match dates which does not exist, such as 2022/05/32 + only_valid = true, + -- if true, it only matches dates with word boundary + word = false, + }, + }, +} +``` + +In the `pattern` argument, you can use the following escape sequences: + +|Sequence|Meaning | +|-----|------------------------------------------------------------------------------| +|`%Y` |4-digit year. (e.g. `2022`) | +|`%y` |Last 2 digits of year. The upper 2 digits are interpreted as `20`. (e.g. `22`)| +|`%m` |2-digit month. (e.g. `09`) | +|`%d` |2-digit day. (e.g. `28`) | +|`%H` |2-digit hour, expressed in 24 hours. (e.g. `15`) | +|`%I` |2-digit hour, expressed in 12 hours. (e.g. `03`) | +|`%M` |2-digit minute. (e.g. `05`) | +|`%S` |2-digit second. (e.g. `08`) | +|`%-y`|1- or 2-digit year. (e.g. `9` represents 2009) | +|`%-m`|1- or 2-digit month. (e.g. `9`) | +|`%-d`|1- or 2-digit day. (e.g. `28`) | +|`%-H`|1- or 2-digit hour, expressed in 24 hours. (e.g. `15`) | +|`%-I`|1- or 2-digit hour, expressed in 12 hours. (e.g. `3`) | +|`%-M`|1- or 2-digit minute. (e.g. `5`) | +|`%-S`|1- or 2-digit second. (e.g. `8`) | +|`%a` |English weekdays (`Sun`, `Mon`, ..., `Sat`) | +|`%A` |English full weekdays (`Sunday`, `Monday`, ..., `Saturday`) | +|`%b` |English month names (`Jan`, ..., `Dec`) | +|`%B` |English month full names (`January`, ..., `December`) | +|`%p` |`AM` or `PM`. | +|`%J` |Japanese weekdays (`日`, `月`, ..., `土`) | + +## `constant` + +Predefined sequence of strings. You can use this rule with `augend.constant.new{ ...opts }`. + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.constant.new{ + elements = {"and", "or"}, + word = true, -- if false, "sand" is incremented into "sor", "doctor" into "doctand", etc. + cyclic = true, -- "or" is incremented into "and". + }, + augend.constant.new{ + elements = {"&&", "||"}, + word = false, + cyclic = true, + }, + }, +} +``` + +### `hexcolor` + +RGB color code such as `#000000` and `#ffffff`. + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.hexcolor.new{ + case = "lower", + }, + }, +} +``` + +### `semver` + +Semantic versions. You can use this rule by augend alias described below. + +It differs from a simple nonnegative integer increment/decrement in these ways: + +* When the cursor is before the semver string, the patch version is incremented. +* When the minor version is incremented, the patch version is reset to zero. +* When the major version is incremented, the minor and patch versions are reset to zero. + +### `user` + +Custom augends. + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.user.new{ + find = require("dial.augend.common").find_pattern("%d+"), + add = function(text, addend, cursor) + local n = tonumber(text) + n = math.floor(n * (2 ^ addend)) + text = tostring(n) + cursor = #text + return {text = text, cursor = cursor} + end + }, + }, +} +``` + +## Augend Alias + +Some augend rules are defined as alias. It can be used directly without using `new` function. + +```lua +require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + }, +} +``` + +|Alias Name |Explanation |Examples | +|------------------------------------------|-------------------------------------------------|------------------------------------| +|`augend.integer.alias.decimal` |decimal natural number |`0`, `1`, ..., `9`, `10`, `11`, ... | +|`augend.integer.alias.decimal_int` |decimal integer (including negative number) |`0`, `314`, `-1592`, ... | +|`augend.integer.alias.hex` |hex natural number |`0x00`, `0x3f3f`, ... | +|`augend.integer.alias.octal` |octal natural number |`0o00`, `0o11`, `0o24`, ... | +|`augend.integer.alias.binary` |binary natural number |`0b0101`, `0b11001111`, ... | +|`augend.date.alias["%Y/%m/%d"]` |Date in the format `%Y/%m/%d` (`0` padding) |`2021/01/23`, ... | +|`augend.date.alias["%m/%d/%Y"]` |Date in the format `%m/%d/%Y` (`0` padding) |`23/01/2021`, ... | +|`augend.date.alias["%d/%m/%Y"]` |Date in the format `%d/%m/%Y` (`0` padding) |`01/23/2021`, ... | +|`augend.date.alias["%m/%d/%y"]` |Date in the format `%m/%d/%y` (`0` padding) |`01/23/21`, ... | +|`augend.date.alias["%d/%m/%y"]` |Date in the format `%d/%m/%y` (`0` padding) |`23/01/21`, ... | +|`augend.date.alias["%m/%d"]` |Date in the format `%m/%d` (`0` padding) |`01/04`, `02/28`, `12/25`, ... | +|`augend.date.alias["%-m/%-d"]` |Date in the format `%-m/%-d` (no paddings) |`1/4`, `2/28`, `12/25`, ... | +|`augend.date.alias["%Y-%m-%d"]` |Date in the format `%Y-%m-%d` (`0` padding) |`2021-01-04`, ... | +|`augend.date.alias["%d.%m.%Y"]` |Date in the format `%d.%m.%Y` (`0` padding) |`23.01.2021`, ... | +|`augend.date.alias["%d.%m.%y"]` |Date in the format `%d.%m.%y` (`0` padding) |`23.01.21`, ... | +|`augend.date.alias["%d.%m."]` |Date in the format `%d.%m.` (`0` padding) |`04.01.`, `28.02.`, `25.12.`, ... | +|`augend.date.alias["%-d.%-m."]` |Date in the format `%-d.%-m.` (no paddings) |`4.1.`, `28.2.`, `25.12.`, ... | +|`augend.date.alias["%Y年%-m月%-d日"]` |Date in the format `%Y年%-m月%-d日` (no paddings)|`2021年1月4日`, ... | +|`augend.date.alias["%Y年%-m月%-d日(%ja)"]`|Date in the format `%Y年%-m月%-d日(%ja)` |`2021年1月4日(月)`, ... | +|`augend.date.alias["%H:%M:%S"]` |Time in the format `%H:%M:%S` |`14:30:00`, ... | +|`augend.date.alias["%H:%M"]` |Time in the format `%H:%M` |`14:30`, ... | +|`augend.constant.alias.de_weekday` |German weekday |`Mo`, `Di`, ..., `Sa`, `So` | +|`augend.constant.alias.de_weekday_full` |German full weekday |`Montag`, `Dienstag`, ..., `Sonntag`| +|`augend.constant.alias.ja_weekday` |Japanese weekday |`月`, `火`, ..., `土`, `日` | +|`augend.constant.alias.ja_weekday_full` |Japanese full weekday |`月曜日`, `火曜日`, ..., `日曜日` | +|`augend.constant.alias.bool` |elements in boolean algebra (`true` and `false`) |`true`, `false` | +|`augend.constant.alias.alpha` |Lowercase alphabet letter (word) |`a`, `b`, `c`, ..., `z` | +|`augend.constant.alias.Alpha` |Uppercase alphabet letter (word) |`A`, `B`, `C`, ..., `Z` | +|`augend.semver.alias.semver` |Semantic version |`0.3.0`, `1.22.1`, `3.9.1`, ... | + + +If you don't specify any settings, the following augends is set as the value of the `default` group. + +* `augend.integer.alias.decimal` +* `augend.integer.alias.hex` +* `augend.date.alias["%Y/%m/%d"]` +* `augend.date.alias["%Y-%m-%d"]` +* `augend.date.alias["%m/%d"]` +* `augend.date.alias["%H:%M"]` +* `augend.constant.alias.ja_weekday_full` + +## Changelog + +See [HISTORY](./HISTORY.md). + +## Testing + +This plugin uses `PlenaryBustedDirectory` in [`plenary.nvim`](https://github.com/nvim-lua/plenary.nvim). diff --git a/etc/soft/nvim/+plugins/dial.nvim/README_ja.md b/etc/soft/nvim/+plugins/dial.nvim/README_ja.md new file mode 100644 index 0000000..5ab0687 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/README_ja.md @@ -0,0 +1,354 @@ +# dial.nvim + +**NOTICE: 本プラグインはまだ開発段階であり、事前告知なくインターフェースが変更となることがあります。** + +## 旧バージョン (v0.2.0) を使っていた人へ + +2022/02/20 に v0.3.0 がリリースされ、既存のインターフェースとの互換性がなくなりました。 +以前のバージョン向けの設定を行っていた方は、[TROUBLESHOOTING.md](./TROUBLESHOOTING_ja.md) を参考に再設定を行ってください。 + +## 概要 + +[Neovim](https://github.com/neovim/neovim) の数値増減機能を拡張する Lua 製プラグイン。 +既存の `` や `` コマンドを拡張し、数値以外も増減・トグルできるようにします。 + +![demo.gif](https://github.com/monaqa/dial.nvim/wiki/fig/dial-demo-2.gif) + +## 特徴 + +* 数値をはじめとする様々なものの増減 + * n 進数 (`2 <= n <= 36`) の整数 + * 日付・時刻 + * キーワードや演算子など、所定文字列のトグル + * `true` ⇄ `false` + * `&&` ⇄ `||` + * `a` ⇄ `b` ⇄ ... ⇄ `z` + * `日` ⇄ `月` ⇄ ... ⇄ `土` ⇄ `日` ⇄ ... + * Hex color + * SemVer +* VISUAL mode での `` / `` / `g` / `g` に対応 +* 増減対象の柔軟な設定 + * 特定のファイルタイプでのみ有効なルールの設定 + * VISUAL モードでのみ有効なルールの設定 +* カウンタに対応 +* ドットリピートに対応 + +## 類似プラグイン + +* [tpope/vim-speeddating](https://github.com/tpope/vim-speeddating) +* [Cycle.vim](https://github.com/zef/vim-cycle) +* [AndrewRadev/switch.vim](https://github.com/AndrewRadev/switch.vim) + +## インストール + +本プラグインには Neovim 0.5.0 以上が必要です(Neovim 0.6.1 以降を推奨)。 + +好きなパッケージマネージャの指示に従うことでインストールできます。 + +## 使用方法 + +本プラグインはデフォルトではキーマッピングを設定/上書きしません。 +本プラグインを有効にするには、いずれかのキーに以下のような割り当てを行う必要があります。 + +```vim +nmap (dial-increment) +nmap (dial-decrement) +vmap (dial-increment) +vmap (dial-decrement) +vmap g g(dial-increment) +vmap g g(dial-decrement) +``` + +または Lua 上で以下のように設定することもできます。 + +```lua +vim.api.nvim_set_keymap("n", "", require("dial.map").inc_normal(), {noremap = true}) +vim.api.nvim_set_keymap("n", "", require("dial.map").dec_normal(), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").inc_visual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").dec_visual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "g", require("dial.map").inc_gvisual(), {noremap = true}) +vim.api.nvim_set_keymap("v", "g", require("dial.map").dec_gvisual(), {noremap = true}) +``` + +## 設定方法 + +dial.nvim では操作対象を表す**被加数** (augend) と、複数の被加数をまとめた**グループ**を用いることで、増減させるルールを自由に設定することができます。 + +```lua +local augend = require("dial.augend") +require("dial.config").augends:register_group{ + -- グループ名を指定しない場合に用いられる被加数 + default = { + augend.integer.alias.decimal, -- nonnegative decimal number (0, 1, 2, 3, ...) + augend.integer.alias.hex, -- nonnegative hex number (0x01, 0x1a1f, etc.) + augend.date.alias["%Y/%m/%d"], -- date (2022/02/19, etc.) + }, + + -- `mygroup` というグループ名を使用した際に用いられる被加数 + mygroup = { + augend.integer.alias.decimal, + augend.constant.alias.bool, -- boolean value (true <-> false) + augend.date.alias["%m/%d/%Y"], -- date (02/19/2022, etc.) + } +} +``` + +* `"dial.config"` モジュールに存在する `augends:register_group` 関数を用いてグループを定義することができます。 + 関数の引数には、グループ名をキー、被加数のリストを値とする辞書を指定します。 + +* 上の例で `augend` という名前のローカル変数に代入されている `"dial.augend"` モジュールでは、さまざまな被加数が定義されています。 + +以下のように **expression register** ([`:h @=`](https://neovim.io/doc/user/change.html#quote_=)) を用いると、増減対象のグループを指定できます。 + +``` +"=mygroup +``` + +増減のたびに expression register を指定するのが面倒であれば、以下のようにマッピングすることも可能です。 + +```vim +nmap a "=mygroup(dial-increment) +``` + +また、 Lua 上で以下のように記述すれば expression register を使わずにマッピングを設定できます。 + +```lua +vim.api.nvim_set_keymap("n", "a", require("dial.map").inc_normal("mygroup"), {noremap = true}) +``` + +expression register などでグループ名を指定しなかった場合、`default` グループにある被加数がかわりに用いられます。 + +### 設定例 + +```vim +lua << EOF +local augend = require("dial.augend") +require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + }, + typescript = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.constant.new{ elements = {"let", "const"} }, + }, + visual = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + augend.constant.alias.alpha, + augend.constant.alias.Alpha, + }, +} + +-- VISUAL モードでの被加数を変更する +vim.api.nvim_set_keymap("v", "", require("dial.map").inc_normal("visual"), {noremap = true}) +vim.api.nvim_set_keymap("v", "", require("dial.map").dec_normal("visual"), {noremap = true}) +EOF + +" 特定のファイルタイプでのみ有効にする +autocmd FileType typescript lua vim.api.nvim_buf_set_keymap(0, "n", "", require("dial.map").inc_normal("typescript"), {noremap = true}) +autocmd FileType typescript lua vim.api.nvim_buf_set_keymap(0, "n", "", require("dial.map").dec_normal("typescript"), {noremap = true}) +``` + +## 被加数の種類と一覧 + +以下簡単のため、 `augend` という変数は以下のように定義されているものとします。 + +```lua +local augend = require("dial.augend") +``` + +### 整数 + +n 進数の整数 (`2 <= n <= 36`) を表します。 `augend.integer.new{ ...opts }` で使用できます。 + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.integer.new{ + radix = 16, + prefix = "0x", + natural = true, + case = "upper", + }, + }, +} +``` + +### 日付 + +日付や時刻を表します。 + +```lua +require("dial.config").augends:register_group{ + default = { + -- date with format `yyyy/mm/dd` + augend.date.new{ + pattern = "%Y/%m/%d", + default_kind = "day", + -- if true, it does not match dates which does not exist, such as 2022/05/32 + only_valid = true, + -- if true, it only matches dates with word boundary + word = false, + }, + }, +} +``` + +`pattern` で指定する文字列には、以下のエスケープシーケンスを使用できます。 + +|文字列|意味 | +|-----|-------------------------------------------------------------| +|`%Y` |4桁の西暦。 (e.g. `2022`) | +|`%y` |西暦の下2桁。上2桁は `20` として解釈されます。 (e.g. `22`) | +|`%m` |2桁の月。 (e.g. `09`) | +|`%d` |2桁の日。 (e.g. `28`) | +|`%H` |24時間で表示した2桁の時間。 (e.g. `15`) | +|`%I` |12時間で表示した2桁の時間。 (e.g. `03`) | +|`%M` |2桁の分。 (e.g. `05`) | +|`%S` |2桁の秒。 (e.g. `08`) | +|`%-y`|西暦の下2桁を1–2桁で表したもの。(e.g. `9` で `2009` 年を表す)| +|`%-m`|1–2桁の月。 (e.g. `9`) | +|`%-d`|1–2桁の日。 (e.g. `28`) | +|`%-H`|24時間で表示した1–2桁の時間。 (e.g. `15`) | +|`%-I`|12時間で表示した1–2桁の時間。 (e.g. `3`) | +|`%-M`|1–2桁の分。 (e.g. `5`) | +|`%-S`|1–2桁の秒。 (e.g. `8`) | +|`%a` |英語表記の短い曜日。 (`Sun`, `Mon`, ..., `Sat`) | +|`%A` |英語表記の曜日。 (`Sunday`, `Monday`, ..., `Saturday`) | +|`%b` |英語表記の短い月名。 (`Jan`, ..., `Dec`) | +|`%B` |英語表記の月名。 (`January`, ..., `December`) | +|`%p` |`AM` または `PM`。 | +|`%J` |日本語表記の曜日。 (`日`, `月`, ..., `土`) | + +### 定数 + +キーワードなどの決められた文字列をトグルします。 `augend.constant.new{ ...opts }` で使用できます。 + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.constant.new{ + elements = {"and", "or"}, + word = true, -- if false, "sand" is incremented into "sor", "doctor" into "doctand", etc. + cyclic = true, -- "or" is incremented into "and". + }, + augend.constant.new{ + elements = {"&&", "||"}, + word = false, + cyclic = true, + }, + }, +} +``` + +### hex color + +`#000000` や `#ffffff` といった形式の RGB カラーコードを増減します。 `augend.hexcolor.new{ ...opts }` で使用できます。 + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.hexcolor.new{ + case = "lower", + }, + }, +} +``` + +### SemVer + +Semantic version を増減します。後述のエイリアスを用います。 +単なる非負整数のインクリメントとは以下の点で異なります。 + +- semver 文字列よりもカーソルが手前にあるときは、パッチバージョンが優先してインクリメントされます。 +- マイナーバージョンの値が増加したとき、パッチバージョンの値は0にリセットされます。 +- メジャーバージョンの値が増加したとき、マイナー・パッチバージョンの値は0にリセットされます。 + +### カスタム + +ユーザ自身が増減ルールを定義したい場合には `augend.user.new{ ...opts }` を使用できます。 + +```lua +require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.user.new{ + find = require("dial.augend.common").find_pattern("%d+"), + add = function(text, addend, cursor) + local n = tonumber(text) + n = math.floor(n * (2 ^ addend)) + text = tostring(n) + cursor = #text + return {text = text, cursor = cursor} + end + }, + }, +} +``` + +### エイリアス + +エイリアスはライブラリで予め定義された被加数です。 `new` 関数を用いることなく、そのまま使用できます。 + +```lua +require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.alias["%Y/%m/%d"], + }, +} +``` + +エイリアスとして提供されている被加数は以下の通りです。 + +|Alias Name |Explanation |Examples | +|------------------------------------------|-------------------------------------------------|-----------------------------------| +|`augend.integer.alias.decimal` |decimal natural number |`0`, `1`, ..., `9`, `10`, `11`, ...| +|`augend.integer.alias.decimal_int` |decimal integer (including negative number) |`0`, `314`, `-1592`, ... | +|`augend.integer.alias.hex` |hex natural number |`0x00`, `0x3f3f`, ... | +|`augend.integer.alias.octal` |octal natural number |`0o00`, `0o11`, `0o24`, ... | +|`augend.integer.alias.binary` |binary natural number |`0b0101`, `0b11001111`, ... | +|`augend.date.alias["%Y/%m/%d"]` |Date in the format `%Y/%m/%d` (`0` padding) |`2021/01/23`, ... | +|`augend.date.alias["%m/%d/%Y"]` |Date in the format `%m/%d/%Y` (`0` padding) |`23/01/2021`, ... | +|`augend.date.alias["%d/%m/%Y"]` |Date in the format `%d/%m/%Y` (`0` padding) |`01/23/2021`, ... | +|`augend.date.alias["%m/%d/%y"]` |Date in the format `%m/%d/%y` (`0` padding) |`01/23/21`, ... | +|`augend.date.alias["%d/%m/%y"]` |Date in the format `%d/%m/%y` (`0` padding) |`23/01/21`, ... | +|`augend.date.alias["%m/%d"]` |Date in the format `%m/%d` (`0` padding) |`01/04`, `02/28`, `12/25`, ... | +|`augend.date.alias["%-m/%-d"]` |Date in the format `%-m/%-d` (no paddings) |`1/4`, `2/28`, `12/25`, ... | +|`augend.date.alias["%Y-%m-%d"]` |Date in the format `%Y-%m-%d` (`0` padding) |`2021-01-04`, ... | +|`augend.date.alias["%Y年%-m月%-d日"]` |Date in the format `%Y年%-m月%-d日` (no paddings)|`2021年1月4日`, ... | +|`augend.date.alias["%Y年%-m月%-d日(%ja)"]`|Date in the format `%Y年%-m月%-d日(%ja)` |`2021年1月4日(月)`, ... | +|`augend.date.alias["%H:%M:%S"]` |Time in the format `%H:%M:%S` |`14:30:00`, ... | +|`augend.date.alias["%H:%M"]` |Time in the format `%H:%M` |`14:30`, ... | +|`augend.constant.alias.ja_weekday` |Japanese weekday |`月`, `火`, ..., `土`, `日` | +|`augend.constant.alias.ja_weekday_full` |Japanese full weekday |`月曜日`, `火曜日`, ..., `日曜日` | +|`augend.constant.alias.bool` |elements in boolean algebra (`true` and `false`) |`true`, `false` | +|`augend.constant.alias.alpha` |Lowercase alphabet letter (word) |`a`, `b`, `c`, ..., `z` | +|`augend.constant.alias.Alpha` |Uppercase alphabet letter (word) |`A`, `B`, `C`, ..., `Z` | +|`augend.semver.alias.semver` |Semantic version |`0.3.0`, `1.22.1`, `3.9.1`, ... | + +何も設定しなかった場合は以下の被加数が `default` グループの値としてセットされます。 + +- `augend.integer.alias.decimal` +- `augend.integer.alias.hex` +- `augend.date.alias["%Y/%m/%d"]` +- `augend.date.alias["%Y-%m-%d"]` +- `augend.date.alias["%m/%d"]` +- `augend.date.alias["%H:%M"]` +- `augend.constant.alias.ja_weekday_full` + +## 更新履歴 + +[HISTORY](./HISTORY.md) を参照。 + +## Testing + +[`plenary.nvim`](https://github.com/nvim-lua/plenary.nvim) の `PlenaryBustedDirectory` を用いています。 diff --git a/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING.md b/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING.md new file mode 100644 index 0000000..c3c4573 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING.md @@ -0,0 +1,61 @@ +# Troubleshooting + +## Upgrading from v0.2.0 to v0.3.0 + +With the update from 0.2.0 to 0.3.0, new features and augend have been implemented, +and at the same time, the configuration scripts are no longer compatible. You need to rewrite it. + +Here is an example of rewriting the configuration. + +* Example settings (old) + ```lua + local dial = require("dial") + + dial.config.searchlist.normal = { + "number#decimal", + "date#[%m/%d]", + "char#alph#small#word", + } + ``` + +* Example settings (new) + ```lua + local augend = require("dial.augend") + + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.date.alias["%m/%d"], + augend.constant.alias.alpha, + }, + } + ``` + + +### Correspondence of augend names + +|Old (v0.2.0) |New (v0.3.0) | +|----------------------------|------------------------------------------| +|`number#decimal` |`augend.integer.alias.decimal` | +|`number#decimal#int` |`augend.integer.alias.decimal` | +|`number#decimal#fixed#zero` |not implemented | +|`number#decimal#fixed#space`|not implemented | +|`number#hex` |`augend.integer.alias.hex` | +|`number#octal` |`augend.integer.alias.octal` | +|`number#binary` |`augend.integer.alias.binary` | +|`date#[%Y/%m/%d]` |`augend.date.alias["%Y/%m/%d"]` | +|`date#[%m/%d]` |`augend.date.alias["%m/%d"]` | +|`date#[%-m/%-d]` |`augend.date.alias["%-m/%-d"]` | +|`date#[%Y-%m-%d]` |`augend.date.alias["%Y-%m-%d"]` | +|`date#[%Y年%-m月%-d日]` |`augend.date.alias["%Y年%-m月%-d日"]` | +|`date#[%Y年%-m月%-d日(%ja)]`|`augend.date.alias["%Y年%-m月%-d日(%ja)"]`| +|`date#[%H:%M:%S]` |`augend.date.alias["%H:%M:%S"]` | +|`date#[%H:%M]` |`augend.date.alias["%H:%M"]` | +|`date#[%ja]` |`augend.constant.alias.ja_weekday` | +|`date#[%jA]` |`augend.constant.alias.ja_weekday_full` | +|`char#alph#small#word` |`augend.constant.alias.alpha` | +|`char#alph#capital#word` |`augend.constant.alias.Alpha` | +|`char#alph#small#str` |can be defined with `augend.constant.new` | +|`char#alph#capital#str` |can be defined with `augend.constant.new` | +|`color#hex` |`augend.hexcolor.new{}` | +|`markup#markdown#header` |`augend.misc.alias.markdown_header` | diff --git a/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING_ja.md b/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING_ja.md new file mode 100644 index 0000000..bb0b1ba --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/TROUBLESHOOTING_ja.md @@ -0,0 +1,59 @@ +# トラブルシューティング + +## v0.2.0 から v0.3.0 へのアップデート + +`0.2.0` から `0.3.0` へのアップデートにあたり、新機能や新たな augend が実装されたと同時に設定方法の互換性がなくなりました。 + +以下のように設定を書き換える必要があります。 + +* 設定例(旧) + ```lua + local dial = require("dial") + + dial.config.searchlist.normal = { + "number#decimal", + "date#[%m/%d]", + "char#alph#small#word", + } + ``` + +* 設定例(新) + ```lua + local augend = require("dial.augend") + + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.date.alias["%m/%d"], + augend.constant.alias.alpha, + }, + } + ``` + +### 被加数の新旧対応 + +|旧 |新 | +|----------------------------|------------------------------------------| +|`number#decimal` |`augend.integer.alias.decimal` | +|`number#decimal#int` |`augend.integer.alias.decimal` | +|`number#decimal#fixed#zero` |not implemented | +|`number#decimal#fixed#space`|not implemented | +|`number#hex` |`augend.integer.alias.hex` | +|`number#octal` |`augend.integer.alias.octal` | +|`number#binary` |`augend.integer.alias.binary` | +|`date#[%Y/%m/%d]` |`augend.date.alias["%Y/%m/%d"]` | +|`date#[%m/%d]` |`augend.date.alias["%m/%d"]` | +|`date#[%-m/%-d]` |`augend.date.alias["%-m/%-d"]` | +|`date#[%Y-%m-%d]` |`augend.date.alias["%Y-%m-%d"]` | +|`date#[%Y年%-m月%-d日]` |`augend.date.alias["%Y年%-m月%-d日"]` | +|`date#[%Y年%-m月%-d日(%ja)]`|`augend.date.alias["%Y年%-m月%-d日(%ja)"]`| +|`date#[%H:%M:%S]` |`augend.date.alias["%H:%M:%S"]` | +|`date#[%H:%M]` |`augend.date.alias["%H:%M"]` | +|`date#[%ja]` |`augend.constant.alias.ja_weekday` | +|`date#[%jA]` |`augend.constant.alias.ja_weekday_full` | +|`char#alph#small#word` |`augend.constant.alias.alpha` | +|`char#alph#capital#word` |`augend.constant.alias.Alpha` | +|`char#alph#small#str` |can be defined with `augend.constant.new` | +|`char#alph#capital#str` |can be defined with `augend.constant.new` | +|`color#hex` |`augend.hexcolor.new{}` | +|`markup#markdown#header` |`augend.misc.alias.markdown_header` | diff --git a/etc/soft/nvim/+plugins/dial.nvim/autoload/dial/operator.vim b/etc/soft/nvim/+plugins/dial.nvim/autoload/dial/operator.vim new file mode 100644 index 0000000..220341c --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/autoload/dial/operator.vim @@ -0,0 +1,23 @@ +function dial#operator#increment_normal(type, ...) + lua require("dial.command").operator_normal("increment") +endfunction + +function dial#operator#decrement_normal(type, ...) + lua require("dial.command").operator_normal("decrement") +endfunction + +function dial#operator#increment_visual(type, ...) + lua require("dial.command").operator_visual("increment", false) +endfunction + +function dial#operator#decrement_visual(type, ...) + lua require("dial.command").operator_visual("decrement", false) +endfunction + +function dial#operator#increment_gvisual(type, ...) + lua require("dial.command").operator_visual("increment", true) +endfunction + +function dial#operator#decrement_gvisual(type, ...) + lua require("dial.command").operator_visual("decrement", true) +endfunction diff --git a/etc/soft/nvim/+plugins/dial.nvim/doc/.gitignore b/etc/soft/nvim/+plugins/dial.nvim/doc/.gitignore new file mode 100644 index 0000000..0778641 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/doc/.gitignore @@ -0,0 +1 @@ +tags* diff --git a/etc/soft/nvim/+plugins/dial.nvim/doc/dial.jax b/etc/soft/nvim/+plugins/dial.nvim/doc/dial.jax new file mode 100644 index 0000000..81b24ea --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/doc/dial.jax @@ -0,0 +1,1417 @@ +*dial.txt* CTRL-A / CTRL-X を拡張するLua製プラグイン + +Vimの標準機能である CTRL-A / CTRL-X を拡張し、英数字に限らない様々な文字列を +インクリメント/デクリメントできるようにするパッケージです。 + +Author: Mogami Shinichi (@monaqa) +Version: 0.3.0 +License: MIT license + + +============================================================================== +CONTENTS *dial-contents* + +Requirements |dial-requirements| +Introduction |dial-introduction| + Features |dial-features| +Usage |dial-usage| +Configurations |dial-config| + Aliases |dial-aliases| + Augends |dial-augends| + Number |dial-augends-number| + Date |dial-augends-date| + Constant |dial-augends-constant| + Case |dial-augends-case| + Hexcolor |dial-augends-hexcolor| + Semver |dial-augends-semver| + Paren |dial-augends-paren| + Misc |dial-augends-misc| + User |dial-augends-user| +Advanced Usage |dial-advanced-usage| + Dot Repeating |dial-dot-repeating| +Interface |dial-interface| + Mapping |dial-mapping| + Command |dial-command| + Lua API |dial-lua-api| + +============================================================================== +REQUIREMENTS *dial-requirements* + +* Neovim 0.6.1 or later(0.5.0 以降なら動くと考えられますが保証はしません) + +============================================================================== +INTRODUCTION *dial-introduction* + +*dial.nvim* は、様々な形式の文字列をインクリメント/デクリメントするパッケージ +です。標準の CTRL-A や CTRL-X、 g CTRL-A といったコマンドの動作を拡張します。 + +------------------------------------------------------------------------------ +FEATURES *dial-features* + +多岐にわたる文字列の増減~ + 以下のように、さまざまな文字列を増減できます。 + + * n 進数 (`2 <= n <= 36`) の整数 + * 日付 + * キーワードや演算子など、所定文字列のトグル + * `true` ⇄ `false` + * `&&` ⇄ `||` + * `a` ⇄ `b` ⇄ ... ⇄ `z` + * `日` ⇄ `月` ⇄ ... ⇄ `土` ⇄ `日` ⇄ ... + * 識別子の命名規則 (camelCase, snake_case, etc.) + * Hex color + * SemVer + + カスタムルールに基づく増減ルールを設定することも可能です。 + +増減対象の (augend) の柔軟な設定~ + |dial.nvim| が提供する増減ルールは多岐にわたりますが、ユーザは本当に必要な + ものだけを選択して有効化することができます。 + 競合するルール(例: `mm/dd/yyyy`形式の日付と`dd/mm/yyyy`形式の日付)がある + 場合も、片方だけを有効化することによって取り違えることなく増減操作を行うこ + とができます。 + 特定のバッファやファイルタイプでのみ増減対象を変更することも可能です。 + +カウンタへの対応~ + 通常の CTRL-A / CTRL-X と同様に、 `10` のように打つことで加数を変更で + きます。 + +ドットリピートへの対応~ + 通常の CTRL-A / CTRL-X と同様に、|.| を用いて直前の操作を繰り返せます。 + |dial.nvim| では直前の増減対象を記憶しており、たとえば直前に「`yyyy/mm/dd` + 形式の月」をインクリメントしていた場合は、たとえより手前に他の増減対象文字 + 列があったとしても「`yyyy/mm/dd`形式の月」を探してインクリメントします。 + +VISUAL mode での操作に対応~ + 標準の CTRL-A や CTRL-X は VISUAL モードでも用いることができますが、同様 + の機能を dial.nvim も備えています。 |v_g_CTRL-A| や |v_g_CTRL-X| にも対応 + しています。NORMAL モードとVISUAL モードで増減対象を変えることもできます。 + +============================================================================== +USAGE *dial-usage* + +|dial.nvim|自信はキーマップの設定および上書きを行わないため、プラグインを動作 +させるために以下の設定を追加する必要があります。 +> + nmap (dial-increment) + nmap (dial-decrement) + vmap (dial-increment) + vmap (dial-decrement) + vmap g g(dial-increment) + vmap g g(dial-decrement) +< + +もしくは Lua 上で以下のように設定することもできます。 + +> + vim.api.nvim_set_keymap( + "n", "", require("dial.map").inc_normal(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "n", "", require("dial.map").dec_normal(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").inc_visual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").dec_visual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "g", require("dial.map").inc_gvisual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "g", require("dial.map").dec_gvisual(), {noremap = true} + ) +< + +============================================================================== +CONFIGURATIONS *dial-config* + +dial.nvim では操作対象を表す被加数 (augend) と、複数の被加数をまとめたグループ +を用いることで、増減ルールを自由に設定することができます。 + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + -- グループ名を指定しない場合に用いられる被加数 + default = { + augend.integer.alias.decimal, -- nonnegative decimal number + augend.integer.alias.hex, -- nonnegative hex number + -- date (2022/02/19, etc.) + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + }, + + -- `mygroup` というグループ名を使用した際に用いられる被加数 + mygroup = { + augend.integer.alias.decimal, + augend.constant.alias.bool, -- boolean value (true <-> false) + -- date (02/19/2022, etc.) + augend.date.new { + pattern = "%m/%d/%Y", + default_kind = "day", + }, + } + } +< + +`"dial.config"` モジュールに存在する `augends:register_group` 関数を用いてグル +ープを定義することができます。関数の引数には、グループ名をキー、被加数のリスト +を値とする辞書を指定します。 + +上の例で `augend` という名前のローカル変数に代入されている `"dial.augend"` +モジュールにはさまざまな被加数が定義されています。具体的に定義されている被加数 +とエイリアスについては |dial-augends| および |dial-aliases| を参照。 + +|dial.nvim| では以下のように **expression register** |@=| を用いることで増減対 +象のグループを指定します。 +> + "=mygroup +< + +増減のたびに expression register を指定するのが面倒であれば、以下のようにマッ +ピングすることも可能です。 +> + nmap a "=mygroup(dial-increment) +< + +Lua 上で以下のように記述すれば expression register を使わずマッピングを設定で +きます。 + +> + vim.api.nvim_set_keymap( + "n", + "a", + require("dial.map").inc_normal("mygroup"), + {noremap = true} + ) +< + + *dial-config-augends-default* +デフォルトグループ~ + +`default` は特殊なグループであり、標準で定義されています。expression register +などでグループ名を指定しなかった場合は `default` グループにある被加数がかわり +に用いられます。 + +何も設定しなかった場合は以下の設定を行っているのと等価となります。 + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%Y-%m-%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%m/%d", + default_kind = "day", + only_valid = true, + }, + augend.date.new { + pattern = "%H:%M", + default_kind = "day", + only_valid = true, + }, + augend.constant.alias.ja_weekday_full, + }, + } +< + +VISUAL mode 限定のマッピング~ + +VISUAL モード用のグループを作成することで可能です。 + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + visual = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + augend.constant.alias.alpha, + augend.constant.alias.Alpha, + }, + } + + -- VISUAL モードでの被加数を変更する + vim.api.nvim_set_keymap( + "v", "", require("dial.map").inc_normal("visual"), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").dec_normal("visual"), {noremap = true} + ) +< + +特定のファイルタイプ限定のマッピング~ + +特定のファイルタイプ用のグループを作成し、 |autocmd| でバッファごとにキーマッ +ピングを設定することで実現できます。 + +> + lua << EOF + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + typescript = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.constant.new{ elements = {"let", "const"} }, + }, + } + EOF + + " 特定のファイルタイプでのみ有効にする + autocmd FileType typescript nmap "=typescript(dial-increment) + autocmd FileType typescript nmap "=typescript(dial-decrement) +< + +------------------------------------------------------------------------------ +ALIASES *dial-aliases* + +被加数のエイリアスとして、デフォルトで以下が提供されています。 +詳しくは |dial-augends| の対応する節を参照。 + + integer: + `augend.integer.alias.decimal` + `augend.integer.alias.decimal_int` + `augend.integer.alias.hex` + `augend.integer.alias.octal` + `augend.integer.alias.binary` + + date: + `augend.date.alias["%Y/%m/%d"]` + `augend.date.alias["%m/%d/%Y"]` + `augend.date.alias["%d/%m/%Y"]` + `augend.date.alias["%m/%d/%y"]` + `augend.date.alias["%d/%m/%y"]` + `augend.date.alias["%m/%d"]` + `augend.date.alias["%-m/%-d"]` + `augend.date.alias["%Y-%m-%d"]` + `augend.date.alias["%Y年%-m月%-d日"]` + `augend.date.alias["%Y年%-m月%-d日(%ja)"]` + `augend.date.alias["%H:%M:%S"]` + `augend.date.alias["%H:%M"]` + + constant: + `augend.constant.alias.ja_weekday` + `augend.constant.alias.ja_weekday_full` + `augend.constant.alias.bool` + `augend.constant.alias.alpha` + `augend.constant.alias.Alpha` + + semver: + `augend.semver.alias.semver` + +NOTE: date の alias は deprecated であり、今後のアップデートで削除される可能性 +があります。 + +------------------------------------------------------------------------------ +AUGENDS *dial-augends* + +以下簡単のため、 `augend` という変数は以下のように定義されているものとします。 + +> + local augend = require("dial.augend") +< + +NUMBER *dial-augends-number* +------ + +n 進数の整数 (`2 <= n <= 36`) を表します。 `augend.integer.new{ ...opts }` で +使用できます。 + +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.integer.new{ + radix = 16, + prefix = "0x", + natural = true, + case = "upper", + }, + }, + } +< + +`augend.integer.new` の引数のテーブルは以下のキーをとることができます。 + radix (number, default: 10) + 表記に用いる進数を表します。 + prefix (string, default: "") + 数値に前置される文字列を指定できます (e.g. `0x`)。 + natural (boolean, default: true) + true のときは自然数にのみマッチします。 + つまり、 `-1` のうえにカーソルがあったとしても `1` にマッチし、 + `-1` にはマッチしません。 + case ("upper" | "lower", default: "lower") + 16進数のように10よりも大きな進数のときに用いるアルファベットの + 大文字/小文字を指定します。 + delimiter (char, default: "") + 数値の区切り文字を指定します。たとえば "," を与えると、以下の + ような数値にもマッチするようになります。 +> + 1,234,567 + 1,23,4,5,67 +< + いずれも数値を解釈するときは単にカンマが無視されますが、増減時 + にカンマの位置が n 桁区切りとなるよう整形されます。n の値は後 + 述する `delimiter_digits` の値で制御できます。 + delimiter_digits (number, default: 3) + 数値の区切り文字が指定されているとき、数値を何桁区切りで表示す + るか指定します。 + `delimiter` が空文字列のときには意味がありません。 + +augend.integer.alias.decimal~ + +十進数の整数。 +> + 0 + 123 + 3141592 +< +といった文字列にマッチし、1つインクリメントするごとにその数値を1増加させます。 +標準の CTRL-A と異なり、数字の先頭に `-` が付いていても考慮しません。 +> + file-1.jpg +< +という文字列の `1` にカーソルがある状態で |(dial-increment)| を行うと +> + file-2.jpg +< +となります。 + +augend.integer.alias.decimal_int~ + +基本的には `augend.integer.alias.decimal` と同じですが、こちらは負の整数にも +マッチします。 +> + file-1.jpg +< +という文字列の `1` にカーソルがある状態で |(dial-increment)| を行うと +> + file0.jpg +< +となります。 + +augend.integer.alias.hex~ + +16進数の非負整数にマッチし、16進数の表記でインクリメント・デクリメントが行われ +ます。 +> + 0x12 + 0xafbf + 0xAFBF +< +などの文字列にマッチします。 + +augend.integer.alias.octal~ + +8進数の非負整数にマッチし、8進数の表記でインクリメント・デクリメントが行われま +す。 +> + 0o37 + 0o567 + 0o01212 +< +などの文字列にマッチします。 + +augend.integer.alias.binary~ + +2進数の非負整数にマッチし、2進数の表記でインクリメント・デクリメントが行われま +す。 +> + 0b00 + 0b1010 + 0b10100101 +< +などの文字列にマッチします。 + +DATE *dial-augends-date* +----- + +日付や時刻を表します。`augend.date.new{ ...opts }` で使用できます。 + +> + require("dial.config").augends:register_group{ + default = { + augend.date.new{ + pattern = "%Y/%m/%d", + default_kind = "day", + only_valid = true, + word = false, + }, + }, + } +< + + *dial-augends-date-arguments* +`augend.date.new` の引数のテーブルは以下のキーをとることができます。 + pattern (string, 必須) + マッチさせたい日付のパターン。詳細は |dial-augends-date-pattern| + 参照。 + default_kind (string, 必須) + カーソル位置が日付から離れている場合、または選択モード中で増減操作 + を行うとき、日付をどの単位で増減させるかを表します。以下の6種類の + 値から選択します: + * `year` + * `month` + * `day` + * `hour` + * `min` + * `sec` + only_valid (boolean, default: false) + true のとき、フォーマットには合致するものの現実には存在しない日付 + や時刻(`2022/13/52` や `27:30`など)にはマッチしなくなります。意 + 図せぬマッチを防ぐのに役立ちます。 + false のとき、現実には存在しない日付にもマッチし、増減の際に現実に + 存在する日付へと強制的に修正します。たとえば `2022/06/31` の状態で + 日付を1だけ増加させると、もともとの文字列は `2022/07/01` という日 + 付と解釈され、最終的に `2022/07/02` となります。 + word (boolean, default: false) + true のとき、フォーマットに一致する文字列が単語の境界にあるときの + みマッチします。 + clamp (boolean, default: false) + 月や年をインクリメントすることによって日付がその月の末日を超えると + きにどう扱うかを制御する。true ならば末日での切り詰めを行う。 + |dial-augends-date-clamp| 参照。 + end_sensitive (boolean, default: false) + 月や年をインクリメントする際に、月の末日のみ特別扱いするかどうかを + 制御する。true ならば、末日を特別扱いして扱う。 + |dial-augends-date-end-sensitive| 参照。 + custom_date_elements (table, default: {}) + ユーザが自分自身で日付の形式を定義する際に用いる発展的なオプション。 + 詳細は |dial-augends-date-custom-format| を参照。 + + *dial-augends-date-pattern* +`pattern` 引数には以下のような文字列を指定することができます。 +> + %Y/%m/%d (e.g. 2022/02/06) + %H:%M:%S (e.g. 15:30:00) + %-m/%-d (e.g. 2/6) +< +`%` は日付の要素を表すために用いられるエスケープ文字であり、以下のようにして用 +いることができます。 + +* `%` とアルファベット1文字。 + `%Y` 4桁の西暦。 (e.g. `2022`) + `%y` 西暦の下2桁。上2桁は `20` として解釈されます。 (e.g. `22`) + `%m` 2桁の月。 (e.g. `09`) + `%d` 2桁の日。 (e.g. `28`) + `%H` 24時間で表示した2桁の時間。 (e.g. `15`) + `%I` 12時間で表示した2桁の時間。 (e.g. `03`) + `%M` 2桁の分。 (e.g. `05`) + `%S` 2桁の秒。 (e.g. `08`) + `%a` 英語表記の短い曜日。 (`Sun`, `Mon`, ..., `Sat`) + `%A` 英語表記の曜日。 (`Sunday`, `Monday`, ..., `Saturday`) + `%b` 英語表記の短い月名。 (`Jan`, ..., `Dec`) + `%B` 英語表記の月名。 (`January`, ..., `December`) + `%p` `AM` または `PM`。 + `%J` 日本語表記の曜日。 (`日`, `月`, ..., `土`) +* `%-` とアルファベット1文字。 + `%-y` 西暦の下2桁を1–2桁で表したもの。(e.g. `9` で `2009` 年を表す) + `%-m` 1–2桁の月。 (e.g. `9`) + `%-d` 1–2桁の日。 (e.g. `28`) + `%-H` 24時間で表示した1–2桁の時間。 (e.g. `15`) + `%-I` 12時間で表示した1–2桁の時間。 (e.g. `3`) + `%-M` 1–2桁の分。 (e.g. `5`) + `%-S` 1–2桁の秒。 (e.g. `8`) +* `%%`。これは `%` 自体を表現するのに用います。 +* `%(element_name)` という形式のテキスト (|dial-augends-date-custom-format|)。 + `element_name` には `)` という文字が出現しない任意の文字列を入れることができ + ます。 + +NOTE パターンには日付または時刻に関する情報を含める必要があり、曜日単体で使う +ことはできません。たとえば `%a` をパターンとして指定しても期待通りの動作をしま +せん。曜日だけでは日付を推定できないためです。 + + *dial-augends-date-custom-format* +日付のパターンにおいて、`%(element_name)` はユーザの手で定義できる特殊なプレー +スホルダーです。プレースホルダーは `augend.date.new` の `custom_date_elements` +引数で以下のように与えることができます。 + +> + local WEEKDAYS_FLUSHRIGHT = { + " Sunday", + " Monday", + " Tuesday", + "Wednesday", + " Thursday", + " Friday", + " Saturday", + } + require("dial.config").augends:register_group { + default = { + augend.date.new { + pattern = "%d/%m/%y %(week_flushright)", + default_kind = "day", + custom_date_elements = { + week_flushright = { + kind = nil, + regex = common.enum_to_regex(WEEKDAYS_FLUSHRIGHT), + update_date = function(_, date) + return date + end, + format = function(time) + local wday = os.date("*t", time).wday + return WEEKDAYS_FLUSHRIGHT[wday] + end, + } + } + } + }, + } +< + +上の例では `week_flushright` という日付の要素を定義し、`%(week_flushright)` と +してパターン内で使用しています。その結果このルールは +> + 18/10/22 Tuesday +< +にマッチし、日付の上で を実行することで +> + 19/10/22 Wednesday +< +へとインクリメントできるようになります。 + +日付の要素に求められるフィールドは以下のとおりです。 + kind (string | nil, default: nil) + その要素が日付のどの情報に紐付いているか。 + * `year` + * `month` + * `day` + * `hour` + * `min` + * `sec` + regex (string, 必須) + その要素を検索する際のパターン文字列。 + update_date (function, 必須) + その要素を読んだことによって、日付がどのようにアップデートされるか。 + 1つ目の引数にはプレースホルダーにマッチした文字列、2つ目の引数には + アップデート前の日付の情報 (`os.date("*t")` と同形式のテーブル) が + 渡されます。 + format (function, 必須) + 日付(時刻)が与えられたとき、その要素をどのように文字列に変換する + か。1つ目の引数には unix time が integer で渡されます。 + +非常に柔軟にパターンを定義できるものの、設定が複雑になるため可能な限り `%a` や +`%y` といった既存の要素を用いることが推奨されます。 + + *dial-augends-date-clamp* + *dial-augends-date-end-sensitive* +「1ヶ月後」「1年後」のような増減操作を行うとき、それが表す日付は自明ではない場 +合があります。たとえば `yyyy-mm-dd` 形式で `2022-01-31` という日付を考えると、 +その1ヶ月後は31日を加算した `2022-03-03` だと考える人もいるかもしれませんし、 +「1月の次の月の末日」である `2022-02-28` だと考える人もいるでしょう。様々なケ +ースに対応するため、 augend.date には `clamp` と `end_sensitive` という2種類の +オプションが用意されています。 + +具体例として、 `2022-01-31` の1ヶ月後を考えてみます。素直に考えれば +`2022-02-31` ですが、残念ながらこの日付は存在しません。`clamp = false` のとき +は、次の月に繰り越すことで存在する日付へと修正します。 +> + 2022-01-31 --(+1 month)--> 2022-02-31 (that does not exist) + ~~> 2022-03-03 (FINAL RESULT) +< +一方で`clamp = true` のときは、月を保ちつつ日付を切り詰めることで修正します。 +> + 2022-01-31 --(+1 month)--> 2022-02-31 (that does not exist) + ~~> 2022-02-28 (FINAL RESULT) +< + +`end_sensitive` オプションを使うと、末日を特別扱いすることができます。 +`end_sensitive = true` のとき、増減対象の日付がある月の末日だった場合、年月を +インクリメントすると `clamp` にかかわらず強制的にその月の末日へと変換されます。 +よって +> + 2022-01-31 --> 2022-02-28 +< +となります。 + +`clamp` と `end_sensitive` はよく似たオプションですが、動作が微妙に異なります。 +自分のユースケースに最も合った値をそれぞれ選択してください。 + +* `clamp = false, end_sensitive = false` のとき +> + 2022-01-30 --(+1 month)--> 2022-03-02 + 2022-01-31 --(+1 month)--> 2022-03-03 + 2022-02-28 --(+1 month)--> 2022-03-28 + 2022-03-31 --(-1 month)--> 2022-03-03 +< +* `clamp = true, end_sensitive = false` のとき +> + 2022-01-30 --(+1 month)--> 2022-02-28 # Clamped! + 2022-01-31 --(+1 month)--> 2022-02-28 # Clamped! + 2022-02-28 --(+1 month)--> 2022-03-28 + 2022-03-31 --(-1 month)--> 2022-02-28 # Clamped! +< +* `clamp = false, end_sensitive = true` のとき +> + 2022-01-30 --(+1 month)--> 2022-03-02 + 2022-01-31 --(+1 month)--> 2022-02-28 # Last day of the month! + 2022-02-28 --(+1 month)--> 2022-03-31 # Last day of the month! + 2022-03-31 --(-1 month)--> 2022-02-28 # Last day of the month! +< +* `clamp = true, end_sensitive = true` のとき +> + 2022-01-30 --(+1 month)--> 2022-02-28 # Clamped! + 2022-01-31 --(+1 month)--> 2022-02-28 # Last day of the month! + 2022-02-28 --(+1 month)--> 2022-03-31 # Last day of the month! + 2022-03-31 --(-1 month)--> 2022-02-28 # Last day of the month! +< + + *dial-augends-date-aliases* +`augend.date` には以下のエイリアスが用意されています。 + +NOTE: 以下のエイリアスは、日付のフォーマットを指定するのが困難であった時代の名 +残です。現在はフォーマットを自由に指定できるため、エイリアスの意義が失われつつ +あります。互換性のため現在は残していますが、将来的には削除予定です。 + +augend.date.alias["%Y/%m/%d"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 1970/12/31 + 2021/01/01 + 2021/02/13 +< + +カーソルが当該フォーマットより左にある場合は日付単位でのインクリメントが行われ +ますが、カーソルがフォーマット上にある場合、具体的なカーソルの位置によってイン +クリメントの対象が変化します。 + +`1970/12/31` を例に取ってみます。 + +`1970` の4文字のいずれかの上にカーソルがあるときにインクリメントを行うと +> + 1971/12/31 +< +のように年がインクリメントされます。 + +`/12` のいずれかの上にカーソルがあるときにインクリメントを行うと +> + 1971/01/31 +< +のように月がインクリメントされます。 + +`/31` のいずれかの上にカーソルがあるときにインクリメントを行うと +> + 1971/01/01 +< +のように日付がインクリメントされます。 + +当該フォーマットでは +> + 2021/02/29 + 2021/14/59 +< +のように現実には存在しない日付でもマッチするものの、インクリメントを行う際に現 +実に存在する日付へと強制的に修正されます。 + +augend.date.alias["%m/%d/%Y"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 12/31/1970 + 01/01/2021 + 02/13/2021 +< +意図しないマッチを避けるため、存在しない日付にはマッチしません。 + +augend.date.alias["%d/%m/%Y"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 31/12/1970 + 01/01/2021 + 13/02/2021 +< +意図しないマッチを避けるため、存在しない日付にはマッチしません。 + +augend.date.alias["%m/%d/%y"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 12/31/70 + 01/01/21 + 02/13/21 +< +意図しないマッチを避けるため、存在しない日付にはマッチしません。 + +augend.date.alias["%d/%m/%y"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 31/12/19 + 01/01/20 + 13/02/20 +< +意図しないマッチを避けるため、存在しない日付にはマッチしません。 + +augend.date.alias["%m/%d"]~ + +以下のような日付にマッチします。 + +> + 12/31 + 01/01 + 02/13 +< + +`12/31` の例の場合、 + +* カーソルが `12` より左にある場合:日付 +* カーソルが `12` のいずれかの上にある場合:月 +* カーソルが `/31` のいずれかの上にある場合:日付 + +がインクリメントされます。 + +現実に存在しない日付でもマッチし、操作の際に現実に存在する日付へと強制的に修正 +されます。 + +augend.date.alias["%-m/%-d"]~ + +基本的に |dial-augends-date#[%m/%d]| と同様ですが、こちらは月日が2桁にパディン +グされていなくてもマッチします。すなわち、 +> + 12/31 + 1/1 + 2/13 +< +などにマッチします。 +意図しないマッチを避けるため、存在しない日付にはマッチしません。 + +augend.date.alias["%Y-%m-%d"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 1970-12-31 + 2021-01-01 + 2021-02-13 +< +現実に存在しない日付でもマッチし、操作の際に現実に存在する日付へと強制的に修正 +されます。 + +augend.date.alias["%Y年%-m月%-d日"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 1970年12月31日 + 2021年1月1日 + 2021年2月13日 +< + +augend.date.alias["%Y年%-m月%-d日(%ja)"]~ + +以下のようなフォーマットの日付にマッチします。 +> + 1970年12月31日(木) + 2021年1月1日(金) + 2021年2月13日(土) +< +日付と曜日の整合性が取れていない場合もマッチし、インクリメント・デクリメントの +際に正しい曜日となるよう自動で修正されます。 + +augend.date.alias["%H:%M:%S"]~ + +以下のようなフォーマットの時刻(時:分:秒)にマッチします。 +> + 00:00:00 + 12:34:56 +< + +`12:34:56` の例の場合、 + +* カーソルが `12` より左にある場合:秒 +* カーソルが `12` のいずれかの上にある場合:時 +* カーソルが `:34` のいずれかの上にある場合:分 +* カーソルが `:56` のいずれかの上にある場合:秒 + +がインクリメントされます。 +意図しないマッチを避けるため、存在しない時刻にはマッチしません。 + +augend.date.alias["%H:%M"]~ + +以下のようなフォーマットの時刻(時:分)にマッチします。 +> + 00:00 + 12:34 +< + +`12:34` の例の場合、 + +* カーソルが `12` より左にある場合:分 +* カーソルが `12` のいずれかの上にある場合:時 +* カーソルが `:34` のいずれかの上にある場合:分 + +がインクリメントされます。 +意図しないマッチを避けるため、存在しない時刻にはマッチしません。 + +CONSTANT *dial-augends-constant* +-------- + +キーワードなどの決められた文字列をトグルします。 +`augend.constant.new{ ...opts }` で使用できます。 +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.constant.new{ + elements = {"and", "or"}, + word = true, -- if false, "sand" is incremented into "sor", "doctor" into "doctand", etc. + cyclic = true, -- "or" is incremented into "and". + }, + augend.constant.new{ + elements = {"&&", "||"}, + word = false, + cyclic = true, + }, + }, + } +< + +以下のパラメータを有します。 + elements (string[], 必須) + 相互変換したい文字列のリスト。 + word (boolean, default: true) + true のとき、elements に一致する文字列が単語の境界にあるときの + みマッチします。 + cyclic (boolean, default: true) + true のとき、増減操作を行う際に巡回します。 + preserve_case (boolean, default: false) + true のとき、アルファベットの大文字・小文字のパターンを保存し + ての変換を試みます。 + pattern_regexp (string, default: `\C\V\<\(%s\)\>`) + 文字列検索を行う際の正規表現。 elements に指定した文字列を + `%s` に指定します。 + +注意: `&&` と `||` のように、記号からなる文字列を相互変換するときは `cyclic` +を false にしてください。 +注意: `preserve_case` が true のときは、大文字・小文字を無視して elements に +指定した文字列にマッチします。このとき、文字列が以下の3パターンのいずれかに該 +当する場合に限り、大文字・小文字のパターンを保存して文字列を切り替えます。 +* 全て小文字の場合(例: `true <-> false`) +* 全て大文字の場合(例: `TRUE <-> FALSE`) +* 先頭のみ大文字の場合(例: `True <-> False`) +それ以外のときは全て小文字に変換します。 + +augend.constant.alias.ja_weekday~ + +日本語の曜日名にマッチします。 +> + 月 + 火 + 水 + 木 + 金 + 土 + 日 +< + +それぞれ Vim の意味での単語になっているものに対してマッチするため、「日用品」 +など熟語の一部にはマッチしません。 + +augend.constant.alias.ja_weekday_full~ + +日本語の曜日名にマッチします。 +> + 月曜日 + 火曜日 + 水曜日 + 木曜日 + 金曜日 + 土曜日 + 日曜日 +< + +こちらは augend.constant.alias.ja_weekday と異なり、単語ではなく文字列としてマ +ッチするため、「毎週火曜日」のように漢字が連なっていてもマッチします。 + +augend.constant.alias.bool~ + +`true` または `false` という単語にマッチします。 +> + true + false +< + +augend.constant.alias.alpha~ + +`a` や `z` など、1つの小文字アルファベットにマッチします。ノーマルモードの場合 +はそれ自身が Vim の意味で単語として独立していない限りマッチしません。例えば +> + (a) + a. +< +などのように単語として独立した `a` にはマッチしますが、 +> + cafe + 2a +< +のように単語として独立していない `a` にはマッチしません。 + +なお、インクリメント時に巡回は行われません。すなわち `z` をインクリメントして +も`z` のまま変わらず、 `a` をデクリメントしても `a` のまま変わりません。 + +augend.constant.alias.Alpha~ + +`A` や `Z` など、1つの大文字アルファベットにマッチします。 + +CASE *dial-augends-case* +----- + +camelCase や snake_case といったプログラミング言語における変数名や関数名の規則 +を変更します。 `augend.case.new { ...opts }` で使用できます。 + +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.case.new{ + types = {"camelCase", "snake_case"}, + cyclic = true, + }, + }, + } +< + +以下のパラメータを有します。 + patterns (string[], 必須) + 相互変換したい命名規則のリスト。以下から選択: + - `camelCase` + - `snake_case` + - `kebab-case` + - `PascalCase` + - `SCREAMING_SNAKE_CASE` + cyclic (boolean, default: true) + true のとき、増減操作を行う際に巡回します。 + +NOTE: 現時点で、1つの単語のみから構成される識別子には反応しません。たとえば +`dial_nvim` や `dialNvim` には反応しますが、 `dial` という識別子には反応しませ +ん。これは `dial` という識別子だけを見て、それが camelCase なのか snake_case +なのか判別することが不可能だからです。これは PascalCase など他の規則を選んだ場 +合も同様であり、 `Dial` や `DIAL` には反応しません。 + +HEXCOLOR *dial-augends-hexcolor* +-------- + +`#000000` や `#ffffff` といった形式の RGB カラーコードを増減します。 +`augend.hexcolor.new{ ...opts }` で使用できます。 + +> + require("dial.config").augends:register_group{ + default = { + augend.hexcolor.new{ + case = "lower", + }, + }, + } +< + +得られる被加数は以下のような16進数のカラーコード (#rrggbb) にマッチします。 +> + #1280af + #808080 + #001351 +< + +`#1280af` の例の場合、 + +* カーソルが `#1280af` より左、もしくは `#` 上にある場合: RGB 全て +* カーソルが `12` のいずれかの上にある場合:R +* カーソルが `80` のいずれかの上にある場合:G +* カーソルが `af` のいずれかの上にある場合:B + +がインクリメントの対象となります。 + +SEMVER *dial-augends-semver* +------ + +Semantic version を増減します。後述のエイリアスを用います。単なる非負整数のイ +ンクリメントとは以下の点で異なります。 + +- semver 文字列よりもカーソルが手前にあるときは、パッチバージョンが優先してイ + ンクリメントされます。 +- マイナーバージョンの値が増加したとき、パッチバージョンの値は0にリセットされ + ます。 +- メジャーバージョンの値が増加したとき、マイナー・パッチバージョンの値は0にリ + セットされます。 + +augend.semver.alias.semver~ + +以下のようなバージョンを表す数字の三つ組にマッチします。 +> + 0.3.0 + 3.9.14 + 1.2.3 +< + +`1.2.3` の例でインクリメントする場合を考えます。 + +* カーソルが `1` より手前にある場合 + パッチバージョンがインクリメントされ、 `1.2.4` となります。 +* カーソルが `1` の上にある場合 + メジャーバージョンがインクリメントされ、 `1.3.0` となります。 +* カーソルが `.2` の上にある場合 + マイナーバージョンがインクリメントされ、 `2.0.0` となります。 +* カーソルが `.3` の上にある場合 + パッチバージョンがインクリメントされ、 `1.2.4` となります。 + +なお、デクリメントする場合はメジャー・マイナー・パッチどのバージョンにおいても +下位の数値は変化しません。 `1.2.3` でメジャーバージョンを1デクリメントすると +`0.2.3` となります。 + +PAREN *dial-augends-paren* +----- + +括弧やクオーテーションで囲まれた文字列を対象とし、括弧の種類を変更します。 +`augend.paren.new{ ...opts }` で使用できます。 + +> + require("dial.config").augends:register_group{ + default = { + augend.paren.new{ + patterns = { + {'"', '"'}, + {"[[", "]]"}, + {"[=[", "]=]"}, + {"[==[", "]==]"}, + {"[===[", "]===]"}, + }, + nested = false, + cyclic = false, + }, + augend.paren.new{ + patterns = { {"'", "'"}, {'"', '"'} }, + nested = false, + escape_char = [[\]], + cyclic = true, + }, + }, + } +< + +以下のパラメータを有します。 + patterns (string[][], 必須) + 相互変換したい括弧のペア。 + 各要素は string の2つ組であり、開き括弧と閉じ括弧を表します。 + nested (boolean, default: true) + true のとき、ネストを検出します。 + 開き括弧と閉じ括弧の文字列が一致する場合は意味を成しません。 + cyclic (boolean, default: true) + true のとき、増減操作を行う際に巡回します。 + escape_char (string, default: nil) + 定義されていれば、その文字列を escape char と見做します。 + escape char の直後では、開き括弧や閉じ括弧の字句が無視されます。 + ただし、 escape char が2つ連続で並んだ場合はescape char 自体が + escape されたものと見做します。 + +NOTE: 複数行に渡る括弧を検出・操作することはできません。dial.nvim 全体の方針と +して、CTRL-A や CTRL-X による増減操作は1行で完結するものに制限しています。複数 +行に渡る括弧の操作は、より括弧操作に特化したプラグインの使用をお勧めします。 + +augend.paren.alias.quote~ + +single-quoted literal と double-quoted literal を相互変換します。 +`\` はエスケープ文字として扱われます。 +> + "foo" -> 'foo' + 'foo' -> "foo" + "foo\"" -> 'foo\"' +< + +augend.paren.alias.brackets~ + +3種類の括弧 `()`, `[]`, `{}` を相互変換します。ネストも考慮されます。 +> + (foo) -> [foo] -> {foo} -> (foo) -> ... +< + +augend.paren.alias.lua_str_literal~ + +Lua の string literal 記法を相互変換します。 +> + "foo" -> [[foo]] -> [=[foo]=] -> [==[foo]==] -> [===[foo]===] +< + +augend.paren.alias.rust_str_literal~ + +Rust の string literal 記法を相互変換します。 +> + "foo" -> r#"foo"# -> r##"foo"## -> r###"foo"### +< + +MISC *dial-augends-misc* +----- + +`augend.misc.alias` 下に雑多な被加数がまとめて定義されています。 + +augend.misc.alias.markdown_header~ + +Markdown のヘッダレベルを増減します。 +> + # Header1 + ## Header2 + ###### Header6 +< + +NOTE: augend としては珍しく、カーソルが `#` より後ろにある場合でもマッチします。 +ただしその場合、カーソル上またはカーソルの後に他の有効な被加数が存在すればそち +らが優先されます。確実に markdown_header を増減させたければ、カーソルを行頭に +移動させてから操作することをお勧めします。 + + +USER *dial-augends-user* +----- + +ユーザ自身が増減ルールを定義したい場合には `augend.user.new{ ...opts }` を使用 +できます。 +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.user.new{ + find = require("dial.augend.common").find_pattern("%d+"), + add = function(text, addend, cursor) + local n = tonumber(text) + n = math.floor(n * (2 ^ addend)) + text = tostring(n) + cursor = #text + return {text = text, cursor = cursor} + end + }, + }, + } +< + +============================================================================== +ADVANCED USAGE *dial-advanced-usage* + +|dial.nvim| のうち、発展的ではあるものの便利な機能を紹介します。 + +------------------------------------------------------------------------------ +DOT REPEATING *dial-dot-repeating* + +|dial.nvim| が提供するすべてのキーコマンドはドットリピートに対応しています。 +すなわち、"." を押すことによって直前の増減操作を繰り返すことができます。 +ただし標準の とは若干挙動が異なり、増減ルールが固定されます。 + +具体的な例として、以下のような設定を考えます。 +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.date.alias["%Y/%m/%d"], + }, + } +< +これはすなわち、十進数の数値と `yyyy/MM/dd` 形式の日付が によって増減可 +能であることを意味します。 + +さて、以下のような記述が書かれたバッファをイメージしてください。 +> + date: 2020/11/08 + ... + due date of 1st report: 2020/11/23 + ... + due date of 2nd report: 2020/12/21 + ... + date of exam: 2021/01/14 +< + +このバッファにある全ての期日をちょうど1ヶ月延長しなければならなくなったとしま +す。日付が大量にある場合この操作はきわめて退屈で面倒ですが、 |dial.nvim| を用 +いればとても Vim らしいアプローチで楽にこの操作を実現することができます。 + +まずはいつもどおり1行目の `2020/11/08` の `11` のところにジャンプし、 を +タイプして日付を1ヶ月伸ばします。`2020/12/08` となったはずです。 + +次に "/" コマンドを用いて `date` で検索し、3行目にジャンプします。 +検索文字列はなんでもよいのですが、変更したい日付の前にはいずれも `date` がある +ことから、`date` にジャンプするのが今回は最も楽だと期待できます。 +ジャンプしたら、その直後で "." コマンドを実行してください。なんの問題もなく、 +`2020/11/23` が1ヶ月延期されるはずです。 +> + date: 2020/12/08 + ... + Due date (1st lecture): 2020/12/23 + ... + Due date (2nd lecture): 2020/12/21 + ... + Due date (3rd lecture): 2021/01/14 +< +ここで重要なのは、もしここで "." ではなく を押していた場合、 `1st` と書 +かれた数字が増減の対象となってしまい、 `2st` になってしまっていたという点です。 +仮に `1st` という文字列がなかったとしても、(カーソル位置が `11` にないため) +日付が1日延長されるだけだったでしょう。 + はコマンドが実行されるたびにその行について適切なルールを探すものの、ドッ +トリピートを行った場合は以前に行ったルールを保持します。ここでは「日付を1ヶ月 +延ばす」という操作を保持していたため、カーソルを適切な位置に移動させずとも、自 +動的に近くにある日付の「月」のところまで移動してから増減操作を実行します。 +したがって上の例では、ひとたび検索を行った後であれば `.n.n.` とするだけですべ +ての期限を1ヶ月延ばすことができるのです。 + +============================================================================== +INTERFACE *dial-interface* + +------------------------------------------------------------------------------ +MAPPING *dial-mapping* + +["x](dial-increment) *(dial-increment)* + NORMAL モードにおける CTRL-A に相当する機能を提供します。 + すなわち、カーソル下または後の被加数を [count] だけインクリメントします。 + カーソルと同じ行にある被加数のみが対象です。カーソルより手前にある被加数に + は基本的に反応しないものの、被加数によっては例外的にカーソルの手前にあって + もインクリメントを行う場合があります。 + + expression register |@=| を指定することができ、その場合は記述したグループ + 名に基づく被加数が用いられます。指定しなかった場合は `default` という名前 + のグループに記述された被加数が用いられます(|dial-config-augends-default|)。 + それ以外のレジスタを指定しても効果はありません。 + +{VISUAL}["x](dial-increment) *v_(dial-increment)* + NORMAL モードにおける CTRL-A に相当する機能を提供します。 + 以下のどのモードに入っているかによって挙動が変化します。 + * 通常の VISUAL モード (|v|) + * 行 VISUAL モード (|V|) + * 矩形 VISUAL モード (|CTRL-V|) + + 通常のビジュアルモード (|v|) の場合、ビジュアル選択されたテキストの中に含 + まれる被加数を [count] だけインクリメントします。既存の CTRL-A コマンドの + 代替です。 + + 行選択ビジュアルモード (|V|) の場合、ビジュアル行選択されたテキスト中の被 + 加数を、行ごとに [count] だけインクリメントします。各行ごとに被加数の位置 + を探索し、マッチしたもののうち最も手前の被加数を一つだけ対象とします。 + + 矩形選択ビジュアルモード (|CTRL-V|) の場合は行選択ビジュアルモードの場合と + 似ているものの、マッチする対象が矩形選択された範囲に絞られます。 + +["x](dial-decrement) *(dial-decrement)* + NORMAL モードにおける CTRL-X に相当する機能を提供します。 + +{VISUAL}["x](dial-decrement) *v_(dial-decrement)* + VISUAL モードにおける CTRL-X に相当する機能を提供します。 + +{VISUAL}["x]g(dial-increment) *v_g(dial-increment)* + VISUAL モードにおける |v_g_CTRL-A| に相当する機能を提供します。 + + 使用方法は|v_(dial-increment)| と似ているものの、上から数えてn行目に + ある被加数がn * [count] だけ増加します。箇条書きや予定表など、 一定の数値 + ずつ増加する等差数列を作成したい場合に便利です。たとえば +> + 09/12 + 09/12 + 09/12 + 09/12 +< + という日付のリストがある時、2番目の `1/1` へと移動して下3行を行選択ビジュ + アルモードで選択し、 `7g` と押すと +> + 09/12 + 09/19 + 09/26 + 10/03 +< + のように、1週間ごとの日付が得られます(日付の被加数が有効のときに限る)。 + +{VISUAL}["x]g(dial-decrement) *v_g(dial-decrement)* + VISUAL モードにおける |v_g_CTRL-X| に相当する機能を提供します。 + +------------------------------------------------------------------------------ +COMMAND *dial-command* + +:[range]DialIncrement {augend} *:DialIncrement* + + 選択範囲に対して、{augend} で指定された被加数を1だけインクリメントします。 + 行選択ビジュアルモードにおける |v_(dial-increment)| と似ていますが、 + 被加数を指定し、そのルールに制限することができます。 + 加数(いくつ増加させるか)を指定することはできないため、 |@:| などを用いて + 繰り返すことが想定されています。 + +:[range]DialDecrement {augend} *:DialDecrement* + + 選択範囲に対して、{augend} で指定された被加数を1だけデクリメントします。 + +------------------------------------------------------------------------------ +LUA API *dial-lua-api* + + *dial-lua-api-map* +"dial.map" module~ + +|dial.nvim| 固有のマッピングを提供するモジュールです。 + + *dial.map.inc_normal()* +require("dial.map").inc_normal([group_name]) + + NORMAL モードにおいて与えられたグループ名をもとにインクリメントを行うため + のキーシーケンスを出力します。group_name は省略すると `default` と等価にな + ります。|vim.api| と|nvim_set_keymap| や |nvim_buf_set_keymap| を組み合わ + せて以下のような形で使用することが想定されています。 +> + vim.api.nvim_set_keymap( + "n", "", require("dial.map").inc_normal(), {noremap = true} + ) +< + + *dial.map.dec_normal()* +require("dial.map").dec_normal([group_name]) + + NORMAL モードにおいて与えられたグループ名をもとにデクリメントを行うための + キーシーケンスを出力します。 + + *dial.map.inc_visual()* +require("dial.map").inc_visual([group_name]) + + VISUAL モードにおいて与えられたグループ名をもとにインクリメントを行うための + キーシーケンスを出力します。 + + *dial.map.dec_visual()* +require("dial.map").dec_visual([group_name]) + + VISUAL モードにおいて与えられたグループ名をもとにデクリメントを行うための + キーシーケンスを出力します。 + + *dial.map.inc_gvisual()* +require("dial.map").inc_gvisual([group_name]) + + VISUAL モードにおいて与えられたグループ名をもとにイデクリメントを行うため + のキーシーケンスを出力します。被加数は上から順に [count] ずつ増加します。 + + *dial.map.dec_gvisual()* +require("dial.map").dec_gvisual([group_name]) + + VISUAL モードにおいて与えられたグループ名をもとにデクリメントを行うための + キーシーケンスを出力します。被加数は上から順に [count] ずつ増加します。 + +"dial.augend" module~ + +被加数を定義するモジュールです。 |dial-augends| 参照。 + +"dial.config" module~ + + *dial.config.augends:register_group()* +require("dial.config").augends:register_group(tbl) + + 与えられた辞書形式のテーブルに基づいてグループを定義または上書きします。 + 関数の引数には、グループ名をキー、被加数のリストを値とする辞書を指定します。 + + *dial.config.augends:get()* +require("dial.config").augends:get(group_name) + + 登録されているグループのうち、 `group_name` をグループ名にもつものを取得し + ます。 + +vim:tw=78:fo=tcq2mM:ts=4:ft=help:norl:noet:fdm=marker:fen diff --git a/etc/soft/nvim/+plugins/dial.nvim/doc/dial.txt b/etc/soft/nvim/+plugins/dial.nvim/doc/dial.txt new file mode 100644 index 0000000..e25f60d --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/doc/dial.txt @@ -0,0 +1,1477 @@ +*dial.txt* Lua plugin that extends CTRL-A / CTRL-X + +A package that extends the standard Vim commands CTRL-A / CTRL-X to allow +increment/decrement of various strings, not limited to alphanumeric +characters. + +Author: Mogami Shinichi (@monaqa) +Version: 0.3.0 +License: MIT license + + +============================================================================== +CONTENTS *dial-contents* + +Requirements |dial-requirements| +Introduction |dial-introduction| + Features |dial-features| +Usage |dial-usage| +Configurations |dial-config| + Aliases |dial-aliases| + Augends |dial-augends| + Number |dial-augends-number| + Date |dial-augends-date| + Constant |dial-augends-constant| + Case |dial-augends-case| + Hexcolor |dial-augends-hexcolor| + Semver |dial-augends-semver| + Paren |dial-augends-paren| + Misc |dial-augends-misc| + User |dial-augends-user| +Advanced Usage |dial-advanced-usage| + Dot Repeating |dial-dot-repeating| +Interface |dial-interface| + Mapping |dial-mapping| + Command |dial-command| + Lua API |dial-lua-api| + +============================================================================== +REQUIREMENTS *dial-requirements* + +* Neovim 0.6.1 or later + (0.5.0 or later will probably work, but not guaranteed) + +============================================================================== +INTRODUCTION *dial-introduction* + +*dial.nvim* is a package that increments/decrements strings of various +formats. It extends the standard Vim commands such as CTRL-A, CTRL-X, and g +CTRL-A in VISUAL mode. For example, the following strings are covered. + +------------------------------------------------------------------------------ +FEATURES *dial-features* + +Increment/Decrement of various type of string~ + You can increment/decrement a wide variety of strings, as follows: + + * n-ary (`2 <= n <= 36`) integers + * date + * constant (toggle a specific string, such as a keyword or operator) + * `true` ⇄ `false` + * `&&` ⇄ `||` + * `a` ⇄ `b` ⇄ ... ⇄ `z` + * hex colors + * semantic version + + You can also define new increment/decrement rules. + +Flexible configuration of target (augends)~ + |dial.nvim| provides a lot of rules, and you can select and activate only + the ones you really need. + Although some rules of |dial.nvim| have conflicts, such as dates in + `mm/dd/yyyy` format and dates in `dd/mm/yyyy` format, you can enable only + one of them to perform operations without conflicts. + It is also possible to change the target only for a specific buffer or + |filetype|. + +Support counter~ + You can change addends (the number of additions) by typing `10` as + well as the usual CTRL-A / CTRL-X. + +Support dot repeating~ + You can repeat the previous operation with |.|. + |dial.nvim| remembers the last increment or decrement target type. For + example, if the last increment was "month in `yyyy/mm/dd` format", + dot-repeat will find and increment the same type, "month in `yyyy/mm/dd` + format", even if there is another string to increment or decrement before + it. + +Support VISUAL mode~ + You can also use |dial.nvim|'s operation in VISUAL mode. + +============================================================================== +USAGE *dial-usage* + +Since this plugin does not replace the default key-mapping, you need to add +the following description to the configuration file after installing the +plugin. +> + nmap (dial-increment) + nmap (dial-decrement) + vmap (dial-increment) + vmap (dial-decrement) + vmap g g(dial-increment) + vmap g g(dial-decrement) +< + +Alternatively, you can configure with Lua as follows: +> + vim.api.nvim_set_keymap( + "n", "", require("dial.map").inc_normal(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "n", "", require("dial.map").dec_normal(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").inc_visual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").dec_visual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "g", require("dial.map").inc_gvisual(), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "g", require("dial.map").dec_gvisual(), {noremap = true} + ) +< + +============================================================================== +CONFIGURATIONS *dial-config* + +In this plugin, flexible increment/decrement rules can be set by using augend +and group, where augend represents the target of the increment/decrement +operation, and group represents a group of multiple augends. + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + -- default augends used when no group name is specified + default = { + augend.integer.alias.decimal, -- nonnegative decimal number + augend.integer.alias.hex, -- nonnegative hex number + -- date (2022/02/19, etc.) + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + }, + + -- augends used when group with name `mygroup` is specified + mygroup = { + augend.integer.alias.decimal, + augend.constant.alias.bool, -- boolean value (true <-> false) + -- date (02/19/2022, etc.) + augend.date.new { + pattern = "%m/%d/%Y", + default_kind = "day", + }, + } + } +< + +To define a group, use the `augends:register_group` function in the +`"dial.config"` module. The arguments is a dictionary whose keys are the group +names and whose values are the list of augends. + +Various augends are defined `"dial.augend"` by default. See also: +|dial-augends| and |dial-aliases|. + +To specify the group of augends, you can use expression register |@=| as follows: +> + "=mygroup +< + +If it is tedious to specify the expression register for each increase or +decrease, you can "map" it: +> + nmap a "=mygroup(dial-increment) +< + +Alternatively, you can set the same mapping without expression register: +> + vim.api.nvim_set_keymap( + "n", + "a", + require("dial.map").inc_normal("mygroup"), + {noremap = true} + ) +< + + *dial-config-augends-default* +Default Augend Group~ + +`default` group is a special group, which is specified by default. +If you don't specify the group name with expression register or other, the +augends in the `default` group is used instead. + +If no settings are made, it is equivalent to having the following settings: + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%Y-%m-%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%m/%d", + default_kind = "day", + only_valid = true, + }, + augend.date.new { + pattern = "%H:%M", + default_kind = "day", + only_valid = true, + }, + augend.constant.alias.ja_weekday_full, + }, + } +< + +Augends Only Valid in VISUAL Mode~ + +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + visual = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + augend.constant.alias.alpha, + augend.constant.alias.Alpha, + }, + } + + -- change augends in VISUAL mode + vim.api.nvim_set_keymap( + "v", "", require("dial.map").inc_normal("visual"), {noremap = true} + ) + vim.api.nvim_set_keymap( + "v", "", require("dial.map").dec_normal("visual"), {noremap = true} + ) +< + +Augends Only Valid in Specific File Type~ + +> + lua << EOF + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + typescript = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.constant.new{ elements = {"let", "const"} }, + }, + } + EOF + + " change augends in specific file type + autocmd FileType typescript nmap "=typescript(dial-increment) + autocmd FileType typescript nmap "=typescript(dial-decrement) +< + +------------------------------------------------------------------------------ +ALIASES *dial-aliases* + +The following aliases are provided by default (See |dial-augends| for detail): + + integer: + `augend.integer.alias.decimal` + `augend.integer.alias.decimal_int` + `augend.integer.alias.hex` + `augend.integer.alias.octal` + `augend.integer.alias.binary` + + date: + `augend.date.alias["%Y/%m/%d"]` + `augend.date.alias["%m/%d/%Y"]` + `augend.date.alias["%d/%m/%Y"]` + `augend.date.alias["%m/%d/%y"]` + `augend.date.alias["%d/%m/%y"]` + `augend.date.alias["%m/%d"]` + `augend.date.alias["%-m/%-d"]` + `augend.date.alias["%Y-%m-%d"]` + `augend.date.alias["%Y年%-m月%-d日"]` + `augend.date.alias["%Y年%-m月%-d日(%ja)"]` + `augend.date.alias["%H:%M:%S"]` + `augend.date.alias["%H:%M"]` + + constant: + `augend.constant.alias.ja_weekday` + `augend.constant.alias.ja_weekday_full` + `augend.constant.alias.bool` + `augend.constant.alias.alpha` + `augend.constant.alias.Alpha` + + semver: + `augend.semver.alias.semver` + + paren: + `augend.paren.alias.quote` + `augend.paren.alias.brackets` + `augend.paren.alias.lua_str_literal` + `augend.paren.alias.rust_str_literal` + + misc: + `augend.misc.alias.markdown_header` + +NOTE: aliases in `augend.date.alias` are deprecated. These will be removed in +future release. + +------------------------------------------------------------------------------ +AUGENDS *dial-augends* + +For simplicity, we define the variable `augend` as follows. + +> + local augend = require("dial.augend") +< + +NUMBER *dial-augends-number* +------ + +Represents n-ary integer. + +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.integer.new{ + radix = 16, + prefix = "0x", + natural = true, + case = "upper", + }, + }, + } +< + +The argument table of `augend.integer.new` can take the following keys: + radix (number, default: 10) + represents radix of the number, e.g., 10 in decimal system and + 2 in binary system. + prefix (string, default: "") + can be used to specify a string to be prepended to a number, + e.g., `0x`. + natural (boolean, default: true) + toggles whether or not a number should be treated as a natural + number. If true, the augend matches only natural numbers. That + is, if the cursor is on `-1`, it does not match `-1` but `1`. + case ("upper" | "lower", default: "lower") + represents the case of the alphabet used for numbers with + radix larger than 10, such as hexadecimal numbers. + delimiter (char, default: "") + represents a numerical separator. For example, giving ",", the + augend will match the following numbers: +> + 1,234,567 + 1,23,4,5,67 +< + In every situation, the separator is simply ignored when + evaluating the number, but when incrementing/decrementing, the + position of the separator is formatted as an n-digit + separator. The value of n can be controlled by the value of + `delimiter_digits`, described below. + delimiter_digits (number, default: 3) + specifies how many digits to split a number when `delimiter` + is specified. + It is meaningless if `delimiter` is an empty string. + +augend.integer.alias.decimal~ + +Increments/decrements a non-negative decimal number. It matches such as +> + 0 + 123 + 3141592 +< +and each increment increases that number by one. Unlike the standard CTRL-A +command, it does not consider any `-` at the beginning oc the number. If you +perform |(dial-increment)| with the cursor at `1` of the following +string: +> + file-1.jpg +< +then you will get the following result. +> + file-2.jpg +< + +augend.integer.alias.decimal_int~ + +Basically the same as augend.integer.alias.decimal, but this one also matches +negative integers. If you perform |(dial-increment)| with the cursor at +`1` of the following string: +> + file-1.jpg +< +then you will get the following result. +> + file0.jpg +< + +augend.integer.alias.hex~ + +Matches a hexadecimal non-negative integer and increments/decrements in +hexadecimal notation. It matches strings such as: +> + 0x12 + 0xafbf + 0xAFBF +< + +augend.integer.alias.octal~ + +Matches a octal non-negative integer and increments/decrements in octal +notation. It matches strings such as: +> + 0o37 + 0o567 + 0o01212 +< + +augend.integer.alias.binary~ + +Matches a binary non-negative integer and increments/decrements in binary +notation. It matches strings such as: +> + 0b00 + 0b1010 + 0b10100101 +< + +DATE *dial-augends-date* +----- + +Matches dates and times. + +> + require("dial.config").augends:register_group{ + default = { + augend.date.new{ + pattern = "%Y/%m/%d", + default_kind = "day", + only_valid = true, + word = false, + }, + }, + } +< + +The argument table of `augend.date.new` can take the following keys: + pattern (string, required) + date pattern to match. See |dial-augends-date-pattern| for + details. + default_kind (string, required) + represents in which unit the date to be incremented/decremented + when the cursor position is far from the date or in VISUAL mode. + Choose from the following values: + * `year` + * `month` + * `day` + * `hour` + * `min` + * `sec` + only_valid (boolean, default: false) + When true, it doesn't match non-existent dates or times, such as + `2022/13/52` and `27:30`. It is useful to avoid matching an + unintended range. + When false, it matches non-existent dates or times, and corrects + them to the existent ones. For example, when you increment the + date `2022/06/31` by one day, the original string is interpreted + as the date `2022/07/01` and the final date becomes `2022/07/02`. + word (boolean, default: false) + If true, the augend matches only if the string matching + elements is on a word boundary. + clamp (boolean, default: false) + See |dial-augends-date-clamp|. + end_sensitive (boolean, default: false) + See |dial-augends-date-end-sensitive|. + custom_date_elements (table, default: {}) + Advanced option to define the custom date elements by user. + Refer to |dial-augends-date-custom-format| for detail. + + *dial-augends-date-pattern* +The argument `pattern` can be a string like the following: +> + %Y/%m/%d (e.g. 2022/02/06) + %H:%M:%S (e.g. 15:30:00) + %-m/%-d (e.g. 2/6) +< +`%` is an escape character to represent a date elements and can be used by the +following pattern. + +* `%` + a single alphabetical character. + `%Y` 4-digit year. (e.g. `2022`) + `%y` Last 2 digits of year. + The upper 2 digits are interpreted as `20`. (e.g. `22`) + `%m` 2-digit month. (e.g. `09`) + `%d` 2-digit day. (e.g. `28`) + `%H` 2-digit hour, expressed in 24 hours. (e.g. `15`) + `%I` 2-digit hour, expressed in 12 hours. (e.g. `03`) + `%M` 2-digit minute. (e.g. `05`) + `%S` 2-digit second. (e.g. `08`) + `%a` English weekdays (`Sun`, `Mon`, ..., `Sat`) + `%A` English full weekdays (`Sunday`, `Monday`, ..., `Saturday`) + `%b` English month names (`Jan`, ..., `Dec`) + `%B` English month full names (`January`, ..., `December`) + `%p` `AM` or `PM`. + `%J` Japanese weekdays (`日`, `月`, ..., `土`) + +* `%-` + a single alphabetical character. + `%-y` 1- or 2-digit year. (e.g. `9` represents 2009) + `%-m` 1- or 2-digit month. (e.g. `9`) + `%-d` 1- or 2-digit day. (e.g. `28`) + `%-H` 1- or 2-digit hour, expressed in 24 hours. (e.g. `15`) + `%-I` 1- or 2-digit hour, expressed in 12 hours. (e.g. `3`) + `%-M` 1- or 2-digit minute. (e.g. `5`) + `%-S` 1- or 2-digit second. (e.g. `8`) + +* `%%`. This is used to represent `%` itself. + +* Text of the form `%(element_name)` (|dial-augends-date-custom-format|), + where `element_name` can be any string in which the `)` character does not + appear. + +NOTE: patterns must contain information about date or time. For example, the +string that only contains `%a` cannot be a pattern, because the day of the +week alone is not enough to infer the date. + + *dial-augends-date-custom-format* +In date patterns, `%(element_name)` is a special placeholder that can be +defined by the user. The placeholder can be given in the +`custom_date_elements` argument of `augend.date.new`. + +For example, consider the following configuration: +> + local WEEKDAYS_FLUSHRIGHT = { + " Sunday", + " Monday", + " Tuesday", + "Wednesday", + " Thursday", + " Friday", + " Saturday", + } + require("dial.config").augends:register_group { + default = { + augend.date.new { + pattern = "%d/%m/%y %(week_flushright)", + default_kind = "day", + custom_date_elements = { + week_flushright = { + kind = nil, + regex = common.enum_to_regex(WEEKDAYS_FLUSHRIGHT), + update_date = function(_, date) + return date + end, + format = function(time) + local wday = os.date("*t", time).wday + return WEEKDAYS_FLUSHRIGHT[wday] + end, + } + } + } + }, + } +< +This example defines a custom date element named `week_flushright` and uses +the element with `%(week_flushright)`. As the result, this rule matches the +following string: +> + 18/10/22 Tuesday +< +and pressing on the date increments it to the following string: +> + 19/10/22 Wednesday +< + +The following fileds are required in the custom_date_elements value: + kind (string | nil, default: nil) + which information is tied to the element on the date. + * `year` + * `month` + * `day` + * `hour` + * `min` + * `sec` + regex (string, required) + regular expression to search the element. + update_date (function, required) + How the date is updated by reading the element: the first argument + is a string matching the placeholder, and the second argument is + information about the date before the update (a table of the same + form as `os.date("*t")`). + format (function, required) + Given a date (and time), how the element should be converted to a + string. unix time is passed as an integer for the first argument. + +Yes, it is really flexible! Flexible, but complicated. We recommend to use +existing elements such as `%a` and `%y`, if possible. + + *dial-augends-date-clamp* + *dial-augends-date-end-sensitive* +When operating increment/decrement such as "1 month later" or "1 year later", +the date it represents may not be obvious. For example, given the date +`2022-01-31` in `yyyy-mm-dd` format, one might think that 1 month later of +`2022-01-31` is `2022-03-31` with 31 days added, or `2022-02-28`, tha last day +of the month following January. To accomodate various cases, augend.date has +two options `clamp` and `end_sensitive.` + +For example, consider one month after `2022-01-31`. The straightforward answer +is `2022-02-31`, but such a date does not exist. When `clamp = false`, it is +automatically corrected to a date that exists by carrying forward to the next +month: +> + 2022-01-31 --(+1 month)--> 2022-02-31 (that does not exist) + ~~> 2022-03-03 (FINAL RESULT) +< +On the other hand, when `clamp = false`, it is automatically corrected by +truncating it preserving the month. +> + 2022-01-31 --(+1 month)--> 2022-02-31 (that does not exist) + ~~> 2022-02-28 (FINAL RESULT) +< + +By using `end_sensitive` option, the last day of the month is treated +specially. +When `end_sensitive = true` and the date to operate is the last day of the +month, incrementing/decrementing the year or month will force the conversion +to the last day of the month, regardless of the `clamp` option. +> + 2022-01-31 --(+1 month)--> 2022-02-28 +< + +`clamp` and `end_sensitive` are very similar, but work slightly differently. +Choose the best value that fits your use case, respectively. + +* When `clamp = false, end_sensitive = false` +> + 2022-01-30 --(+1 month)--> 2022-03-02 + 2022-01-31 --(+1 month)--> 2022-03-03 + 2022-02-28 --(+1 month)--> 2022-03-28 + 2022-03-31 --(-1 month)--> 2022-03-03 +< +* When `clamp = true, end_sensitive = false` +> + 2022-01-30 --(+1 month)--> 2022-02-28 # Clamped! + 2022-01-31 --(+1 month)--> 2022-02-28 # Clamped! + 2022-02-28 --(+1 month)--> 2022-03-28 + 2022-03-31 --(-1 month)--> 2022-02-28 # Clamped! +< +* When `clamp = false, end_sensitive = true` +> + 2022-01-30 --(+1 month)--> 2022-03-02 + 2022-01-31 --(+1 month)--> 2022-02-28 # Last day of the month! + 2022-02-28 --(+1 month)--> 2022-03-31 # Last day of the month! + 2022-03-31 --(-1 month)--> 2022-02-28 # Last day of the month! +< +* When `clamp = true, end_sensitive = true` +> + 2022-01-30 --(+1 month)--> 2022-02-28 # Clamped! + 2022-01-31 --(+1 month)--> 2022-02-28 # Last day of the month! + 2022-02-28 --(+1 month)--> 2022-03-31 # Last day of the month! + 2022-03-31 --(-1 month)--> 2022-02-28 # Last day of the month! +< + + *dial-augends-date-aliases* +There are some aliases in `augend.date`. + +NOTE: the following aliases are remnants of a time when it was hard and +tedious to specify date formats. Now that you can freely specify the date +format, the aliases are losing their value. They are currently retained for +compatibility, but will be removed in the future. + +augend.date.alias["%Y/%m/%d"]~ + +Matches dates with the following format (year/month/day): +> + 1970/12/31 + 2021/01/01 + 2021/02/13 +< + +If the cursor is to the left of the format, the increment is always done in +date units, but if the cursor is on the format, the increment depends on the +specific position of the cursor. + +Let's take `1970/12/31` as an example. + +When the cursor is over any of the four `1970` characters, the year will be +incremented as shown below. +> + 1971/12/31 +< + +When the cursor is over any of the three `/12` characters, the month will be +incremented as shown below. +> + 1971/01/31 +< + +When the cursor is over any of the three `/31` characters, the day will be +incremented as shown below. +> + 1971/01/01 +< + +The format matches dates that do not exist in reality such as: +> + 2021/02/29 + 2021/14/59 +< +The date is corrected to a date that actually exists when incrementing. + +augend.date.alias["%m/%d/%Y"]~ + +Matches dates with the following format: +> + 12/31/1970 + 01/01/2021 + 02/13/2021 +< +To avoid matching an unintended range, it doesn't match non-existent dates. + +augend.date.alias["%m/%d/%y"]~ + +Matches dates with the following format: +> + 12/31/70 + 01/01/21 + 02/13/21 +< +To avoid matching an unintended range, it doesn't match non-existent dates. + +augend.date.alias["%d/%m/%y"]~ + +Matches dates with the following format: +> + 31/12/19 + 01/01/20 + 13/02/20 +< +To avoid matching an unintended range, it doesn't match non-existent dates. + +augend.date.alias["%m/%d"]~ + +Matches dates with the following format: +> + 12/31 + 01/01 + 02/13 +< + +`12/31` の例の場合、 + +* カーソルが `12` より左にある場合:日付 +* カーソルが `12` のいずれかの上にある場合:月 +* カーソルが `/31` のいずれかの上にある場合:日付 + +がインクリメントされます。 + +現実に存在しない日付でもマッチし、操作の際に現実に存在する日付へと強制的に修正 +されます。 + +augend.date.alias["%-m/%-d"]~ + +Matches dates with the following format: +> + 12/31 + 1/1 + 2/13 +< +To avoid matching an unintended range, it doesn't match non-existent dates. + +augend.date.alias["%Y-%m-%d"]~ + +Matches dates with the following format: +> + 1970-12-31 + 2021-01-01 + 2021-02-13 +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%d.%m.%Y"]~ + +Matches dates with the following format: +> + 31.12.1970 + 01.01.2021 + 13.02.2021 +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%d.%m.%y"]~ + +Matches dates with the following format: +> + 31.12.70 + 01.01.21 + 13.02.21 +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%d.%m."]~ + +Matches dates with the following format: +> + 31.12. + 01.01. + 13.02. +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%-d.%-m."]~ + +Matches dates with the following format: +> + 31.12. + 1.1. + 13.2. +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%Y年%-m月%-d日"]~ + +Matches dates with the following format: +> + 1970年12月31日 + 2021年1月1日 + 2021年2月13日 +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%Y年%-m月%-d日(%ja)"]~ + +Matches dates with the following format: +> + 1970年12月31日(木) + 2021年1月1日(金) + 2021年2月13日(土) +< +It matches non-existent dates and corrects them to the existent ones. + +augend.date.alias["%H:%M:%S"]~ + +Matches times with the following format: +> + 00:00:00 + 12:34:56 +< + +If we take `12:34:56` as an example, the following rules will determine the +target of the increment. + +* If the cursor is to the left of `12`: second +* If the cursor is over any of `12`: hour +* If the cursor is over any of `:34`: minute +* If the cursor is over any of `:56`: second + +The format matches times that do not exist in reality such as `52:27:93`, and +the time is corrected to a date that actually exists when incrementing. + +augend.date.alias["%H:%M"]~ + +Matches times with the following format: +> + 00:00 + 12:34 +< + +If we take `12:34` as an example, the following rules will determine the +target of the increment. + +* If the cursor is to the left of `12`: minute +* If the cursor is over any of `12`: hour +* If the cursor is over any of `:34`: minute + +The format matches times that do not exist in reality such as `52:99`, and +the time is corrected to a date that actually exists when incrementing. + +CONSTANT *dial-augends-constant* +-------- + +Predefined sequence of strings. You can use this rule with +`augend.constant.new{ ...opts }` . +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.constant.new{ + elements = {"and", "or"}, + word = true, -- if false, "sand" is incremented into "sor", "doctor" into "doctand", etc. + cyclic = true, -- "or" is incremented into "and". + }, + augend.constant.new{ + elements = {"&&", "||"}, + word = false, + cyclic = true, + }, + }, + } +< + +The argument table of `augend.constant.new` can take the following keys: + elements (string[], required) + An array of strings you want to cross-convert. + word (boolean, default: true) + If true, the augend matches only if the string matching + elements is on a word boundary. + cyclic (boolean, default: true) + If true, the augend circulates the patterns for + increment/decrement operations. + preserve_case (boolean, default: false) + If true, the augend attempts to preserve the pattern of + uppercase/lowercase letters for conversion. + pattern_regexp (string, default: `\C\V\<\(%s\)\>`) + A regular expression used in search. `%s` represents the string + specified in `elements.` + +NOTE: Set `cyclic` to false when cross-converting strings consisting of +symbols, such as `&&` and `||`. +NOTE: When `preserve_case` is true, it matches the string specified for +elements, ignoring case. The case pattern is preserved only if the string +matches one of the following three patterns: +* All lowercase (e.g.: `true <-> false`) +* All uppercase (g.g.: `TRUE <-> FALSE`) +* Only the first letter is uppercase (e.g.: `True <-> False`) +Otherwise the string is converted to lowercase. + +augend.constant.alias.ja_weekday~ + +Matches Japanese day names: +> + 月 + 火 + 水 + 木 + 金 + 土 + 日 +< +Each of these matches a |word| in the Vim sense, so some Japanese idioms such +as `日用品` will not match. + +augend.constant.alias.ja_weekday_full~ + +Matches Japanese day names: +> + 月曜日 + 火曜日 + 水曜日 + 木曜日 + 金曜日 + 土曜日 + 日曜日 +< + +Unlike |augend.constant.alias.ja_weekday|, this simply matches a string (not a +|word|), so even if you have a series of kanji characters, such as `毎週火曜日 +`, it will match. + +augend.constant.alias.de_weekday~ + +Matches German day names: +> + Mo + Di + Mi + Do + Fr + Sa + So +< +Each of these matches a |word| in the Vim sense. + +augend.constant.alias.de_weekday_full~ + +Matches German day names: +> + Montag + Dienstag + Mittwoch + Donnerstag + Freitag + Samstag + Sonntag +< +Each of these matches a |word| in the Vim sense. + +augend.constant.alias.bool~ + +Matches a word `true` or `false`. +> + true + false +< + +augend.constant.alias.alpha~ + +Matches a single lowercase alphabet, such as `a` or `z`. In NORMAL mode, it +will not match unless it is a separate |word| in the Vim sense. For example, +this will match the following `a`: +> + (a) + a. +< +while this will not match the following `a`: +> + cafe + 2a +< + +Note that the incrementing/decrementing is performed in the non-cyclic way. +That is, incrementing `z` will not change the value to `z`, and decrementing +`a` will not change the value to `a`. + +augend.constant.alias.Alpha~ + +Matches a single lowercase alphabet, such as `a` or `z`. + +CASE *dial-augends-case* +----- + +This rule changes identifier (variable, function, and others) name conventions +in programming languages such as "camelCase" and "snake_case". You can use +this rule with `augend.case.new { ...opts }`. +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.case.new{ + types = {"camelCase", "snake_case"}, + cyclic = true, + }, + }, + } +< + +The argument table of `augend.case.new` can take the following keys: + patterns (string[], required) + An array of naming conventions you want to corss-convert. + Select from the following: + - `camelCase` + - `snake_case` + - `kebab-case` + - `PascalCase` + - `SCREAMING_SNAKE_CASE` + cyclic (boolean, default: true) + If true, the augend circulates the patterns for + increment/decrement operations. + +NOTE: Currently, this augend does NOT recognize identifiers that consist of +only one word. For example, it can recognize `dial_nvim` and `dialNvim`, +but not `dial`. This is because it is impossible to determine whether an +identifier `dial` is camelCase or snake_case based on the identifier `dial` +alone. The same applies to other naming conventions, such as PascalCase or +SCREAMING_SNAKE_CASE, which do not recognize `Dial` or `DIAL`. + +HEXCOLOR *dial-augends-hexcolor* +-------- + +Matches hexadecimal color codes (#RRGGBB) such as the following: +> + #1280af + #808080 + #001351 +< + +You can use it with `augend.hexcolor.new{ ...opts }` . + +> + require("dial.config").augends:register_group{ + default = { + augend.hexcolor.new{ + case = "lower", + }, + }, + } +< + +If we take `#1280af` as an example, the following rules will determine the +target of the increment. + +* If the cursor is to the left of `#1280af` or over `#` : R, G and B +* If the cursor is over any of `12`: R +* If the cursor is over any of `80`: G +* If the cursor is over any of `af`: B + +SEMVER *dial-augends-semver* +------ + +Matches semantic versions. You can use it with the alias described below. It +differs from a simple nonnegative integer increment/decrement in these ways: + +* When the cursor is before the semver string, the patch version is + incremented. +* When the minor version is incremented, the patch version is reset to zero. +* When the major version is incremented, the minor and patch versions are + reset to zero. + +augend.semver.alias.semver~ + +Matches following triplet of natural integers: +> + 0.3.0 + 3.9.14 + 1.2.3 +< + +PAREN *dial-augends-paren* +----- + +Matches strings surrounded by brackets or quotations and changes the type of +what surrounds it. You can use it with `augend.paren.new{ ...opts }` . + +> + require("dial.config").augends:register_group{ + default = { + augend.paren.new{ + patterns = { + {'"', '"'}, + {"[[", "]]"}, + {"[=[", "]=]"}, + {"[==[", "]==]"}, + {"[===[", "]===]"}, + }, + nested = false, + cyclic = false, + }, + augend.paren.new{ + patterns = { {"'", "'"}, {'"', '"'} }, + nested = false, + escape_char = [[\]], + cyclic = true, + }, + }, + } +< + +Function `new` has these parameters: + patterns (string[][], required) + List of the pairs of parens you want to interconvert. Each + element of a pair represents an opening bracket and closing + bracket, respectively. + nested (boolean, default: true) + If true, the augend detects nested parens. + It does not make sense if the opening and closing bracket + strings are same. + cyclic (boolean, default: true) + If true, the augends circulates the patterns for + increment/decrement operations. + escape_char (string, default: nil) + If defined, regards the string as the escape character. + Immediately following the escape char, the opening and closing + bracket characters are ignored. However, if two escape chars + appears in succession, the escape char itself is considered + to be escaped. + +NOTE: It is not possible to detect or manipulate parentheses across multiple +lines. dial.nvim's overall policy is to limit operations with CTRL-A and +CTRL-X to those that fits within a single line. For multi-line parenthesis +manipulation, we recommend the use of plugins that specialize more in +parenthesis manipulation. + +augend.paren.alias.quote~ + +Interconverts single-quoted literal and double-quoted literal. +`\` is treated as escape character. +> + "foo" -> 'foo' + 'foo' -> "foo" + "foo\"" -> 'foo\"' +< + +augend.paren.alias.brackets~ + +Interconverts `()`, `[]`, `{}`. It considers nestings. +> + (foo) -> [foo] -> {foo} -> (foo) -> ... +< + +augend.paren.alias.lua_str_literal~ + +Interconverts Lua string literals. +> + "foo" -> [[foo]] -> [=[foo]=] -> [==[foo]==] -> [===[foo]===] +< + +augend.paren.alias.rust_str_literal~ + +Interconverts Rust-style string literals. +> + "foo" -> r#"foo"# -> r##"foo"## -> r###"foo"### +< + +MISC *dial-augends-misc* +----- + +Miscellaneous augends are defined together under `augend.misc.alias`. + +augend.misc.alias.markdown_header~ + +Increment/decrement header levels in Markdown. +> + # Header1 + ## Header2 + ###### Header6 +< + +NOTE: unusually for an augend, it matches even when the cursor is positioned +after `#`. +In that case, however, any other valid augend on or after the cursor will take +precedence. If you want to increment/decrement `markdown_header` with +certainty, it is recommended that you move the cursor to the beginning of the +line before operating. + +USER *dial-augends-user* +----- + +If you define custom augends, you can use `augend.user.new{ ...opts }` . +> + require("dial.config").augends:register_group{ + default = { + -- uppercase hex number (0x1A1A, 0xEEFE, etc.) + augend.user.new{ + find = require("dial.augend.common").find_pattern("%d+"), + add = function(text, addend, cursor) + local n = tonumber(text) + n = math.floor(n * (2 ^ addend)) + text = tostring(n) + cursor = #text + return {text = text, cursor = cursor} + end + }, + }, + } +< + +============================================================================== +ADVANCED USAGE *dial-advanced-usage* + +------------------------------------------------------------------------------ +DOT REPEATING *dial-dot-repeating* + +All key commands in |dial.nvim| support dot repeat (|.|). That is, pressing +"." to repeat the previous increment or decrement operation. However, the +behavior is slightly different from the standard or , where the +increment/decrement in dot repeat is based on the previous operation. + +As a concrete example, let's consider the following configuration. +> + local augend = require("dial.augend") + require("dial.config").augends:register_group{ + default = { + augend.integer.alias.decimal, + augend.date.alias["%Y/%m/%d"], + }, + } +< + +Now, imagine a buffer with the following text. +> + date: 2020/11/08 + ... + due date of 1st report: 2020/11/23 + ... + due date of 2nd report: 2020/12/21 + ... + date of exam: 2021/01/14 +< + +Suppose you need to extend all the due dates in this buffer by one month. If +there are a lot of dates, this operation can be quite tedious and cumbersome, +but with |dial.nvim|, you can easily accomplish this operation in a very +Vim-ish approach. + +First, jump to `11` of `2020/11/08` in the first line as usual, and type +to increment the date by one month. It should now be `2020/12/08`. + +Next, use the "/" command to search for `date` and jump to the third line. +(The search string does not necessary have to be `date`, but we expect that +jumping to `date` seems to be the easiest this time, since `date` is always in +front of the date we want to change.) +After the jump, run the |.| command right after it. You should see +`2020/11/23` postponed by one month. +> + date: 2020/12/08 + ... + Due date (1st lecture): 2020/12/23 + ... + Due date (2nd lecture): 2020/12/21 + ... + Due date (3rd lecture): 2021/01/14 +< +The important thing here is that if you had pressed instead of |.| here, +the number written as `1st` would have been incremented and become `2st`. Even +if the string `1st` had not been there, the date would have only been extended +by one day (since the cursor position is not at `11`). +While looks for the appropriate rule each time the command is executed, +dot-repeat retains the rule it has done before. In this case, it holds the +operation "Extend date by one month", so the cursor will automatically move to +the nearest "month" of the date and then the same increment operation is +performed. Thus, in the above example, once you have done the search, you can +extend all the due dates by one month just by typing `.n.n.`. + +============================================================================== +INTERFACE *dial-interface* + +------------------------------------------------------------------------------ +MAPPING *dial-mapping* + +["x](dial-increment) *(dial-increment)* + Increments the number of addends under or after the cursor by [count]. + This is an alternative to the existing CTRL-A command in NORMAL mode. + It responds only to addends that are on the same line as the cursor. + Basically, it is not applied to addends that are before the cursor, but + some types of addends may allow incrementing even if they are before the + cursor. + + If the expression register |@=| is specified, the augend group written in + the register is used. If not, `default` group is used + (|dial-config-augends-default|). Specifying any other register has no + effect. + +{VISUAL}["x](dial-increment) *v_(dial-increment)* + An alternative to the existing CTRL-A command in VISUAL mode. + + The behavior changes depending on which of the following modes you are in. + * character-wise VISUAL mode (|v|) + * linweise VISUAL mode (|V|) + * blockwise VISUAL mode (|CTRL-V|) + + In character-wise VISUAL mode, this command increments the number of + addends in the visually selected text by [count]. This is an alternative + to the existing CTRL-A command in VISUAL mode. + + In linweise VISUAL mode (|V|), this command increments the addends in the + selected text by [count] per line. For each line, it searches for the + position of the addend and targets only the one addend that is most + left-hand. + + In linweise VISUAL mode (|V|), this command works similarly in linewise + VISUAL mode, but the target is narrowed down to the selection. + +["x](dial-decrement) *(dial-decrement)* + An alternative to the existing CTRL-X command in NORMAL mode. + +{VISUAL}["x](dial-decrement) *v_(dial-decrement)* + An alternative to the existing CTRL-X command in VISUAL mode. + +{VISUAL}["x]g(dial-increment) *v_g(dial-increment)* + An alternative to the existing |v_g_CTRL-A| command in VISUAL mode. + Its usage is similar to |v_(dial-increment)|, but the number of + addends on line n, counting from the top, is increased by n * [count]. It + is useful when you want to create an equidistant sequence of numbers that + increases by a fixed number, such as a bullet list or calendar. + For example, consider you have the following list of dates: +> + 09/12 + 09/12 + 09/12 + 09/12 +< + If you move the cursor to the second `09/12`, select the bottom three + lines in line selection visual mode, and press `7g`, you get: +> + 09/12 + 09/19 + 09/26 + 10/03 +< + +{VISUAL}["x]g(dial-decrement) *v_g(dial-decrement)* + An alternative to the existing |v_g_CTRL-X| command in VISUAL mode. + +------------------------------------------------------------------------------ +COMMAND *dial-command* + +:[range]DialIncrement {augend} *:DialIncrement* + + Increments the augend specified by {augend} by 1 for the selected range. + It is similar to |v_(dial-increment)| in linewise visual mode, but + allows you to specify and limit the objective augend. Since it is not + possible to specify the addend (i.e., how many to increment), it is + assumed to be repeated using |@:|. + +:[range]DialDecrement {augend} *:DialDecrement* + + Decrements the augend specified by {augend} by 1 for the selected range. + +------------------------------------------------------------------------------ +LUA API *dial-lua-api* + + *dial-lua-api-map* +"dial.map" module~ + +A module that provides specific mappings. + + *dial.map.inc_normal()* +require("dial.map").inc_normal([group_name]) + + Outputs a key sequence for incrementing based on the given `group_name` in + NORMAL mode. If `group_name` is omitted, it is equivalent to `default`. It + is expected to be used in combination with |vim.api| and |nvim_set_keymap| + or |nvim_buf_set_keymap| in the following way: +> + vim.api.nvim_set_keymap( + "n", "", require("dial.map").inc_normal(), {noremap = true} + ) +< + + *dial.map.dec_normal()* +require("dial.map").dec_normal([group_name]) + + Outputs a key sequence for decrementing based on the given `group_name` in + NORMAL mode. + + *dial.map.inc_visual()* +require("dial.map").inc_visual([group_name]) + + Outputs a key sequence for incrementing based on the given `group_name` in + VISUAL mode. + + *dial.map.dec_visual()* +require("dial.map").dec_visual([group_name]) + + Outputs a key sequence for decrementing based on the given `group_name` in + VISUAL mode. + + *dial.map.inc_gvisual()* +require("dial.map").inc_gvisual([group_name]) + + Outputs a key sequence for incrementing based on the given `group_name` in + VISUAL mode. The number of addends on line n, counting from the top, is + increased by n * [count]. + + *dial.map.dec_gvisual()* +require("dial.map").dec_gvisual([group_name]) + + Outputs a key sequence for decrementing based on the given `group_name` in + VISUAL mode. The number of addends on line n, counting from the top, is + decreased by n * [count]. + +"dial.augend" module~ + +A module that defines augends. See |dial-augends|. + +"dial.config" module~ + +A module for configuration. + + *dial.config.augends:register_group()* +require("dial.config").augends:register_group(tbl) + + Defines or overrides a group based on a given table in the dictionary + format. The function argument is a dictionary whose keys are the group + names and whose values are the list of augends. + + *dial.config.augends:get()* +require("dial.config").augends:get(group_name) + + Get the registered groups which have `group_name` as their group name. + + +vim:tw=78:fo=tcq2mM:ts=4:ft=help:norl:noet:fdm=marker:fen diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend.lua new file mode 100644 index 0000000..64c7ba6 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend.lua @@ -0,0 +1,21 @@ +local case = require "dial.augend.case" +local constant = require "dial.augend.constant" +local date = require "dial.augend.date" +local hexcolor = require "dial.augend.hexcolor" +local integer = require "dial.augend.integer" +local semver = require "dial.augend.semver" +local user = require "dial.augend.user" +local paren = require "dial.augend.paren" +local misc = require "dial.augend.misc" + +return { + case = case, + constant = constant, + date = date, + hexcolor = hexcolor, + integer = integer, + semver = semver, + user = user, + paren = paren, + misc = misc, +} diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/case.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/case.lua new file mode 100644 index 0000000..ebe22b7 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/case.lua @@ -0,0 +1,266 @@ +local common = require "dial.augend.common" +local util = require "dial.util" + +local M = {} + +---@alias casetype '"PascalCase"' | '"camelCase"' | '"snake_case"' | '"kebab-case"' | '"SCREAMING_SNAKE_CASE"' +---@alias extractf fun(word: string) -> string[] | nil +---@alias constractf fun(terms: string[]) -> string +---@alias casepattern { word_regex: string, extract: extractf, constract: constractf } + +---@class AugendCase +---@implement Augend +---@field config { types: casetype[], cyclic: boolean } +---@field patterns casepattern[] +local AugendCase = {} + +---@type table +M.case_patterns = {} + +M.case_patterns["camelCase"] = { + word_regex = [[\C\v<([a-z][a-z0-9]*)([A-Z][a-z0-9]*)+>]], + + ---@param word string + ---@return string[] | nil + extract = function(word) + local subwords = {} + local ptr = 1 + for i = 1, word:len(), 1 do + local char = word:sub(i, i) + if not (char == char:lower()) then + -- i 番目の文字が大文字の場合は直前で切る + -- 小文字や数字などは切らない + if i == 1 then + -- ただし最初の文字が大文字になることはないはず + return nil + end + table.insert(subwords, word:sub(ptr, i - 1)) + ptr = i + end + end + table.insert(subwords, word:sub(ptr, word:len())) + return vim.tbl_map(function(s) + return s:lower() + end, subwords) + end, + + ---@param terms string[] + ---@return string + constract = function(terms) + local result = "" + for index, term in ipairs(terms) do + if index == 1 then + result = result .. term + else + result = result .. term:sub(1, 1):upper() .. term:sub(2) + end + end + + return result + end, +} + +M.case_patterns["PascalCase"] = { + word_regex = [[\C\v<([A-Z][a-z0-9]*)+>]], + + ---@param word string + ---@return string[] | nil + extract = function(word) + local subwords = {} + local ptr = 1 + for i = 2, word:len(), 1 do + local char = word:sub(i, i) + if not (char == char:lower()) then + -- i 番目の文字が大文字の場合は直前で切る + -- 小文字や数字などは切らない + table.insert(subwords, word:sub(ptr, i - 1)) + ptr = i + end + end + table.insert(subwords, word:sub(ptr, word:len())) + return vim.tbl_map(function(s) + return s:lower() + end, subwords) + end, + + ---@param terms string[] + ---@return string + constract = function(terms) + local result = "" + for _, term in ipairs(terms) do + result = result .. term:sub(1, 1):upper() .. term:sub(2) + end + + return result + end, +} + +M.case_patterns["snake_case"] = { + word_regex = [[\C\v<([a-z][a-z0-9]*)(_[a-z0-9]*)+>]], + + ---@param word string + ---@return string[] | nil + extract = function(word) + local subwords = {} + local ptr = 1 + for i = 1, word:len(), 1 do + local char = word:sub(i, i) + if char == "_" then + table.insert(subwords, word:sub(ptr, i - 1)) + ptr = i + 1 + end + end + table.insert(subwords, word:sub(ptr, word:len())) + return subwords + end, + + ---@param terms string[] + ---@return string + constract = function(terms) + return table.concat(terms, "_") + end, +} + +M.case_patterns["kebab-case"] = { + word_regex = [[\C\v<([a-z][a-z0-9]*)(-[a-z0-9]*)+>]], + + ---@param word string + ---@return string[] | nil + extract = function(word) + local subwords = {} + local ptr = 1 + for i = 1, word:len(), 1 do + local char = word:sub(i, i) + if char == "-" then + table.insert(subwords, word:sub(ptr, i - 1)) + ptr = i + 1 + end + end + table.insert(subwords, word:sub(ptr, word:len())) + return subwords + end, + + ---@param terms string[] + ---@return string + constract = function(terms) + return table.concat(terms, "-") + end, +} + +M.case_patterns["SCREAMING_SNAKE_CASE"] = { + word_regex = [[\C\v<([A-Z][A-Z0-9]*)(_[A-Z0-9]*)+>]], + + ---@param word string + ---@return string[] | nil + extract = function(word) + local subwords = {} + local ptr = 1 + for i = 1, word:len(), 1 do + local char = word:sub(i, i) + if char == "_" then + table.insert(subwords, word:sub(ptr, i - 1)) + ptr = i + 1 + end + end + table.insert(subwords, word:sub(ptr, word:len())) + return vim.tbl_map(function(s) + return s:lower() + end, subwords) + end, + + ---@param terms string[] + ---@return string + constract = function(terms) + return table.concat(terms, "_"):upper() + end, +} + +---@param config { types: casetype[], cyclic?: boolean } +---@return Augend +function M.new(config) + vim.validate { + cyclic = { config.cyclic, "boolean", true }, + } + if config.cyclic == nil then + config.cyclic = true + end + util.validate_list("types", config.types, function(val) + if + val == "PascalCase" + or val == "camelCase" + or val == "snake_case" + or val == "kebab-case" + or val == "SCREAMING_SNAKE_CASE" + then + return true + end + return false + end) + local patterns = vim.tbl_map(function(type) + return M.case_patterns[type] + end, config.types) + + -- local query = prefix .. util.if_expr(natural, "", "-?") .. "[" .. radix_to_query_character(radix) .. delimiter .. "]+" + return setmetatable({ + patterns = patterns, + config = config, + }, { __index = AugendCase }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendCase:find(line, cursor) + ---@type textrange? + local most_front_range = nil + + for _, caseptn in ipairs(self.patterns) do + ---@type textrange + local range = common.find_pattern_regex(caseptn.word_regex)(line, cursor) + if range ~= nil then + if most_front_range == nil or range.from < most_front_range.from then + most_front_range = range + end + end + end + return most_front_range +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendCase:add(text, addend, cursor) + local len_patterns = #self.patterns + ---@type integer + local index + for i, caseptn in ipairs(self.patterns) do + local range = common.find_pattern_regex(caseptn.word_regex)(text, 1) + if range ~= nil then + index = i + break + end + end + + local terms = self.patterns[index].extract(text) + + local new_index + if self.config.cyclic then + new_index = (len_patterns + (index - 1 + addend) % len_patterns) % len_patterns + 1 + else + new_index = index + addend + if new_index <= 0 then + new_index = 1 + end + if new_index > len_patterns then + new_index = len_patterns + end + end + if new_index == index then + return { cursor = text:len() } + end + text = self.patterns[new_index].constract(terms) + return { text = text, cursor = text:len() } +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/common.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/common.lua new file mode 100644 index 0000000..a499c32 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/common.lua @@ -0,0 +1,76 @@ +-- augend で共通して用いられる関数。 + +local util = require "dial.util" + +local M = {} + +---augend の find field を簡単に実装する。 +---@param ptn string +---@return findf +function M.find_pattern(ptn) + ---@param line string + ---@param cursor? integer + ---@return textrange? + local function f(line, cursor) + local idx_start = 1 + while idx_start <= #line do + local s, e = line:find(ptn, idx_start) + if s then + -- 検索結果があったら + if cursor == nil or cursor <= e then + -- cursor が終了文字より後ろにあったら終了 + return { from = s, to = e } + else + -- 終了文字の後ろから探し始める + idx_start = e + 1 + end + else + -- 検索結果がなければそこで終了 + break + end + end + return nil + end + return f +end + +-- augend の find field を簡単に実装する。 +---@param ptn string +---@return findf +function M.find_pattern_regex(ptn) + ---@param line string + ---@param cursor? integer + ---@return textrange? + local function f(line, cursor) + local idx_start = 1 + while idx_start <= #line do + local s, e = vim.regex(ptn):match_str(line:sub(idx_start)) + + if s then + s = s + idx_start -- 上で得られた s は相対位置なので + e = e + idx_start - 1 -- 上で得られた s は相対位置なので + + -- 検索結果があったら + if cursor == nil or cursor <= e then + -- cursor が終了文字より後ろにあったら終了 + return { from = s, to = e } + else + -- 終了文字の後ろから探し始める + idx_start = e + 1 + end + else + -- 検索結果がなければそこで終了 + break + end + end + return nil + end + return f +end + +---@param elems string[] +function M.enum_to_regex(elems) + return table.concat(elems, [[\|]]) +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/constant.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/constant.lua new file mode 100644 index 0000000..1e5344f --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/constant.lua @@ -0,0 +1,226 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +---@alias AugendConstantConfig { elements: string[], cyclic: boolean, pattern_regexp: string, preserve_case: boolean } + +---@class AugendConstant +---@implement Augend +---@field config AugendConstantConfig +local AugendConstant = {} + +local M = {} + +---@param word string +---@return string +local function to_first_upper(word) + local first_letter = word:sub(1, 1) + local rest = word:sub(2) + return first_letter:upper() .. rest:lower() +end + +---@param word string +---@return "all-lower" | "all-upper" | "first-upper" | nil +local function preserve_case(word) + if word:lower() == word then + return "all-lower" + end + if word:upper() == word then + return "all-upper" + end + if to_first_upper(word) == word then + return "first-upper" + end + return nil +end + +---@param config { elements: string[], word?: boolean, cyclic?: boolean, pattern_regexp?: string, preserve_case?: boolean } +---@return AugendConstant +function M.new(config) + util.validate_list("config.elements", config.elements, "string") + + vim.validate { + word = { config.word, "boolean", true }, + cyclic = { config.cyclic, "boolean", true }, + pattern_regexp = { config.pattern_regexp, "string", true }, + preserve_case = { config.preserve_case, "boolean", true }, + } + if config.preserve_case == nil then + config.preserve_case = false + end + if config.pattern_regexp == nil then + local case_sensitive_flag = util.if_expr(config.preserve_case, [[\c]], [[\C]]) + local word = util.unwrap_or(config.word, true) + if word then + config.pattern_regexp = case_sensitive_flag .. [[\V\<\(%s\)\>]] + else + config.pattern_regexp = case_sensitive_flag .. [[\V\(%s\)]] + end + end + if config.cyclic == nil then + config.cyclic = true + end + return setmetatable({ config = config }, { __index = AugendConstant }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendConstant:find(line, cursor) + local escaped_elements = vim.tbl_map(function(e) + return vim.fn.escape(e, [[/\]]) + end, self.config.elements) + local vim_regex_ptn = self.config.pattern_regexp:format(table.concat(escaped_elements, [[\|]])) + return common.find_pattern_regex(vim_regex_ptn)(line, cursor) +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendConstant:add(text, addend, cursor) + local elements = self.config.elements + local n_patterns = #elements + local n = 1 + + local query + if self.config.preserve_case then + query = function(elem) + return text:lower() == elem:lower() + end + else + query = function(elem) + return text == elem + end + end + + for i, elem in ipairs(elements) do + if query(elem) then + n = i + end + end + if self.config.cyclic then + n = (n + addend - 1) % n_patterns + 1 + else + n = n + addend + if n < 1 then + n = 1 + end + if n > n_patterns then + n = n_patterns + end + end + local new_text = elements[n] + + local case = nil + if self.config.preserve_case then + case = preserve_case(text) + end + if case == "all-lower" then + text = new_text:lower() + elseif case == "all-upper" then + text = new_text:upper() + elseif case == "first-upper" then + text = to_first_upper(new_text) + else + text = new_text + end + + cursor = #text + return { text = text, cursor = cursor } +end + +M.alias = { + bool = M.new { elements = { "true", "false" } }, + alpha = M.new { + elements = { + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + }, + cyclic = false, + }, + Alpha = M.new { + elements = { + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + }, + cyclic = false, + }, + ja_weekday = M.new { + elements = { "日", "月", "火", "水", "木", "金", "土" }, + word = true, + cyclic = true, + }, + ja_weekday_full = M.new { + elements = { "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" }, + word = false, + cyclic = true, + }, + de_weekday = M.new { + elements = { "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So" }, + word = true, + cyclic = true, + }, + de_weekday_full = M.new { + elements = { + "Montag", + "Dienstag", + "Mittwoch", + "Donnerstag", + "Freitag", + "Samstag", + "Sonntag", + }, + word = true, + cyclic = true, + }, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/date.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/date.lua new file mode 100644 index 0000000..d2db67a --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/date.lua @@ -0,0 +1,728 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +local M = {} + +---@alias datekind '"year"' | '"month"' | '"day"' | '"hour"' | '"min"' | '"sec"' +---@alias dttable table +---@alias dateparser fun(string, osdate): osdate +---@alias dateformatter fun(osdate): string +---@alias dateelement {kind?: datekind, regex: string, update_date: dateparser, format?: dateformatter} + +---@param datekind datekind | nil +---@return fun(string, osdate): osdate +local function simple_updater(datekind) + if datekind == nil then + return function(_, date) + return date + end + end + return function(text, date) + date[datekind] = tonumber(text) + return date + end +end + +local WEEKDAYS = { + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", +} +local WEEKDAYS_FULL = { + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +} +local WEEKDAYS_JA = { + "日", + "月", + "火", + "水", + "木", + "金", + "土", +} +local MONTHS = { + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +} +local MONTHS_FULL = { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +} + +---@type table +local date_elements = { + ["Y"] = { + kind = "year", + regex = [[\d\d\d\d]], + update_date = simple_updater "year", + }, + ["y"] = { + kind = "year", + regex = [[\d\d]], + update_date = function(text, date) + date.year = 2000 + tonumber(text) + return date + end, + }, + ["m"] = { + kind = "month", + regex = [[\d\d]], + update_date = simple_updater "month", + }, + ["d"] = { + kind = "day", + regex = [[\d\d]], + update_date = simple_updater "day", + }, + ["H"] = { + kind = "hour", + regex = [[\d\d]], + update_date = simple_updater "hour", + }, + ["I"] = { + kind = "hour", + regex = [[\d\d]], + update_date = function(text, date) + local hour = tonumber(text) + if date.hour < 12 and hour >= 12 then + date.hour = hour - 12 + elseif date.hour >= 12 and hour < 12 then + date.hour = hour + 12 + else + date.hour = hour + end + return date + end, + }, + ["M"] = { + kind = "min", + regex = [[\d\d]], + update_date = simple_updater "min", + }, + ["S"] = { + kind = "sec", + regex = [[\d\d]], + update_date = simple_updater "sec", + }, + + -- with hyphen + ["-y"] = { + kind = "year", + regex = [[\d\{1,2\}]], + update_date = function(text, date) + date.year = 2000 + tonumber(text) + return date + end, + }, + ["-m"] = { + kind = "month", + regex = [[\d\{1,2\}]], + update_date = simple_updater "month", + }, + ["-d"] = { + kind = "day", + regex = [[\d\{1,2\}]], + update_date = simple_updater "day", + }, + ["-H"] = { + kind = "hour", + regex = [[\d\{1,2\}]], + update_date = simple_updater "hour", + }, + ["-I"] = { + kind = "hour", + regex = [[\d\{1,2\}]], + update_date = function(text, date) + local hour = tonumber(text) + if date.hour < 12 and hour >= 12 then + date.hour = hour - 12 + elseif date.hour >= 12 and hour < 12 then + date.hour = hour + 12 + else + date.hour = hour + end + return date + end, + }, + ["-M"] = { + kind = "min", + regex = [[\d\{1,2\}]], + update_date = simple_updater "min", + }, + ["-S"] = { + kind = "sec", + regex = [[\d\{1,2\}]], + update_date = simple_updater "sec", + }, + + -- names + ["a"] = { + kind = nil, + regex = common.enum_to_regex(WEEKDAYS), + update_date = function(_, date) + return date + end, + format = function(time) + local wday = os.date("*t", time).wday --[[ @as integer ]] + return WEEKDAYS[wday] + end, + }, + ["A"] = { + kind = nil, + regex = common.enum_to_regex(WEEKDAYS_FULL), + update_date = function(_, date) + return date + end, + format = function(time) + local wday = os.date("*t", time).wday --[[ @as integer ]] + return WEEKDAYS_FULL[wday] + end, + }, + ["b"] = { + kind = "month", + regex = common.enum_to_regex(MONTHS), + update_date = function(text, date) + for index, value in ipairs(MONTHS) do + if value == text then + date.month = index + end + end + return date + end, + format = function(time) + local month = os.date("*t", time).month --[[ @as integer ]] + return MONTHS[month] + end, + }, + ["B"] = { + kind = "month", + regex = common.enum_to_regex(MONTHS_FULL), + update_date = function(text, date) + for index, value in ipairs(MONTHS_FULL) do + if value == text then + date.month = index + end + end + return date + end, + format = function(time) + local month = os.date("*t", time).month --[[ @as integer ]] + return MONTHS_FULL[month] + end, + }, + ["p"] = { + kind = "hour", + regex = common.enum_to_regex { "AM", "PM" }, + update_date = function(text, date) + if text == "PM" and date.hour < 12 then + date.hour = date.hour + 12 + end + if text == "AM" and date.hour >= 12 then + date.hour = date.hour - 12 + end + return date + end, + -- format = function(time) + -- local hour = os.date("*t", time).hour --[[ @as integer ]] + -- if hour < 12 then + -- return "am" + -- end + -- return "pm" + -- end, + }, + + -- custom + ["J"] = { + kind = nil, + regex = common.enum_to_regex(WEEKDAYS_JA), + update_date = simple_updater(), + format = function(time) + local wday = os.date("*t", time).wday --[[ @as integer ]] + return WEEKDAYS_JA[wday] + end, + }, +} + +---@param pattern string +---@return string[] +---@param custom_date_element_keys string[] +local function parse_date_pattern(pattern, custom_date_element_keys) + local date_elements_keys = vim.tbl_keys(date_elements) --[[@as string[] ]] + + local sequences = {} + + ---@type string + local stack = "" + + for c in util.chars(pattern) do + if vim.startswith(stack, "%(") then + if c == ")" then + local custom_element_name = stack:sub(3) + if vim.tbl_contains(custom_date_element_keys, custom_element_name) then + table.insert(sequences, stack .. ")") + stack = "" + else + error(("Unknown custom elements: %s"):format(custom_element_name)) + end + else + stack = stack .. c + end + elseif stack == "%-" then + if vim.tbl_contains(date_elements_keys, c) then + table.insert(sequences, "%-" .. c) + stack = "" + else + error("Unsupported special character: %-" .. c) + end + elseif stack == "%" then + -- special character + if c == "-" or c == "(" then + stack = "%" .. c + elseif c == "%" then + table.insert(sequences, "%") + stack = "" + elseif vim.tbl_contains(date_elements_keys, c) then + table.insert(sequences, "%" .. c) + stack = "" + else + error("Unsupported special character: %" .. c) + end + else + -- escape character + if c == "%" then + if stack ~= "" then + table.insert(sequences, stack) + end + stack = "%" + else + stack = stack .. c + end + end + end + + if stack ~= "" then + if vim.startswith(stack, "%(") then + error("The end of custom date element was not found:'" .. stack .. "'.") + elseif vim.startswith(stack, "%") then + error("Pattern string cannot end with '" .. stack .. "'.") + else + table.insert(sequences, stack) + stack = "" + end + end + + return sequences +end + +---@class DateFormat +---@field sequences string[] +---@field default_kind datekind +---@field word boolean +---@field custom_date_elements table +local DateFormat = {} + +---Parse date pattern string and create new DateFormat. +---@param pattern string +---@param default_kind datekind +---@param word? boolean +---@param custom_date_elements? table +---@return DateFormat +function DateFormat.new(pattern, default_kind, word, custom_date_elements) + word = util.unwrap_or(word, false) + custom_date_elements = util.unwrap_or(custom_date_elements, {}) + + local custom_date_elements_keys = vim.tbl_keys(custom_date_elements) --[[@as string[] ]] + local sequences = parse_date_pattern(pattern, custom_date_elements_keys) + + return setmetatable( + { sequences = sequences, default_kind = default_kind, word = word, custom_date_elements = custom_date_elements }, + { __index = DateFormat } + ) +end + +---@param pattern string +---@return dateelement +function DateFormat:get_date_elements(pattern) + if vim.startswith(pattern, "%(") and vim.endswith(pattern, ")") then + local custom_element_name = pattern:sub(3, -2) + return self.custom_date_elements[custom_element_name] + elseif vim.startswith(pattern, "%") then + local element_name = pattern:sub(2) + return date_elements[element_name] + else + error(("unknown pattern: '%s'"):format(pattern)) + end +end + +---returns the regex. +---@return string +function DateFormat:regex() + local regexes = vim.tbl_map( + ---@param s string + ---@return string + function(s) + if s == "%" then + return [[%]] + elseif vim.startswith(s, "%") then + return [[\(]] .. self:get_date_elements(s).regex .. [[\)]] + else + return vim.fn.escape(s, [[\]]) + end + end, + self.sequences + ) --[[ @as string[] ]] + + if self.word then + return [[\V\C\<]] .. table.concat(regexes, "") .. [[\>]] + else + return [[\V\C]] .. table.concat(regexes, "") + end +end + +---@param line string +---@param cursor? integer +---@return {range: textrange, dt_info: osdate, kind: datekind}? +function DateFormat:find(line, cursor) + local range = common.find_pattern_regex(self:regex())(line, cursor) + if range == nil then + return nil + end + + -- cursor が nil になるときはカーソルが最初にあるときとみなして良い + if cursor == nil then + cursor = 0 + end + + local matchlist = vim.fn.matchlist(line:sub(range.from, range.to), self:regex()) + local scan_cursor = range.from - 1 + local flag_set_status = scan_cursor >= cursor + local dt_info = os.date("*t", os.time()) --[[@as osdate]] + local datekind = self.default_kind + + local match_idx = 2 + for _, pattern in ipairs(self.sequences) do + ---@type string + if pattern ~= "%" and vim.startswith(pattern, "%") then + local substr = matchlist[match_idx] + scan_cursor = scan_cursor + #substr + local date_element = self:get_date_elements(pattern) + dt_info = date_element.update_date(substr, dt_info) + if scan_cursor >= cursor and not flag_set_status and date_element.kind ~= nil then + datekind = date_element.kind + flag_set_status = true + end + match_idx = match_idx + 1 + else + scan_cursor = scan_cursor + #pattern + end + end + return { range = range, dt_info = dt_info, kind = datekind } +end + +---@param line string +---@param cursor? integer +---@return {range: textrange, dt_info: osdate, kind: datekind}? +function DateFormat:find_with_validity_check(line, cursor) + local find_result = self:find(line, cursor) + if find_result == nil then + return nil + end + local text = line:sub(find_result.range.from, find_result.range.to) + local time = os.time(find_result.dt_info) + local add_result = self:strftime(time, "day") + local correct_text = add_result.text + if correct_text == text then + return find_result + else + return nil + end +end + +---@param time integer +---@param datekind? datekind +---@return addresult +function DateFormat:strftime(time, datekind) + local text = "" + local cursor + for _, pattern in ipairs(self.sequences) do + if pattern ~= "%" and vim.startswith(pattern, "%") then + local date_element = self:get_date_elements(pattern) + if date_element.format ~= nil then + text = text .. date_element.format(time) + else + text = text .. os.date(pattern, time) + end + if date_element.kind == datekind then + cursor = #text + end + else + text = text .. pattern + end + end + if datekind == nil then + return { text = text } + else + return { text = text, cursor = cursor } + end +end + +---@class AugendDate +---@implement Augend +---@field kind datekind +---@field config {pattern: string, default_kind: datekind, only_valid: boolean, word: boolean, clamp: boolean, end_sensitive: boolean, custom_date_elements: dateelement} +---@field date_format DateFormat +local AugendDate = {} + +---@param config {pattern: string, default_kind: datekind, only_valid?: boolean, word?: boolean, clamp?: boolean, end_sensitive?: boolean, custom_date_elements?: table} +---@return AugendDate +function M.new(config) + vim.validate { + pattern = { config.pattern, "string" }, + default_kind = { config.default_kind, "string" }, + only_valid = { config.only_valid, "boolean", true }, + word = { config.word, "boolean", true }, + clamp = { config.clamp, "boolean", true }, + end_sensitive = { config.end_sensitive, "boolean", true }, + custom_date_elements = { config.custom_date_elements, "table", true }, + } + + config.only_valid = util.unwrap_or(config.only_valid, false) + config.word = util.unwrap_or(config.word, false) + config.clamp = util.unwrap_or(config.clamp, false) + config.end_sensitive = util.unwrap_or(config.end_sensitive, false) + + local date_format = DateFormat.new(config.pattern, config.default_kind, config.word, config.custom_date_elements) + + return setmetatable( + { config = config, kind = config.default_kind, date_format = date_format }, + { __index = AugendDate } + ) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendDate:find(line, cursor) + local find_result + if self.config.only_valid then + find_result = self.date_format:find_with_validity_check(line, cursor) + else + find_result = self.date_format:find(line, cursor) + end + if find_result == nil then + return nil + end + return find_result.range +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendDate:find_stateful(line, cursor) + local find_result + if self.config.only_valid then + find_result = self.date_format:find_with_validity_check(line, cursor) + else + find_result = self.date_format:find(line, cursor) + end + if find_result == nil then + return nil + end + self.kind = find_result.kind + return find_result.range +end + +---@param year integer +---@param month integer +---@return integer +local function calc_end_day(year, month) + if month == 4 or month == 6 or month == 9 or month == 11 then + return 30 + elseif month == 2 then + if year % 400 == 0 or (year % 4 == 0 and year % 100 ~= 0) then + return 29 + else + return 28 + end + else + return 31 + end +end + +---@param dt_info osdate +---@param kind datekind +---@param addend integer +---@param clamp boolean +---@param end_sensitive boolean +---@return osdate +local function update_dt_info(dt_info, kind, addend, clamp, end_sensitive) + if kind ~= "year" and kind ~= "month" then + dt_info[kind] = dt_info[kind] + addend + return dt_info + end + + local end_day_before_add = calc_end_day(dt_info.year, dt_info.month) + local day_before_add = dt_info.day + dt_info.day = 1 + dt_info[kind] = dt_info[kind] + addend + -- update date information to existent one + dt_info = os.date("*t", os.time(dt_info)) --[[@as osdate]] + local end_day_after_add = calc_end_day(dt_info.year, dt_info.month) + + if end_sensitive and end_day_before_add == day_before_add then + dt_info.day = end_day_after_add + elseif clamp and day_before_add > end_day_after_add then + dt_info.day = end_day_after_add + else + dt_info.day = day_before_add + end + return dt_info +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendDate:add(text, addend, cursor) + local find_result = self.date_format:find(text) + if find_result == nil or self.kind == nil then + return {} + end + local dt_info = find_result.dt_info + + dt_info = update_dt_info(dt_info, self.kind, addend, self.config.clamp, self.config.end_sensitive) + -- dt_info[self.kind] = dt_info[self.kind] + addend + local time = os.time(dt_info) + + return self.date_format:strftime(time, self.kind) +end + +M.alias = {} + +M.alias["%Y/%m/%d"] = M.new { + pattern = "%Y/%m/%d", + default_kind = "day", + only_valid = false, +} + +M.alias["%d/%m/%Y"] = M.new { + pattern = "%d/%m/%Y", + default_kind = "day", + only_valid = true, +} + +M.alias["%d/%m/%y"] = M.new { + pattern = "%d/%m/%y", + default_kind = "day", + only_valid = true, +} + +M.alias["%m/%d/%Y"] = M.new { + pattern = "%m/%d/%Y", + default_kind = "day", + only_valid = true, +} + +M.alias["%m/%d/%y"] = M.new { + pattern = "%m/%d/%y", + default_kind = "day", + only_valid = true, +} + +M.alias["%m/%d"] = M.new { + pattern = "%m/%d", + default_kind = "day", + only_valid = false, +} + +M.alias["%Y-%m-%d"] = M.new { + pattern = "%Y-%m-%d", + default_kind = "day", + only_valid = false, +} + +M.alias["%-m/%-d"] = M.new { + pattern = "%-m/%-d", + default_kind = "day", + only_valid = true, +} + +M.alias["%Y年%-m月%-d日"] = M.new { + pattern = "%Y年%-m月%-d日", + default_kind = "day", + only_valid = false, +} + +M.alias["%Y年%-m月%-d日(%ja)"] = M.new { + pattern = "%Y年%-m月%-d日(%J)", + default_kind = "day", + only_valid = false, +} + +M.alias["%d.%m.%Y"] = M.new { + pattern = "%d.%m.%Y", + default_kind = "day", + only_valid = true, +} + +M.alias["%d.%m.%y"] = M.new { + pattern = "%d.%m.%y", + default_kind = "day", + only_valid = true, +} + +M.alias["%d.%m."] = M.new { + pattern = "%d.%m.", + default_kind = "day", + only_valid = true, +} + +M.alias["%-d.%-m."] = M.new { + pattern = "%-d.%-m.", + default_kind = "day", + only_valid = true, +} + +M.alias["%H:%M:%S"] = M.new { + pattern = "%H:%M:%S", + default_kind = "sec", + only_valid = true, +} + +M.alias["%H:%M"] = M.new { + pattern = "%H:%M", + default_kind = "sec", + only_valid = true, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/hexcolor.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/hexcolor.lua new file mode 100644 index 0000000..3bd4701 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/hexcolor.lua @@ -0,0 +1,94 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +local function cast_u8(n) + if n <= 0 then + return 0 + end + if n >= 255 then + return 255 + end + return n +end + +---@alias colorcase '"upper"' | '"lower"' +---@alias colorkind '"r"' | '"g"' | '"b"' | '"all"' + +---@class AugendHexColor +---@implement Augend +---@field datefmt datefmt +---@field kind colorkind +local AugendHexColor = {} + +local M = {} + +---@param config { case: colorcase } +---@return Augend +function M.new(config) + vim.validate { + case = { config.case, "string", true }, + } + + return setmetatable({ config = config, kind = "all" }, { __index = AugendHexColor }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendHexColor:find(line, cursor) + return common.find_pattern "#%x%x%x%x%x%x"(line, cursor) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendHexColor:find_stateful(line, cursor) + local range = common.find_pattern "#%x%x%x%x%x%x"(line, cursor) + if range == nil then + return + end + local relcurpos = cursor - range.from + 1 + if relcurpos <= 1 then + self.kind = "all" + elseif relcurpos <= 3 then + self.kind = "r" + elseif relcurpos <= 5 then + self.kind = "g" + else + self.kind = "b" + end + return range +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendHexColor:add(text, addend, cursor) + local r = tonumber(text:sub(2, 3), 16) + local g = tonumber(text:sub(4, 5), 16) + local b = tonumber(text:sub(6, 7), 16) + if cursor == nil then + cursor = 1 + end -- default: all + if self.kind == "all" then + -- increment all + r = cast_u8(r + addend) + g = cast_u8(g + addend) + b = cast_u8(b + addend) + cursor = 1 + elseif self.kind == "r" then + r = cast_u8(r + addend) + cursor = 3 + elseif self.kind == "g" then + g = cast_u8(g + addend) + cursor = 5 + else + b = cast_u8(b + addend) + cursor = 7 + end + text = "#" .. string.format("%02x", r) .. string.format("%02x", g) .. string.format("%02x", b) + return { text = text, cursor = cursor } +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/integer.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/integer.lua new file mode 100644 index 0000000..4f35760 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/integer.lua @@ -0,0 +1,180 @@ +local common = require "dial.augend.common" +local util = require "dial.util" + +---@alias AugendIntegerConfig {} + +---@class AugendInteger +---@implement Augend +---@field radix integer +---@field prefix string +---@field natural boolean +---@field query string +---@field case '"upper"' | '"lower"' +---@field delimiter string +---@field delimiter_digits integer +local AugendInteger = {} + +local M = {} + +---convert integer with given prefix +---@param n integer +---@param radix integer +---@param case '"upper"' | '"lower"' +---@return string +local function tostring_with_radix(n, radix, case) + local floor, insert = math.floor, table.insert + n = floor(n) + if not radix or radix == 10 then + return tostring(n) + end + + local digits = "0123456789abcdefghijklmnopqrstuvwxyz" + if case == "upper" then + digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + end + + local t = {} + local sign = "" + if n < 0 then + sign = "-" + n = -n + end + repeat + local d = (n % radix) + 1 + n = floor(n / radix) + insert(t, 1, digits:sub(d, d)) + until n == 0 + return sign .. table.concat(t, "") +end + +---デリミタを挟んだ数字を出力する。 +---@param digits string +---@param delimiter string +---@param delimiter_digits integer +---@return string +local function add_delimiter(digits, delimiter, delimiter_digits) + local blocks = {} + if #digits <= delimiter_digits then + return digits + end + for i = 0, #digits - 1, delimiter_digits do + -- 部分文字列の終端は右から i 桁目 + local e = #digits - i + -- e から数えて delimiter_digits の数だけ取る + local s = e - delimiter_digits + 1 + if s < 1 then + s = 1 + end + table.insert(blocks, 1, digits:sub(s, e)) + end + return table.concat(blocks, delimiter) +end + +---@param radix integer +---@return string +local function radix_to_query_character(radix) + if radix < 2 or radix > 36 then + error(("radix must satisfy 2 <= radix <= 36, got %d"):format(radix)) + end + if radix <= 10 then + return "0-" .. tostring(radix - 1) + end + return "0-9a-" .. string.char(86 + radix) .. "A-" .. string.char(54 + radix) +end + +---@param config { radix?: integer, prefix?: string, natural?: boolean, case?: '"upper"' | '"lower"', delimiter?: string, delimiter_digits?: number } +---@return Augend +function M.new(config) + vim.validate { + radix = { config.radix, "number", true }, + prefix = { config.prefix, "string", true }, + natural = { config.natural, "boolean", true }, + case = { config.case, "string", true }, + delimiter = { config.delimiter, "string", true }, + delimiter_digits = { config.delimiter_digits, "number", true }, + } + local radix = util.unwrap_or(config.radix, 10) + local prefix = util.unwrap_or(config.prefix, "") + local natural = util.unwrap_or(config.natural, true) + local case = util.unwrap_or(config.case, "lower") + local delimiter = util.unwrap_or(config.delimiter, "") + local delimiter_digits = util.unwrap_or(config.delimiter_digits, 3) + + -- local query = prefix .. util.if_expr(natural, "", "-?") .. "[" .. radix_to_query_character(radix) .. delimiter .. "]+" + local query = ([[\V%s%s\(\[%s]\+%s\)\*\[%s]\+]]):format( + prefix, + util.if_expr(natural, "", [[-\?]]), + radix_to_query_character(radix), + vim.fn.escape(delimiter, [[]\/]]), + radix_to_query_character(radix) + ) + + return setmetatable({ + radix = radix, + prefix = prefix, + natural = natural, + query = query, + case = case, + delimiter = delimiter, + delimiter_digits = delimiter_digits, + }, { __index = AugendInteger }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendInteger:find(line, cursor) + return common.find_pattern_regex(self.query)(line, cursor) +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendInteger:add(text, addend, cursor) + local n_prefix = #self.prefix + local subtext = text:sub(n_prefix + 1) + if self.delimiter ~= "" then + local ptn = self.delimiter + if ptn == "." or ptn == "%" or ptn == "^" or ptn == "$" then + ptn = "%" .. ptn + end + subtext = text:gsub(ptn, "") + end + local n = tonumber(subtext, self.radix) + local n_string_digit = subtext:len() + -- local n_actual_digit = tostring(n):len() + local n_actual_digit = tostring_with_radix(n, self.radix, self.case):len() + n = n + addend + if self.natural and n < 0 then + n = 0 + end + local digits + if n_string_digit == n_actual_digit then + -- 増減前の数字が0か0始まりでない数字だったら + -- text = ("%d"):format(n) + digits = tostring_with_radix(n, self.radix, self.case) + else + -- 増減前の数字が0始まりの正の数だったら + -- text = ("%0" .. n_string_digit .. "d"):format(n) + local num_string = tostring_with_radix(n, self.radix, self.case) + local pad = ("0"):rep(math.max(n_string_digit - num_string:len(), 0)) + digits = pad .. num_string + end + if self.delimiter ~= "" then + digits = add_delimiter(digits, self.delimiter, self.delimiter_digits) + end + text = self.prefix .. digits + cursor = #text + return { text = text, cursor = cursor } +end + +M.alias = { + decimal = M.new {}, + decimal_int = M.new { natural = false }, + binary = M.new { radix = 2, prefix = "0b", natural = true }, + octal = M.new { radix = 8, prefix = "0o", natural = true }, + hex = M.new { radix = 16, prefix = "0x", natural = true }, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/misc.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/misc.lua new file mode 100644 index 0000000..253944d --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/misc.lua @@ -0,0 +1,40 @@ +local util = require "dial.util" +local common = require "dial.augend.common" +local user = require "dial.augend.user" + +local M = {} + +M.alias = {} + +M.alias.markdown_header = user.new { + ---@param line string + ---@param cursor? integer + ---@return textrange? + find = function(line, cursor) + local header_mark_s, header_mark_e = line:find "^#+" + if header_mark_s == nil or header_mark_e >= 7 then + return nil + end + return { from = header_mark_s, to = header_mark_e } + end, + + ---@param text string + ---@param addend integer + ---@param cursor? integer + ---@return { text?: string, cursor?: integer } + add = function(text, addend, cursor) + local n = #text + n = n + addend + if n < 1 then + n = 1 + end + if n > 6 then + n = 6 + end + text = ("#"):rep(n) + cursor = 1 + return { text = text, cursor = cursor } + end, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/paren.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/paren.lua new file mode 100644 index 0000000..13f938f --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/paren.lua @@ -0,0 +1,243 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +---@class AugendParen +---@implement Augend +---@field config { patterns: string[][], } +---@field find_pattern string +local AugendParen = {} + +local M = {} + +---text の i 文字目から先が ptn から始まっていたら true を、そうでなければ false を返す。 +---@param text string +---@param ptn string +---@param idx integer +---@return boolean +local function precedes(text, ptn, idx) + return text:sub(idx, idx + #ptn - 1) == ptn +end + +---括弧のペアを見つける。 +---@param line string +---@param open string +---@param close string +---@param nested boolean +---@param cursor_idx integer +---@param escape_char? string +---@return textrange? +local function find_nested_paren(line, open, close, nested, cursor_idx, escape_char) + local depth_at_cursor = nil + local start_idx_stack = {} + local escaped = false + + -- idx: 探索している場所 + local idx = 1 + while idx <= #line do + -- util.dbg{ + -- idx = idx, + -- depth_at_cursor = depth_at_cursor, + -- start_idx_stack = start_idx_stack, + -- escaped = escaped, + -- } + + -- idx が cursor_idx を超えた瞬間に paren_nested_level_at_cursor を記録 + if depth_at_cursor == nil and idx >= cursor_idx then + -- util.dbg"cursor detected!" + depth_at_cursor = #start_idx_stack + end + + -- idx を増やしつつ走査。 + -- 括弧の open, close または escape char に当たったら特別処理を入れる。 + -- open と close が同じパターン列の場合は close を優先。 + local from, to = (function() + -- escape 文字: escaped のトグルを行う + if escape_char ~= nil and precedes(line, escape_char, idx) then + -- util.dbg"escape char detected!" + idx = idx + #escape_char + escaped = not escaped + return nil + end + + -- close 文字: 括弧の終了 + if #start_idx_stack >= 1 and precedes(line, close, idx) then + -- 括弧が閉じきっていないときに close が見つかったら stack を pop + -- util.dbg"close char detected!" + idx = idx + #close + local close_end_idx = idx - 1 + + -- idx が cursor_idx を超えた瞬間に paren_nested_level_at_cursor を記録 + if depth_at_cursor == nil and close_end_idx >= cursor_idx then + -- util.dbg"cursor detected!" + depth_at_cursor = #start_idx_stack + end + if escaped then + escaped = false + return nil + end + escaped = false + local start_idx = table.remove(start_idx_stack, #start_idx_stack) + + -- カーソル下の深さの括弧を抜けたらその時点で探索終了 + if depth_at_cursor ~= nil and #start_idx_stack <= depth_at_cursor then + return start_idx, idx - 1 + end + return nil + end + + -- open 文字: 括弧の開始 + if precedes(line, open, idx) then + -- util.dbg"open char detected!" + idx = idx + #open + if escaped then + escaped = false + return nil + end + escaped = false + + if nested or #start_idx_stack == 0 then + table.insert(start_idx_stack, idx - #open) + end + return nil + end + escaped = false + idx = idx + 1 + end)() + + -- range が見つかった場合は速やかに本関数から return する + if from ~= nil then + return { from = from, to = to } + end + end + + -- nest がすべて解決しなかったので nil を返す + return nil +end + +---@param config { patterns?: string[][], escape_char?: string, cyclic?: boolean, nested?: boolean } +---@return Augend +function M.new(config) + if config.patterns == nil then + config.patterns = { { [[']], [[']] }, { [["]], [["]] } } + end + if config.nested == nil then + config.nested = true + end + if config.cyclic == nil then + config.cyclic = true + end + vim.validate { + cyclic = { config.cyclic, "boolean" }, + } + util.validate_list("patterns", config.patterns, "table") + + return setmetatable({ config = config }, { __index = AugendParen }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendParen:find(line, cursor) + ---@type textrange? + local tmp_range = nil + + for _, ptn in ipairs(self.config.patterns) do + local open = ptn[1] + local close = ptn[2] + local range = find_nested_paren(line, open, close, self.config.nested, cursor, self.config.escape_char) + if range ~= nil then + if tmp_range == nil then + tmp_range = range + else + local rel = range.from > cursor + local tmp_rel = tmp_range.from > cursor + if tmp_rel and rel then + tmp_range = util.if_expr(tmp_range.from < range.from, tmp_range, range) + elseif tmp_rel and not rel then + tmp_range = range + elseif not tmp_rel and rel then + else + tmp_range = util.if_expr(tmp_range.from > range.from, tmp_range, range) + end + end + end + end + + return tmp_range +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendParen:add(text, addend, cursor) + local n_patterns = #self.config.patterns + local n = 1 + for i, elem in ipairs(self.config.patterns) do + local open = elem[1] + if precedes(text, open, 1) then + n = i + break + end + end + local old_paren_pair = self.config.patterns[n] + -- util.dbg{old_paren_pair = old_paren_pair, text = text} + local text_inner = text:sub(#old_paren_pair[1] + 1, #text - #old_paren_pair[2]) + + if self.config.cyclic then + n = (n + addend - 1) % n_patterns + 1 + else + n = n + addend + if n < 1 then + n = 1 + end + if n > n_patterns then + n = n_patterns + end + end + local new_paren_pair = self.config.patterns[n] + local new_paren_open = new_paren_pair[1] + local new_paren_close = new_paren_pair[2] + + text = new_paren_open .. text_inner .. new_paren_close + cursor = #text + return { text = text, cursor = cursor } +end + +M.alias = { + quote = M.new { + patterns = { { "'", "'" }, { '"', '"' } }, + nested = false, + escape_char = [[\]], + cyclic = true, + }, + brackets = M.new { + patterns = { { "(", ")" }, { "[", "]" }, { "{", "}" } }, + nested = true, + cyclic = true, + }, + lua_str_literal = M.new { + patterns = { + { "'", "'" }, + { '"', '"' }, + { "[[", "]]" }, + { "[=[", "]=]" }, + { "[==[", "]==]" }, + { "[===[", "]===]" }, + }, + nested = false, + cyclic = false, + }, + rust_str_literal = M.new { + patterns = { + { '"', '"' }, + { 'r#"', '"#' }, + { 'r##"', '"##' }, + { 'r###"', '"###' }, + }, + nested = false, + cyclic = false, + }, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/semver.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/semver.lua new file mode 100644 index 0000000..4d43835 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/semver.lua @@ -0,0 +1,107 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +local function cast_u8(n) + if n <= 0 then + return 0 + end + if n >= 255 then + return 255 + end + return n +end + +---@class AugendSemver +---@implement Augend +---@field kind '"major"' | '"minor"' | '"patch"' +local AugendSemver = {} + +local M = {} + +---@param config {} +---@return Augend +function M.new(config) + vim.validate {} + + return setmetatable({ kind = "patch" }, { __index = AugendSemver }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendSemver:find(line, cursor) + return common.find_pattern "%d+%.%d+%.%d+"(line, cursor) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendSemver:find_stateful(line, cursor) + local range = common.find_pattern "%d+%.%d+%.%d+"(line, cursor) + if range == nil then + return + end + + if cursor == nil then + -- always increments patch version in VISUAL mode + self.kind = "patch" + return range + end + local relcurpos = cursor - range.from + 1 + local text = line:sub(range.from, range.to) + local iterator = text:gmatch "%d+" + local major = iterator() + local minor = iterator() + + if relcurpos <= 0 then + self.kind = "patch" + elseif relcurpos <= #major then + self.kind = "major" + elseif relcurpos <= #major + #minor + 1 then + self.kind = "minor" + else + self.kind = "patch" + end + return range +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendSemver:add(text, addend, cursor) + local iterator = text:gmatch "%d+" + local major = tonumber(iterator()) + local minor = tonumber(iterator()) + local patch = tonumber(iterator()) + + if cursor == nil then + cursor = 0 + end -- default: all + + if self.kind == "major" then + major = major + addend + if addend > 0 then + minor = 0 + patch = 0 + end + cursor = #tostring(major) + elseif self.kind == "minor" then + minor = minor + addend + if addend > 0 then + patch = 0 + end + cursor = #tostring(major) + 1 + #tostring(minor) + else -- (if cursor == 6 or cursor == 7 then) + patch = patch + addend + cursor = #tostring(major) + 1 + #tostring(minor) + 1 + #tostring(patch) + end + text = ("%d.%d.%d"):format(major, minor, patch) + return { text = text, cursor = cursor } +end + +M.alias = { + semver = M.new {}, +} + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/user.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/user.lua new file mode 100644 index 0000000..6adcb1b --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/augend/user.lua @@ -0,0 +1,37 @@ +local util = require "dial.util" +local common = require "dial.augend.common" + +---@alias AugendUserConfig { find: findf, add: addf } + +---@class AugendUser +---@implement Augend +---@field config AugendUserConfig +local AugendUser = {} + +---@param config AugendUserConfig +---@return Augend +function AugendUser.new(config) + vim.validate { + find = { config.find, "function" }, + add = { config.add, "function" }, + } + + return setmetatable({ config = config }, { __index = AugendUser }) +end + +---@param line string +---@param cursor? integer +---@return textrange? +function AugendUser:find(line, cursor) + return self.config.find(line, cursor) +end + +---@param text string +---@param addend integer +---@param cursor? integer +---@return { text?: string, cursor?: integer } +function AugendUser:add(text, addend, cursor) + return self.config.add(text, addend, cursor) +end + +return AugendUser diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/command.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/command.lua new file mode 100644 index 0000000..8dfc87c --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/command.lua @@ -0,0 +1,224 @@ +-- Interface between Neovim and dial.nvim. +-- Functions in this module edits the buffer and read variables defined by Neovim. + +local config = require "dial.config" +local handler = require("dial.handle").new() +local util = require "dial.util" + +local M = {} + +VISUAL_BLOCK = string.char(22) + +---Select the most appropriate augend from given augend group (in NORMAL mode). +---@param group_name? string +function M.select_augend_normal(group_name) + if group_name == nil and vim.v.register == "=" then + group_name = vim.fn.getreg("=", 1) + else + group_name = util.unwrap_or(group_name, "default") + end + local augends = config.augends.group[group_name] + if augends == nil then + error(("undefined augend group name: %s"):format(group_name)) + end + + local count = vim.v.count + if count ~= 0 then + handler:set_count(count) + else + handler:set_count(1) + end + local col = vim.fn.col "." + local line = vim.fn.getline "." + handler:select_augend(line, col, augends) +end + +---Select the most appropriate augend from given augend group (in VISUAL mode). +---@param group_name? string +function M.select_augend_visual(group_name) + if group_name == nil and vim.v.register == "=" then + group_name = vim.fn.getreg("=", 1) + else + group_name = util.unwrap_or(group_name, "default") + end + local augends = config.augends.group[group_name] + if augends == nil then + error(("undefined augend group name: %s"):format(group_name)) + end + + local count = vim.v.count + if count ~= 0 then + handler:set_count(count) + else + handler:set_count(1) + end + + local mode = vim.fn.mode(0) + ---@type integer + local _, line1, col1, _ = unpack(vim.fn.getpos "v") + ---@type integer + local _, line2, col2, _ = unpack(vim.fn.getpos ".") + + if mode == "V" then + -- line-wise visual mode + local line_min = math.min(line1, line2) + local line_max = math.max(line1, line2) + local lines = {} + for line_num = line_min, line_max, 1 do + table.insert(lines, vim.fn.getline(line_num)) + end + + handler:select_augend_visual(lines, nil, augends) + elseif mode == VISUAL_BLOCK then + -- block-wise visual mode + local line_min = math.min(line1, line2) + local line_max = math.max(line1, line2) + local col_min = math.min(col1, col2) + local col_max = math.max(col1, col2) + local lines = {} + for line_num = line_min, line_max, 1 do + local line = vim.fn.getline(line_num) + table.insert(lines, line:sub(col_min)) + end + + handler:select_augend_visual(lines, nil, augends) + else + -- char-wise visual mode + local line_min = math.min(line1, line2) + local col_min = math.min(col1, col2) + ---@type string + local text = vim.fn.getline(line_min) + if line1 == line2 then + local col_max = math.max(col1, col2) + text = text:sub(col_min, col_max) + else + text = text:sub(col_min) + end + handler:select_augend(text, nil, augends) + end +end + +---Select the most appropriate augend from given augend group (in VISUAL mode with g). +---@param group_name? string +function M.select_augend_gvisual(group_name) + M.select_augend_visual(group_name) +end + +---The process that runs when operator is called (in NORMAL mode). +---@param direction direction +function M.operator_normal(direction) + local col = vim.fn.col "." + local line_num = vim.fn.line "." + local line = vim.fn.getline "." + + local result = handler:operate(line, col, direction) + + if result.line ~= nil then + vim.fn.setline(".", result.line) + end + if result.cursor ~= nil then + vim.fn.cursor { line_num, result.cursor } + end +end + +---The process that runs when operator is called (in VISUAL mode). +---@param direction direction +---@param stairlike boolean +function M.operator_visual(direction, stairlike) + local mode = vim.fn.visualmode(0) + local _, line1, col1, _ = unpack(vim.fn.getpos "'[") + local _, line2, col2, _ = unpack(vim.fn.getpos "']") + local tier = 1 + + ---@param lnum integer + ---@param range {from: integer, to?: integer} + local function operate_line(lnum, range) + local line = vim.fn.getline(lnum) + local result = handler:operate_visual(line, range, direction, tier) + if result.line ~= nil then + vim.fn.setline(lnum, result.line) + if stairlike then + tier = tier + 1 + end + end + end + + local line_start = util.if_expr(line1 < line2, line1, line2) + local line_end = util.if_expr(line1 < line2, line2, line1) + + if mode == "v" then + local col_start = util.if_expr(line1 < line2, col1, col2) + local col_end = util.if_expr(line1 < line2, col2, col1) + if line_start == line_end then + operate_line(line_start, { from = math.min(col1, col2), to = math.max(col1, col2) }) + else + local lnum = line_start + operate_line(lnum, { from = col_start }) + for idx = line_start + 1, line_end - 1, 1 do + operate_line(idx, { from = 1 }) + end + operate_line(line_end, { from = 1, to = col_end }) + end + elseif mode == "V" then + for lnum = line_start, line_end, 1 do + operate_line(lnum, { from = 1 }) + end + else + local col_start = util.if_expr(col1 < col2, col1, col2) + local col_end = util.if_expr(col1 < col2, col2, col1) + for lnum = line_start, line_end, 1 do + operate_line(lnum, { from = col_start, to = col_end }) + end + end +end + +---The process that runs when text object is called. +---Call handler.findTextRange() to select a range based on the information in the current line. +---Also, for dot repeat, it receives the value of the specified counter and updates the addend. +function M.textobj() + local count = vim.v.count + if count ~= 0 then + handler:set_count(count) + end + local col = vim.fn.col "." + local line = vim.fn.getline "." + + handler:find_text_range(line, col) +end + +function M.command(direction, line_range, groups) + local group_name = groups[1] + if group_name == nil and vim.v.register == "=" then + group_name = vim.fn.getreg("=", 1) + else + group_name = util.unwrap_or(group_name, "default") + end + local augends = config.augends.group[group_name] + if augends == nil then + error(("undefined augend group name: %s"):format(group_name)) + end + + local line_min = line_range.from + local line_max = line_range.to + local lines = {} + for line_num = line_min, line_max, 1 do + table.insert(lines, vim.fn.getline(line_num)) + end + handler:select_augend_visual(lines, nil, augends) + + ---@param lnum integer + ---@param range {from: integer, to?: integer} + local function operate_line(lnum, range) + local line = vim.fn.getline(lnum) + local result = handler:operate_visual(line, range, direction, 1) + if result.line ~= nil then + vim.fn.setline(lnum, result.line) + end + end + + for lnum = line_min, line_max, 1 do + operate_line(lnum, { from = 1 }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/config.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/config.lua new file mode 100644 index 0000000..083c0bc --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/config.lua @@ -0,0 +1,58 @@ +local augend = require "dial.augend" +local util = require "dial.util" +local M = {} + +---@class augends +---@field group table +M.augends = { + group = { + default = { + augend.integer.alias.decimal, + augend.integer.alias.hex, + augend.date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%Y-%m-%d", + default_kind = "day", + }, + augend.date.new { + pattern = "%m/%d", + default_kind = "day", + only_valid = true, + }, + augend.date.new { + pattern = "%H:%M", + default_kind = "day", + only_valid = true, + }, + augend.constant.alias.ja_weekday_full, + }, + }, +} + +---新しいグループを登録する。 +---@param tbl table +function M.augends:register_group(tbl) + -- TODO: validate augends + + for name, augends in pairs(tbl) do + local nil_keys = util.index_with_nil_value(augends) + + if #nil_keys ~= 0 then + local str_nil_keys = table.concat(nil_keys, ", ") + error(("Failed to register augend group '%s'. it contains nil at index %s."):format(name, str_nil_keys)) + end + + self.group[name] = augends + end +end + +---グループを取得する。 +---@param group_name string +function M.augends:get(group_name) + return self.group[group_name] +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/handle.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/handle.lua new file mode 100644 index 0000000..18bd048 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/handle.lua @@ -0,0 +1,252 @@ +-- The business logic of dial.nvim. +-- To achieve dot repeating, dial.nvim divides the increment/decrement process +-- into the following three parts: +-- +-- 1. Select the rule: +-- determine the augend rule to increment/decrement from the current line and cursor position. +-- +-- 2. Select the range: +-- determine the range of strings (text object) on the buffer +-- to be incremented/decremented based on the rule determined above. +-- +-- 3. Edit the buffer: +-- actually increment/decrement the string (operator). +-- +-- In NORMAL mode /, 1, 2, and 3 are called. +-- In NORMAL mode dot repeating, only 2 and 3 are called. +-- +-- `Handler` class, defined in this module, saves information such as augend rule +-- and text range as a state, and performs the actual increment/decrement operation +-- by calling the augend function. +-- Text on buffers is not manipulated from the handler instance. + +---Scores used to determine which rules to operate. +---@class Score +---@field cursor_loc integer +---@field start_pos integer +---@field neg_end_pos integer +local Score = {} + +local util = require "dial.util" + +---constructor +---@param cursor_loc integer +---@param start_pos integer +---@param neg_end_pos integer +---@return Score +function Score.new(cursor_loc, start_pos, neg_end_pos) + return setmetatable( + { cursor_loc = cursor_loc, start_pos = start_pos, neg_end_pos = neg_end_pos }, + { __index = Score } + ) +end + +---Calculate the score. +---@param s integer +---@param e integer +---@param cursor? integer +---@return {cursor_loc: integer, start_pos: integer, neg_end_pos: integer} +local function calc_score(s, e, cursor) + local cursor_loc + if (cursor or 0) > e then + cursor_loc = 2 + elseif (cursor or 0) < s then + cursor_loc = 1 + else + cursor_loc = 0 + end + return { cursor_loc = cursor_loc, start_pos = s, neg_end_pos = -e } +end + +---Calculate the score from the cursor position and text range. +---@param s integer +---@param e integer +---@param cursor? integer +---@return Score +function Score.from_cursor(s, e, cursor) + local tbl = calc_score(s, e, cursor) + return setmetatable(tbl, { __index = Score }) +end + +---Compare the score. +---If and only if `self` has the higher priority than `rhs`, returns true. +---@param rhs Score +function Score.cmp(self, rhs) + if self.cursor_loc < rhs.cursor_loc then + return true + end + if self.cursor_loc > rhs.cursor_loc then + return false + end + if self.start_pos < rhs.start_pos then + return true + end + if self.start_pos > rhs.start_pos then + return false + end + if self.neg_end_pos < rhs.neg_end_pos then + return true + end + if self.neg_end_pos > rhs.neg_end_pos then + return false + end + return false +end + +---@class Handler +---@field count integer +---@field range textrange? +---@field active_augend Augend? +local Handler = {} + +function Handler.new() + return setmetatable({ count = 1, range = nil, active_augend = nil }, { __index = Handler }) +end + +---Get addend value. +---@param direction direction +---@return integer +function Handler:get_addend(direction) + if direction == "increment" then + return self.count + else + return -self.count + end +end + +---Set count value. +---@param count integer +function Handler:set_count(count) + self.count = count +end + +---Select the most appropriate augend (in NORMAL mode). +---@param line string +---@param cursor? integer +---@param augends Augend[] +function Handler:select_augend(line, cursor, augends) + local interim_augend = nil + local interim_score = Score.new(3, 0, 0) -- score with the lowest priority + + for _, augend in ipairs(augends) do + (function() + ---@type textrange? + local range = nil + if augend.find_stateful == nil then + range = augend:find(line, cursor) + else + range = augend:find_stateful(line, cursor) + end + if range == nil then + return + end + local score = Score.from_cursor(range.from, range.to, cursor) + if score:cmp(interim_score) then + interim_augend = augend + interim_score = score + end + end)() + end + self.active_augend = interim_augend +end + +---Select the most appropriate augend (in VISUAL mode). +---@param lines string[] +---@param cursor? integer +---@param augends Augend[] +function Handler:select_augend_visual(lines, cursor, augends) + local interim_augend = nil + local interim_score = Score.new(3, 0, 0) -- 最も優先度の低いスコア + + for _, line in ipairs(lines) do + for _, augend in ipairs(augends) do + (function() + ---@type textrange? + local range = nil + if augend.find_stateful == nil then + range = augend:find(line, cursor) + else + range = augend:find_stateful(line, cursor) + end + if range == nil then + return -- equivalent to break (of nested for block) + end + local score = Score.from_cursor(range.from, range.to, cursor) + if score:cmp(interim_score) then + interim_augend = augend + interim_score = score + end + end)() + end + if interim_augend ~= nil then + self.active_augend = interim_augend + return + end + end +end + +---The process that runs when operator is called (in NORMAL mode). +---@param line string +---@param cursor integer +---@param direction direction +---@return {line?: string, cursor?: integer} +function Handler:operate(line, cursor, direction) + if self.range == nil or self.active_augend == nil then + return {} + end + + local text = line:sub(self.range.from, self.range.to) + local addend = self:get_addend(direction) + local add_result = self.active_augend:add(text, addend, cursor) + local new_line = nil + local new_cursor = nil + + if add_result.text ~= nil then + new_line = line:sub(1, self.range.from - 1) .. add_result.text .. line:sub(self.range.to + 1) + end + if add_result.cursor ~= nil then + new_cursor = self.range.from - 1 + add_result.cursor + end + + return { line = new_line, cursor = new_cursor } +end + +---The process that runs when operator is called (in VISUAL mode). +---@param selected_range {from: integer, to?: integer} +---@param direction direction +---@param tier integer +---@return {result?: string} +function Handler:operate_visual(line, selected_range, direction, tier) + if self.active_augend == nil then + return {} + end + tier = util.unwrap_or(tier, 1) + local line_partial = line:sub(selected_range.from, selected_range.to) + local range = self.active_augend:find(line_partial, 0) + if range == nil then + return {} + end + local addend = self:get_addend(direction) + local from = selected_range.from + range.from - 1 + local to = selected_range.from + range.to - 1 + local text = line:sub(from, to) + local add_result = self.active_augend:add(text, addend * tier) + local newline = nil + if add_result.text ~= nil then + newline = line:sub(1, from - 1) .. add_result.text .. line:sub(to + 1) + end + return { line = newline } +end + +---Set self.range to the target range of the currently active augend (without side effects). +---@param line any +---@param cursor any +function Handler:find_text_range(line, cursor) + if self.active_augend == nil then + self.range = nil + return + end + self.range = self.active_augend:find(line, cursor) +end + +return Handler diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/map.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/map.lua new file mode 100644 index 0000000..667e47a --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/map.lua @@ -0,0 +1,67 @@ +local M = {} + +local command = require "dial.command" +local util = require "dial.util" + +---Sandwich input string between and . +---@param body string +local function cmdcr(body) + local cmd_sequences = "" + local cr_sequences = "" + return cmd_sequences .. body .. cr_sequences +end + +---Output command sequence which provides dial operation. +---@param direction direction +---@param mode mode +---@param group_name? string +local function _cmd_sequence(direction, mode, group_name) + local select + if group_name == nil then + select = cmdcr([[lua require"dial.command".select_augend_]] .. mode .. "()") + else + select = cmdcr([[lua require"dial.command".select_augend_]] .. mode .. [[("]] .. group_name .. [[")]]) + end + -- command.select_augend_normal(vim.v.count, group_name) + local setopfunc = cmdcr([[let &opfunc="dial#operator#]] .. direction .. "_" .. mode .. [["]]) + local textobj = util.if_expr(mode == "normal", cmdcr [[lua require("dial.command").textobj()]], "") + return select .. setopfunc .. "g@" .. textobj +end + +---@param group_name? string +---@return string +function M.inc_normal(group_name) + return _cmd_sequence("increment", "normal", group_name) +end + +---@param group_name? string +---@return string +function M.dec_normal(group_name) + return _cmd_sequence("decrement", "normal", group_name) +end + +---@param group_name? string +---@return string +function M.inc_visual(group_name) + return _cmd_sequence("increment", "visual", group_name) +end + +---@param group_name? string +---@return string +function M.dec_visual(group_name) + return _cmd_sequence("decrement", "visual", group_name) +end + +---@param group_name? string +---@return string +function M.inc_gvisual(group_name) + return _cmd_sequence("increment", "gvisual", group_name) +end + +---@param group_name? string +---@return string +function M.dec_gvisual(group_name) + return _cmd_sequence("decrement", "gvisual", group_name) +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/types.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/types.lua new file mode 100644 index 0000000..b6a4d48 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/types.lua @@ -0,0 +1,18 @@ +-- define type annotations +-- (See: https://github.com/sumneko/lua-language-server/wiki/Annotations) + +---@alias direction '"increment"' | '"decrement"' +---@alias mode '"normal"' | '"visual"' | '"gvisual"' +---@alias textrange {from: integer, to: integer} +---@alias addresult {text?: string, cursor?: integer} + +---@alias findf fun(line: string, cursor?: integer) -> textrange? +---@alias addf fun(text: string, addend: integer, cursor?: integer) -> addresult? + +---@alias findmethod fun(self: Augend, line: string, cursor?: integer) -> textrange? +---@alias addmethod fun(self: Augend, text: string, addend: integer, cursor?: integer) -> addresult? + +---@class Augend +---@field find findmethod +---@field find_stateful? findmethod +---@field add addmethod diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/dial/util.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/util.lua new file mode 100644 index 0000000..8032943 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/dial/util.lua @@ -0,0 +1,189 @@ +-- utils +local M = {} + +---@generic T +---@param cond boolean +---@param branch_true T +---@param branch_false T +---@return T +function M.if_expr(cond, branch_true, branch_false) + if cond then + return branch_true + end + return branch_false +end + +---@generic T +---@param x T | nil +---@param default T +---@return T +function M.unwrap_or(x, default) + if x == nil then + return default + end + return x +end + +function M.Set(list) + local set = {} + for _, l in ipairs(list) do + set[l] = true + end + return set +end + +-- Check if the argument is a valid list (which does not contain nil). +---@param name string +---@param list any[] +---@param arg1 string | function +---@param arg2? string +function M.validate_list(name, list, arg1, arg2) + if not vim.tbl_islist(list) then + error(("%s is not list."):format(name)) + end + + if type(arg1) == "string" then + local typename, _ = arg1, arg2 + + local count_idx = 0 + for idx, value in ipairs(list) do + count_idx = idx + if type(value) ~= typename then + error(("Type error: %s[%d] should have type %s, got %s"):format(name, idx, typename, type(value))) + end + end + + if count_idx ~= #list then + error(("The %s[%d] is nil. nil is not allowed in a list."):format(name, count_idx + 1)) + end + else + local checkf, errormsg = arg1, arg2 + + local count_idx = 0 + for idx, value in ipairs(list) do + count_idx = idx + local ok, err = checkf(value) + if not ok then + error(("List validation error: %s[%d] does not satisfy '%s' (%s)"):format(name, idx, errormsg, err)) + end + end + + if count_idx ~= #list then + error(("The %s[%d] is nil. nil is not allowed in valid list."):format(name, count_idx + 1)) + end + end +end + +---Returns the indices with the value nil. +---returns an index array +---@param tbl array +---@return integer[] +function M.index_with_nil_value(tbl) + -- local maxn, k = 0, nil + -- repeat + -- k = next( table, k ) + -- if type( k ) == 'number' and k > maxn then + -- maxn = k + -- end + -- until not k + -- M.dbg(maxn) + + local maxn = table.maxn(tbl) + local nil_keys = {} + for i = 1, maxn, 1 do + if tbl[i] == nil then + table.insert(nil_keys, i) + end + end + return nil_keys +end + +function M.filter(fn, ary) + local a = {} + for i = 1, #ary do + if fn(ary[i]) then + table.insert(a, ary[i]) + end + end + return a +end + +function M.filter_map(fn, ary) + local a = {} + for i = 1, #ary do + if fn(ary[i]) ~= nil then + table.insert(a, fn(ary[i])) + end + end + return a +end + +function M.filter_map_zip(fn, ary) + local a = {} + for i = 1, #ary do + if fn(ary[i]) ~= nil then + table.insert(a, { ary[i], fn(ary[i]) }) + end + end + return a +end + +function M.tostring_with_base(n, b, wid, pad) + n = math.floor(n) + if not b or b == 10 then + return tostring(n) + end + local digits = "0123456789abcdefghijklmnopqrstuvwxyz" + local t = {} + if n < 0 then + -- be positive + n = -n + end + repeat + local d = (n % b) + 1 + n = math.floor(n / b) + table.insert(t, 1, digits:sub(d, d)) + until n == 0 + local text = table.concat(t, "") + if wid then + if #text < wid then + if pad == nil then + pad = " " + end + local padding = pad:rep(wid - #text) + return padding .. text + end + end + return text +end + +-- util.try_get_keys({foo = "bar", hoge = "fuga", teka = "pika"}, ["teka", "foo"]) +-- -> ["pika", "bar"] +function M.try_get_keys(tbl, keylst) + if not vim.tbl_islist(keylst) then + return nil, "the 2nd argument is not list." + end + + local values = {} + + for _, key in ipairs(keylst) do + local val = tbl[key] + if val ~= nil then + table.insert(values, val) + else + local errmsg = ("The value corresponding to the key '%s' is not found in the table."):format(key) + return nil, errmsg + end + end + + return values +end + +---return the iterator returning UTF-8 based characters +---@param text string +---@return fun(): string | nil +function M.chars(text) + return text:gmatch "[%z\1-\127\194-\244][\128-\191]*" +end + +return M diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/case_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/case_spec.lua new file mode 100644 index 0000000..6d625b0 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/case_spec.lua @@ -0,0 +1,38 @@ +local case = require("dial.augend").case + +describe("Test of case between camelCase and snake_case (cyclic = true):", function() + local augend = case.new { types = { "camelCase", "snake_case" }, cyclic = true } + + describe("find function", function() + it("can find camelCase or snake_case identifier", function() + assert.are.same(augend:find("fooBar", 1), { from = 1, to = 6 }) + assert.are.same(augend:find("fooBarBaz", 1), { from = 1, to = 9 }) + assert.are.same(augend:find("fooBarBazPiyo9", 1), { from = 1, to = 14 }) + assert.are.same(augend:find("foo_bar", 1), { from = 1, to = 7 }) + end) + it("skips non-camelCase and non-snake_case word", function() + assert.are.same(augend:find("foo_Bar_baz_foo", 1), nil) + assert.are.same(augend:find("foo_Bar", 1), nil) + assert.are.same(augend:find("FooBar", 1), nil) + assert.are.same(augend:find("1foo_bar", 1), nil) + end) + it("skips identifier constructed from one word", function() + assert.are.same(augend:find("foo", 1), nil) + assert.are.same(augend:find("foo bar_baz", 1), { from = 5, to = 11 }) + end) + it("chooses the one in front", function() + assert.are.same(augend:find("foo_bar fooBar", 1), { from = 1, to = 7 }) + assert.are.same(augend:find("foo_bar fooBar", 8), { from = 9, to = 14 }) + assert.are.same(augend:find("fooBar foo_bar", 1), { from = 1, to = 6 }) + end) + end) + + describe("add function", function() + it("can convert cases", function() + assert.are.same(augend:add("fooBar", 1, 1), { text = "foo_bar", cursor = 7 }) + assert.are.same(augend:add("fooBar", 2, 1), { cursor = 6 }) + assert.are.same(augend:add("foo_bar", 1, 1), { text = "fooBar", cursor = 6 }) + assert.are.same(augend:add("foo_bar_baz", 1, 1), { text = "fooBarBaz", cursor = 9 }) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/constant_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/constant_spec.lua new file mode 100644 index 0000000..6850513 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/constant_spec.lua @@ -0,0 +1,5 @@ +local constant = require("dial.augend").constant + +describe("Test of constant between two words", function() + local augend = constant.new { elements = { "true", "false" } } +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/date_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/date_spec.lua new file mode 100644 index 0000000..93b8cf6 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/date_spec.lua @@ -0,0 +1,297 @@ +local date = require("dial.augend").date + +describe([[Test of date with format "%Y-%m-%d":]], function() + local augend = date.alias["%Y-%m-%d"] + + describe("find function", function() + it("can find dates in a given format", function() + assert.are.same(augend:find_stateful("date: 2022-10-16", 1), { from = 7, to = 16 }) + assert.are.same(augend.kind, "day") + assert.are.same(augend:find_stateful("date: 2022-10-16", 6), { from = 7, to = 16 }) + assert.are.same(augend.kind, "day") + assert.are.same(augend:find_stateful("date: 2022-10-16", 7), { from = 7, to = 16 }) + assert.are.same(augend.kind, "year") + assert.are.same(augend:find_stateful("date: 2022-10-16", 10), { from = 7, to = 16 }) + assert.are.same(augend.kind, "year") + assert.are.same(augend:find_stateful("date: 2022-10-16", 11), { from = 7, to = 16 }) + assert.are.same(augend.kind, "month") + assert.are.same(augend:find_stateful("date: 2022-10-16", 13), { from = 7, to = 16 }) + assert.are.same(augend.kind, "month") + assert.are.same(augend:find_stateful("date: 2022-10-16", 14), { from = 7, to = 16 }) + assert.are.same(augend.kind, "day") + assert.are.same(augend:find_stateful("date: 2022-10-16", 16), { from = 7, to = 16 }) + assert.are.same(augend.kind, "day") + end) + it("cannot find dates in unspecified format", function() + assert.are.same(augend:find("2022/10/16", 1), nil) + end) + it("can find dates in a given format but not exist", function() + assert.are.same(augend:find("2022-10-32", 1), { from = 1, to = 10 }) + end) + end) + + describe("add function", function() + it("can inc/dec days", function() + augend.kind = "day" + assert.are.same(augend:add("2022-10-16", 1), { text = "2022-10-17", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", 1, 1), { text = "2022-10-17", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", 1, 7), { text = "2022-10-17", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", 1, 10), { text = "2022-10-17", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", 5), { text = "2022-10-21", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", 21), { text = "2022-11-06", cursor = 10 }) + assert.are.same(augend:add("2022-10-16", -21), { text = "2022-09-25", cursor = 10 }) + assert.are.same(augend:add("2022-12-16", 21), { text = "2023-01-06", cursor = 10 }) + end) + + it("can inc/dec months", function() + augend.kind = "month" + assert.are.same(augend:add("2022-10-16", 1), { text = "2022-11-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", 1, 1), { text = "2022-11-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", 1, 7), { text = "2022-11-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", 1, 10), { text = "2022-11-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", 2), { text = "2022-12-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", 5), { text = "2023-03-16", cursor = 7 }) + assert.are.same(augend:add("2022-10-16", -5), { text = "2022-05-16", cursor = 7 }) + assert.are.same(augend:add("2022-01-31", 1), { text = "2022-03-03", cursor = 7 }) + end) + + it("can inc/dec years", function() + augend.kind = "year" + assert.are.same(augend:add("2022-10-16", 1), { text = "2023-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", 1, 1), { text = "2023-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", 1, 7), { text = "2023-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", 1, 10), { text = "2023-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", 2), { text = "2024-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", 5), { text = "2027-10-16", cursor = 4 }) + assert.are.same(augend:add("2022-10-16", -5), { text = "2017-10-16", cursor = 4 }) + assert.are.same(augend:add("2020-02-29", 1), { text = "2021-03-01", cursor = 4 }) + end) + + it("correct date and increment days", function() + augend.kind = "day" + assert.are.same(augend:add("2022-10-32", 1), { text = "2022-11-02", cursor = 10 }) + end) + end) +end) + +describe([[Test of date with format "%-m/%-d":]], function() + local augend = date.alias["%-m/%-d"] + + describe("find function", function() + it("can find dates in a given format", function() + assert.are.same(augend:find("10/16", 1), { from = 1, to = 5 }) + end) + it("cannot find dates in unspecified format", function() + assert.are.same(augend:find("10-16", 1), nil) + end) + it("can find dates in a given format but not exist", function() + assert.are.same(augend:find("10/32", 1), nil) + end) + end) + + describe("add function", function() + it("can inc/dec days", function() + augend.kind = "day" + assert.are.same(augend:add("10/16", 1), { text = "10/17", cursor = 5 }) + assert.are.same(augend:add("10/16", 1, 1), { text = "10/17", cursor = 5 }) + assert.are.same(augend:add("10/16", 1, 5), { text = "10/17", cursor = 5 }) + assert.are.same(augend:add("10/16", 5), { text = "10/21", cursor = 5 }) + assert.are.same(augend:add("10/16", 21), { text = "11/6", cursor = 4 }) + assert.are.same(augend:add("10/16", -21), { text = "9/25", cursor = 4 }) + assert.are.same(augend:add("12/16", 21), { text = "1/6", cursor = 3 }) + end) + + it("can inc/dec months", function() + augend.kind = "month" + assert.are.same(augend:add("10/16", 1), { text = "11/16", cursor = 2 }) + assert.are.same(augend:add("10/16", 1, 1), { text = "11/16", cursor = 2 }) + assert.are.same(augend:add("10/16", 1, 5), { text = "11/16", cursor = 2 }) + assert.are.same(augend:add("10/16", 2), { text = "12/16", cursor = 2 }) + assert.are.same(augend:add("10/16", 5), { text = "3/16", cursor = 1 }) + assert.are.same(augend:add("10/16", -5), { text = "5/16", cursor = 1 }) + end) + end) +end) + +describe([[Test of date with format "%Y年%-m月%-d日(%ja)":]], function() + local augend = date.alias["%Y年%-m月%-d日(%ja)"] + + describe("find function", function() + it("can find dates in a given format", function() + assert.are.same(augend:find("2022年10月16日(日)", 1), { from = 1, to = 22 }) + end) + it("cannot find dates in unspecified format", function() + assert.are.same(augend:find("2022/10/16", 1), nil) + end) + it("can find dates in a given format but not exist", function() + assert.are.same(augend:find("2022年10月16日(金)", 1), { from = 1, to = 22 }) + assert.are.same(augend:find("2022年10月32日(火)", 1), { from = 1, to = 22 }) + end) + end) + + describe("add function", function() + it("can inc/dec days", function() + augend.kind = "day" + assert.are.same(augend:add("2022年10月16日(日)", 1), { text = "2022年10月17日(月)", cursor = 14 }) + assert.are.same( + augend:add("2022年10月16日(日)", 1, 1), + { text = "2022年10月17日(月)", cursor = 14 } + ) + assert.are.same( + augend:add("2022年10月16日(日)", 1, 7), + { text = "2022年10月17日(月)", cursor = 14 } + ) + assert.are.same( + augend:add("2022年10月16日(日)", 1, 10), + { text = "2022年10月17日(月)", cursor = 14 } + ) + assert.are.same(augend:add("2022年10月16日(日)", 5), { text = "2022年10月21日(金)", cursor = 14 }) + assert.are.same(augend:add("2022年10月16日(日)", 21), { text = "2022年11月6日(日)", cursor = 13 }) + assert.are.same(augend:add("2022年10月16日(日)", -21), { text = "2022年9月25日(日)", cursor = 13 }) + assert.are.same(augend:add("2022年12月16日(金)", 21), { text = "2023年1月6日(金)", cursor = 12 }) + end) + + it("correct date and increment days", function() + augend.kind = "day" + assert.are.same(augend:add("2022年10月32日(日)", 1), { text = "2022年11月2日(水)", cursor = 13 }) + end) + end) +end) + +describe([[Test of clamp & end_sensitive option:]], function() + describe("{clamp = false and end_sensitive = false}", function() + local augend = date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + clamp = false, + end_sensitive = false, + } + it("does not clamp day or treat last days of month specially", function() + augend.kind = "month" + assert.are.same(augend:add("2022/01/28", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/29", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/01/30", 1), { text = "2022/03/02", cursor = 7 }) + assert.are.same(augend:add("2022/01/31", 1), { text = "2022/03/03", cursor = 7 }) + assert.are.same(augend:add("2022/02/01", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", 1), { text = "2022/03/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", 1), { text = "2022/03/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", 1), { text = "2022/05/01", cursor = 7 }) + assert.are.same(augend:add("2021/12/31", 2), { text = "2022/03/03", cursor = 7 }) + + assert.are.same(augend:add("2022/03/28", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/29", -1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", -1), { text = "2022/03/02", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", -1), { text = "2022/03/03", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", -1), { text = "2022/01/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", -1), { text = "2022/01/28", cursor = 7 }) + assert.are.same(augend:add("2022/12/31", -1), { text = "2022/12/01", cursor = 7 }) + + augend.kind = "year" + assert.are.same(augend:add("2024/02/29", 1), { text = "2025/03/01", cursor = 4 }) + assert.are.same(augend:add("2025/02/28", -1), { text = "2024/02/28", cursor = 4 }) + end) + end) + + describe("{clamp = true and end_sensitive = false}", function() + local augend = date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + clamp = true, + end_sensitive = false, + } + it("clamps day but does not treat last days of month specially", function() + augend.kind = "month" + assert.are.same(augend:add("2022/01/28", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/29", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/30", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/31", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/01", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", 1), { text = "2022/03/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", 1), { text = "2022/03/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2021/12/31", 2), { text = "2022/02/28", cursor = 7 }) + + assert.are.same(augend:add("2022/03/28", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/29", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", -1), { text = "2022/01/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", -1), { text = "2022/01/28", cursor = 7 }) + assert.are.same(augend:add("2022/12/31", -1), { text = "2022/11/30", cursor = 7 }) + + augend.kind = "year" + assert.are.same(augend:add("2024/02/29", 1), { text = "2025/02/28", cursor = 4 }) + assert.are.same(augend:add("2025/02/28", -1), { text = "2024/02/28", cursor = 4 }) + end) + end) + + describe("{clamp = false and end_sensitive = true}", function() + local augend = date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + clamp = false, + end_sensitive = true, + } + it("does not clamp day but treat last days of month specially", function() + augend.kind = "month" + assert.are.same(augend:add("2022/01/28", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/29", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/01/30", 1), { text = "2022/03/02", cursor = 7 }) + assert.are.same(augend:add("2022/01/31", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/01", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", 1), { text = "2022/03/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", 1), { text = "2022/03/31", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2021/12/31", 2), { text = "2022/02/28", cursor = 7 }) + + assert.are.same(augend:add("2022/03/28", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/29", -1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", -1), { text = "2022/03/02", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", -1), { text = "2022/01/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", -1), { text = "2022/01/31", cursor = 7 }) + assert.are.same(augend:add("2022/12/31", -1), { text = "2022/11/30", cursor = 7 }) + + augend.kind = "year" + assert.are.same(augend:add("2024/02/29", 1), { text = "2025/02/28", cursor = 4 }) + assert.are.same(augend:add("2025/02/28", -1), { text = "2024/02/29", cursor = 4 }) + end) + end) + + describe("{clamp = true and end_sensitive = true}", function() + local augend = date.new { + pattern = "%Y/%m/%d", + default_kind = "day", + clamp = true, + end_sensitive = true, + } + it("clamp day and treat last days of month specially", function() + augend.kind = "month" + assert.are.same(augend:add("2022/01/28", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/29", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/30", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/01/31", 1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/01", 1), { text = "2022/03/01", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", 1), { text = "2022/03/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", 1), { text = "2022/03/31", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", 1), { text = "2022/04/30", cursor = 7 }) + assert.are.same(augend:add("2021/12/31", 2), { text = "2022/02/28", cursor = 7 }) + + assert.are.same(augend:add("2022/03/28", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/29", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/30", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/03/31", -1), { text = "2022/02/28", cursor = 7 }) + assert.are.same(augend:add("2022/02/27", -1), { text = "2022/01/27", cursor = 7 }) + assert.are.same(augend:add("2022/02/28", -1), { text = "2022/01/31", cursor = 7 }) + assert.are.same(augend:add("2022/12/31", -1), { text = "2022/11/30", cursor = 7 }) + + augend.kind = "year" + assert.are.same(augend:add("2024/02/29", 1), { text = "2025/02/28", cursor = 4 }) + assert.are.same(augend:add("2025/02/28", -1), { text = "2024/02/29", cursor = 4 }) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/integer_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/integer_spec.lua new file mode 100644 index 0000000..b8ec8fb --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/integer_spec.lua @@ -0,0 +1,201 @@ +local integer = require("dial.augend").integer + +describe("Test of integer.alias.decimal:", function() + local augend = integer.alias.decimal + + describe("find function", function() + it("can find a decimal number", function() + assert.are.same(augend:find("123", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 3), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 4), { from = 5, to = 8 }) + assert.are.same(augend:find("123 4567 89", 5), { from = 5, to = 8 }) + end) + it("returns nil when no number appears", function() + assert.are.same(augend:find("", 1), nil) + assert.are.same(augend:find("foo bar", 1), nil) + end) + it("doesn't pick up negative number", function() + assert.are.same(augend:find("-123", 1), { from = 2, to = 4 }) + assert.are.same(augend:find("fname-1", 1), { from = 7, to = 7 }) + end) + it("doesn't pick up hex number", function() + assert.are.same(augend:find("0xaf", 1), { from = 1, to = 1 }) + end) + end) + + describe("add function", function() + it("can increment numbers", function() + assert.are.same(augend:add("12", 1, 1), { text = "13", cursor = 2 }) + assert.are.same(augend:add("12", 1, 2), { text = "13", cursor = 2 }) + assert.are.same(augend:add("12", 3, 1), { text = "15", cursor = 2 }) + assert.are.same(augend:add("12", 123, 1), { text = "135", cursor = 3 }) + assert.are.same(augend:add("12", -1, 1), { text = "11", cursor = 2 }) + assert.are.same(augend:add("12", -3, 1), { text = "9", cursor = 1 }) + assert.are.same(augend:add("12", -15, 1), { text = "0", cursor = 1 }) + end) + end) +end) + +describe("Test of integer.alias.decimal_int:", function() + local augend = integer.alias.decimal_int + + describe("find function", function() + it("can find a decimal number", function() + assert.are.same(augend:find("123", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 3), { from = 1, to = 3 }) + assert.are.same(augend:find("123 4567 89", 4), { from = 5, to = 8 }) + assert.are.same(augend:find("123 4567 89", 5), { from = 5, to = 8 }) + end) + it("returns nil when no number appears", function() + assert.are.same(augend:find("", 1), nil) + assert.are.same(augend:find("foo bar", 1), nil) + end) + it("picks up negative number", function() + assert.are.same(augend:find("-123", 1), { from = 1, to = 4 }) + assert.are.same(augend:find("fname-1", 1), { from = 6, to = 7 }) + end) + end) + + describe("add function", function() + it("can increment numbers", function() + assert.are.same(augend:add("12", 1, 1), { text = "13", cursor = 2 }) + assert.are.same(augend:add("12", 1, 2), { text = "13", cursor = 2 }) + assert.are.same(augend:add("12", 3, 1), { text = "15", cursor = 2 }) + assert.are.same(augend:add("12", 123, 1), { text = "135", cursor = 3 }) + assert.are.same(augend:add("12", -1, 1), { text = "11", cursor = 2 }) + assert.are.same(augend:add("12", -3, 1), { text = "9", cursor = 1 }) + assert.are.same(augend:add("12", -15, 1), { text = "-3", cursor = 2 }) + assert.are.same(augend:add("-3", 1, 1), { text = "-2", cursor = 2 }) + assert.are.same(augend:add("-3", 4, 1), { text = "1", cursor = 1 }) + assert.are.same(augend:add("-3", -24, 1), { text = "-27", cursor = 3 }) + end) + end) +end) + +describe("Test of integer.alias.hex:", function() + local augend = integer.alias.hex + + describe("find function", function() + it("can find a hex number", function() + assert.are.same(augend:find("0x0", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("0xa", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("0x1213", 3), { from = 1, to = 6 }) + assert.are.same(augend:find("0x1a2b", 4), { from = 1, to = 6 }) + assert.are.same(augend:find("0xabcd", 5), { from = 1, to = 6 }) + assert.are.same(augend:find("0xabcdefg", 5), { from = 1, to = 8 }) + assert.are.same(augend:find("0xAB12", 5), { from = 1, to = 6 }) + assert.are.same(augend:find("0xAB12", 5), { from = 1, to = 6 }) + end) + it("does not detect hex number without `0x`", function() + assert.are.same(augend:find("0", 1), nil) + assert.are.same(augend:find("1a1a", 1), nil) + assert.are.same(augend:find("1A1A", 1), nil) + end) + it("does not detect hex number starting with `0X`", function() + assert.are.same(augend:find("0X1", 1), nil) + end) + end) + + describe("add function", function() + it("can increment numbers", function() + assert.are.same(augend:add("0x1", 1, 1), { text = "0x2", cursor = 3 }) + assert.are.same(augend:add("0xf", 1, 1), { text = "0x10", cursor = 4 }) + assert.are.same(augend:add("0x12", 8, 1), { text = "0x1a", cursor = 4 }) + assert.are.same(augend:add("0x2020", -1, 1), { text = "0x201f", cursor = 6 }) + assert.are.same(augend:add("0x1F1F", 2, 1), { text = "0x1f21", cursor = 6 }) + end) + end) +end) + +describe([[Test of integer.new {delimiter = ","}:]], function() + local augend = integer.new { delimiter = "," } + + describe("find function", function() + it("can find comma-separated integer", function() + assert.are.same(augend:find("123", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123,456", 1), { from = 1, to = 7 }) + assert.are.same(augend:find("123,456,789", 1), { from = 1, to = 11 }) + assert.are.same(augend:find("123,456,789", 4), { from = 1, to = 11 }) + assert.are.same(augend:find("123,456,789", 5), { from = 1, to = 11 }) + assert.are.same(augend:find("123,456,789", 7), { from = 1, to = 11 }) + end) + it("does not consider number of digits", function() + assert.are.same(augend:find("1,2,3", 1), { from = 1, to = 5 }) + assert.are.same(augend:find("123,4,56", 1), { from = 1, to = 8 }) + assert.are.same(augend:find("123,456,789", 1), { from = 1, to = 11 }) + end) + it("does not match illegal format", function() + assert.are.same(augend:find("123, 456", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123, 456", 4), { from = 6, to = 8 }) + assert.are.same(augend:find("123 ,456", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("foo,1", 1), { from = 5, to = 5 }) + end) + end) + + describe("add function", function() + it("can increment numbers", function() + assert.are.same(augend:add("1,999", 1, 1), { text = "2,000", cursor = 5 }) + assert.are.same(augend:add("9,999,999", 1, 1), { text = "10,000,000", cursor = 10 }) + assert.are.same(augend:add("999,999", 1, 1), { text = "1,000,000", cursor = 9 }) + assert.are.same(augend:add("100,000", -1, 1), { text = "99,999", cursor = 6 }) + assert.are.same(augend:add("1,000", -1, 1), { text = "999", cursor = 3 }) + end) + it("fixes the place of separators", function() + assert.are.same(augend:add("19,99", 1, 1), { text = "2,000", cursor = 5 }) + assert.are.same(augend:add("1,0,0,0,0,0", -1, 1), { text = "99,999", cursor = 6 }) + end) + end) +end) + +describe([[Test of integer.new {delimiter = "."}:]], function() + local augend = integer.new { delimiter = "." } + + describe("find function", function() + it("can find comma-separated integer", function() + assert.are.same(augend:find("123", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123.456", 1), { from = 1, to = 7 }) + end) + it("does not consider number of digits", function() + assert.are.same(augend:find("1.2.3", 1), { from = 1, to = 5 }) + end) + it("does not match illegal format", function() + assert.are.same(augend:find("123. 456", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("123. 456", 4), { from = 6, to = 8 }) + assert.are.same(augend:find("123 .456", 1), { from = 1, to = 3 }) + assert.are.same(augend:find("foo.1", 1), { from = 5, to = 5 }) + end) + end) + + describe("add function", function() + it("can increment numbers", function() + assert.are.same(augend:add("1.999", 1, 1), { text = "2.000", cursor = 5 }) + assert.are.same(augend:add("9.999.999", 1, 1), { text = "10.000.000", cursor = 10 }) + assert.are.same(augend:add("999.999", 1, 1), { text = "1.000.000", cursor = 9 }) + assert.are.same(augend:add("100.000", -1, 1), { text = "99.999", cursor = 6 }) + assert.are.same(augend:add("1.000", -1, 1), { text = "999", cursor = 3 }) + end) + it("fixes the place of separators", function() + assert.are.same(augend:add("19.99", 1, 1), { text = "2.000", cursor = 5 }) + assert.are.same(augend:add("1.0.0.0.0.0", -1, 1), { text = "99.999", cursor = 6 }) + end) + end) +end) + +describe([[Test of integer.new {delimiter = ",", delimiter_digits = 4}:]], function() + local augend = integer.new { delimiter = ",", delimiter_digits = 4 } + + describe("find function", function() + it("can find comma-separated integer", function() + assert.are.same(augend:find("123,456,789", 1), { from = 1, to = 11 }) + end) + end) + + describe("add function", function() + it("separates numbers by four digits", function() + assert.are.same(augend:add("1,999", 1, 1), { text = "2000", cursor = 4 }) + assert.are.same(augend:add("9,999,999", 1, 1), { text = "1000,0000", cursor = 9 }) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/misc_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/misc_spec.lua new file mode 100644 index 0000000..c398157 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/misc_spec.lua @@ -0,0 +1,29 @@ +local misc = require("dial.augend").misc + +describe("Test of misc.alias.markdown_header:", function() + local augend = misc.alias.markdown_header + + describe("find function", function() + it("can find a markdown header", function() + assert.are.same(augend:find("# Header 1", 1), { from = 1, to = 1 }) + assert.are.same(augend:find("## Header 2", 1), { from = 1, to = 2 }) + assert.are.same(augend:find("###### Header 6", 1), { from = 1, to = 6 }) + assert.are.same(augend:find("#Header 1", 1), { from = 1, to = 1 }) + assert.are.same(augend:find("# Header 1", 3), { from = 1, to = 1 }) + end) + it("ignores non-header elements", function() + assert.are.same(augend:find("foo # bar", 1), nil) + assert.are.same(augend:find("####### Header 7?", 1), nil) + end) + end) + + describe("add function", function() + it("can increment header level", function() + assert.are.same(augend:add("#", 1, 1), { text = "##", cursor = 1 }) + assert.are.same(augend:add("##", 3, 1), { text = "#####", cursor = 1 }) + assert.are.same(augend:add("#", 7, 1), { text = "######", cursor = 1 }) + assert.are.same(augend:add("##", -4, 1), { text = "#", cursor = 1 }) + assert.are.same(augend:add("####", -1, 6), { text = "###", cursor = 1 }) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/paren_spec.lua b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/paren_spec.lua new file mode 100644 index 0000000..b72abb2 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/lua/spec/dial/augend/paren_spec.lua @@ -0,0 +1,181 @@ +local paren = require("dial.augend").paren + +describe([[Test of paren between '...' and "...":]], function() + local augend = paren.new { + patterns = { { "'", "'" }, { '"', '"' } }, + nested = false, + cyclic = true, + escape_char = [[\]], + } + + describe("find function", function() + it("can find single- or double- quoted string", function() + assert.are.same(augend:find([["foo"]], 1), { from = 1, to = 5 }) + assert.are.same(augend:find([['foo']], 1), { from = 1, to = 5 }) + assert.are.same(augend:find([[foo"bar"baz]], 1), { from = 4, to = 8 }) + assert.are.same(augend:find([[foo"bar"baz]], 4), { from = 4, to = 8 }) + assert.are.same(augend:find([[foo"bar"baz]], 5), { from = 4, to = 8 }) + assert.are.same(augend:find([[foo"bar"baz]], 8), { from = 4, to = 8 }) + assert.are.same(augend:find([[foo"bar"baz]], 9), nil) + assert.are.same(augend:find([[foo""baz]], 1), { from = 4, to = 5 }) + end) + it("returns nil when no string literal appears", function() + assert.are.same(augend:find([[foo bar]], 1), nil) + assert.are.same(augend:find([[foo "bar]], 1), nil) + assert.are.same(augend:find([[foo 'bar"]], 1), nil) + end) + it("considers escape character", function() + assert.are.same(augend:find([[foo"bar\"baz"]], 1), { from = 4, to = 13 }) + assert.are.same(augend:find([[foo'bar\'baz']], 1), { from = 4, to = 13 }) + assert.are.same(augend:find([[foo'bar\"baz']], 1), { from = 4, to = 13 }) + assert.are.same(augend:find([[foo'bar\nbaz']], 1), { from = 4, to = 13 }) + assert.are.same(augend:find([[foo'bar\'baz"]], 1), nil) + assert.are.same(augend:find([[foo"bar\\"baz"]], 1), { from = 4, to = 10 }) + end) + it("handle multiple quote areas", function() + assert.are.same(augend:find([[a"b"c"b"a]], 1), { from = 2, to = 4 }) + assert.are.same(augend:find([[a"b"c"b"a]], 4), { from = 2, to = 4 }) + assert.are.same(augend:find([[a"b"c"b"a]], 5), { from = 6, to = 8 }) + assert.are.same(augend:find([[a"b"c"b"a]], 8), { from = 6, to = 8 }) + assert.are.same(augend:find([[a"b'c'b"a]], 1), { from = 2, to = 8 }) + assert.are.same(augend:find([[a"b'c'b"a]], 4), { from = 4, to = 6 }) + end) + end) + + describe("add function", function() + it("can convert single-quote to double and vice versa", function() + assert.are.same(augend:add([['foo']], 1, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([["foo"]], -1, 1), { text = [['foo']], cursor = 5 }) + assert.are.same(augend:add([['fo\'o']], 1, 1), { text = [["fo\'o"]], cursor = 7 }) + end) + it("has cyclic behavior", function() + assert.are.same(augend:add([['foo']], -1, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([["foo"]], 1, 1), { text = [['foo']], cursor = 5 }) + assert.are.same(augend:add([["foo"]], 2, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([["foo"]], 3, 1), { text = [['foo']], cursor = 5 }) + end) + end) +end) + +describe("Test of brackets `()`, `[]`, and `{}`:", function() + local augend = paren.new { + patterns = { + { "(", ")" }, + { "[", "]" }, + { "{", "}" }, + }, + nested = true, + cyclic = true, + } + + describe("find function", function() + it("can find brackets", function() + assert.are.same(augend:find("foo(bar)", 1), { from = 4, to = 8 }) + assert.are.same(augend:find("foo[bar]", 1), { from = 4, to = 8 }) + assert.are.same(augend:find("foo{bar}", 1), { from = 4, to = 8 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 1), { from = 4, to = 8 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 4), { from = 4, to = 8 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 8), { from = 4, to = 8 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 9), { from = 14, to = 18 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 14), { from = 14, to = 18 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 18), { from = 14, to = 18 }) + assert.are.same(augend:find("foo(bar), bar[baz].", 19), nil) + assert.are.same(augend:find("foo(bar]", 1), nil) + assert.are.same(augend:find("foo(bar])", 1), { from = 4, to = 9 }) + end) + it("considers nested brackets", function() + assert.are.same(augend:find("foo({true}, 1) + 1", 1), { from = 4, to = 14 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 4), { from = 4, to = 14 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 5), { from = 5, to = 10 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 10), { from = 5, to = 10 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 11), { from = 4, to = 14 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 14), { from = 4, to = 14 }) + assert.are.same(augend:find("foo({true}, 1) + 1", 15), nil) + assert.are.same(augend:find("foo{{true}, 1} + 1", 1), { from = 4, to = 14 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 4), { from = 4, to = 14 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 5), { from = 5, to = 10 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 10), { from = 5, to = 10 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 11), { from = 4, to = 14 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 14), { from = 4, to = 14 }) + assert.are.same(augend:find("foo{{true}, 1} + 1", 15), nil) + assert.are.same(augend:find("foo(bar[)baz]", 1), { from = 4, to = 9 }) + assert.are.same(augend:find("foo(bar[)baz]", 4), { from = 4, to = 9 }) + assert.are.same(augend:find("foo(bar[)baz]", 5), { from = 4, to = 9 }) + assert.are.same(augend:find("foo(bar[)baz]", 8), { from = 8, to = 13 }) + assert.are.same(augend:find("foo(bar[)baz]", 9), { from = 8, to = 13 }) + assert.are.same(augend:find("foo(bar[)baz]", 10), { from = 8, to = 13 }) + end) + end) + + describe("add function", function() + it("can convert brackets", function() + assert.are.same(augend:add("(foo)", 1, 1), { text = "[foo]", cursor = 5 }) + assert.are.same(augend:add("[foo]", 1, 1), { text = "{foo}", cursor = 5 }) + assert.are.same(augend:add("(foo)", 2, 1), { text = "{foo}", cursor = 5 }) + assert.are.same(augend:add("[foo]", -1, 1), { text = "(foo)", cursor = 5 }) + assert.are.same(augend:add("{foo}", -2, 1), { text = "(foo)", cursor = 5 }) + end) + it("has cyclic behavior", function() + assert.are.same(augend:add("{foo}", 1, 1), { text = "(foo)", cursor = 5 }) + assert.are.same(augend:add("(foo)", 3, 1), { text = "(foo)", cursor = 5 }) + assert.are.same(augend:add("(foo)", -1, 1), { text = "{foo}", cursor = 5 }) + assert.are.same(augend:add("(foo)", -3, 1), { text = "(foo)", cursor = 5 }) + end) + end) +end) + +describe([[Test of paren between Rust-style str literal:]], function() + local augend = paren.new { + patterns = { + { '"', '"' }, + { 'r#"', '"#' }, + { 'r##"', '"##' }, + { 'r###"', '"###' }, + }, + nested = false, + cyclic = false, + } + + describe("find function", function() + it("can find string literals", function() + assert.are.same(augend:find([["foo"]], 1), { from = 1, to = 5 }) + assert.are.same(augend:find([[r#"foo"#]], 1), { from = 1, to = 8 }) + assert.are.same(augend:find([[r##"foo"##]], 1), { from = 1, to = 10 }) + assert.are.same(augend:find([[four##"foo"##]], 1), { from = 4, to = 13 }) + assert.are.same(augend:find([[r##"foo"#]], 1), { from = 4, to = 8 }) + assert.are.same(augend:find([[r##"foo"#bar"##]], 1), { from = 1, to = 15 }) + end) + it("behaves naturally with respect to cursor", function() + assert.are.same(augend:find([[println!(r##"foo"##);]], 1), { from = 10, to = 19 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 9), { from = 10, to = 19 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 10), { from = 10, to = 19 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 11), { from = 10, to = 19 }) + -- TODO: is this natural? + assert.are.same(augend:find([[println!(r##"foo"##);]], 13), { from = 13, to = 17 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 14), { from = 13, to = 17 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 17), { from = 13, to = 17 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 18), { from = 10, to = 19 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 19), { from = 10, to = 19 }) + assert.are.same(augend:find([[println!(r##"foo"##);]], 21), nil) + end) + it("returns nil when no string literal appears", function() + assert.are.same(augend:find([[r#"foo#]], 1), nil) + end) + end) + + describe("add function", function() + it("can convert quotes", function() + assert.are.same(augend:add([["foo"]], 1, 1), { text = [[r#"foo"#]], cursor = 8 }) + assert.are.same(augend:add([["foo"]], 2, 1), { text = [[r##"foo"##]], cursor = 10 }) + assert.are.same(augend:add([["foo"]], 3, 1), { text = [[r###"foo"###]], cursor = 12 }) + assert.are.same(augend:add([[r#"foo"#]], 1, 1), { text = [[r##"foo"##]], cursor = 10 }) + assert.are.same(augend:add([[r#"foo"#]], -1, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([[r###"foo"###]], -2, 1), { text = [[r#"foo"#]], cursor = 8 }) + end) + it("does not have cyclic behavior", function() + assert.are.same(augend:add([["foo"]], -1, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([[r#"foo"#]], -2, 1), { text = [["foo"]], cursor = 5 }) + assert.are.same(augend:add([["foo"]], 4, 1), { text = [[r###"foo"###]], cursor = 12 }) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/dial.nvim/plugin/dial.vim b/etc/soft/nvim/+plugins/dial.nvim/plugin/dial.vim new file mode 100644 index 0000000..e6df5b2 --- /dev/null +++ b/etc/soft/nvim/+plugins/dial.nvim/plugin/dial.vim @@ -0,0 +1,21 @@ +if exists('g:loaded_dial') | finish | endif " prevent loading file twice + +let s:save_cpo = &cpo " save user coptions +set cpo&vim " reset them to defaults + +lua << EOF +vim.api.nvim_set_keymap("n", "(dial-increment)", require("dial.map").inc_normal(), {noremap = true}) +vim.api.nvim_set_keymap("n", "(dial-decrement)", require("dial.map").dec_normal(), {noremap = true}) +vim.api.nvim_set_keymap("v", "(dial-increment)", require("dial.map").inc_visual() .. "gv", {noremap = true}) +vim.api.nvim_set_keymap("v", "(dial-decrement)", require("dial.map").dec_visual() .. "gv", {noremap = true}) +vim.api.nvim_set_keymap("v", "g(dial-increment)", require("dial.map").inc_gvisual() .. "gv", {noremap = true}) +vim.api.nvim_set_keymap("v", "g(dial-decrement)", require("dial.map").dec_gvisual() .. "gv", {noremap = true}) +EOF + +command! -range -nargs=? DialIncrement lua require"dial.command".command("increment", {from = , to = }, {}) +command! -range -nargs=? DialDecrement lua require"dial.command".command("decrement", {from = , to = }, {}) + +let &cpo = s:save_cpo " and restore after +unlet s:save_cpo + +let g:loaded_dial = 1 diff --git a/etc/soft/nvim/+plugins/dressing.nvim/LICENSE b/etc/soft/nvim/+plugins/dressing.nvim/LICENSE new file mode 100644 index 0000000..abcc314 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Steven Arcangeli + +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. diff --git a/etc/soft/nvim/+plugins/dressing.nvim/README.md b/etc/soft/nvim/+plugins/dressing.nvim/README.md new file mode 100644 index 0000000..bce16b8 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/README.md @@ -0,0 +1,356 @@ +# Dressing.nvim + +With the release of Neovim 0.6 we were given the start of extensible core UI +hooks ([vim.ui.select](https://github.com/neovim/neovim/pull/15771) and +[vim.ui.input](https://github.com/neovim/neovim/pull/15959)). They exist to +allow plugin authors to override them with improvements upon the default +behavior, so that's exactly what we're going to do. + +It is a goal to match and not extend the core Neovim API. All options that core +respects will be respected, and we will not accept any custom parameters or +options in the functions. Customization will be done entirely using a separate +[configuration](#configuration) method. + +- [Requirements](#requirements) +- [Screenshots](#screenshots) +- [Installation](#installation) +- [Configuration](#configuration) +- [Highlights](#highlights) +- [Advanced configuration](#advanced-configuration) +- [Notes for plugin authors](#notes-for-plugin-authors) +- [Alternative and related projects](#alternative-and-related-projects) + +## Requirements + +Neovim 0.7.0+ (for earlier versions, use the [nvim-0.5 branch](https://github.com/stevearc/dressing.nvim/tree/nvim-0.5)) + +## Screenshots + +`vim.input` replacement (handling a LSP rename) + +![Screenshot from 2021-12-09 17-36-16](https://user-images.githubusercontent.com/506791/145502533-3dc2f87d-95ea-422d-a318-12c0092f1bdf.png) + +`vim.select` (telescope) + +![Screenshot from 2021-12-02 19-46-01](https://user-images.githubusercontent.com/506791/144541916-4fa60c50-cadc-4f0f-b3c1-6307310e6e99.png) + +`vim.select` (fzf) + +![Screenshot from 2021-12-02 19-46-54](https://user-images.githubusercontent.com/506791/144541986-6081b4f8-b3b2-418d-9265-b9dabec2c4c4.png) + +`vim.select` (nui) + +![Screenshot from 2021-12-02 19-47-56](https://user-images.githubusercontent.com/506791/144542071-1aa66f81-b07c-492e-9884-fdafed1006df.png) + +`vim.select` (built-in) + +![Screenshot from 2021-12-04 17-14-32](https://user-images.githubusercontent.com/506791/144729527-ede0d7ba-a6e6-41e0-be5a-1a5f16d35b05.png) + +## Installation + +dressing.nvim supports all the usual plugin managers + +
+ Packer + +```lua +require('packer').startup(function() + use {'stevearc/dressing.nvim'} +end) +``` + +
+ +
+ Paq + +```lua +require "paq" { + {'stevearc/dressing.nvim'}; +} +``` + +
+ +
+ vim-plug + +```vim +Plug 'stevearc/dressing.nvim' +``` + +
+ +
+ dein + +```vim +call dein#add('stevearc/dressing.nvim') +``` + +
+ +
+ Pathogen + +```sh +git clone --depth=1 https://github.com/stevearc/dressing.nvim.git ~/.vim/bundle/ +``` + +
+ +
+ Neovim native package + +```sh +git clone --depth=1 https://github.com/stevearc/dressing.nvim.git \ + "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/pack/dressing.nvim/start/dressing.nvim +``` + +
+ +## Configuration + +If you're fine with the defaults, you're good to go after installation. If you +want to tweak, call this function: + +```lua +require('dressing').setup({ + input = { + -- Set to false to disable the vim.ui.input implementation + enabled = true, + + -- Default prompt string + default_prompt = "Input:", + + -- Can be 'left', 'right', or 'center' + prompt_align = "left", + + -- When true, will close the modal + insert_only = true, + + -- When true, input will start in insert mode. + start_in_insert = true, + + -- These are passed to nvim_open_win + anchor = "SW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "cursor", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + prefer_width = 40, + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- Set to `false` to disable + mappings = { + n = { + [""] = "Close", + [""] = "Confirm", + }, + i = { + [""] = "Close", + [""] = "Confirm", + [""] = "HistoryPrev", + [""] = "HistoryNext", + }, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + + -- see :help dressing_get_config + get_config = nil, + }, + select = { + -- Set to false to disable the vim.ui.select implementation + enabled = true, + + -- Priority list of preferred vim.select implementations + backend = { "telescope", "fzf_lua", "fzf", "builtin", "nui" }, + + -- Trim trailing `:` from prompt + trim_prompt = true, + + -- Options for telescope selector + -- These are passed into the telescope picker directly. Can be used like: + -- telescope = require('telescope.themes').get_ivy({...}) + telescope = nil, + + -- Options for fzf selector + fzf = { + window = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for fzf_lua selector + fzf_lua = { + winopts = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for nui Menu + nui = { + position = "50%", + size = nil, + relative = "editor", + border = { + style = "rounded", + }, + buf_options = { + swapfile = false, + filetype = "DressingSelect", + }, + win_options = { + winblend = 10, + }, + max_width = 80, + max_height = 40, + min_width = 40, + min_height = 10, + }, + + -- Options for built-in selector + builtin = { + -- These are passed to nvim_open_win + anchor = "NW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "editor", + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + -- the min_ and max_ options can be a list of mixed types. + -- max_width = {140, 0.8} means "the lesser of 140 columns or 80% of total" + width = nil, + max_width = { 140, 0.8 }, + min_width = { 40, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 10, 0.2 }, + + -- Set to `false` to disable + mappings = { + [""] = "Close", + [""] = "Close", + [""] = "Confirm", + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + }, + + -- Used to override format_item. See :help dressing-format + format_item_override = {}, + + -- see :help dressing_get_config + get_config = nil, + }, +}) +``` + +## Highlights + +The built-in `vim.ui.input` and `vim.ui.select` components mostly use the +standard highlight groups for neovim floating windows (e.g. `NormalFloat` for +the text, `FloatBorder` for the border). In addition, the window title uses a +non-standard `FloatTitle` group that is linked to `FloatBorder` by default. + +A common way to adjust the highlighting of just the dressing windows is by +providing a `winhighlight` option in the config. For example, `winhighlight = 'NormalFloat:DiagnosticError'` +would change the default text color in the dressing windows. See `:help winhighlight` +for more details. + +Note that you can't change `FloatTitle` via `winhighlight` since it is not a +built-in group. + +## Advanced configuration + +For each of the `input` and `select` configs, there is an option +`get_config`. This can be a function that accepts the `opts` parameter that +is passed in to `vim.select` or `vim.input`. It must return either `nil` (to +no-op) or config values to use in place of the global config values for that +module. + +For example, if you want to use a specific configuration for code actions: + +```lua +require('dressing').setup({ + select = { + get_config = function(opts) + if opts.kind == 'codeaction' then + return { + backend = 'nui', + nui = { + relative = 'cursor', + max_width = 40, + } + } + end + end + } +}) + +``` + +## Notes for plugin authors + +TL;DR: you can customize the telescope `vim.ui.select` implementation by passing `telescope` into `opts`. + +The `vim.ui` hooks are a great boon for us because we can now assume that users +will have a reasonable UI available for simple input operations. We no longer +have to build separate implementations for each of fzf, telescope, ctrlp, etc. +The tradeoff is that `vim.ui.select` is less customizable than any of these +options, so if you wanted to have a preview window (like telescope supports), it +is no longer an option. + +My solution to this is extending the `opts` that are passed to `vim.ui.select`. +You can add a `telescope` field that will be passed directly into the picker, +allowing you to customize any part of the UI. If a user has both dressing and +telescope installed, they will get your custom picker UI. If either of those +are not true, the selection UI will gracefully degrade to whatever the user has +configured for `vim.ui.select`. + +An example of usage: + +```lua +vim.ui.select({'apple', 'banana', 'mango'}, { + prompt = "Title", + telescope = require("telescope.themes").get_cursor(), +}, function(selected) end) +``` + +For now this is available only for the telescope backend, but feel free to request additions. + +## Alternative and related projects + +- [telescope-ui-select](https://github.com/nvim-telescope/telescope-ui-select.nvim) - provides a `vim.ui.select` implementation for telescope +- [nvim-fzy](https://github.com/mfussenegger/nvim-fzy) - fzf alternative that also provides a `vim.ui.select` implementation ([#13](https://github.com/mfussenegger/nvim-fzy/pull/13)) +- [guihua.lua](https://github.com/ray-x/guihua.lua) - multipurpose GUI library that provides `vim.ui.select` and `vim.ui.input` implementations +- [nvim-notify](https://github.com/rcarriga/nvim-notify) - doing pretty much the + same thing but for `vim.notify` +- [nui.nvim](https://github.com/MunifTanjim/nui.nvim) - provides common UI + components for plugin authors diff --git a/etc/soft/nvim/+plugins/dressing.nvim/autoload/dressing.vim b/etc/soft/nvim/+plugins/dressing.nvim/autoload/dressing.vim new file mode 100644 index 0000000..2d549bf --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/autoload/dressing.vim @@ -0,0 +1,12 @@ +function! dressing#fzf_run(labels, options, window) abort + call fzf#run(fzf#wrap({ + \ 'source': a:labels, + \ 'sink': funcref('dressing#fzf_choice'), + \ 'options': a:options, + \ 'window': a:window, + \})) +endfunction + +function! dressing#fzf_choice(label) abort + call v:lua.dressing_fzf_choice(a:label) +endfunction diff --git a/etc/soft/nvim/+plugins/dressing.nvim/doc/dressing.txt b/etc/soft/nvim/+plugins/dressing.nvim/doc/dressing.txt new file mode 100644 index 0000000..c8809e6 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/doc/dressing.txt @@ -0,0 +1,208 @@ +*dressing.txt* +*Dressing* *dressing* *dressing.nvim* +=============================================================================== +CONFIGURATION *dressing-configuration* + +Configure dressing.nvim by calling the setup() function. +> + require('dressing').setup({ + input = { + -- Set to false to disable the vim.ui.input implementation + enabled = true, + + -- Default prompt string + default_prompt = "Input:", + + -- Can be 'left', 'right', or 'center' + prompt_align = "left", + + -- When true, will close the modal + insert_only = true, + + -- When true, input will start in insert mode. + start_in_insert = true, + + -- These are passed to nvim_open_win + anchor = "SW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "cursor", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + prefer_width = 40, + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- Set to `false` to disable + mappings = { + n = { + [""] = "Close", + [""] = "Confirm", + }, + i = { + [""] = "Close", + [""] = "Confirm", + [""] = "HistoryPrev", + [""] = "HistoryNext", + }, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + + -- see :help dressing_get_config + get_config = nil, + }, + select = { + -- Set to false to disable the vim.ui.select implementation + enabled = true, + + -- Priority list of preferred vim.select implementations + backend = { "telescope", "fzf_lua", "fzf", "builtin", "nui" }, + + -- Trim trailing `:` from prompt + trim_prompt = true, + + -- Options for telescope selector + -- These are passed into the telescope picker directly. Can be used like: + -- telescope = require('telescope.themes').get_ivy({...}) + telescope = nil, + + -- Options for fzf selector + fzf = { + window = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for fzf_lua selector + fzf_lua = { + winopts = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for nui Menu + nui = { + position = "50%", + size = nil, + relative = "editor", + border = { + style = "rounded", + }, + buf_options = { + swapfile = false, + filetype = "DressingSelect", + }, + win_options = { + winblend = 10, + }, + max_width = 80, + max_height = 40, + min_width = 40, + min_height = 10, + }, + + -- Options for built-in selector + builtin = { + -- These are passed to nvim_open_win + anchor = "NW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "editor", + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + -- the min_ and max_ options can be a list of mixed types. + -- max_width = {140, 0.8} means "the lesser of 140 columns or 80% of total" + width = nil, + max_width = { 140, 0.8 }, + min_width = { 40, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 10, 0.2 }, + + -- Set to `false` to disable + mappings = { + [""] = "Close", + [""] = "Close", + [""] = "Confirm", + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + }, + + -- Used to override format_item. See :help dressing-format + format_item_override = {}, + + -- see :help dressing_get_config + get_config = nil, + }, + }) + +dressing.get_config() *dressing_get_config()* + For each of the `input` and `select` configs, there is an option + `get_config`. This can be a function that accepts the `opts` parameter that + is passed in to `vim.select` or `vim.input`. It must return either `nil` (to + no-op) or config values to use in place of the global config values for that + module. + + For example, if you want to use a specific configuration for code actions: +> + require('dressing').setup({ + select = { + get_config = function(opts) + if opts.kind == 'codeaction' then + return { + backend = 'nui', + nui = { + relative = 'cursor', + max_width = 40, + } + } + end + end + } + }) + +=============================================================================== + *dressing-format* +Sometimes you may wish to change how choices are displayed for +`vim.ui.select`. The calling function can pass a specific "kind" to the select +function (for example, code actions from |vim.lsp.buf.code_action| +use kind="codeaction"). You can, in turn, specify an override for the +"format_item" function when selecting for that kind. For example, this +configuration will display the name of the language server next to code +actions: +> + format_item_override = { + codeaction = function(action_tuple) + local title = action_tuple[2].title:gsub("\r\n", "\\r\\n") + local client = vim.lsp.get_client_by_id(action_tuple[1]) + return string.format("%s\t[%s]", title:gsub("\n", "\\n"), client.name) + end, + } + +=============================================================================== +vim:ft=help:et:ts=2:sw=2:sts=2:norl diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/config.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/config.lua new file mode 100644 index 0000000..8f6ef3e --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/config.lua @@ -0,0 +1,213 @@ +local default_config = { + input = { + -- Set to false to disable the vim.ui.input implementation + enabled = true, + + -- Default prompt string + default_prompt = "Input:", + + -- Can be 'left', 'right', or 'center' + prompt_align = "left", + + -- When true, will close the modal + insert_only = true, + + -- When true, input will start in insert mode. + start_in_insert = true, + + -- These are passed to nvim_open_win + anchor = "SW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "cursor", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + prefer_width = 40, + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- Set to `false` to disable + mappings = { + n = { + [""] = "Close", + [""] = "Confirm", + }, + i = { + [""] = "Close", + [""] = "Confirm", + [""] = "HistoryPrev", + [""] = "HistoryNext", + }, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + + -- see :help dressing_get_config + get_config = nil, + }, + select = { + -- Set to false to disable the vim.ui.select implementation + enabled = true, + + -- Priority list of preferred vim.select implementations + backend = { "telescope", "fzf_lua", "fzf", "builtin", "nui" }, + + -- Trim trailing `:` from prompt + trim_prompt = true, + + -- Options for telescope selector + -- These are passed into the telescope picker directly. Can be used like: + -- telescope = require('telescope.themes').get_ivy({...}) + telescope = nil, + + -- Options for fzf selector + fzf = { + window = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for fzf_lua selector + fzf_lua = { + winopts = { + width = 0.5, + height = 0.4, + }, + }, + + -- Options for nui Menu + nui = { + position = "50%", + size = nil, + relative = "editor", + border = { + style = "rounded", + }, + buf_options = { + swapfile = false, + filetype = "DressingSelect", + }, + win_options = { + winblend = 10, + }, + max_width = 80, + max_height = 40, + min_width = 40, + min_height = 10, + }, + + -- Options for built-in selector + builtin = { + -- These are passed to nvim_open_win + anchor = "NW", + border = "rounded", + -- 'editor' and 'win' will default to being centered + relative = "editor", + + -- Window transparency (0-100) + winblend = 10, + -- Change default highlight groups (see :help winhl) + winhighlight = "", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + -- the min_ and max_ options can be a list of mixed types. + -- max_width = {140, 0.8} means "the lesser of 140 columns or 80% of total" + width = nil, + max_width = { 140, 0.8 }, + min_width = { 40, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 10, 0.2 }, + + -- Set to `false` to disable + mappings = { + [""] = "Close", + [""] = "Close", + [""] = "Confirm", + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + }, + + -- Used to override format_item. See :help dressing-format + format_item_override = {}, + + -- see :help dressing_get_config + get_config = nil, + }, +} + +local M = vim.deepcopy(default_config) + +M.update = function(opts) + local newconf = vim.tbl_deep_extend("force", default_config, opts or {}) + + if + newconf.input.row + or newconf.input.col + or newconf.select.builtin.row + or newconf.select.builtin.col + then + vim.notify( + "Deprecated: Dressing row and col are no longer used. Use the override to customize layout (:help dressing)", + vim.log.levels.WARN + ) + end + + if + newconf.select.telescope + and newconf.select.telescope.theme + and vim.tbl_count(newconf.select.telescope) == 1 + then + vim.notify( + "Deprecated: dressing.select.telescope.theme is deprecated. Pass in telescope options directly (:help dressing)", + vim.log.levels.WARN + ) + local theme = newconf.select.telescope.theme + local ttype = type(theme) + if ttype == "string" then + newconf.select.telescope = require("telescope.themes")[string.format("get_%s", theme)]() + elseif ttype == "function" then + newconf.select.telescope = theme({}) + else + newconf.select.telescope = theme + end + end + + for k, v in pairs(newconf) do + M[k] = v + end +end + +-- Used to get the effective config value for a module. +-- Use like: config.get_mod_config('input') +M.get_mod_config = function(key, ...) + if not M[key].get_config then + return M[key] + end + local conf = M[key].get_config(...) + if conf then + return vim.tbl_deep_extend("force", M[key], conf) + else + return M[key] + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/init.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/init.lua new file mode 100644 index 0000000..3b76399 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/init.lua @@ -0,0 +1,28 @@ +local patch = require("dressing.patch") + +local M = {} + +M.setup = function(opts) + require("dressing.config").update(opts) + patch.all() +end + +---Patch all the vim.ui methods +M.patch = function() + patch.all() +end + +---Unpatch all the vim.ui methods +---@param names? string[] Names of vim.ui modules to unpatch +M.unpatch = function(names) + if not names then + return patch.all(false) + elseif type(names) ~= "table" then + names = { names } + end + for _, name in ipairs(names) do + patch.mod(name, false) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/input.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/input.lua new file mode 100644 index 0000000..7cfdfdd --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/input.lua @@ -0,0 +1,364 @@ +local map_util = require("dressing.map_util") +local global_config = require("dressing.config") +local patch = require("dressing.patch") +local util = require("dressing.util") +local M = {} + +local context = { + opts = nil, + on_confirm = nil, + winid = nil, + history_idx = nil, + history_tip = nil, + start_in_insert = nil, +} + +local keymaps = { + { + desc = "Close vim.ui.input without a result", + plug = "DressingInput:Close", + rhs = function() + M.close() + end, + }, + { + desc = "Close vim.ui.input with the current buffer contents", + plug = "DressingInput:Confirm", + rhs = function() + M.confirm() + end, + }, + { + desc = "Show previous vim.ui.input history entry", + plug = "DressingInput:HistoryPrev", + rhs = function() + M.history_prev() + end, + }, + { + desc = "Show next vim.ui.input history entry", + plug = "DressingInput:HistoryNext", + rhs = function() + M.history_next() + end, + }, +} + +local function set_input(text) + vim.api.nvim_buf_set_lines(0, 0, -1, true, { text }) + vim.api.nvim_win_set_cursor(0, { 1, vim.api.nvim_strwidth(text) }) +end +local history = {} +M.history_prev = function() + if context.history_idx == nil then + if #history == 0 then + return + end + context.history_tip = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1] + context.history_idx = #history + elseif context.history_idx == 1 then + return + else + context.history_idx = context.history_idx - 1 + end + set_input(history[context.history_idx]) +end +M.history_next = function() + if not context.history_idx then + return + elseif context.history_idx == #history then + context.history_idx = nil + set_input(context.history_tip) + else + context.history_idx = context.history_idx + 1 + set_input(history[context.history_idx]) + end +end + +local function close_completion_window() + if vim.fn.pumvisible() == 1 then + local escape_key = vim.api.nvim_replace_termcodes("", true, false, true) + vim.api.nvim_feedkeys(escape_key, "n", true) + end +end + +local function confirm(text) + if not context.on_confirm then + return + end + close_completion_window() + local ctx = context + context = {} + if not ctx.start_in_insert then + vim.cmd("stopinsert") + end + -- We have to wait briefly for the popup window to close (if present), + -- otherwise vim gets into a very weird and bad state. I was seeing text get + -- deleted from the buffer after the input window closes. + vim.defer_fn(function() + pcall(vim.api.nvim_win_close, ctx.winid, true) + if text and history[#history] ~= text then + table.insert(history, text) + end + -- Defer the callback because we just closed windows and left insert mode. + -- In practice from my testing, if the user does something right now (like, + -- say, opening another input modal) it could happen improperly. I was + -- seeing my successive modals fail to enter insert mode. + vim.defer_fn(function() + ctx.on_confirm(text) + end, 5) + end, 5) +end + +M.confirm = function() + local text = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1] + confirm(text) +end + +M.close = function() + confirm(context.opts and context.opts.cancelreturn) +end + +M.highlight = function() + if not context.opts then + return + end + local bufnr = vim.api.nvim_win_get_buf(context.winid) + local opts = context.opts + local text = vim.api.nvim_buf_get_lines(bufnr, 0, 1, true)[1] + local ns = vim.api.nvim_create_namespace("DressingHighlight") + local highlights = {} + if type(opts.highlight) == "function" then + highlights = opts.highlight(text) + elseif opts.highlight then + highlights = vim.fn[opts.highlight](text) + end + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + for _, highlight in ipairs(highlights) do + local start = highlight[1] + local stop = highlight[2] + local group = highlight[3] + vim.api.nvim_buf_add_highlight(bufnr, ns, group, 0, start, stop) + end +end + +local function split(string, pattern) + local ret = {} + for token in string.gmatch(string, "[^" .. pattern .. "]+") do + table.insert(ret, token) + end + return ret +end + +M.completefunc = function(findstart, base) + if not context.opts or not context.opts.completion then + return findstart == 1 and 0 or {} + end + if findstart == 1 then + return 0 + else + local completion = context.opts.completion + local pieces = split(completion, ",") + if pieces[1] == "custom" or pieces[1] == "customlist" then + local vimfunc = pieces[2] + local ret + if vim.startswith(vimfunc, "v:lua.") then + local load_func = string.format("return %s(...)", vimfunc:sub(7)) + local luafunc, err = loadstring(load_func) + if not luafunc then + vim.api.nvim_err_writeln( + string.format("Could not find completion function %s: %s", vimfunc, err) + ) + return {} + end + ret = luafunc(base, base, vim.fn.strlen(base)) + else + ret = vim.fn[vimfunc](base, base, vim.fn.strlen(base)) + end + if pieces[1] == "custom" then + ret = split(ret, "\n") + end + return ret + else + local ok, result = pcall(vim.fn.getcompletion, base, context.opts.completion) + if ok then + return result + else + vim.api.nvim_err_writeln( + string.format("dressing.nvim: unsupported completion method '%s'", completion) + ) + return {} + end + end + end +end + +_G.dressing_input_complete = M.completefunc + +M.trigger_completion = function() + if vim.fn.pumvisible() == 1 then + return "" + else + return "" + end +end + +local function create_or_update_win(config, prompt, opts) + local parent_win = 0 + local winopt + local win_conf + -- If the previous window is still open and valid, we're going to update it + if context.winid and vim.api.nvim_win_is_valid(context.winid) then + win_conf = vim.api.nvim_win_get_config(context.winid) + parent_win = win_conf.win + winopt = { + relative = win_conf.relative, + win = win_conf.win, + } + else + winopt = { + relative = config.relative, + anchor = config.anchor, + border = config.border, + height = 1, + style = "minimal", + noautocmd = true, + } + end + -- First calculate the desired base width of the modal + local prefer_width = + util.calculate_width(config.relative, config.prefer_width, config, parent_win) + -- Then expand the width to fit the prompt and default value + prefer_width = math.max(prefer_width, 4 + vim.api.nvim_strwidth(prompt)) + if opts.default then + prefer_width = math.max(prefer_width, 2 + vim.api.nvim_strwidth(opts.default)) + end + -- Then recalculate to clamp final value to min/max + local width = util.calculate_width(config.relative, prefer_width, config, parent_win) + winopt.row = util.calculate_row(config.relative, 1, parent_win) + winopt.col = util.calculate_col(config.relative, width, parent_win) + winopt.width = width + + if win_conf and config.relative == "cursor" then + -- If we're cursor-relative we should actually not adjust the row/col to + -- prevent jumping. Also remove related args. + if config.relative == "cursor" then + winopt.row = nil + winopt.col = nil + winopt.relative = nil + winopt.win = nil + end + end + + winopt = config.override(winopt) or winopt + + -- If the floating win was already open + if win_conf then + -- Make sure the previous on_confirm callback is called with nil + vim.schedule(context.on_confirm) + vim.api.nvim_win_set_config(context.winid, winopt) + local start_in_insert = context.start_in_insert + return context.winid, start_in_insert + else + local start_in_insert = string.sub(vim.api.nvim_get_mode().mode, 1, 1) == "i" + local bufnr = vim.api.nvim_create_buf(false, true) + local winid = vim.api.nvim_open_win(bufnr, true, winopt) + return winid, start_in_insert + end +end + +setmetatable(M, { + -- use schedule_wrap to avoid a bug when vim opens + -- (see https://github.com/stevearc/dressing.nvim/issues/15) + __call = util.schedule_wrap_before_vimenter(function(_, opts, on_confirm) + vim.validate({ + on_confirm = { on_confirm, "function", false }, + }) + opts = opts or {} + if type(opts) ~= "table" then + opts = { prompt = tostring(opts) } + end + local config = global_config.get_mod_config("input", opts) + if not config.enabled then + return patch.original_mods.input(opts, on_confirm) + end + if vim.fn.hlID("DressingInputText") ~= 0 then + vim.notify( + 'DressingInputText highlight group is deprecated. Set winhighlight="NormalFloat:MyHighlightGroup" instead', + vim.log.levels.WARN + ) + end + + -- Create or update the window + local prompt = opts.prompt or config.default_prompt + + local winid, start_in_insert = create_or_update_win(config, prompt, opts) + context = { + winid = winid, + on_confirm = on_confirm, + opts = opts, + history_idx = nil, + start_in_insert = start_in_insert, + } + vim.api.nvim_win_set_option(winid, "winblend", config.winblend) + vim.api.nvim_win_set_option(winid, "winhighlight", config.winhighlight) + vim.api.nvim_win_set_option(winid, "wrap", false) + local bufnr = vim.api.nvim_win_get_buf(winid) + + -- Finish setting up the buffer + vim.api.nvim_buf_set_option(bufnr, "swapfile", false) + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + + map_util.create_plug_maps(bufnr, keymaps) + for mode, user_maps in pairs(config.mappings) do + map_util.create_maps_to_plug(bufnr, mode, user_maps, "DressingInput:") + end + + if config.insert_only then + vim.keymap.set("i", "", M.close, { buffer = bufnr }) + end + + vim.api.nvim_buf_set_option(bufnr, "filetype", "DressingInput") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { opts.default or "" }) + -- Disable nvim-cmp if installed + local ok, cmp = pcall(require, "cmp") + if ok then + cmp.setup.buffer({ enabled = false }) + end + -- Disable mini.nvim completion if installed + vim.api.nvim_buf_set_var(bufnr, "minicompletion_disable", true) + util.add_title_to_win( + winid, + string.gsub(prompt, "^%s*(.-)%s*$", "%1"), + { align = config.prompt_align } + ) + + vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { + desc = "Update highlights", + buffer = bufnr, + callback = M.highlight, + }) + + if opts.completion then + vim.api.nvim_buf_set_option(bufnr, "completefunc", "v:lua.dressing_input_complete") + vim.api.nvim_buf_set_option(bufnr, "omnifunc", "") + vim.keymap.set("i", "", M.trigger_completion, { buffer = bufnr, expr = true }) + end + + vim.api.nvim_create_autocmd("BufLeave", { + desc = "Cancel vim.ui.input", + buffer = bufnr, + nested = true, + once = true, + callback = M.close, + }) + + if config.start_in_insert then + vim.cmd("startinsert!") + end + close_completion_window() + M.highlight() + end), +}) + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/map_util.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/map_util.lua new file mode 100644 index 0000000..61b1e3b --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/map_util.lua @@ -0,0 +1,38 @@ +local M = {} + +M.create_plug_maps = function(bufnr, plug_bindings) + for _, binding in ipairs(plug_bindings) do + vim.keymap.set("", binding.plug, binding.rhs, { buffer = bufnr, desc = binding.desc }) + end +end + +---@param bufnr number +---@param mode string +---@param bindings table +---@param prefix string +M.create_maps_to_plug = function(bufnr, mode, bindings, prefix) + local maps + if mode == "i" then + maps = vim.api.nvim_buf_get_keymap(bufnr, "") + end + for lhs, rhs in pairs(bindings) do + if rhs then + -- Prefix with unless this is a or :Cmd mapping + if type(rhs) == "string" and not rhs:match("[<:]") then + rhs = "" .. prefix .. rhs + end + if mode == "i" then + -- HACK for some reason I can't get plug mappings to work in insert mode + for _, map in ipairs(maps) do + if map.lhs == rhs then + rhs = map.callback or map.rhs + break + end + end + end + vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, remap = true }) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/patch.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/patch.lua new file mode 100644 index 0000000..169747d --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/patch.lua @@ -0,0 +1,42 @@ +local all_modules = { "input", "select" } + +local M = {} + +-- For Neovim before 0.6 +if not vim.ui then + vim.ui = {} +end + +local enabled_mods = {} +M.original_mods = {} + +for _, key in ipairs(all_modules) do + M.original_mods[key] = vim.ui[key] + vim.ui[key] = function(...) + local enabled = enabled_mods[key] + if enabled == nil then + enabled = require("dressing.config")[key].enabled + end + if enabled then + require(string.format("dressing.%s", key))(...) + else + return M.original_mods[key](...) + end + end +end + +---Patch or unpatch all vim.ui methods +---@param enabled? boolean When nil, use the default from config +M.all = function(enabled) + for _, name in ipairs(all_modules) do + M.mod(name, enabled) + end +end + +---@param name string +---@param enabled? boolean When nil, use the default from config +M.mod = function(name, enabled) + enabled_mods[name] = enabled +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/builtin.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/builtin.lua new file mode 100644 index 0000000..95d88b1 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/builtin.lua @@ -0,0 +1,110 @@ +local map_util = require("dressing.map_util") +local util = require("dressing.util") +local M = {} + +local keymaps = { + { + desc = "Close vim.ui.select without a result", + plug = "DressingSelect:Close", + rhs = function() + M.cancel() + end, + }, + { + desc = "Select the current vim.ui.select item under the cursor", + plug = "DressingSelect:Confirm", + rhs = function() + M.choose() + end, + }, +} + +M.is_supported = function() + return true +end + +local _callback = function(item, idx) end +local _items = {} +local function clear_callback() + _callback = function() end + _items = {} +end + +M.select = function(config, items, opts, on_choice) + if vim.fn.hlID("DressingSelectText") ~= 0 then + vim.notify( + 'DressingSelectText highlight group is deprecated. Set winhighlight="NormalFloat:MyHighlightGroup" instead', + vim.log.levels.WARN + ) + end + _callback = on_choice + _items = items + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(bufnr, "swapfile", false) + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + local lines = {} + local max_width = 1 + for _, item in ipairs(items) do + local line = opts.format_item(item) + max_width = math.max(max_width, vim.api.nvim_strwidth(line)) + table.insert(lines, line) + end + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + local width = util.calculate_width(config.relative, max_width, config, 0) + local height = util.calculate_height(config.relative, #lines, config, 0) + local row = util.calculate_row(config.relative, height, 0) + local col = util.calculate_col(config.relative, width, 0) + local winopt = { + relative = config.relative, + anchor = config.anchor, + row = row, + col = col, + border = config.border, + width = width, + height = height, + zindex = 150, + style = "minimal", + } + winopt = config.override(winopt) or winopt + local winnr = vim.api.nvim_open_win(bufnr, true, winopt) + vim.api.nvim_win_set_option(winnr, "winblend", config.winblend) + vim.api.nvim_win_set_option(winnr, "winhighlight", config.winhighlight) + vim.api.nvim_win_set_option(winnr, "cursorline", true) + pcall(vim.api.nvim_win_set_option, winnr, "cursorlineopt", "both") + vim.api.nvim_buf_set_option(bufnr, "filetype", "DressingSelect") + util.add_title_to_win(winnr, opts.prompt) + + map_util.create_plug_maps(bufnr, keymaps) + map_util.create_maps_to_plug(bufnr, "n", config.mappings, "DressingSelect:") + vim.api.nvim_create_autocmd("BufLeave", { + desc = "Cancel vim.ui.select", + buffer = bufnr, + nested = true, + once = true, + callback = M.cancel, + }) +end + +local function close_window() + local callback = _callback + local items = _items + clear_callback() + vim.api.nvim_win_close(0, true) + return callback, items +end + +M.choose = function() + local cursor = vim.api.nvim_win_get_cursor(0) + local idx = cursor[1] + local callback, items = close_window() + local item = items[idx] + callback(item, idx) +end + +M.cancel = function() + local callback = close_window() + callback(nil, nil) +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf.lua new file mode 100644 index 0000000..ee1839d --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf.lua @@ -0,0 +1,41 @@ +local M = {} + +M.is_supported = function() + return vim.fn.exists("*fzf#run") ~= 0 +end + +local function clear_callback() + _G.dressing_fzf_choice = function() end + _G.dressing_fzf_cancel = function() end +end + +clear_callback() + +M._on_term_close = function() + if vim.v.event.status ~= 0 then + _G.dressing_fzf_cancel() + end +end + +M.select = function(config, items, opts, on_choice) + local labels = {} + for i, item in ipairs(items) do + table.insert(labels, string.format("%d: %s", i, opts.format_item(item))) + end + _G.dressing_fzf_cancel = function() + clear_callback() + on_choice(nil, nil) + end + _G.dressing_fzf_choice = function(label) + clear_callback() + local colon = string.find(label, ":") + local lnum = tonumber(string.sub(label, 1, colon - 1)) + local item = items[lnum] + on_choice(item, lnum) + end + vim.fn["dressing#fzf_run"](labels, string.format('--prompt="%s"', opts.prompt), config.window) + -- fzf doesn't have a cancel callback, so we have to make one. + vim.cmd([[autocmd TermClose ++once lua require('dressing.select.fzf')._on_term_close()]]) +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf_lua.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf_lua.lua new file mode 100644 index 0000000..e1153f1 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/fzf_lua.lua @@ -0,0 +1,40 @@ +local M = {} + +M.is_supported = function() + return pcall(require, "fzf-lua") +end + +M.select = function(config, items, opts, on_choice) + local fzf = require("fzf-lua") + local labels = {} + for i, item in ipairs(items) do + table.insert(labels, string.format("%d: %s", i, opts.format_item(item))) + end + + local prompt = (opts.prompt or "Select one of") .. "> " + + local fzf_opts = vim.tbl_deep_extend("keep", config, { + prompt = prompt, + fzf_opts = { + ["--no-multi"] = "", + ["--preview-window"] = "hidden:right:0", + }, + actions = { + -- "default" gets called when pressing "enter" + -- all fzf style binds (i.e. "ctrl-y") are valid + ["default"] = function(selected, _) + if not selected then + on_choice(nil, nil) + else + local label = selected[1] + local lnum = tonumber(label:match("^(%d+):")) + local item = items[lnum] + on_choice(item, lnum) + end + end, + }, + }) + fzf.fzf_exec(labels, fzf_opts) +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/init.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/init.lua new file mode 100644 index 0000000..0fb427f --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/init.lua @@ -0,0 +1,74 @@ +local global_config = require("dressing.config") +local patch = require("dressing.patch") + +local function get_backend(config) + local backends = config.backend + if type(backends) ~= "table" then + backends = { backends } + end + for _, backend in ipairs(backends) do + local ok, mod = pcall(require, string.format("dressing.select.%s", backend)) + if ok and mod.is_supported() then + return mod, backend + end + end + return require("dressing.select.builtin"), "builtin" +end + +-- use schedule_wrap to avoid a bug when vim opens +-- (see https://github.com/stevearc/dressing.nvim/issues/15) +-- also to prevent focus problems for providers +-- (see https://github.com/stevearc/dressing.nvim/issues/59) +return vim.schedule_wrap(function(items, opts, on_choice) + vim.validate({ + items = { + items, + function(a) + return type(a) == "table" and vim.tbl_islist(a) + end, + "list-like table", + }, + on_choice = { on_choice, "function", false }, + }) + opts = opts or {} + local config = global_config.get_mod_config("select", opts, items) + + if not config.enabled then + return patch.original_mods.select(items, opts, on_choice) + end + + opts.prompt = opts.prompt or "Select one of:" + if config.trim_prompt and opts.prompt:sub(-1, -1) == ":" then + opts.prompt = opts.prompt:sub(1, -2) + end + + local format_override = config.format_item_override[opts.kind] + if format_override then + opts.format_item = format_override + elseif opts.format_item then + -- format_item doesn't *technically* have to return a string for the + -- core implementation. We should maintain compatibility by wrapping the + -- return value with tostring + local format_item = opts.format_item + opts.format_item = function(item) + return tostring(format_item(item)) + end + else + opts.format_item = tostring + end + + local backend, name = get_backend(config) + local winid = vim.api.nvim_get_current_win() + local cursor = vim.api.nvim_win_get_cursor(winid) + backend.select( + config[name], + items, + opts, + vim.schedule_wrap(function(...) + if vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_cursor(winid, cursor) + end + on_choice(...) + end) + ) +end) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/nui.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/nui.lua new file mode 100644 index 0000000..da3412a --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/nui.lua @@ -0,0 +1,63 @@ +local M = {} + +M.is_supported = function() + return pcall(require, "nui.menu") +end + +M.select = function(config, items, opts, on_choice) + local Menu = require("nui.menu") + local event = require("nui.utils.autocmd").event + local lines = {} + local line_width = 1 + for i, item in ipairs(items) do + local line = opts.format_item(item) + line_width = math.max(line_width, vim.api.nvim_strwidth(line)) + table.insert(lines, Menu.item(line, { value = item, idx = i })) + end + + if not config.size then + line_width = math.max(line_width, config.min_width) + local height = math.max(#lines, config.min_height) + config.size = { + width = line_width, + height = height, + } + end + + local border = vim.deepcopy(config.border) + border.text = { + top = opts.prompt, + top_align = "center", + } + local menu = Menu({ + position = config.position, + size = config.size, + relative = config.relative, + border = border, + buf_options = config.buf_options, + win_options = config.win_options, + enter = true, + }, { + lines = lines, + max_width = config.max_width, + max_height = config.max_height, + keymap = { + focus_next = { "j", "", "" }, + focus_prev = { "k", "", "" }, + close = { "", "" }, + submit = { "" }, + }, + on_close = function() + on_choice(nil, nil) + end, + on_submit = function(item) + on_choice(item.value, item.idx) + end, + }) + + menu:mount() + + menu:on(event.BufLeave, menu.menu_props.on_close, { once = true }) +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/telescope.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/telescope.lua new file mode 100644 index 0000000..558c0ce --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/select/telescope.lua @@ -0,0 +1,138 @@ +local M = {} + +M.is_supported = function() + return pcall(require, "telescope") +end + +M.custom_kind = { + codeaction = function(opts, defaults, items) + local entry_display = require("telescope.pickers.entry_display") + local finders = require("telescope.finders") + local displayer + + local function make_display(entry) + local columns = { + { entry.idx .. ":", "TelescopePromptPrefix" }, + entry.text, + { entry.client_name, "Comment" }, + } + return displayer(columns) + end + + local entries = {} + local client_width = 1 + local text_width = 1 + local idx_width = 1 + for idx, item in ipairs(items) do + local client_id = item[1] + local client_name = vim.lsp.get_client_by_id(client_id).name + local text = opts.format_item(item) + + client_width = math.max(client_width, vim.api.nvim_strwidth(client_name)) + text_width = math.max(text_width, vim.api.nvim_strwidth(text)) + idx_width = math.max(idx_width, vim.api.nvim_strwidth(tostring(idx))) + + table.insert(entries, { + idx = idx, + display = make_display, + text = text, + client_name = client_name, + ordinal = idx .. " " .. text .. " " .. client_name, + value = item, + }) + end + displayer = entry_display.create({ + separator = " ", + items = { + { width = idx_width + 1 }, + { width = text_width }, + { width = client_width }, + }, + }) + + defaults.finder = finders.new_table({ + results = entries, + entry_maker = function(item) + return item + end, + }) + end, +} + +M.select = function(config, items, opts, on_choice) + local themes = require("telescope.themes") + local actions = require("telescope.actions") + local state = require("telescope.actions.state") + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local conf = require("telescope.config").values + + local entry_maker = function(item) + local formatted = opts.format_item(item) + return { + display = formatted, + ordinal = formatted, + value = item, + } + end + + local picker_opts = config + + -- Default to the dropdown theme if no options supplied + if picker_opts == nil then + picker_opts = themes.get_dropdown() + end + + local defaults = { + prompt_title = opts.prompt, + previewer = false, + finder = finders.new_table({ + results = items, + entry_maker = entry_maker, + }), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = state.get_selected_entry() + local callback = on_choice + -- Replace on_choice with a no-op so closing doesn't trigger it + on_choice = function(_, _) end + actions.close(prompt_bufnr) + if not selection then + -- User did not select anything. + callback(nil, nil) + return + end + local idx = nil + for i, item in ipairs(items) do + if item == selection.value then + idx = i + break + end + end + callback(selection.value, idx) + end) + + actions.close:enhance({ + post = function() + on_choice(nil, nil) + end, + }) + + return true + end, + } + + if M.custom_kind[opts.kind] then + M.custom_kind[opts.kind](opts, defaults, items) + end + + -- Hook to allow the caller of vim.ui.select to customize the telescope opts + if opts.telescope then + pickers.new(opts.telescope, defaults):find() + else + pickers.new(picker_opts, defaults):find() + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/util.lua b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/util.lua new file mode 100644 index 0000000..bbe4351 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/lua/dressing/util.lua @@ -0,0 +1,177 @@ +local M = {} + +local function is_float(value) + local _, p = math.modf(value) + return p ~= 0 +end + +local function calc_float(value, max_value) + if value and is_float(value) then + return math.min(max_value, value * max_value) + else + return value + end +end + +local function calc_list(values, max_value, aggregator, limit) + local ret = limit + if type(values) == "table" then + for _, v in ipairs(values) do + ret = aggregator(ret, calc_float(v, max_value)) + end + return ret + else + ret = aggregator(ret, calc_float(values, max_value)) + end + return ret +end + +local function calculate_dim(desired_size, size, min_size, max_size, total_size) + local ret = calc_float(size, total_size) + local min_val = calc_list(min_size, total_size, math.max, 1) + local max_val = calc_list(max_size, total_size, math.min, total_size) + if not ret then + if not desired_size then + ret = (min_val + max_val) / 2 + else + ret = calc_float(desired_size, total_size) + end + end + ret = math.min(ret, max_val) + ret = math.max(ret, min_val) + return math.floor(ret) +end + +local function get_max_width(relative, winid) + if relative == "editor" then + return vim.o.columns + else + return vim.api.nvim_win_get_width(winid or 0) + end +end + +local function get_max_height(relative, winid) + if relative == "editor" then + return vim.o.lines - vim.o.cmdheight + else + return vim.api.nvim_win_get_height(winid or 0) + end +end + +M.calculate_col = function(relative, width, winid) + if relative == "cursor" then + return 0 + else + return math.floor((get_max_width(relative, winid) - width) / 2) + end +end + +M.calculate_row = function(relative, height, winid) + if relative == "cursor" then + return 0 + else + return math.floor((get_max_height(relative, winid) - height) / 2) + end +end + +M.calculate_width = function(relative, desired_width, config, winid) + return calculate_dim( + desired_width, + config.width, + config.min_width, + config.max_width, + get_max_width(relative, winid) + ) +end + +M.calculate_height = function(relative, desired_height, config, winid) + return calculate_dim( + desired_height, + config.height, + config.min_height, + config.max_height, + get_max_height(relative, winid) + ) +end + +local winid_map = {} +M.add_title_to_win = function(winid, title, opts) + opts = opts or {} + opts.align = opts.align or "center" + if not vim.api.nvim_win_is_valid(winid) then + return + end + -- HACK to force the parent window to position itself + -- See https://github.com/neovim/neovim/issues/13403 + vim.cmd("redraw") + local width = math.min(vim.api.nvim_win_get_width(winid) - 4, 2 + vim.api.nvim_strwidth(title)) + local title_winid = winid_map[winid] + local bufnr + if title_winid and vim.api.nvim_win_is_valid(title_winid) then + vim.api.nvim_win_set_width(title_winid, width) + bufnr = vim.api.nvim_win_get_buf(title_winid) + else + bufnr = vim.api.nvim_create_buf(false, true) + local col = 1 + if opts.align == "center" then + col = math.floor((vim.api.nvim_win_get_width(winid) - width) / 2) + elseif opts.align == "right" then + col = vim.api.nvim_win_get_width(winid) - 1 - width + elseif opts.align ~= "left" then + vim.notify( + string.format("Unknown dressing window title alignment: '%s'", opts.align), + vim.log.levels.ERROR + ) + end + title_winid = vim.api.nvim_open_win(bufnr, false, { + relative = "win", + win = winid, + width = width, + height = 1, + row = -1, + col = col, + focusable = false, + zindex = 151, + style = "minimal", + noautocmd = true, + }) + winid_map[winid] = title_winid + vim.api.nvim_win_set_option( + title_winid, + "winblend", + vim.api.nvim_win_get_option(winid, "winblend") + ) + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + vim.cmd(string.format( + [[ + autocmd WinClosed %d ++once lua require('dressing.util')._on_win_closed(%d) + ]], + winid, + winid + )) + end + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { " " .. title .. " " }) + local ns = vim.api.nvim_create_namespace("DressingWindow") + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + vim.api.nvim_buf_add_highlight(bufnr, ns, "FloatTitle", 0, 0, -1) +end + +M._on_win_closed = function(winid) + local title_winid = winid_map[winid] + if title_winid and vim.api.nvim_win_is_valid(title_winid) then + vim.api.nvim_win_close(title_winid, true) + end + winid_map[winid] = nil +end + +M.schedule_wrap_before_vimenter = function(func) + return function(...) + if vim.v.vim_did_enter == 0 then + return vim.schedule_wrap(func)(...) + else + return func(...) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/dressing.nvim/plugin/dressing.lua b/etc/soft/nvim/+plugins/dressing.nvim/plugin/dressing.lua new file mode 100644 index 0000000..618a316 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/plugin/dressing.lua @@ -0,0 +1,2 @@ +require("dressing").patch() +vim.cmd([[highlight default link FloatTitle FloatBorder]]) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/run_tests.sh b/etc/soft/nvim/+plugins/dressing.nvim/run_tests.sh new file mode 100755 index 0000000..d9d764a --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/run_tests.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +mkdir -p ".testenv/config/nvim" +mkdir -p ".testenv/data/nvim" +mkdir -p ".testenv/state/nvim" +mkdir -p ".testenv/run/nvim" +mkdir -p ".testenv/cache/nvim" +PLUGINS=".testenv/data/nvim/site/pack/plugins/start" + +if [ ! -e "$PLUGINS/plenary.nvim" ]; then + git clone --depth=1 https://github.com/nvim-lua/plenary.nvim.git "$PLUGINS/plenary.nvim" +fi + +XDG_CONFIG_HOME=".testenv/config" \ + XDG_DATA_HOME=".testenv/data" \ + XDG_STATE_HOME=".testenv/state" \ + XDG_RUNTIME_DIR=".testenv/run" \ + XDG_CACHE_HOME=".testenv/cache" \ + nvim --headless -u tests/minimal_init.lua \ + -c "PlenaryBustedDirectory ${1-tests} { minimal_init = './tests/minimal_init.lua' }" +echo "Success" diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/input_spec.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/input_spec.lua new file mode 100644 index 0000000..a95d690 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/input_spec.lua @@ -0,0 +1,181 @@ +require("plenary.async").tests.add_to_env() +local dressing = require("dressing") +local util = require("tests.util") +local channel = a.control.channel + +local function run_input(keys, opts) + opts = opts or {} + local tx, rx = channel.oneshot() + vim.ui.input(opts, tx) + util.feedkeys(vim.list_extend( + { "i" }, -- HACK have to do this because :startinsert doesn't work in tests, + keys + )) + if opts.after_fn then + opts.after_fn() + end + return rx() +end + +a.describe("input modal", function() + before_each(function() + dressing.patch() + dressing.setup() + end) + + after_each(function() + -- Clean up all floating windows so one test failure doesn't cascade + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_get_config(winid).relative ~= "" then + vim.api.nvim_win_close(winid, true) + end + end + end) + + a.it("accepts input", function() + local ret = run_input({ + "my text", + "", + }) + assert(ret == "my text", string.format("Got '%s' expected 'my text'", ret)) + end) + + a.it("Cancels input on ", function() + local ret = run_input({ + "my text", + "", + }) + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + end) + + a.it("cancels input when leaving the window", function() + local ret = run_input({ + "my text", + }, { + after_fn = function() + vim.cmd([[wincmd p]]) + end, + }) + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + end) + + a.it("cancels on when insert_only = true", function() + require("dressing.config").input.insert_only = true + local ret = run_input({ + "my text", + "", + }) + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + end) + + a.it("does not cancel on when insert_only = false", function() + require("dressing.config").input.insert_only = false + local ret = run_input({ + "my text", + "", + "", + }) + assert(ret == "my text", string.format("Got '%s' expected 'my text'", ret)) + end) + + a.it("returns cancelreturn when input is canceled ", function() + local ret = run_input({ + "my text", + "", + }, { cancelreturn = "CANCELED" }) + assert(ret == "CANCELED", string.format("Got '%s' expected nil", ret)) + end) + + a.it("returns empty string when input is empty", function() + local ret = run_input({ + "", + }) + assert(ret == "", string.format("Got '%s' expected nil", ret)) + end) + + a.it("returns empty string when input is empty, even if cancelreturn set", function() + local ret = run_input({ + "", + }, { cancelreturn = "CANCELED" }) + assert(ret == "", string.format("Got '%s' expected nil", ret)) + end) + + a.it("starts in normal mode when start_in_insert = false", function() + local orig_cmd = vim.cmd + local startinsert_called = false + vim.cmd = function(cmd) + if cmd == "startinsert!" then + startinsert_called = true + end + orig_cmd(cmd) + end + + require("dressing.config").input.start_in_insert = false + run_input({ + "my text", + "", + }, { + after_fn = function() + vim.cmd = orig_cmd + end, + }) + assert(not startinsert_called, "Got 'true' expected 'false'") + end) + + a.it("cancels first callback if second input is opened", function() + local tx, rx = channel.oneshot() + vim.ui.input({}, tx) + util.feedkeys({ + "i", -- HACK have to do this because :startinsert doesn't work in tests, + "my text", + }) + vim.ui.input({}, function() end) + local ret = rx() + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + end) + + a.it("supports completion", function() + vim.opt.completeopt = { "menu", "menuone", "noselect" } + vim.cmd([[ + function! CustomComplete(arglead, cmdline, cursorpos) + return "first\nsecond\nthird" + endfunction + ]]) + local ret = run_input({ + "", -- Using tab twice to test both versions of the mapping + }, { + completion = "custom,CustomComplete", + }) + assert(ret == "second", string.format("Got '%s' expected 'second'", ret)) + assert(vim.fn.pumvisible() == 0, "Popup menu should not be visible after leaving modal") + end) + + a.it("can cancel out when popup menu is open", function() + vim.opt.completeopt = { "menu", "menuone", "noselect" } + local ret = run_input({ + "", + "", + }, { + completion = "command", + }) + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + assert(vim.fn.pumvisible() == 0, "Popup menu should not be visible after leaving modal") + end) + + a.it("doesn't delete text in original buffer", function() + -- This is a regression test for weird behavior I was seeing with the + -- completion popup menu + vim.api.nvim_buf_set_lines(0, 0, 1, true, { "some text" }) + vim.api.nvim_win_set_cursor(0, { 1, 4 }) + vim.opt.completeopt = { "menu", "menuone", "noselect" } + local ret = run_input({ + "", + "", + }, { + completion = "command", + }) + assert(ret == nil, string.format("Got '%s' expected nil", ret)) + local line = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1] + assert(line == "some text", "Doing with popup menu open deleted buffer text o.0") + end) +end) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/completion.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/completion.lua new file mode 100644 index 0000000..5e64683 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/completion.lua @@ -0,0 +1,67 @@ +-- Run this test with :source % + +local idx = 1 +local cases = { + { + prompt = "Complete file: ", + completion = "file", + }, + { + prompt = "Complete cmd: ", + completion = "command", + }, + { + prompt = "Complete custom: ", + completion = "custom,CustomComplete", + }, + { + prompt = "Complete customlist: ", + completion = "customlist,CustomCompleteList", + }, + { + prompt = "Complete custom lua: ", + completion = "custom,v:lua.custom_complete_func", + }, + { + prompt = "Complete customlist: ", + completion = "customlist,v:lua.custom_complete_list", + }, +} + +vim.cmd([[ +function! CustomComplete(arglead, cmdline, cursorpos) + return "first\nsecond\nthird" +endfunction + +function! CustomCompleteList(arglead, cmdline, cursorpos) + return ['first', 'second', 'third'] +endfunction +]]) + +function _G.custom_complete_func(arglead, cmdline, cursorpos) + return "first\nsecond\nthird" +end + +function _G.custom_complete_list(arglead, cmdline, cursorpos) + return { "first", "second", "third" } +end + +local function next() + local opts = cases[idx] + if opts then + idx = idx + 1 + vim.ui.input(opts, next) + end +end + +next() + +-- Uncomment this to test opening a modal while the previous one is open +-- vim.ui.input(cases[1], function(text) +-- print(text) +-- end) +-- vim.defer_fn(function() +-- vim.ui.input(cases[2], function(text) +-- print(text) +-- end) +-- end, 2000) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/highlight.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/highlight.lua new file mode 100644 index 0000000..3a0da87 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/highlight.lua @@ -0,0 +1,30 @@ +-- Run this test with :source % + +vim.cmd([[ + highlight RBP1 guibg=Red ctermbg=red + highlight RBP2 guibg=Yellow ctermbg=yellow + highlight RBP3 guibg=Green ctermbg=green + highlight RBP4 guibg=Blue ctermbg=blue +]]) +local rainbow_levels = 4 +local function rainbow_hl(cmdline) + local ret = {} + local lvl = 0 + for i = 1, string.len(cmdline) do + local char = string.sub(cmdline, i, i) + if char == "(" then + table.insert(ret, { i - 1, i, string.format("RBP%d", (lvl % rainbow_levels) + 1) }) + lvl = lvl + 1 + elseif char == ")" then + lvl = lvl - 1 + table.insert(ret, { i - 1, i, string.format("RBP%d", (lvl % rainbow_levels) + 1) }) + end + end + return ret +end + +vim.ui.input({ + prompt = "Rainbow: ", + default = "((()(())))", + highlight = rainbow_hl, +}, function() end) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/select.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/select.lua new file mode 100644 index 0000000..16d6a08 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/manual/select.lua @@ -0,0 +1,21 @@ +-- Run this test with :source % + +local function run_test(backend) + local config = require("dressing.config") + local prev_backend = config.select.backend + config.select.backend = backend + vim.ui.select({ "first", "second", "third" }, { + prompt = "Make selection", + kind = "test", + }, function(item, lnum) + if item and lnum then + vim.notify(string.format("selected '%s' (idx %d)", item, lnum), vim.log.levels.INFO) + else + vim.notify("Selection canceled", vim.log.levels.INFO) + end + config.select.backend = prev_backend + end) +end + +-- Replace this with the desired backend to test +run_test("fzf_lua") diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/minimal_init.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/minimal_init.lua new file mode 100644 index 0000000..d2e93e8 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/minimal_init.lua @@ -0,0 +1,2 @@ +vim.cmd([[set runtimepath+=.]]) +vim.cmd([[runtime! plugin/plenary.vim]]) diff --git a/etc/soft/nvim/+plugins/dressing.nvim/tests/util.lua b/etc/soft/nvim/+plugins/dressing.nvim/tests/util.lua new file mode 100644 index 0000000..909fc91 --- /dev/null +++ b/etc/soft/nvim/+plugins/dressing.nvim/tests/util.lua @@ -0,0 +1,19 @@ +require("plenary.async").tests.add_to_env() +local M = {} + +M.feedkeys = function(actions, timestep) + timestep = timestep or 10 + a.util.sleep(timestep) + for _, action in ipairs(actions) do + a.util.sleep(timestep) + local escaped = vim.api.nvim_replace_termcodes(action, true, false, true) + vim.api.nvim_feedkeys(escaped, "m", true) + end + a.util.sleep(timestep) + -- process pending keys until the queue is empty. + -- Note that this will exit insert mode. + vim.api.nvim_feedkeys("", "x", true) + a.util.sleep(timestep) +end + +return M diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/README.md b/etc/soft/nvim/+plugins/git-conflict.nvim/README.md new file mode 100644 index 0000000..568848c --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/README.md @@ -0,0 +1,114 @@ +# git-conflict.nvim + +https://user-images.githubusercontent.com/22454918/159362564-a66d8c23-f7dc-4d1d-8e88-c5c73a49047e.mov + +A plugin to visualise and resolve conflicts in neovim. +This plugin was inspired by [conflict-marker.vim](https://github.com/rhysd/conflict-marker.vim) + +## Status + +This plugin is under active development, it should generally work, but you're likely to +encounter some bugs during usage. + +## Requirements + +- `git` +- `nvim 0.7+` + +## Installation + +```lua +use {'akinsho/git-conflict.nvim', tag = "*", config = function() + require('git-conflict').setup() +end} +``` + +I recommend using the tag field of you package manager, so your version of this plugin is only updated when a new tag is pushed as `main` itself might be **unstable**. + +## Configuration + +```lua +{ + default_mappings = true, -- disable buffer local mapping created by this plugin + default_commands = true, -- disable commands created by this plugin + disable_diagnostics = false, -- This will disable the diagnostics in a buffer whilst it is conflicted + highlights = { -- They must have background color, otherwise the default color will be used + incoming = 'DiffText', + current = 'DiffAdd', + } +} +``` + +## Commands + +- `GitConflictChooseOurs` — Select the current changes. +- `GitConflictChooseTheirs` — Select the incoming changes. +- `GitConflictChooseBoth` — Select both changes. +- `GitConflictChooseNone` — Select none of the changes. +- `GitConflictNextConflict` — Move to the next conflict. +- `GitConflictPrevConflict` — Move to the previous conflict. +- `GitConflictListQf` — Get all conflict to quickfix + +### Listing conflicts + +You can list conflicts in the quick fix list using the `GitConflictListQf` command + +Screen Shot 2022-03-27 at 12 03 43 + +quickfix displayed using [nvim-pqf](https://gitlab.com/yorickpeterse/nvim-pqf) + +## Autocommands + +When a conflict is detected by this plugin a `User` autocommand is fired +called `GitConflictDetected`. When this is resolved another command is +fired called `GitConflictResolved`. + +Either of these can be used to run logic whilst dealing with conflicts +e.g. + +```lua +vim.api.nvim_create_autocommand('User', { + pattern = 'GitConflictDetected', + callback = function() + vim.notify('Conflict detected in '..vim.fn.expand('')) + vim.keymap.set('n', 'cww', function() + engage.conflict_buster() + create_buffer_local_mappings() + end) + end +}) + +``` + +## Mappings + +This plugin offers default buffer local mappings inside conflicted files. This is primarily because applying these mappings only to relevant buffers +is impossible through global mappings. A user can however disable these by setting `default_mappings = false` anyway and create global mappings as shown below. +The default mappings are: + +- co — choose ours +- ct — choose theirs +- cb — choose both +- c0 — choose none +- ]x — move to previous conflict +- [x — move to next conflict + +If you would rather not use these then disable default mappings an can then map these yourself. + +```lua +vim.keymap.set('n', 'co', '(git-conflict-ours)') +vim.keymap.set('n', 'ct', '(git-conflict-theirs)') +vim.keymap.set('n', 'cb', '(git-conflict-both)') +vim.keymap.set('n', 'c0', '(git-conflict-none)') +vim.keymap.set('n', ']x', '(git-conflict-prev-conflict)') +vim.keymap.set('n', '[x', '(git-conflict-next-conflict)') +``` + +## Issues + +**Please read this** — This plugin is not intended to do anything other than provide fancy visuals, and some mappings to handle conflict resolution +It will not be expanded to become a full git management plugin, there are a zillion plugins that do that already, this won't be one of those. + +### Feature requests + +Open source should be collaborative, if you have an idea for a feature you'd like to see added. Submit a PR rather than a feature request. diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/create_conflict.sh b/etc/soft/nvim/+plugins/git-conflict.nvim/create_conflict.sh new file mode 100755 index 0000000..82b704f --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/create_conflict.sh @@ -0,0 +1,20 @@ +#!/bin/bash +[ -d ./../conflict-test/ ] && rm -rf ./conflict-test/ +mkdir conflict-test +cd conflict-test || exit +git init +touch conflicted.lua +git add conflicted.lua +echo "local value = 1 + 1" > conflicted.lua +git commit -am 'initial' +git checkout -b new_branch +echo "local value = 1 - 1" > conflicted.lua +git commit -am 'first commit on new_branch' +git checkout main +cat > conflicted.lua<< EOF +local value = 5 + 7 +print(value) +print(string.format("value is %d", value)) +EOF +git commit -am 'second commit on main' +git merge new_branch diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict.lua b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict.lua new file mode 100644 index 0000000..6eb1051 --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict.lua @@ -0,0 +1,734 @@ +local M = {} + +local color = require('git-conflict.colors') +local utils = require('git-conflict.utils') + +local fn = vim.fn +local api = vim.api +local fmt = string.format +local map = vim.keymap.set +local job = utils.job +-----------------------------------------------------------------------------// +-- REFERENCES: +-----------------------------------------------------------------------------// +-- Detecting the state of a git repository based on files in the .git directory. +-- https://stackoverflow.com/questions/49774200/how-to-tell-if-my-git-repo-is-in-a-conflict +-- git diff commands to git a list of conflicted files +-- https://stackoverflow.com/questions/3065650/whats-the-simplest-way-to-list-conflicted-files-in-git +-- how to show a full path for files in a git diff command +-- https://stackoverflow.com/questions/10459374/making-git-diff-stat-show-full-file-path +-- Advanced merging +-- https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging + +-----------------------------------------------------------------------------// +-- Types +-----------------------------------------------------------------------------// + +---@alias ConflictSide "'ours'"|"'theirs'"|"'both'"|"'none" + +--- @class ConflictHighlights +--- @field current string +--- @field incoming string +--- @field ancestor string? + +---@class RangeMark +---@field label integer +---@field content string + +--- @class PositionMarks +--- @field current RangeMark +--- @field incoming RangeMark +--- @field ancestor RangeMark + +--- @class Range +--- @field range_start integer +--- @field range_end integer +--- @field content_start integer +--- @field content_end integer + +--- @class ConflictPosition +--- @field incoming Range +--- @field middle Range +--- @field current Range +--- @field marks PositionMarks + +--- @class ConflictBufferCache +--- @field lines table map of conflicted line numbers +--- @field positions ConflictPosition[] +--- @field tick integer +--- @field bufnr integer + +--- @class GitConflictConfig +--- @field default_mappings boolean +--- @field disable_diagnostics boolean +--- @field highlights ConflictHighlights +--- @field debug boolean + +-----------------------------------------------------------------------------// +-- Constants +-----------------------------------------------------------------------------// +local SIDES = { + OURS = 'ours', + THEIRS = 'theirs', + BOTH = 'both', + BASE = 'base', + NONE = 'none', +} + +-- A mapping between the internal names and the display names +local name_map = { + ours = 'current', + theirs = 'incoming', + base = 'ancestor', + both = 'both', + none = 'none', +} + +local CURRENT_HL = 'GitConflictCurrent' +local INCOMING_HL = 'GitConflictIncoming' +local ANCESTOR_HL = 'GitConflictAncestor' +local CURRENT_LABEL_HL = 'GitConflictCurrentLabel' +local INCOMING_LABEL_HL = 'GitConflictIncomingLabel' +local ANCESTOR_LABEL_HL = 'GitConflictAncestorLabel' +local PRIORITY = vim.highlight.priorities.user +local NAMESPACE = api.nvim_create_namespace('git-conflict') +local AUGROUP_NAME = 'GitConflictCommands' + +local sep = package.config:sub(1, 1) + +local conflict_start = '^<<<<<<<' +local conflict_middle = '^=======' +local conflict_end = '^>>>>>>>' +local conflict_ancestor = '^|||||||' + +local DEFAULT_CURRENT_BG_COLOR = 4218238 -- #405d7e +local DEFAULT_INCOMING_BG_COLOR = 3229523 -- #314753 +local DEFAULT_ANCESTOR_BG_COLOR = 6824314 -- #68217A +-----------------------------------------------------------------------------// + +--- @type GitConflictConfig +local config = { + debug = false, + default_mappings = true, + default_commands = true, + disable_diagnostics = false, + highlights = { + current = 'DiffText', + incoming = 'DiffAdd', + ancestor = nil, + }, +} + +--- @return table +local function create_visited_buffers() + return setmetatable({}, { + __index = function(t, k) + if type(k) == 'number' then return t[api.nvim_buf_get_name(k)] end + end, + }) +end + +--- A list of buffers that have conflicts in them. This is derived from +--- git using the diff command, and updated at intervals +local visited_buffers = create_visited_buffers() + +local state = { + ---@type string? + current_watcher_dir = nil, +} + +-----------------------------------------------------------------------------// + +---Get full path to the repository of the directory passed in +---@param dir any +---@param callback fun(data: string) +local function get_git_root(dir, callback) + job(fmt('git -C "%s" rev-parse --show-toplevel', dir), function(data) callback(data[1]) end) +end + +--- Get a list of the conflicted files within the specified directory +--- NOTE: only conflicted files within the git repository of the directory passed in are returned +--- also we add a line prefix to the git command so that the full path is returned +--- e.g. --line-prefix=`git rev-parse --show-toplevel` +---@reference: https://stackoverflow.com/a/10874862 +---@param dir string? +---@param callback fun(files: table, string) +local function get_conflicted_files(dir, callback) + local cmd = fmt('git -C "%s" diff --line-prefix=%s%s --name-only --diff-filter=U', dir, dir, sep) + job(cmd, function(data) + local files = {} + for _, filename in ipairs(data) do + if #filename > 0 then files[filename] = files[filename] or {} end + end + callback(files, dir) + end) +end + +---Add the positions to the buffer in our in memory buffer list +---positions are keyed by a list of range start and end for each mark +---@param buf integer +---@param positions ConflictPosition[] +local function update_visited_buffers(buf, positions) + if not buf or not api.nvim_buf_is_valid(buf) then return end + local name = api.nvim_buf_get_name(buf) + -- If this buffer is not in the list + if not visited_buffers[name] then return end + visited_buffers[name].bufnr = buf + visited_buffers[name].tick = vim.b[buf].changedtick + visited_buffers[name].positions = positions +end + +---Set an extmark for each section of the git conflict +---@param bufnr integer +---@param hl string +---@param range_start integer +---@param range_end integer +---@return integer? extmark_id +local function hl_range(bufnr, hl, range_start, range_end) + if not range_start or not range_end then return end + return api.nvim_buf_set_extmark(bufnr, NAMESPACE, range_start, 0, { + hl_group = hl, + hl_eol = true, + hl_mode = 'combine', + end_row = range_end, + priority = PRIORITY, + }) +end + +---Add highlights and additional data to each section heading of the conflict marker +---These works by covering the underlying text with an extmark that contains the same information +---with some extra detail appended. +---TODO: ideally this could be done by using virtual text at the EOL and highlighting the +---background but this doesn't work and currently this is done by filling the rest of the line with +---empty space and overlaying the line content +---@param bufnr integer +---@param hl_group string +---@param label string +---@param lnum integer +---@return integer extmark id +local function draw_section_label(bufnr, hl_group, label, lnum) + local remaining_space = api.nvim_win_get_width(0) - api.nvim_strwidth(label) + return api.nvim_buf_set_extmark(bufnr, NAMESPACE, lnum, 0, { + hl_group = hl_group, + virt_text = { { label .. string.rep(' ', remaining_space), hl_group } }, + virt_text_pos = 'overlay', + priority = PRIORITY, + }) +end + +---Highlight each part of a git conflict i.e. the incoming changes vs the current/HEAD changes +---TODO: should extmarks be ephemeral? or is it less expensive to save them and only re-apply +---them when a buffer changes since otherwise we have to reparse the whole buffer constantly +---@param positions table +---@param lines string[] +local function highlight_conflicts(positions, lines) + local bufnr = api.nvim_get_current_buf() + M.clear(bufnr) + + for _, position in ipairs(positions) do + local current_start = position.current.range_start + local current_end = position.current.range_end + local incoming_start = position.incoming.range_start + local incoming_end = position.incoming.range_end + -- Add one since the index access in lines is 1 based + local current_label = lines[current_start + 1] .. ' (Current changes)' + local incoming_label = lines[incoming_end + 1] .. ' (Incoming changes)' + + local curr_label_id = draw_section_label(bufnr, CURRENT_LABEL_HL, current_label, current_start) + local curr_id = hl_range(bufnr, CURRENT_HL, current_start, current_end + 1) + local inc_id = hl_range(bufnr, INCOMING_HL, incoming_start, incoming_end) + local inc_label_id = draw_section_label(bufnr, INCOMING_LABEL_HL, incoming_label, incoming_end) + + position.marks = { + current = { label = curr_label_id, content = curr_id }, + incoming = { label = inc_label_id, content = inc_id }, + ancestor = {}, + } + if not vim.tbl_isempty(position.ancestor) then + local ancestor_start = position.ancestor.range_start + local ancestor_end = position.ancestor.range_end + local ancestor_label = lines[ancestor_start + 1] .. ' (Base changes)' + local id = hl_range(bufnr, ANCESTOR_HL, ancestor_start + 1, ancestor_end + 1) + local label_id = draw_section_label(bufnr, ANCESTOR_LABEL_HL, ancestor_label, ancestor_start) + position.marks.ancestor = { label = label_id, content = id } + end + end +end + +---Iterate through the buffer line by line checking there is a matching conflict marker +---when we find a starting mark we collect the position details and add it to a list of positions +---@param lines string[] +---@return boolean +---@return ConflictPosition[] +local function detect_conflicts(lines) + local positions = {} + local position, has_start, has_middle, has_ancestor = nil, false, false, false + for index, line in ipairs(lines) do + local lnum = index - 1 + if line:match(conflict_start) then + has_start = true + position = { + current = { range_start = lnum, content_start = lnum + 1 }, + middle = {}, + incoming = {}, + ancestor = {}, + } + end + if has_start and line:match(conflict_ancestor) then + has_ancestor = true + position.ancestor.range_start = lnum + position.ancestor.content_start = lnum + 1 + position.current.range_end = lnum - 1 + position.current.content_end = lnum - 1 + end + if has_start and line:match(conflict_middle) then + has_middle = true + if has_ancestor then + position.ancestor.content_end = lnum - 1 + position.ancestor.range_end = lnum - 1 + else + position.current.range_end = lnum - 1 + position.current.content_end = lnum - 1 + end + position.middle.range_start = lnum + position.middle.range_end = lnum + 1 + position.incoming.range_start = lnum + 1 + position.incoming.content_start = lnum + 1 + end + if has_start and has_middle and line:match(conflict_end) then + position.incoming.range_end = lnum + position.incoming.content_end = lnum - 1 + positions[#positions + 1] = position + + position, has_start, has_middle, has_ancestor = nil, false, false, false + end + end + return #positions > 0, positions +end + +---Helper function to find a conflict position based on a comparator function +---@param bufnr integer +---@param comparator fun(string, integer): boolean +---@param opts table? +---@return ConflictPosition? +local function find_position(bufnr, comparator, opts) + local match = visited_buffers[bufnr] + if not match then return end + local line = utils.get_cursor_pos() + + if opts and opts.reverse then + for i = #match.positions, 1, -1 do + local position = match.positions[i] + if comparator(line, position) then return position end + end + return nil + end + + for _, position in ipairs(match.positions) do + if comparator(line, position) then return position end + end + return nil +end + +---Retrieves a conflict marker position by checking the visited buffers for a supported range +---@param bufnr integer +---@return ConflictPosition? +local function get_current_position(bufnr) + return find_position( + bufnr, + function(line, position) + return position.current.range_start <= line and position.incoming.range_end >= line + end + ) +end + +---@param position ConflictPosition? +---@param side ConflictSide +local function set_cursor(position, side) + if not position then return end + local target = side == SIDES.OURS and position.current or position.incoming + api.nvim_win_set_cursor(0, { target.range_start + 1, 0 }) +end + +---Get the conflict marker positions for a buffer if any and update the buffers state +---@param bufnr integer +---@param range_start integer +---@param range_end integer +local function parse_buffer(bufnr, range_start, range_end) + local lines = utils.get_buf_lines(range_start or 0, range_end or -1, bufnr) + local prev_conflicts = visited_buffers[bufnr].positions ~= nil + and #visited_buffers[bufnr].positions > 0 + local has_conflict, positions = detect_conflicts(lines) + + update_visited_buffers(bufnr, positions) + if has_conflict then + highlight_conflicts(positions, lines) + else + M.clear(bufnr) + end + if prev_conflicts ~= has_conflict then + local pattern = has_conflict and 'GitConflictDetected' or 'GitConflictResolved' + api.nvim_exec_autocmds('User', { pattern = pattern }) + end +end + +--- Fetch the conflicted files for the current buffer file's repo +--- this is throttled by tracking when last we checked for conflicts +--- and if it is over this interval we check again otherwise we return. +--- When clearing only clear buffers that are in the same repository as the conflicted files +--- as the result (files) might contain only files from a buffer in +--- a different repository in which case extmarks could be cleared for unrelated projects +local function fetch_conflicts(buf) + buf = buf or api.nvim_get_current_buf() + get_git_root(fn.fnamemodify(api.nvim_buf_get_name(buf), ':h'), function(git_root) + get_conflicted_files(git_root, function(files, repo) + for name, b in pairs(visited_buffers) do + -- FIXME: this will not work for nested repositories + if vim.startswith(name, repo) and not files[name] and b.bufnr then + visited_buffers[name] = nil + M.clear(b.bufnr) + end + end + for path, _ in pairs(files) do + visited_buffers[path] = visited_buffers[path] or {} + end + end) + end) +end + +---@type table +local watchers = {} + +local on_throttled_change = utils.throttle(1000, function(dir, err, change) + if err then return utils.notify(fmt('Error watching %s(%s): %s', dir, err, change), 'error') end + if config.debug then utils.notify(fmt('Watching %s - change: %s ', dir, change), 'info') end + fetch_conflicts() +end) + +--- Stop any watchers that aren't for the current project +---@param curr_dir string +local function stop_running_watchers(curr_dir) + for prev_dir, watcher in pairs(watchers) do + if watcher ~= watchers[curr_dir] then + watcher:stop() + watchers[prev_dir] = nil + end + end +end + +--- Create a FS watcher for the current git directory or restart an existing one +---@param dir string +local function watch_gitdir(dir) + -- Stop if there is already a watcher running + if watchers[dir] then return end + + ---@type userdata + watchers[dir] = vim.loop.new_fs_event() + watchers[dir]:start( + dir, + { recursive = true }, + vim.schedule_wrap(function(...) on_throttled_change(dir, ...) end) + ) + state.current_watcher_dir = dir +end + +local throttled_watcher = utils.throttle(1000, watch_gitdir) + +---Process a buffer if the changed tick has changed +---@param bufnr integer? +local function process(bufnr, range_start, range_end) + bufnr = bufnr or api.nvim_get_current_buf() + if visited_buffers[bufnr] and visited_buffers[bufnr].tick == vim.b[bufnr].changedtick then + return + end + parse_buffer(bufnr, range_start, range_end) +end + +-----------------------------------------------------------------------------// +-- Commands +-----------------------------------------------------------------------------// + +local function set_commands() + local command = api.nvim_create_user_command + command('GitConflictRefresh', function() fetch_conflicts() end, { nargs = 0 }) + command('GitConflictListQf', function() + M.conflicts_to_qf_items(function(items) + if #items > 0 then + fn.setqflist(items, 'r') + vim.cmd([[copen]]) + end + end) + end, { nargs = 0 }) + command('GitConflictChooseOurs', function() M.choose('ours') end, { nargs = 0 }) + command('GitConflictChooseTheirs', function() M.choose('theirs') end, { nargs = 0 }) + command('GitConflictChooseBoth', function() M.choose('both') end, { nargs = 0 }) + command('GitConflictChooseBase', function() M.choose('base') end, { nargs = 0 }) + command('GitConflictChooseNone', function() M.choose('none') end, { nargs = 0 }) + command('GitConflictNextConflict', function() M.find_next('ours') end, { nargs = 0 }) + command('GitConflictPrevConflict', function() M.find_prev('ours') end, { nargs = 0 }) +end + +-----------------------------------------------------------------------------// +-- Mappings +-----------------------------------------------------------------------------// + +local function set_plug_mappings() + local function opts(desc) return { silent = true, desc = 'Git Conflict: ' .. desc } end + + map('n', '(git-conflict-ours)', 'GitConflictChooseOurs', opts('Choose Ours')) + map('n', '(git-conflict-both)', 'GitConflictChooseBoth', opts('Choose Both')) + map('n', '(git-conflict-none)', 'GitConflictChooseNone', opts('Choose None')) + map('n', '(git-conflict-theirs)', 'GitConflictChooseTheirs', opts('Choose Theirs')) + map( + 'n', + '(git-conflict-next-conflict)', + 'GitConflictNextConflict', + opts('Next Conflict') + ) + map( + 'n', + '(git-conflict-prev-conflict)', + 'GitConflictPrevConflict', + opts('Previous Conflict') + ) +end + +local function setup_buffer_mappings(bufnr) + local function opts(desc) + return { silent = true, buffer = bufnr, desc = 'Git Conflict: ' .. desc } + end + + map('n', 'co', '(git-conflict-ours)', opts('Choose Ours')) + map('n', 'cb', '(git-conflict-both)', opts('Choose Both')) + map('n', 'c0', '(git-conflict-none)', opts('Choose None')) + map('n', 'ct', '(git-conflict-theirs)', opts('Choose Theirs')) + map('n', '[x', '(git-conflict-prev-conflict)', opts('Previous Conflict')) + map('n', ']x', '(git-conflict-next-conflict)', opts('Next Conflict')) + vim.b[bufnr].conflict_mappings_set = true +end + +---@param key string +---@param mode "'n'|'v'|'o'|'nv'|'nvo'"? +---@return boolean +local function is_mapped(key, mode) return fn.hasmapto(key, mode or 'n') > 0 end + +local function clear_buffer_mappings(bufnr) + if not bufnr or not vim.b[bufnr].conflict_mappings_set then return end + if is_mapped('co') then api.nvim_buf_del_keymap(bufnr, 'n', 'co') end + if is_mapped('cb') then api.nvim_buf_del_keymap(bufnr, 'n', 'cb') end + if is_mapped('c0') then api.nvim_buf_del_keymap(bufnr, 'n', 'c0') end + if is_mapped('ct') then api.nvim_buf_del_keymap(bufnr, 'n', 'ct') end + if is_mapped('[x') then api.nvim_buf_del_keymap(bufnr, 'n', '[x') end + if is_mapped(']x') then api.nvim_buf_del_keymap(bufnr, 'n', ']x') end + vim.b[bufnr].conflict_mappings_set = false +end + +-----------------------------------------------------------------------------// +-- Highlights +-----------------------------------------------------------------------------// + +---Derive the colour of the section label highlights based on each sections highlights +---@param highlights ConflictHighlights +local function set_highlights(highlights) + local current_color = utils.get_hl(highlights.current) + local incoming_color = utils.get_hl(highlights.incoming) + local ancestor_color = utils.get_hl(highlights.ancestor) + local current_bg = current_color.background or DEFAULT_CURRENT_BG_COLOR + local incoming_bg = incoming_color.background or DEFAULT_INCOMING_BG_COLOR + local ancestor_bg = ancestor_color.background or DEFAULT_ANCESTOR_BG_COLOR + local current_label_bg = color.shade_color(current_bg, 60) + local incoming_label_bg = color.shade_color(incoming_bg, 60) + local ancestor_label_bg = color.shade_color(ancestor_bg, 60) + api.nvim_set_hl(0, CURRENT_HL, { background = current_bg, bold = true }) + api.nvim_set_hl(0, INCOMING_HL, { background = incoming_bg, bold = true }) + api.nvim_set_hl(0, ANCESTOR_HL, { background = ancestor_bg, bold = true }) + api.nvim_set_hl(0, CURRENT_LABEL_HL, { background = current_label_bg }) + api.nvim_set_hl(0, INCOMING_LABEL_HL, { background = incoming_label_bg }) + api.nvim_set_hl(0, ANCESTOR_LABEL_HL, { background = ancestor_label_bg }) +end + +function M.setup(user_config) + if fn.executable('git') <= 0 then + return vim.schedule( + function() utils.notify('You need to have git installed in order to use this plugin', 'error', true) end + ) + end + + config = vim.tbl_deep_extend('force', config, user_config or {}) + set_highlights(config.highlights) + + if config.default_commands then + set_commands() + end + + set_plug_mappings() + + api.nvim_create_augroup(AUGROUP_NAME, { clear = true }) + api.nvim_create_autocmd({ 'VimEnter', 'BufRead', 'SessionLoadPost', 'DirChanged' }, { + group = AUGROUP_NAME, + callback = function(args) + local gitdir = fn.getcwd() .. sep .. '.git' + if fn.isdirectory(gitdir) == 0 or state.current_watcher_dir == fn.getcwd() then return end + stop_running_watchers(gitdir) + fetch_conflicts(args.buf) + throttled_watcher(gitdir) + end, + }) + + api.nvim_create_autocmd('VimLeavePre', { + group = AUGROUP_NAME, + callback = function() + for key, watcher in pairs(watchers) do + watcher:stop() + watchers[key] = nil + end + end, + }) + + api.nvim_create_autocmd('User', { + group = AUGROUP_NAME, + pattern = 'GitConflictDetected', + callback = function() + local bufnr = api.nvim_get_current_buf() + if config.disable_diagnostics then vim.diagnostic.disable(bufnr) end + if config.default_mappings then setup_buffer_mappings(bufnr) end + end, + }) + + api.nvim_create_autocmd('User', { + group = AUGROUP_NAME, + pattern = 'GitConflictResolved', + callback = function() + local bufnr = api.nvim_get_current_buf() + if config.disable_diagnostics then vim.diagnostic.enable(bufnr) end + if config.default_mappings then clear_buffer_mappings(bufnr) end + end, + }) + + api.nvim_set_decoration_provider(NAMESPACE, { + on_buf = function(_, bufnr, _) return utils.is_valid_buf(bufnr) end, + on_win = function(_, _, bufnr, _, _) + if visited_buffers[bufnr] then process(bufnr) end + end, + }) +end + +--- Add additional metadata to a quickfix entry if we have already visited the buffer and have that +--- information +---@param item table +---@param items table[] +---@param visited_buf ConflictBufferCache +local function quickfix_items_from_positions(item, items, visited_buf) + if vim.tbl_isempty(visited_buf.positions) then return end + for _, pos in ipairs(visited_buf.positions) do + for key, value in pairs(pos) do + if + vim.tbl_contains({ name_map.ours, name_map.theirs, name_map.base }, key) + and not vim.tbl_isempty(value) + then + local lnum = value.range_start + 1 + local next_item = vim.deepcopy(item) + next_item.text = fmt('%s change', key, lnum) + next_item.lnum = lnum + next_item.col = 0 + table.insert(items, next_item) + end + end + end +end + +--- Convert the conflicts detected via get conflicted files into a list of quickfix entries. +---@param callback fun(files: table) +function M.conflicts_to_qf_items(callback) + local items = {} + ---@diagnostic disable-next-line: missing-parameter + get_conflicted_files(fn.expand('%:p:h'), function(files) + for filename, _ in pairs(files) do + local item = { + filename = filename, + pattern = conflict_start, + text = 'git conflict', + type = 'E', + valid = 1, + } + local visited_buf = nil + + if next(visited_buffers[filename]) ~= nil then visited_buf = visited_buffers[filename] end + + if visited_buf then + quickfix_items_from_positions(item, items, visited_buf) + else + table.insert(items, item) + end + end + callback(items) + end) +end + +---@param bufnr integer? +function M.clear(bufnr) + if bufnr and not api.nvim_buf_is_valid(bufnr) then return end + bufnr = bufnr or 0 + api.nvim_buf_clear_namespace(bufnr, NAMESPACE, 0, -1) +end + +---@param side ConflictSide +function M.find_next(side) + local pos = find_position( + 0, + function(line, position) + return position.current.range_start >= line and position.incoming.range_end >= line + end + ) + set_cursor(pos, side) +end + +---@param side ConflictSide +function M.find_prev(side) + local pos = find_position( + 0, + function(line, position) + return position.current.range_start <= line and position.incoming.range_end <= line + end, + { reverse = true } + ) + set_cursor(pos, side) +end + +---Select the changes to keep +---@param side ConflictSide +function M.choose(side) + local bufnr = api.nvim_get_current_buf() + local position = get_current_position(bufnr) + if not position then return end + local lines = {} + if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS, SIDES.BASE }, side) then + local data = position[name_map[side]] + lines = utils.get_buf_lines(data.content_start, data.content_end + 1) + elseif side == SIDES.BOTH then + local first = + utils.get_buf_lines(position.current.content_start, position.current.content_end + 1) + local second = + utils.get_buf_lines(position.incoming.content_start, position.incoming.content_end + 1) + lines = vim.list_extend(first, second) + elseif side == SIDES.NONE then + lines = {} + else + return + end + + local pos_start = position.current.range_start < 0 and 0 or position.current.range_start + local pos_end = position.incoming.range_end + 1 + + api.nvim_buf_set_lines(0, pos_start, pos_end, false, lines) + api.nvim_buf_del_extmark(0, NAMESPACE, position.marks.incoming.label) + api.nvim_buf_del_extmark(0, NAMESPACE, position.marks.current.label) + if position.marks.ancestor.label then + api.nvim_buf_del_extmark(0, NAMESPACE, position.marks.ancestor.label) + end + parse_buffer(bufnr) +end + +function M.debug_watchers() vim.pretty_print({ watchers = watchers }) end + +return M diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/colors.lua b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/colors.lua new file mode 100644 index 0000000..d37e938 --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/colors.lua @@ -0,0 +1,35 @@ +local M = {} + +local bit = require('bit') +local rshift, band = bit.rshift, bit.band + +--- Returns a table containing the RGB values encoded inside 24 least +--- significant bits of the number @rgb_24bit +--- +--@param rgb_24bit (number) 24-bit RGB value +--@returns (table) with keys 'r', 'g', 'b' in [0,255] +local function decode_24bit_rgb(rgb_24bit) + vim.validate({ rgb_24bit = { rgb_24bit, 'n', true } }) + local r = band(rshift(rgb_24bit, 16), 255) + local g = band(rshift(rgb_24bit, 8), 255) + local b = band(rgb_24bit, 255) + return { r = r, g = g, b = b } +end + +local function alter(attr, percent) return math.floor(attr * (100 + percent) / 100) end + +---@source https://stackoverflow.com/q/5560248 +---@see: https://stackoverflow.com/a/37797380 +---Darken a specified hex color +---@param color string +---@param percent number +---@return string +function M.shade_color(color, percent) + local rgb = decode_24bit_rgb(color) + if not rgb.r or not rgb.g or not rgb.b then return 'NONE' end + local r, g, b = alter(rgb.r, percent), alter(rgb.g, percent), alter(rgb.b, percent) + r, g, b = math.min(r, 255), math.min(g, 255), math.min(b, 255) + return string.format('#%02x%02x%02x', r, g, b) +end + +return M diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/utils.lua b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/utils.lua new file mode 100644 index 0000000..f5b49b1 --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/lua/git-conflict/utils.lua @@ -0,0 +1,76 @@ +-----------------------------------------------------------------------------// +-- Utils +-----------------------------------------------------------------------------// +local M = {} + +local api = vim.api +local fn = vim.fn + +--- Wrapper for [vim.notify] +---@param msg string|string[] +---@param level "error" | "trace" | "debug" | "info" | "warn" +---@param once boolean? +function M.notify(msg, level, once) + if type(msg) == 'table' then msg = table.concat(msg, '\n') end + local lvl = vim.log.levels[level:upper()] or vim.log.levels.INFO + local opts = { title = 'Git conflict' } + if once then return vim.notify_once(msg, lvl, opts) end + vim.notify(msg, lvl, opts) +end + +--- Start an async job +---@param cmd string +---@param callback fun(data: string[]): nil +function M.job(cmd, callback) + fn.jobstart(cmd, { + stdout_buffered = true, + on_stdout = function(_, data, _) callback(data) end, + }) +end + +---Only call the passed function once every timeout in ms +---@param timeout integer +---@param func function +---@return function +function M.throttle(timeout, func) + local timer = vim.loop.new_timer() + local running = false + return function(...) + if not running then + func(...) + running = true + timer:start(timeout, 0, function() running = false end) + end + end +end + +---Wrapper around `api.nvim_buf_get_lines` which defaults to the current buffer +---@param start integer +---@param _end integer +---@param buf integer? +---@return string[] +function M.get_buf_lines(start, _end, buf) + return api.nvim_buf_get_lines(buf or 0, start, _end, false) +end + +---Get cursor row and column as (1, 0) based +---@param win_id integer? +---@return integer, integer +function M.get_cursor_pos(win_id) return unpack(api.nvim_win_get_cursor(win_id or 0)) end + +---Check if the buffer is likely to have actionable conflict markers +---@param bufnr integer? +---@return boolean +function M.is_valid_buf(bufnr) + bufnr = bufnr or 0 + return #vim.bo[bufnr].buftype == 0 and vim.bo[bufnr].modifiable +end + +---@param name string? +---@return table +function M.get_hl(name) + if not name then return {} end + return api.nvim_get_hl_by_name(name, true) +end + +return M diff --git a/etc/soft/nvim/+plugins/git-conflict.nvim/stylua.toml b/etc/soft/nvim/+plugins/git-conflict.nvim/stylua.toml new file mode 100644 index 0000000..79e1f10 --- /dev/null +++ b/etc/soft/nvim/+plugins/git-conflict.nvim/stylua.toml @@ -0,0 +1,6 @@ +column_width = 100 +indent_type = 'Spaces' +quote_style = 'AutoPreferSingle' +indent_width = 2 +no_call_parentheses = false +collapse_simple_statement = "Always" diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/CONTRIBUTING.md b/etc/soft/nvim/+plugins/gitsigns.nvim/CONTRIBUTING.md new file mode 100644 index 0000000..6eb7cae --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/CONTRIBUTING.md @@ -0,0 +1,85 @@ +## Requirements + +- [Luarocks](https://luarocks.org/) + - `brew install luarocks` + +## Writing Teal + + **Do not edit files in the lua dir**. + +Gitsigns is implemented in teal which is essentially lua+types. +The teal source files are generated into lua files and must be checked in together when making changes. +CI will enforce this. + +Once you have made changes in teal, the corresponding lua files can be built with: + +``` +make tl-build +``` + +## Generating docs + +Most of the documentation is handwritten however the documentation for the configuration is generated from `teal/gitsigns/config.tl` which contains the configuration schema. +The documentation is generated with the lua script `gen_help.lua` which has been developed just enough to handle the current configuration schema so from time to time this script might need small improvements to handle new features but for the most part it works. + +The documentation can be updated with: + +``` +make gen_help +``` + +Note: The default Make target is to run both `tl-build` and `gen_help` so it's often easier to just run `make` to update generated files (or even `:make` from within Neovim). + +## Testsuite + +The testsuite uses the same framework as Neovims funcitonaltest suite. +This is just busted with lots of helper code to create headless neovim instances which are controlled via RPC. + +The first time you run the testsuite, Neovim will be compiled from source (this is the Neovim build that tests will use). +This is arguably a little bit overkill for such a plugin but it allows: + +- Easily running tests with Neovim master +- Tests which check the screen state (essential for a UI plugin) + +To run the testsuite: + +``` +make test +``` + +## [Diagnostic-ls](https://github.com/iamcco/diagnostic-languageserver) config for teal + +``` +require('lspconfig').diagnosticls.setup{ + filetypes = {'teal'}, + init_options = { + filetypes = {teal = {'tealcheck'}}, + linters = { + tealcheck = { + sourceName = "tealcheck", + command = "tl", + args = {'check', '%file'}, + isStdout = false, isStderr = true, + rootPatterns = {"tlconfig.lua", ".git"}, + formatPattern = { + '^([^:]+):(\\d+):(\\d+): (.+)$', { + sourceName = 1, sourceNameFilter = true, + line = 2, column = 3, message = 4 + } + } + } + } + } +} +``` + +## [null-ls.nvim](https://github.com/jose-elias-alvarez/null-ls.nvim) config for teal + +``` +local null_ls = require("null-ls") + +null_ls.config {sources = { + null_ls.builtins.diagnostics.teal +}} +require("lspconfig")["null-ls"].setup {} +``` diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/LICENSE b/etc/soft/nvim/+plugins/gitsigns.nvim/LICENSE new file mode 100644 index 0000000..6598444 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Lewis Russell + +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. diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/Makefile b/etc/soft/nvim/+plugins/gitsigns.nvim/Makefile new file mode 100644 index 0000000..677371e --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/Makefile @@ -0,0 +1,85 @@ + +export PJ_ROOT=$(PWD) + +FILTER ?= .* + +LUA_VERSION := 5.1 +TL_VERSION := 0.14.1 +NEOVIM_BRANCH ?= master + +DEPS_DIR := $(PWD)/deps/nvim-$(NEOVIM_BRANCH) + +LUAROCKS := luarocks --lua-version=$(LUA_VERSION) +LUAROCKS_TREE := $(DEPS_DIR)/luarocks/usr +LUAROCKS_LPATH := $(LUAROCKS_TREE)/share/lua/$(LUA_VERSION) +LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) && + +.DEFAULT_GOAL := build + +$(DEPS_DIR)/neovim: + @mkdir -p $(DEPS_DIR) + git clone --depth 1 https://github.com/neovim/neovim --branch $(NEOVIM_BRANCH) $@ + @# disable LTO to reduce compile time + make -C $@ \ + DEPS_BUILD_DIR=$(dir $(LUAROCKS_TREE)) \ + CMAKE_BUILD_TYPE=RelWithDebInfo \ + CMAKE_EXTRA_FLAGS=-DENABLE_LTO=OFF + +TL := $(LUAROCKS_TREE)/bin/tl + +$(TL): + @mkdir -p $@ + $(LUAROCKS) --tree $(LUAROCKS_TREE) install tl $(TL_VERSION) + +INSPECT := $(LUAROCKS_LPATH)/inspect.lua + +$(INSPECT): + @mkdir -p $@ + $(LUAROCKS) --tree $(LUAROCKS_TREE) install inspect + +.PHONY: lua_deps +lua_deps: $(TL) $(INSPECT) + +.PHONY: test_deps +test_deps: $(DEPS_DIR)/neovim + +export VIMRUNTIME=$(DEPS_DIR)/neovim/runtime +export TEST_COLORS=1 + +.PHONY: test +test: $(DEPS_DIR)/neovim + $(LUAROCKS_INIT) busted \ + -v \ + --lazy \ + --helper=$(PWD)/test/preload.lua \ + --output test.busted.outputHandlers.nvim \ + --lpath=$(DEPS_DIR)/neovim/?.lua \ + --lpath=$(DEPS_DIR)/neovim/build/?.lua \ + --lpath=$(DEPS_DIR)/neovim/runtime/lua/?.lua \ + --lpath=$(DEPS_DIR)/?.lua \ + --lpath=$(PWD)/lua/?.lua \ + --filter="$(FILTER)" \ + $(PWD)/test + + -@stty sane + +.PHONY: tl-check +tl-check: $(TL) + $(TL) check teal/*.tl teal/**/*.tl + +.PHONY: tl-build +tl-build: tlconfig.lua $(TL) + @$(TL) build + @echo Updated lua files + +.PHONY: gen_help +gen_help: $(INSPECT) + @$(LUAROCKS_INIT) ./gen_help.lua + @echo Updated help + +.PHONY: build +build: tl-build gen_help + +.PHONY: tl-ensure +tl-ensure: tl-build + git diff --exit-code -- lua diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/README.md b/etc/soft/nvim/+plugins/gitsigns.nvim/README.md new file mode 100644 index 0000000..8463335 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/README.md @@ -0,0 +1,334 @@ +# gitsigns.nvim + +[![CI](https://github.com/lewis6991/gitsigns.nvim/workflows/CI/badge.svg?branch=main)](https://github.com/lewis6991/gitsigns.nvim/actions?query=workflow%3ACI) +[![Version](https://img.shields.io/github/v/release/lewis6991/gitsigns.nvim)](https://github.com/lewis6991/gitsigns.nvim/releases) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Gitter](https://badges.gitter.im/gitsigns-nvim/community.svg)](https://gitter.im/gitsigns-nvim/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +Super fast git decorations implemented purely in lua/teal. + +## Preview + +| Hunk Actions | Line Blame | +| --- | ----------- | +| | | + +## Features + +- Signs for added, removed, and changed lines +- Asynchronous using [luv] +- Navigation between hunks +- Stage hunks (with undo) +- Preview diffs of hunks (with word diff) +- Customizable (signs, highlights, mappings, etc) +- Status bar integration +- Git blame a specific line using virtual text. +- Hunk text object +- Automatically follow files moved in the index. +- Live intra-line word diff +- Ability to display deleted/changed lines via virtual lines. +- Support for [yadm] +- Support for detached working trees. + +## Requirements + +- Neovim >= 0.7.0 + + **Note:** If your version of Neovim is too old, then you can use a past [release]. + + **Note:** If you are running a development version of Neovim (aka `master`), then breakage may occur if your build is behind latest. + +- Newish version of git. Older versions may not work with some features. + +## Installation + +[packer.nvim]: +```lua +use { + 'lewis6991/gitsigns.nvim', + -- tag = 'release' -- To use the latest release (do not use this if you run Neovim nightly or dev builds!) +} +``` + +[vim-plug]: +```vim +Plug 'lewis6991/gitsigns.nvim' +``` + +## Usage + +For basic setup with all batteries included: +```lua +require('gitsigns').setup() +``` + +If using [packer.nvim] gitsigns can +be setup directly in the plugin spec: + +```lua +use { + 'lewis6991/gitsigns.nvim', + config = function() + require('gitsigns').setup() + end +} +``` + +Configuration can be passed to the setup function. Here is an example with most of +the default settings: + +```lua +require('gitsigns').setup { + signs = { + add = { hl = 'GitSignsAdd' , text = '│', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + change = { hl = 'GitSignsChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + delete = { hl = 'GitSignsDelete', text = '_', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + topdelete = { hl = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = { hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + untracked = { hl = 'GitSignsAdd' , text = '┆', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + }, + signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` + numhl = false, -- Toggle with `:Gitsigns toggle_numhl` + linehl = false, -- Toggle with `:Gitsigns toggle_linehl` + word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` + watch_gitdir = { + interval = 1000, + follow_files = true + }, + attach_to_untracked = true, + current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` + current_line_blame_opts = { + virt_text = true, + virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' + delay = 1000, + ignore_whitespace = false, + }, + current_line_blame_formatter = ', - ', + sign_priority = 6, + update_debounce = 100, + status_formatter = nil, -- Use default + max_file_length = 40000, -- Disable if file is longer than this (in lines) + preview_config = { + -- Options passed to nvim_open_win + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1 + }, + yadm = { + enable = false + }, +} +``` + +For information on configuring Neovim via lua please see [nvim-lua-guide]. + +### Keymaps + +Gitsigns provides an `on_attach` callback which can be used to setup buffer mappings. + +Here is a suggested example: + +```lua +require('gitsigns').setup{ + ... + on_attach = function(bufnr) + local gs = package.loaded.gitsigns + + local function map(mode, l, r, opts) + opts = opts or {} + opts.buffer = bufnr + vim.keymap.set(mode, l, r, opts) + end + + -- Navigation + map('n', ']c', function() + if vim.wo.diff then return ']c' end + vim.schedule(function() gs.next_hunk() end) + return '' + end, {expr=true}) + + map('n', '[c', function() + if vim.wo.diff then return '[c' end + vim.schedule(function() gs.prev_hunk() end) + return '' + end, {expr=true}) + + -- Actions + map({'n', 'v'}, 'hs', ':Gitsigns stage_hunk') + map({'n', 'v'}, 'hr', ':Gitsigns reset_hunk') + map('n', 'hS', gs.stage_buffer) + map('n', 'hu', gs.undo_stage_hunk) + map('n', 'hR', gs.reset_buffer) + map('n', 'hp', gs.preview_hunk) + map('n', 'hb', function() gs.blame_line{full=true} end) + map('n', 'tb', gs.toggle_current_line_blame) + map('n', 'hd', gs.diffthis) + map('n', 'hD', function() gs.diffthis('~') end) + map('n', 'td', gs.toggle_deleted) + + -- Text object + map({'o', 'x'}, 'ih', ':Gitsigns select_hunk') + end +} +``` + +Note this requires Neovim v0.7 which introduces `vim.keymap.set`. If you are using Neovim with version prior to v0.7 then use the following: +
+ Click to expand + +```lua +require('gitsigns').setup { + ... + on_attach = function(bufnr) + local function map(mode, lhs, rhs, opts) + opts = vim.tbl_extend('force', {noremap = true, silent = true}, opts or {}) + vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts) + end + + -- Navigation + map('n', ']c', "&diff ? ']c' : 'Gitsigns next_hunk'", {expr=true}) + map('n', '[c', "&diff ? '[c' : 'Gitsigns prev_hunk'", {expr=true}) + + -- Actions + map('n', 'hs', ':Gitsigns stage_hunk') + map('v', 'hs', ':Gitsigns stage_hunk') + map('n', 'hr', ':Gitsigns reset_hunk') + map('v', 'hr', ':Gitsigns reset_hunk') + map('n', 'hS', 'Gitsigns stage_buffer') + map('n', 'hu', 'Gitsigns undo_stage_hunk') + map('n', 'hR', 'Gitsigns reset_buffer') + map('n', 'hp', 'Gitsigns preview_hunk') + map('n', 'hb', 'lua require"gitsigns".blame_line{full=true}') + map('n', 'tb', 'Gitsigns toggle_current_line_blame') + map('n', 'hd', 'Gitsigns diffthis') + map('n', 'hD', 'lua require"gitsigns".diffthis("~")') + map('n', 'td', 'Gitsigns toggle_deleted') + + -- Text object + map('o', 'ih', ':Gitsigns select_hunk') + map('x', 'ih', ':Gitsigns select_hunk') + end +} +``` + +
+ +## Non-Goals + +### Implement every feature in [vim-fugitive] + +This plugin is actively developed and by one of the most well regarded vim plugin developers. +Gitsigns will only implement features of this plugin if: it is simple, or, the technologies leveraged by Gitsigns (LuaJIT, Libuv, Neovim's API, etc) can provide a better experience. + +### Support for other VCS + +There aren't any active developers of this plugin who use other kinds of VCS, so adding support for them isn't feasible. +However a well written PR with a commitment of future support could change this. + +## Status Line + +Use `b:gitsigns_status` or `b:gitsigns_status_dict`. `b:gitsigns_status` is +formatted using `config.status_formatter`. `b:gitsigns_status_dict` is a +dictionary with the keys `added`, `removed`, `changed` and `head`. + +Example: +```viml +set statusline+=%{get(b:,'gitsigns_status','')} +``` + +For the current branch use the variable `b:gitsigns_head`. + +## Comparison with [vim-gitgutter] + +Feature | gitsigns.nvim | vim-gitgutter | Note +---------------------------------------------------------|----------------------|-----------------------------------------------|-------- +Shows signs for added, modified, and removed lines | :white_check_mark: | :white_check_mark: | +Asynchronous | :white_check_mark: | :white_check_mark: | +Runs diffs in-process (no IO or pipes) | :white_check_mark: * | | * Via [lua](https://github.com/neovim/neovim/pull/14536) or FFI. +Supports Nvim's diff-linematch | :white_check_mark: * | | * Via [diff-linematch] +Only adds signs for drawn lines | :white_check_mark: * | | * Via Neovims decoration API +Updates immediately | :white_check_mark: | * | * Triggered on CursorHold +Ensures signs are always up to date | :white_check_mark: * | | * Watches the git dir to do so +Never saves the buffer | :white_check_mark: | :white_check_mark: :heavy_exclamation_mark: * | * Writes [buffer](https://github.com/airblade/vim-gitgutter/blob/0f98634b92da9a35580b618c11a6d2adc42d9f90/autoload/gitgutter/diff.vim#L106) (and index) to short lived temp files +Quick jumping between hunks | :white_check_mark: | :white_check_mark: | +Stage/reset/preview individual hunks | :white_check_mark: | :white_check_mark: | +Preview hunks directly in the buffer (inline) | :white_check_mark: * | | * Via `preview_hunk_inline` +Stage/reset hunks in range/selection | :white_check_mark: | :white_check_mark: :heavy_exclamation_mark: * | * Only stage +Stage/reset all hunks in buffer | :white_check_mark: | | +Undo staged hunks | :white_check_mark: | | +Word diff in buffer | :white_check_mark: | | +Word diff in hunk preview | :white_check_mark: | :white_check_mark: | +Show deleted/changes lines directly in buffer | :white_check_mark: * | | * Via [virtual lines] +Stage partial hunks | :white_check_mark: | | +Hunk text object | :white_check_mark: | :white_check_mark: | +Diff against index or any commit | :white_check_mark: | :white_check_mark: | +Folding of unchanged text | | :white_check_mark: | +Fold text showing whether folded lines have been changed | | :white_check_mark: | +Load hunk locations into the quickfix or location list | :white_check_mark: | :white_check_mark: | +Optional line highlighting | :white_check_mark: | :white_check_mark: | +Optional line number highlighting | :white_check_mark: | :white_check_mark: | +Optional counts on signs | :white_check_mark: | | +Customizable signs and mappings | :white_check_mark: | :white_check_mark: | +Customizable extra diff arguments | :white_check_mark: | :white_check_mark: | +Can be toggled globally or per buffer | :white_check_mark: * | :white_check_mark: | * Through the detach/attach functions +Statusline integration | :white_check_mark: | :white_check_mark: | +Works with [yadm] | :white_check_mark: | | +Live blame in buffer (using virtual text) | :white_check_mark: | | +Blame preview | :white_check_mark: | | +Automatically follows open files moved with `git mv` | :white_check_mark: | | +CLI with completion | :white_check_mark: | * | * Provides individual commands for some actions +Open diffview with any revision/commit | :white_check_mark: | | + +As of 2022-09-01 + +## Integrations + +### [vim-fugitive] + +When viewing revisions of a file (via `:0Gclog` for example), Gitsigns will attach to the fugitive buffer with the base set to the commit immediately before the commit of that revision. +This means the signs placed in the buffer reflect the changes introduced by that revision of the file. + +### [null-ls] + +Null-ls can provide code actions from Gitsigns. To setup: + +```lua +local null_ls = require("null-ls") + +null_ls.setup { + sources = { + null_ls.builtins.code_actions.gitsigns, + ... + } +} +``` + +Will enable `:lua vim.lsp.buf.code_action()` to retrieve code actions from Gitsigns. + +### [trouble.nvim] + +If installed and enabled (via `config.trouble`; defaults to true if installed), `:Gitsigns setqflist` or `:Gitsigns seqloclist` will open Trouble instead of Neovim's built-in quickfix or location list windows. + +## Similar plugins + +- [coc-git] +- [vim-gitgutter] +- [vim-signify] + + +[coc-git]: https://github.com/neoclide/coc-git +[diff-linematch]: https://github.com/neovim/neovim/pull/14537 +[luv]: https://github.com/luvit/luv/blob/master/docs.md +[null-ls]: https://github.com/jose-elias-alvarez/null-ls.nvim +[nvim-lua-guide]: https://github.com/nanotee/nvim-lua-guide +[packer.nvim]: https://github.com/wbthomason/packer.nvim +[release]: https://github.com/lewis6991/gitsigns.nvim/releases +[trouble.nvim]: https://github.com/folke/trouble.nvim +[vim-fugitive]: https://github.com/tpope/vim-fugitive +[vim-gitgutter]: https://github.com/airblade/vim-gitgutter +[vim-plug]: https://github.com/junegunn/vim-plug +[vim-signify]: https://github.com/mhinz/vim-signify +[virtual lines]: https://github.com/neovim/neovim/pull/15351 +[yadm]: https://yadm.io diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/doc/gitsigns.txt b/etc/soft/nvim/+plugins/gitsigns.nvim/doc/gitsigns.txt new file mode 100644 index 0000000..930e0ee --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/doc/gitsigns.txt @@ -0,0 +1,1023 @@ +*gitsigns.txt* Gitsigns +*gitsigns.nvim* + +Author: Lewis Russell +Version: 0.6-dev +Homepage: +License: MIT license + +============================================================================== +INTRODUCTION *gitsigns* + +Gitsigns is a plugin for Neovim that provides integration with Git via a +feature set which includes (but not limited to): + • Provides signs in the |signcolumn| to show changed/added/removed lines. + • Mappings to operate on hunks to stage, undo or reset against Git's index. + +Gitsigns is implemented entirely in Lua which is built into Neovim and +requires no external dependencies other than git. This is unlike other plugins +that require python, node, etc, which need to communicate with Neovim using +|RPC|. By default, Gitsigns also uses Neovim's built-in diff library +(`vim.diff`) unlike other similar plugins that need to run `git-diff` as an +external process which is less efficient, has tighter bottlenecks and requires +file IO. + +============================================================================== +USAGE *gitsigns-usage* + +For basic setup with all batteries included: +> + require('gitsigns').setup() +< + +Configuration can be passed to the setup function. Here is an example with most +of the default settings: +> + require('gitsigns').setup { + signs = { + add = { hl = 'GitSignsAdd' , text = '│', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + change = { hl = 'GitSignsChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + delete = { hl = 'GitSignsDelete', text = '_', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + topdelete = { hl = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = { hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + untracked = { hl = 'GitSignsAdd' , text = '┆', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + }, + signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` + numhl = false, -- Toggle with `:Gitsigns toggle_numhl` + linehl = false, -- Toggle with `:Gitsigns toggle_linehl` + word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` + watch_gitdir = { + interval = 1000, + follow_files = true + }, + attach_to_untracked = true, + current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame` + current_line_blame_opts = { + virt_text = true, + virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' + delay = 1000, + ignore_whitespace = false, + }, + current_line_blame_formatter = ', - ', + sign_priority = 6, + update_debounce = 100, + status_formatter = nil, -- Use default + max_file_length = 40000, -- Disable if file is longer than this (in lines) + preview_config = { + -- Options passed to nvim_open_win + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1 + }, + yadm = { + enable = false + }, + } +< + +============================================================================== +MAPPINGS *gitsigns-mappings* + +Custom mappings can be defined in the `on_attach` callback in the config table +passed to |gitsigns-setup()|. See |gitsigns-config-on_attach|. + +Most actions can be repeated with `.` if you have |vim-repeat| installed. + +============================================================================== +FUNCTIONS *gitsigns-functions* + +Note functions with the {async} attribute are run asynchronously and are +non-blocking (return immediately). + + +setup({cfg}) *gitsigns.setup()* + Setup and start Gitsigns. + + Attributes: ~ + {async} + + Parameters: ~ + {cfg} Table object containing configuration for + Gitsigns. See |gitsigns-usage| for more details. + +attach({bufnr}) *gitsigns.attach()* + Attach Gitsigns to the buffer. + + Attributes: ~ + {async} + + Parameters: ~ + {bufnr} (number): Buffer number + +detach({bufnr}) *gitsigns.detach()* + Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not + provided then the current buffer is used. + + Parameters: ~ + {bufnr} (number): Buffer number + +detach_all() *gitsigns.detach_all()* + Detach Gitsigns from all buffers it is attached to. + +refresh() *gitsigns.refresh()* + Refresh all buffers. + + Attributes: ~ + {async} + +get_actions() *gitsigns.get_actions()* + Get all the available line specific actions for the current + buffer at the cursor position. + + Return: ~ + Dictionary of action name to function which when called + performs action. + +setloclist({nr}, {target}) *gitsigns.setloclist()* + Populate the location list with hunks. Automatically opens the + location list window. + + Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` + + Attributes: ~ + {async} + + Parameters: ~ + {nr} (integer): Window number or the |window-ID|. + `0` for the current window (default). + {target} (integer or string): See |gitsigns.setqflist()|. + +setqflist({target}, {opts}) *gitsigns.setqflist()* + Populate the quickfix list with hunks. Automatically opens the + quickfix window. + + Attributes: ~ + {async} + + Parameters: ~ + {target} (integer or string): + Specifies which files hunks are collected from. + Possible values. + • [integer]: The buffer with the matching buffer + number. `0` for current buffer (default). + • `"attached"`: All attached buffers. + • `"all"`: All modified files for each git + directory of all attached buffers in addition + to the current working directory. + {opts} (table|nil): + Additional options: + • {use_location_list}: (boolean) + Populate the location list instead of the + quickfix list. Default to `false`. + • {nr}: (integer) + Window number or ID when using location list. + Expand folds when navigating to a hunk which is + inside a fold. Defaults to `0`. + • {open}: (boolean) + Open the quickfix/location list viewer. + Defaults to `true`. + +show({revision}) *gitsigns.show()* + Show revision {base} of the current file, if it is given, or + with the currently set base (index by default). + + If {base} is the index, then the opened buffer is editable and + any written changes will update the index accordingly. + + Examples: > + " View the index version of the file + :Gitsigns show + + " View revision of file in the last commit + :Gitsigns show ~1 +< + + For a more complete list of ways to specify bases, see + |gitsigns-revision|. + + Attributes: ~ + {async} + +diffthis({base}, {opts}) *gitsigns.diffthis()* + Perform a |vimdiff| on the given file with {base} if it is + given, or with the currently set base (index by default). + + If {base} is the index, then the opened buffer is editable and + any written changes will update the index accordingly. + + Parameters: ~ + {base} (string|nil): Revision to diff against. Defaults + to index. + {opts} (table|nil): + Additional options: + • {vertical}: {boolean}. Split window vertically. Defaults to + config.diff_opts.vertical. If running via command line, then + this is taken from the command modifiers. + • {split}: {string}. One of: 'aboveleft', 'belowright', + 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to + 'aboveleft'. If running via command line, then this is taken + from the command modifiers. + + Examples: > + " Diff against the index + :Gitsigns diffthis + + " Diff against the last commit + :Gitsigns diffthis ~1 +< + + For a more complete list of ways to specify bases, see + |gitsigns-revision|. + + Attributes: ~ + {async} + +reset_base({global}) *gitsigns.reset_base()* + Reset the base revision to diff against back to the + index. + + Alias for `change_base(nil, {global})` . + +change_base({base}, {global}) *gitsigns.change_base()* + Change the base revision to diff against. If {base} is not + given, then the original base is used. If {global} is given + and true, then change the base revision of all buffers, + including any new buffers. + + Attributes: ~ + {async} + + Parameters:~ + {base} string|nil The object/revision to diff against. + {global} boolean|nil Change the base of all buffers. + + Examples: > + " Change base to 1 commit behind head + :lua require('gitsigns').change_base('HEAD~1') + + " Also works using the Gitsigns command + :Gitsigns change_base HEAD~1 + + " Other variations + :Gitsigns change_base ~1 + :Gitsigns change_base ~ + :Gitsigns change_base ^ + + " Commits work too + :Gitsigns change_base 92eb3dd + + " Revert to original base + :Gitsigns change_base +< + + For a more complete list of ways to specify bases, see + |gitsigns-revision|. + +blame_line({opts}) *gitsigns.blame_line()* + Run git blame on the current line and show the results in a + floating window. If already open, calling this will cause the + window to get focus. + + Parameters: ~ + {opts} (table|nil): + Additional options: + • {full}: (boolean) + Display full commit message with hunk. + • {ignore_whitespace}: (boolean) + Ignore whitespace when running blame. + + Attributes: ~ + {async} + +get_hunks({bufnr}) *gitsigns.get_hunks()* + Get hunk array for specified buffer. + + Parameters: ~ + {bufnr} integer: Buffer number, if not provided (or 0) + will use current buffer. + + Return: ~ + Array of hunk objects. Each hunk object has keys: + • `"type"`: String with possible values: "add", "change", + "delete" + • `"head"`: Header that appears in the unified diff + output. + • `"lines"`: Line contents of the hunks prefixed with + either `"-"` or `"+"`. + • `"removed"`: Sub-table with fields: + • `"start"`: Line number (1-based) + • `"count"`: Line count + • `"added"`: Sub-table with fields: + • `"start"`: Line number (1-based) + • `"count"`: Line count + +select_hunk() *gitsigns.select_hunk()* + Select the hunk under the cursor. + +preview_hunk_inline() *gitsigns.preview_hunk_inline()* + Preview the hunk at the cursor position inline in the buffer. + +preview_hunk() *gitsigns.preview_hunk()* + Preview the hunk at the cursor position in a floating + window. If the preview is already open, calling this + will cause the window to get focus. + +prev_hunk({opts}) *gitsigns.prev_hunk()* + Jump to the previous hunk in the current buffer. If a hunk preview + (popup or inline) was previously opened, it will be re-opened + at the previous hunk. + + Parameters: ~ + See |gitsigns.next_hunk()|. + +next_hunk({opts}) *gitsigns.next_hunk()* + Jump to the next hunk in the current buffer. If a hunk preview + (popup or inline) was previously opened, it will be re-opened + at the next hunk. + + Parameters: ~ + {opts} table|nil Configuration table. Keys: + • {wrap}: (boolean) + Whether to loop around file or not. Defaults + to the value 'wrapscan' + • {navigation_message}: (boolean) + Whether to show navigation messages or not. + Looks at 'shortmess' for default behaviour. + • {foldopen}: (boolean) + Expand folds when navigating to a hunk which is + inside a fold. Defaults to `true` if 'foldopen' + contains `search`. + • {preview}: (boolean) + Automatically open preview_hunk() upon navigating + to a hunk. + • {greedy}: (boolean) + Only navigate between non-contiguous hunks. Only useful if + 'diff_opts' contains `linematch`. Defaults to `true`. + +reset_buffer_index() *gitsigns.reset_buffer_index()* + Unstage all hunks for current buffer in the index. Note: + Unlike |gitsigns.undo_stage_hunk()| this doesn't simply undo + stages, this runs an `git reset` on current buffers file. + + Attributes: ~ + {async} + +stage_buffer() *gitsigns.stage_buffer()* + Stage all hunks in current buffer. + + Attributes: ~ + {async} + +undo_stage_hunk() *gitsigns.undo_stage_hunk()* + Undo the last call of stage_hunk(). + + Note: only the calls to stage_hunk() performed in the current + session can be undone. + + Attributes: ~ + {async} + +reset_buffer() *gitsigns.reset_buffer()* + Reset the lines of all hunks in the buffer. + +reset_hunk({range}, {opts}) *gitsigns.reset_hunk()* + Reset the lines of the hunk at the cursor position, or all + lines in the given range. If {range} is provided, all lines in + the given range are reset. This supports partial-hunks, + meaning if a range only includes a portion of a particular + hunk, only the lines within the range will be reset. + + Parameters:~ + {range} table|nil List-like table of two integers making + up the line range from which you want to reset the hunks. + If running via command line, then this is taken from the + command modifiers. + {opts} table|nil Additional options: + • {greedy}: (boolean) + Stage all contiguous hunks. Only useful if 'diff_opts' + contains `linematch`. Defaults to `true`. + + +stage_hunk({range}, {opts}) *gitsigns.stage_hunk()* + Stage the hunk at the cursor position, or all lines in the + given range. If {range} is provided, all lines in the given + range are staged. This supports partial-hunks, meaning if a + range only includes a portion of a particular hunk, only the + lines within the range will be staged. + + Attributes: ~ + {async} + + Parameters:~ + {range} table|nil List-like table of two integers making + up the line range from which you want to stage the hunks. + If running via command line, then this is taken from the + command modifiers. + {opts} table|nil Additional options: + • {greedy}: (boolean) + Stage all contiguous hunks. Only useful if 'diff_opts' + contains `linematch`. Defaults to `true`. + + +toggle_deleted({value}) *gitsigns.toggle_deleted()* + Toggle |gitsigns-config-show_deleted| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-show_deleted| + +toggle_current_line_blame({value}) *gitsigns.toggle_current_line_blame()* + Toggle |gitsigns-config-current_line_blame| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-current_line_blame| + +toggle_word_diff({value}) *gitsigns.toggle_word_diff()* + Toggle |gitsigns-config-word_diff| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-word_diff| + +toggle_linehl({value}) *gitsigns.toggle_linehl()* + Toggle |gitsigns-config-linehl| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-linehl| + +toggle_numhl({value}) *gitsigns.toggle_numhl()* + Toggle |gitsigns-config-numhl| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-numhl| + +toggle_signs({value}) *gitsigns.toggle_signs()* + Toggle |gitsigns-config-signbooleancolumn| + + Parameters:~ + {value} boolean|nil Value to set toggle. If `nil` + the toggle value is inverted. + + Returns:~ + Current value of |gitsigns-config-signcolumn| + + +============================================================================== +CONFIGURATION *gitsigns-config* + +This section describes the configuration fields which can be passed to +|gitsigns.setup()|. Note fields of type `table` may be marked with extended +meaning the field is merged with the default, with the user value given higher +precedence. This allows only specific sub-fields to be configured without +having to redefine the whole field. + +signs *gitsigns-config-signs* + Type: `table[extended]` + Default: > + { + add = {hl = 'GitSignsAdd' , text = '┃', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + change = {hl = 'GitSignsChange', text = '┃', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + delete = {hl = 'GitSignsDelete', text = '▁', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + topdelete = {hl = 'GitSignsDelete', text = '▔', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + untracked = {hl = 'GitSignsAdd' , text = '┆', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + } +< + Configuration for signs: + • `hl` specifies the highlight group to use for the sign. + • `text` specifies the character to use for the sign. + • `numhl` specifies the highlight group to use for the number column + (see |gitsigns-config.numhl|). + • `linehl` specifies the highlight group to use for the line + (see |gitsigns-config.linehl|). + • `show_count` to enable showing count of hunk, e.g. number of deleted + lines. + + Note if a highlight is not defined, it will be automatically derived by + searching for other defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*Gutter` + • `diff*` + • `Diff*` + + For example if `GitSignsAdd` is not defined but `GitGutterAdd` is defined, + then `GitSignsAdd` will be linked to `GitGutterAdd`. + +keymaps *gitsigns-config-keymaps* + DEPRECATED + config.keymaps is now deprecated. Please define mappings in config.on_attach() instead. + + Type: `table`, Default: `{}` + + Keymaps to set up when attaching to a buffer. + + Each key in the table defines the mode and key (whitespace delimited) + for the mapping and the value defines what the key maps to. The value + can be a table which can contain keys matching the options defined in + |map-arguments| which are: `expr`, `noremap`, `nowait`, `script`, `silent` + and `unique`. These options can also be used in the top level of the + table to define default options for all mappings. + + Since this field is not extended (unlike |gitsigns-config-signs|), + mappings defined in this field can be disabled by setting the whole field + to `{}`, and |gitsigns-config-on_attach| can instead be used to define + mappings. + +worktrees *gitsigns-config-worktrees* + Type: `table`, Default: `nil` + + Detached working trees. + + Array of tables with the keys `gitdir` and `toplevel`. + + If normal attaching fails, then each entry in the table is attempted + with the work tree details set. + + Example: > + worktrees = { + { + toplevel = vim.env.HOME, + gitdir = vim.env.HOME .. '/projects/dotfiles/.git' + } + } + +on_attach *gitsigns-config-on_attach* + Type: `function`, Default: `nil` + + Callback called when attaching to a buffer. Mainly used to setup keymaps + when `config.keymaps` is empty. The buffer number is passed as the first + argument. + + This callback can return `false` to prevent attaching to the buffer. + + Example: > + on_attach = function(bufnr) + if vim.api.nvim_buf_get_name(bufnr):match() then + -- Don't attach to specific buffers whose name matches a pattern + return false + end + + -- Setup keymaps + vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) + ... -- More keymaps + end +< + +watch_gitdir *gitsigns-config-watch_gitdir* + Type: `table[extended]` + Default: > + { + enable = true, + interval = 1000, + follow_files = true + } +< + When opening a file, a libuv watcher is placed on the respective + `.git` directory to detect when changes happen to use as a trigger to + update signs. + + Fields: ~ + • `enable`: + Whether the watcher is enabled. + + • `interval`: + Interval the watcher waits between polls of the gitdir in milliseconds. + + • `follow_files`: + If a file is moved with `git mv`, switch the buffer to the new location. + +sign_priority *gitsigns-config-sign_priority* + Type: `number`, Default: `6` + + Priority to use for signs. + +signcolumn *gitsigns-config-signcolumn* + Type: `boolean`, Default: `true` + + Enable/disable symbols in the sign column. + + When enabled the highlights defined in `signs.*.hl` and symbols defined + in `signs.*.text` are used. + +numhl *gitsigns-config-numhl* + Type: `boolean`, Default: `false` + + Enable/disable line number highlights. + + When enabled the highlights defined in `signs.*.numhl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + +linehl *gitsigns-config-linehl* + Type: `boolean`, Default: `false` + + Enable/disable line highlights. + + When enabled the highlights defined in `signs.*.linehl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + +show_deleted *gitsigns-config-show_deleted* + Type: `boolean`, Default: `false` + + Show the old version of hunks inline in the buffer (via virtual lines). + + Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. + +diff_opts *gitsigns-config-diff_opts* + Type: `table[extended]`, Default: derived from 'diffopt' + + Diff options. + + Fields: ~ + • algorithm: string + Diff algorithm to use. Values: + • "myers" the default algorithm + • "minimal" spend extra time to generate the + smallest possible diff + • "patience" patience diff algorithm + • "histogram" histogram diff algorithm + • internal: boolean + Use Neovim's built in xdiff library for running diffs. + + Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses + `vim.diff`. + • indent_heuristic: boolean + Use the indent heuristic for the internal + diff library. + • vertical: boolean + Start diff mode with vertical splits. + • linematch: integer + Enable second-stage diff on hunks to align lines. + Requires `internal=true`. + +base *gitsigns-config-base* + Type: `string`, Default: index + + The object/revision to diff against. + See |gitsigns-revision|. + +count_chars *gitsigns-config-count_chars* + Type: `table` + Default: > + { + [1] = '1', -- '₁', + [2] = '2', -- '₂', + [3] = '3', -- '₃', + [4] = '4', -- '₄', + [5] = '5', -- '₅', + [6] = '6', -- '₆', + [7] = '7', -- '₇', + [8] = '8', -- '₈', + [9] = '9', -- '₉', + ['+'] = '>', -- '₊', + } +< + The count characters used when `signs.*.show_count` is enabled. The + `+` entry is used as a fallback. With the default, any count outside + of 1-9 uses the `>` character in the sign. + + Possible use cases for this field: + • to specify unicode characters for the counts instead of 1-9. + • to define characters to be used for counts greater than 9. + +status_formatter *gitsigns-config-status_formatter* + Type: `function` + Default: > + function(status) + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then table.insert(status_txt, '+'..added ) end + if changed and changed > 0 then table.insert(status_txt, '~'..changed) end + if removed and removed > 0 then table.insert(status_txt, '-'..removed) end + return table.concat(status_txt, ' ') + end +< + Function used to format `b:gitsigns_status`. + +max_file_length *gitsigns-config-max_file_length* + Type: `number`, Default: `40000` + + Max file length (in lines) to attach to. + +preview_config *gitsigns-config-preview_config* + Type: `table[extended]` + Default: > + { + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1 + } +< + Option overrides for the Gitsigns preview window. Table is passed directly + to `nvim_open_win`. + +attach_to_untracked *gitsigns-config-attach_to_untracked* + Type: `boolean`, Default: `true` + + Attach to untracked files. + +update_debounce *gitsigns-config-update_debounce* + Type: `number`, Default: `100` + + Debounce time for updates (in milliseconds). + +current_line_blame *gitsigns-config-current_line_blame* + Type: `boolean`, Default: `false` + + Adds an unobtrusive and customisable blame annotation at the end of + the current line. + + The highlight group used for the text is `GitSignsCurrentLineBlame`. + +current_line_blame_opts *gitsigns-config-current_line_blame_opts* + Type: `table[extended]` + Default: > + { + virt_text = true, + virt_text_pos = 'eol', + virt_text_priority = 100, + delay = 1000 + } +< + Options for the current line blame annotation. + + Fields: ~ + • virt_text: boolean + Whether to show a virtual text blame annotation. + • virt_text_pos: string + Blame annotation position. Available values: + `eol` Right after eol character. + `overlay` Display over the specified column, without + shifting the underlying text. + `right_align` Display right aligned in the window. + • delay: integer + Sets the delay (in milliseconds) before blame virtual text is + displayed. + • ignore_whitespace: boolean + Ignore whitespace when running blame. + • virt_text_priority: integer + Priority of virtual text. + +current_line_blame_formatter_opts + *gitsigns-config-current_line_blame_formatter_opts* + DEPRECATED + + Type: `table[extended]` + Default: > + { + relative_time = false + } +< + Options for the current line blame annotation formatter. + + Fields: ~ + • relative_time: boolean + +current_line_blame_formatter *gitsigns-config-current_line_blame_formatter* + Type: `string|function`, Default: `' , - '` + + String or function used to format the virtual text of + |gitsigns-config-current_line_blame|. + + When a string, accepts the following format specifiers: + + • `` + • `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` + + For `` and ``, `FORMAT` can + be any valid date format that is accepted by `os.date()` with the + addition of `%R` (defaults to `%Y-%m-%d`): + + • `%a` abbreviated weekday name (e.g., Wed) + • `%A` full weekday name (e.g., Wednesday) + • `%b` abbreviated month name (e.g., Sep) + • `%B` full month name (e.g., September) + • `%c` date and time (e.g., 09/16/98 23:48:10) + • `%d` day of the month (16) [01-31] + • `%H` hour, using a 24-hour clock (23) [00-23] + • `%I` hour, using a 12-hour clock (11) [01-12] + • `%M` minute (48) [00-59] + • `%m` month (09) [01-12] + • `%p` either "am" or "pm" (pm) + • `%S` second (10) [00-61] + • `%w` weekday (3) [0-6 = Sunday-Saturday] + • `%x` date (e.g., 09/16/98) + • `%X` time (e.g., 23:48:10) + • `%Y` full year (1998) + • `%y` two-digit year (98) [00-99] + • `%%` the character `%´ + • `%R` relative (e.g., 4 months ago) + + When a function: + Parameters: ~ + {name} Git user name returned from `git config user.name` . + {blame_info} Table with the following keys: + • `abbrev_sha`: string + • `orig_lnum`: integer + • `final_lnum`: integer + • `author`: string + • `author_mail`: string + • `author_time`: integer + • `author_tz`: string + • `committer`: string + • `committer_mail`: string + • `committer_time`: integer + • `committer_tz`: string + • `summary`: string + • `previous`: string + • `filename`: string + + Note that the keys map onto the output of: + `git blame --line-porcelain` + + {opts} Passed directly from + |gitsigns-config-current_line_blame_formatter_opts|. + + Return: ~ + The result of this function is passed directly to the `opts.virt_text` + field of |nvim_buf_set_extmark| and thus must be a list of + [text, highlight] tuples. + +current_line_blame_formatter_nc + *gitsigns-config-current_line_blame_formatter_nc* + Type: `string|function`, Default: `' '` + + String or function used to format the virtual text of + |gitsigns-config-current_line_blame| for lines that aren't committed. + + See |gitsigns-config-current_line_blame_formatter| for more information. + +trouble *gitsigns-config-trouble* + Type: `boolean`, Default: true if installed + + When using setqflist() or setloclist(), open Trouble instead of the + quickfix/location list window. + +yadm *gitsigns-config-yadm* + Type: `table`, Default: `{ enable = false }` + + yadm configuration. + +word_diff *gitsigns-config-word_diff* + Type: `boolean`, Default: `false` + + Highlight intra-line word differences in the buffer. + Requires `config.diff_opts.internal = true` . + + Uses the highlights: + • For word diff in previews: + • `GitSignsAddInline` + • `GitSignsChangeInline` + • `GitSignsDeleteInline` + • For word diff in buffer: + • `GitSignsAddLnInline` + • `GitSignsChangeLnInline` + • `GitSignsDeleteLnInline` + • For word diff in virtual lines (e.g. show_deleted): + • `GitSignsAddVirtLnInline` + • `GitSignsChangeVirtLnInline` + • `GitSignsDeleteVirtLnInline` + +debug_mode *gitsigns-config-debug_mode* + Type: `boolean`, Default: `false` + + Enables debug logging and makes the following functions + available: `dump_cache`, `debug_messages`, `clear_debug`. + + +============================================================================== +COMMAND *gitsigns-command* + + *:Gitsigns* +:Gitsigns {subcmd} {args} Run a Gitsigns command. {subcmd} can be any + function documented in |gitsigns-functions|. + Each argument in {args} will be attempted to be + parsed as a Lua value using `loadstring`, however + if this fails the argument will remain as the + string given by ||. + + Note this command is equivalent to: + `:lua require('gitsigns').{subcmd}({args})` + + Examples: > + :Gitsigns diffthis HEAD~1 + :Gitsigns blame_line + :Gitsigns toggle_signs + :Gitsigns toggle_current_line_blame + :Gitsigns change_base ~ + :Gitsigns reset_buffer + :Gitsigns change_base nil true + +============================================================================== +SPECIFYING OBJECTS *gitsigns-object* *gitsigns-revision* + +Gitsigns objects are Git revisions as defined in the "SPECIFYING REVISIONS" +section in the gitrevisions(7) man page. For commands that accept an optional +base, the default is the file in the index. Examples: + +Object Meaning ~ +@ Version of file in the commit referenced by @ aka HEAD +main Version of file in the commit referenced by main +main^ Version of file in the parent of the commit referenced by main +main~ " +main~1 " +main...other Version of file in the merge base of main and other +@^ Version of file in the parent of HEAD +@~2 Version of file in the grandparent of HEAD +92eb3dd Version of file in the commit 92eb3dd +:1 The file's common ancestor during a conflict +:2 The alternate file in the target branch during a conflict + +============================================================================== +STATUSLINE *gitsigns-statusline* + + *b:gitsigns_status* *b:gitsigns_status_dict* +The buffer variables `b:gitsigns_status` and `b:gitsigns_status_dict` are +provided. `b:gitsigns_status` is formatted using `config.status_formatter` +. `b:gitsigns_status_dict` is a dictionary with the keys: + + • `added` - Number of added lines. + • `changed` - Number of changed lines. + • `removed` - Number of removed lines. + • `head` - Name of current HEAD (branch or short commit hash). + • `root` - Top level directory of the working tree. + • `gitdir` - .git directory. + +Example: +> + set statusline+=%{get(b:,'gitsigns_status','')} +< + *b:gitsigns_head* *g:gitsigns_head* +Use `g:gitsigns_head` and `b:gitsigns_head` to return the name of the current +HEAD (usually branch name). If the current HEAD is detached then this will be +a short commit hash. `g:gitsigns_head` returns the current HEAD for the +current working directory, whereas `b:gitsigns_head` returns the current HEAD +for each buffer. + + *b:gitsigns_blame_line* *b:gitsigns_blame_line_dict* +Provided if |gitsigns-config-current_line_blame| is enabled. +`b:gitsigns_blame_line` if formatted using +`config.current_line_blame_formatter`. `b:gitsigns_blame_line_dict` is a +dictionary containing of the blame object for the current line. For complete +list of keys, see the {blame_info} argument from +|gitsigns-config-current_line_blame_formatter|. + +============================================================================== +TEXT OBJECTS *gitsigns-textobject* + +Since text objects are defined via keymaps, these are exposed and configurable +via the config, see |gitsigns-config-keymaps|. The lua implementation is +exposed through |gitsigns.select_hunk()|. + +============================================================================== +EVENT *gitsigns-event* + +Every time Gitsigns updates its knowledge about hunks, it issues a custom +|User| event named `GitSignsUpdate`. You can use it via usual autocommands, +like so: > + + vim.api.nvim_create_autocmd('User', { + pattern = 'GitSignsUpdate', + callback = function() + print(os.time() .. ' Gitsigns made an update') + end + }) + +------------------------------------------------------------------------------ +vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/etc/doc_template.txt b/etc/soft/nvim/+plugins/gitsigns.nvim/etc/doc_template.txt new file mode 100644 index 0000000..54faf96 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/etc/doc_template.txt @@ -0,0 +1,165 @@ +*gitsigns.txt* Gitsigns +*gitsigns.nvim* + +Author: Lewis Russell +Version: {{VERSION}} +Homepage: +License: MIT license + +============================================================================== +INTRODUCTION *gitsigns* + +Gitsigns is a plugin for Neovim that provides integration with Git via a +feature set which includes (but not limited to): + • Provides signs in the |signcolumn| to show changed/added/removed lines. + • Mappings to operate on hunks to stage, undo or reset against Git's index. + +Gitsigns is implemented entirely in Lua which is built into Neovim and +requires no external dependencies other than git. This is unlike other plugins +that require python, node, etc, which need to communicate with Neovim using +|RPC|. By default, Gitsigns also uses Neovim's built-in diff library +(`vim.diff`) unlike other similar plugins that need to run `git-diff` as an +external process which is less efficient, has tighter bottlenecks and requires +file IO. + +============================================================================== +USAGE *gitsigns-usage* + +For basic setup with all batteries included: +> + require('gitsigns').setup() +< + +Configuration can be passed to the setup function. Here is an example with most +of the default settings: +> +{{SETUP}} +< + +============================================================================== +MAPPINGS *gitsigns-mappings* + +Custom mappings can be defined in the `on_attach` callback in the config table +passed to |gitsigns-setup()|. See |gitsigns-config-on_attach|. + +Most actions can be repeated with `.` if you have |vim-repeat| installed. + +============================================================================== +FUNCTIONS *gitsigns-functions* + +Note functions with the {async} attribute are run asynchronously and are +non-blocking (return immediately). + +{{FUNCTIONS}} + +============================================================================== +CONFIGURATION *gitsigns-config* + +This section describes the configuration fields which can be passed to +|gitsigns.setup()|. Note fields of type `table` may be marked with extended +meaning the field is merged with the default, with the user value given higher +precedence. This allows only specific sub-fields to be configured without +having to redefine the whole field. + +{{CONFIG}} + +============================================================================== +COMMAND *gitsigns-command* + + *:Gitsigns* +:Gitsigns {subcmd} {args} Run a Gitsigns command. {subcmd} can be any + function documented in |gitsigns-functions|. + Each argument in {args} will be attempted to be + parsed as a Lua value using `loadstring`, however + if this fails the argument will remain as the + string given by ||. + + Note this command is equivalent to: + `:lua require('gitsigns').{subcmd}({args})` + + Examples: > + :Gitsigns diffthis HEAD~1 + :Gitsigns blame_line + :Gitsigns toggle_signs + :Gitsigns toggle_current_line_blame + :Gitsigns change_base ~ + :Gitsigns reset_buffer + :Gitsigns change_base nil true + +============================================================================== +SPECIFYING OBJECTS *gitsigns-object* *gitsigns-revision* + +Gitsigns objects are Git revisions as defined in the "SPECIFYING REVISIONS" +section in the gitrevisions(7) man page. For commands that accept an optional +base, the default is the file in the index. Examples: + +Object Meaning ~ +@ Version of file in the commit referenced by @ aka HEAD +main Version of file in the commit referenced by main +main^ Version of file in the parent of the commit referenced by main +main~ " +main~1 " +main...other Version of file in the merge base of main and other +@^ Version of file in the parent of HEAD +@~2 Version of file in the grandparent of HEAD +92eb3dd Version of file in the commit 92eb3dd +:1 The file's common ancestor during a conflict +:2 The alternate file in the target branch during a conflict + +============================================================================== +STATUSLINE *gitsigns-statusline* + + *b:gitsigns_status* *b:gitsigns_status_dict* +The buffer variables `b:gitsigns_status` and `b:gitsigns_status_dict` are +provided. `b:gitsigns_status` is formatted using `config.status_formatter` +. `b:gitsigns_status_dict` is a dictionary with the keys: + + • `added` - Number of added lines. + • `changed` - Number of changed lines. + • `removed` - Number of removed lines. + • `head` - Name of current HEAD (branch or short commit hash). + • `root` - Top level directory of the working tree. + • `gitdir` - .git directory. + +Example: +> + set statusline+=%{get(b:,'gitsigns_status','')} +< + *b:gitsigns_head* *g:gitsigns_head* +Use `g:gitsigns_head` and `b:gitsigns_head` to return the name of the current +HEAD (usually branch name). If the current HEAD is detached then this will be +a short commit hash. `g:gitsigns_head` returns the current HEAD for the +current working directory, whereas `b:gitsigns_head` returns the current HEAD +for each buffer. + + *b:gitsigns_blame_line* *b:gitsigns_blame_line_dict* +Provided if |gitsigns-config-current_line_blame| is enabled. +`b:gitsigns_blame_line` if formatted using +`config.current_line_blame_formatter`. `b:gitsigns_blame_line_dict` is a +dictionary containing of the blame object for the current line. For complete +list of keys, see the {blame_info} argument from +|gitsigns-config-current_line_blame_formatter|. + +============================================================================== +TEXT OBJECTS *gitsigns-textobject* + +Since text objects are defined via keymaps, these are exposed and configurable +via the config, see |gitsigns-config-keymaps|. The lua implementation is +exposed through |gitsigns.select_hunk()|. + +============================================================================== +EVENT *gitsigns-event* + +Every time Gitsigns updates its knowledge about hunks, it issues a custom +|User| event named `GitSignsUpdate`. You can use it via usual autocommands, +like so: > + + vim.api.nvim_create_autocmd('User', { + pattern = 'GitSignsUpdate', + callback = function() + print(os.time() .. ' Gitsigns made an update') + end + }) + +------------------------------------------------------------------------------ +vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/gen_help.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/gen_help.lua new file mode 100755 index 0000000..f463a1f --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/gen_help.lua @@ -0,0 +1,327 @@ +#!/bin/sh +_=[[ +exec lua "$0" "$@" +]] +-- Simple script to update the help doc by reading the config schema. + +local inspect = require('inspect') +local config = require('lua.gitsigns.config') + +function table.slice(tbl, first, last, step) + local sliced = {} + for i = first or 1, last or #tbl, step or 1 do + sliced[#sliced+1] = tbl[i] + end + return sliced +end + +local function is_simple_type(t) + return t == 'number' or t == 'string' or t == 'boolean' +end + +local function startswith(str, start) + return str.sub(str, 1, string.len(start)) == start +end + +local function read_file(path) + local f = assert(io.open(path, 'r')) + local t = f:read("*all") + f:close() + return t +end + +local function read_file_lines(path) + local lines = {} + for l in read_file(path):gmatch("([^\n]*)\n?") do + table.insert(lines, l) + end + return lines +end + +-- To make sure the output is consistent between runs (to minimise diffs), we +-- need to iterate through the schema keys in a deterministic way. To do this we +-- do a smple scan over the file the schema is defined in and collect the keys +-- in the order they are defined. +local function get_ordered_schema_keys() + local c = read_file('lua/gitsigns/config.lua') + + local ci = c:gmatch("[^\n\r]+") + + for l in ci do + if startswith(l, 'M.schema = {') then + break + end + end + + local keys = {} + for l in ci do + if startswith(l, '}') then + break + end + if l:find('^ (%w+).*') then + local lc = l:gsub('^%s*([%w_]+).*', '%1') + table.insert(keys, lc) + end + end + + return keys +end + +local function get_default(field) + local cfg = read_file_lines('teal/gitsigns/config.tl') + + local fs, fe + for i = 1, #cfg do + local l = cfg[i] + if l:match('^ '..field..' =') then + fs = i + end + if fs and l:match('^ }') then + fe = i + break + end + end + + local ds, de + for i = fs, fe do + local l = cfg[i] + if l:match('^ default =') then + ds = i + if l:match('},') or l:match('nil,') or l:match("default = '.*'") then + de = i + break + end + end + if ds and l:match('^ }') then + de = i + break + end + end + + local ret = {} + for i = ds, de do + local l = cfg[i] + if i == ds then + l = l:gsub('%s*default = ', '') + end + if i == de then + l = l:gsub('(.*),', '%1') + end + table.insert(ret, l) + end + + return table.concat(ret, '\n') +end + +local function gen_config_doc_deprecated(dep_info, out) + if type(dep_info) == 'table' and dep_info.hard then + out(' HARD-DEPRECATED') + else + out(' DEPRECATED') + end + if type(dep_info) == 'table' then + if dep_info.message then + out(' '..dep_info.message) + end + if dep_info.new_field then + out('') + local opts_key, field = dep_info.new_field:match('(.*)%.(.*)') + if opts_key and field then + out((' Please instead use the field `%s` in |gitsigns-config-%s|.'):format(field, opts_key)) + else + out((' Please instead use |gitsigns-config-%s|.'):format(dep_info.new_field)) + end + end + end + out('') +end + +local function gen_config_doc_field(field, out) + local v = config.schema[field] + + -- Field heading and tag + local t = ('*gitsigns-config-%s*'):format(field) + if #field + #t < 80 then + out(('%-29s %48s'):format(field, t)) + else + out(('%-29s'):format(field)) + out(('%78s'):format(t)) + end + + if v.deprecated then + gen_config_doc_deprecated(v.deprecated, out) + end + + if v.description then + local d + if v.default_help ~= nil then + d = v.default_help + elseif is_simple_type(v.type) then + d = inspect(v.default) + d = ('`%s`'):format(d) + else + d = get_default(field) + if d:find('\n') then + d = d:gsub('\n([^\n\r])', '\n%1') + else + d = ('`%s`'):format(d) + end + end + + local vtype = (function() + if v.type == 'table' and v.deep_extend then + return 'table[extended]' + end + if type(v.type) == 'table' then + v.type = table.concat(v.type, '|') + end + return v.type + end)() + + if d:find('\n') then + out((' Type: `%s`'):format(vtype)) + out(' Default: >') + out(' '..d:gsub('\n([^\n\r])', '\n %1')) + out('<') + else + out((' Type: `%s`, Default: %s'):format(vtype, d)) + out() + end + + out(v.description:gsub(' +$', '')) + end +end + +local function gen_config_doc() + local res = {} + local function out(line) + res[#res+1] = line or '' + end + for _, k in ipairs(get_ordered_schema_keys()) do + gen_config_doc_field(k, out) + end + return table.concat(res, '\n') +end + +local function parse_func_header(line) + local func = line:match('%.([^ ]+)') + if not func then + error('Unable to parse: '..line) + end + local args_raw = line:match('function%((.*)%)') + local args = {} + for k in string.gmatch(args_raw, "([%w_]+):") do + if k:sub(1, 1) ~= '_' then + args[#args+1] = string.format('{%s}', k) + end + end + return string.format( + '%-40s%38s', + string.format('%s(%s)', func, table.concat(args, ', ')), + '*gitsigns.'..func..'()*' + ) +end + +local function gen_functions_doc_from_file(path) + local i = read_file(path):gmatch("([^\n]*)\n?") + + local res = {} + local blocks = {} + local block = {''} + + local in_block = false + for l in i do + local l1 = l:match('^%-%-%- ?(.*)') + if l1 then + in_block = true + if l1 ~= '' and l1 ~= '<' then + l1 = ' '..l1 + end + block[#block+1] = l1 + else + if in_block then + -- First line after block + block[1] = parse_func_header(l) + blocks[#blocks+1] = block + block = {''} + end + in_block = false + end + end + + for j = #blocks, 1, -1 do + local b = blocks[j] + for k = 1, #b do + res[#res+1] = b[k] + end + res[#res+1] = '' + end + + return table.concat(res, '\n') +end + +local function gen_functions_doc(files) + local res = '' + for _, path in ipairs(files) do + res = res..'\n'..gen_functions_doc_from_file(path) + end + return res +end + +local function get_setup_from_readme() + local i = read_file('README.md'):gmatch("([^\n]*)\n?") + local res = {} + local function append(line) + res[#res+1] = line ~= '' and ' '..line or '' + end + for l in i do + if l:match("require%('gitsigns'%).setup {") then + append(l) + break + end + end + + for l in i do + append(l) + if l == '}' then + break + end + end + + return table.concat(res, '\n') +end + +local function get_marker_text(marker) + return ({ + VERSION = '0.6-dev', + CONFIG = gen_config_doc, + FUNCTIONS = gen_functions_doc{ + 'teal/gitsigns.tl', + 'teal/gitsigns/actions.tl', + }, + SETUP = get_setup_from_readme + })[marker] +end + +local function main() + local i = read_file('etc/doc_template.txt'):gmatch("([^\n]*)\n?") + + local out = io.open('doc/gitsigns.txt', 'w') + + for l in i do + local marker = l:match('{{(.*)}}') + if marker then + local sub = get_marker_text(marker) + if sub then + if type(sub) == 'function' then + sub = sub() + end + sub = sub:gsub('%%', '%%%%') + l = l:gsub('{{'..marker..'}}', sub) + end + end + out:write(l or '', '\n') + end +end + +main() diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/gitsigns.nvim-scm-1.rockspec b/etc/soft/nvim/+plugins/gitsigns.nvim/gitsigns.nvim-scm-1.rockspec new file mode 100644 index 0000000..d3a6fa9 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/gitsigns.nvim-scm-1.rockspec @@ -0,0 +1,37 @@ +local _MODREV, _SPECREV = 'scm', '-1' + +rockspec_format = "3.0" +package = 'gitsigns.nvim' +version = _MODREV .. _SPECREV + +description = { + summary = 'Git signs written in pure lua', + detailed = [[ + Super fast git decorations implemented purely in lua/teal. + ]], + homepage = 'http://github.com/lewis6991/gitsigns.nvim', + license = 'MIT/X11', + labels = { 'neovim' } +} + +dependencies = { + 'lua == 5.1', +} + +source = { + url = 'http://github.com/lewis6991/gitsigns.nvim/archive/v' .. _MODREV .. '.zip', + dir = 'gitsigns.nvim-' .. _MODREV, +} + +if _MODREV == 'scm' then + source = { + url = 'git://github.com/lewis6991/gitsigns.nvim', + } +end + +build = { + type = 'builtin', + copy_directories = { + 'doc' + } +} diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/README.md b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/README.md new file mode 100644 index 0000000..2fc5ae8 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/README.md @@ -0,0 +1,3 @@ +**WARNING**: Do not edit the files in this directory. The files are generated from [teal](https://github.com/teal-language/tl). For the original source files please look in the [teal](../teal) directory. + +See [Makefile](../Makefile) for targets on handling the teal files. diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns.lua new file mode 100644 index 0000000..291f4f1 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns.lua @@ -0,0 +1,546 @@ +local async = require('gitsigns.async') +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler + +local Status = require("gitsigns.status") +local git = require('gitsigns.git') +local manager = require('gitsigns.manager') +local util = require('gitsigns.util') +local hl = require('gitsigns.highlight') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local gs_config = require('gitsigns.config') +local Config = gs_config.Config +local config = gs_config.config + +local gs_debug = require("gitsigns.debug") +local dprintf = gs_debug.dprintf +local dprint = gs_debug.dprint + +local Debounce = require("gitsigns.debounce") +local debounce_trailing = Debounce.debounce_trailing +local throttle_by_id = Debounce.throttle_by_id + +local api = vim.api +local uv = vim.loop +local current_buf = api.nvim_get_current_buf + +local M = {} + + + + + + + + + + +M.detach_all = function() + for k, _ in pairs(cache) do + M.detach(k) + end +end + + + + + + +M.detach = function(bufnr, _keep_signs) + + + + + + + bufnr = bufnr or current_buf() + dprint('Detached') + local bcache = cache[bufnr] + if not bcache then + dprint('Cache was nil') + return + end + + manager.detach(bufnr, _keep_signs) + + + Status:clear(bufnr) + + cache:destroy(bufnr) +end + +local function parse_fugitive_uri(name) + local _, _, root_path, sub_module_path, commit, real_path = + name:find([[^fugitive://(.*)/%.git(.*/)/(%x-)/(.*)]]) + if commit == '0' then + + commit = nil + end + if root_path then + sub_module_path = sub_module_path:gsub("^/modules", "") + name = root_path .. sub_module_path .. real_path + end + return name, commit +end + +local function parse_gitsigns_uri(name) + + local _, _, root_path, commit, rel_path = + name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) + if commit == ':0' then + + commit = nil + end + if root_path then + name = root_path .. '/' .. rel_path + end + return name, commit +end + +local function get_buf_path(bufnr) + local file = + uv.fs_realpath(api.nvim_buf_get_name(bufnr)) or + + api.nvim_buf_call(bufnr, function() + return vim.fn.expand('%:p') + end) + + if not vim.wo.diff then + if vim.startswith(file, 'fugitive://') then + local path, commit = parse_fugitive_uri(file) + dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + + if vim.startswith(file, 'gitsigns://') then + local path, commit = parse_gitsigns_uri(file) + dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + end + + return file +end + +local vimgrep_running = false + +local function on_lines(_, bufnr, _, first, last_orig, last_new, byte_count) + if first == last_orig and last_orig == last_new and byte_count == 0 then + + + return + end + return manager.on_lines(bufnr, first, last_orig, last_new) +end + +local function on_reload(_, bufnr) + local __FUNC__ = 'on_reload' + dprint('Reload') + manager.update_debounced(bufnr) +end + +local function on_detach(_, bufnr) + M.detach(bufnr, true) +end + +local function on_attach_pre(bufnr) + local gitdir, toplevel + if config._on_attach_pre then + local res = async.wrap(config._on_attach_pre, 2)(bufnr) + dprintf('ran on_attach_pre with result %s', vim.inspect(res)) + if type(res) == "table" then + if type(res.gitdir) == 'string' then + gitdir = res.gitdir + end + if type(res.toplevel) == 'string' then + toplevel = res.toplevel + end + end + end + return gitdir, toplevel +end + +local function try_worktrees(_bufnr, file, encoding) + if not config.worktrees then + return + end + + for _, wt in ipairs(config.worktrees) do + local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel) + if git_obj and git_obj.object_name then + dprintf('Using worktree %s', vim.inspect(wt)) + return git_obj + end + end +end + + + + +local attach_throttled = throttle_by_id(function(cbuf, aucmd) + local __FUNC__ = 'attach' + if vimgrep_running then + dprint('attaching is disabled') + return + end + + if cache[cbuf] then + dprint('Already attached') + return + end + + if aucmd then + dprintf('Attaching (trigger=%s)', aucmd) + else + dprint('Attaching') + end + + if not api.nvim_buf_is_loaded(cbuf) then + dprint('Non-loaded buffer') + return + end + + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end + + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + local file, commit = get_buf_path(cbuf) + local encoding = vim.bo[cbuf].fileencoding + + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + local gitdir_oap, toplevel_oap = on_attach_pre(cbuf) + local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + + if not git_obj then + git_obj = try_worktrees(cbuf, file, encoding) + scheduler() + end + + if not git_obj then + dprint('Empty git obj') + return + end + local repo = git_obj.repo + + scheduler() + Status:update(cbuf, { + head = repo.abbrev_head, + root = repo.toplevel, + gitdir = repo.gitdir, + }) + + if vim.startswith(file, repo.gitdir .. util.path_sep) then + dprint('In non-standard git dir') + return + end + + if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then + dprint('Not a file') + return + end + + if not git_obj.relpath then + dprint('Cannot resolve file in repo') + return + end + + if not config.attach_to_untracked and git_obj.object_name == nil then + dprint('File is untracked') + return + end + + + + scheduler() + + if config.on_attach and config.on_attach(cbuf) == false then + dprint('User on_attach() returned false') + return + end + + cache[cbuf] = CacheEntry.new({ + base = config.base, + file = file, + commit = commit, + gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), + git_obj = git_obj, + }) + + if not api.nvim_buf_is_loaded(cbuf) then + dprint('Un-loaded buffer') + return + end + + + + api.nvim_buf_attach(cbuf, false, { + on_lines = on_lines, + on_reload = on_reload, + on_detach = on_detach, + }) + + + manager.update(cbuf, cache[cbuf]) + + if config.keymaps and not vim.tbl_isempty(config.keymaps) then + require('gitsigns.mappings')(config.keymaps, cbuf) + end +end) + + + + + + + + +M.attach = void(function(bufnr, _trigger) + attach_throttled(bufnr or current_buf(), _trigger) +end) + +local M0 = M + +local function complete(arglead, line) + local words = vim.split(line, '%s+') + local n = #words + + local actions = require('gitsigns.actions') + local matches = {} + if n == 2 then + for _, m in ipairs({ actions, M0 }) do + for func, _ in pairs(m) do + if not func:match('^[a-z]') then + + elseif vim.startswith(func, arglead) then + table.insert(matches, func) + end + end + end + elseif n > 2 then + + local cmp_func = actions._get_cmp_func(words[2]) + if cmp_func then + return cmp_func(arglead) + end + end + return matches +end + + + + + + + + +local function parse_to_lua(a) + if tonumber(a) then + return tonumber(a) + elseif a == 'false' or a == 'true' then + return a == 'true' + elseif a == 'nil' then + return nil + end + return a +end + +local run_cmd_func = void(function(params) + local pos_args_raw, named_args_raw = require('gitsigns.argparse').parse_args(params.args) + + local func = pos_args_raw[1] + + if not func then + func = async.wrap(vim.ui.select, 3)(complete('', 'Gitsigns '), {}) + end + + local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) + local named_args = vim.tbl_map(parse_to_lua, named_args_raw) + local args = vim.tbl_extend('error', pos_args, named_args) + + local actions = require('gitsigns.actions') + local actions0 = actions + + dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, { newline = ' ', indent = '' })) + + local cmd_func = actions._get_cmd_func(func) + if cmd_func then + + + cmd_func(args, params) + return + end + + if type(actions0[func]) == 'function' then + actions0[func](unpack(pos_args), named_args) + return + end + + if type(M0[func]) == 'function' then + + M0[func](unpack(pos_args)) + return + end + + error(string.format('%s is not a valid function or action', func)) +end) + +local function setup_command() + api.nvim_create_user_command('Gitsigns', run_cmd_func, + { force = true, nargs = '*', range = true, complete = complete }) +end + +local function wrap_func(fn, ...) + local args = { ... } + local nargs = select('#', ...) + return function() + fn(unpack(args, 1, nargs)) + end +end + +local function autocmd(event, opts) + local opts0 = {} + if type(opts) == "function" then + opts0.callback = wrap_func(opts) + else + opts0 = opts + end + opts0.group = 'gitsigns' + api.nvim_create_autocmd(event, opts0) +end + +local function on_or_after_vimenter(fn) + if vim.v.vim_did_enter == 1 then + fn() + else + api.nvim_create_autocmd('VimEnter', { + callback = wrap_func(fn), + once = true, + }) + end +end + + + + + + + + + +M.setup = void(function(cfg) + gs_config.build(cfg) + + if vim.fn.executable('git') == 0 then + print('gitsigns: git not in path. Aborting setup') + return + end + if config.yadm.enable and vim.fn.executable('yadm') == 0 then + print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config") + config.yadm.enable = false + return + end + + gs_debug.debug_mode = config.debug_mode + gs_debug.verbose = config._verbose + + if config.debug_mode then + for nm, f in pairs(gs_debug.add_debug_functions(cache)) do + M0[nm] = f + end + end + + manager.setup() + + Status.formatter = config.status_formatter + + + + + on_or_after_vimenter(hl.setup_highlights) + + setup_command() + + git.enable_yadm = config.yadm.enable + git.set_version(config._git_version) + scheduler() + + + for _, buf in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_is_loaded(buf) and + api.nvim_buf_get_name(buf) ~= '' then + M.attach(buf, 'setup') + scheduler() + end + end + + api.nvim_create_augroup('gitsigns', {}) + + autocmd('VimLeavePre', M.detach_all) + autocmd('ColorScheme', hl.setup_highlights) + autocmd('BufRead', wrap_func(M.attach, nil, 'BufRead')) + autocmd('BufNewFile', wrap_func(M.attach, nil, 'BufNewFile')) + autocmd('BufWritePost', wrap_func(M.attach, nil, 'BufWritePost')) + + autocmd('OptionSet', { + pattern = 'fileformat', + callback = function() + require('gitsigns.actions').refresh() + end, }) + + + + + autocmd('QuickFixCmdPre', { + pattern = '*vimgrep*', + callback = function() + vimgrep_running = true + end, + }) + + autocmd('QuickFixCmdPost', { + pattern = '*vimgrep*', + callback = function() + vimgrep_running = false + end, + }) + + require('gitsigns.current_line_blame').setup() + + scheduler() + manager.update_cwd_head() + + + autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head)) +end) + +if _TEST then + M.parse_fugitive_uri = parse_fugitive_uri +end + +return setmetatable(M, { + __index = function(_, f) + return (require('gitsigns.actions'))[f] + end, +}) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/actions.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/actions.lua new file mode 100644 index 0000000..5b657ed --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/actions.lua @@ -0,0 +1,1269 @@ +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler + +local config = require('gitsigns.config').config +local mk_repeatable = require('gitsigns.repeat').mk_repeatable +local popup = require('gitsigns.popup') +local util = require('gitsigns.util') +local manager = require('gitsigns.manager') +local git = require('gitsigns.git') +local run_diff = require('gitsigns.diff') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local gs_hunks = require('gitsigns.hunks') +local Hunk = gs_hunks.Hunk +local Hunk_Public = gs_hunks.Hunk_Public + +local api = vim.api +local current_buf = api.nvim_get_current_buf + + + + + + + + + + + + + + + + + + + + + + + + + + + + +local M = {QFListOpts = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +local C = {} + + + +local CP = {} + +local ns_inline = api.nvim_create_namespace('gitsigns_preview_inline') + +local function complete_heads(arglead) + local all = vim.fn.systemlist({ 'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes' }) + return vim.tbl_filter(function(x) + return vim.startswith(x, arglead) + end, all) +end + + + + + + + + + +M.toggle_signs = function(value) + if value ~= nil then + config.signcolumn = value + else + config.signcolumn = not config.signcolumn + end + M.refresh() + return config.signcolumn +end + + + + + + + + + +M.toggle_numhl = function(value) + if value ~= nil then + config.numhl = value + else + config.numhl = not config.numhl + end + M.refresh() + return config.numhl +end + + + + + + + + + +M.toggle_linehl = function(value) + if value ~= nil then + config.linehl = value + else + config.linehl = not config.linehl + end + M.refresh() + return config.linehl +end + + + + + + + + + +M.toggle_word_diff = function(value) + if value ~= nil then + config.word_diff = value + else + config.word_diff = not config.word_diff + end + + api.nvim__buf_redraw_range(0, vim.fn.line('w0') - 1, vim.fn.line('w$')) + return config.word_diff +end + + + + + + + + + +M.toggle_current_line_blame = function(value) + if value ~= nil then + config.current_line_blame = value + else + config.current_line_blame = not config.current_line_blame + end + M.refresh() + return config.current_line_blame +end + + + + + + + + + +M.toggle_deleted = function(value) + if value ~= nil then + config.show_deleted = value + else + config.show_deleted = not config.show_deleted + end + M.refresh() + return config.show_deleted +end + +local function get_cursor_hunk(bufnr, hunks) + bufnr = bufnr or current_buf() + hunks = hunks or cache[bufnr].hunks + + local lnum = api.nvim_win_get_cursor(0)[1] + return gs_hunks.find_hunk(lnum, hunks) +end + +local function update(bufnr) + manager.update(bufnr) + scheduler() + if vim.wo.diff then + require('gitsigns.diffthis').update(bufnr) + end +end + +local function get_range(params) + local range + if params.range > 0 then + range = { params.line1, params.line2 } + end + return range +end + +local function get_hunks(bufnr, bcache, greedy) + local hunks + + if greedy then + + local buftext = util.buf_lines(bufnr) + hunks = run_diff(bcache.compare_text, buftext, false) + scheduler() + else + hunks = bcache.hunks + end + + return hunks +end + + + + + + + + + + + + + + + + + + + + +M.stage_hunk = mk_repeatable(void(function(range, opts) + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + if not util.path_exists(bcache.file) then + print("Error: Cannot stage lines. Please add the file to the working tree.") + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy ~= false) + local hunk + + if range then + table.sort(range) + local top, bot = range[1], range[2] + hunk = gs_hunks.create_partial_hunk(hunks, top, bot) + hunk.added.lines = api.nvim_buf_get_lines(bufnr, top - 1, bot, false) + hunk.removed.lines = vim.list_slice( + bcache.compare_text, + hunk.removed.start, + hunk.removed.start + hunk.removed.count - 1) + + else + hunk = get_cursor_hunk(bufnr, hunks) + end + + if not hunk then + return + end + + bcache.git_obj:stage_hunks({ hunk }) + + table.insert(bcache.staged_diffs, hunk) + + bcache:invalidate() + update(bufnr) +end)) + +C.stage_hunk = function(_, params) + M.stage_hunk(get_range(params)) +end + + + + + + + + + + + + + + + + + +M.reset_hunk = mk_repeatable(void(function(range, opts) + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy ~= false) + local hunk + + if range then + table.sort(range) + local top, bot = range[1], range[2] + hunk = gs_hunks.create_partial_hunk(hunks, top, bot) + hunk.added.lines = api.nvim_buf_get_lines(bufnr, top - 1, bot, false) + hunk.removed.lines = vim.list_slice( + bcache.compare_text, + hunk.removed.start, + hunk.removed.start + hunk.removed.count - 1) + + else + hunk = get_cursor_hunk(bufnr, hunks) + end + + if not hunk then + return + end + + local lstart, lend + if hunk.type == 'delete' then + lstart = hunk.added.start + lend = hunk.added.start + else + lstart = hunk.added.start - 1 + lend = hunk.added.start - 1 + hunk.added.count + end + util.set_lines(bufnr, lstart, lend, hunk.removed.lines) +end)) + +C.reset_hunk = function(_, params) + M.reset_hunk(get_range(params)) +end + + +M.reset_buffer = function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + util.set_lines(bufnr, 0, -1, bcache.compare_text) +end + + + + + + + + +M.undo_stage_hunk = void(function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunk = table.remove(bcache.staged_diffs) + if not hunk then + print("No hunks to undo") + return + end + + bcache.git_obj:stage_hunks({ hunk }, true) + bcache:invalidate() + update(bufnr) +end) + + + + + +M.stage_buffer = void(function() + local bufnr = current_buf() + + local bcache = cache[bufnr] + if not bcache then + return + end + + + local hunks = bcache.hunks + if #hunks == 0 then + print("No unstaged changes in file to stage") + return + end + + if not util.path_exists(bcache.git_obj.file) then + print("Error: Cannot stage file. Please add it to the working tree.") + return + end + + bcache.git_obj:stage_hunks(hunks) + + for _, hunk in ipairs(hunks) do + table.insert(bcache.staged_diffs, hunk) + end + + bcache:invalidate() + update(bufnr) +end) + + + + + + + +M.reset_buffer_index = void(function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + + + + + + + bcache.staged_diffs = {} + + bcache.git_obj:unstage_file() + + bcache:invalidate() + update(bufnr) +end) + +local function process_nav_opts(opts) + + if opts.navigation_message == nil then + opts.navigation_message = not vim.opt.shortmess:get().S + end + + + if opts.wrap == nil then + opts.wrap = vim.opt.wrapscan:get() + end + + if opts.foldopen == nil then + opts.foldopen = vim.tbl_contains(vim.opt.foldopen:get(), 'search') + end + + if opts.greedy == nil then + opts.greedy = true + end +end + + +local function defer(fn) + if vim.in_fast_event() then + vim.schedule(fn) + else + vim.defer_fn(fn, 1) + end +end + +local function has_preview_inline(bufnr) + return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, { limit = 1 }) > 0 +end + +local nav_hunk = void(function(opts) + process_nav_opts(opts) + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy) + + if not hunks or vim.tbl_isempty(hunks) then + if opts.navigation_message then + api.nvim_echo({ { 'No hunks', 'WarningMsg' } }, false, {}) + end + return + end + local line = api.nvim_win_get_cursor(0)[1] + + local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, opts.forwards, opts.wrap) + + if hunk == nil then + if opts.navigation_message then + api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) + end + return + end + + local row = opts.forwards and hunk.added.start or hunk.vend + if row then + + if row == 0 then + row = 1 + end + vim.cmd([[ normal! m' ]]) + api.nvim_win_set_cursor(0, { row, 0 }) + if opts.foldopen then + vim.cmd('silent! foldopen!') + end + if opts.preview or popup.is_open('hunk') ~= nil then + + + defer(M.preview_hunk) + elseif has_preview_inline(bufnr) then + defer(M.preview_hunk_inline) + end + + if index ~= nil and opts.navigation_message then + api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {}) + end + + end +end) + + + + + + + + + + + + + + + + + + + + + + + +M.next_hunk = function(opts) + opts = opts or {} + opts.forwards = true + nav_hunk(opts) +end + + + + + + + +M.prev_hunk = function(opts) + opts = opts or {} + opts.forwards = false + nav_hunk(opts) +end + +local HlMark = popup.HlMark + +local function lines_format(fmt, + info) + + local ret = vim.deepcopy(fmt) + + for _, line in ipairs(ret) do + for _, s in ipairs(line) do + s[1] = util.expand_format(s[1], info) + end + end + + return ret +end + +local function hlmarks_for_hunk(hunk, hl) + local hls = {} + + local removed, added = hunk.removed, hunk.added + + if hl then + hls[#hls + 1] = { + hl_group = hl, + start_row = 0, + end_row = removed.count + added.count, + } + end + + hls[#hls + 1] = { + hl_group = 'GitSignsDeletePreview', + start_row = 0, + end_row = removed.count, + } + + hls[#hls + 1] = { + hl_group = 'GitSignsAddPreview', + start_row = removed.count, + end_row = removed.count + added.count, + } + + if config.diff_opts.internal then + local removed_regions, added_regions = + require('gitsigns.diff_int').run_word_diff(removed.lines, added.lines) + for _, region in ipairs(removed_regions) do + hls[#hls + 1] = { + hl_group = 'GitSignsDeleteInline', + start_row = region[1] - 1, + start_col = region[3], + end_col = region[4], + } + end + for _, region in ipairs(added_regions) do + hls[#hls + 1] = { + hl_group = 'GitSignsAddInline', + start_row = region[1] + removed.count - 1, + start_col = region[3], + end_col = region[4], + } + end + end + + return hls +end + +local function insert_hunk_hlmarks(fmt, hunk) + for _, line in ipairs(fmt) do + for _, s in ipairs(line) do + local hl = s[2] + if s[1] == '' and type(hl) == "string" then + s[2] = hlmarks_for_hunk(hunk, hl) + end + end + end +end + +local function noautocmd(f) + return function() + local ei = vim.o.eventignore + vim.o.eventignore = 'all' + f() + vim.o.eventignore = ei + end +end + + + + +M.preview_hunk = noautocmd(function() + if popup.focus_open('hunk') then + return + end + + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunk, index = get_cursor_hunk(bufnr, bcache.hunks) + + if not hunk then return end + + local lines_fmt = { + { { 'Hunk of ', 'Title' } }, + { { '', 'NormalFloat' } }, + } + + insert_hunk_hlmarks(lines_fmt, hunk) + + local lines_spec = lines_format(lines_fmt, { + hunk_no = index, + num_hunks = #bcache.hunks, + hunk = gs_hunks.patch_lines(hunk, vim.bo[bufnr].fileformat), + }) + + popup.create(lines_spec, config.preview_config, 'hunk') +end) + + +M.preview_hunk_inline = function() + local bufnr = current_buf() + + local hunk = get_cursor_hunk(bufnr) + + if not hunk then + return + end + + manager.show_added(bufnr, ns_inline, hunk) + manager.show_deleted(bufnr, ns_inline, hunk) + + api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter' }, { + callback = function() + api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) + end, + once = true, + }) + +end + + +M.select_hunk = function() + local hunk = get_cursor_hunk() + if not hunk then return end + + vim.cmd('normal! ' .. hunk.added.start .. 'GV' .. hunk.vend .. 'G') +end + + + + + + + + + + + + + + + + + + + + + +M.get_hunks = function(bufnr) + bufnr = bufnr or current_buf() + if not cache[bufnr] then return end + local ret = {} + + for _, h in ipairs(cache[bufnr].hunks or {}) do + ret[#ret + 1] = { + head = h.head, + lines = gs_hunks.patch_lines(h, vim.bo[bufnr].fileformat), + type = h.type, + added = h.added, + removed = h.removed, + } + end + return ret +end + +local function get_blame_hunk(repo, info) + local a = {} + + if info.previous then + a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) + end + local b = repo:get_show_text(info.sha .. ':' .. info.filename) + local hunks = run_diff(a, b, false) + local hunk, i = gs_hunks.find_hunk(info.orig_lnum, hunks) + return hunk, i, #hunks +end + +local function create_blame_fmt(is_committed, full) + if not is_committed then + return { + { { '', 'Label' } }, + } + end + + local header = { + { ' ', 'Directory' }, + { ' ', 'MoreMsg' }, + { '()', 'Label' }, + { ':', 'NormalFloat' }, + } + + if full then + return { + header, + { { '', 'NormalFloat' } }, + { { 'Hunk of ', 'Title' }, { ' ', 'LineNr' } }, + { { '', 'NormalFloat' } }, + } + end + + return { + header, + { { '', 'NormalFloat' } }, + } +end + + + + + + + + + + + + + + + +M.blame_line = void(function(opts) + if popup.focus_open('blame') then + return + end + + opts = opts or {} + + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then return end + + local loading = vim.defer_fn(function() + popup.create({ { { 'Loading...', 'Title' } } }, config.preview_config) + end, 1000) + + scheduler() + local buftext = util.buf_lines(bufnr) + local fileformat = vim.bo[bufnr].fileformat + local lnum = api.nvim_win_get_cursor(0)[1] + local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + pcall(function() + loading:close() + end) + + local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 + + local blame_fmt = create_blame_fmt(is_committed, opts.full) + + local info = result + + if is_committed and opts.full then + info.body = bcache.git_obj:command({ 'show', '-s', '--format=%B', result.sha }) + + local hunk + + hunk, info.hunk_no, info.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) + + info.hunk = gs_hunks.patch_lines(hunk, fileformat) + info.hunk_head = hunk.head + insert_hunk_hlmarks(blame_fmt, hunk) + end + + scheduler() + + popup.create(lines_format(blame_fmt, info), config.preview_config, 'blame') +end) + +local function update_buf_base(buf, bcache, base) + bcache.base = base + bcache:invalidate() + update(buf) +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M.change_base = void(function(base, global) + base = util.calc_base(base) + + if global then + config.base = base + + for bufnr, bcache in pairs(cache) do + update_buf_base(bufnr, bcache, base) + end + else + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then return end + + update_buf_base(bufnr, bcache, base) + end +end) + +C.change_base = function(args, _) + M.change_base(args[1], (args[2] or args.global)) +end + +CP.change_base = complete_heads + + + + + +M.reset_base = function(global) + M.change_base(nil, global) +end + +C.reset_base = function(args, _) + M.change_base(nil, (args[1] or args.global)) +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M.diffthis = function(base, opts) + + if base ~= nil then + base = tostring(base) + end + opts = opts or {} + local diffthis = require('gitsigns.diffthis') + if not opts.vertical then + opts.vertical = config.diff_opts.vertical + end + diffthis.diffthis(base, opts) +end + +C.diffthis = function(args, params) + + local opts = { + vertical = args.vertical, + split = args.split, + } + + if params.smods then + if params.smods.split ~= '' and opts.split == nil then + opts.split = params.smods.split + end + if opts.vertical == nil then + opts.vertical = params.smods.vertical + end + end + + M.diffthis(args[1], opts) +end + +CP.diffthis = complete_heads + + + + + + + + + + + + + + + + + + + + + + + + + + +M.show = function(revision) + local diffthis = require('gitsigns.diffthis') + diffthis.show(revision) +end + +CP.show = complete_heads + +local function hunks_to_qflist(buf_or_filename, hunks, qflist) + for i, hunk in ipairs(hunks) do + qflist[#qflist + 1] = { + bufnr = type(buf_or_filename) == "number" and (buf_or_filename) or nil, + filename = type(buf_or_filename) == "string" and buf_or_filename or nil, + lnum = hunk.added.start, + text = string.format('Lines %d-%d (%d/%d)', + hunk.added.start, hunk.vend, i, #hunks), + } + end +end + +local function buildqflist(target) + target = target or current_buf() + if target == 0 then target = current_buf() end + local qflist = {} + + if type(target) == 'number' then + local bufnr = target + if not cache[bufnr] then return end + hunks_to_qflist(bufnr, cache[bufnr].hunks, qflist) + elseif target == 'attached' then + for bufnr, bcache in pairs(cache) do + hunks_to_qflist(bufnr, bcache.hunks, qflist) + end + elseif target == 'all' then + local repos = {} + for _, bcache in pairs(cache) do + local repo = bcache.git_obj.repo + if not repos[repo.gitdir] then + repos[repo.gitdir] = repo + end + end + + local repo = git.Repo.new(vim.loop.cwd()) + if not repos[repo.gitdir] then + repos[repo.gitdir] = repo + end + + for _, r in pairs(repos) do + for _, f in ipairs(r:files_changed()) do + local f_abs = r.toplevel .. '/' .. f + local stat = vim.loop.fs_stat(f_abs) + if stat and stat.type == 'file' then + local a = r:get_show_text(':0:' .. f) + scheduler() + local hunks = run_diff(a, util.file_lines(f_abs)) + hunks_to_qflist(f_abs, hunks, qflist) + end + end + end + + end + return qflist +end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M.setqflist = void(function(target, opts) + opts = opts or {} + if opts.open == nil then + opts.open = true + end + local qfopts = { + items = buildqflist(target), + title = 'Hunks', + } + scheduler() + if opts.use_location_list then + local nr = opts.nr or 0 + vim.fn.setloclist(nr, {}, ' ', qfopts) + if opts.open then + if config.trouble then + require('trouble').open("loclist") + else + vim.cmd([[lopen]]) + end + end + else + vim.fn.setqflist({}, ' ', qfopts) + if opts.open then + if config.trouble then + require('trouble').open("quickfix") + else + vim.cmd([[copen]]) + end + end + end +end) + + + + + + + + + + + + + +M.setloclist = function(nr, target) + M.setqflist(target, { + nr = nr, + use_location_list = true, + }) +end + + + + + + + +M.get_actions = function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + local hunk = get_cursor_hunk(bufnr, bcache.hunks) + + local actions_l = {} + + local function add_action(action) + actions_l[#actions_l + 1] = action + end + + if hunk then + add_action('stage_hunk') + add_action('reset_hunk') + add_action('preview_hunk') + add_action('select_hunk') + else + add_action('blame_line') + end + + if not vim.tbl_isempty(bcache.staged_diffs) then + add_action('undo_stage_hunk') + end + + local actions = {} + for _, a in ipairs(actions_l) do + actions[a] = (M)[a] + end + + return actions +end + + + + + +M.refresh = void(function() + manager.reset_signs() + require('gitsigns.highlight').setup_highlights() + require('gitsigns.current_line_blame').setup() + for k, v in pairs(cache) do + v:invalidate() + manager.update(k, v) + end +end) + +function M._get_cmd_func(name) + return C[name] +end + +function M._get_cmp_func(name) + return CP[name] +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/argparse.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/argparse.lua new file mode 100644 index 0000000..6e8d365 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/argparse.lua @@ -0,0 +1,91 @@ +local M = {} + + + +local function is_char(x) + return x:match('[^=\'"%s]') ~= nil +end + + + + + + + + + +function M.parse_args(x) + local pos_args, named_args = {}, {} + + local state = 'in_arg' + local cur_arg = '' + local cur_val = '' + local cur_quote = '' + + local function peek(idx) + return x:sub(idx + 1, idx + 1) + end + + local i = 1 + while i <= #x do + local ch = x:sub(i, i) + + + if state == 'in_arg' then + if is_char(ch) then + cur_arg = cur_arg .. ch + elseif ch:match('%s') then + pos_args[#pos_args + 1] = cur_arg + state = 'in_ws' + elseif ch == '=' then + cur_val = '' + local next_ch = peek(i) + if next_ch == "'" or next_ch == '"' then + cur_quote = next_ch + i = i + 1 + state = 'in_quote' + else + state = 'in_value' + end + end + elseif state == 'in_ws' then + if is_char(ch) then + cur_arg = ch + state = 'in_arg' + end + elseif state == 'in_value' then + if is_char(ch) then + cur_val = cur_val .. ch + elseif ch:match('%s') then + named_args[cur_arg] = cur_val + cur_arg = '' + state = 'in_ws' + end + elseif state == 'in_quote' then + local next_ch = peek(i) + if ch == "\\" and next_ch == cur_quote then + cur_val = cur_val .. next_ch + i = i + 1 + elseif ch == cur_quote then + named_args[cur_arg] = cur_val + state = 'in_ws' + if next_ch ~= '' and not next_ch:match('%s') then + error('malformed argument: ' .. next_ch) + end + else + cur_val = cur_val .. ch + end + end + i = i + 1 + end + + if state == 'in_arg' and #cur_arg > 0 then + pos_args[#pos_args + 1] = cur_arg + elseif state == 'in_value' and #cur_arg > 0 then + named_args[cur_arg] = cur_val + end + + return pos_args, named_args +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/async.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/async.lua new file mode 100644 index 0000000..99cd272 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/async.lua @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + +local M = {} + + + + + + + + + + + +local main_co_or_nil = coroutine.running() + + + + + +function M.wrap(func, argc) + assert(argc) + return function(...) + if coroutine.running() == main_co_or_nil then + return func(...) + end + return coroutine.yield(func, argc, ...) + end +end + + + + + +function M.void(func) + return function(...) + if coroutine.running() ~= main_co_or_nil then + return func(...) + end + + local co = coroutine.create(func) + + local function step(...) + local ret = { coroutine.resume(co, ...) } + local stat, err_or_fn, nargs = unpack(ret) + + if not stat then + error(string.format("The coroutine failed with this message: %s\n%s", + err_or_fn, debug.traceback(co))) + end + + if coroutine.status(co) == 'dead' then + return + end + + assert(type(err_or_fn) == "function", "type error :: expected func") + + local args = { select(4, unpack(ret)) } + args[nargs] = step + err_or_fn(unpack(args, 1, nargs)) + end + + step(...) + end +end + + + +M.scheduler = M.wrap(vim.schedule, 1) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/cache.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/cache.lua new file mode 100644 index 0000000..f9add93 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/cache.lua @@ -0,0 +1,85 @@ +local Hunk = require("gitsigns.hunks").Hunk +local GitObj = require('gitsigns.git').Obj + +local M = {CacheEntry = {}, CacheObj = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + + +local CacheEntry = M.CacheEntry + +CacheEntry.get_compare_rev = function(self, base) + base = base or self.base + if base then + return base + end + + if self.commit then + + return string.format('%s^', self.commit) + end + + local stage = self.git_obj.has_conflicts and 1 or 0 + return string.format(':%d', stage) +end + +CacheEntry.get_rev_bufname = function(self, rev) + rev = rev or self:get_compare_rev() + return string.format( + 'gitsigns://%s/%s:%s', + self.git_obj.repo.gitdir, + rev, + self.git_obj.relpath) + +end + +CacheEntry.invalidate = function(self) + self.compare_text = nil + self.hunks = nil +end + +CacheEntry.new = function(o) + o.staged_diffs = o.staged_diffs or {} + return setmetatable(o, { __index = CacheEntry }) +end + +CacheEntry.destroy = function(self) + local w = self.gitdir_watcher + if w and not w:is_closing() then + w:close() + end +end + +M.CacheObj.destroy = function(self, bufnr) + self[bufnr]:destroy() + self[bufnr] = nil +end + +M.cache = setmetatable({}, { + __index = M.CacheObj, +}) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/config.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/config.lua new file mode 100644 index 0000000..8c4eedd --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/config.lua @@ -0,0 +1,834 @@ +local warn +do + + local ok, ret = pcall(require, 'gitsigns.message') + if ok then + warn = ret.warn + end +end + + + + + + + + + + + + + + + + + + +local M = {Config = {DiffOpts = {}, SignConfig = {}, watch_gitdir = {}, current_line_blame_formatter_opts = {}, current_line_blame_opts = {}, yadm = {}, Worktree = {}, }, } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +M.config = {} + +M.schema = { + signs = { + type = 'table', + deep_extend = true, + default = { + add = { hl = 'GitSignsAdd', text = '┃', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' }, + change = { hl = 'GitSignsChange', text = '┃', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn' }, + delete = { hl = 'GitSignsDelete', text = '▁', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn' }, + topdelete = { hl = 'GitSignsDelete', text = '▔', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn' }, + changedelete = { hl = 'GitSignsChange', text = '~', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn' }, + untracked = { hl = 'GitSignsAdd', text = '┆', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' }, + }, + description = [[ + Configuration for signs: + • `hl` specifies the highlight group to use for the sign. + • `text` specifies the character to use for the sign. + • `numhl` specifies the highlight group to use for the number column + (see |gitsigns-config.numhl|). + • `linehl` specifies the highlight group to use for the line + (see |gitsigns-config.linehl|). + • `show_count` to enable showing count of hunk, e.g. number of deleted + lines. + + Note if a highlight is not defined, it will be automatically derived by + searching for other defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*Gutter` + • `diff*` + • `Diff*` + + For example if `GitSignsAdd` is not defined but `GitGutterAdd` is defined, + then `GitSignsAdd` will be linked to `GitGutterAdd`. + ]], + }, + + keymaps = { + deprecated = { + message = "config.keymaps is now deprecated. Please define mappings in config.on_attach() instead.", + }, + type = 'table', + default = {}, + description = [[ + Keymaps to set up when attaching to a buffer. + + Each key in the table defines the mode and key (whitespace delimited) + for the mapping and the value defines what the key maps to. The value + can be a table which can contain keys matching the options defined in + |map-arguments| which are: `expr`, `noremap`, `nowait`, `script`, `silent` + and `unique`. These options can also be used in the top level of the + table to define default options for all mappings. + + Since this field is not extended (unlike |gitsigns-config-signs|), + mappings defined in this field can be disabled by setting the whole field + to `{}`, and |gitsigns-config-on_attach| can instead be used to define + mappings. + ]], + }, + + worktrees = { + type = 'table', + default = nil, + description = [[ + Detached working trees. + + Array of tables with the keys `gitdir` and `toplevel`. + + If normal attaching fails, then each entry in the table is attempted + with the work tree details set. + + Example: > + worktrees = { + { + toplevel = vim.env.HOME, + gitdir = vim.env.HOME .. '/projects/dotfiles/.git' + } + } + ]], + }, + + _on_attach_pre = { + type = 'function', + default = nil, + description = [[ + Asynchronous hook called before attaching to a buffer. Mainly used to + configure detached worktrees. + + This callback must call its callback argument. The callback argument can + accept an optional table argument with the keys: 'gitdir' and 'toplevel'. + + Example: > + on_attach_pre = function(bufnr, callback) + ... + callback { + gitdir = ..., + toplevel = ... + } + end +< + ]], + }, + + on_attach = { + type = 'function', + default = nil, + description = [[ + Callback called when attaching to a buffer. Mainly used to setup keymaps + when `config.keymaps` is empty. The buffer number is passed as the first + argument. + + This callback can return `false` to prevent attaching to the buffer. + + Example: > + on_attach = function(bufnr) + if vim.api.nvim_buf_get_name(bufnr):match() then + -- Don't attach to specific buffers whose name matches a pattern + return false + end + + -- Setup keymaps + vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) + ... -- More keymaps + end +< + ]], + }, + + watch_gitdir = { + type = 'table', + deep_extend = true, + default = { + enable = true, + interval = 1000, + follow_files = true, + }, + description = [[ + When opening a file, a libuv watcher is placed on the respective + `.git` directory to detect when changes happen to use as a trigger to + update signs. + + Fields: ~ + • `enable`: + Whether the watcher is enabled. + + • `interval`: + Interval the watcher waits between polls of the gitdir in milliseconds. + + • `follow_files`: + If a file is moved with `git mv`, switch the buffer to the new location. + ]], + }, + + sign_priority = { + type = 'number', + default = 6, + description = [[ + Priority to use for signs. + ]], + }, + + signcolumn = { + type = 'boolean', + default = true, + description = [[ + Enable/disable symbols in the sign column. + + When enabled the highlights defined in `signs.*.hl` and symbols defined + in `signs.*.text` are used. + ]], + }, + + numhl = { + type = 'boolean', + default = false, + description = [[ + Enable/disable line number highlights. + + When enabled the highlights defined in `signs.*.numhl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + ]], + }, + + linehl = { + type = 'boolean', + default = false, + description = [[ + Enable/disable line highlights. + + When enabled the highlights defined in `signs.*.linehl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + ]], + }, + + show_deleted = { + type = 'boolean', + default = false, + description = [[ + Show the old version of hunks inline in the buffer (via virtual lines). + + Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. + ]], + }, + + diff_opts = { + type = 'table', + deep_extend = true, + default = function() + local r = { + algorithm = 'myers', + internal = false, + indent_heuristic = false, + vertical = true, + linematch = nil, + } + for _, o in ipairs(vim.opt.diffopt:get()) do + if o == 'indent-heuristic' then + r.indent_heuristic = true + elseif o == 'internal' then + if vim.diff then + r.internal = true + elseif jit and jit.os ~= "Windows" then + + r.internal = true + end + elseif o == 'horizontal' then + r.vertical = false + elseif vim.startswith(o, 'algorithm:') then + r.algorithm = string.sub(o, ('algorithm:'):len() + 1) + elseif vim.startswith(o, 'linematch:') then + r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) + end + end + return r + end, + default_help = "derived from 'diffopt'", + description = [[ + Diff options. + + Fields: ~ + • algorithm: string + Diff algorithm to use. Values: + • "myers" the default algorithm + • "minimal" spend extra time to generate the + smallest possible diff + • "patience" patience diff algorithm + • "histogram" histogram diff algorithm + • internal: boolean + Use Neovim's built in xdiff library for running diffs. + + Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses + `vim.diff`. + • indent_heuristic: boolean + Use the indent heuristic for the internal + diff library. + • vertical: boolean + Start diff mode with vertical splits. + • linematch: integer + Enable second-stage diff on hunks to align lines. + Requires `internal=true`. + ]], + }, + + base = { + type = 'string', + default = nil, + default_help = 'index', + description = [[ + The object/revision to diff against. + See |gitsigns-revision|. + ]], + }, + + count_chars = { + type = 'table', + default = { + [1] = '1', + [2] = '2', + [3] = '3', + [4] = '4', + [5] = '5', + [6] = '6', + [7] = '7', + [8] = '8', + [9] = '9', + ['+'] = '>', + }, + description = [[ + The count characters used when `signs.*.show_count` is enabled. The + `+` entry is used as a fallback. With the default, any count outside + of 1-9 uses the `>` character in the sign. + + Possible use cases for this field: + • to specify unicode characters for the counts instead of 1-9. + • to define characters to be used for counts greater than 9. + ]], + }, + + status_formatter = { + type = 'function', + default = function(status) + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then table.insert(status_txt, '+' .. added) end + if changed and changed > 0 then table.insert(status_txt, '~' .. changed) end + if removed and removed > 0 then table.insert(status_txt, '-' .. removed) end + return table.concat(status_txt, ' ') + end, + default_help = [[function(status) + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then table.insert(status_txt, '+'..added ) end + if changed and changed > 0 then table.insert(status_txt, '~'..changed) end + if removed and removed > 0 then table.insert(status_txt, '-'..removed) end + return table.concat(status_txt, ' ') + end]], + description = [[ + Function used to format `b:gitsigns_status`. + ]], + }, + + max_file_length = { + type = 'number', + default = 40000, + description = [[ + Max file length (in lines) to attach to. + ]], + }, + + preview_config = { + type = 'table', + deep_extend = true, + default = { + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1, + }, + description = [[ + Option overrides for the Gitsigns preview window. Table is passed directly + to `nvim_open_win`. + ]], + }, + + attach_to_untracked = { + type = 'boolean', + default = true, + description = [[ + Attach to untracked files. + ]], + }, + + update_debounce = { + type = 'number', + default = 100, + description = [[ + Debounce time for updates (in milliseconds). + ]], + }, + + current_line_blame = { + type = 'boolean', + default = false, + description = [[ + Adds an unobtrusive and customisable blame annotation at the end of + the current line. + + The highlight group used for the text is `GitSignsCurrentLineBlame`. + ]], + }, + + current_line_blame_opts = { + type = 'table', + deep_extend = true, + default = { + virt_text = true, + virt_text_pos = 'eol', + virt_text_priority = 100, + delay = 1000, + }, + description = [[ + Options for the current line blame annotation. + + Fields: ~ + • virt_text: boolean + Whether to show a virtual text blame annotation. + • virt_text_pos: string + Blame annotation position. Available values: + `eol` Right after eol character. + `overlay` Display over the specified column, without + shifting the underlying text. + `right_align` Display right aligned in the window. + • delay: integer + Sets the delay (in milliseconds) before blame virtual text is + displayed. + • ignore_whitespace: boolean + Ignore whitespace when running blame. + • virt_text_priority: integer + Priority of virtual text. + ]], + }, + + current_line_blame_formatter_opts = { + type = 'table', + deep_extend = true, + deprecated = true, + default = { + relative_time = false, + }, + description = [[ + Options for the current line blame annotation formatter. + + Fields: ~ + • relative_time: boolean + ]], + }, + + current_line_blame_formatter = { + type = { 'string', 'function' }, + default = ' , - ', + description = [[ + String or function used to format the virtual text of + |gitsigns-config-current_line_blame|. + + When a string, accepts the following format specifiers: + + • `` + • `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` + + For `` and ``, `FORMAT` can + be any valid date format that is accepted by `os.date()` with the + addition of `%R` (defaults to `%Y-%m-%d`): + + • `%a` abbreviated weekday name (e.g., Wed) + • `%A` full weekday name (e.g., Wednesday) + • `%b` abbreviated month name (e.g., Sep) + • `%B` full month name (e.g., September) + • `%c` date and time (e.g., 09/16/98 23:48:10) + • `%d` day of the month (16) [01-31] + • `%H` hour, using a 24-hour clock (23) [00-23] + • `%I` hour, using a 12-hour clock (11) [01-12] + • `%M` minute (48) [00-59] + • `%m` month (09) [01-12] + • `%p` either "am" or "pm" (pm) + • `%S` second (10) [00-61] + • `%w` weekday (3) [0-6 = Sunday-Saturday] + • `%x` date (e.g., 09/16/98) + • `%X` time (e.g., 23:48:10) + • `%Y` full year (1998) + • `%y` two-digit year (98) [00-99] + • `%%` the character `%´ + • `%R` relative (e.g., 4 months ago) + + When a function: + Parameters: ~ + {name} Git user name returned from `git config user.name` . + {blame_info} Table with the following keys: + • `abbrev_sha`: string + • `orig_lnum`: integer + • `final_lnum`: integer + • `author`: string + • `author_mail`: string + • `author_time`: integer + • `author_tz`: string + • `committer`: string + • `committer_mail`: string + • `committer_time`: integer + • `committer_tz`: string + • `summary`: string + • `previous`: string + • `filename`: string + + Note that the keys map onto the output of: + `git blame --line-porcelain` + + {opts} Passed directly from + |gitsigns-config-current_line_blame_formatter_opts|. + + Return: ~ + The result of this function is passed directly to the `opts.virt_text` + field of |nvim_buf_set_extmark| and thus must be a list of + [text, highlight] tuples. + ]], + }, + + current_line_blame_formatter_nc = { + type = { 'string', 'function' }, + default = ' ', + description = [[ + String or function used to format the virtual text of + |gitsigns-config-current_line_blame| for lines that aren't committed. + + See |gitsigns-config-current_line_blame_formatter| for more information. + ]], + }, + + trouble = { + type = 'boolean', + default = function() + local has_trouble = pcall(require, 'trouble') + return has_trouble + end, + default_help = "true if installed", + description = [[ + When using setqflist() or setloclist(), open Trouble instead of the + quickfix/location list window. + ]], + }, + + yadm = { + type = 'table', + default = { enable = false }, + description = [[ + yadm configuration. + ]], + }, + + _git_version = { + type = 'string', + default = 'auto', + description = [[ + Version of git available. Set to 'auto' to automatically detect. + ]], + }, + + _verbose = { + type = 'boolean', + default = false, + description = [[ + More verbose debug message. Requires debug_mode=true. + ]], + }, + + word_diff = { + type = 'boolean', + default = false, + description = [[ + Highlight intra-line word differences in the buffer. + Requires `config.diff_opts.internal = true` . + + Uses the highlights: + • For word diff in previews: + • `GitSignsAddInline` + • `GitSignsChangeInline` + • `GitSignsDeleteInline` + • For word diff in buffer: + • `GitSignsAddLnInline` + • `GitSignsChangeLnInline` + • `GitSignsDeleteLnInline` + • For word diff in virtual lines (e.g. show_deleted): + • `GitSignsAddVirtLnInline` + • `GitSignsChangeVirtLnInline` + • `GitSignsDeleteVirtLnInline` + ]], + }, + + _refresh_staged_on_update = { + type = 'boolean', + default = false, + description = [[ + Always refresh the staged file on each update. Disabling this will cause + the staged file to only be refreshed when an update to the index is + detected. + ]], + }, + + _blame_cache = { + type = 'boolean', + default = true, + description = [[ + Cache blame results for current_line_blame + ]], + }, + + _threaded_diff = { + type = 'boolean', + default = false, + description = [[ + Run diffs on a separate thread + ]], + }, + + _extmark_signs = { + type = 'boolean', + default = false, + description = [[ + Use extmarks for placing signs. + ]], + }, + + debug_mode = { + type = 'boolean', + default = false, + description = [[ + Enables debug logging and makes the following functions + available: `dump_cache`, `debug_messages`, `clear_debug`. + ]], + }, + +} + +warn = function(s, ...) + vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) +end + +local function validate_config(config) + for k, v in pairs(config) do + local kschema = M.schema[k] + if kschema == nil then + warn("gitsigns: Ignoring invalid configuration field '%s'", k) + elseif kschema.type then + if type(kschema.type) == 'string' then + vim.validate({ + [k] = { v, kschema.type }, + }) + end + end + end +end + +local function resolve_default(v) + if type(v.default) == 'function' and v.type ~= 'function' then + return (v.default)() + else + return v.default + end +end + +local function handle_deprecated(cfg) + for k, v in pairs(M.schema) do + local dep = v.deprecated + if dep and cfg[k] ~= nil then + if type(dep) == "table" then + if dep.new_field then + local opts_key, field = dep.new_field:match('(.*)%.(.*)') + if opts_key and field then + + local opts = (cfg[opts_key] or {}) + opts[field] = cfg[k] + cfg[opts_key] = opts + else + + cfg[dep.new_field] = cfg[k] + end + end + + if dep.hard then + if dep.message then + warn(dep.message) + elseif dep.new_field then + warn('%s is now deprecated, please use %s', k, dep.new_field) + else + warn('%s is now deprecated; ignoring', k) + end + end + end + end + end +end + +function M.build(user_config) + user_config = user_config or {} + + handle_deprecated(user_config) + + validate_config(user_config) + + local config = M.config + for k, v in pairs(M.schema) do + if user_config[k] ~= nil then + if v.deep_extend then + local d = resolve_default(v) + config[k] = vim.tbl_deep_extend('force', d, user_config[k]) + else + config[k] = user_config[k] + end + else + config[k] = resolve_default(v) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/current_line_blame.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/current_line_blame.lua new file mode 100644 index 0000000..2488cc5 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/current_line_blame.lua @@ -0,0 +1,213 @@ +local a = require('gitsigns.async') +local wrap = a.wrap +local void = a.void +local scheduler = a.scheduler + +local cache = require('gitsigns.cache').cache +local config = require('gitsigns.config').config +local BlameInfo = require('gitsigns.git').BlameInfo +local util = require('gitsigns.util') +local uv = require('gitsigns.uv') + +local api = vim.api + +local current_buf = api.nvim_get_current_buf + +local namespace = api.nvim_create_namespace('gitsigns_blame') + +local timer = uv.new_timer(true) + +local M = {} + + + +local wait_timer = wrap(vim.loop.timer_start, 4) + +local function set_extmark(bufnr, row, opts) + opts = opts or {} + opts.id = 1 + api.nvim_buf_set_extmark(bufnr, namespace, row - 1, 0, opts) +end + +local function get_extmark(bufnr) + local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) + if pos[1] then + return pos[1] + 1 + end + return +end + +local function reset(bufnr) + bufnr = bufnr or current_buf() + api.nvim_buf_del_extmark(bufnr, namespace, 1) + vim.b[bufnr].gitsigns_blame_line_dict = nil +end + + +local max_cache_size = 1000 + +local BlameCache = {Elem = {}, } + + + + + + + + +BlameCache.contents = {} + +function BlameCache:add(bufnr, lnum, x) + if not config._blame_cache then return end + local scache = self.contents[bufnr] + if scache.size <= max_cache_size then + scache.cache[lnum] = x + scache.size = scache.size + 1 + end +end + +function BlameCache:get(bufnr, lnum) + if not config._blame_cache then return end + + + local tick = vim.b[bufnr].changedtick + if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then + self.contents[bufnr] = { tick = tick, cache = {}, size = 0 } + end + + return self.contents[bufnr].cache[lnum] +end + +local function expand_blame_format(fmt, name, info) + if info.author == name then + info.author = 'You' + end + return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time) +end + +local function flatten_virt_text(virt_text) + local res = {} + for _, part in ipairs(virt_text) do + res[#res + 1] = part[1] + end + return table.concat(res) +end + + +local update = void(function() + local bufnr = current_buf() + local lnum = api.nvim_win_get_cursor(0)[1] + + local old_lnum = get_extmark(bufnr) + if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then + + return + end + + if api.nvim_get_mode().mode == 'i' then + reset(bufnr) + return + end + + + + + + if get_extmark(bufnr) then + reset(bufnr) + set_extmark(bufnr, lnum) + end + + + if vim.fn.foldclosed(lnum) ~= -1 then + return + end + + local opts = config.current_line_blame_opts + + + wait_timer(timer, opts.delay, 0) + scheduler() + + local bcache = cache[bufnr] + if not bcache or not bcache.git_obj.object_name then + return + end + + local result = BlameCache:get(bufnr, lnum) + if not result then + local buftext = util.buf_lines(bufnr) + result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + BlameCache:add(bufnr, lnum, result) + scheduler() + end + + local lnum1 = api.nvim_win_get_cursor(0)[1] + if bufnr == current_buf() and lnum ~= lnum1 then + + return + end + + if not api.nvim_buf_is_loaded(bufnr) then + + return + end + + vim.b[bufnr].gitsigns_blame_line_dict = result + + if result then + local virt_text + local clb_formatter = result.author == 'Not Committed Yet' and + config.current_line_blame_formatter_nc or + config.current_line_blame_formatter + if type(clb_formatter) == "string" then + virt_text = { { + expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result), + 'GitSignsCurrentLineBlame', + }, } + else + virt_text = clb_formatter( + bcache.git_obj.repo.username, + result, + config.current_line_blame_formatter_opts) + + end + + vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text) + + if opts.virt_text then + set_extmark(bufnr, lnum, { + virt_text = virt_text, + virt_text_pos = opts.virt_text_pos, + priority = opts.virt_text_priority, + hl_mode = 'combine', + }) + end + end +end) + +M.setup = function() + api.nvim_create_augroup('gitsigns_blame', {}) + + for k, _ in pairs(cache) do + reset(k) + end + + if config.current_line_blame then + api.nvim_create_autocmd( + { 'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI' }, + { group = 'gitsigns_blame', callback = function() update() end }) + + + api.nvim_create_autocmd( + { 'InsertEnter', 'FocusLost', 'BufLeave' }, + { group = 'gitsigns_blame', callback = function() reset() end }) + + + + + vim.schedule(update) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debounce.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debounce.lua new file mode 100644 index 0000000..3b12249 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debounce.lua @@ -0,0 +1,84 @@ +local uv = require('gitsigns.uv') + +local M = {} + + + + + + + + + + + +function M.debounce_trailing(ms, fn) + local timer = uv.new_timer(true) + return function(...) + local argv = { ... } + timer:start(ms, 0, function() + timer:stop() + fn(unpack(argv)) + end) + end +end + + + + + + + +function M.throttle_leading(ms, fn) + local timer = uv.new_timer(true) + local running = false + return function(...) + if not running then + timer:start(ms, 0, function() + running = false + timer:stop() + end) + running = true + fn(...) + end + end +end + + + + + + + + + + + + + + + +function M.throttle_by_id(fn, schedule) + local scheduled = {} + local running = {} + return function(id, ...) + if scheduled[id] then + + return + end + if not running[id] or schedule then + scheduled[id] = true + end + if running[id] then + return + end + while scheduled[id] do + scheduled[id] = nil + running[id] = true + fn(id, ...) + running[id] = nil + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debug.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debug.lua new file mode 100644 index 0000000..d05bd75 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/debug.lua @@ -0,0 +1,150 @@ +local M = { + debug_mode = false, + verbose = false, + messages = {}, +} + +local function getvarvalue(name, lvl) + lvl = lvl + 1 + local value + local found + + + local i = 1 + while true do + local n, v = debug.getlocal(lvl, i) + if not n then break end + if n == name then + value = v + found = true + end + i = i + 1 + end + if found then return value end + + + local func = debug.getinfo(lvl).func + i = 1 + while true do + local n, v = debug.getupvalue(func, i) + if not n then break end + if n == name then return v end + i = i + 1 + end + + + return getfenv(func)[name] +end + +local function get_context(lvl) + lvl = lvl + 1 + local ret = {} + ret.name = getvarvalue('__FUNC__', lvl) + if not ret.name then + local name0 = debug.getinfo(lvl, 'n').name or '' + ret.name = name0:gsub('(.*)%d+$', '%1') + end + ret.bufnr = getvarvalue('bufnr', lvl) or + getvarvalue('_bufnr', lvl) or + getvarvalue('cbuf', lvl) or + getvarvalue('buf', lvl) + + return ret +end + + + +local function cprint(obj, lvl) + lvl = lvl + 1 + local msg = type(obj) == "string" and obj or vim.inspect(obj) + local ctx = get_context(lvl) + local msg2 + if ctx.bufnr then + msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg) + else + msg2 = string.format('%s: %s', ctx.name, msg) + end + table.insert(M.messages, msg2) +end + +function M.dprint(obj) + if not M.debug_mode then return end + cprint(obj, 2) +end + +function M.dprintf(obj, ...) + if not M.debug_mode then return end + cprint(obj:format(...), 2) +end + +function M.vprint(obj) + if not (M.debug_mode and M.verbose) then return end + cprint(obj, 2) +end + +function M.vprintf(obj, ...) + if not (M.debug_mode and M.verbose) then return end + cprint(obj:format(...), 2) +end + +local function eprint(msg, level) + local info = debug.getinfo(level + 2, 'Sl') + if info then + msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg) + end + M.messages[#M.messages + 1] = msg + if M.debug_mode then + error(msg) + end +end + +function M.eprint(msg) + eprint(msg, 1) +end + +function M.eprintf(fmt, ...) + eprint(fmt:format(...), 1) +end + +local function process(raw_item, path) + if path[#path] == vim.inspect.METATABLE then + return nil + elseif type(raw_item) == "function" then + return nil + elseif type(raw_item) == "table" then + local key = path[#path] + if key == 'compare_text' then + local item = raw_item + return { '...', length = #item, head = item[1] } + elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then + return { '...', length = #vim.tbl_keys(raw_item) } + end + end + return raw_item +end + +function M.add_debug_functions(cache) + local R = {} + R.dump_cache = function() + local text = vim.inspect(cache, { process = process }) + vim.api.nvim_echo({ { text } }, false, {}) + return cache + end + + R.debug_messages = function(noecho) + if not noecho then + for _, m in ipairs(M.messages) do + vim.api.nvim_echo({ { m } }, false, {}) + end + end + return M.messages + end + + R.clear_debug = function() + M.messages = {} + end + + return R +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff.lua new file mode 100644 index 0000000..68970c2 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff.lua @@ -0,0 +1,18 @@ +local config = require('gitsigns.config').config +local Hunk = require('gitsigns.hunks').Hunk + +return function(a, b, linematch) + local diff_opts = config.diff_opts + local f + if diff_opts.internal then + f = require('gitsigns.diff_int').run_diff + else + f = require('gitsigns.diff_ext').run_diff + end + + local linematch0 + if linematch ~= false then + linematch0 = diff_opts.linematch + end + return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0) +end diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_ext.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_ext.lua new file mode 100644 index 0000000..6b2466c --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_ext.lua @@ -0,0 +1,80 @@ +local git_diff = require('gitsigns.git').diff + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk +local util = require('gitsigns.util') +local scheduler = require('gitsigns.async').scheduler + +local M = {} + + + + +local function write_to_file(path, text) + local f, err = io.open(path, 'wb') + if f == nil then + error(err) + end + for _, l in ipairs(text) do + f:write(l) + f:write('\n') + end + f:close() +end + +M.run_diff = function( + text_cmp, + text_buf, + diff_algo, + indent_heuristic) + + local results = {} + + + if vim.in_fast_event() then + scheduler() + end + + local file_buf = util.tmpname() + local file_cmp = util.tmpname() + + write_to_file(file_buf, text_buf) + write_to_file(file_cmp, text_cmp) + + + + + + + + + + + + + + + + + + local out = git_diff(file_cmp, file_buf, indent_heuristic, diff_algo) + + for _, line in ipairs(out) do + if vim.startswith(line, '@@') then + results[#results + 1] = gs_hunks.parse_diff_line(line) + elseif #results > 0 then + local r = results[#results] + if line:sub(1, 1) == '-' then + r.removed.lines[#r.removed.lines + 1] = line:sub(2) + elseif line:sub(1, 1) == '+' then + r.added.lines[#r.added.lines + 1] = line:sub(2) + end + end + end + + os.remove(file_buf) + os.remove(file_cmp) + return results +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int.lua new file mode 100644 index 0000000..6590216 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int.lua @@ -0,0 +1,153 @@ +local create_hunk = require("gitsigns.hunks").create_hunk +local Hunk = require('gitsigns.hunks').Hunk +local config = require('gitsigns.config').config +local async = require('gitsigns.async') + +local M = {} + + + + + + + +local run_diff_xdl = function( + fa, fb, + algorithm, indent_heuristic, + linematch) + + + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' + + return vim.diff(a, b, { + result_type = 'indices', + algorithm = algorithm, + indent_heuristic = indent_heuristic, + linematch = linematch, + }) +end + +local run_diff_xdl_async = async.wrap(function( + fa, fb, + algorithm, indent_heuristic, + linematch, + callback) + + + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' + + vim.loop.new_work(function( + a0, b0, + algorithm0, indent_heuristic0, + linematch0) + + return vim.mpack.encode(vim.diff(a0, b0, { + result_type = 'indices', + algorithm = algorithm0, + indent_heuristic = indent_heuristic0, + linematch = linematch0, + })) + end, function(r) + callback(vim.mpack.decode(r)) + end):queue(a, b, algorithm, indent_heuristic, linematch) +end, 6) + +if not vim.diff then + run_diff_xdl = require('gitsigns.diff_int.xdl_diff_ffi') +end + +M.run_diff = async.void(function( + fa, fb, + diff_algo, indent_heuristic, + linematch) + + local run_diff0 + if config._threaded_diff and vim.is_thread then + run_diff0 = run_diff_xdl_async + else + run_diff0 = run_diff_xdl + end + + local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch) + + local hunks = {} + + for _, r in ipairs(results) do + local rs, rc, as, ac = unpack(r) + local hunk = create_hunk(rs, rc, as, ac) + if rc > 0 then + for i = rs, rs + rc - 1 do + hunk.removed.lines[#hunk.removed.lines + 1] = fa[i] or '' + end + end + if ac > 0 then + for i = as, as + ac - 1 do + hunk.added.lines[#hunk.added.lines + 1] = fb[i] or '' + end + end + hunks[#hunks + 1] = hunk + end + + return hunks +end) + + + +local gaps_between_regions = 5 + +local function denoise_hunks(hunks) + + local ret = { hunks[1] } + for j = 2, #hunks do + local h, n = ret[#ret], hunks[j] + if not h or not n then break end + if n.added.start - h.added.start - h.added.count < gaps_between_regions then + h.added.count = n.added.start + n.added.count - h.added.start + h.removed.count = n.removed.start + n.removed.count - h.removed.start + + if h.added.count > 0 or h.removed.count > 0 then + h.type = 'change' + end + else + ret[#ret + 1] = n + end + end + return ret +end + +function M.run_word_diff(removed, added) + local adds = {} + local rems = {} + + if #removed ~= #added then + return rems, adds + end + + for i = 1, #removed do + + local a, b = vim.split(removed[i], ''), vim.split(added[i], '') + + local hunks = {} + for _, r in ipairs(run_diff_xdl(a, b)) do + local rs, rc, as, ac = unpack(r) + + + if rc == 0 then rs = rs + 1 end + if ac == 0 then as = as + 1 end + + hunks[#hunks + 1] = create_hunk(rs, rc, as, ac) + end + + hunks = denoise_hunks(hunks) + + for _, h in ipairs(hunks) do + adds[#adds + 1] = { i, h.type, h.added.start, h.added.start + h.added.count } + rems[#rems + 1] = { i, h.type, h.removed.start, h.removed.start + h.removed.count } + end + end + return rems, adds +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int/xdl_diff_ffi.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int/xdl_diff_ffi.lua new file mode 100644 index 0000000..9a4f13b --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diff_int/xdl_diff_ffi.lua @@ -0,0 +1,145 @@ +local ffi = require("ffi") + +ffi.cdef([[ + typedef struct s_mmbuffer { const char *ptr; long size; } mmbuffer_t; + + typedef struct s_xpparam { + unsigned long flags; + + // See Documentation/diff-options.txt. + char **anchors; + size_t anchors_nr; + } xpparam_t; + + typedef long (__stdcall *find_func_t)( + const char *line, + long line_len, + char *buffer, + long buffer_size, + void *priv + ); + + typedef int (__stdcall *xdl_emit_hunk_consume_func_t)( + long start_a, long count_a, long start_b, long count_b, + void *cb_data + ); + + typedef struct s_xdemitconf { + long ctxlen; + long interhunkctxlen; + unsigned long flags; + find_func_t find_func; + void *find_func_priv; + xdl_emit_hunk_consume_func_t hunk_func; + } xdemitconf_t; + + typedef struct s_xdemitcb { + void *priv; + int (__stdcall *outf)(void *, mmbuffer_t *, int); + } xdemitcb_t; + + int xdl_diff( + mmbuffer_t *mf1, + mmbuffer_t *mf2, + xpparam_t const *xpp, + xdemitconf_t const *xecfg, + xdemitcb_t *ecb + ); +]]) + + + + + + + +local function setup_mmbuffer(lines) + local text = vim.tbl_isempty(lines) and '' or table.concat(lines, '\n') .. '\n' + return text, #text +end + + + + + + + + + +local function get_xpparam_flag(diff_algo) + local daflag = 0 + + if diff_algo == 'minimal' then daflag = 1 + elseif diff_algo == 'patience' then daflag = math.floor(2 ^ 14) + elseif diff_algo == 'histogram' then daflag = math.floor(2 ^ 15) + end + + return daflag +end + + + + + + + + + + + + + + + + + + + + + + + + + +local mmba = ffi.new('mmbuffer_t') +local mmbb = ffi.new('mmbuffer_t') +local xpparam = ffi.new('xpparam_t') +local emitcb = ffi.new('xdemitcb_t') + +local function run_diff_xdl(fa, fb, diff_algo) + mmba.ptr, mmba.size = setup_mmbuffer(fa) + mmbb.ptr, mmbb.size = setup_mmbuffer(fb) + xpparam.flags = get_xpparam_flag(diff_algo) + + local results = {} + + local hunk_func = ffi.cast('xdl_emit_hunk_consume_func_t', function( + start_a, count_a, start_b, count_b) + + local ca = tonumber(count_a) + local cb = tonumber(count_b) + local sa = tonumber(start_a) + local sb = tonumber(start_b) + + + + if ca > 0 then sa = sa + 1 end + if cb > 0 then sb = sb + 1 end + + results[#results + 1] = { sa, ca, sb, cb } + return 0 + end) + + local emitconf = ffi.new('xdemitconf_t') + emitconf.hunk_func = hunk_func + + local ok = ffi.C.xdl_diff(mmba, mmbb, xpparam, emitconf, emitcb) + + hunk_func:free() + + return ok == 0 and results +end + +jit.off(run_diff_xdl) + +return run_diff_xdl diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diffthis.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diffthis.lua new file mode 100644 index 0000000..fec417c --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/diffthis.lua @@ -0,0 +1,201 @@ +local api = vim.api + +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler +local awrap = require('gitsigns.async').wrap + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local util = require('gitsigns.util') +local manager = require('gitsigns.manager') +local message = require('gitsigns.message') + +local throttle_by_id = require('gitsigns.debounce').throttle_by_id + +local input = awrap(vim.ui.input, 2) + +local M = {DiffthisOpts = {}, } + + + + + + + + + + +local bufread = void(function(bufnr, dbufnr, base, bcache) + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local text + if util.calc_base(base) == util.calc_base(bcache.base) then + text = bcache.compare_text + else + local err + text, err = bcache.git_obj:get_show_text(comp_rev) + if err then + error(err, 2) + end + scheduler() + if vim.bo[bufnr].fileformat == 'dos' then + text = util.strip_cr(text) + end + end + + local modifiable = vim.bo[dbufnr].modifiable + vim.bo[dbufnr].modifiable = true + util.set_lines(dbufnr, 0, -1, text) + + vim.bo[dbufnr].modifiable = modifiable + vim.bo[dbufnr].modified = false + vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype + vim.bo[dbufnr].bufhidden = 'wipe' +end) + +local bufwrite = void(function(bufnr, dbufnr, base, bcache) + local buftext = util.buf_lines(dbufnr) + bcache.git_obj:stage_lines(buftext) + scheduler() + vim.bo[dbufnr].modified = false + + + if util.calc_base(base) == util.calc_base(bcache.base) then + bcache.compare_text = buftext + manager.update(bufnr, bcache) + end +end) + +local function run(base, diffthis, opts) + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + opts = opts or {} + + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local bufname = bcache:get_rev_bufname(comp_rev) + + local dbuf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(dbuf, bufname) + + local ok, err = pcall(bufread, bufnr, dbuf, base, bcache) + if not ok then + message.error(err) + scheduler() + vim.cmd('bdelete') + if diffthis then + vim.cmd('diffoff') + end + return + end + + if comp_rev == ':0' then + vim.bo[dbuf].buftype = 'acwrite' + + api.nvim_create_autocmd('BufReadCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufread(bufnr, dbuf, base, bcache) + if diffthis then + vim.cmd('diffthis') + end + end, + }) + + api.nvim_create_autocmd('BufWriteCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufwrite(bufnr, dbuf, base, bcache) + end, + }) + else + vim.bo[dbuf].buftype = 'nowrite' + vim.bo[dbuf].modifiable = false + end + + if diffthis then + vim.cmd(table.concat({ + 'keepalt', opts.split or 'aboveleft', + opts.vertical and 'vertical' or '', + 'diffsplit', bufname, + }, ' ')) + else + vim.cmd('edit ' .. bufname) + end +end + +M.diffthis = void(function(base, opts) + if vim.wo.diff then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + if not base and bcache.git_obj.has_conflicts then + local cwin = api.nvim_get_current_win() + run(':2', true, opts) + api.nvim_set_current_win(cwin) + opts.split = 'belowright' + run(':3', true, opts) + api.nvim_set_current_win(cwin) + else + run(base, true, opts) + end +end) + +M.show = void(function(base) + run(base, false) +end) + +local function should_reload(bufnr) + if not vim.bo[bufnr].modified then + return true + end + local response + while not vim.tbl_contains({ 'O', 'L' }, response) do + response = input({ + prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:', + }) + end + return response == 'L' +end + + +M.update = throttle_by_id(void(function(bufnr) + if not vim.wo.diff then + return + end + + local bcache = cache[bufnr] + + + + local bufname = bcache:get_rev_bufname() + + for _, w in ipairs(api.nvim_list_wins()) do + if api.nvim_win_is_valid(w) then + local b = api.nvim_win_get_buf(w) + local bname = api.nvim_buf_get_name(b) + if bname == bufname or vim.startswith(bname, 'fugitive://') then + if should_reload(b) then + api.nvim_buf_call(b, function() + vim.cmd('doautocmd BufReadCmd') + vim.cmd('diffthis') + end) + end + end + end + end +end)) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/git.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/git.lua new file mode 100644 index 0000000..454e9f9 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/git.lua @@ -0,0 +1,620 @@ +local wrap = require('gitsigns.async').wrap +local scheduler = require('gitsigns.async').scheduler + +local gsd = require("gitsigns.debug") +local util = require('gitsigns.util') +local subprocess = require('gitsigns.subprocess') + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk + +local uv = vim.loop +local startswith = vim.startswith + +local dprint = require("gitsigns.debug").dprint +local eprint = require("gitsigns.debug").eprint +local err = require('gitsigns.message').error + + + + + + + + + + + +local M = {BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = {}, Obj = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +local in_git_dir = function(file) + for _, p in ipairs(vim.split(file, util.path_sep)) do + if p == '.git' then + return true + end + end + return false +end + +local Obj = M.Obj +local Repo = M.Repo + +local function parse_version(version) + assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) + local ret = {} + local parts = vim.split(version, '%.') + ret.major = tonumber(parts[1]) + ret.minor = tonumber(parts[2]) + + if parts[3] == 'GIT' then + ret.patch = 0 + else + ret.patch = tonumber(parts[3]) + end + + return ret +end + + +local function check_version(version) + if not M.version then + return false + end + if M.version.major < version[1] then + return false + end + if version[2] and M.version.minor < version[2] then + return false + end + if version[3] and M.version.patch < version[3] then + return false + end + return true +end + + + +M.command = wrap(function(args, spec, callback) + spec = spec or {} + spec.command = spec.command or 'git' + spec.args = spec.command == 'git' and + { '--no-pager', '--literal-pathspecs', unpack(args) } or args + subprocess.run_job(spec, function(_, _, stdout, stderr) + if not spec.suppress_stderr then + if stderr then + gsd.eprint(stderr) + end + end + + local stdout_lines = vim.split(stdout or '', '\n', true) + + + + if stdout_lines[#stdout_lines] == '' then + stdout_lines[#stdout_lines] = nil + end + + if gsd.verbose then + gsd.vprintf('%d lines:', #stdout_lines) + for i = 1, math.min(10, #stdout_lines) do + gsd.vprintf('\t%s', stdout_lines[i]) + end + end + + callback(stdout_lines, stderr) + end) +end, 3) + +M.diff = function(file_cmp, file_buf, indent_heuristic, diff_algo) + return M.command({ + '-c', 'core.safecrlf=false', + 'diff', + '--color=never', + '--' .. (indent_heuristic and '' or 'no-') .. 'indent-heuristic', + '--diff-algorithm=' .. diff_algo, + '--patch-with-raw', + '--unified=0', + file_cmp, + file_buf, + }) + +end + +local function process_abbrev_head(gitdir, head_str, path, cmd) + if not gitdir then + return head_str + end + if head_str == 'HEAD' then + local short_sha = M.command({ 'rev-parse', '--short', 'HEAD' }, { + command = cmd or 'git', + suppress_stderr = true, + cwd = path, + })[1] or '' + if gsd.debug_mode and short_sha ~= '' then + short_sha = 'HEAD' + end + if util.path_exists(gitdir .. '/rebase-merge') or + util.path_exists(gitdir .. '/rebase-apply') then + return short_sha .. '(rebasing)' + end + return short_sha + end + return head_str +end + +local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 + +local cygpath_convert + +if has_cygpath then + cygpath_convert = function(path) + return M.command({ '-aw', path }, { command = 'cygpath' })[1] + end +end + +local function normalize_path(path) + if path and has_cygpath and not uv.fs_stat(path) then + + + path = cygpath_convert(path) + end + return path +end + +M.get_repo_info = function(path, cmd, gitdir, toplevel) + + + local has_abs_gd = check_version({ 2, 13 }) + local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' + + + + scheduler() + + local args = {} + + if gitdir then + vim.list_extend(args, { '--git-dir', gitdir }) + end + + if toplevel then + vim.list_extend(args, { '--work-tree', toplevel }) + end + + vim.list_extend(args, { + 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', + }) + + local results = M.command(args, { + command = cmd or 'git', + suppress_stderr = true, + cwd = path, + }) + + local ret = { + toplevel = normalize_path(results[1]), + gitdir = normalize_path(results[2]), + } + ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd) + if ret.gitdir and not has_abs_gd then + ret.gitdir = uv.fs_realpath(ret.gitdir) + end + ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel .. '/.git' + return ret +end + +M.set_version = function(version) + if version ~= 'auto' then + M.version = parse_version(version) + return + end + local results, stderr = M.command({ '--version' }) + local line = results[1] + if not line then + err("Unable to detect git version as 'git --version' failed to return anything") + eprint(stderr) + return + end + assert(type(line) == 'string', 'Unexpected output: ' .. line) + assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) +end + + + + + + +Repo.command = function(self, args, spec) + spec = spec or {} + spec.cwd = self.toplevel + + local args1 = { + '--git-dir', self.gitdir, + } + + if self.detached then + vim.list_extend(args1, { '--work-tree', self.toplevel }) + end + + vim.list_extend(args1, args) + + return M.command(args1, spec) +end + +Repo.files_changed = function(self) + local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) + + local ret = {} + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret + 1] = line:sub(4, -1) + end + end + return ret +end + + +Repo.get_show_text = function(self, object, encoding) + local stdout, stderr = self:command({ 'show', object }, { suppress_stderr = true }) + + if encoding ~= 'utf-8' then + scheduler() + for i, l in ipairs(stdout) do + + if vim.fn.type(l) == vim.v.t_string then + stdout[i] = vim.fn.iconv(l, encoding, 'utf-8') + end + end + end + + return stdout, stderr +end + +Repo.update_abbrev_head = function(self) + self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head +end + +Repo.new = function(dir, gitdir, toplevel) + local self = setmetatable({}, { __index = Repo }) + + self.username = M.command({ 'config', 'user.name' })[1] + local info = M.get_repo_info(dir, nil, gitdir, toplevel) + for k, v in pairs(info) do + (self)[k] = v + end + + + if M.enable_yadm and not self.gitdir then + if vim.startswith(dir, os.getenv('HOME')) and + #M.command({ 'ls-files', dir }, { command = 'yadm' }) ~= 0 then + M.get_repo_info(dir, 'yadm', gitdir, toplevel) + local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel) + for k, v in pairs(yadm_info) do + (self)[k] = v + end + end + end + + return self +end + + + + + + +Obj.command = function(self, args, spec) + return self.repo:command(args, spec) +end + +Obj.update_file_info = function(self, update_relpath, silent) + local old_object_name = self.object_name + local props = self:file_info(self.file, silent) + + if update_relpath then + self.relpath = props.relpath + end + self.object_name = props.object_name + self.mode_bits = props.mode_bits + self.has_conflicts = props.has_conflicts + self.i_crlf = props.i_crlf + self.w_crlf = props.w_crlf + + return old_object_name ~= self.object_name +end + +Obj.file_info = function(self, file, silent) + local results, stderr = self:command({ + '-c', 'core.quotepath=off', + 'ls-files', + '--stage', + '--others', + '--exclude-standard', + '--eol', + file or self.file, + }, { suppress_stderr = true }) + + if stderr and not silent then + + + if not stderr:match('^warning: could not open directory .*: No such file or directory') then + gsd.eprint(stderr) + end + end + + local result = {} + for _, line in ipairs(results) do + local parts = vim.split(line, '\t') + if #parts > 2 then + local eol = vim.split(parts[2], '%s+') + result.i_crlf = eol[1] == 'i/crlf' + result.w_crlf = eol[2] == 'w/crlf' + result.relpath = parts[3] + local attrs = vim.split(parts[1], '%s+') + local stage = tonumber(attrs[3]) + if stage <= 1 then + result.mode_bits = attrs[1] + result.object_name = attrs[2] + else + result.has_conflicts = true + end + else + result.relpath = parts[2] + end + end + return result +end + +Obj.get_show_text = function(self, revision) + if not self.relpath then + return {} + end + + local stdout, stderr = self.repo:get_show_text(revision .. ':' .. self.relpath, self.encoding) + + if not self.i_crlf and self.w_crlf then + + for i = 1, #stdout do + stdout[i] = stdout[i] .. '\r' + end + end + + return stdout, stderr +end + +Obj.unstage_file = function(self) + self:command({ 'reset', self.file }) +end + +Obj.run_blame = function(self, lines, lnum, ignore_whitespace) + if not self.object_name or self.repo.abbrev_head == '' then + + + + return { + author = 'Not Committed Yet', + ['author_mail'] = '', + committer = 'Not Committed Yet', + ['committer_mail'] = '', + } + end + + local args = { + 'blame', + '--contents', '-', + '-L', lnum .. ',+1', + '--line-porcelain', + self.file, + } + + if ignore_whitespace then + args[#args + 1] = '-w' + end + + local ignore_file = self.repo.toplevel .. '/.git-blame-ignore-revs' + if uv.fs_stat(ignore_file) then + vim.list_extend(args, { '--ignore-revs-file', ignore_file }) + end + + local results = self:command(args, { writer = lines }) + if #results == 0 then + return + end + local header = vim.split(table.remove(results, 1), ' ') + + local ret = {} + ret.sha = header[1] + ret.orig_lnum = tonumber(header[2]) + ret.final_lnum = tonumber(header[3]) + ret.abbrev_sha = string.sub(ret.sha, 1, 8) + for _, l in ipairs(results) do + if not startswith(l, '\t') then + local cols = vim.split(l, ' ') + local key = table.remove(cols, 1):gsub('-', '_') + ret[key] = table.concat(cols, ' ') + if key == 'previous' then + ret.previous_sha = cols[1] + ret.previous_filename = cols[2] + end + end + end + return ret +end + +Obj.ensure_file_in_index = function(self) + if not self.object_name or self.has_conflicts then + if not self.object_name then + + self:command({ 'add', '--intent-to-add', self.file }) + else + + + local info = string.format('%s,%s,%s', self.mode_bits, self.object_name, self.relpath) + self:command({ 'update-index', '--add', '--cacheinfo', info }) + end + + self:update_file_info() + end +end + +Obj.stage_lines = function(self, lines) + local stdout = self:command({ + 'hash-object', '-w', '--path', self.relpath, '--stdin', + }, { writer = lines }) + + local new_object = stdout[1] + + self:command({ + 'update-index', '--cacheinfo', string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath), + }) +end + +Obj.stage_hunks = function(self, hunks, invert) + self:ensure_file_in_index() + self:command({ + 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-', + }, { + writer = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert), + }) +end + +Obj.has_moved = function(self) + local out = self:command({ 'diff', '--name-status', '-C', '--cached' }) + local orig_relpath = self.orig_relpath or self.relpath + for _, l in ipairs(out) do + local parts = vim.split(l, '%s+') + if #parts == 3 then + local orig, new = parts[2], parts[3] + if orig_relpath == orig then + self.orig_relpath = orig_relpath + self.relpath = new + self.file = self.repo.toplevel .. '/' .. new + return new + end + end + end +end + +Obj.new = function(file, encoding, gitdir, toplevel) + if in_git_dir(file) then + dprint('In git dir') + return nil + end + local self = setmetatable({}, { __index = Obj }) + + self.file = file + self.encoding = encoding + self.repo = Repo.new(util.dirname(file), gitdir, toplevel) + + if not self.repo.gitdir then + dprint('Not in git repo') + return nil + end + + + local silent = gitdir ~= nil and toplevel ~= nil + + self:update_file_info(true, silent) + + return self +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/health.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/health.lua new file mode 100644 index 0000000..b42dda6 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/health.lua @@ -0,0 +1,17 @@ +local M = {} + +function M.check() + local fns = vim.fn + local report_ok = fns['health#report_ok'] + local report_error = fns['health#report_error'] + + local ok, v = pcall(vim.fn.systemlist, { 'git', '--version' }) + + if not ok then + report_error(v) + else + report_ok(v[1]) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/highlight.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/highlight.lua new file mode 100644 index 0000000..664825b --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/highlight.lua @@ -0,0 +1,74 @@ +local api = vim.api + +local dprintf = require("gitsigns.debug").dprintf + +local M = {} + + + + +local hls = { + { GitSignsAdd = { 'GitGutterAdd', 'SignifySignAdd', 'DiffAddedGutter', 'diffAdded', 'DiffAdd' } }, + { GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffModifiedGutter', 'diffChanged', 'DiffChange' } }, + { GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffRemovedGutter', 'diffRemoved', 'DiffDelete' } }, + + { GitSignsAddNr = { 'GitGutterAddLineNr', 'GitSignsAdd' } }, + { GitSignsChangeNr = { 'GitGutterChangeLineNr', 'GitSignsChange' } }, + { GitSignsDeleteNr = { 'GitGutterDeleteLineNr', 'GitSignsDelete' } }, + + { GitSignsAddLn = { 'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd' } }, + { GitSignsChangeLn = { 'GitGutterChangeLine', 'SignifyLineChange', 'DiffChange' } }, + + + + { GitSignsAddPreview = { 'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd' } }, + { GitSignsDeletePreview = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete' } }, + + { GitSignsCurrentLineBlame = { 'NonText' } }, + + { GitSignsAddInline = { 'TermCursor' } }, + { GitSignsDeleteInline = { 'TermCursor' } }, + { GitSignsChangeInline = { 'TermCursor' } }, + + { GitSignsAddLnInline = { 'GitSignsAddInline' } }, + { GitSignsChangeLnInline = { 'GitSignsChangeInline' } }, + { GitSignsDeleteLnInline = { 'GitSignsDeleteInline' } }, + + { GitSignsAddLnVirtLn = { 'GitSignsAddLn' } }, + { GitSignsChangeVirtLn = { 'GitSignsChangeLn' } }, + { GitSignsDeleteVirtLn = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete' } }, + + { GitSignsAddLnVirtLnInLine = { 'GitSignsAddLnInline' } }, + { GitSignsChangeVirtLnInLine = { 'GitSignsChangeLnInline' } }, + { GitSignsDeleteVirtLnInLine = { 'GitSignsDeleteLnInline' } }, +} + +local function is_hl_set(hl_name) + + local exists, hl = pcall(api.nvim_get_hl_by_name, hl_name, true) + local color = hl.foreground or hl.background or hl.reverse + return exists and color ~= nil +end + + + +M.setup_highlights = function() + for _, hlg in ipairs(hls) do + for hl, candidates in pairs(hlg) do + if is_hl_set(hl) then + + dprintf('Highlight %s is already defined', hl) + else + for _, d in ipairs(candidates) do + if is_hl_set(d) then + dprintf('Deriving %s from %s', hl, d) + api.nvim_set_hl(0, hl, { default = true, link = d }) + break + end + end + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/hunks.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/hunks.lua new file mode 100644 index 0000000..aca1bb7 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/hunks.lua @@ -0,0 +1,306 @@ +local Sign = require('gitsigns.signs').Sign +local StatusObj = require('gitsigns.status').StatusObj + +local util = require('gitsigns.util') + +local min, max = math.min, math.max + +local M = {Node = {}, Hunk = {}, Hunk_Public = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +local Hunk = M.Hunk + +function M.create_hunk(old_start, old_count, new_start, new_count) + return { + removed = { start = old_start, count = old_count, lines = {} }, + added = { start = new_start, count = new_count, lines = {} }, + head = ('@@ -%d%s +%d%s @@'):format( + old_start, old_count > 0 and ',' .. old_count or '', + new_start, new_count > 0 and ',' .. new_count or ''), + + vend = new_start + math.max(new_count - 1, 0), + type = new_count == 0 and 'delete' or + old_count == 0 and 'add' or + 'change', + } +end + +function M.create_partial_hunk(hunks, top, bot) + local pretop, precount = top, bot - top + 1 + for _, h in ipairs(hunks) do + local added_in_hunk = h.added.count - h.removed.count + + local added_in_range = 0 + if h.added.start >= top and h.vend <= bot then + + added_in_range = added_in_hunk + else + local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) + local added_above_top = max(0, top - (h.added.start + h.removed.count)) + + if h.added.start >= top and h.added.start <= bot then + + added_in_range = added_above_bot + elseif h.vend >= top and h.vend <= bot then + + added_in_range = added_in_hunk - added_above_top + pretop = pretop - added_above_top + elseif h.added.start <= top and h.vend >= bot then + + added_in_range = added_above_bot - added_above_top + pretop = pretop - added_above_top + end + + if top > h.vend then + pretop = pretop - added_in_hunk + end + end + + precount = precount - added_in_range + end + + if precount == 0 then + pretop = pretop - 1 + end + + return M.create_hunk(pretop, precount, top, bot - top + 1) +end + +function M.patch_lines(hunk, fileformat) + local lines = {} + for _, l in ipairs(hunk.removed.lines) do + lines[#lines + 1] = '-' .. l + end + for _, l in ipairs(hunk.added.lines) do + lines[#lines + 1] = '+' .. l + end + + if fileformat == 'dos' then + lines = util.strip_cr(lines) + end + return lines +end + +function M.parse_diff_line(line) + local diffkey = vim.trim(vim.split(line, '@@', true)[2]) + + + + local pre, now = unpack(vim.tbl_map(function(s) + return vim.split(string.sub(s, 2), ',') + end, vim.split(diffkey, ' '))) + + local hunk = M.create_hunk( + tonumber(pre[1]), (tonumber(pre[2]) or 1), + tonumber(now[1]), (tonumber(now[2]) or 1)) + + hunk.head = line + + return hunk +end + +local function change_end(hunk) + if hunk.added.count == 0 then + + return hunk.added.start + elseif hunk.removed.count == 0 then + + return hunk.added.start + hunk.added.count - 1 + else + + return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 + end +end + + +function M.calc_signs(hunk, min_lnum, max_lnum, untracked) + assert(not untracked or hunk.type == 'add') + min_lnum = min_lnum or 1 + max_lnum = max_lnum or math.huge + local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count + + if hunk.type == 'delete' and start == 0 then + if min_lnum <= 1 then + + return { { type = 'topdelete', count = removed, lnum = 1 } } + else + return {} + end + end + + local signs = {} + + local cend = change_end(hunk) + + for lnum = max(start, min_lnum), min(cend, max_lnum) do + local changedelete = hunk.type == 'change' and removed > added and lnum == cend + + signs[#signs + 1] = { + type = changedelete and 'changedelete' or + untracked and 'untracked' or hunk.type, + count = lnum == start and (hunk.type == 'add' and added or removed), + lnum = lnum, + } + end + + if hunk.type == "change" and added > removed and + hunk.vend >= min_lnum and cend <= max_lnum then + for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do + signs[#signs + 1] = { + type = 'add', + count = lnum == hunk.vend and (added - removed), + lnum = lnum, + } + end + end + + return signs +end + +function M.create_patch(relpath, hunks, mode_bits, invert) + invert = invert or false + + local results = { + string.format('diff --git a/%s b/%s', relpath, relpath), + 'index 000000..000000 ' .. mode_bits, + '--- a/' .. relpath, + '+++ b/' .. relpath, + } + + local offset = 0 + + for _, process_hunk in ipairs(hunks) do + local start, pre_count, now_count = + process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count + + if process_hunk.type == 'add' then + start = start + 1 + end + + local pre_lines = process_hunk.removed.lines + local now_lines = process_hunk.added.lines + + if invert then + pre_count, now_count = now_count, pre_count + pre_lines, now_lines = now_lines, pre_lines + end + + table.insert(results, string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count)) + for _, l in ipairs(pre_lines) do + results[#results + 1] = '-' .. l + end + for _, l in ipairs(now_lines) do + results[#results + 1] = '+' .. l + end + + process_hunk.removed.start = start + offset + offset = offset + (now_count - pre_count) + end + + return results +end + +function M.get_summary(hunks) + local status = { added = 0, changed = 0, removed = 0 } + + for _, hunk in ipairs(hunks or {}) do + if hunk.type == 'add' then + status.added = status.added + hunk.added.count + elseif hunk.type == 'delete' then + status.removed = status.removed + hunk.removed.count + elseif hunk.type == 'change' then + local add, remove = hunk.added.count, hunk.removed.count + local delta = min(add, remove) + status.changed = status.changed + delta + status.added = status.added + add - delta + status.removed = status.removed + remove - delta + end + end + + return status +end + +function M.find_hunk(lnum, hunks) + for i, hunk in ipairs(hunks) do + if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then + return hunk, i + end + + if hunk.added.start <= lnum and hunk.vend >= lnum then + return hunk, i + end + end +end + +function M.find_nearest_hunk(lnum, hunks, forwards, wrap) + local ret + local index + if forwards then + for i = 1, #hunks do + local hunk = hunks[i] + if hunk.added.start > lnum then + ret = hunk + index = i + break + end + end + else + for i = #hunks, 1, -1 do + local hunk = hunks[i] + if hunk.vend < lnum then + ret = hunk + index = i + break + end + end + end + if not ret and wrap then + index = forwards and 1 or #hunks + ret = hunks[index] + end + return ret, index +end + +function M.compare_heads(a, b) + if (a == nil) ~= (b == nil) then + return true + elseif a and #a ~= #b then + return true + end + for i, ah in ipairs(a or {}) do + if b[i].head ~= ah.head then + return true + end + end + return false +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/manager.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/manager.lua new file mode 100644 index 0000000..c9bd332 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/manager.lua @@ -0,0 +1,508 @@ +local void = require('gitsigns.async').void +local awrap = require('gitsigns.async').wrap +local scheduler = require('gitsigns.async').scheduler + +local gs_cache = require('gitsigns.cache') +local CacheEntry = gs_cache.CacheEntry +local cache = gs_cache.cache + +local Signs = require('gitsigns.signs') + +local Status = require("gitsigns.status") + +local debounce_trailing = require('gitsigns.debounce').debounce_trailing +local throttle_by_id = require('gitsigns.debounce').throttle_by_id +local gs_debug = require("gitsigns.debug") +local dprint = gs_debug.dprint +local dprintf = gs_debug.dprintf +local eprint = gs_debug.eprint +local subprocess = require('gitsigns.subprocess') +local util = require('gitsigns.util') +local run_diff = require('gitsigns.diff') +local git = require('gitsigns.git') +local uv = require('gitsigns.uv') + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk + +local config = require('gitsigns.config').config + +local api = vim.api + +local signs + +local M = {} + + + + + + + + + + +local schedule_if_buf_valid = function(buf, cb) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) then + cb() + end + end) +end + +local scheduler_if_buf_valid = awrap(schedule_if_buf_valid, 2) + +local function apply_win_signs(bufnr, hunks, top, bot, clear, untracked) + if clear then + signs:remove(bufnr) + end + + + hunks = hunks or {} + + + + + + if clear and hunks[1] then + signs:add(bufnr, gs_hunks.calc_signs(hunks[1], hunks[1].added.start, hunks[1].added.start, untracked)) + end + + for _, hunk in ipairs(hunks) do + if top <= hunk.vend and bot >= hunk.added.start then + signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked)) + end + if hunk.added.start > bot then + break + end + end +end + +M.on_lines = function(buf, first, last_orig, last_new) + local bcache = cache[buf] + if not bcache then + dprint('Cache for buffer was nil. Detaching') + return true + end + + signs:on_lines(buf, first, last_orig, last_new) + + + + if signs:contains(buf, first, last_new) then + + bcache.force_next_update = true + end + + M.update_debounced(buf, cache[buf]) +end + +local ns = api.nvim_create_namespace('gitsigns') + +local function apply_word_diff(bufnr, row) + + if vim.fn.foldclosed(row + 1) ~= -1 then + return + end + + if not cache[bufnr] or not cache[bufnr].hunks then + return + end + + local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] + if not line then + + return + end + + local lnum = row + 1 + + local hunk = gs_hunks.find_hunk(lnum, cache[bufnr].hunks) + if not hunk then + + return + end + + if hunk.added.count ~= hunk.removed.count then + + return + end + + local pos = lnum - hunk.added.start + 1 + + local added_line = hunk.added.lines[pos] + local removed_line = hunk.removed.lines[pos] + + local _, added_regions = require('gitsigns.diff_int').run_word_diff({ removed_line }, { added_line }) + + local cols = #line + + for _, region in ipairs(added_regions) do + local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 + if ecol == scol then + + ecol = scol + 1 + end + + local hl_group = rtype == 'add' and 'GitSignsAddLnInline' or + rtype == 'change' and 'GitSignsChangeLnInline' or + 'GitSignsDeleteLnInline' + + local opts = { + ephemeral = true, + priority = 1000, + } + + if ecol > cols and ecol == scol + 1 then + + opts.virt_text = { { ' ', hl_group } } + opts.virt_text_pos = 'overlay' + else + opts.end_col = ecol + opts.hl_group = hl_group + end + + api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) + api.nvim__buf_redraw_range(bufnr, row, row + 1) + end +end + +local ns_rm = api.nvim_create_namespace('gitsigns_removed') + +local VIRT_LINE_LEN = 300 + +local function clear_deleted(bufnr) + local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) + for _, mark in ipairs(marks) do + api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) + end +end + +function M.show_deleted(bufnr, nsd, hunk) + local virt_lines = {} + + for i, line in ipairs(hunk.removed.lines) do + local vline = {} + local last_ecol = 1 + + if config.word_diff then + local regions = require('gitsigns.diff_int').run_word_diff( + { hunk.removed.lines[i] }, { hunk.added.lines[i] }) + + for _, region in ipairs(regions) do + local rline, scol, ecol = region[1], region[3], region[4] + if rline > 1 then + break + end + vline[#vline + 1] = { line:sub(last_ecol, scol - 1), 'GitSignsDeleteVirtLn' } + vline[#vline + 1] = { line:sub(scol, ecol - 1), 'GitSignsDeleteVirtLnInline' } + last_ecol = ecol + end + end + + if #line > 0 then + vline[#vline + 1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn' } + end + + + local padding = string.rep(' ', VIRT_LINE_LEN - #line) + vline[#vline + 1] = { padding, 'GitSignsDeleteVirtLn' } + + virt_lines[i] = vline + end + + local topdelete = hunk.added.start == 0 and hunk.type == 'delete' + + local row = topdelete and 0 or hunk.added.start - 1 + api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { + virt_lines = virt_lines, + + virt_lines_above = hunk.type ~= 'delete' or topdelete, + }) +end + +function M.show_added(bufnr, nsw, hunk) + local start_row = hunk.added.start - 1 + + for offset = 0, hunk.added.count - 1 do + local row = start_row + offset + api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { + end_row = row + 1, + hl_group = 'GitSignsAddPreview', + hl_eol = true, + priority = 1000, + }) + end + + local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) + + for _, region in ipairs(added_regions) do + local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 + api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { + end_col = ecol, + hl_group = rtype == 'add' and 'GitSignsAddInline' or + rtype == 'change' and 'GitSignsChangeInline' or + 'GitSignsDeleteInline', + priority = 1001, + }) + end +end + +local function update_show_deleted(bufnr) + local bcache = cache[bufnr] + + clear_deleted(bufnr) + if config.show_deleted then + for _, hunk in ipairs(bcache.hunks) do + M.show_deleted(bufnr, ns_rm, hunk) + end + end +end + +local update_cnt = 0 + + + + + +M.update = throttle_by_id(function(bufnr, bcache) + local __FUNC__ = 'update' + bcache = bcache or cache[bufnr] + if not bcache then + eprint('Cache for buffer ' .. bufnr .. ' was nil') + return + end + + scheduler_if_buf_valid(bufnr) + local buftext = util.buf_lines(bufnr) + local git_obj = bcache.git_obj + + if not bcache.compare_text or config._refresh_staged_on_update then + bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev()) + end + + local old_hunks = bcache.hunks + bcache.hunks = run_diff(bcache.compare_text, buftext) + + scheduler_if_buf_valid(bufnr) + + + if bcache.force_next_update or gs_hunks.compare_heads(bcache.hunks, old_hunks) then + + + apply_win_signs(bufnr, bcache.hunks, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil) + + update_show_deleted(bufnr) + bcache.force_next_update = false + + api.nvim_exec_autocmds('User', { + pattern = 'GitSignsUpdate', + modeline = false, + }) + end + local summary = gs_hunks.get_summary(bcache.hunks) + summary.head = git_obj.repo.abbrev_head + Status:update(bufnr, summary) + + update_cnt = update_cnt + 1 + + dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt) +end, true) + +M.detach = function(bufnr, keep_signs) + if not keep_signs then + signs:remove(bufnr) + end +end + +local function handle_moved(bufnr, bcache, old_relpath) + local git_obj = bcache.git_obj + local do_update = false + + local new_name = git_obj:has_moved() + if new_name then + dprintf('File moved to %s', new_name) + git_obj.relpath = new_name + if not git_obj.orig_relpath then + git_obj.orig_relpath = old_relpath + end + do_update = true + elseif git_obj.orig_relpath then + local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath + if git_obj:file_info(orig_file).relpath then + dprintf('Moved file reset') + git_obj.relpath = git_obj.orig_relpath + git_obj.orig_relpath = nil + do_update = true + end + else + + end + + if do_update then + git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath + bcache.file = git_obj.file + git_obj:update_file_info() + scheduler() + + local bufexists = vim.fn.bufexists(bcache.file) == 1 + local old_name = api.nvim_buf_get_name(bufnr) + + if not bufexists then + util.buf_rename(bufnr, bcache.file) + end + + local msg = bufexists and 'Cannot rename' or 'Renamed' + dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) + end +end + + +M.watch_gitdir = function(bufnr, gitdir) + if not config.watch_gitdir.enable then + return + end + + dprintf('Watching git dir') + local w = uv.new_fs_poll(true) + w:start(gitdir, config.watch_gitdir.interval, void(function(err) + local __FUNC__ = 'watcher_cb' + if err then + dprintf('Git dir update error: %s', err) + return + end + dprint('Git dir update') + + local bcache = cache[bufnr] + + if not bcache then + + + + dprint('Has detached, aborting') + return + end + + local git_obj = bcache.git_obj + + git_obj.repo:update_abbrev_head() + + scheduler() + Status:update(bufnr, { head = git_obj.repo.abbrev_head }) + + local was_tracked = git_obj.object_name ~= nil + local old_relpath = git_obj.relpath + + if not git_obj:update_file_info() then + dprint('File not changed') + return + end + + if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then + + + handle_moved(bufnr, bcache, old_relpath) + end + + + bcache.compare_text = nil + + M.update(bufnr, bcache) + end)) + return w +end + +local cwd_watcher + +M.update_cwd_head = void(function() + if cwd_watcher then + cwd_watcher:stop() + else + cwd_watcher = uv.new_fs_poll(true) + end + + local cwd = vim.loop.cwd() + local gitdir, head + + + for _, bcache in pairs(cache) do + local repo = bcache.git_obj.repo + if repo.toplevel == cwd then + head = repo.abbrev_head + gitdir = repo.gitdir + break + end + end + + if not head or not gitdir then + local info = git.get_repo_info(cwd) + gitdir = info.gitdir + head = info.abbrev_head + end + + scheduler() + vim.g.gitsigns_head = head + + if not gitdir then + return + end + + local towatch = gitdir .. '/HEAD' + + if cwd_watcher:getpath() == towatch then + + return + end + + + cwd_watcher:start( + towatch, + config.watch_gitdir.interval, + void(function(err) + local __FUNC__ = 'cwd_watcher_cb' + if err then + dprintf('Git dir update error: %s', err) + return + end + dprint('Git cwd dir update') + + local new_head = git.get_repo_info(cwd).abbrev_head + scheduler() + vim.g.gitsigns_head = new_head + end)) + +end) + +M.reset_signs = function() + signs:reset() +end + +M.setup = function() + + + api.nvim_set_decoration_provider(ns, { + on_win = function(_, _, bufnr, topline, botline_guess) + local bcache = cache[bufnr] + if not bcache or not bcache.hunks then + return false + end + local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) + + local untracked = bcache.git_obj.object_name == nil + + apply_win_signs(bufnr, bcache.hunks, topline + 1, botline + 1, false, untracked) + + if not (config.word_diff and config.diff_opts.internal) then + return false + end + end, + on_line = function(_, _winid, bufnr, row) + apply_word_diff(bufnr, row) + end, + }) + + signs = Signs.new(config.signs) + M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/mappings.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/mappings.lua new file mode 100644 index 0000000..dbfe573 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/mappings.lua @@ -0,0 +1,87 @@ + + + +local validate = vim.validate +local api = vim.api + +local valid_modes = { + n = 'n', v = 'v', x = 'x', i = 'i', o = 'o', t = 't', c = 'c', s = 's', + + ['!'] = '!', [' '] = '', +} + +local valid_options = { + buffer = 'boolean', + expr = 'boolean', + noremap = 'boolean', + nowait = 'boolean', + script = 'boolean', + silent = 'boolean', + unique = 'boolean', +} + +local function validate_option_keywords(options) + for option_name, expected_type in pairs(valid_options) do + local value = options[option_name] + if value then + validate({ + [option_name] = { value, expected_type }, + }) + end + end +end + +local function apply_mappings(mappings, bufnr) + validate({ + mappings = { mappings, 'table' }, + }) + + local default_options = {} + for key, val in pairs(mappings) do + + if valid_options[key] then + default_options[key] = val + end + end + + for key, opts in pairs(mappings) do + repeat + + if valid_options[key] then + break + end + + local rhs + local options + if type(opts) == "string" then + rhs = opts + options = {} + elseif type(opts) == "table" then + rhs = opts[1] + local boptions = {} + for k in pairs(valid_options) do + boptions[k] = opts[k] + end + options = boptions + else + error(("Invalid type for option rhs: %q = %s"):format(type(opts), vim.inspect(opts))) + end + options = vim.tbl_extend('keep', default_options, options) + + validate_option_keywords(options) + + local mode, mapping = key:match("^(.)[ ]*(.+)$") + + if not mode or not valid_modes[mode] then + error("Invalid mode specified for keymapping. mode=" .. mode) + end + + + options.buffer = nil + + api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) + until true + end +end + +return apply_mappings diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/message.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/message.lua new file mode 100644 index 0000000..3104441 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/message.lua @@ -0,0 +1,16 @@ + + +local M = {} + + + + +M.warn = vim.schedule_wrap(function(s, ...) + vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) +end) + +M.error = vim.schedule_wrap(function(s, ...) + vim.notify(s:format(...), vim.log.levels.ERROR, { title = 'gitsigns' }) +end) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/popup.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/popup.lua new file mode 100644 index 0000000..4bd74a4 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/popup.lua @@ -0,0 +1,217 @@ +local popup = {HlMark = {}, } + + + + + + + + + + +local HlMark = popup.HlMark + +local api = vim.api + +local function bufnr_calc_width(bufnr, lines) + return api.nvim_buf_call(bufnr, function() + local width = 0 + for _, l in ipairs(lines) do + if vim.fn.type(l) == vim.v.t_string then + local len = vim.fn.strdisplaywidth(l) + if len > width then + width = len + end + end + end + return width + 1 + end) +end + + +local function expand_height(winid, nlines) + local newheight = 0 + for _ = 0, 50 do + local winheight = api.nvim_win_get_height(winid) + if newheight > winheight then + + break + end + local wd = api.nvim_win_call(winid, function() + return vim.fn.line('w$') + end) + if wd >= nlines then + break + end + newheight = winheight + nlines - wd + api.nvim_win_set_height(winid, newheight) + end +end + +local function offset_hlmarks(hlmarks, row_offset) + for _, h in ipairs(hlmarks) do + if h.start_row then + h.start_row = h.start_row + row_offset + end + if h.end_row then + h.end_row = h.end_row + row_offset + end + end +end + +local function process_linesspec(fmt) + local lines = {} + local hls = {} + + local row = 0 + for _, section in ipairs(fmt) do + local sec = {} + local pos = 0 + for _, part in ipairs(section) do + local text = part[1] + local hl = part[2] + + sec[#sec + 1] = text + + local srow = row + local scol = pos + + local ts = vim.split(text, '\n') + + if #ts > 1 then + pos = 0 + row = row + #ts - 1 + else + pos = pos + #text + end + + if type(hl) == "string" then + hls[#hls + 1] = { + hl_group = hl, + start_row = srow, + end_row = row, + start_col = scol, + end_col = pos, + } + else + offset_hlmarks(hl, srow) + vim.list_extend(hls, hl) + end + end + for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do + lines[#lines + 1] = l + end + row = row + 1 + end + + return lines, hls +end + +local function close_all_but(id) + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview ~= nil and + vim.w[winid].gitsigns_preview ~= id then + pcall(api.nvim_win_close, winid, true) + end + end +end + +function popup.create0(lines, opts, id) + + close_all_but(id) + + local ts = vim.bo.tabstop + local bufnr = api.nvim_create_buf(false, true) + assert(bufnr, "Failed to create buffer") + + + vim.bo[bufnr].modifiable = true + api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + vim.bo[bufnr].modifiable = false + + + + vim.bo[bufnr].tabstop = ts + + local opts1 = vim.deepcopy(opts or {}) + opts1.height = opts1.height or #lines + opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) + + local winid = api.nvim_open_win(bufnr, false, opts1) + + vim.w[winid].gitsigns_preview = id or true + + if not opts.height then + expand_height(winid, #lines) + end + + if opts1.style == 'minimal' then + + + vim.wo[winid].signcolumn = 'no' + end + + + + local group = 'gitsigns_popup' + api.nvim_create_augroup(group, {}) + local old_cursor = api.nvim_win_get_cursor(0) + + api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { + group = group, + callback = function() + local cursor = api.nvim_win_get_cursor(0) + + if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) and + api.nvim_get_current_win() ~= winid then + + api.nvim_create_augroup(group, {}) + pcall(api.nvim_win_close, winid, true) + return + end + old_cursor = cursor + end, + }) + + return winid, bufnr +end + +local ns = api.nvim_create_namespace('gitsigns_popup') + +function popup.create(lines_spec, opts, id) + local lines, highlights = process_linesspec(lines_spec) + local winid, bufnr = popup.create0(lines, opts, id) + + for _, hl in ipairs(highlights) do + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { + hl_group = hl.hl_group, + end_row = hl.end_row, + end_col = hl.end_col, + hl_eol = true, + }) + if not ok then + error(vim.inspect(hl) .. '\n' .. err) + end + end + + return winid, bufnr +end + +function popup.is_open(id) + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview == id then + return winid + end + end + return nil +end + +function popup.focus_open(id) + local winid = popup.is_open(id) + if winid then + api.nvim_set_current_win(winid) + end + return winid +end + +return popup diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/repeat.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/repeat.lua new file mode 100644 index 0000000..2f83be8 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/repeat.lua @@ -0,0 +1,30 @@ +local api = vim.api + +local M = {} + + + + + + +function M.mk_repeatable(fn) + return function(...) + local args = { ... } + local nargs = select('#', ...) + vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" + + M.repeat_action = function() + fn(unpack(args, 1, nargs)) + if vim.fn.exists('*repeat#set') == 1 then + local action = api.nvim_replace_termcodes( + string.format('call %s()', vim.go.operatorfunc), + true, true, true) + vim.fn['repeat#set'](action, -1) + end + end + + vim.cmd('normal! g@l') + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs.lua new file mode 100644 index 0000000..9a58319 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs.lua @@ -0,0 +1,30 @@ +local config = require('gitsigns.config').config +local SignsConfig = require('gitsigns.config').Config.SignsConfig + +local dprint = require('gitsigns.debug').dprint + +local B = require('gitsigns.signs.base') + +local M = {} + +local function init() + local __FUNC__ = 'signs.init' + if config._extmark_signs then + dprint('Using extmark signs') + M = require('gitsigns.signs.extmarks') + else + dprint('Using vimfn signs') + M = require('gitsigns.signs.vimfn') + end +end + +function M.new(cfg, name) + init() + return M.new(cfg, name) +end + +return setmetatable(B, { + __index = function(_, k) + return rawget(M, k) + end, +}) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/base.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/base.lua new file mode 100644 index 0000000..2ee785d --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/base.lua @@ -0,0 +1,36 @@ +local SignsConfig = require('gitsigns.config').Config.SignsConfig + +local M = {Sign = {}, } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/extmarks.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/extmarks.lua new file mode 100644 index 0000000..ec18315 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/extmarks.lua @@ -0,0 +1,88 @@ +local api = vim.api + +local SignsConfig = require('gitsigns.config').Config.SignsConfig +local config = require('gitsigns.config').config + +local B = require('gitsigns.signs.base') + +local M = {} + +local group_base = 'gitsigns_extmark_signs_' + +function M.new(cfg, name) + local self = setmetatable({}, { __index = M }) + self.config = cfg + self.group = group_base .. (name or '') + self.ns = api.nvim_create_namespace(self.group) + return self +end + +function M:on_lines(buf, _, last_orig, last_new) + + + if last_orig > last_new then + self:remove(buf, last_new + 1, last_orig) + end +end + +function M:remove(bufnr, start_lnum, end_lnum) + if start_lnum then + api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum - 1, end_lnum or start_lnum) + else + api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) + end +end + +function M:add(bufnr, signs) + if not config.signcolumn and not config.numhl and not config.linehl then + + return + end + + local cfg = self.config + + for _, s in ipairs(signs) do + if not self:contains(bufnr, s.lnum) then + local cs = cfg[s.type] + local text = cs.text + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_char = cc[count] or cc['+'] or '' + text = cs.text .. count_char + end + + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum - 1, -1, { + id = s.lnum, + sign_text = config.signcolumn and text or '', + priority = config.sign_priority, + sign_hl_group = cs.hl, + number_hl_group = config.numhl and cs.numhl or nil, + line_hl_group = config.linehl and cs.linehl or nil, + }) + + if not ok and config.debug_mode then + vim.schedule(function() + error(table.concat({ + string.format('Error placing extmark on line %d', s.lnum), + err, + }, '\n')) + end) + end + end + end +end + +function M:contains(bufnr, start, last) + local marks = api.nvim_buf_get_extmarks( + bufnr, self.ns, { start - 1, 0 }, { last or start, 0 }, { limit = 1 }) + return #marks > 0 +end + +function M:reset() + for _, buf in ipairs(api.nvim_list_bufs()) do + self:remove(buf) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/vimfn.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/vimfn.lua new file mode 100644 index 0000000..758ee54 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/signs/vimfn.lua @@ -0,0 +1,159 @@ +local fn = vim.fn + +local SignsConfig = require('gitsigns.config').Config.SignsConfig +local config = require('gitsigns.config').config + +local emptytable = require('gitsigns.util').emptytable + +local B = require('gitsigns.signs.base') + +local M = {} + + + + + + + + + +local function capitalise_word(x) + return x:sub(1, 1):upper() .. x:sub(2) +end + +local sign_define_cache = {} +local sign_name_cache = {} + +local function get_sign_name(stype) + if not sign_name_cache[stype] then + sign_name_cache[stype] = string.format( + '%s%s', 'GitSigns', capitalise_word(stype)) + end + + return sign_name_cache[stype] +end + +local function sign_get(name) + if not sign_define_cache[name] then + local s = fn.sign_getdefined(name) + if not vim.tbl_isempty(s) then + sign_define_cache[name] = s + end + end + return sign_define_cache[name] +end + +local function define_sign(name, opts, redefine) + if redefine then + sign_define_cache[name] = nil + fn.sign_undefine(name) + fn.sign_define(name, opts) + elseif not sign_get(name) then + fn.sign_define(name, opts) + end +end + +local function define_signs(obj, redefine) + + for stype, cs in pairs(obj.config) do + define_sign(get_sign_name(stype), { + texthl = cs.hl, + text = config.signcolumn and cs.text or nil, + numhl = config.numhl and cs.numhl or nil, + linehl = config.linehl and cs.linehl or nil, + }, redefine) + end +end + +local group_base = 'gitsigns_vimfn_signs_' + +function M.new(cfg, name) + local self = setmetatable({}, { __index = M }) + self.group = group_base .. (name or '') + self.config = cfg + self.placed = emptytable() + + define_signs(self, false) + + return self +end + +function M:on_lines(_, _, _, _) +end + +function M:remove(bufnr, start_lnum, end_lnum) + end_lnum = end_lnum or start_lnum + + if start_lnum then + for lnum = start_lnum, end_lnum do + self.placed[bufnr][lnum] = nil + fn.sign_unplace(self.group, { buffer = bufnr, id = lnum }) + end + else + self.placed[bufnr] = nil + fn.sign_unplace(self.group, { buffer = bufnr }) + end +end + +function M:add(bufnr, signs) + if not config.signcolumn and not config.numhl and not config.linehl then + + return + end + + local to_place = {} + + local cfg = self.config + for _, s in ipairs(signs) do + local sign_name = get_sign_name(s.type) + + local cs = cfg[s.type] + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or '' + local count_char = cc[count] or cc['+'] or '' + sign_name = sign_name .. count_suffix + define_sign(sign_name, { + texthl = cs.hl, + text = config.signcolumn and cs.text .. count_char or '', + numhl = config.numhl and cs.numhl or nil, + linehl = config.linehl and cs.linehl or nil, + }) + end + + if not self.placed[bufnr][s.lnum] then + local sign = { + id = s.lnum, + group = self.group, + name = sign_name, + buffer = bufnr, + lnum = s.lnum, + priority = config.sign_priority, + } + self.placed[bufnr][s.lnum] = s + to_place[#to_place + 1] = sign + end + end + + if #to_place > 0 then + fn.sign_placelist(to_place) + end +end + +function M:contains(bufnr, start, last) + for i = start + 1, last + 1 do + if self.placed[bufnr][i] then + return true + end + end + return false +end + +function M:reset() + self.placed = emptytable() + fn.sign_unplace(self.group) + define_signs(self, true) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/status.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/status.lua new file mode 100644 index 0000000..f4f43de --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/status.lua @@ -0,0 +1,43 @@ +local api = vim.api + +local StatusObj = {} + + + + + + + + +local Status = { + StatusObj = StatusObj, + formatter = nil, +} + +function Status:update(bufnr, status) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + local bstatus = vim.b[bufnr].gitsigns_status_dict + if bstatus then + status = vim.tbl_extend('force', bstatus, status) + end + vim.b[bufnr].gitsigns_head = status.head or '' + vim.b[bufnr].gitsigns_status_dict = status + vim.b[bufnr].gitsigns_status = self.formatter(status) +end + +function Status:clear(bufnr) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + vim.b[bufnr].gitsigns_head = nil + vim.b[bufnr].gitsigns_status_dict = nil + vim.b[bufnr].gitsigns_status = nil +end + +function Status:clear_diff(bufnr) + self:update(bufnr, { added = 0, removed = 0, changed = 0 }) +end + +return Status diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/subprocess.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/subprocess.lua new file mode 100644 index 0000000..4c9b83a --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/subprocess.lua @@ -0,0 +1,117 @@ +local gsd = require("gitsigns.debug") +local guv = require("gitsigns.uv") +local uv = vim.loop + +local M = {JobSpec = {State = {}, }, } + + + + + + + + + + + + + + + + + + + + + + + +M.job_cnt = 0 + +local function try_close(pipe) + if pipe and not pipe:is_closing() then + pipe:close() + end +end + +function M.run_job(obj, callback) + if gsd.debug_mode then + local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ') + gsd.dprint(cmd) + end + + obj._state = {} + local s = obj._state + s.stdout_data = {} + s.stderr_data = {} + + s.stdout = guv.new_pipe(false) + s.stderr = guv.new_pipe(false) + if obj.writer then + s.stdin = guv.new_pipe(false) + end + + s.handle, s.pid = guv.spawn(obj.command, { + args = obj.args, + stdio = { s.stdin, s.stdout, s.stderr }, + cwd = obj.cwd, + }, + function(code, signal) + s.handle:close() + s.code = code + s.signal = signal + + if s.stdout then s.stdout:read_stop() end + if s.stderr then s.stderr:read_stop() end + + try_close(s.stdin) + try_close(s.stdout) + try_close(s.stderr) + + local stdout_result = #s.stdout_data > 0 and table.concat(s.stdout_data) or nil + local stderr_result = #s.stderr_data > 0 and table.concat(s.stderr_data) or nil + + callback(code, signal, stdout_result, stderr_result) + end) + + + if not s.handle then + try_close(s.stdin) + try_close(s.stdout) + try_close(s.stderr) + error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) + end + + s.stdout:read_start(function(_, data) + s.stdout_data[#s.stdout_data + 1] = data + end) + + s.stderr:read_start(function(_, data) + s.stderr_data[#s.stderr_data + 1] = data + end) + + local writer = obj.writer + if type(writer) == "table" then + local writer_len = #writer + for i, v in ipairs(writer) do + s.stdin:write(v) + if i ~= writer_len then + s.stdin:write("\n") + else + s.stdin:write("\n", function() + try_close(s.stdin) + end) + end + end + elseif writer then + + s.stdin:write(writer, function() + try_close(s.stdin) + end) + end + + M.job_cnt = M.job_cnt + 1 + return obj +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/test.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/test.lua new file mode 100644 index 0000000..120c669 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/test.lua @@ -0,0 +1,43 @@ + +local M = {} + + + +local function eq(act, exp) + assert(act == exp, string.format('%s != %s', act, exp)) +end + +M._tests = {} + +M._tests.expand_format = function() + local util = require('gitsigns.util') + assert('hello % world % 2021' == util.expand_format(' % % ', { + var1 = 'hello', var2 = 'world', var_time = 1616838297, })) +end + + +M._tests.test_args = function() + local parse_args = require('gitsigns.argparse').parse_args + + local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') + + eq(pos_args[1], 'hello') + eq(pos_args[2], 'there') + eq(named_args.key, 'value,') + eq(named_args.key1, 'a b c') + + pos_args, named_args = parse_args('base=HEAD~1 posarg') + + eq(named_args.base, 'HEAD~1') + eq(pos_args[1], 'posarg') +end + +M._tests.test_name_parse = function() + local gs = require('gitsigns') + local path, commit = gs.parse_fugitive_uri( + 'fugitive:///home/path/to/project/.git//1b441b947c4bc9a59db428f229456619051dd133/subfolder/to/a/file.txt') + eq(path, '/home/path/to/project/subfolder/to/a/file.txt') + eq(commit, '1b441b947c4bc9a59db428f229456619051dd133') +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/util.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/util.lua new file mode 100644 index 0000000..c9a1e0c --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/util.lua @@ -0,0 +1,199 @@ +local M = {} + + + + + +function M.path_exists(path) + return vim.loop.fs_stat(path) and true or false +end + +local jit_os + +if jit then + jit_os = jit.os:lower() +end + +local is_unix = false +if jit_os then + is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' +else + local binfmt = package.cpath:match("%p[\\|/]?%p(%a+)") + is_unix = binfmt ~= "dll" +end + +function M.dirname(file) + return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) +end + +function M.file_lines(file) + local text = {} + for line in io.lines(file) do + text[#text + 1] = line + end + return text +end + +M.path_sep = package.config:sub(1, 1) + +function M.buf_lines(bufnr) + + local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #buftext do + buftext[i] = buftext[i] .. '\r' + end + end + return buftext +end + +local function delete_alt(buf) + local alt = vim.api.nvim_buf_call(buf, function() + return vim.fn.bufnr('#') + end) + if alt ~= buf and alt ~= -1 then + pcall(vim.api.nvim_buf_delete, alt, { force = true }) + end +end + +function M.buf_rename(bufnr, name) + vim.api.nvim_buf_set_name(bufnr, name) + delete_alt(bufnr) +end + +function M.set_lines(bufnr, start_row, end_row, lines) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #lines do + lines[i] = lines[i]:gsub('\r$', '') + end + end + vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) +end + +function M.tmpname() + if is_unix then + return os.tmpname() + end + return vim.fn.tempname() +end + +function M.get_relative_time(timestamp) + local current_timestamp = os.time() + local elapsed = current_timestamp - timestamp + + if elapsed == 0 then + return 'a while ago' + end + + local minute_seconds = 60 + local hour_seconds = minute_seconds * 60 + local day_seconds = hour_seconds * 24 + local month_seconds = day_seconds * 30 + local year_seconds = month_seconds * 12 + + local to_relative_string = function(time, divisor, time_word) + local num = math.floor(time / divisor) + if num > 1 then + time_word = time_word .. 's' + end + + return num .. ' ' .. time_word .. ' ago' + end + + if elapsed < minute_seconds then + return to_relative_string(elapsed, 1, 'second') + elseif elapsed < hour_seconds then + return to_relative_string(elapsed, minute_seconds, 'minute') + elseif elapsed < day_seconds then + return to_relative_string(elapsed, hour_seconds, 'hour') + elseif elapsed < month_seconds then + return to_relative_string(elapsed, day_seconds, 'day') + elseif elapsed < year_seconds then + return to_relative_string(elapsed, month_seconds, 'month') + else + return to_relative_string(elapsed, year_seconds, 'year') + end +end + +function M.copy_array(x) + local r = {} + for i, e in ipairs(x) do + r[i] = e + end + return r +end + + +function M.strip_cr(xs0) + for i = 1, #xs0 do + if xs0[i]:sub(-1) ~= '\r' then + + return xs0 + end + end + + local xs = vim.deepcopy(xs0) + for i = 1, #xs do + xs[i] = xs[i]:sub(1, -2) + end + return xs +end + +function M.calc_base(base) + if base and base:sub(1, 1):match('[~\\^]') then + base = 'HEAD' .. base + end + return base +end + +function M.emptytable() + return setmetatable({}, { + __index = function(t, k) + t[k] = {} + return t[k] + end, + }) +end + +local function expand_date(fmt, time) + if fmt == '%R' then + return M.get_relative_time(time) + end + return os.date(fmt, time) +end + + +function M.expand_format(fmt, info, reltime) + local ret = {} + + for _ = 1, 20 do + + local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') + if not match then + break + end + + ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1) + + local v = info[key] + + if v then + if type(v) == "table" then + v = table.concat(v, '\n') + end + if vim.endswith(key, '_time') then + if time_fmt == '' then + time_fmt = reltime and '%R' or '%Y-%m-%d' + end + v = expand_date(time_fmt, v) + end + match = tostring(v) + end + ret[#ret + 1] = match + end + + ret[#ret + 1] = fmt + return table.concat(ret, '') +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/uv.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/uv.lua new file mode 100644 index 0000000..b2aebb5 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/lua/gitsigns/uv.lua @@ -0,0 +1,63 @@ +local uv = vim.loop + +local M = {} + + + +local handles = {} + +M.handles = handles + +function M.print_handles() + local none = true + for _, e in pairs(handles) do + local handle, longlived, tr = unpack(e) + if handle and not longlived and not handle:is_closing() then + print('') + print(tr) + none = false + end + end + if none then + print('No active handles') + end +end + +vim.api.nvim_create_autocmd('VimLeavePre', { + callback = function() + for _, e in pairs(handles) do + local handle = e[1] + if handle and not handle:is_closing() then + handle:close() + end + end + end, +}) + +function M.new_timer(longlived) + local r = uv.new_timer() + handles[#handles + 1] = { r, longlived, debug.traceback() } + return r +end + +function M.new_fs_poll(longlived) + local r = uv.new_fs_poll() + handles[#handles + 1] = { r, longlived, debug.traceback() } + return r +end + +function M.new_pipe(ipc) + local r = uv.new_pipe(ipc) + handles[#handles + 1] = { r, false, debug.traceback() } + return r +end + +function M.spawn(cmd, opts, on_exit) + local handle, pid = uv.spawn(cmd, opts, on_exit) + if handle then + handles[#handles + 1] = { handle, false, cmd .. ' ' .. vim.inspect(opts) } + end + return handle, pid +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns.tl new file mode 100644 index 0000000..e6fa1b4 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns.tl @@ -0,0 +1,546 @@ +local async = require('gitsigns.async') +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler + +local Status = require("gitsigns.status") +local git = require('gitsigns.git') +local manager = require('gitsigns.manager') +local util = require('gitsigns.util') +local hl = require('gitsigns.highlight') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local gs_config = require('gitsigns.config') +local Config = gs_config.Config +local config = gs_config.config + +local gs_debug = require("gitsigns.debug") +local dprintf = gs_debug.dprintf +local dprint = gs_debug.dprint + +local Debounce = require("gitsigns.debounce") +local debounce_trailing = Debounce.debounce_trailing +local throttle_by_id = Debounce.throttle_by_id + +local api = vim.api +local uv = vim.loop +local current_buf = api.nvim_get_current_buf + +local record M + setup : function(cfg: Config) + detach : function(bufnr: integer, _keep_signs: boolean) + detach_all : function() + attach : function(cbuf: integer, trigger: string) + + -- Exposed for tests + parse_fugitive_uri: function(name: string): string, string +end + +--- Detach Gitsigns from all buffers it is attached to. +M.detach_all = function() + for k, _ in pairs(cache as {integer:CacheEntry}) do + M.detach(k) + end +end + +--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not +--- provided then the current buffer is used. +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +M.detach = function(bufnr: integer, _keep_signs: boolean) + -- When this is called interactively (with no arguments) we want to remove all + -- the signs, however if called via a detach event (due to nvim_buf_attach) + -- then we don't want to clear the signs in case the buffer is just being + -- updated due to the file externally changing. When this happens a detach and + -- attach event happen in sequence and so we keep the old signs to stop the + -- sign column width moving about between updates. + bufnr = bufnr or current_buf() + dprint('Detached') + local bcache = cache[bufnr] + if not bcache then + dprint('Cache was nil') + return + end + + manager.detach(bufnr, _keep_signs) + + -- Clear status variables + Status:clear(bufnr) + + cache:destroy(bufnr) +end + +local function parse_fugitive_uri(name: string): string, string + local _, _, root_path, sub_module_path, commit, real_path = + name:find([[^fugitive://(.*)/%.git(.*/)/(%x-)/(.*)]]) + if commit == '0' then + -- '0' means the index so cleat commit so we attach normally + commit = nil + end + if root_path then + sub_module_path = sub_module_path:gsub("^/modules", "") + name = root_path .. sub_module_path .. real_path + end + return name, commit +end + +local function parse_gitsigns_uri(name: string): string, string + -- TODO(lewis6991): Support submodules + local _, _, root_path, commit, rel_path = + name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) + if commit == ':0' then + -- ':0' means the index so clear commit so we attach normally + commit = nil + end + if root_path then + name = root_path .. '/' .. rel_path + end + return name, commit +end + +local function get_buf_path(bufnr: integer): string, string + local file = + uv.fs_realpath(api.nvim_buf_get_name(bufnr)) + or + api.nvim_buf_call(bufnr, function(): string + return vim.fn.expand('%:p') + end) + + if not vim.wo.diff then + if vim.startswith(file, 'fugitive://') then + local path, commit = parse_fugitive_uri(file) + dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + + if vim.startswith(file, 'gitsigns://') then + local path, commit = parse_gitsigns_uri(file) + dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + end + + return file +end + +local vimgrep_running = false + +local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean + if first == last_orig and last_orig == last_new and byte_count == 0 then + -- on_lines can be called twice for undo events; ignore the second + -- call which indicates no changes. + return + end + return manager.on_lines(bufnr, first, last_orig, last_new) +end + +local function on_reload(_, bufnr: integer) + local __FUNC__ = 'on_reload' + dprint('Reload') + manager.update_debounced(bufnr) +end + +local function on_detach(_, bufnr: integer) + M.detach(bufnr, true) +end + +local function on_attach_pre(bufnr: integer): string, string + local gitdir, toplevel: string, string + if config._on_attach_pre then + local res: any = async.wrap(config._on_attach_pre, 2)(bufnr) + dprintf('ran on_attach_pre with result %s', vim.inspect(res)) + if res is table then + if type(res.gitdir) == 'string' then + gitdir = res.gitdir as string + end + if type(res.toplevel) == 'string' then + toplevel = res.toplevel as string + end + end + end + return gitdir, toplevel +end + +local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj + if not config.worktrees then + return + end + + for _, wt in ipairs(config.worktrees) do + local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel) + if git_obj and git_obj.object_name then + dprintf('Using worktree %s', vim.inspect(wt)) + return git_obj + end + end +end + +-- Ensure attaches cannot be interleaved. +-- Since attaches are asynchronous we need to make sure an attach isn't +-- performed whilst another one is in progress. +local attach_throttled = throttle_by_id(function(cbuf: integer, aucmd: string) + local __FUNC__ = 'attach' + if vimgrep_running then + dprint('attaching is disabled') + return + end + + if cache[cbuf] then + dprint('Already attached') + return + end + + if aucmd then + dprintf('Attaching (trigger=%s)', aucmd) + else + dprint('Attaching') + end + + if not api.nvim_buf_is_loaded(cbuf) then + dprint('Non-loaded buffer') + return + end + + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end + + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + local file, commit = get_buf_path(cbuf) + local encoding = vim.bo[cbuf].fileencoding + + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + local gitdir_oap, toplevel_oap = on_attach_pre(cbuf) + local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + + if not git_obj then + git_obj = try_worktrees(cbuf, file, encoding) + scheduler() + end + + if not git_obj then + dprint('Empty git obj') + return + end + local repo = git_obj.repo + + scheduler() + Status:update(cbuf, { + head = repo.abbrev_head, + root = repo.toplevel, + gitdir = repo.gitdir, + }) + + if vim.startswith(file, repo.gitdir..util.path_sep) then + dprint('In non-standard git dir') + return + end + + if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then + dprint('Not a file') + return + end + + if not git_obj.relpath then + dprint('Cannot resolve file in repo') + return + end + + if not config.attach_to_untracked and git_obj.object_name == nil then + dprint('File is untracked') + return + end + + -- On windows os.tmpname() crashes in callback threads so initialise this + -- variable on the main thread. + scheduler() + + if config.on_attach and config.on_attach(cbuf) == false then + dprint('User on_attach() returned false') + return + end + + cache[cbuf] = CacheEntry.new { + base = config.base, + file = file, + commit = commit, + gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), + git_obj = git_obj + } + + if not api.nvim_buf_is_loaded(cbuf) then + dprint('Un-loaded buffer') + return + end + + -- Make sure to attach before the first update (which is async) so we pick up + -- changes from BufReadCmd. + api.nvim_buf_attach(cbuf, false, { + on_lines = on_lines, + on_reload = on_reload, + on_detach = on_detach + }) + + -- Initial update + manager.update(cbuf, cache[cbuf]) + + if config.keymaps and not vim.tbl_isempty(config.keymaps) then + require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf) + end +end) + +--- Attach Gitsigns to the buffer. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +M.attach = void(function(bufnr: integer, _trigger: string) + attach_throttled(bufnr or current_buf(), _trigger) +end) + +local M0 = M as {string:function} + +local function complete(arglead: string, line: string): {string} + local words = vim.split(line, '%s+') + local n: integer = #words + + local actions = require('gitsigns.actions') + local matches: {string} = {} + if n == 2 then + for _, m in ipairs{actions as {string:function}, M0} do + for func, _ in pairs(m) do + if not func:match('^[a-z]') then + -- exclude + elseif vim.startswith(func, arglead) then + table.insert(matches, func) + end + end + end + elseif n > 2 then + -- Subcommand completion + local cmp_func = actions._get_cmp_func(words[2]) + if cmp_func then + return cmp_func(arglead) + end + end + return matches +end + +-- try to parse each argument as a lua boolean, nil or number, if fails then +-- keep argument as a string: +-- +-- 'false' -> false +-- 'nil' -> nil +-- '100' -> 100 +-- 'HEAD~300' -> 'HEAD~300' +local function parse_to_lua(a: string): any + if tonumber(a) then + return tonumber(a) + elseif a == 'false' or a == 'true' then + return a == 'true' + elseif a == 'nil' then + return nil + end + return a +end + +local run_cmd_func = void(function(params: api.UserCmdParams) + local pos_args_raw, named_args_raw = require('gitsigns.argparse').parse_args(params.args) + + local func = pos_args_raw[1] + + if not func then + func = async.wrap(vim.ui.select, 3)(complete('', 'Gitsigns '), {}) + end + + local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) as {any} + local named_args = vim.tbl_map(parse_to_lua, named_args_raw) as {string:any} + local args = vim.tbl_extend('error', pos_args, named_args) + + local actions = require('gitsigns.actions') + local actions0 = actions as {string:function} + + dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, {newline=' ', indent=''})) + + local cmd_func = actions._get_cmd_func(func) + if cmd_func then + -- Action has a specialised mapping function from command form to lua + -- function + cmd_func(args, params) + return + end + + if type(actions0[func]) == 'function' then + actions0[func](unpack(pos_args), named_args) + return + end + + if type(M0[func]) == 'function' then + -- Note functions here do not have named arguments + M0[func](unpack(pos_args)) + return + end + + error(string.format('%s is not a valid function or action', func)) +end) + +local function setup_command() + api.nvim_create_user_command('Gitsigns', run_cmd_func, + { force = true, nargs = '*', range = true, complete = complete }) +end + +local function wrap_func(fn: function, ...: any): function() + local args = {...} + local nargs = select('#', ...) + return function() + fn(unpack(args, 1, nargs)) + end +end + +local function autocmd(event: string, opts: function|vim.api.AutoCmdOpts) + local opts0: vim.api.AutoCmdOpts = {} + if opts is function then + opts0.callback = wrap_func(opts) + else + opts0 = opts + end + opts0.group = 'gitsigns' + api.nvim_create_autocmd(event, opts0) +end + +local function on_or_after_vimenter(fn: function) + if vim.v.vim_did_enter == 1 then + fn() + else + api.nvim_create_autocmd('VimEnter', { + callback = wrap_func(fn), + once = true + }) + end +end + +--- Setup and start Gitsigns. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {cfg} Table object containing configuration for +--- Gitsigns. See |gitsigns-usage| for more details. +M.setup = void(function(cfg: Config) + gs_config.build(cfg) + + if vim.fn.executable('git') == 0 then + print('gitsigns: git not in path. Aborting setup') + return + end + if config.yadm.enable and vim.fn.executable('yadm') == 0 then + print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config") + config.yadm.enable = false + return + end + + gs_debug.debug_mode = config.debug_mode + gs_debug.verbose = config._verbose + + if config.debug_mode then + for nm, f in pairs(gs_debug.add_debug_functions(cache)) do + M0[nm] = f + end + end + + manager.setup() + + Status.formatter = config.status_formatter as function(Status.StatusObj): string + + -- Make sure highlights are setup on or after VimEnter so the colorscheme is + -- loaded. Do not set them up with vim.schedule as this removes the intro + -- message. + on_or_after_vimenter(hl.setup_highlights) + + setup_command() + + git.enable_yadm = config.yadm.enable + git.set_version(config._git_version) + scheduler() + + -- Attach to all open buffers + for _, buf in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_is_loaded(buf) + and api.nvim_buf_get_name(buf) ~= '' then + M.attach(buf, 'setup') + scheduler() + end + end + + api.nvim_create_augroup('gitsigns', {}) + + autocmd('VimLeavePre' , M.detach_all) + autocmd('ColorScheme' , hl.setup_highlights) + autocmd('BufRead' , wrap_func(M.attach, nil, 'BufRead')) + autocmd('BufNewFile' , wrap_func(M.attach, nil, 'BufNewFile')) + autocmd('BufWritePost', wrap_func(M.attach, nil, 'BufWritePost')) + + autocmd('OptionSet', { + pattern = 'fileformat', + callback = function() + require('gitsigns.actions').refresh() + end} + ) + + -- vimpgrep creates and deletes lots of buffers so attaching to each one will + -- waste lots of resource and even slow down vimgrep. + autocmd('QuickFixCmdPre', { + pattern ='*vimgrep*', + callback = function() + vimgrep_running = true + end + }) + + autocmd('QuickFixCmdPost', { + pattern ='*vimgrep*', + callback = function() + vimgrep_running = false + end + }) + + require('gitsigns.current_line_blame').setup() + + scheduler() + manager.update_cwd_head() + -- Need to debounce in case some plugin changes the cwd too often + -- (like vim-grepper) + autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head)) +end) + +if _TEST then + M.parse_fugitive_uri = parse_fugitive_uri +end + +return setmetatable(M, { + __index = function(_, f: string): any + return (require('gitsigns.actions') as {string:function})[f] + end +}) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/actions.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/actions.tl new file mode 100644 index 0000000..6f926bd --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/actions.tl @@ -0,0 +1,1269 @@ +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler + +local config = require('gitsigns.config').config +local mk_repeatable = require('gitsigns.repeat').mk_repeatable +local popup = require('gitsigns.popup') +local util = require('gitsigns.util') +local manager = require('gitsigns.manager') +local git = require('gitsigns.git') +local run_diff = require('gitsigns.diff') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local gs_hunks = require('gitsigns.hunks') +local Hunk = gs_hunks.Hunk +local Hunk_Public = gs_hunks.Hunk_Public + +local api = vim.api +local current_buf = api.nvim_get_current_buf + +local record DiffthisOpts + vertical: boolean + split: string +end + +local record NavHunkOpts + forwards: boolean + wrap: boolean + navigation_message: boolean + foldopen: boolean + preview: boolean + greedy: boolean +end + +local record BlameOpts + full: boolean + ignore_whitespace: boolean +end + +local record StageHunkOpts + greedy: boolean +end + +local record ResetHunkOpts + greedy: boolean +end + +local record M + stage_hunk : function({integer, integer}, StageHunkOpts) + undo_stage_hunk : function() + reset_hunk : function({integer, integer}, ResetHunkOpts) + + stage_buffer : function() + reset_buffer : function() + reset_buffer_index : function() + + next_hunk : function(NavHunkOpts) + prev_hunk : function(NavHunkOpts) + preview_hunk : function() + preview_hunk_inline: function() + select_hunk : function() + get_hunks : function(bufnr: integer): {Hunk_Public} + + blame_line : function(BlameOpts) + change_base : function(base: string, global: boolean) + reset_base : function(global: boolean) + diffthis : function(base: string, opts: DiffthisOpts) + show : function(base: string) + + test: function(base: string, opts: {string:any}) + + record QFListOpts + use_location_list: boolean + nr: integer + open: boolean + end + + setqflist : function(target:integer|string, opts: QFListOpts, callback: function) + setloclist : function(nr: integer, target:integer|string) + + get_actions : function(bufnr: integer, lnum: integer) + + refresh : function() + toggle_signs : function(boolean): boolean + toggle_numhl : function(boolean): boolean + toggle_linehl : function(boolean): boolean + toggle_word_diff : function(boolean): boolean + toggle_current_line_blame : function(boolean): boolean + toggle_deleted : function(boolean): boolean + + arg_spec: {string:{integer,boolean,boolean}} +end + +local type CmdFunc = function(args: table, params: api.UserCmdParams) + +-- Variations of functions from M which are used for the Gitsigns command +local C: {string:CmdFunc} = {} + +local type CmpFunc = function(arglead: string): {string} + +local CP: {string:CmpFunc} = {} + +local ns_inline = api.nvim_create_namespace('gitsigns_preview_inline') + +local function complete_heads(arglead: string): {string} + local all = vim.fn.systemlist{'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes'} + return vim.tbl_filter(function(x: string): boolean + return vim.startswith(x, arglead) + end, all) +end + +--- Toggle |gitsigns-config-signbooleancolumn| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-signcolumn| +M.toggle_signs = function(value: boolean): boolean + if value ~= nil then + config.signcolumn = value + else + config.signcolumn = not config.signcolumn + end + M.refresh() + return config.signcolumn +end + +--- Toggle |gitsigns-config-numhl| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-numhl| +M.toggle_numhl = function(value: boolean): boolean + if value ~= nil then + config.numhl = value + else + config.numhl = not config.numhl + end + M.refresh() + return config.numhl +end + +--- Toggle |gitsigns-config-linehl| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-linehl| +M.toggle_linehl = function(value: boolean): boolean + if value ~= nil then + config.linehl = value + else + config.linehl = not config.linehl + end + M.refresh() + return config.linehl +end + +--- Toggle |gitsigns-config-word_diff| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-word_diff| +M.toggle_word_diff = function(value: boolean): boolean + if value ~= nil then + config.word_diff = value + else + config.word_diff = not config.word_diff + end + -- Don't use refresh() to avoid flicker + api.nvim__buf_redraw_range(0, vim.fn.line('w0') - 1, vim.fn.line('w$')) + return config.word_diff +end + +--- Toggle |gitsigns-config-current_line_blame| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-current_line_blame| +M.toggle_current_line_blame = function(value: boolean): boolean + if value ~= nil then + config.current_line_blame = value + else + config.current_line_blame = not config.current_line_blame + end + M.refresh() + return config.current_line_blame +end + +--- Toggle |gitsigns-config-show_deleted| +--- +--- Parameters:~ +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. +--- +--- Returns:~ +--- Current value of |gitsigns-config-show_deleted| +M.toggle_deleted = function(value: boolean): boolean + if value ~= nil then + config.show_deleted = value + else + config.show_deleted = not config.show_deleted + end + M.refresh() + return config.show_deleted +end + +local function get_cursor_hunk(bufnr: integer, hunks: {Hunk}): Hunk, integer + bufnr = bufnr or current_buf() + hunks = hunks or cache[bufnr].hunks + + local lnum = api.nvim_win_get_cursor(0)[1] + return gs_hunks.find_hunk(lnum, hunks) +end + +local function update(bufnr: integer) + manager.update(bufnr) + scheduler() + if vim.wo.diff then + require('gitsigns.diffthis').update(bufnr) + end +end + +local function get_range(params: api.UserCmdParams): {integer, integer} + local range: {integer, integer} + if params.range > 0 then + range = {params.line1, params.line2} + end + return range +end + +local function get_hunks(bufnr: integer, bcache: CacheEntry, greedy: boolean): {Hunk} + local hunks: {Hunk} + + if greedy then + -- Re-run the diff without linematch + local buftext = util.buf_lines(bufnr) + hunks = run_diff(bcache.compare_text, buftext, false) + scheduler() + else + hunks = bcache.hunks + end + + return hunks +end + +--- Stage the hunk at the cursor position, or all lines in the +--- given range. If {range} is provided, all lines in the given +--- range are staged. This supports partial-hunks, meaning if a +--- range only includes a portion of a particular hunk, only the +--- lines within the range will be staged. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters:~ +--- {range} table|nil List-like table of two integers making +--- up the line range from which you want to stage the hunks. +--- If running via command line, then this is taken from the +--- command modifiers. +--- {opts} table|nil Additional options: +--- • {greedy}: (boolean) +--- Stage all contiguous hunks. Only useful if 'diff_opts' +--- contains `linematch`. Defaults to `true`. +--- +M.stage_hunk = mk_repeatable(void(function(range: {integer, integer}, opts: StageHunkOpts) + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + if not util.path_exists(bcache.file) then + print("Error: Cannot stage lines. Please add the file to the working tree.") + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy ~= false) + local hunk: Hunk + + if range then + table.sort(range) + local top, bot = range[1], range[2] + hunk = gs_hunks.create_partial_hunk(hunks, top, bot) + hunk.added.lines = api.nvim_buf_get_lines(bufnr, top-1, bot, false) + hunk.removed.lines = vim.list_slice( + bcache.compare_text, + hunk.removed.start, + hunk.removed.start + hunk.removed.count - 1 + ) + else + hunk = get_cursor_hunk(bufnr, hunks) + end + + if not hunk then + return + end + + bcache.git_obj:stage_hunks({hunk}) + + table.insert(bcache.staged_diffs, hunk) + + bcache:invalidate() + update(bufnr) +end)) + +C.stage_hunk = function(_: table, params: api.UserCmdParams) + M.stage_hunk(get_range(params)) +end + +--- Reset the lines of the hunk at the cursor position, or all +--- lines in the given range. If {range} is provided, all lines in +--- the given range are reset. This supports partial-hunks, +--- meaning if a range only includes a portion of a particular +--- hunk, only the lines within the range will be reset. +--- +--- Parameters:~ +--- {range} table|nil List-like table of two integers making +--- up the line range from which you want to reset the hunks. +--- If running via command line, then this is taken from the +--- command modifiers. +--- {opts} table|nil Additional options: +--- • {greedy}: (boolean) +--- Stage all contiguous hunks. Only useful if 'diff_opts' +--- contains `linematch`. Defaults to `true`. +--- +M.reset_hunk = mk_repeatable(void(function(range: {integer, integer}, opts: ResetHunkOpts) + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy ~= false) + local hunk: Hunk + + if range then + table.sort(range) + local top, bot = range[1], range[2] + hunk = gs_hunks.create_partial_hunk(hunks, top, bot) + hunk.added.lines = api.nvim_buf_get_lines(bufnr, top-1, bot, false) + hunk.removed.lines = vim.list_slice( + bcache.compare_text, + hunk.removed.start, + hunk.removed.start + hunk.removed.count - 1 + ) + else + hunk = get_cursor_hunk(bufnr, hunks) + end + + if not hunk then + return + end + + local lstart, lend: integer, integer + if hunk.type == 'delete' then + lstart = hunk.added.start + lend = hunk.added.start + else + lstart = hunk.added.start - 1 + lend = hunk.added.start - 1 + hunk.added.count + end + util.set_lines(bufnr, lstart, lend, hunk.removed.lines) +end)) + +C.reset_hunk = function(_: table, params: api.UserCmdParams) + M.reset_hunk(get_range(params)) +end + +--- Reset the lines of all hunks in the buffer. +M.reset_buffer = function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + util.set_lines(bufnr, 0, -1, bcache.compare_text) +end + +--- Undo the last call of stage_hunk(). +--- +--- Note: only the calls to stage_hunk() performed in the current +--- session can be undone. +--- +--- Attributes: ~ +--- {async} +M.undo_stage_hunk = void(function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunk = table.remove(bcache.staged_diffs) + if not hunk then + print("No hunks to undo") + return + end + + bcache.git_obj:stage_hunks({hunk}, true) + bcache:invalidate() + update(bufnr) +end) + +--- Stage all hunks in current buffer. +--- +--- Attributes: ~ +--- {async} +M.stage_buffer = void(function() + local bufnr = current_buf() + + local bcache = cache[bufnr] + if not bcache then + return + end + + -- Only process files with existing hunks + local hunks = bcache.hunks + if #hunks == 0 then + print("No unstaged changes in file to stage") + return + end + + if not util.path_exists(bcache.git_obj.file) then + print("Error: Cannot stage file. Please add it to the working tree.") + return + end + + bcache.git_obj:stage_hunks(hunks) + + for _, hunk in ipairs(hunks) do + table.insert(bcache.staged_diffs, hunk) + end + + bcache:invalidate() + update(bufnr) +end) + +--- Unstage all hunks for current buffer in the index. Note: +--- Unlike |gitsigns.undo_stage_hunk()| this doesn't simply undo +--- stages, this runs an `git reset` on current buffers file. +--- +--- Attributes: ~ +--- {async} +M.reset_buffer_index = void(function() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + -- `bcache.staged_diffs` won't contain staged changes outside of current + -- neovim session so signs added from this unstage won't be complete They will + -- however be fixed by gitdir watcher and properly updated We should implement + -- some sort of initial population from git diff, after that this function can + -- be improved to check if any staged hunks exists and it can undo changes + -- using git apply line by line instead of resetting whole file + bcache.staged_diffs = {} + + bcache.git_obj:unstage_file() + + bcache:invalidate() + update(bufnr) +end) + +local function process_nav_opts(opts: NavHunkOpts) + -- show navigation message + if opts.navigation_message == nil then + opts.navigation_message = not vim.opt.shortmess:get().S + end + + -- wrap around + if opts.wrap == nil then + opts.wrap = vim.opt.wrapscan:get() + end + + if opts.foldopen == nil then + opts.foldopen = vim.tbl_contains(vim.opt.foldopen:get(), 'search') + end + + if opts.greedy == nil then + opts.greedy = true + end +end + +-- Defer function to the next main event +local function defer(fn: function) + if vim.in_fast_event() then + vim.schedule(fn) + else + vim.defer_fn(fn, 1) + end +end + +local function has_preview_inline(bufnr: integer): boolean + return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, {limit=1}) > 0 +end + +local nav_hunk = void(function(opts: NavHunkOpts) + process_nav_opts(opts) + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunks = get_hunks(bufnr, bcache, opts.greedy) + + if not hunks or vim.tbl_isempty(hunks) then + if opts.navigation_message then + api.nvim_echo({{'No hunks', 'WarningMsg'}}, false, {}) + end + return + end + local line = api.nvim_win_get_cursor(0)[1] + + local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, opts.forwards, opts.wrap) + + if hunk == nil then + if opts.navigation_message then + api.nvim_echo({{'No more hunks', 'WarningMsg'}}, false, {}) + end + return + end + + local row = opts.forwards and hunk.added.start or hunk.vend + if row then + -- Handle topdelete + if row == 0 then + row = 1 + end + vim.cmd [[ normal! m' ]] -- add current cursor position to the jump list + api.nvim_win_set_cursor(0, {row, 0}) + if opts.foldopen then + vim.cmd('silent! foldopen!') + end + if opts.preview or popup.is_open('hunk') ~= nil then + -- Use defer so the cursor change can settle, otherwise the popup might + -- appear in the old position + defer(M.preview_hunk) + elseif has_preview_inline(bufnr) then + defer(M.preview_hunk_inline) + end + + if index ~= nil and opts.navigation_message then + api.nvim_echo({{string.format('Hunk %d of %d', index, #hunks), 'None'}}, false, {}) + end + + end +end) + +--- Jump to the next hunk in the current buffer. If a hunk preview +--- (popup or inline) was previously opened, it will be re-opened +--- at the next hunk. +--- +--- Parameters: ~ +--- {opts} table|nil Configuration table. Keys: +--- • {wrap}: (boolean) +--- Whether to loop around file or not. Defaults +--- to the value 'wrapscan' +--- • {navigation_message}: (boolean) +--- Whether to show navigation messages or not. +--- Looks at 'shortmess' for default behaviour. +--- • {foldopen}: (boolean) +--- Expand folds when navigating to a hunk which is +--- inside a fold. Defaults to `true` if 'foldopen' +--- contains `search`. +--- • {preview}: (boolean) +--- Automatically open preview_hunk() upon navigating +--- to a hunk. +--- • {greedy}: (boolean) +--- Only navigate between non-contiguous hunks. Only useful if +--- 'diff_opts' contains `linematch`. Defaults to `true`. +M.next_hunk = function(opts: NavHunkOpts) + opts = opts or {} + opts.forwards = true + nav_hunk(opts) +end + +--- Jump to the previous hunk in the current buffer. If a hunk preview +--- (popup or inline) was previously opened, it will be re-opened +--- at the previous hunk. +--- +--- Parameters: ~ +--- See |gitsigns.next_hunk()|. +M.prev_hunk = function(opts: NavHunkOpts) + opts = opts or {} + opts.forwards = false + nav_hunk(opts) +end + +local HlMark = popup.HlMark + +local function lines_format(fmt: {{{string,string|{HlMark}}}}, + info: util.FmtInfo): {{{string,string|{HlMark}}}} + + local ret = vim.deepcopy(fmt) + + for _, line in ipairs(ret) do + for _, s in ipairs(line) do + s[1] = util.expand_format(s[1], info) + end + end + + return ret +end + +local function hlmarks_for_hunk(hunk: Hunk, hl: string): {HlMark} + local hls: {HlMark} = {} + + local removed, added = hunk.removed, hunk.added + + if hl then + hls[#hls+1] = { + hl_group = hl, + start_row = 0, + end_row = removed.count + added.count, + } + end + + hls[#hls+1] = { + hl_group = 'GitSignsDeletePreview', + start_row = 0, + end_row = removed.count, + } + + hls[#hls+1] = { + hl_group = 'GitSignsAddPreview', + start_row = removed.count, + end_row = removed.count + added.count, + } + + if config.diff_opts.internal then + local removed_regions, added_regions = + require('gitsigns.diff_int').run_word_diff(removed.lines, added.lines) + for _, region in ipairs(removed_regions) do + hls[#hls+1] = { + hl_group = 'GitSignsDeleteInline', + start_row = region[1] - 1, + start_col = region[3], + end_col = region[4], + } + end + for _, region in ipairs(added_regions) do + hls[#hls+1] = { + hl_group = 'GitSignsAddInline', + start_row = region[1] + removed.count - 1, + start_col = region[3], + end_col = region[4], + } + end + end + + return hls +end + +local function insert_hunk_hlmarks(fmt: popup.LinesSpec, hunk: Hunk) + for _, line in ipairs(fmt) do + for _, s in ipairs(line) do + local hl = s[2] + if s[1] == '' and hl is string then + s[2] = hlmarks_for_hunk(hunk, hl) + end + end + end +end + +local function noautocmd(f: function()): function() + return function() + local ei = vim.o.eventignore + vim.o.eventignore = 'all' + f() + vim.o.eventignore = ei + end +end + +--- Preview the hunk at the cursor position in a floating +--- window. If the preview is already open, calling this +--- will cause the window to get focus. +M.preview_hunk = noautocmd(function() + if popup.focus_open('hunk') then + return + end + -- Wrap in noautocmd so vim-repeat continues to work + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + local hunk, index = get_cursor_hunk(bufnr, bcache.hunks) + + if not hunk then return end + + local lines_fmt = { + {{'Hunk of ', 'Title'}}, + {{'', 'NormalFloat' }} + } + + insert_hunk_hlmarks(lines_fmt, hunk) + + local lines_spec = lines_format(lines_fmt, { + hunk_no = index, + num_hunks = #bcache.hunks, + hunk = gs_hunks.patch_lines(hunk, vim.bo[bufnr].fileformat), + }) + + popup.create(lines_spec, config.preview_config, 'hunk') +end) + +--- Preview the hunk at the cursor position inline in the buffer. +M.preview_hunk_inline = function() + local bufnr = current_buf() + + local hunk = get_cursor_hunk(bufnr) + + if not hunk then + return + end + + manager.show_added(bufnr, ns_inline, hunk) + manager.show_deleted(bufnr, ns_inline, hunk) + + api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter' }, { + callback = function() + api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) + end, + once = true + }) + +end + +--- Select the hunk under the cursor. +M.select_hunk = function() + local hunk = get_cursor_hunk() + if not hunk then return end + + vim.cmd('normal! '..hunk.added.start..'GV'..hunk.vend..'G') +end + +--- Get hunk array for specified buffer. +--- +--- Parameters: ~ +--- {bufnr} integer: Buffer number, if not provided (or 0) +--- will use current buffer. +--- +--- Return: ~ +--- Array of hunk objects. Each hunk object has keys: +--- • `"type"`: String with possible values: "add", "change", +--- "delete" +--- • `"head"`: Header that appears in the unified diff +--- output. +--- • `"lines"`: Line contents of the hunks prefixed with +--- either `"-"` or `"+"`. +--- • `"removed"`: Sub-table with fields: +--- • `"start"`: Line number (1-based) +--- • `"count"`: Line count +--- • `"added"`: Sub-table with fields: +--- • `"start"`: Line number (1-based) +--- • `"count"`: Line count +M.get_hunks = function(bufnr: integer): {Hunk_Public} + bufnr = bufnr or current_buf() + if not cache[bufnr] then return end + local ret = {} + -- TODO(lewis6991): allow this to accept a greedy option + for _, h in ipairs(cache[bufnr].hunks or {}) do + ret[#ret+1] = { + head = h.head, + lines = gs_hunks.patch_lines(h, vim.bo[bufnr].fileformat), + type = h.type, + added = h.added, + removed = h.removed + } + end + return ret +end + +local function get_blame_hunk(repo: git.Repo, info: git.BlameInfo): Hunk, integer, integer + local a = {} + -- If no previous so sha of blame added the file + if info.previous then + a = repo:get_show_text(info.previous_sha..':'..info.previous_filename) + end + local b = repo:get_show_text(info.sha..':'..info.filename) + local hunks = run_diff(a, b, false) + local hunk, i = gs_hunks.find_hunk(info.orig_lnum, hunks) + return hunk, i, #hunks +end + +local function create_blame_fmt(is_committed: boolean, full: boolean): popup.LinesSpec + if not is_committed then + return { + {{'', 'Label'}}, + } + end + + local header = { + {' ', 'Directory'}, + {' ', 'MoreMsg'}, + {'()', 'Label'}, + {':', 'NormalFloat'} + } + + if full then + return { + header, + {{'', 'NormalFloat'}}, + {{'Hunk of ', 'Title'}, {' ', 'LineNr'}}, + {{'', 'NormalFloat'}} + } + end + + return { + header, + {{'', 'NormalFloat'}} + } +end + +--- Run git blame on the current line and show the results in a +--- floating window. If already open, calling this will cause the +--- window to get focus. +--- +--- Parameters: ~ +--- {opts} (table|nil): +--- Additional options: +--- • {full}: (boolean) +--- Display full commit message with hunk. +--- • {ignore_whitespace}: (boolean) +--- Ignore whitespace when running blame. +--- +--- Attributes: ~ +--- {async} +M.blame_line = void(function(opts: BlameOpts) + if popup.focus_open('blame') then + return + end + + opts = opts or {} + + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then return end + + local loading = vim.defer_fn(function() + popup.create({{{'Loading...', 'Title'}}}, config.preview_config) + end, 1000) + + scheduler() + local buftext = util.buf_lines(bufnr) + local fileformat = vim.bo[bufnr].fileformat + local lnum = api.nvim_win_get_cursor(0)[1] + local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + pcall(function() + loading:close() + end) + + local is_committed = result.sha and tonumber('0x'..result.sha) ~= 0 + + local blame_fmt = create_blame_fmt(is_committed, opts.full) + + local info = result as util.FmtInfo + + if is_committed and opts.full then + info.body = bcache.git_obj:command{ 'show', '-s', '--format=%B', result.sha } as string + + local hunk: Hunk + + hunk, info.hunk_no, info.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) + + info.hunk = gs_hunks.patch_lines(hunk, fileformat) + info.hunk_head = hunk.head + insert_hunk_hlmarks(blame_fmt, hunk) + end + + scheduler() + + popup.create(lines_format(blame_fmt, info), config.preview_config, 'blame') +end) + +local function update_buf_base(buf: integer, bcache: CacheEntry, base: string) + bcache.base = base + bcache:invalidate() + update(buf) +end + +--- Change the base revision to diff against. If {base} is not +--- given, then the original base is used. If {global} is given +--- and true, then change the base revision of all buffers, +--- including any new buffers. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters:~ +--- {base} string|nil The object/revision to diff against. +--- {global} boolean|nil Change the base of all buffers. +--- +--- Examples: > +--- " Change base to 1 commit behind head +--- :lua require('gitsigns').change_base('HEAD~1') +--- +--- " Also works using the Gitsigns command +--- :Gitsigns change_base HEAD~1 +--- +--- " Other variations +--- :Gitsigns change_base ~1 +--- :Gitsigns change_base ~ +--- :Gitsigns change_base ^ +--- +--- " Commits work too +--- :Gitsigns change_base 92eb3dd +--- +--- " Revert to original base +--- :Gitsigns change_base +--- < +--- +--- For a more complete list of ways to specify bases, see +--- |gitsigns-revision|. +M.change_base = void(function(base: string, global: boolean) + base = util.calc_base(base) + + if global then + config.base = base + + for bufnr, bcache in pairs(cache as {integer:CacheEntry}) do + update_buf_base(bufnr, bcache, base) + end + else + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then return end + + update_buf_base(bufnr, bcache, base) + end +end) + +C.change_base = function(args: table, _: api.UserCmdParams) + M.change_base(args[1] as string, (args[2] or args.global) as boolean) +end + +CP.change_base = complete_heads + +--- Reset the base revision to diff against back to the +--- index. +--- +--- Alias for `change_base(nil, {global})` . +M.reset_base = function(global: boolean) + M.change_base(nil, global) +end + +C.reset_base = function(args: table, _: api.UserCmdParams) + M.change_base(nil, (args[1] or args.global) as boolean) +end + +--- Perform a |vimdiff| on the given file with {base} if it is +--- given, or with the currently set base (index by default). +--- +--- If {base} is the index, then the opened buffer is editable and +--- any written changes will update the index accordingly. +--- +--- Parameters: ~ +--- {base} (string|nil): Revision to diff against. Defaults +--- to index. +--- {opts} (table|nil): +--- Additional options: +--- • {vertical}: {boolean}. Split window vertically. Defaults to +--- config.diff_opts.vertical. If running via command line, then +--- this is taken from the command modifiers. +--- • {split}: {string}. One of: 'aboveleft', 'belowright', +--- 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to +--- 'aboveleft'. If running via command line, then this is taken +--- from the command modifiers. +--- +--- Examples: > +--- " Diff against the index +--- :Gitsigns diffthis +--- +--- " Diff against the last commit +--- :Gitsigns diffthis ~1 +--- < +--- +--- For a more complete list of ways to specify bases, see +--- |gitsigns-revision|. +--- +--- Attributes: ~ +--- {async} +M.diffthis = function(base: string, opts: DiffthisOpts) + -- TODO(lewis6991): can't pass numbers as strings from the command line + if base ~= nil then + base = tostring(base) + end + opts = opts or {} + local diffthis = require('gitsigns.diffthis') + if not opts.vertical then + opts.vertical = config.diff_opts.vertical + end + diffthis.diffthis(base, opts as diffthis.DiffthisOpts) +end + +C.diffthis = function(args: table, params: api.UserCmdParams) + -- TODO(lewis6991): validate these + local opts: DiffthisOpts = { + vertical = args.vertical as boolean, + split = args.split as string + } + + if params.smods then + if params.smods.split ~= '' and opts.split == nil then + opts.split = params.smods.split + end + if opts.vertical == nil then + opts.vertical = params.smods.vertical + end + end + + M.diffthis(args[1] as string, opts) +end + +CP.diffthis = complete_heads + +-- C.test = function(pos_args: {any}, named_args: {string:any}, params: api.UserCmdParams) +-- print('POS ARGS:', vim.inspect(pos_args)) +-- print('NAMED ARGS:', vim.inspect(named_args)) +-- print('PARAMS:', vim.inspect(params)) +-- end + +--- Show revision {base} of the current file, if it is given, or +--- with the currently set base (index by default). +--- +--- If {base} is the index, then the opened buffer is editable and +--- any written changes will update the index accordingly. +--- +--- Examples: > +--- " View the index version of the file +--- :Gitsigns show +--- +--- " View revision of file in the last commit +--- :Gitsigns show ~1 +--- < +--- +--- For a more complete list of ways to specify bases, see +--- |gitsigns-revision|. +--- +--- Attributes: ~ +--- {async} +M.show = function(revision: string) + local diffthis = require('gitsigns.diffthis') + diffthis.show(revision) +end + +CP.show = complete_heads + +local function hunks_to_qflist(buf_or_filename: number|string, hunks: {Hunk}, qflist: {vim.fn.QFItem}) + for i, hunk in ipairs(hunks) do + qflist[#qflist+1] = { + bufnr = buf_or_filename is number and (buf_or_filename as integer) or nil, + filename = buf_or_filename is string and buf_or_filename or nil, + lnum = hunk.added.start, + text = string.format('Lines %d-%d (%d/%d)', + hunk.added.start, hunk.vend, i, #hunks), + } + end +end + +local function buildqflist(target: integer|string): {vim.fn.QFItem} + target = target or current_buf() + if target == 0 then target = current_buf() end + local qflist: {vim.fn.QFItem} = {} + + if type(target) == 'number' then + local bufnr = target as integer + if not cache[bufnr] then return end + hunks_to_qflist(bufnr, cache[bufnr].hunks, qflist) + elseif target == 'attached' then + for bufnr, bcache in pairs(cache as {integer:CacheEntry}) do + hunks_to_qflist(bufnr, bcache.hunks, qflist) + end + elseif target == 'all' then + local repos: {string:git.Repo} = {} + for _, bcache in pairs(cache as {integer:CacheEntry}) do + local repo = bcache.git_obj.repo + if not repos[repo.gitdir] then + repos[repo.gitdir] = repo + end + end + + local repo = git.Repo.new(vim.loop.cwd()) + if not repos[repo.gitdir] then + repos[repo.gitdir] = repo + end + + for _, r in pairs(repos) do + for _, f in ipairs(r:files_changed()) do + local f_abs = r.toplevel..'/'..f + local stat = vim.loop.fs_stat(f_abs) + if stat and stat.type == 'file' then + local a = r:get_show_text(':0:'..f) + scheduler() + local hunks = run_diff(a, util.file_lines(f_abs)) + hunks_to_qflist(f_abs, hunks, qflist) + end + end + end + + end + return qflist +end + +--- Populate the quickfix list with hunks. Automatically opens the +--- quickfix window. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {target} (integer or string): +--- Specifies which files hunks are collected from. +--- Possible values. +--- • [integer]: The buffer with the matching buffer +--- number. `0` for current buffer (default). +--- • `"attached"`: All attached buffers. +--- • `"all"`: All modified files for each git +--- directory of all attached buffers in addition +--- to the current working directory. +--- {opts} (table|nil): +--- Additional options: +--- • {use_location_list}: (boolean) +--- Populate the location list instead of the +--- quickfix list. Default to `false`. +--- • {nr}: (integer) +--- Window number or ID when using location list. +--- Expand folds when navigating to a hunk which is +--- inside a fold. Defaults to `0`. +--- • {open}: (boolean) +--- Open the quickfix/location list viewer. +--- Defaults to `true`. +M.setqflist = void(function(target: integer|string, opts: M.QFListOpts) + opts = opts or {} + if opts.open == nil then + opts.open = true + end + local qfopts = { + items = buildqflist(target), + title = 'Hunks' + } + scheduler() + if opts.use_location_list then + local nr = opts.nr or 0 + vim.fn.setloclist(nr, {}, ' ', qfopts) + if opts.open then + if config.trouble then + require'trouble'.open("loclist") + else + vim.cmd[[lopen]] + end + end + else + vim.fn.setqflist({}, ' ', qfopts) + if opts.open then + if config.trouble then + require'trouble'.open("quickfix") + else + vim.cmd[[copen]] + end + end + end +end) + +--- Populate the location list with hunks. Automatically opens the +--- location list window. +--- +--- Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {nr} (integer): Window number or the |window-ID|. +--- `0` for the current window (default). +--- {target} (integer or string): See |gitsigns.setqflist()|. +M.setloclist = function(nr: integer, target: integer|string) + M.setqflist(target, { + nr = nr, + use_location_list = true + }) +end + +--- Get all the available line specific actions for the current +--- buffer at the cursor position. +--- +--- Return: ~ +--- Dictionary of action name to function which when called +--- performs action. +M.get_actions = function(): {string:function} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + local hunk = get_cursor_hunk(bufnr, bcache.hunks) + + local actions_l: {string} = {} + + local function add_action(action: string) + actions_l[#actions_l+1] = action + end + + if hunk then + add_action('stage_hunk') + add_action('reset_hunk') + add_action('preview_hunk') + add_action('select_hunk') + else + add_action('blame_line') + end + + if not vim.tbl_isempty(bcache.staged_diffs) then + add_action('undo_stage_hunk') + end + + local actions: {string:function} = {} + for _, a in ipairs(actions_l) do + actions[a] = (M as {string:function})[a] + end + + return actions +end + +--- Refresh all buffers. +--- +--- Attributes: ~ +--- {async} +M.refresh = void(function() + manager.reset_signs() + require('gitsigns.highlight').setup_highlights() + require('gitsigns.current_line_blame').setup() + for k, v in pairs(cache as {integer:CacheEntry}) do + v:invalidate() + manager.update(k, v) + end +end) + +function M._get_cmd_func(name: string): CmdFunc + return C[name] +end + +function M._get_cmp_func(name: string): CmpFunc + return CP[name] +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/argparse.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/argparse.tl new file mode 100644 index 0000000..55043d5 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/argparse.tl @@ -0,0 +1,91 @@ +local record M + parse_args: function(x: string): {string}, {string:string} +end + +local function is_char(x: string): boolean + return x:match('[^=\'"%s]') ~= nil +end + +local enum ArgState + "in_arg" + "in_ws" + "in_value" + "in_quote" +end + +-- Return positional arguments and names arguments +function M.parse_args(x: string): {string}, {string:string} + local pos_args, named_args: {string}, {string:string} = {}, {} + + local state: ArgState = 'in_arg' + local cur_arg = '' + local cur_val = '' + local cur_quote = '' + + local function peek(idx: integer): string + return x:sub(idx+1, idx+1) + end + + local i = 1 + while i <= #x do + local ch = x:sub(i, i) + -- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch) + + if state == 'in_arg' then + if is_char(ch) then + cur_arg = cur_arg .. ch + elseif ch:match('%s') then + pos_args[#pos_args+1] = cur_arg + state = 'in_ws' + elseif ch == '=' then + cur_val = '' + local next_ch = peek(i) + if next_ch == "'" or next_ch == '"' then + cur_quote = next_ch + i = i + 1 + state = 'in_quote' + else + state = 'in_value' + end + end + elseif state == 'in_ws' then + if is_char(ch) then + cur_arg = ch + state = 'in_arg' + end + elseif state == 'in_value' then + if is_char(ch) then + cur_val = cur_val .. ch + elseif ch:match('%s') then + named_args[cur_arg] = cur_val + cur_arg = '' + state = 'in_ws' + end + elseif state == 'in_quote' then + local next_ch = peek(i) + if ch == "\\" and next_ch == cur_quote then + cur_val = cur_val .. next_ch + i = i + 1 + elseif ch == cur_quote then + named_args[cur_arg] = cur_val + state = 'in_ws' + if next_ch ~= '' and not next_ch:match('%s') then + error('malformed argument: '..next_ch) + end + else + cur_val = cur_val .. ch + end + end + i = i + 1 + end + + if state == 'in_arg' and #cur_arg > 0 then + pos_args[#pos_args+1] = cur_arg + elseif state == 'in_value' and #cur_arg > 0 then + named_args[cur_arg] = cur_val + end + + return pos_args, named_args +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/async.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/async.tl new file mode 100644 index 0000000..655b63f --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/async.tl @@ -0,0 +1,87 @@ +local record Async + -- Order by highest number of return types + + void: function (function() ): function() + void: function (function(A1) ): function(A1) + void: function(function(A1,A2)): function(A1,A2) + void: function(function(A1,A2,A3,A4): R1): function(A1,A2,A3,A4): R1 + void: function(function(A1,A2,A3,A4,A5): R1): function(A1,A2,A3,A4,A5): R1 + void: function(function(A1,A2,A3,A4)): function(A1,A2,A3,A4) + + wrap: function (function(A1,A2, function(R1,R2)), integer): function(A1,A2): R1,R2 + wrap: function(function(A1,A2,A3,A4,function(R1) ), integer): function(A1,A2,A3,A4): R1 + wrap: function(function(A1,A2,A3,A4,A5,function(R1)), integer): function(A1,A2,A3,A4,A5): R1 + wrap: function (function(A1,A2,A3, function()) , integer): function(A1,A2,A3) + + scheduler: function() +end + +local record M + scheduler: function() +end + +-- Coroutine.running() was changed between Lua 5.1 and 5.2: +-- - 5.1: Returns the running coroutine, or nil when called by the main thread. +-- - 5.2: Returns the running coroutine plus a boolean, true when the running +-- coroutine is the main one. +-- +-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT +-- +-- We need to handle both. +local main_co_or_nil = coroutine.running() + +---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 +function M.wrap(func: function, argc: integer): function + assert(argc) + return function(...): any... + if coroutine.running() == main_co_or_nil then + return func(...) + end + return coroutine.yield(func, argc, ...) + 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 +function M.void(func: function): function + return function(...): any + if coroutine.running() ~= main_co_or_nil then + return func(...) + end + + local co = coroutine.create(func) + + local function step(...) + local ret = {coroutine.resume(co, ...)} as {any} + local stat, err_or_fn, nargs = unpack(ret) as (boolean, function, integer) + + if not stat then + error(string.format("The coroutine failed with this message: %s\n%s", + err_or_fn, debug.traceback(co))) + end + + if coroutine.status(co) == 'dead' then + return + end + + assert(err_or_fn is function, "type error :: expected func") + + local args = {select(4, unpack(ret))} + args[nargs] = step + err_or_fn(unpack(args, 1, nargs)) + end + + step(...) + end +end + +---An async function that when called will yield to the Neovim scheduler to be +---able to call the API. +M.scheduler = M.wrap(vim.schedule, 1) as function() + +return M as Async diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/cache.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/cache.tl new file mode 100644 index 0000000..194af80 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/cache.tl @@ -0,0 +1,85 @@ +local Hunk = require("gitsigns.hunks").Hunk +local GitObj = require('gitsigns.git').Obj + +local record M + record CacheEntry + file : string + base : string + compare_text : {string} -- For use with internal diff + hunks : {Hunk} + force_next_update: boolean + staged_diffs : {Hunk} + gitdir_watcher : vim.loop.FSPollObj -- Timer object watching the gitdir + git_obj : GitObj + commit : string + + get_compare_rev: function(CacheEntry, base: string): string + get_rev_bufname: function(self: CacheEntry, rev: string): string + invalidate: function(CacheEntry) + new: function(CacheEntry): CacheEntry + destroy: function(CacheEntry) + end + + record CacheObj + {CacheEntry} + + destroy: function(CacheObj, bufnr: integer) + end + + cache: CacheObj +end + +local CacheEntry = M.CacheEntry + +CacheEntry.get_compare_rev = function(self: CacheEntry, base: string): string + base = base or self.base + if base then + return base + end + + if self.commit then + -- Buffer is a fugutive commit so compare against the parent of the commit + return string.format('%s^', self.commit) + end + + local stage = self.git_obj.has_conflicts and 1 or 0 + return string.format(':%d', stage) +end + +CacheEntry.get_rev_bufname = function(self: CacheEntry, rev: string): string + rev = rev or self:get_compare_rev() + return string.format( + 'gitsigns://%s/%s:%s', + self.git_obj.repo.gitdir, + rev, + self.git_obj.relpath + ) +end + +CacheEntry.invalidate = function(self: CacheEntry) + self.compare_text = nil + self.hunks = nil +end + +CacheEntry.new = function(o: CacheEntry): CacheEntry + o.staged_diffs = o.staged_diffs or {} + return setmetatable(o, {__index = CacheEntry}) +end + +CacheEntry.destroy = function(self: CacheEntry) + local w = self.gitdir_watcher + if w and not w:is_closing() then + w:close() + end +end + +M.CacheObj.destroy = function(self: M.CacheObj, bufnr: integer) + self[bufnr]:destroy() + self[bufnr] = nil +end + +M.cache = setmetatable({}, { + __index = M.CacheObj, +}) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/config.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/config.tl new file mode 100644 index 0000000..9d7a118 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/config.tl @@ -0,0 +1,834 @@ +local warn: function(string, ...: any) +do + -- this is included in gen_help.lua so don't error if requires fail + local ok, ret = pcall(require, 'gitsigns.message') + if ok then + warn = ret.warn + end +end + +local record SchemaElem + type: string|{string} + deep_extend: boolean + default: any + + record Deprecated + new_field: string + message: string + hard: boolean + end + + deprecated: Deprecated|boolean + + default_help: string + description: string +end + +local record M + record Config + debug_mode: boolean + + record DiffOpts + algorithm: string + internal: boolean + indent_heuristic: boolean + vertical: boolean + linematch: integer + end + + diff_opts: DiffOpts + base: string + + record SignConfig + show_count: boolean + hl: string + text: string + numhl: string + linehl: string + keymaps: {string:string} + end + + enum SignType + 'add' + 'change' + 'delete' + 'topdelete' + 'changedelete' + 'untracked' + end + + type SignsConfig = {SignType: SignConfig} + + signs: SignsConfig + + count_chars: {string|integer:string} + signcolumn: boolean + numhl: boolean + linehl: boolean + show_deleted: boolean + sign_priority: integer + keymaps: {string:any} + _on_attach_pre: function(bufnr: integer, callback: function(table)) + on_attach: function(bufnr: integer) + record watch_gitdir + enable: boolean + interval: integer + follow_files: boolean + end + max_file_length: integer + update_debounce: integer + status_formatter: function({string:any}): string + + current_line_blame: boolean + + record current_line_blame_formatter_opts + relative_time: boolean + end + + current_line_blame_formatter: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}} + current_line_blame_formatter_nc: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}} + + record current_line_blame_opts + virt_text: boolean + + enum VirtTextPos + 'eol' + 'overlay' + 'right_align' + end + virt_text_pos: VirtTextPos + + delay: integer + ignore_whitespace: boolean + virt_text_priority: integer + end + + preview_config: {string:any} + attach_to_untracked: boolean + + record yadm + enable: boolean + end + + trouble: boolean + + record Worktree + toplevel: string + gitdir: string + end + worktrees: {Worktree} + + -- Undocumented + word_diff: boolean + _refresh_staged_on_update: boolean + _blame_cache: boolean + _threaded_diff: boolean + _extmark_signs: boolean + + _git_version: string + _verbose: boolean + end + + schema: {string:SchemaElem} + config: Config +end + +M.config = {} + +M.schema = { + signs = { + type = 'table', + deep_extend = true, + default = { + add = {hl = 'GitSignsAdd' , text = '┃', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + change = {hl = 'GitSignsChange', text = '┃', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + delete = {hl = 'GitSignsDelete', text = '▁', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + topdelete = {hl = 'GitSignsDelete', text = '▔', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, + untracked = {hl = 'GitSignsAdd' , text = '┆', numhl='GitSignsAddNr' , linehl='GitSignsAddLn' }, + }, + description = [[ + Configuration for signs: + • `hl` specifies the highlight group to use for the sign. + • `text` specifies the character to use for the sign. + • `numhl` specifies the highlight group to use for the number column + (see |gitsigns-config.numhl|). + • `linehl` specifies the highlight group to use for the line + (see |gitsigns-config.linehl|). + • `show_count` to enable showing count of hunk, e.g. number of deleted + lines. + + Note if a highlight is not defined, it will be automatically derived by + searching for other defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*Gutter` + • `diff*` + • `Diff*` + + For example if `GitSignsAdd` is not defined but `GitGutterAdd` is defined, + then `GitSignsAdd` will be linked to `GitGutterAdd`. + ]] + }, + + keymaps = { + deprecated = { + message = "config.keymaps is now deprecated. Please define mappings in config.on_attach() instead." + }, + type = 'table', + default = {}, + description = [[ + Keymaps to set up when attaching to a buffer. + + Each key in the table defines the mode and key (whitespace delimited) + for the mapping and the value defines what the key maps to. The value + can be a table which can contain keys matching the options defined in + |map-arguments| which are: `expr`, `noremap`, `nowait`, `script`, `silent` + and `unique`. These options can also be used in the top level of the + table to define default options for all mappings. + + Since this field is not extended (unlike |gitsigns-config-signs|), + mappings defined in this field can be disabled by setting the whole field + to `{}`, and |gitsigns-config-on_attach| can instead be used to define + mappings. + ]] + }, + + worktrees = { + type = 'table', + default = nil, + description = [[ + Detached working trees. + + Array of tables with the keys `gitdir` and `toplevel`. + + If normal attaching fails, then each entry in the table is attempted + with the work tree details set. + + Example: > + worktrees = { + { + toplevel = vim.env.HOME, + gitdir = vim.env.HOME .. '/projects/dotfiles/.git' + } + } + ]] + }, + + _on_attach_pre = { + type = 'function', + default = nil, + description = [[ + Asynchronous hook called before attaching to a buffer. Mainly used to + configure detached worktrees. + + This callback must call its callback argument. The callback argument can + accept an optional table argument with the keys: 'gitdir' and 'toplevel'. + + Example: > + on_attach_pre = function(bufnr, callback) + ... + callback { + gitdir = ..., + toplevel = ... + } + end +< + ]] + }, + + on_attach = { + type = 'function', + default = nil, + description = [[ + Callback called when attaching to a buffer. Mainly used to setup keymaps + when `config.keymaps` is empty. The buffer number is passed as the first + argument. + + This callback can return `false` to prevent attaching to the buffer. + + Example: > + on_attach = function(bufnr) + if vim.api.nvim_buf_get_name(bufnr):match() then + -- Don't attach to specific buffers whose name matches a pattern + return false + end + + -- Setup keymaps + vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) + ... -- More keymaps + end +< + ]] + }, + + watch_gitdir = { + type = 'table', + deep_extend = true, + default = { + enable = true, + interval = 1000, + follow_files = true + }, + description = [[ + When opening a file, a libuv watcher is placed on the respective + `.git` directory to detect when changes happen to use as a trigger to + update signs. + + Fields: ~ + • `enable`: + Whether the watcher is enabled. + + • `interval`: + Interval the watcher waits between polls of the gitdir in milliseconds. + + • `follow_files`: + If a file is moved with `git mv`, switch the buffer to the new location. + ]] + }, + + sign_priority = { + type = 'number', + default = 6, + description = [[ + Priority to use for signs. + ]] + }, + + signcolumn = { + type = 'boolean', + default = true, + description = [[ + Enable/disable symbols in the sign column. + + When enabled the highlights defined in `signs.*.hl` and symbols defined + in `signs.*.text` are used. + ]] + }, + + numhl = { + type = 'boolean', + default = false, + description = [[ + Enable/disable line number highlights. + + When enabled the highlights defined in `signs.*.numhl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + ]] + }, + + linehl = { + type = 'boolean', + default = false, + description = [[ + Enable/disable line highlights. + + When enabled the highlights defined in `signs.*.linehl` are used. If + the highlight group does not exist, then it is automatically defined + and linked to the corresponding highlight group in `signs.*.hl`. + ]] + }, + + show_deleted = { + type = 'boolean', + default = false, + description = [[ + Show the old version of hunks inline in the buffer (via virtual lines). + + Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. + ]] + }, + + diff_opts = { + type = 'table', + deep_extend = true, + default = function(): {string:any} + local r: M.Config.DiffOpts = { + algorithm = 'myers', + internal = false, + indent_heuristic = false, + vertical = true, + linematch = nil + } + for _, o in ipairs(vim.opt.diffopt:get()) do + if o == 'indent-heuristic' then + r.indent_heuristic = true + elseif o == 'internal' then + if vim.diff then + r.internal = true + elseif jit and jit.os ~= "Windows" then + -- Use FFI + r.internal = true + end + elseif o == 'horizontal' then + r.vertical = false + elseif vim.startswith(o, 'algorithm:') then + r.algorithm = string.sub(o, ('algorithm:'):len() + 1) + elseif vim.startswith(o, 'linematch:') then + r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) as integer + end + end + return r + end, + default_help = "derived from 'diffopt'", + description = [[ + Diff options. + + Fields: ~ + • algorithm: string + Diff algorithm to use. Values: + • "myers" the default algorithm + • "minimal" spend extra time to generate the + smallest possible diff + • "patience" patience diff algorithm + • "histogram" histogram diff algorithm + • internal: boolean + Use Neovim's built in xdiff library for running diffs. + + Note Neovim v0.5 uses LuaJIT's FFI interface, whereas v0.5+ uses + `vim.diff`. + • indent_heuristic: boolean + Use the indent heuristic for the internal + diff library. + • vertical: boolean + Start diff mode with vertical splits. + • linematch: integer + Enable second-stage diff on hunks to align lines. + Requires `internal=true`. + ]] + }, + + base = { + type = 'string', + default = nil, + default_help = 'index', + description = [[ + The object/revision to diff against. + See |gitsigns-revision|. + ]] + }, + + count_chars = { + type = 'table', + default = { + [1] = '1', -- '₁', + [2] = '2', -- '₂', + [3] = '3', -- '₃', + [4] = '4', -- '₄', + [5] = '5', -- '₅', + [6] = '6', -- '₆', + [7] = '7', -- '₇', + [8] = '8', -- '₈', + [9] = '9', -- '₉', + ['+'] = '>', -- '₊', + }, + description = [[ + The count characters used when `signs.*.show_count` is enabled. The + `+` entry is used as a fallback. With the default, any count outside + of 1-9 uses the `>` character in the sign. + + Possible use cases for this field: + • to specify unicode characters for the counts instead of 1-9. + • to define characters to be used for counts greater than 9. + ]] + }, + + status_formatter = { + type = 'function', + default = function(status: {string:number}): string + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then table.insert(status_txt, '+'..added ) end + if changed and changed > 0 then table.insert(status_txt, '~'..changed) end + if removed and removed > 0 then table.insert(status_txt, '-'..removed) end + return table.concat(status_txt, ' ') + end, + default_help = [[function(status) + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then table.insert(status_txt, '+'..added ) end + if changed and changed > 0 then table.insert(status_txt, '~'..changed) end + if removed and removed > 0 then table.insert(status_txt, '-'..removed) end + return table.concat(status_txt, ' ') + end]], + description = [[ + Function used to format `b:gitsigns_status`. + ]] + }, + + max_file_length = { + type = 'number', + default = 40000, + description = [[ + Max file length (in lines) to attach to. + ]] + }, + + preview_config = { + type = 'table', + deep_extend = true, + default = { + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1 + }, + description = [[ + Option overrides for the Gitsigns preview window. Table is passed directly + to `nvim_open_win`. + ]] + }, + + attach_to_untracked = { + type = 'boolean', + default = true, + description = [[ + Attach to untracked files. + ]] + }, + + update_debounce = { + type = 'number', + default = 100, + description = [[ + Debounce time for updates (in milliseconds). + ]] + }, + + current_line_blame = { + type = 'boolean', + default = false, + description = [[ + Adds an unobtrusive and customisable blame annotation at the end of + the current line. + + The highlight group used for the text is `GitSignsCurrentLineBlame`. + ]] + }, + + current_line_blame_opts = { + type = 'table', + deep_extend = true, + default = { + virt_text = true, + virt_text_pos = 'eol', + virt_text_priority = 100, + delay = 1000 + }, + description = [[ + Options for the current line blame annotation. + + Fields: ~ + • virt_text: boolean + Whether to show a virtual text blame annotation. + • virt_text_pos: string + Blame annotation position. Available values: + `eol` Right after eol character. + `overlay` Display over the specified column, without + shifting the underlying text. + `right_align` Display right aligned in the window. + • delay: integer + Sets the delay (in milliseconds) before blame virtual text is + displayed. + • ignore_whitespace: boolean + Ignore whitespace when running blame. + • virt_text_priority: integer + Priority of virtual text. + ]] + }, + + current_line_blame_formatter_opts = { + type = 'table', + deep_extend = true, + deprecated = true, + default = { + relative_time = false + }, + description = [[ + Options for the current line blame annotation formatter. + + Fields: ~ + • relative_time: boolean + ]] + }, + + current_line_blame_formatter = { + type = {'string', 'function'}, + default = ' , - ', + description = [[ + String or function used to format the virtual text of + |gitsigns-config-current_line_blame|. + + When a string, accepts the following format specifiers: + + • `` + • `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` or `` + • `` + • `` + • `` + • `` + + For `` and ``, `FORMAT` can + be any valid date format that is accepted by `os.date()` with the + addition of `%R` (defaults to `%Y-%m-%d`): + + • `%a` abbreviated weekday name (e.g., Wed) + • `%A` full weekday name (e.g., Wednesday) + • `%b` abbreviated month name (e.g., Sep) + • `%B` full month name (e.g., September) + • `%c` date and time (e.g., 09/16/98 23:48:10) + • `%d` day of the month (16) [01-31] + • `%H` hour, using a 24-hour clock (23) [00-23] + • `%I` hour, using a 12-hour clock (11) [01-12] + • `%M` minute (48) [00-59] + • `%m` month (09) [01-12] + • `%p` either "am" or "pm" (pm) + • `%S` second (10) [00-61] + • `%w` weekday (3) [0-6 = Sunday-Saturday] + • `%x` date (e.g., 09/16/98) + • `%X` time (e.g., 23:48:10) + • `%Y` full year (1998) + • `%y` two-digit year (98) [00-99] + • `%%` the character `%´ + • `%R` relative (e.g., 4 months ago) + + When a function: + Parameters: ~ + {name} Git user name returned from `git config user.name` . + {blame_info} Table with the following keys: + • `abbrev_sha`: string + • `orig_lnum`: integer + • `final_lnum`: integer + • `author`: string + • `author_mail`: string + • `author_time`: integer + • `author_tz`: string + • `committer`: string + • `committer_mail`: string + • `committer_time`: integer + • `committer_tz`: string + • `summary`: string + • `previous`: string + • `filename`: string + + Note that the keys map onto the output of: + `git blame --line-porcelain` + + {opts} Passed directly from + |gitsigns-config-current_line_blame_formatter_opts|. + + Return: ~ + The result of this function is passed directly to the `opts.virt_text` + field of |nvim_buf_set_extmark| and thus must be a list of + [text, highlight] tuples. + ]] + }, + + current_line_blame_formatter_nc = { + type = {'string', 'function'}, + default = ' ', + description = [[ + String or function used to format the virtual text of + |gitsigns-config-current_line_blame| for lines that aren't committed. + + See |gitsigns-config-current_line_blame_formatter| for more information. + ]] + }, + + trouble = { + type = 'boolean', + default = function(): boolean + local has_trouble = pcall(require, 'trouble') + return has_trouble + end, + default_help = "true if installed", + description = [[ + When using setqflist() or setloclist(), open Trouble instead of the + quickfix/location list window. + ]] + }, + + yadm = { + type = 'table', + default = { enable = false }, + description = [[ + yadm configuration. + ]] + }, + + _git_version = { + type = 'string', + default = 'auto', + description = [[ + Version of git available. Set to 'auto' to automatically detect. + ]] + }, + + _verbose = { + type = 'boolean', + default = false, + description = [[ + More verbose debug message. Requires debug_mode=true. + ]] + }, + + word_diff = { + type = 'boolean', + default = false, + description = [[ + Highlight intra-line word differences in the buffer. + Requires `config.diff_opts.internal = true` . + + Uses the highlights: + • For word diff in previews: + • `GitSignsAddInline` + • `GitSignsChangeInline` + • `GitSignsDeleteInline` + • For word diff in buffer: + • `GitSignsAddLnInline` + • `GitSignsChangeLnInline` + • `GitSignsDeleteLnInline` + • For word diff in virtual lines (e.g. show_deleted): + • `GitSignsAddVirtLnInline` + • `GitSignsChangeVirtLnInline` + • `GitSignsDeleteVirtLnInline` + ]] + }, + + _refresh_staged_on_update = { + type = 'boolean', + default = false, + description = [[ + Always refresh the staged file on each update. Disabling this will cause + the staged file to only be refreshed when an update to the index is + detected. + ]] + }, + + _blame_cache = { + type = 'boolean', + default = true, + description = [[ + Cache blame results for current_line_blame + ]] + }, + + _threaded_diff = { + type = 'boolean', + default = false, + description = [[ + Run diffs on a separate thread + ]] + }, + + _extmark_signs = { + type = 'boolean', + default = false, + description = [[ + Use extmarks for placing signs. + ]] + }, + + debug_mode = { + type = 'boolean', + default = false, + description = [[ + Enables debug logging and makes the following functions + available: `dump_cache`, `debug_messages`, `clear_debug`. + ]] + }, + +} + +warn = function(s: string, ...: any) + vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'}) +end + +local function validate_config(config: {string:any}) + for k, v in pairs(config) do + local kschema = M.schema[k] + if kschema == nil then + warn("gitsigns: Ignoring invalid configuration field '%s'", k) + elseif kschema.type then + if type(kschema.type) == 'string' then + vim.validate { + [k] = { v, kschema.type } as {any}; + } + end + end + end +end + +local function resolve_default(v: SchemaElem): any + if type(v.default) == 'function' and v.type ~= 'function' then + return (v.default as function)() + else + return v.default + end +end + +local function handle_deprecated(cfg: {string:any}) + for k, v in pairs(M.schema) do + local dep = v.deprecated + if dep and cfg[k] ~= nil then + if dep is SchemaElem.Deprecated then + if dep.new_field then + local opts_key, field = dep.new_field:match('(.*)%.(.*)') + if opts_key and field then + -- Field moved to an options table + local opts = (cfg[opts_key] or {}) as {string:any} + opts[field] = cfg[k] + cfg[opts_key] = opts + else + -- Field renamed + cfg[dep.new_field] = cfg[k] + end + end + + if dep.hard then + if dep.message then + warn(dep.message) + elseif dep.new_field then + warn('%s is now deprecated, please use %s', k, dep.new_field) + else + warn('%s is now deprecated; ignoring', k) + end + end + end + end + end +end + +function M.build(user_config: {string:any}) + user_config = user_config or {} + + handle_deprecated(user_config) + + validate_config(user_config) + + local config = M.config as {string:any} + for k, v in pairs(M.schema) do + if user_config[k] ~= nil then + if v.deep_extend then + local d = resolve_default(v) + config[k] = vim.tbl_deep_extend('force', d as table, user_config[k] as table) + else + config[k] = user_config[k] + end + else + config[k] = resolve_default(v) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/current_line_blame.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/current_line_blame.tl new file mode 100644 index 0000000..87fcbcd --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/current_line_blame.tl @@ -0,0 +1,213 @@ +local a = require('gitsigns.async') +local wrap = a.wrap +local void = a.void +local scheduler = a.scheduler + +local cache = require('gitsigns.cache').cache +local config = require('gitsigns.config').config +local BlameInfo = require('gitsigns.git').BlameInfo +local util = require('gitsigns.util') +local uv = require('gitsigns.uv') + +local api = vim.api + +local current_buf = api.nvim_get_current_buf + +local namespace = api.nvim_create_namespace('gitsigns_blame') + +local timer = uv.new_timer(true) + +local record M + setup: function() +end + +local wait_timer = wrap(vim.loop.timer_start, 4) + +local function set_extmark(bufnr: integer, row: integer, opts: {string:any}) + opts = opts or {} + opts.id = 1 + api.nvim_buf_set_extmark(bufnr, namespace, row-1, 0, opts) +end + +local function get_extmark(bufnr: integer): integer + local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) as {integer, integer} + if pos[1] then + return pos[1] + 1 + end + return +end + +local function reset(bufnr: integer) + bufnr = bufnr or current_buf() + api.nvim_buf_del_extmark(bufnr, namespace, 1) + vim.b[bufnr].gitsigns_blame_line_dict = nil +end + +-- TODO: expose as config +local max_cache_size = 1000 + +local record BlameCache + record Elem + tick: integer + cache: {integer:BlameInfo} + size: integer + end + contents: {integer:Elem} +end + +BlameCache.contents = {} + +function BlameCache:add(bufnr: integer, lnum: integer, x: BlameInfo) + if not config._blame_cache then return end + local scache = self.contents[bufnr] + if scache.size <= max_cache_size then + scache.cache[lnum] = x + scache.size = scache.size + 1 + end +end + +function BlameCache:get(bufnr: integer, lnum: integer): BlameInfo + if not config._blame_cache then return end + + -- init and invalidate + local tick = vim.b[bufnr].changedtick + if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then + self.contents[bufnr] = {tick = tick, cache = {}, size = 0} + end + + return self.contents[bufnr].cache[lnum] +end + +local function expand_blame_format(fmt: string, name: string, info: BlameInfo): string + if info.author == name then + info.author = 'You' + end + return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time) +end + +local function flatten_virt_text(virt_text: {{string, string}}): string + local res = {} + for _, part in ipairs(virt_text) do + res[#res+1] = part[1] + end + return table.concat(res) +end + +-- Update function, must be called in async context +local update = void(function() + local bufnr = current_buf() + local lnum = api.nvim_win_get_cursor(0)[1] + + local old_lnum = get_extmark(bufnr) + if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then + -- Don't update if on the same line and we already have results + return + end + + if api.nvim_get_mode().mode == 'i' then + reset(bufnr) + return + end + + -- Set an empty extmark to save the line number. + -- This will also clear virt_text. + -- Only do this if there was already an extmark to avoid clearing the intro + -- text. + if get_extmark(bufnr) then + reset(bufnr) + set_extmark(bufnr, lnum) + end + + -- Can't show extmarks on folded lines so skip + if vim.fn.foldclosed(lnum) ~= -1 then + return + end + + local opts = config.current_line_blame_opts + + -- Note because the same timer is re-used, this call has a debouncing effect. + wait_timer(timer, opts.delay, 0) + scheduler() + + local bcache = cache[bufnr] + if not bcache or not bcache.git_obj.object_name then + return + end + + local result = BlameCache:get(bufnr, lnum) + if not result then + local buftext = util.buf_lines(bufnr) + result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + BlameCache:add(bufnr, lnum, result) + scheduler() + end + + local lnum1 = api.nvim_win_get_cursor(0)[1] + if bufnr == current_buf() and lnum ~= lnum1 then + -- Cursor has moved during events; abort + return + end + + if not api.nvim_buf_is_loaded(bufnr) then + -- Buffer is no longer loaded; abort + return + end + + vim.b[bufnr].gitsigns_blame_line_dict = result + + if result then + local virt_text: {{string, string}} + local clb_formatter = result.author == 'Not Committed Yet' and + config.current_line_blame_formatter_nc or + config.current_line_blame_formatter + if clb_formatter is string then + virt_text = {{ + expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result), + 'GitSignsCurrentLineBlame' + }} + else -- function + virt_text = clb_formatter( + bcache.git_obj.repo.username, + result, + config.current_line_blame_formatter_opts + ) + end + + vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text) + + if opts.virt_text then + set_extmark(bufnr, lnum, { + virt_text = virt_text, + virt_text_pos = opts.virt_text_pos, + priority = opts.virt_text_priority, + hl_mode = 'combine', + }) + end + end +end) + +M.setup = function() + api.nvim_create_augroup('gitsigns_blame', {}) + + for k, _ in pairs(cache as {integer:any}) do + reset(k) + end + + if config.current_line_blame then + api.nvim_create_autocmd( + {'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI'}, + { group = 'gitsigns_blame', callback = function() update() end } + ) + + api.nvim_create_autocmd( + {'InsertEnter', 'FocusLost', 'BufLeave'}, + { group = 'gitsigns_blame', callback = function() reset() end } + ) + + -- Call via vim.schedule to avoid the debounce timer killing the async + -- coroutine + vim.schedule(update) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debounce.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debounce.tl new file mode 100644 index 0000000..34d7c00 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debounce.tl @@ -0,0 +1,84 @@ +local uv = require('gitsigns.uv') + +local record M + throttle_by_id: function (fn: function(T1 ), schedule: boolean): function(T1) + throttle_by_id: function(fn: function(T1,T2), schedule: boolean): function(T1,T2) + + debounce_trailing: function(ms: number, fn: function(T1,T2), schedule: boolean): function(T1,T2) +end + +--- Debounces a function on the trailing edge. +--- +---@param ms (number) Timeout in ms +---@param fn (function) Function to debounce +---@returns (function) Debounced function. +function M.debounce_trailing(ms: number, fn: function): function + local timer = uv.new_timer(true) + return function(...) + local argv = {...} + timer:start(ms, 0, function() + timer:stop() + fn(unpack(argv)) + end) + end +end + + +--- Throttles a function on the leading edge. +--- +--@param ms (number) Timeout in ms +--@param fn (function) Function to throttle +--@returns (function) throttled function. +function M.throttle_leading(ms: number, fn: function): function + local timer = uv.new_timer(true) + local running = false + return function(...) + if not running then + timer:start(ms, 0, function() + running = false + timer:stop() + end) + running = true + fn(...) + end + end +end + +--- Throttles a function using the first argument as an ID +--- +--- If function is already running then the function will be scheduled to run +--- again once the running call has finished. +--- +--- fn#1 _/‾\__/‾\_/‾\_____________________________ +--- throttled#1 _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\/‾‾‾‾‾‾‾‾‾‾\____________ +-- +--- fn#2 ______/‾\___________/‾\___________________ +--- throttled#2 ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________ +--- +-- +--@param fn (function) Function to throttle +--@returns (function) throttled function. +function M.throttle_by_id(fn: function, schedule: boolean): function + local scheduled: {any:boolean} = {} + local running: {any:boolean} = {} + return function(id: any, ...) + if scheduled[id] then + -- If fn is already scheduled, then drop + return + end + if not running[id] or schedule then + scheduled[id] = true + end + if running[id] then + return + end + while scheduled[id] do + scheduled[id] = nil + running[id] = true + fn(id, ...) + running[id] = nil + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debug.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debug.tl new file mode 100644 index 0000000..39716f2 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/debug.tl @@ -0,0 +1,150 @@ +local M = { + debug_mode = false, + verbose = false, + messages: {string} = {} +} + +local function getvarvalue(name: string, lvl: integer): any + lvl = lvl + 1 + local value: any + local found: boolean + + -- try local variables + local i = 1 + while true do + local n, v = debug.getlocal(lvl as function, i) as (string, any) + if not n then break end + if n == name then + value = v + found = true + end + i = i + 1 + end + if found then return value end + + -- try upvalues + local func = debug.getinfo(lvl).func as function + i = 1 + while true do + local n, v = debug.getupvalue(func, i) as (string, any) + if not n then break end + if n == name then return v end + i = i + 1 + end + + -- not found; get global + return getfenv(func)[name] +end + +local function get_context(lvl: integer): table + lvl = lvl + 1 + local ret: table = {} + ret.name = getvarvalue('__FUNC__', lvl) as string + if not ret.name then + local name0 = debug.getinfo(lvl, 'n').name or '' + ret.name = name0:gsub('(.*)%d+$', '%1') + end + ret.bufnr = getvarvalue('bufnr', lvl) + or getvarvalue('_bufnr', lvl) + or getvarvalue('cbuf', lvl) + or getvarvalue('buf', lvl) + + return ret +end + +-- If called in a callback then make sure the callback defines a __FUNC__ +-- variable which can be used to identify the name of the function. +local function cprint(obj: any, lvl: integer) + lvl = lvl + 1 + local msg = obj is string and obj or vim.inspect(obj) + local ctx = get_context(lvl) + local msg2: string + if ctx.bufnr then + msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg) + else + msg2 = string.format('%s: %s', ctx.name, msg) + end + table.insert(M.messages, msg2) +end + +function M.dprint(obj: any) + if not M.debug_mode then return end + cprint(obj, 2) +end + +function M.dprintf(obj: string, ...:any) + if not M.debug_mode then return end + cprint(obj:format(...), 2) +end + +function M.vprint(obj: any) + if not (M.debug_mode and M.verbose) then return end + cprint(obj, 2) +end + +function M.vprintf(obj: string, ...:any) + if not (M.debug_mode and M.verbose) then return end + cprint(obj:format(...), 2) +end + +local function eprint(msg: string, level: integer) + local info = debug.getinfo(level+2, 'Sl') + if info then + msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg) + end + M.messages[#M.messages+1] = msg + if M.debug_mode then + error(msg) + end +end + +function M.eprint(msg: string) + eprint(msg, 1) +end + +function M.eprintf(fmt: string, ...:any) + eprint(fmt:format(...), 1) +end + +local function process(raw_item: any, path: {string}): any + if path[#path] == vim.inspect.METATABLE then + return nil + elseif raw_item is function then + return nil + elseif raw_item is table then + local key = path[#path] + if key == 'compare_text' then + local item = raw_item as {string} + return { '...', length=#item, head=item[1] } + elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then + return { '...', length=#vim.tbl_keys(raw_item) } + end + end + return raw_item +end + +function M.add_debug_functions(cache: any): {string:function} + local R: {string:function} = {} + R.dump_cache = function(): any + local text = vim.inspect(cache, { process = process }) + vim.api.nvim_echo({{text}}, false, {}) + return cache + end + + R.debug_messages = function(noecho: boolean): {string} + if not noecho then + for _, m in ipairs(M.messages) do + vim.api.nvim_echo({{m}}, false, {}) + end + end + return M.messages + end + + R.clear_debug = function() + M.messages = {} + end + + return R +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff.tl new file mode 100644 index 0000000..5505279 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff.tl @@ -0,0 +1,18 @@ +local config = require('gitsigns.config').config +local Hunk = require('gitsigns.hunks').Hunk + +return function(a: {string}, b: {string}, linematch: boolean): {Hunk} + local diff_opts = config.diff_opts + local f: function({string}, {string}, string, boolean, integer): {Hunk} + if diff_opts.internal then + f = require('gitsigns.diff_int').run_diff + else + f = require('gitsigns.diff_ext').run_diff + end + + local linematch0: integer + if linematch ~= false then + linematch0 = diff_opts.linematch + end + return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0) +end diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_ext.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_ext.tl new file mode 100644 index 0000000..59b6f2c --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_ext.tl @@ -0,0 +1,80 @@ +local git_diff = require('gitsigns.git').diff + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk +local util = require('gitsigns.util') +local scheduler = require('gitsigns.async').scheduler + +local record M + -- Async function + run_diff: function({string}, {string}, string, boolean): {Hunk} +end + +local function write_to_file(path: string, text: {string}) + local f, err = io.open(path, 'wb') + if f == nil then + error(err) + end + for _, l in ipairs(text) do + f:write(l) + f:write('\n') + end + f:close() +end + +M.run_diff = function( + text_cmp: {string}, + text_buf: {string}, + diff_algo: string, + indent_heuristic: boolean +): {Hunk} + local results: {Hunk} = {} + + -- tmpname must not be called in a callback + if vim.in_fast_event() then + scheduler() + end + + local file_buf = util.tmpname() + local file_cmp = util.tmpname() + + write_to_file(file_buf, text_buf) + write_to_file(file_cmp, text_cmp) + + -- Taken from gitgutter, diff.vim: + -- + -- If a file has CRLF line endings and git's core.autocrlf is true, the file + -- in git's object store will have LF line endings. Writing it out via + -- git-show will produce a file with LF line endings. + -- + -- If this last file is one of the files passed to git-diff, git-diff will + -- convert its line endings to CRLF before diffing -- which is what we want + -- but also by default outputs a warning on stderr. + -- + -- warning: LF will be replace by CRLF in . + -- The file will have its original line endings in your working directory. + -- + -- We can safely ignore the warning, we turn it off by passing the '-c + -- "core.safecrlf=false"' argument to git-diff. + + local out = git_diff(file_cmp, file_buf, indent_heuristic, diff_algo) + + for _, line in ipairs(out) do + if vim.startswith(line, '@@') then + results[#results+1] = gs_hunks.parse_diff_line(line) + elseif #results > 0 then + local r = results[#results] + if line:sub(1, 1) == '-' then + r.removed.lines[#r.removed.lines+1] = line:sub(2) + elseif line:sub(1, 1) == '+' then + r.added.lines[#r.added.lines+1] = line:sub(2) + end + end + end + + os.remove(file_buf) + os.remove(file_cmp) + return results +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int.tl new file mode 100644 index 0000000..e3ee079 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int.tl @@ -0,0 +1,153 @@ +local create_hunk = require("gitsigns.hunks").create_hunk +local Hunk = require('gitsigns.hunks').Hunk +local config = require('gitsigns.config').config +local async = require('gitsigns.async') + +local record M + run_diff: function({string}, {string}, string, boolean, integer): {Hunk} + run_word_diff: function({string}, {string}): {Region}, {Region} +end + +local type DiffFun = function({string}, {string}, string, boolean, integer): {DiffResult} +local type DiffResult = {integer, integer, integer, integer} + +local run_diff_xdl = function( + fa: {string}, fb: {string}, + algorithm: string, indent_heuristic: boolean, + linematch: integer + ): {DiffResult} + + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n' + + return vim.diff(a, b, { + result_type = 'indices', + algorithm = algorithm, + indent_heuristic = indent_heuristic, + linematch = linematch + }) +end + +local run_diff_xdl_async = async.wrap(function( + fa: {string}, fb: {string}, + algorithm: string, indent_heuristic: boolean, + linematch: integer, + callback: function({DiffResult}) + ) + + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n' + + vim.loop.new_work(function( + a0: string, b0: string, + algorithm0: string, indent_heuristic0: boolean, + linematch0: integer + ): string + return vim.mpack.encode(vim.diff(a0, b0, { + result_type = 'indices', + algorithm = algorithm0, + indent_heuristic = indent_heuristic0, + linematch = linematch0 + })) + end, function(r: string) + callback(vim.mpack.decode(r) as {DiffResult}) + end):queue(a, b, algorithm, indent_heuristic, linematch) +end, 6) + +if not vim.diff then + run_diff_xdl = require('gitsigns.diff_int.xdl_diff_ffi') +end + +M.run_diff = async.void(function( + fa: {string}, fb: {string}, + diff_algo: string, indent_heuristic: boolean, + linematch: integer +): {Hunk} + local run_diff0: DiffFun + if config._threaded_diff and vim.is_thread then + run_diff0 = run_diff_xdl_async + else + run_diff0 = run_diff_xdl + end + + local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch) + + local hunks: {Hunk} = {} + + for _, r in ipairs(results) do + local rs, rc, as, ac = unpack(r) + local hunk = create_hunk(rs, rc, as, ac) + if rc > 0 then + for i = rs, rs+rc-1 do + hunk.removed.lines[#hunk.removed.lines+1] = fa[i] or '' + end + end + if ac > 0 then + for i = as, as+ac-1 do + hunk.added.lines[#hunk.added.lines+1] = fb[i] or '' + end + end + hunks[#hunks+1] = hunk + end + + return hunks +end) + +local type Region = {integer, string, integer, integer} + +local gaps_between_regions = 5 + +local function denoise_hunks(hunks: {Hunk}): {Hunk} + -- Denoise the hunks + local ret = {hunks[1]} + for j = 2, #hunks do + local h, n = ret[#ret], hunks[j] + if not h or not n then break end + if n.added.start - h.added.start - h.added.count < gaps_between_regions then + h.added.count = n.added.start + n.added.count - h.added.start + h.removed.count = n.removed.start + n.removed.count - h.removed.start + + if h.added.count > 0 or h.removed.count > 0 then + h.type = 'change' + end + else + ret[#ret+1] = n + end + end + return ret +end + +function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region} + local adds: {Region} = {} + local rems: {Region} = {} + + if #removed ~= #added then + return rems, adds + end + + for i = 1, #removed do + -- pair lines by position + local a, b = vim.split(removed[i], ''), vim.split(added[i], '') + + local hunks: {Hunk} = {} + for _, r in ipairs(run_diff_xdl(a, b)) do + local rs, rc, as, ac = unpack(r) + + -- Balance of the unknown offset done in hunk_func + if rc == 0 then rs = rs + 1 end + if ac == 0 then as = as + 1 end + + hunks[#hunks+1] = create_hunk(rs, rc, as, ac) + end + + hunks = denoise_hunks(hunks) + + for _, h in ipairs(hunks) do + adds[#adds+1] = {i, h.type, h.added.start , h.added.start + h.added.count} + rems[#rems+1] = {i, h.type, h.removed.start, h.removed.start + h.removed.count} + end + end + return rems, adds +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int/xdl_diff_ffi.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int/xdl_diff_ffi.tl new file mode 100644 index 0000000..0b7b3f5 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diff_int/xdl_diff_ffi.tl @@ -0,0 +1,145 @@ +local ffi = require("ffi") + +ffi.cdef[[ + typedef struct s_mmbuffer { const char *ptr; long size; } mmbuffer_t; + + typedef struct s_xpparam { + unsigned long flags; + + // See Documentation/diff-options.txt. + char **anchors; + size_t anchors_nr; + } xpparam_t; + + typedef long (__stdcall *find_func_t)( + const char *line, + long line_len, + char *buffer, + long buffer_size, + void *priv + ); + + typedef int (__stdcall *xdl_emit_hunk_consume_func_t)( + long start_a, long count_a, long start_b, long count_b, + void *cb_data + ); + + typedef struct s_xdemitconf { + long ctxlen; + long interhunkctxlen; + unsigned long flags; + find_func_t find_func; + void *find_func_priv; + xdl_emit_hunk_consume_func_t hunk_func; + } xdemitconf_t; + + typedef struct s_xdemitcb { + void *priv; + int (__stdcall *outf)(void *, mmbuffer_t *, int); + } xdemitcb_t; + + int xdl_diff( + mmbuffer_t *mf1, + mmbuffer_t *mf2, + xpparam_t const *xpp, + xdemitconf_t const *xecfg, + xdemitcb_t *ecb + ); +]] + +local record MMBuffer + userdata + ptr: number + size: number +end + +local function setup_mmbuffer(lines: {string}): number, number + local text = vim.tbl_isempty(lines) and '' or table.concat(lines, '\n')..'\n' + return text as number, #text +end + +local record XPParam + userdata + flags: number -- unsigned long flags; + + -- char **anchors; + -- size_t anchors_nr; +end + +local function get_xpparam_flag(diff_algo: string): number + local daflag = 0 -- myers + + if diff_algo == 'minimal' then daflag = 1 + elseif diff_algo == 'patience' then daflag = math.floor(2^14) + elseif diff_algo == 'histogram' then daflag = math.floor(2^15) + end + + return daflag +end + +local record Long + userdata +end + +local record XDEmitConf + userdata + hunk_func: function(Long, Long, Long, Long, any): number +end + +-- local DIFF_FILLER = 0x001 -- display filler lines +-- local DIFF_IBLANK = 0x002 -- ignore empty lines +-- local DIFF_ICASE = 0x004 -- ignore case +-- local DIFF_IWHITE = 0x008 -- ignore change in white space +-- local DIFF_IWHITEALL = 0x010 -- ignore all white space changes +-- local DIFF_IWHITEEOL = 0x020 -- ignore change in white space at EOL +-- local DIFF_HORIZONTAL = 0x040 -- horizontal splits +-- local DIFF_VERTICAL = 0x080 -- vertical splits +-- local DIFF_HIDDEN_OFF = 0x100 -- diffoff when hidden +-- local DIFF_INTERNAL = 0x200 -- use internal xdiff algorithm +-- local DIFF_CLOSE_OFF = 0x400 -- diffoff when closing window +-- local DIFF_FOLLOWWRAP = 0x800 -- follow the wrap option + +local type DiffResult = {integer, integer, integer, integer} + +local mmba = ffi.new('mmbuffer_t') as MMBuffer +local mmbb = ffi.new('mmbuffer_t') as MMBuffer +local xpparam = ffi.new('xpparam_t') as XPParam +local emitcb = ffi.new('xdemitcb_t') as any + +local function run_diff_xdl(fa: {string}, fb: {string}, diff_algo: string): {DiffResult} + mmba.ptr, mmba.size = setup_mmbuffer(fa) + mmbb.ptr, mmbb.size = setup_mmbuffer(fb) + xpparam.flags = get_xpparam_flag(diff_algo) + + local results: {DiffResult} = {} + + local hunk_func = ffi.cast('xdl_emit_hunk_consume_func_t', function( + start_a: Long, count_a: Long, start_b: Long, count_b: Long + ): number + local ca = tonumber(count_a) as integer + local cb = tonumber(count_b) as integer + local sa = tonumber(start_a) as integer + local sb = tonumber(start_b) as integer + + -- Not fully sure why this offset is needed but xdiff does it too + -- (see neovim/src/nvim/xdiff/xutils.c:356,368) + if ca > 0 then sa = sa + 1 end + if cb > 0 then sb = sb + 1 end + + results[#results+1] = {sa, ca, sb, cb} + return 0 + end) + + local emitconf = ffi.new('xdemitconf_t') as XDEmitConf + emitconf.hunk_func = hunk_func as function(Long, Long, Long, Long): integer + + local ok = ffi.C.xdl_diff(mmba, mmbb, xpparam, emitconf, emitcb) + + hunk_func:free() + + return ok == 0 and results +end + +jit.off(run_diff_xdl) + +return run_diff_xdl diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diffthis.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diffthis.tl new file mode 100644 index 0000000..227e235 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/diffthis.tl @@ -0,0 +1,201 @@ +local api = vim.api + +local void = require('gitsigns.async').void +local scheduler = require('gitsigns.async').scheduler +local awrap = require('gitsigns.async').wrap + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry + +local util = require('gitsigns.util') +local manager = require('gitsigns.manager') +local message = require('gitsigns.message') + +local throttle_by_id = require('gitsigns.debounce').throttle_by_id + +local input = awrap(vim.ui.input, 2) + +local record M + record DiffthisOpts + vertical: boolean + split: string + end + + diffthis: function(base: string, opts: DiffthisOpts) + show: function(base: string) + update: function(bufnr: integer) +end + +local bufread = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry) + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local text: {string} + if util.calc_base(base) == util.calc_base(bcache.base) then + text = bcache.compare_text + else + local err: string + text, err = bcache.git_obj:get_show_text(comp_rev) + if err then + error(err, 2) + end + scheduler() + if vim.bo[bufnr].fileformat == 'dos' then + text = util.strip_cr(text) + end + end + + local modifiable = vim.bo[dbufnr].modifiable + vim.bo[dbufnr].modifiable = true + util.set_lines(dbufnr, 0, -1, text) + + vim.bo[dbufnr].modifiable = modifiable + vim.bo[dbufnr].modified = false + vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype + vim.bo[dbufnr].bufhidden = 'wipe' +end) + +local bufwrite = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry) + local buftext = util.buf_lines(dbufnr) + bcache.git_obj:stage_lines(buftext) + scheduler() + vim.bo[dbufnr].modified = false + -- If diff buffer base matches the bcache base then also update the + -- signs. + if util.calc_base(base) == util.calc_base(bcache.base) then + bcache.compare_text = buftext + manager.update(bufnr, bcache) + end +end) + +local function run(base: string, diffthis: boolean, opts: M.DiffthisOpts) + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + opts = opts or {} + + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local bufname = bcache:get_rev_bufname(comp_rev) + + local dbuf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(dbuf, bufname) + + local ok, err = pcall(bufread as function, bufnr, dbuf, base, bcache) + if not ok then + message.error(err as string) + scheduler() + vim.cmd'bdelete' + if diffthis then + vim.cmd'diffoff' + end + return + end + + if comp_rev == ':0' then + vim.bo[dbuf].buftype = 'acwrite' + + api.nvim_create_autocmd('BufReadCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufread(bufnr, dbuf, base, bcache) + if diffthis then + vim.cmd'diffthis' + end + end + }) + + api.nvim_create_autocmd('BufWriteCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufwrite(bufnr, dbuf, base, bcache) + end + }) + else + vim.bo[dbuf].buftype = 'nowrite' + vim.bo[dbuf].modifiable = false + end + + if diffthis then + vim.cmd(table.concat({ + 'keepalt', opts.split or 'aboveleft', + opts.vertical and 'vertical' or '', + 'diffsplit', bufname + }, ' ')) + else + vim.cmd('edit '..bufname) + end +end + +M.diffthis = void(function(base: string, opts: M.DiffthisOpts) + if vim.wo.diff then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + if not base and bcache.git_obj.has_conflicts then + local cwin = api.nvim_get_current_win() + run(':2', true, opts) + api.nvim_set_current_win(cwin) + opts.split = 'belowright' + run(':3', true, opts) + api.nvim_set_current_win(cwin) + else + run(base, true, opts) + end +end) + +M.show = void(function(base: string) + run(base, false) +end) + +local function should_reload(bufnr: integer): boolean + if not vim.bo[bufnr].modified then + return true + end + local response: string + while not vim.tbl_contains({'O', 'L'}, response) do + response = input{ + prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:' + } + end + return response == 'L' +end + +-- This function needs to be throttled as there is a call to vim.ui.input +M.update = throttle_by_id(void(function(bufnr: integer) + if not vim.wo.diff then + return + end + + local bcache = cache[bufnr] + + -- Note this will be the bufname for the currently set base + -- which are the only ones we want to update + local bufname = bcache:get_rev_bufname() + + for _, w in ipairs(api.nvim_list_wins()) do + if api.nvim_win_is_valid(w) then + local b = api.nvim_win_get_buf(w) + local bname = api.nvim_buf_get_name(b) + if bname == bufname or vim.startswith(bname, 'fugitive://') then + if should_reload(b) then + api.nvim_buf_call(b, function(): nil + vim.cmd('doautocmd BufReadCmd') + vim.cmd('diffthis') + end) + end + end + end + end +end)) + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/git.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/git.tl new file mode 100644 index 0000000..42227df --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/git.tl @@ -0,0 +1,620 @@ +local wrap = require('gitsigns.async').wrap +local scheduler = require('gitsigns.async').scheduler + +local gsd = require("gitsigns.debug") +local util = require('gitsigns.util') +local subprocess = require('gitsigns.subprocess') + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk + +local uv = vim.loop +local startswith = vim.startswith + +local dprint = require("gitsigns.debug").dprint +local eprint = require("gitsigns.debug").eprint +local err = require('gitsigns.message').error + +local record GJobSpec + command: string + args: {string} + cwd: string + writer: {string} + + -- local extensions + suppress_stderr: boolean +end + +local record M + record BlameInfo + -- Info in header + sha: string + abbrev_sha: string + orig_lnum: integer + final_lnum: integer + + -- Porcelain fields + author: string + author_mail: string + author_time: integer + author_tz: string + committer: string + committer_mail: string + committer_time: integer + committer_tz: string + summary: string + previous: string + previous_filename: string + previous_sha: string + filename: string + end + + record Version + major: integer + minor: integer + patch: integer + end + version: Version + + enable_yadm: boolean + + set_version: function(string) + + record RepoInfo + gitdir: string + toplevel: string + detached: boolean + abbrev_head: string + end + + get_repo_info: function(path: string, cmd: string, gitdir: string, toplevel: string): RepoInfo + + command : function(args: {string}, spec: GJobSpec): {string}, string + diff : function(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string + + record Repo + toplevel : string + gitdir : string + detached : boolean + abbrev_head: string + username : string + + command : function(Repo, {string}, GJobSpec): {string}, string + files_changed : function(Repo): {string} + get_show_text : function(Repo, string, string): {string}, string + update_abbrev_head : function(Repo) + new : function(dir: string, gitdir: string, toplevel: string): Repo + end + + record FileProps + relpath : string + orig_relpath : string -- Use for tracking moved files + object_name : string + mode_bits : string + has_conflicts : boolean + i_crlf : boolean -- Object has crlf + w_crlf : boolean -- Working copy has crlf + end + + record Obj + repo : Repo + file : string + relpath : string + orig_relpath : string -- Use for tracking moved files + object_name : string + mode_bits : string + has_conflicts : boolean + i_crlf : boolean -- Object has crlf + w_crlf : boolean -- Working copy has crlf + encoding : string + + command : function(Obj, {string}, GJobSpec): {string}, string + update_file_info : function(Obj, boolean, silent: boolean): boolean + unstage_file : function(Obj, string, string) + run_blame : function(Obj, {string}, number, boolean): BlameInfo + file_info : function(Obj, string, silent: boolean): FileProps + get_show_text : function(Obj, string): {string}, string + ensure_file_in_index : function(Obj) + stage_hunks : function(Obj, {Hunk}, boolean) + stage_lines : function(Obj, {string}) + has_moved : function(Obj): string + new : function(path: string, enc: string, gitdir: string, toplevel: string): Obj + end + +end + +local in_git_dir = function(file: string): boolean + for _, p in ipairs(vim.split(file, util.path_sep)) do + if p == '.git' then + return true + end + end + return false +end + +local Obj = M.Obj +local Repo = M.Repo + +local function parse_version(version: string): M.Version + assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: '..version) + local ret: M.Version = {} + local parts = vim.split(version, '%.') + ret.major = tonumber(parts[1]) as integer + ret.minor = tonumber(parts[2]) as integer + + if parts[3] == 'GIT' then + ret.patch = 0 + else + ret.patch = tonumber(parts[3]) as integer + end + + return ret +end + +-- Usage: check_version{2,3} +local function check_version(version: {number,number,number}): boolean + if not M.version then + return false + end + if M.version.major < version[1] then + return false + end + if version[2] and M.version.minor < version[2] then + return false + end + if version[3] and M.version.patch < version[3] then + return false + end + return true +end + +local type JobSpec = subprocess.JobSpec + +M.command = wrap(function(args: {string}, spec: GJobSpec, callback: function({string}, string)) + spec = spec or {} + spec.command = spec.command or 'git' + spec.args = spec.command == 'git' and + { '--no-pager', '--literal-pathspecs', unpack(args) } or args + subprocess.run_job(spec as JobSpec, function(_: integer, _: integer, stdout: string, stderr: string) + if not spec.suppress_stderr then + if stderr then + gsd.eprint(stderr) + end + end + + local stdout_lines = vim.split(stdout or '', '\n', true) + + -- If stdout ends with a newline, then remove the final empty string after + -- the split + if stdout_lines[#stdout_lines] == '' then + stdout_lines[#stdout_lines] = nil + end + + if gsd.verbose then + gsd.vprintf('%d lines:', #stdout_lines) + for i = 1, math.min(10, #stdout_lines) do + gsd.vprintf('\t%s', stdout_lines[i]) + end + end + + callback(stdout_lines, stderr) + end) +end, 3) + +M.diff = function(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string + return M.command{ + '-c', 'core.safecrlf=false', + 'diff', + '--color=never', + '--'..(indent_heuristic and '' or 'no-')..'indent-heuristic', + '--diff-algorithm='..diff_algo, + '--patch-with-raw', + '--unified=0', + file_cmp, + file_buf, + } + +end + +local function process_abbrev_head(gitdir: string, head_str: string, path: string, cmd: string): string + if not gitdir then + return head_str + end + if head_str == 'HEAD' then + local short_sha = M.command({'rev-parse', '--short', 'HEAD'}, { + command = cmd or 'git', + suppress_stderr = true, + cwd = path, + })[1] or '' + if gsd.debug_mode and short_sha ~= '' then + short_sha = 'HEAD' + end + if util.path_exists(gitdir..'/rebase-merge') + or util.path_exists(gitdir..'/rebase-apply') then + return short_sha..'(rebasing)' + end + return short_sha + end + return head_str +end + +local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 + +local cygpath_convert: function(path: string): string + +if has_cygpath then + cygpath_convert = function(path: string): string + return M.command({ '-aw', path }, { command = 'cygpath' })[1] + end +end + +local function normalize_path(path: string): string + if path and has_cygpath and not uv.fs_stat(path) then + -- If on windows and path isn't recognizable as a file, try passing it + -- through cygpath + path = cygpath_convert(path) + end + return path +end + +M.get_repo_info = function(path: string, cmd: string, gitdir: string, toplevel: string): M.RepoInfo + -- Does git rev-parse have --absolute-git-dir, added in 2.13: + -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ + local has_abs_gd = check_version{2,13} + local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' + + -- Wait for internal scheduler to settle before running command + -- https://github.com/lewis6991/gitsigns.nvim/pull/215 + scheduler() + + local args = {} + + if gitdir then + vim.list_extend(args, {'--git-dir', gitdir}) + end + + if toplevel then + vim.list_extend(args, {'--work-tree', toplevel}) + end + + vim.list_extend(args, { + 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', + }) + + local results = M.command(args, { + command = cmd or 'git', + suppress_stderr = true, + cwd = path + }) + + local ret: M.RepoInfo = { + toplevel = normalize_path(results[1]), + gitdir = normalize_path(results[2]), + } + ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd) + if ret.gitdir and not has_abs_gd then + ret.gitdir = uv.fs_realpath(ret.gitdir) + end + ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel..'/.git' + return ret +end + +M.set_version = function(version: string) + if version ~= 'auto' then + M.version = parse_version(version) + return + end + local results, stderr = M.command{'--version'} + local line = results[1] + if not line then + err("Unable to detect git version as 'git --version' failed to return anything") + eprint(stderr) + return + end + assert(type(line) == 'string', 'Unexpected output: '..line) + assert(startswith(line, 'git version'), 'Unexpected output: '..line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) +end + +-------------------------------------------------------------------------------- +-- Git repo object methods +-------------------------------------------------------------------------------- + +--- Run git command the with the objects gitdir and toplevel +Repo.command = function(self: Repo, args: {string}, spec: GJobSpec): {string}, string + spec = spec or {} + spec.cwd = self.toplevel + + local args1 = { + '--git-dir', self.gitdir, + } + + if self.detached then + vim.list_extend(args1, {'--work-tree', self.toplevel}) + end + + vim.list_extend(args1, args) + + return M.command(args1, spec) +end + +Repo.files_changed = function(self: Repo): {string} + local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) + + local ret: {string} = {} + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret+1] = line:sub(4, -1) + end + end + return ret +end + +--- Get version of file in the index, return array lines +Repo.get_show_text = function(self: Repo, object: string, encoding: string): {string}, string + local stdout, stderr = self:command({'show', object}, {suppress_stderr = true}) + + if encoding ~= 'utf-8' then + scheduler() + for i, l in ipairs(stdout) do + -- TODO(lewis6991): How should we handle blob types? + if vim.fn.type(l) == vim.v.t_string then + stdout[i] = vim.fn.iconv(l, encoding, 'utf-8') + end + end + end + + return stdout, stderr +end + +Repo.update_abbrev_head = function(self: Repo) + self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head +end + +Repo.new = function(dir: string, gitdir: string, toplevel: string): Repo + local self = setmetatable({} as Repo, {__index = Repo}) + + self.username = M.command({'config', 'user.name'})[1] + local info = M.get_repo_info(dir, nil, gitdir, toplevel) + for k, v in pairs(info as {string:any}) do + (self as table)[k] = v + end + + -- Try yadm + if M.enable_yadm and not self.gitdir then + if vim.startswith(dir, os.getenv('HOME')) + and #M.command({'ls-files', dir}, {command = 'yadm'}) ~= 0 then + M.get_repo_info(dir, 'yadm', gitdir, toplevel) + local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel) + for k, v in pairs(yadm_info as {string:any}) do + (self as table)[k] = v + end + end + end + + return self +end + +-------------------------------------------------------------------------------- +-- Git object methods +-------------------------------------------------------------------------------- + +--- Run git command the with the objects gitdir and toplevel +Obj.command = function(self: Obj, args: {string}, spec: GJobSpec): {string}, string + return self.repo:command(args, spec) +end + +Obj.update_file_info = function(self: Obj, update_relpath: boolean, silent: boolean): boolean + local old_object_name = self.object_name + local props = self:file_info(self.file, silent) + + if update_relpath then + self.relpath = props.relpath + end + self.object_name = props.object_name + self.mode_bits = props.mode_bits + self.has_conflicts = props.has_conflicts + self.i_crlf = props.i_crlf + self.w_crlf = props.w_crlf + + return old_object_name ~= self.object_name +end + +Obj.file_info = function(self: Obj, file: string, silent: boolean): M.FileProps + local results, stderr = self:command({ + '-c', 'core.quotepath=off', + 'ls-files', + '--stage', + '--others', + '--exclude-standard', + '--eol', + file or self.file + }, {suppress_stderr = true}) + + if stderr and not silent then + -- Suppress_stderr for the cases when we run: + -- git ls-files --others exists/nonexist + if not stderr:match('^warning: could not open directory .*: No such file or directory') then + gsd.eprint(stderr) + end + end + + local result: M.FileProps = {} + for _, line in ipairs(results) do + local parts = vim.split(line, '\t') + if #parts > 2 then -- tracked file + local eol = vim.split(parts[2], '%s+') + result.i_crlf = eol[1] == 'i/crlf' + result.w_crlf = eol[2] == 'w/crlf' + result.relpath = parts[3] + local attrs = vim.split(parts[1], '%s+') + local stage = tonumber(attrs[3]) + if stage <= 1 then + result.mode_bits = attrs[1] + result.object_name = attrs[2] + else + result.has_conflicts = true + end + else -- untracked file + result.relpath = parts[2] + end + end + return result +end + +Obj.get_show_text = function(self: Obj, revision: string): {string}, string + if not self.relpath then + return {} + end + + local stdout, stderr = self.repo:get_show_text(revision..':'..self.relpath, self.encoding) + + if not self.i_crlf and self.w_crlf then + -- Add cr + for i = 1, #stdout do + stdout[i] = stdout[i]..'\r' + end + end + + return stdout, stderr +end + +Obj.unstage_file = function(self: Obj) + self:command{'reset', self.file } +end + +Obj.run_blame = function(self: Obj, lines: {string}, lnum: number, ignore_whitespace: boolean): M.BlameInfo + if not self.object_name or self.repo.abbrev_head == '' then + -- As we support attaching to untracked files we need to return something if + -- the file isn't isn't tracked in git. + -- If abbrev_head is empty, then assume the repo has no commits + return { + author = 'Not Committed Yet', + ['author_mail'] = '', + committer = 'Not Committed Yet', + ['committer_mail'] = '', + } + end + + local args = { + 'blame', + '--contents', '-', + '-L', lnum..',+1', + '--line-porcelain', + self.file + } + + if ignore_whitespace then + args[#args+1] = '-w' + end + + local ignore_file = self.repo.toplevel ..'/.git-blame-ignore-revs' + if uv.fs_stat(ignore_file) then + vim.list_extend(args, {'--ignore-revs-file', ignore_file}) + end + + local results = self:command(args, { writer = lines }) + if #results == 0 then + return + end + local header = vim.split(table.remove(results, 1), ' ') + + local ret: {string:any} = {} + ret.sha = header[1] + ret.orig_lnum = tonumber(header[2]) as integer + ret.final_lnum = tonumber(header[3]) as integer + ret.abbrev_sha = string.sub(ret.sha as string, 1, 8) + for _, l in ipairs(results) do + if not startswith(l, '\t') then + local cols = vim.split(l, ' ') + local key = table.remove(cols, 1):gsub('-', '_') + ret[key] = table.concat(cols, ' ') + if key == 'previous' then + ret.previous_sha = cols[1] + ret.previous_filename = cols[2] + end + end + end + return ret as M.BlameInfo +end + +Obj.ensure_file_in_index = function(self: Obj) + if not self.object_name or self.has_conflicts then + if not self.object_name then + -- If there is no object_name then it is not yet in the index so add it + self:command{'add', '--intent-to-add', self.file} + else + -- Update the index with the common ancestor (stage 1) which is what bcache + -- stores + local info = string.format('%s,%s,%s', self.mode_bits, self.object_name, self.relpath) + self:command{'update-index', '--add', '--cacheinfo', info} + end + + self:update_file_info() + end +end + +Obj.stage_lines = function(self: Obj, lines: {string}) + local stdout = self:command({ + 'hash-object', '-w', '--path', self.relpath, '--stdin' + }, { writer = lines }) + + local new_object = stdout[1] + + self:command{ + 'update-index', '--cacheinfo', string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath) + } +end + +Obj.stage_hunks = function(self: Obj, hunks: {Hunk}, invert: boolean) + self:ensure_file_in_index() + self:command({ + 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-' + }, { + writer = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert) + }) +end + +Obj.has_moved = function(self: Obj): string + local out = self:command{'diff', '--name-status', '-C', '--cached'} + local orig_relpath = self.orig_relpath or self.relpath + for _, l in ipairs(out) do + local parts = vim.split(l, '%s+') + if #parts == 3 then + local orig, new = parts[2], parts[3] + if orig_relpath == orig then + self.orig_relpath = orig_relpath + self.relpath = new + self.file = self.repo.toplevel..'/'..new + return new + end + end + end +end + +Obj.new = function(file: string, encoding: string, gitdir: string, toplevel: string): Obj + if in_git_dir(file) then + dprint('In git dir') + return nil + end + local self = setmetatable({} as Obj, {__index = Obj}) + + self.file = file + self.encoding = encoding + self.repo = Repo.new(util.dirname(file), gitdir, toplevel) + + if not self.repo.gitdir then + dprint('Not in git repo') + return nil + end + + -- When passing gitdir and toplevel, suppress stderr when resolving the file + local silent = gitdir ~= nil and toplevel ~= nil + + self:update_file_info(true, silent) + + return self +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/health.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/health.tl new file mode 100644 index 0000000..90f1946 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/health.tl @@ -0,0 +1,17 @@ +local M = {} + +function M.check() + local fns = vim.fn as {string:function} + local report_ok = fns['health#report_ok'] + local report_error = fns['health#report_error'] + + local ok, v = pcall(vim.fn.systemlist, {'git', '--version'}) + + if not ok then + report_error(v) + else + report_ok(v[1]) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/highlight.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/highlight.tl new file mode 100644 index 0000000..a1911d8 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/highlight.tl @@ -0,0 +1,74 @@ +local api = vim.api + +local dprintf = require("gitsigns.debug").dprintf + +local record M + setup_highlights: function() +end + +-- Use array of dict so we can iterate deterministically +local hls: {{string:{string}}} = { + {GitSignsAdd = {'GitGutterAdd' , 'SignifySignAdd' , 'DiffAddedGutter' , 'diffAdded' , 'DiffAdd' }}, + {GitSignsChange = {'GitGutterChange', 'SignifySignChange', 'DiffModifiedGutter', 'diffChanged', 'DiffChange'}}, + {GitSignsDelete = {'GitGutterDelete', 'SignifySignDelete', 'DiffRemovedGutter' , 'diffRemoved', 'DiffDelete'}}, + + {GitSignsAddNr = {'GitGutterAddLineNr' , 'GitSignsAdd' }}, + {GitSignsChangeNr = {'GitGutterChangeLineNr', 'GitSignsChange'}}, + {GitSignsDeleteNr = {'GitGutterDeleteLineNr', 'GitSignsDelete'}}, + + {GitSignsAddLn = {'GitGutterAddLine' , 'SignifyLineAdd' , 'DiffAdd' }}, + {GitSignsChangeLn = {'GitGutterChangeLine', 'SignifyLineChange', 'DiffChange'}}, + -- Don't set GitSignsDeleteLn by default + -- {GitSignsDeleteLn = {}}, + + {GitSignsAddPreview = {'GitGutterAddLine' , 'SignifyLineAdd' , 'DiffAdd' }}, + {GitSignsDeletePreview = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete'}}, + + {GitSignsCurrentLineBlame = {'NonText'}}, + + {GitSignsAddInline = {'TermCursor'}}, + {GitSignsDeleteInline = {'TermCursor'}}, + {GitSignsChangeInline = {'TermCursor'}}, + + {GitSignsAddLnInline = {'GitSignsAddInline'}}, + {GitSignsChangeLnInline = {'GitSignsChangeInline'}}, + {GitSignsDeleteLnInline = {'GitSignsDeleteInline'}}, + + {GitSignsAddLnVirtLn = {'GitSignsAddLn' }}, + {GitSignsChangeVirtLn = {'GitSignsChangeLn'}}, + {GitSignsDeleteVirtLn = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete'}}, + + {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline' }}, + {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline'}}, + {GitSignsDeleteVirtLnInLine = {'GitSignsDeleteLnInline'}}, +} + +local function is_hl_set(hl_name: string): boolean + -- TODO: this only works with `set termguicolors` + local exists, hl = pcall(api.nvim_get_hl_by_name, hl_name, true) + local color = hl.foreground or hl.background or hl.reverse + return exists and color ~= nil +end + +-- Setup a GitSign* highlight by deriving it from other potentially present +-- highlights. +M.setup_highlights = function() + for _, hlg in ipairs(hls) do + for hl, candidates in pairs(hlg) do + if is_hl_set(hl) then + -- Already defined + dprintf('Highlight %s is already defined', hl) + else + for _, d in ipairs(candidates) do + if is_hl_set(d) then + dprintf('Deriving %s from %s', hl, d) + api.nvim_set_hl(0, hl, {default = true, link = d }) + break + end + end + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/hunks.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/hunks.tl new file mode 100644 index 0000000..a4c784e --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/hunks.tl @@ -0,0 +1,306 @@ +local Sign = require('gitsigns.signs').Sign +local StatusObj = require('gitsigns.status').StatusObj + +local util = require('gitsigns.util') + +local min, max = math.min, math.max + +local record M + enum Type + "add" + "change" + "delete" + end + + record Node + start: integer + count: integer + lines: {string} + end + + -- For internal use + record Hunk + type: Type + head: string + added: Node + removed: Node + vend: integer + end + + record Hunk_Public + type: Type + head: string + lines: {string} + added: Node + removed: Node + end +end + +local Hunk = M.Hunk + +function M.create_hunk(old_start: integer, old_count: integer, new_start: integer, new_count: integer): Hunk + return { + removed = { start = old_start, count = old_count, lines = {} }, + added = { start = new_start, count = new_count, lines = {} }, + head = ('@@ -%d%s +%d%s @@'):format( + old_start, old_count > 0 and ',' .. old_count or '', + new_start, new_count > 0 and ',' .. new_count or '' + ), + vend = new_start + math.max(new_count - 1, 0), + type = new_count == 0 and 'delete' or + old_count == 0 and 'add' or + 'change' + } +end + +function M.create_partial_hunk(hunks: {Hunk}, top: integer, bot: integer): Hunk + local pretop, precount = top, bot - top + 1 + for _, h in ipairs(hunks) do + local added_in_hunk = h.added.count - h.removed.count + + local added_in_range = 0 + if h.added.start >= top and h.vend <= bot then + -- Range contains hunk + added_in_range = added_in_hunk + else + local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) + local added_above_top = max(0, top - (h.added.start + h.removed.count)) + + if h.added.start >= top and h.added.start <= bot then + -- Range top intersects hunk + added_in_range = added_above_bot + elseif h.vend >= top and h.vend <= bot then + -- Range bottom intersects hunk + added_in_range = added_in_hunk - added_above_top + pretop = pretop - added_above_top + elseif h.added.start <= top and h.vend >= bot then + -- Range within hunk + added_in_range = added_above_bot - added_above_top + pretop = pretop - added_above_top + end + + if top > h.vend then + pretop = pretop - added_in_hunk + end + end + + precount = precount - added_in_range + end + + if precount == 0 then + pretop = pretop - 1 + end + + return M.create_hunk(pretop, precount, top, bot - top + 1) +end + +function M.patch_lines(hunk: Hunk, fileformat: string): {string} + local lines: {string} = {} + for _, l in ipairs(hunk.removed.lines) do + lines[#lines+1] = '-'..l + end + for _, l in ipairs(hunk.added.lines) do + lines[#lines+1] = '+'..l + end + + if fileformat == 'dos' then + lines = util.strip_cr(lines) + end + return lines +end + +function M.parse_diff_line(line: string): Hunk + local diffkey = vim.trim(vim.split(line, '@@', true)[2]) + + -- diffKey: "-xx,n +yy" + -- pre: {xx, n}, now: {yy} + local pre, now = unpack(vim.tbl_map(function(s: string): {string} + return vim.split(string.sub(s, 2), ',') + end, vim.split(diffkey, ' ')) as {{string}}) + + local hunk = M.create_hunk( + tonumber(pre[1]) as integer, (tonumber(pre[2]) or 1) as integer, + tonumber(now[1]) as integer, (tonumber(now[2]) or 1) as integer + ) + hunk.head = line + + return hunk +end + +local function change_end(hunk: Hunk): integer + if hunk.added.count == 0 then + -- delete + return hunk.added.start + elseif hunk.removed.count == 0 then + -- add + return hunk.added.start + hunk.added.count - 1 + else + -- change + return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 + end +end + +-- Calculate signs needed to be applied from a hunk for a specified line range. +function M.calc_signs(hunk: Hunk, min_lnum: integer, max_lnum: integer, untracked: boolean): {Sign} + assert(not untracked or hunk.type == 'add') + min_lnum = min_lnum or 1 + max_lnum = max_lnum or math.huge as integer + local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count + + if hunk.type == 'delete' and start == 0 then + if min_lnum <= 1 then + -- topdelete signs get placed one row lower + return {{ type = 'topdelete', count = removed, lnum = 1 }} + else + return {} + end + end + + local signs: {Sign} = {} + + local cend = change_end(hunk) + + for lnum = max(start, min_lnum), min(cend, max_lnum) do + local changedelete = hunk.type == 'change' and removed > added and lnum == cend + + signs[#signs+1] = { + type = changedelete and 'changedelete' or + untracked and 'untracked' or hunk.type, + count = lnum == start and (hunk.type == 'add' and added or removed), + lnum = lnum + } + end + + if hunk.type == "change" and added > removed and + hunk.vend >= min_lnum and cend <= max_lnum then + for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do + signs[#signs+1] = { + type = 'add', + count = lnum == hunk.vend and (added - removed), + lnum = lnum + } + end + end + + return signs +end + +function M.create_patch(relpath: string, hunks: {Hunk}, mode_bits: string, invert: boolean): {string} + invert = invert or false + + local results = { + string.format('diff --git a/%s b/%s', relpath, relpath), + 'index 000000..000000 '..mode_bits, + '--- a/'..relpath, + '+++ b/'..relpath, + } + + local offset = 0 + + for _, process_hunk in ipairs(hunks) do + local start, pre_count, now_count = + process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count + + if process_hunk.type == 'add' then + start = start + 1 + end + + local pre_lines = process_hunk.removed.lines + local now_lines = process_hunk.added.lines + + if invert then + pre_count, now_count = now_count, pre_count + pre_lines, now_lines = now_lines, pre_lines + end + + table.insert(results, string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count)) + for _, l in ipairs(pre_lines) do + results[#results+1] = '-'..l + end + for _, l in ipairs(now_lines) do + results[#results+1] = '+'..l + end + + process_hunk.removed.start = start + offset + offset = offset + (now_count - pre_count) + end + + return results +end + +function M.get_summary(hunks: {Hunk}): StatusObj + local status = { added = 0, changed = 0, removed = 0 } + + for _, hunk in ipairs(hunks or {}) do + if hunk.type == 'add' then + status.added = status.added + hunk.added.count + elseif hunk.type == 'delete' then + status.removed = status.removed + hunk.removed.count + elseif hunk.type == 'change' then + local add, remove = hunk.added.count, hunk.removed.count + local delta = min(add, remove) + status.changed = status.changed + delta + status.added = status.added + add - delta + status.removed = status.removed + remove - delta + end + end + + return status +end + +function M.find_hunk(lnum: number, hunks: {Hunk}): Hunk, integer + for i, hunk in ipairs(hunks) do + if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then + return hunk, i + end + + if hunk.added.start <= lnum and hunk.vend >= lnum then + return hunk, i + end + end +end + +function M.find_nearest_hunk(lnum: number, hunks: {Hunk}, forwards: boolean, wrap: boolean): Hunk, integer + local ret: Hunk + local index: integer + if forwards then + for i = 1, #hunks do + local hunk = hunks[i] + if hunk.added.start > lnum then + ret = hunk + index = i + break + end + end + else + for i = #hunks, 1, -1 do + local hunk = hunks[i] + if hunk.vend < lnum then + ret = hunk + index = i + break + end + end + end + if not ret and wrap then + index = forwards and 1 or #hunks + ret = hunks[index as integer] + end + return ret, index +end + +function M.compare_heads(a: {Hunk}, b: {Hunk}): boolean + if (a == nil) ~= (b == nil) then + return true + elseif a and #a ~= #b then + return true + end + for i, ah in ipairs(a or {}) do + if b[i].head ~= ah.head then + return true + end + end + return false +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/manager.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/manager.tl new file mode 100644 index 0000000..51ce48f --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/manager.tl @@ -0,0 +1,508 @@ +local void = require('gitsigns.async').void +local awrap = require('gitsigns.async').wrap +local scheduler = require('gitsigns.async').scheduler + +local gs_cache = require('gitsigns.cache') +local CacheEntry = gs_cache.CacheEntry +local cache = gs_cache.cache + +local Signs = require('gitsigns.signs') + +local Status = require("gitsigns.status") + +local debounce_trailing = require('gitsigns.debounce').debounce_trailing +local throttle_by_id = require('gitsigns.debounce').throttle_by_id +local gs_debug = require("gitsigns.debug") +local dprint = gs_debug.dprint +local dprintf = gs_debug.dprintf +local eprint = gs_debug.eprint +local subprocess = require('gitsigns.subprocess') +local util = require('gitsigns.util') +local run_diff = require('gitsigns.diff') +local git = require('gitsigns.git') +local uv = require('gitsigns.uv') + +local gs_hunks = require("gitsigns.hunks") +local Hunk = gs_hunks.Hunk + +local config = require('gitsigns.config').config + +local api = vim.api + +local signs: Signs + +local record M + update : function(bufnr: integer, CacheEntry) + update_debounced : function(bufnr: integer, CacheEntry) + on_lines : function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean + watch_gitdir : function(bufnr: integer, gitdir: string): vim.loop.FSPollObj + update_cwd_head : function() + detach : function(bufnr: integer, keep_signs: boolean) + reset_signs : function() + setup : function() +end + +local schedule_if_buf_valid = function(buf: integer, cb: function) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) then + cb() + end + end) +end + +local scheduler_if_buf_valid = awrap(schedule_if_buf_valid, 2) + +local function apply_win_signs(bufnr: integer, hunks: {Hunk}, top: integer, bot: integer, clear: boolean, untracked: boolean) + if clear then + signs:remove(bufnr) -- Remove all signs + end + + -- hunks can be nil + hunks = hunks or {} + + -- To stop the sign column width changing too much, if there are signs to be + -- added but none of them are visible in the window, then make sure to add at + -- least one sign. Only do this on the first call after an update when we all + -- the signs have been cleared. + if clear and hunks[1] then + signs:add(bufnr, gs_hunks.calc_signs(hunks[1], hunks[1].added.start, hunks[1].added.start, untracked)) + end + + for _, hunk in ipairs(hunks) do + if top <= hunk.vend and bot >= hunk.added.start then + signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked)) + end + if hunk.added.start > bot then + break + end + end +end + +M.on_lines = function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean + local bcache = cache[buf] + if not bcache then + dprint('Cache for buffer was nil. Detaching') + return true + end + + signs:on_lines(buf, first, last_orig, last_new) + + -- Signs in changed regions get invalidated so we need to force a redraw if + -- any signs get removed. + if signs:contains(buf, first, last_new) then + -- Force a sign redraw on the next update (fixes #521) + bcache.force_next_update = true + end + + M.update_debounced(buf, cache[buf]) +end + +local ns = api.nvim_create_namespace('gitsigns') + +local function apply_word_diff(bufnr: integer, row: integer) + -- Don't run on folded lines + if vim.fn.foldclosed(row+1) ~= -1 then + return + end + + if not cache[bufnr] or not cache[bufnr].hunks then + return + end + + local line = api.nvim_buf_get_lines(bufnr, row, row+1, false)[1] + if not line then + -- Invalid line + return + end + + local lnum = row + 1 + + local hunk = gs_hunks.find_hunk(lnum, cache[bufnr].hunks) + if not hunk then + -- No hunk at line + return + end + + if hunk.added.count ~= hunk.removed.count then + -- Only word diff if added count == removed + return + end + + local pos = lnum - hunk.added.start + 1 + + local added_line = hunk.added.lines[pos] + local removed_line = hunk.removed.lines[pos] + + local _, added_regions = require('gitsigns.diff_int').run_word_diff({removed_line}, {added_line}) + + local cols = #line + + for _, region in ipairs(added_regions) do + local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 + if ecol == scol then + -- Make sure region is at least 1 column wide so deletes can be shown + ecol = scol + 1 + end + + local hl_group = rtype == 'add' and 'GitSignsAddLnInline' + or rtype == 'change' and 'GitSignsChangeLnInline' + or 'GitSignsDeleteLnInline' + + local opts: {string:any} = { + ephemeral = true, + priority = 1000 + } + + if ecol > cols and ecol == scol + 1 then + -- delete on last column, use virtual text instead + opts.virt_text = {{' ', hl_group}} + opts.virt_text_pos = 'overlay' + else + opts.end_col = ecol + opts.hl_group = hl_group + end + + api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) + api.nvim__buf_redraw_range(bufnr, row, row+1) + end +end + +local ns_rm = api.nvim_create_namespace('gitsigns_removed') + +local VIRT_LINE_LEN = 300 + +local function clear_deleted(bufnr: integer) + local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) + for _, mark in ipairs(marks as {{integer, integer, integer}}) do + api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) + end +end + +function M.show_deleted(bufnr: integer, nsd: integer, hunk: Hunk) + local virt_lines = {} + + for i, line in ipairs(hunk.removed.lines) do + local vline = {} + local last_ecol = 1 + + if config.word_diff then + local regions = require('gitsigns.diff_int').run_word_diff( + {hunk.removed.lines[i]}, {hunk.added.lines[i]}) + + for _, region in ipairs(regions) do + local rline, scol, ecol = region[1], region[3], region[4] + if rline > 1 then + break + end + vline[#vline+1] = { line:sub(last_ecol, scol-1), 'GitSignsDeleteVirtLn'} + vline[#vline+1] = { line:sub(scol, ecol-1), 'GitSignsDeleteVirtLnInline'} + last_ecol = ecol + end + end + + if #line > 0 then + vline[#vline+1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn'} + end + + -- Add extra padding so the entire line is highlighted + local padding = string.rep(' ', VIRT_LINE_LEN-#line) + vline[#vline+1] = { padding, 'GitSignsDeleteVirtLn'} + + virt_lines[i] = vline + end + + local topdelete = hunk.added.start == 0 and hunk.type == 'delete' + + local row = topdelete and 0 or hunk.added.start - 1 + api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { + virt_lines = virt_lines, + -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 + virt_lines_above = hunk.type ~= 'delete' or topdelete, + }) +end + +function M.show_added(bufnr: integer, nsw: integer, hunk: Hunk) + local start_row = hunk.added.start - 1 + + for offset = 0, hunk.added.count - 1 do + local row = start_row + offset + api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { + end_row = row + 1, + hl_group = 'GitSignsAddPreview', + hl_eol = true, + priority = 1000 + }) + end + + local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) + + for _, region in ipairs(added_regions) do + local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 + api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { + end_col = ecol, + hl_group = rtype == 'add' and 'GitSignsAddInline' + or rtype == 'change' and 'GitSignsChangeInline' + or 'GitSignsDeleteInline', + priority = 1001 + }) + end +end + +local function update_show_deleted(bufnr: integer) + local bcache = cache[bufnr] + + clear_deleted(bufnr) + if config.show_deleted then + for _, hunk in ipairs(bcache.hunks) do + M.show_deleted(bufnr, ns_rm, hunk) + end + end +end + +local update_cnt = 0 + +-- Ensure updates cannot be interleaved. +-- Since updates are asynchronous we need to make sure an update isn't performed +-- whilst another one is in progress. If this happens then schedule another +-- update after the current one has completed. +M.update = throttle_by_id(function(bufnr: integer, bcache: CacheEntry) + local __FUNC__ = 'update' + bcache = bcache or cache[bufnr] + if not bcache then + eprint('Cache for buffer '..bufnr..' was nil') + return + end + + scheduler_if_buf_valid(bufnr) + local buftext = util.buf_lines(bufnr) + local git_obj = bcache.git_obj + + if not bcache.compare_text or config._refresh_staged_on_update then + bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev()) + end + + local old_hunks = bcache.hunks + bcache.hunks = run_diff(bcache.compare_text, buftext) + + scheduler_if_buf_valid(bufnr) + -- Note the decoration provider may have invalidated bcache.hunks at this + -- point + if bcache.force_next_update or gs_hunks.compare_heads(bcache.hunks, old_hunks) then + -- Apply signs to the window. Other signs will be added by the decoration + -- provider as they are drawn. + apply_win_signs(bufnr, bcache.hunks, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil) + + update_show_deleted(bufnr) + bcache.force_next_update = false + + api.nvim_exec_autocmds('User', { + pattern = 'GitSignsUpdate', + modeline = false, + }) + end + local summary = gs_hunks.get_summary(bcache.hunks) + summary.head = git_obj.repo.abbrev_head + Status:update(bufnr, summary) + + update_cnt = update_cnt + 1 + + dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt) +end, true) + +M.detach = function(bufnr: integer, keep_signs: boolean) + if not keep_signs then + signs:remove(bufnr) -- Remove all signs + end +end + +local function handle_moved(bufnr: integer, bcache: CacheEntry, old_relpath: string) + local git_obj = bcache.git_obj + local do_update = false + + local new_name = git_obj:has_moved() + if new_name then + dprintf('File moved to %s', new_name) + git_obj.relpath = new_name + if not git_obj.orig_relpath then + git_obj.orig_relpath = old_relpath + end + do_update = true + elseif git_obj.orig_relpath then + local orig_file = git_obj.repo.toplevel..util.path_sep..git_obj.orig_relpath + if git_obj:file_info(orig_file).relpath then + dprintf('Moved file reset') + git_obj.relpath = git_obj.orig_relpath + git_obj.orig_relpath = nil + do_update = true + end + else + -- File removed from index, do nothing + end + + if do_update then + git_obj.file = git_obj.repo.toplevel..util.path_sep..git_obj.relpath + bcache.file = git_obj.file + git_obj:update_file_info() + scheduler() + + local bufexists = vim.fn.bufexists(bcache.file) == 1 + local old_name = api.nvim_buf_get_name(bufnr) + + if not bufexists then + util.buf_rename(bufnr, bcache.file) + end + + local msg = bufexists and 'Cannot rename' or 'Renamed' + dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) + end +end + + +M.watch_gitdir = function(bufnr: integer, gitdir: string): vim.loop.FSPollObj + if not config.watch_gitdir.enable then + return + end + + dprintf('Watching git dir') + local w = uv.new_fs_poll(true) + w:start(gitdir, config.watch_gitdir.interval, void(function(err: string) + local __FUNC__ = 'watcher_cb' + if err then + dprintf('Git dir update error: %s', err) + return + end + dprint('Git dir update') + + local bcache = cache[bufnr] + + if not bcache then + -- Very occasionally an external git operation may cause the buffer to + -- detach and update the git dir simultaneously. When this happens this + -- handler will trigger but there will be no cache. + dprint('Has detached, aborting') + return + end + + local git_obj = bcache.git_obj + + git_obj.repo:update_abbrev_head() + + scheduler() + Status:update(bufnr, { head = git_obj.repo.abbrev_head}) + + local was_tracked = git_obj.object_name ~= nil + local old_relpath = git_obj.relpath + + if not git_obj:update_file_info() then + dprint('File not changed') + return + end + + if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then + -- File was tracked but is no longer tracked. Must of been removed or + -- moved. Check if it was moved and switch to it. + handle_moved(bufnr, bcache, old_relpath) + end + + -- Invalidate + bcache.compare_text = nil + + M.update(bufnr, bcache) + end)) + return w +end + +local cwd_watcher: vim.loop.FSPollObj + +M.update_cwd_head = void(function() + if cwd_watcher then + cwd_watcher:stop() + else + cwd_watcher = uv.new_fs_poll(true) + end + + local cwd = vim.loop.cwd() + local gitdir, head: string, string + + -- Look in the cache first + for _, bcache in pairs(cache as {number:CacheEntry}) do + local repo = bcache.git_obj.repo + if repo.toplevel == cwd then + head = repo.abbrev_head + gitdir = repo.gitdir + break + end + end + + if not head or not gitdir then + local info = git.get_repo_info(cwd) + gitdir = info.gitdir + head = info.abbrev_head + end + + scheduler() + vim.g.gitsigns_head = head + + if not gitdir then + return + end + + local towatch = gitdir..'/HEAD' + + if cwd_watcher:getpath() == towatch then + -- Already watching + return + end + + -- Watch .git/HEAD to detect branch changes + cwd_watcher:start( + towatch, + config.watch_gitdir.interval, + void(function(err: string) + local __FUNC__ = 'cwd_watcher_cb' + if err then + dprintf('Git dir update error: %s', err) + return + end + dprint('Git cwd dir update') + + local new_head = git.get_repo_info(cwd).abbrev_head + scheduler() + vim.g.gitsigns_head = new_head + end) + ) +end) + +M.reset_signs = function() + signs:reset() -- Remove all signs +end + +M.setup = function() + -- Calling this before any await calls will stop nvim's intro messages being + -- displayed + api.nvim_set_decoration_provider(ns, { + on_win = function(_, _, bufnr: integer, topline: integer, botline_guess: integer): boolean + local bcache = cache[bufnr] + if not bcache or not bcache.hunks then + return false + end + local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) + + local untracked = bcache.git_obj.object_name == nil + + apply_win_signs(bufnr, bcache.hunks, topline+1, botline+1, false, untracked) + + if not (config.word_diff and config.diff_opts.internal) then + return false + end + end, + on_line = function(_, _winid: integer, bufnr: integer, row: integer) + apply_word_diff(bufnr, row) + end + }) + + signs = Signs.new(config.signs) + M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/mappings.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/mappings.tl new file mode 100644 index 0000000..fb61b00 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/mappings.tl @@ -0,0 +1,87 @@ +-- Originated from: +-- https://github.com/norcalli/neovim-plugin/blob/master/lua/neovim-plugin/apply_mappings.lua + +local validate = vim.validate +local api = vim.api + +local valid_modes: {string:string} = { + n = 'n'; v = 'v'; x = 'x'; i = 'i'; o = 'o'; t = 't'; c = 'c'; s = 's'; + -- :map! and :map + ['!'] = '!'; [' '] = ''; +} + +local valid_options: {string:string} = { + buffer = 'boolean', + expr = 'boolean', + noremap = 'boolean', + nowait = 'boolean', + script = 'boolean', + silent = 'boolean', + unique = 'boolean', +} + +local function validate_option_keywords(options: table) + for option_name, expected_type in pairs(valid_options) do + local value = options[option_name] + if value then + validate { + [option_name] = { value, expected_type }; + } + end + end +end + +local function apply_mappings(mappings: {string:any}, bufnr: integer) + validate { + mappings = { mappings, 'table' }; + } + + local default_options = {} + for key, val in pairs(mappings) do + -- Skip any inline default keywords. + if valid_options[key] then + default_options[key] = val + end + end + + for key, opts in pairs(mappings) do + repeat + -- Skip any inline default keywords. + if valid_options[key] then + break + end + + local rhs: string + local options: {string:any} + if opts is string then + rhs = opts + options = {} + elseif opts is table then + rhs = opts[1] as string + local boptions = {} + for k in pairs(valid_options) do + boptions[k] = opts[k] + end + options = boptions + else + error(("Invalid type for option rhs: %q = %s"):format(type(opts), vim.inspect(opts))) + end + options = vim.tbl_extend('keep', default_options, options) as {string:any} + + validate_option_keywords(options) + + local mode, mapping = key:match("^(.)[ ]*(.+)$") + + if not mode or not valid_modes[mode] then + error("Invalid mode specified for keymapping. mode="..mode) + end + + -- In case users haven't updated their config. + options.buffer = nil + + api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) + until true + end +end + +return apply_mappings diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/message.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/message.tl new file mode 100644 index 0000000..342ff57 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/message.tl @@ -0,0 +1,16 @@ +local type MsgFun = function(string, ...: any) + +local record M + warn : MsgFun + error : MsgFun +end + +M.warn = vim.schedule_wrap(function(s: string, ...:any) + vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'}) +end) as MsgFun + +M.error = vim.schedule_wrap(function(s: string, ...:any) + vim.notify(s:format(...), vim.log.levels.ERROR, {title = 'gitsigns'}) +end) as MsgFun + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/popup.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/popup.tl new file mode 100644 index 0000000..a1dbf59 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/popup.tl @@ -0,0 +1,217 @@ +local record popup + type LinesSpec = {{{string,string|{HlMark}}}} + record HlMark + hl_group : string + start_row : integer + end_row : integer + start_col : integer + end_col : integer + end +end + +local HlMark = popup.HlMark + +local api = vim.api + +local function bufnr_calc_width(bufnr: integer, lines: {string}): integer + return api.nvim_buf_call(bufnr, function(): integer + local width = 0 + for _, l in ipairs(lines) do + if vim.fn.type(l) == vim.v.t_string then + local len = vim.fn.strdisplaywidth(l) + if len > width then + width = len + end + end + end + return width + 1 -- Add 1 for some miinor padding + end) +end + +-- Expand height until all lines are visible to account for wrapped lines. +local function expand_height(winid: integer, nlines: integer) + local newheight = 0 + for _ = 0, 50 do + local winheight = api.nvim_win_get_height(winid) + if newheight > winheight then + -- Window must be max height + break + end + local wd = api.nvim_win_call(winid, function(): integer + return vim.fn.line('w$') + end) + if wd >= nlines then + break + end + newheight = winheight+nlines-wd + api.nvim_win_set_height(winid, newheight) + end +end + +local function offset_hlmarks(hlmarks: {HlMark}, row_offset: integer) + for _, h in ipairs(hlmarks) do + if h.start_row then + h.start_row = h.start_row + row_offset + end + if h.end_row then + h.end_row = h.end_row + row_offset + end + end +end + +local function process_linesspec(fmt: popup.LinesSpec): {string}, {HlMark} + local lines: {string} = {} + local hls: {HlMark} = {} + + local row = 0 + for _, section in ipairs(fmt) do + local sec = {} + local pos = 0 + for _, part in ipairs(section) do + local text = part[1] + local hl = part[2] + + sec[#sec+1] = text + + local srow = row + local scol = pos + + local ts = vim.split(text, '\n') + + if #ts > 1 then + pos = 0 + row = row + #ts - 1 + else + pos = pos + #text + end + + if hl is string then + hls[#hls+1] = { + hl_group = hl, + start_row = srow, + end_row = row, + start_col = scol, + end_col = pos, + } + else -- hl is {HlMark} + offset_hlmarks(hl, srow) + vim.list_extend(hls, hl) + end + end + for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do + lines[#lines+1] = l + end + row = row + 1 + end + + return lines, hls +end + +local function close_all_but(id: string) + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview ~= nil and + vim.w[winid].gitsigns_preview ~= id then + pcall(api.nvim_win_close, winid, true) + end + end +end + +function popup.create0(lines: {string}, opts: {string:any}, id: string): integer, integer + -- Close any popups not matching id + close_all_but(id) + + local ts = vim.bo.tabstop + local bufnr = api.nvim_create_buf(false, true) + assert(bufnr, "Failed to create buffer") + + -- In case nvim was opened with '-M' + vim.bo[bufnr].modifiable = true + api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + vim.bo[bufnr].modifiable = false + + -- Set tabstop before calculating the buffer width so that the correct width + -- is calculated + vim.bo[bufnr].tabstop = ts + + local opts1 = vim.deepcopy(opts or {}) + opts1.height = opts1.height or #lines -- Guess, adjust later + opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) + + local winid = api.nvim_open_win(bufnr, false, opts1) + + vim.w[winid].gitsigns_preview = id or true + + if not opts.height then + expand_height(winid, #lines) + end + + if opts1.style == 'minimal' then + -- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause + -- line wrapping. + vim.wo[winid].signcolumn = 'no' + end + + -- Close the popup when navigating to any window which is not the preview + -- itself. + local group = 'gitsigns_popup' + api.nvim_create_augroup(group, {}) + local old_cursor = api.nvim_win_get_cursor(0) + + api.nvim_create_autocmd({'CursorMoved', 'CursorMovedI'}, { + group = group, + callback = function() + local cursor = api.nvim_win_get_cursor(0) + -- Did the cursor REALLY change (neovim/neovim#12923) + if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) + and api.nvim_get_current_win() ~= winid then + -- Clear the augroup + api.nvim_create_augroup(group, {}) + pcall(api.nvim_win_close, winid, true) + return + end + old_cursor = cursor + end + }) + + return winid, bufnr +end + +local ns = api.nvim_create_namespace('gitsigns_popup') + +function popup.create(lines_spec: popup.LinesSpec, opts: {string:any}, id: string): integer, integer + local lines, highlights = process_linesspec(lines_spec) + local winid, bufnr = popup.create0(lines, opts, id) + + for _, hl in ipairs(highlights) do + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { + hl_group = hl.hl_group, + end_row = hl.end_row, + end_col = hl.end_col, + hl_eol = true, + }) + if not ok then + error(vim.inspect(hl)..'\n'..err) + end + end + + return winid, bufnr +end + +function popup.is_open(id: string): integer + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview == id then + return winid + end + end + return nil +end + +function popup.focus_open(id: string): integer + local winid = popup.is_open(id) + if winid then + api.nvim_set_current_win(winid) + end + return winid +end + +return popup diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/repeat.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/repeat.tl new file mode 100644 index 0000000..48c040f --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/repeat.tl @@ -0,0 +1,30 @@ +local api = vim.api + +local record M + mk_repeatable: function(fn: function(A1, A2)): function(A1, A2) + mk_repeatable: function (fn: function(A1) ): function(A1) + repeat_action: function() +end + + +function M.mk_repeatable(fn: function): function + return function(...: any) + local args = {...} + local nargs = select('#', ...) + vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" + + M.repeat_action = function() + fn(unpack(args, 1, nargs)) + if vim.fn.exists('*repeat#set') == 1 then + local action = api.nvim_replace_termcodes( + string.format('call %s()', vim.go.operatorfunc), + true, true, true) + vim.fn['repeat#set'](action, -1) + end + end + + vim.cmd'normal! g@l' + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs.tl new file mode 100644 index 0000000..322f6b7 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs.tl @@ -0,0 +1,30 @@ +local config = require('gitsigns.config').config +local SignsConfig = require('gitsigns.config').Config.SignsConfig + +local dprint = require('gitsigns.debug').dprint + +local B = require('gitsigns.signs.base') + +local M: B = {} + +local function init() + local __FUNC__ = 'signs.init' + if config._extmark_signs then + dprint('Using extmark signs') + M = require('gitsigns.signs.extmarks') + else + dprint('Using vimfn signs') + M = require('gitsigns.signs.vimfn') + end +end + +function M.new(cfg: SignsConfig, name: string): B + init() + return M.new(cfg, name) +end + +return setmetatable(B, { + __index = function(_, k: string): any + return rawget(M as table, k) + end +}) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/base.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/base.tl new file mode 100644 index 0000000..27b6af4 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/base.tl @@ -0,0 +1,36 @@ +local SignsConfig = require('gitsigns.config').Config.SignsConfig + +local record M + enum SignType + "add" + "delete" + "change" + "topdelete" + "changedelete" + "untracked" + end + + record Sign + type: SignType + count: integer + lnum: integer + end + + group: string + config: SignsConfig + + -- Used by signs/extmarks.tl + ns: integer + + -- Used by signs/vimfn.tl + placed: {integer:{integer:Sign}} + + new : function(cfg: SignsConfig, name: string): M + remove : function(M, bufnr: integer, start_lnum: integer, end_lnum: integer) + add : function(M, bufnr: integer, signs: {M.Sign}) + contains : function(M, bufnr: integer, start: integer, last: integer): boolean + on_lines : function(M, bufnr: integer, first: integer, last_orig: integer, last_new: integer) + reset : function(M) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/extmarks.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/extmarks.tl new file mode 100644 index 0000000..e75618b --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/extmarks.tl @@ -0,0 +1,88 @@ +local api = vim.api + +local SignsConfig = require('gitsigns.config').Config.SignsConfig +local config = require('gitsigns.config').config + +local B = require('gitsigns.signs.base') + +local M: B = {} + +local group_base = 'gitsigns_extmark_signs_' + +function M.new(cfg: SignsConfig, name: string): B + local self = setmetatable({} as B, {__index = M}) + self.config = cfg + self.group = group_base..(name or '') + self.ns = api.nvim_create_namespace(self.group) + return self +end + +function M:on_lines(buf: integer, _: integer, last_orig: integer, last_new: integer) + -- Remove extmarks on line deletions to mimic + -- the behaviour of vim signs. + if last_orig > last_new then + self:remove(buf, last_new+1, last_orig) + end +end + +function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer) + if start_lnum then + api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum-1, end_lnum or start_lnum) + else + api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) + end +end + +function M:add(bufnr: integer, signs: {M.Sign}) + if not config.signcolumn and not config.numhl and not config.linehl then + -- Don't place signs if it won't show anything + return + end + + local cfg = self.config + + for _, s in ipairs(signs) do + if not self:contains(bufnr, s.lnum) then + local cs = cfg[s.type] + local text = cs.text + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_char = cc[count] or cc['+'] or '' + text = cs.text..count_char + end + + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum-1, -1, { + id = s.lnum, + sign_text = config.signcolumn and text or '', + priority = config.sign_priority, + sign_hl_group = cs.hl, + number_hl_group = config.numhl and cs.numhl or nil, + line_hl_group = config.linehl and cs.linehl or nil, + }) + + if not ok and config.debug_mode then + vim.schedule(function() + error(table.concat({ + string.format('Error placing extmark on line %d', s.lnum), + err + }, '\n')) + end) + end + end + end +end + +function M:contains(bufnr: integer, start: integer, last: integer): boolean + local marks = api.nvim_buf_get_extmarks( + bufnr, self.ns, {start-1, 0}, {last or start, 0}, {limit=1}) + return #marks > 0 +end + +function M:reset() + for _, buf in ipairs(api.nvim_list_bufs()) do + self:remove(buf) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/vimfn.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/vimfn.tl new file mode 100644 index 0000000..ccc03a9 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/signs/vimfn.tl @@ -0,0 +1,159 @@ +local fn = vim.fn + +local SignsConfig = require('gitsigns.config').Config.SignsConfig +local config = require('gitsigns.config').config + +local emptytable = require('gitsigns.util').emptytable + +local B = require('gitsigns.signs.base') + +local M: B = {} + +-- The internal representation of signs in Neovim is a linked list which is slow +-- to index. To improve efficiency we add an abstraction layer to the signs API +-- which keeps track of which signs have already been placed in the buffer. +-- +-- This allows us to: +-- - efficiently query placed signs. +-- - skip adding a sign if it has already been placed. + +local function capitalise_word(x: string): string + return x:sub(1, 1):upper()..x:sub(2) +end + +local sign_define_cache: {string:table} = {} +local sign_name_cache: {string:string} = {} + +local function get_sign_name(stype: string): string + if not sign_name_cache[stype] then + sign_name_cache[stype] = string.format( + '%s%s', 'GitSigns', capitalise_word(stype)) + end + + return sign_name_cache[stype] +end + +local function sign_get(name: string): table + if not sign_define_cache[name] then + local s = fn.sign_getdefined(name) + if not vim.tbl_isempty(s) then + sign_define_cache[name] = s + end + end + return sign_define_cache[name] +end + +local function define_sign(name: string, opts: {string:any}, redefine: boolean) + if redefine then + sign_define_cache[name] = nil + fn.sign_undefine(name) + fn.sign_define(name, opts) + elseif not sign_get(name) then + fn.sign_define(name, opts) + end +end + +local function define_signs(obj: B, redefine: boolean) + -- Define signs + for stype, cs in pairs(obj.config) do + define_sign(get_sign_name(stype), { + texthl = cs.hl, + text = config.signcolumn and cs.text or nil, + numhl = config.numhl and cs.numhl or nil, + linehl = config.linehl and cs.linehl or nil + }, redefine) + end +end + +local group_base = 'gitsigns_vimfn_signs_' + +function M.new(cfg: SignsConfig, name: string): B + local self = setmetatable({} as B, {__index = M}) + self.group = group_base..(name or '') + self.config = cfg + self.placed = emptytable() + + define_signs(self, false) + + return self +end + +function M:on_lines(_: integer, _: integer, _: integer, _: integer) +end + +function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer) + end_lnum = end_lnum or start_lnum + + if start_lnum then + for lnum = start_lnum, end_lnum do + self.placed[bufnr][lnum] = nil + fn.sign_unplace(self.group, {buffer = bufnr, id = lnum}) + end + else + self.placed[bufnr] = nil + fn.sign_unplace(self.group, {buffer = bufnr}) + end +end + +function M:add(bufnr: integer, signs: {M.Sign}) + if not config.signcolumn and not config.numhl and not config.linehl then + -- Don't place signs if it won't show anything + return + end + + local to_place = {} + + local cfg = self.config + for _, s in ipairs(signs) do + local sign_name = get_sign_name(s.type) + + local cs = cfg[s.type] + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or '' + local count_char = cc[count] or cc['+'] or '' + sign_name = sign_name..count_suffix + define_sign(sign_name, { + texthl = cs.hl, + text = config.signcolumn and cs.text..count_char or '', + numhl = config.numhl and cs.numhl or nil, + linehl = config.linehl and cs.linehl or nil + }) + end + + if not self.placed[bufnr][s.lnum] then + local sign = { + id = s.lnum, + group = self.group, + name = sign_name, + buffer = bufnr, + lnum = s.lnum, + priority = config.sign_priority + } + self.placed[bufnr][s.lnum] = s + to_place[#to_place+1] = sign + end + end + + if #to_place > 0 then + fn.sign_placelist(to_place) + end +end + +function M:contains(bufnr: integer, start: integer, last: integer): boolean + for i = start+1, last+1 do + if self.placed[bufnr][i] then + return true + end + end + return false +end + +function M:reset() + self.placed = emptytable() + fn.sign_unplace(self.group) + define_signs(self, true) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/status.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/status.tl new file mode 100644 index 0000000..d9ba9f0 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/status.tl @@ -0,0 +1,43 @@ +local api = vim.api + +local record StatusObj + added : integer + removed : integer + changed : integer + head : string + root : string + gitdir : string +end + +local Status = { + StatusObj = StatusObj, + formatter: function(StatusObj): string = nil +} + +function Status:update(bufnr: integer, status: StatusObj) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + local bstatus = vim.b[bufnr].gitsigns_status_dict + if bstatus then + status = vim.tbl_extend('force', bstatus as table, status as table) as StatusObj + end + vim.b[bufnr].gitsigns_head = status.head or '' + vim.b[bufnr].gitsigns_status_dict = status + vim.b[bufnr].gitsigns_status = self.formatter(status) +end + +function Status:clear(bufnr: integer) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + vim.b[bufnr].gitsigns_head = nil + vim.b[bufnr].gitsigns_status_dict = nil + vim.b[bufnr].gitsigns_status = nil +end + +function Status:clear_diff(bufnr: integer) + self:update(bufnr, { added = 0, removed = 0, changed = 0 }) +end + +return Status diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/subprocess.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/subprocess.tl new file mode 100644 index 0000000..2ac3f34 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/subprocess.tl @@ -0,0 +1,117 @@ +local gsd = require("gitsigns.debug") +local guv = require("gitsigns.uv") +local uv = vim.loop + +local record M + job_cnt: integer + + record JobSpec + command: string + args: {string} + cwd: string + writer: {string} | string + + record State + handle: uv.Process + pid: integer + stdout_data: {string} + stderr_data: {string} + stdin: uv.Pipe + stdout: uv.Pipe + stderr: uv.Pipe + code: integer + signal: integer + end + _state: State + end +end + +M.job_cnt = 0 + +local function try_close(pipe: uv.Pipe) + if pipe and not pipe:is_closing() then + pipe:close() + end +end + +function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, string)): M.JobSpec + if gsd.debug_mode then + local cmd: string = obj.command..' '..table.concat(obj.args, ' ') + gsd.dprint(cmd) + end + + obj._state = {} + local s = obj._state + s.stdout_data = {} + s.stderr_data = {} + + s.stdout = guv.new_pipe(false) + s.stderr = guv.new_pipe(false) + if obj.writer then + s.stdin = guv.new_pipe(false) + end + + s.handle, s.pid = guv.spawn(obj.command, { + args = obj.args, + stdio = { s.stdin, s.stdout, s.stderr }, + cwd = obj.cwd + }, + function(code: integer, signal: integer) + s.handle:close() + s.code = code + s.signal = signal + + if s.stdout then s.stdout:read_stop() end + if s.stderr then s.stderr:read_stop() end + + try_close(s.stdin) + try_close(s.stdout) + try_close(s.stderr) + + local stdout_result = #s.stdout_data > 0 and table.concat(s.stdout_data) or nil + local stderr_result = #s.stderr_data > 0 and table.concat(s.stderr_data) or nil + + callback(code, signal, stdout_result, stderr_result) + end + ) + + if not s.handle then + try_close(s.stdin) + try_close(s.stdout) + try_close(s.stderr) + error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) + end + + s.stdout:read_start(function(_, data: string) + s.stdout_data[#s.stdout_data+1] = data + end) + + s.stderr:read_start(function(_, data: string) + s.stderr_data[#s.stderr_data+1] = data + end) + + local writer = obj.writer + if writer is {string} then + local writer_len = #writer + for i, v in ipairs(writer) do + s.stdin:write(v) + if i ~= writer_len then + s.stdin:write("\n") + else + s.stdin:write("\n", function() + try_close(s.stdin) + end) + end + end + elseif writer then + -- write is string + s.stdin:write(writer, function() + try_close(s.stdin) + end) + end + + M.job_cnt = M.job_cnt + 1 + return obj +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/test.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/test.tl new file mode 100644 index 0000000..64a88c9 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/test.tl @@ -0,0 +1,43 @@ + +local record M + _tests: {string:function} +end + +local function eq(act: any, exp: any) + assert(act == exp, string.format('%s != %s', act, exp)) +end + +M._tests = {} + +M._tests.expand_format = function() + local util = require'gitsigns.util' + assert('hello % world % 2021' == util.expand_format(' % % ' , { + var1 = 'hello', var2 = 'world', var_time = 1616838297 })) +end + + +M._tests.test_args = function() + local parse_args = require'gitsigns.argparse'.parse_args + + local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') + + eq(pos_args[1], 'hello') + eq(pos_args[2], 'there') + eq(named_args.key, 'value,') + eq(named_args.key1, 'a b c') + + pos_args, named_args = parse_args('base=HEAD~1 posarg') + + eq(named_args.base, 'HEAD~1') + eq(pos_args[1], 'posarg') +end + +M._tests.test_name_parse = function() + local gs = require'gitsigns' + local path, commit = gs.parse_fugitive_uri( + 'fugitive:///home/path/to/project/.git//1b441b947c4bc9a59db428f229456619051dd133/subfolder/to/a/file.txt') + eq(path, '/home/path/to/project/subfolder/to/a/file.txt') + eq(commit, '1b441b947c4bc9a59db428f229456619051dd133') +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/util.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/util.tl new file mode 100644 index 0000000..0a1f32c --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/util.tl @@ -0,0 +1,199 @@ +local record M + type FmtInfo = {string:string|integer|{string}} + + path_sep: string +end + +function M.path_exists(path: string): boolean + return vim.loop.fs_stat(path) and true or false +end + +local jit_os: string + +if jit then + jit_os = jit.os:lower() +end + +local is_unix: boolean = false +if jit_os then + is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' +else + local binfmt = package.cpath:match("%p[\\|/]?%p(%a+)") + is_unix = binfmt ~= "dll" +end + +function M.dirname(file: string): string + return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) +end + +function M.file_lines(file: string): {string} + local text: {string} = {} + for line in io.lines(file) do + text[#text+1] = line + end + return text +end + +M.path_sep = package.config:sub(1, 1) + +function M.buf_lines(bufnr: integer): {string} + -- nvim_buf_get_lines strips carriage returns if fileformat==dos + local buftext: {string} = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #buftext do + buftext[i] = buftext[i]..'\r' + end + end + return buftext +end + +local function delete_alt(buf: integer) + local alt = vim.api.nvim_buf_call(buf, function(): integer + return vim.fn.bufnr('#') + end) as integer + if alt ~= buf and alt ~= -1 then + pcall(vim.api.nvim_buf_delete, alt, {force=true}) + end +end + +function M.buf_rename(bufnr: integer, name: string) + vim.api.nvim_buf_set_name(bufnr, name) + delete_alt(bufnr) +end + +function M.set_lines(bufnr: integer, start_row: integer, end_row: integer, lines: {string}) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #lines do + lines[i] = lines[i]:gsub('\r$', '') + end + end + vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) +end + +function M.tmpname(): string + if is_unix then + return os.tmpname() + end + return vim.fn.tempname() +end + +function M.get_relative_time(timestamp: number): string + local current_timestamp = os.time() + local elapsed = current_timestamp - timestamp + + if elapsed == 0 then + return 'a while ago' + end + + local minute_seconds = 60 + local hour_seconds = minute_seconds * 60 + local day_seconds = hour_seconds * 24 + local month_seconds = day_seconds * 30 + local year_seconds = month_seconds * 12 + + local to_relative_string = function(time: number, divisor: number, time_word: string): string + local num = math.floor(time / divisor) + if num > 1 then + time_word = time_word .. 's' + end + + return num .. ' ' .. time_word .. ' ago' + end + + if elapsed < minute_seconds then + return to_relative_string(elapsed, 1, 'second') + elseif elapsed < hour_seconds then + return to_relative_string(elapsed, minute_seconds, 'minute') + elseif elapsed < day_seconds then + return to_relative_string(elapsed, hour_seconds, 'hour') + elseif elapsed < month_seconds then + return to_relative_string(elapsed, day_seconds, 'day') + elseif elapsed < year_seconds then + return to_relative_string(elapsed, month_seconds, 'month') + else + return to_relative_string(elapsed, year_seconds, 'year') + end +end + +function M.copy_array(x: {T}): {T} + local r = {} + for i, e in ipairs(x) do + r[i] = e + end + return r +end + +-- Strip '\r' from the EOL of each line only if all lines end with '\r' +function M.strip_cr(xs0: {string}): {string} + for i = 1, #xs0 do + if xs0[i]:sub(-1) ~= '\r' then + -- don't strip, return early + return xs0 + end + end + -- all lines end with '\r', need to strip + local xs = vim.deepcopy(xs0) + for i = 1, #xs do + xs[i] = xs[i]:sub(1, -2) + end + return xs +end + +function M.calc_base(base: string): string + if base and base:sub(1, 1):match('[~\\^]') then + base = 'HEAD'..base + end + return base +end + +function M.emptytable(): T + return setmetatable({} as T, { + __index = function(t: table, k: any): any + t[k] = {} + return t[k] + end + }) +end + +local function expand_date(fmt: string, time: integer): string + if fmt == '%R' then + return M.get_relative_time(time) + end + return os.date(fmt, time) +end + +---@param reltime Use relative time as the default date format +function M.expand_format(fmt: string, info: M.FmtInfo, reltime: boolean): string + local ret = {} + + for _ = 1, 20 do -- loop protection + -- Capture or + local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') + if not match then + break + end + + ret[#ret+1], fmt = fmt:sub(1, scol-1), fmt:sub(ecol+1) + + local v = info[key] + + if v then + if v is {string} then + v = table.concat(v, '\n') + end + if vim.endswith(key, '_time') then + if time_fmt == '' then + time_fmt = reltime and '%R' or '%Y-%m-%d' + end + v = expand_date(time_fmt, v as integer) + end + match = tostring(v) + end + ret[#ret+1] = match + end + + ret[#ret+1] = fmt + return table.concat(ret, '') +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/uv.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/uv.tl new file mode 100644 index 0000000..49b2231 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/teal/gitsigns/uv.tl @@ -0,0 +1,63 @@ +local uv = vim.loop + +local record M + handles: {integer:{uv.Handle, boolean, string}} +end + +local handles: {integer:{uv.Handle, boolean, string}}= {} + +M.handles = handles + +function M.print_handles() + local none = true + for _, e in pairs(handles) do + local handle, longlived, tr = unpack(e) as (uv.Handle, boolean, string) + if handle and not longlived and not handle:is_closing() then + print('') + print(tr) + none = false + end + end + if none then + print('No active handles') + end +end + +vim.api.nvim_create_autocmd('VimLeavePre', { + callback = function() + for _, e in pairs(handles) do + local handle = e[1] + if handle and not handle:is_closing() then + handle:close() + end + end + end +}) + +function M.new_timer(longlived: boolean): uv.Timer + local r = uv.new_timer() + handles[#handles+1] = {r as uv.Handle, longlived, debug.traceback()} + return r +end + +function M.new_fs_poll(longlived: boolean): uv.FSPollObj + local r = uv.new_fs_poll() + handles[#handles+1] = {r as uv.Handle, longlived, debug.traceback()} + return r +end + +function M.new_pipe(ipc: boolean): uv.Pipe + local r = uv.new_pipe(ipc) + handles[#handles+1] = {r as uv.Handle, false, debug.traceback()} + return r +end + +function M.spawn(cmd: string, opts: uv.SpawnOpts, on_exit: function(integer, integer)): uv.Process, integer + local handle, pid = uv.spawn(cmd, opts, on_exit) + if handle then + handles[#handles+1] = {handle as uv.Handle, false, cmd..' '..vim.inspect(opts)} + end + return handle, pid +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/actions_spec.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/actions_spec.lua new file mode 100644 index 0000000..1658c36 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/actions_spec.lua @@ -0,0 +1,239 @@ +local helpers = require('test.gs_helpers') + +local setup_gitsigns = helpers.setup_gitsigns +local feed = helpers.feed +local test_file = helpers.test_file +local edit = helpers.edit +local command = helpers.command +local check = helpers.check +local exec_lua = helpers.exec_lua +local fn = helpers.funcs +local system = fn.system +local test_config = helpers.test_config +local cleanup = helpers.cleanup +local clear = helpers.clear +local setup_test_repo = helpers.setup_test_repo +local eq = helpers.eq +local expectf = helpers.expectf + +local it = helpers.it(it) + +local function expect_hunks(exp_hunks) + expectf(function() + local hunks = exec_lua"return require('gitsigns').get_hunks()" + if #exp_hunks~= #hunks then + local msg = {} + msg[#msg+1] = '' + msg[#msg+1] = string.format( + 'Number of hunks do not match. Expected: %d, passed in: %d', + #exp_hunks, #hunks) + msg[#msg+1] = '' + msg[#msg+1] = 'Expected hunks:' + for _, h in ipairs(exp_hunks) do + msg[#msg+1] = h.head + end + msg[#msg+1] = '' + msg[#msg+1] = 'Passed in hunks:' + for _, h in ipairs(hunks) do + msg[#msg+1] = h.head + end + error(table.concat(msg, '\n')) + end + for i, hunk in ipairs(hunks) do + eq(exp_hunks[i], hunk.head) + end + end) +end + +describe('actions', function() + local config + + before_each(function() + clear() + -- Make gitisigns available + exec_lua('package.path = ...', package.path) + config = helpers.deepcopy(test_config) + command('cd '..system{"dirname", os.tmpname()}) + setup_gitsigns(config) + end) + + after_each(function() + -- cleanup() + end) + + it('works with commands', function() + setup_test_repo() + edit(test_file) + + feed("jjjccEDIT") + check { + status = {head='master', added=0, changed=1, removed=0}, + signs = {changed=1} + } + + -- command 'Gitsigns stage_hunk' + -- check { + -- status = {head='master', added=0, changed=0, removed=0}, + -- signs = {} + -- } + + -- command 'Gitsigns undo_stage_hunk' + -- check { + -- status = {head='master', added=0, changed=1, removed=0}, + -- signs = {changed=1} + -- } + + -- -- Add multiple edits + -- feed('ggccThat') + + -- check { + -- status = {head='master', added=0, changed=2, removed=0}, + -- signs = {changed=2} + -- } + + -- command 'Gitsigns stage_buffer' + -- check { + -- status = {head='master', added=0, changed=0, removed=0}, + -- signs = {} + -- } + + -- command 'Gitsigns reset_buffer_index' + -- check { + -- status = {head='master', added=0, changed=2, removed=0}, + -- signs = {changed=2} + -- } + + -- command 'Gitsigns reset_hunk' + -- check { + -- status = {head='master', added=0, changed=1, removed=0}, + -- signs = {changed=1} + -- } + end) + + describe('staging partial hunks', function() + before_each(function() + setup_test_repo{test_file_text={'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'}} + edit(test_file) + end) + + local function set_lines(start, dend, lines) + exec_lua([[ + local start, dend, lines = ... + vim.api.nvim_buf_set_lines(0, start, dend, false, lines) + ]], start, dend, lines) + -- command('write') + end + + describe('can stage add hunks', function() + before_each(function() + set_lines(2, 2, {'c1', 'c2', 'c3', 'c4'}) + expect_hunks{ '@@ -2 +3,4 @@' } + end) + + it('contained in range', function() + command[[1,7 Gitsigns stage_hunk]] + expect_hunks{ } + end) + + it('containing range', function() + command[[4,5 Gitsigns stage_hunk]] + expect_hunks{ + '@@ -2 +3,1 @@', + '@@ -4 +6,1 @@' + } + end) + + it('from top range', function() + command[[1,4 Gitsigns stage_hunk]] + expect_hunks{ '@@ -4 +5,2 @@' } + end) + + it('from bottom range', function() + command[[4,7 Gitsigns stage_hunk]] + expect_hunks{ '@@ -2 +3,1 @@' } + + command[[Gitsigns reset_buffer_index]] + expect_hunks{ '@@ -2 +3,4 @@' } + + command[[4,10 Gitsigns stage_hunk]] + expect_hunks{ '@@ -2 +3,1 @@' } + end) + end) + + describe('can stage modified-add hunks', function() + before_each(function() + set_lines(2, 4, {'c1', 'c2', 'c3', 'c4', 'c5'}) + expect_hunks{ '@@ -3,2 +3,5 @@' } + end) + + it('from top range containing mod', function() + command[[2,3 Gitsigns stage_hunk]] + expect_hunks{ '@@ -4,1 +4,4 @@' } + end) + + it('from top range containing mod-add', function() + command[[2,5 Gitsigns stage_hunk]] + expect_hunks{ '@@ -5 +6,2 @@' } + end) + + it('from bottom range containing add', function() + command[[6,8 Gitsigns stage_hunk]] + expect_hunks{ '@@ -3,2 +3,3 @@' } + end) + + it('containing range containing add', function() + command'write' + command[[5,6 Gitsigns stage_hunk]] + expect_hunks{ + '@@ -3,2 +3,2 @@', + '@@ -6 +7,1 @@' + } + end) + + end) + + describe('can stage modified-remove hunks', function() + before_each(function() + set_lines(2, 7, {'c1', 'c2', 'c3'}) + command('write') + expect_hunks{ '@@ -3,5 +3,3 @@' } + end) + + it('from top range', function() + expect_hunks{ '@@ -3,5 +3,3 @@' } + + command[[2,3 Gitsigns stage_hunk]] + expect_hunks{ '@@ -4,4 +4,2 @@' } + + command[[2,3 Gitsigns reset_buffer_index]] + expect_hunks{ '@@ -3,5 +3,3 @@' } + + command[[2,4 Gitsigns stage_hunk]] + expect_hunks{ '@@ -5,3 +5,1 @@' } + end) + + it('from bottom range', function() + expect_hunks{ '@@ -3,5 +3,3 @@' } + + command[[4,6 Gitsigns stage_hunk]] + expect_hunks{ '@@ -3,1 +3,1 @@' } + + command[[2,3 Gitsigns reset_buffer_index]] + expect_hunks{ '@@ -3,5 +3,3 @@' } + + command[[5,6 Gitsigns stage_hunk]] + expect_hunks{ '@@ -3,2 +3,2 @@' } + end) + end) + + it('can stage remove hunks', function() + set_lines(2, 5, {}) + expect_hunks{ '@@ -3,3 +2 @@' } + + command[[2 Gitsigns stage_hunk]] + expect_hunks{} + end) + + end) + +end) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitdir_watcher_spec.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitdir_watcher_spec.lua new file mode 100644 index 0000000..93f93ec --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitdir_watcher_spec.lua @@ -0,0 +1,123 @@ +local helpers = require('test.gs_helpers') + +local Screen = require('test.functional.ui.screen') + +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local edit = helpers.edit +local eq = helpers.eq +local setup_test_repo = helpers.setup_test_repo +local cleanup = helpers.cleanup +local command = helpers.command +local test_config = helpers.test_config +local match_debug_messages = helpers.match_debug_messages +local p = helpers.p +local setup_gitsigns = helpers.setup_gitsigns +local test_file = helpers.test_file +local git = helpers.git +local get_buf_name = helpers.curbufmeths.get_name + +local it = helpers.it(it) + +local function get_bufs() + local bufs = {} + for _, b in ipairs(helpers.meths.list_bufs()) do + bufs[b.id] = helpers.meths.buf_get_name(b) + end + return bufs +end + +describe('gitdir_watcher', function() + before_each(function() + clear() + + -- Make gitisigns available + exec_lua('package.path = ...', package.path) + command('cd '..helpers.funcs.system{"dirname", os.tmpname()}) + end) + + after_each(function() + cleanup() + end) + + it('can follow moved files', function() + local screen = Screen.new(20, 17) + screen:attach({ext_messages=false}) + setup_test_repo() + setup_gitsigns(test_config) + command('Gitsigns clear_debug') + edit(test_file) + + local test_file2 = test_file..'2' + local test_file3 = test_file..'3' + + match_debug_messages { + 'attach(1): Attaching (trigger=BufRead)', + p"run_job: git .* config user.name", + p"run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD", + p('run_job: git .* ls%-files .* '..test_file), + 'watch_gitdir(1): Watching git dir', + p'run_job: git .* show :0:dummy.txt', + 'update(1): updates: 1, jobs: 6', + } + + eq({[1] = test_file}, get_bufs()) + + command('Gitsigns clear_debug') + + git{'mv', test_file, test_file2} + + match_debug_messages { + 'watcher_cb(1): Git dir update', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p('run_job: git .* ls%-files .* '..test_file), + p'run_job: git .* diff %-%-name%-status %-C %-%-cached', + 'handle_moved(1): File moved to dummy.txt2', + p('run_job: git .* ls%-files .* '..test_file2), + p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2', + p'run_job: git .* show :0:dummy.txt2', + 'update(1): updates: 2, jobs: 11' + } + + eq({[1] = test_file2}, get_bufs()) + + command('Gitsigns clear_debug') + + git{'mv', test_file2, test_file3} + + match_debug_messages { + 'watcher_cb(1): Git dir update', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p('run_job: git .* ls%-files .* '..test_file2), + p'run_job: git .* diff %-%-name%-status %-C %-%-cached', + 'handle_moved(1): File moved to dummy.txt3', + p('run_job: git .* ls%-files .* '..test_file3), + p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3', + p'run_job: git .* show :0:dummy.txt3', + 'update(1): updates: 3, jobs: 16' + } + + eq({[1] = test_file3}, get_bufs()) + + command('Gitsigns clear_debug') + + git{'mv', test_file3, test_file} + + match_debug_messages { + 'watcher_cb(1): Git dir update', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p('run_job: git .* ls%-files .* '..test_file3), + p'run_job: git .* diff %-%-name%-status %-C %-%-cached', + p('run_job: git .* ls%-files .* '..test_file), + 'handle_moved(1): Moved file reset', + p('run_job: git .* ls%-files .* '..test_file), + p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt', + p'run_job: git .* show :0:dummy.txt', + 'update(1): updates: 4, jobs: 22' + } + + eq({[1] = test_file}, get_bufs()) + + end) + +end) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitsigns_spec.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitsigns_spec.lua new file mode 100644 index 0000000..b5f2237 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gitsigns_spec.lua @@ -0,0 +1,762 @@ +-- vim: foldnestmax=5 foldminlines=1 + +local Screen = require('test.functional.ui.screen') +local helpers = require('test.gs_helpers') + +local clear = helpers.clear +local command = helpers.command +local exec_capture = helpers.exec_capture +local feed = helpers.feed +local insert = helpers.insert +local exec_lua = helpers.exec_lua +local split = helpers.split +local get_buf_var = helpers.curbufmeths.get_var +local fn = helpers.funcs +local system = fn.system +local expectf = helpers.expectf +local write_to_file = helpers.write_to_file +local edit = helpers.edit +local cleanup = helpers.cleanup +local test_file = helpers.test_file +local git = helpers.git +local scratch = helpers.scratch +local newfile = helpers.newfile +local debug_messages = helpers.debug_messages +local match_dag = helpers.match_dag +local match_lines = helpers.match_lines +local p = helpers.p +local match_debug_messages = helpers.match_debug_messages +local setup_gitsigns = helpers.setup_gitsigns +local setup_test_repo = helpers.setup_test_repo +local test_config = helpers.test_config +local check = helpers.check +local eq = helpers.eq + +local it = helpers.it(it) + +describe('gitsigns', function() + local screen + local config + + before_each(function() + clear() + screen = Screen.new(20, 17) + screen:attach({ext_messages=true}) + + screen:set_default_attr_ids({ + [1] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray}; + [2] = {background = Screen.colors.LightMagenta}; + [3] = {background = Screen.colors.LightBlue}; + [4] = {background = Screen.colors.LightCyan1, bold = true, foreground = Screen.colors.Blue1}; + [5] = {foreground = Screen.colors.Brown}; + [6] = {foreground = Screen.colors.Blue1, bold = true}; + [7] = {bold = true}, + [8] = {foreground = Screen.colors.White, background = Screen.colors.Red}; + [9] = {foreground = Screen.colors.SeaGreen, bold = true}; + [10] = {foreground = Screen.colors.Red}; + }) + + -- Make gitisigns available + exec_lua('package.path = ...', package.path) + config = helpers.deepcopy(test_config) + command('cd '..system{"dirname", os.tmpname()}) + end) + + after_each(function() + cleanup() + screen:detach() + end) + + it('can run basic setup', function() + setup_gitsigns() + check { status = {}, signs = {} } + end) + + it('gitdir watcher works on a fresh repo', function() + local nvim_ver = exec_lua('return vim.version().minor') + if nvim_ver == 8 then + -- v0.8.0 has some regression that's fixed it v0.9.0 dev + pending() + end + screen:try_resize(20,6) + setup_test_repo{no_add=true} + -- Don't set this too low, or else the test will lock up + config.watch_gitdir = {interval = 100} + setup_gitsigns(config) + edit(test_file) + + expectf(function() + match_dag(debug_messages(), { + p'run_job: git .* %-%-version', + 'attach(1): Attaching (trigger=BufRead)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..test_file), + 'watch_gitdir(1): Watching git dir', + p'run_job: git .* show :0:dummy.txt', + 'update(1): updates: 1, jobs: 7' + }) + end) + + check { + status = {head='', added=18, changed=0, removed=0}, + signs = {untracked=8} + } + + git{"add", test_file} + + check { + status = {head='', added=0, changed=0, removed=0}, + signs = {} + } + end) + + it('can open files not in a git repo', function() + setup_gitsigns(config) + command('Gitsigns clear_debug') + local tmpfile = os.tmpname() + edit(tmpfile) + + match_debug_messages { + 'attach(1): Attaching (trigger=BufRead)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + 'new: Not in git repo', + 'attach(1): Empty git obj', + } + command('Gitsigns clear_debug') + + insert('line') + command("write") + + match_debug_messages { + 'attach(1): Attaching (trigger=BufWritePost)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + 'new: Not in git repo', + 'attach(1): Empty git obj' + } + end) + + describe('when attaching', function() + before_each(function() + setup_test_repo() + setup_gitsigns(config) + end) + + it('can setup mappings', function() + edit(test_file) + expectf(function() + local res = split(exec_capture('nmap '), '\n') + table.sort(res) + + -- Check all keymaps get set + match_lines(res, { + 'n mhS *@lua require"gitsigns".stage_buffer()', + 'n mhU *@lua require"gitsigns".reset_buffer_index()', + 'n mhp *@lua require"gitsigns".preview_hunk()', + 'n mhr *@lua require"gitsigns".reset_hunk()', + 'n mhs *@lua require"gitsigns".stage_hunk()', + 'n mhu *@lua require"gitsigns".undo_stage_hunk()', + }) + end) + end) + + it('does not attach inside .git', function() + command("Gitsigns clear_debug") + edit(scratch..'/.git/index') + + match_debug_messages { + 'attach(1): Attaching (trigger=BufRead)', + 'new: In git dir', + 'attach(1): Empty git obj' + } + end) + + it('doesn\'t attach to ignored files', function() + command("Gitsigns clear_debug") + write_to_file(scratch..'/.gitignore', {'dummy_ignored.txt'}) + + local ignored_file = scratch.."/dummy_ignored.txt" + + system{"touch", ignored_file} + edit(ignored_file) + + match_debug_messages { + 'attach(1): Attaching (trigger=BufRead)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p'run_job: git .* ls%-files .*/dummy_ignored.txt', + 'attach(1): Cannot resolve file in repo', + } + + check {status = {head='master'}} + end) + + it('doesn\'t attach to non-existent files', function() + command("Gitsigns clear_debug") + edit(newfile) + + match_debug_messages { + 'attach(1): Attaching (trigger=BufNewFile)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..newfile), + 'attach(1): Not a file', + } + + check {status = {head='master'}} + end) + + it('doesn\'t attach to non-existent files with non-existent sub-dirs', function() + command("Gitsigns clear_debug") + edit(scratch..'/does/not/exist') + + match_debug_messages { + 'attach(1): Attaching (trigger=BufNewFile)', + 'attach(1): Not a path', + } + + helpers.pcall_err(get_buf_var, 'gitsigns_head') + helpers.pcall_err(get_buf_var, 'gitsigns_status_dict') + end) + + it('can run copen', function() + command("Gitsigns clear_debug") + command("copen") + match_debug_messages { + 'attach(2): Attaching (trigger=BufRead)', + 'attach(2): Non-normal buffer', + } + end) + + it('can run get_hunks()', function() + edit(test_file) + insert("line1") + feed("oline2") + + expectf(function() + eq({{ + head = '@@ -1,1 +1,2 @@', + type = 'change', + lines = { '-This', '+line1This', '+line2' }, + added = { count = 2, start = 1, lines = { 'line1This', 'line2' } }, + removed = { count = 1, start = 1, lines = { 'This'} }, + }}, + exec_lua[[return require'gitsigns'.get_hunks()]] + ) + end) + end) + end) + + describe('current line blame', function() + before_each(function() + config.current_line_blame = true + config.current_line_blame_formatter = ' , - ' + setup_gitsigns(config) + end) + + local function blame_line_ui_test(autocrlf, file_ending) + setup_test_repo() + + git{'config', 'core.autocrlf', autocrlf} + if file_ending == 'dos' then + system("printf 'This\r\nis\r\na\r\nwindows\r\nfile\r\n' > "..newfile) + else + system("printf 'This\nis\na\nwindows\nfile\n' > "..newfile) + end + git{'add', newfile} + git{"commit", "-m", "commit on main"} + + edit(newfile) + feed('gg') + command("Gitsigns clear_debug") + check { signs = {} } + + -- Wait until the virtual blame line appears + screen:sleep(1000) + screen:expect{grid=[[ + ^{MATCH:This {6: tester, %d seco}}| + is | + a | + windows | + file | + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + ]]} + end + + it('doesn\'t error on untracked files', function() + local nvim_ver = exec_lua('return vim.version().minor') + if nvim_ver >= 8 then + pending() + end + + setup_test_repo{no_add=true} + edit(newfile) + insert("line") + command("write") + screen:expect{messages = { { content = { { "<" } }, kind = "" } } } + end) + -- + it('does handle dos fileformats', function() + -- Add a file with windows line ending into the repo + -- Disable autocrlf, so that the file keeps the \r\n file endings. + blame_line_ui_test('false', 'dos') + end) + + it('does handle autocrlf', function() + blame_line_ui_test('true', 'dos') + end) + + it('does handle unix', function() + blame_line_ui_test('false', 'unix') + end) + end) + + describe('configuration', function() + it('handled deprecated fields', function() + -- TODO(lewis6991): All deprecated fields removed. Re-add when we have another deprecated field + pending() + -- config.current_line_blame_delay = 100 + -- setup_gitsigns(config) + -- eq(100, exec_lua([[return package.loaded['gitsigns.config'].config.current_line_blame_opts.delay]])) + end) + end) + + describe('on_attach()', function() + it('can prevent attaching to a buffer', function() + setup_test_repo{no_add=true} + + -- Functions can't be serialized over rpc so need to setup config + -- remotely + setup_gitsigns(config, [[ + config.on_attach = function() + return false + end + ]]) + command("Gitsigns clear_debug") + + edit(test_file) + match_debug_messages { + 'attach(1): Attaching (trigger=BufRead)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p'run_job: git .* rev%-parse %-%-short HEAD', + p'run_job: git .* %-%-git%-dir .* %-%-stage %-%-others %-%-exclude%-standard %-%-eol.*', + 'attach(1): User on_attach() returned false', + } + end) + end) + + describe('change_base()', function() + it('works', function() + setup_test_repo() + edit(test_file) + + feed('oEDIT') + command('write') + + git{'add', test_file} + git{"commit", "-m", "commit on main"} + + -- Don't setup gitsigns until the repo has two commits + setup_gitsigns(config) + + check { + status = {head='master', added=0, changed=0, removed=0}, + signs = {} + } + + command('Gitsigns change_base ~') + + check { + status = {head='master', added=1, changed=0, removed=0}, + signs = {added=1} + } + end) + end) + + local function testsuite(internal_diff) + return function() + before_each(function() + config.diff_opts = { + internal = internal_diff + } + setup_test_repo() + end) + + it('apply basic signs', function() + setup_gitsigns(config) + edit(test_file) + command("set signcolumn=yes") + + feed("dd") -- Top delete + feed("j") + feed("o") -- Add + feed("2j") + feed("x") -- Change + feed("3j") + feed("dd") -- Delete + feed("j") + feed("ddx") -- Change delete + + check { + status = {head='master', added=1, changed=2, removed=3}, + signs = {topdelete=1, changedelete=1, added=1, delete=1, changed=1} + } + + end) + + it('can enable numhl', function() + config.numhl = true + setup_gitsigns(config) + edit(test_file) + command("set signcolumn=no") + command("set number") + + feed("dd") -- Top delete + feed("j") + feed("o") -- Add + feed("2j") + feed("x") -- Change + feed("3j") + feed("dd") -- Delete + feed("j") + feed("ddx") -- Change delete + + -- screen:snapshot_util() + screen:expect{grid=[[ + {4: 1 }is | + {5: 2 }a | + {3: 3 } | + {5: 4 }file | + {2: 5 }sed | + {5: 6 }for | + {4: 7 }testing | + {5: 8 }The | + {2: 9 }^oesn't | + {5: 10 }matter, | + {5: 11 }it | + {5: 12 }just | + {5: 13 }needs | + {5: 14 }to | + {5: 15 }be | + {5: 16 }static. | + {6:~ }| + ]]} + end) + + it('attaches to newly created files', function() + setup_gitsigns(config) + command('Gitsigns clear_debug') + edit(newfile) + match_debug_messages{ + 'attach(1): Attaching (trigger=BufNewFile)', + p'run_job: git .* config user.name', + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p'run_job: git .* ls%-files .*', + 'attach(1): Not a file', + } + command('Gitsigns clear_debug') + command("write") + + local messages = { + 'attach(1): Attaching (trigger=BufWritePost)', + p"run_job: git .* config user.name", + p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', + p'run_job: git .* ls%-files .*', + 'watch_gitdir(1): Watching git dir', + p'run_job: git .* show :0:newfile.txt' + } + + if not internal_diff then + table.insert(messages, p'run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*') + end + + local jobs = internal_diff and 9 or 10 + table.insert(messages, "update(1): updates: 1, jobs: "..jobs) + + match_debug_messages(messages) + + check { + status = {head='master', added=1, changed=0, removed=0}, + signs = {untracked=1} + } + + end) + + it('can add untracked files to the index', function() + setup_gitsigns(config) + + edit(newfile) + feed("iline") + check{ status = {head='master'}} + + command("write") + + check { + status = {head='master', added=1, changed=0, removed=0}, + signs = {untracked=1} + } + + feed('mhs') -- Stage the file (add file to index) + + check { + status = {head='master', added=0, changed=0, removed=0}, + signs = {} + } + + end) + + it('tracks files in new repos', function() + setup_gitsigns(config) + system{"touch", newfile} + edit(newfile) + + feed("iEDIT") + command("write") + + check { + status = {head='master', added=1, changed=0, removed=0}, + signs = {untracked=1} + } + + git{"add", newfile} + + check { + status = {head='master', added=0, changed=0, removed=0}, + signs = {} + } + + git{"reset"} + + check { + status = {head='master', added=1, changed=0, removed=0}, + signs = {untracked=1} + } + + end) + + it('can detach from buffers', function() + setup_gitsigns(config) + edit(test_file) + command("set signcolumn=yes") + + feed("dd") -- Top delete + feed("j") + feed("o") -- Add + feed("2j") + feed("x") -- Change + feed("3j") + feed("dd") -- Delete + feed("j") + feed("ddx") -- Change delete + + check { + status = {head='master', added=1, changed=2, removed=3}, + signs = {topdelete=1, added=1, changed=1, delete=1, changedelete=1} + } + + command('Gitsigns detach') + + check { status = {}, signs = {} } + end) + + it('can stages file with merge conflicts', function() + setup_gitsigns(config) + command("set signcolumn=yes") + + -- Edit a file and commit it on main branch + edit(test_file) + check{ status = {head='master', added=0, changed=0, removed=0} } + feed('iedit') + check{ status = {head='master', added=0, changed=1, removed=0} } + command("write") + command("bdelete") + git{'add', test_file} + git{"commit", "-m", "commit on main"} + + -- Create a branch, remove last commit, edit file again + git{'checkout', '-B', 'abranch'} + git{'reset', '--hard', 'HEAD~1'} + edit(test_file) + check{ status = {head='abranch', added=0, changed=0, removed=0} } + feed('idiff') + check{ status = {head='abranch', added=0, changed=1, removed=0} } + command("write") + command("bdelete") + git{'add', test_file} + git{"commit", "-m", "commit on branch"} + git{"rebase", "master"} + + -- test_file should have a conflict + edit(test_file) + check { + status = {head='HEAD(rebasing)', added=4, changed=1, removed=0}, + signs = {changed=1, added=4} + } + + exec_lua('require("gitsigns.actions").stage_hunk()') + + check { + status = {head='HEAD(rebasing)', added=0, changed=0, removed=0}, + signs = {} + } + + end) + + it('handle files with spaces', function() + setup_gitsigns(config) + command("set signcolumn=yes") + + local spacefile = scratch..'/a b c d' + + write_to_file(spacefile, {'spaces', 'in', 'file'}) + + edit(spacefile) + + check { + status = {head='master', added=3, removed=0, changed=0}, + signs = {untracked=3} + } + + git{'add', spacefile} + edit(spacefile) + + check { + status = {head='master', added=0, removed=0, changed=0}, + signs = {} + } + + end) + end + end + + -- Run regular config + describe('diff-ext', testsuite(false)) + + -- Run with: + -- - internal diff (ffi) + -- - decoration provider + describe('diff-int', testsuite(true)) + + it('can handle vimgrep', function() + setup_test_repo() + + write_to_file(scratch..'/t1.txt', {'hello ben'}) + write_to_file(scratch..'/t2.txt', {'hello ben'}) + write_to_file(scratch..'/t3.txt', {'hello lewis'}) + + setup_gitsigns(config) + command('Gitsigns clear_debug') + + helpers.exc_exec("vimgrep ben "..scratch..'/*') + + screen:expect{messages = {{ + kind = "quickfix", content = { { "(1 of 2): hello ben" } }, + }}} + + eq({ + 'attach(2): attaching is disabled', + 'attach(3): attaching is disabled', + 'attach(4): attaching is disabled', + 'attach(5): attaching is disabled', + }, exec_lua[[return require'gitsigns'.debug_messages(true)]]) + + end) + + it('show short SHA when detached head', function() + setup_test_repo() + git{"checkout", "--detach"} + + -- Disable debug_mode so the sha is calculated + config.debug_mode = false + setup_gitsigns(config) + edit(test_file) + + -- SHA is not deterministic so just check it can be cast as a hex value + expectf(function() + helpers.neq(nil, tonumber('0x'..get_buf_var('gitsigns_head'))) + end) + end) + + it('handles a quick undo', function() + setup_test_repo() + setup_gitsigns(config) + edit(test_file) + -- This test isn't deterministic so run it a few times + for _ = 1, 3 do + feed("x") + check { signs = {changed=1} } + feed("u") + check { signs = {} } + end + end) + + it('handles filenames with unicode characters', function() + screen:try_resize(20,2) + setup_test_repo() + setup_gitsigns(config) + local uni_filename = scratch..'/föobær' + + write_to_file(uni_filename, {'Lorem ipsum'}) + git{"add", uni_filename} + git{"commit", "-m", "another commit"} + + edit(uni_filename) + + screen:expect{grid=[[ + ^Lorem ipsum | + {6:~ }| + ]]} + + feed 'x' + + screen:expect{grid=[[ + {2:~ }^orem ipsum | + {6:~ }| + ]]} + end) + + it('handle #521', function() + screen:detach() + screen:attach({ext_messages=false}) + screen:try_resize(20,3) + setup_test_repo() + setup_gitsigns(config) + edit(test_file) + feed('dd') + + local function check_screen() + screen:expect{grid=[[ + {4:^ }^is | + {1: }a | + {1: }file | + ]]} + end + + check_screen() + + -- Write over the text with itself. This will remove all the signs but the + -- calculated hunks won't change. + exec_lua[[ + local text = vim.api.nvim_buf_get_lines(0, 0, -1, false) + vim.api.nvim_buf_set_lines(0, 0, -1, true, text) + ]] + + check_screen() + + end) + +end) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/gs_helpers.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gs_helpers.lua new file mode 100644 index 0000000..127f836 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/gs_helpers.lua @@ -0,0 +1,301 @@ +local helpers = require('test.functional.helpers')() + +local system = helpers.funcs.system +local exec_lua = helpers.exec_lua +local matches = helpers.matches +local exec_capture = helpers.exec_capture +local eq = helpers.eq +local fn = helpers.funcs +local get_buf_var = helpers.curbufmeths.get_var + +local timeout = 4000 + +local M = helpers + +M.inspect = require('vim.inspect') + +M.scratch = os.getenv('PJ_ROOT')..'/scratch' +M.gitdir = M.scratch..'/.git' +M.test_file = M.scratch..'/dummy.txt' +M.newfile = M.scratch.."/newfile.txt" + +M.test_config = { + debug_mode = true, + signs = { + add = {hl = 'DiffAdd' , text = '+'}, + delete = {hl = 'DiffDelete', text = '_'}, + change = {hl = 'DiffChange', text = '~'}, + topdelete = {hl = 'DiffDelete', text = '^'}, + changedelete = {hl = 'DiffChange', text = '%'}, + untracked = {hl = 'DiffChange', text = '#'}, + }, + keymaps = { + noremap = true, + buffer = true, + ['n mhs'] = 'lua require"gitsigns".stage_hunk()', + ['n mhu'] = 'lua require"gitsigns".undo_stage_hunk()', + ['n mhr'] = 'lua require"gitsigns".reset_hunk()', + ['n mhp'] = 'lua require"gitsigns".preview_hunk()', + ['n mhS'] = 'lua require"gitsigns".stage_buffer()', + ['n mhU'] = 'lua require"gitsigns".reset_buffer_index()', + }, + update_debounce = 5, +} + +local test_file_text = { + 'This', 'is', 'a', 'file', 'used', 'for', 'testing', 'gitsigns.', 'The', + 'content', 'doesn\'t', 'matter,', 'it', 'just', 'needs', 'to', 'be', 'static.' +} + +function M.git(args) + system{"git", "-C", M.scratch, unpack(args)} +end + +function M.cleanup() + system{"rm", "-rf", M.scratch} +end + + +function M.setup_git() + M.git{"init", '-b', 'master'} + + -- Always force color to test settings don't interfere with gitsigns systems + -- commands (addresses #23) + M.git{'config', 'color.branch' , 'always'} + M.git{'config', 'color.ui' , 'always'} + M.git{'config', 'color.diff' , 'always'} + M.git{'config', 'color.interactive', 'always'} + M.git{'config', 'color.status' , 'always'} + M.git{'config', 'color.grep' , 'always'} + M.git{'config', 'color.pager' , 'true'} + M.git{'config', 'color.decorate' , 'always'} + M.git{'config', 'color.showbranch' , 'always'} + + M.git{'config', 'merge.conflictStyle', 'merge'} + + M.git{'config', 'user.email', 'tester@com.com'} + M.git{'config', 'user.name' , 'tester'} + + M.git{'config', 'init.defaultBranch', 'master'} +end + +function M.setup_test_repo(opts) + local text = opts and opts.test_file_text or test_file_text + M.cleanup() + system{"mkdir", M.scratch} + M.setup_git() + system{"touch", M.test_file} + M.write_to_file(M.test_file, text) + if not (opts and opts.no_add) then + M.git{"add", M.test_file} + M.git{"commit", "-m", "init commit"} + end +end + +function M.expectf(cond, interval) + local duration = 0 + interval = interval or 1 + while duration < timeout do + if pcall(cond) then + return + end + duration = duration + interval + helpers.sleep(interval) + interval = interval * 2 + end + cond() +end + +function M.command_fmt(str, ...) + helpers.command(str:format(...)) +end + +function M.edit(path) + M.command_fmt("edit %s", path) +end + +function M.write_to_file(path, text) + local f = io.open(path, 'wb') + for _, l in ipairs(text) do + f:write(l) + f:write('\n') + end + f:close() +end + +function M.match_lines(lines, spec) + local i = 1 + for lid, line in ipairs(lines) do + if line ~= '' then + local s = spec[i] + if s then + if s.pattern then + matches(s.text, line) + else + eq(s, line) + end + else + local extra = {} + for j=lid,#lines do + table.insert(extra, lines[j]) + end + error('Unexpected extra text:\n '..table.concat(extra, '\n ')) + end + i = i + 1 + end + end + if i < #spec + 1 then + local msg = {'lines:'} + for _, l in ipairs(lines) do + msg[#msg+1] = string.format( '"%s"', l) + end + error(('Did not match pattern \'%s\' with %s'):format(spec[i], table.concat(msg, '\n'))) + end +end + +local function match_lines2(lines, spec) + local i = 1 + for _, line in ipairs(lines) do + if line ~= '' then + local s = spec[i] + if s then + if s.pattern then + if string.match(line, s.text) then + i = i + 1 + end + elseif s.next then + eq(s.text, line) + i = i + 1 + else + if s == line then + i = i + 1 + end + end + end + end + end + + if i < #spec + 1 then + local unmatched_msg = table.concat(helpers.tbl_map(function(v) + return string.format(' - %s', v.text or v) + end, spec), '\n') + + local lines_msg = table.concat(helpers.tbl_map(function(v) + return string.format(' - %s', v) + end, lines), '\n') + + error(('Did not match patterns:\n%s\nwith:\n%s'):format( + unmatched_msg, + lines_msg + )) + end +end + +function M.p(str) + return {text=str, pattern=true} +end + +function M.n(str) + return {text=str, next=true} +end + +function M.debug_messages() + return exec_lua("return require'gitsigns'.debug_messages(true)") +end + +function M.match_dag(lines, spec) + for _, s in ipairs(spec) do + match_lines2(lines, {s}) + end +end + +function M.match_debug_messages(spec) + M.expectf(function() + M.match_lines(M.debug_messages(), spec) + end) +end + +function M.setup_gitsigns(config, extra) + extra = extra or '' + exec_lua([[ + local config = ... + ]]..extra..[[ + require('gitsigns').setup(...) + ]], config) + M.expectf(function() + exec_capture('au gitsigns') + end) +end + +local id = 0 +M.it = function(it) + return function(name, test) + id = id+1 + return it(name..' #'..id..'#', test) + end +end + +function M.check(attrs, interval) + attrs = attrs or {} + M.expectf(function() + local status = attrs.status + local signs = attrs.signs + + if status then + if next(status) == nil then + eq(0, fn.exists('b:gitsigns_head'), + 'b:gitsigns_head is unexpectedly set') + eq(0, fn.exists('b:gitsigns_status_dict'), + 'b:gitsigns_status_dict is unexpectedly set') + else + eq(1, fn.exists('b:gitsigns_head'), + 'b:gitsigns_head is not set') + eq(status.head, get_buf_var('gitsigns_head'), + 'b:gitsigns_head does not match') + + local bstatus = get_buf_var("gitsigns_status_dict") + + for _, i in ipairs{'added', 'changed', 'removed', 'head'} do + eq(status[i], bstatus[i], + string.format("status['%s'] did not match gitsigns_status_dict", i)) + end + -- Catch any extra keys + for i, v in pairs(status) do + eq(v, bstatus[i], + string.format("status['%s'] did not match gitsigns_status_dict", i)) + end + end + end + + if signs then + local act = { + added = 0, + changed = 0, + delete = 0, + changedelete = 0, + topdelete = 0, + untracked = 0, + } + + for k, _ in pairs(act) do + signs[k] = signs[k] or 0 + end + + local buf_signs = fn.sign_getplaced("%", {group='*'})[1].signs + + for _, s in ipairs(buf_signs) do + if s.name == "GitSignsAdd" then act.added = act.added + 1 + elseif s.name == "GitSignsChange" then act.changed = act.changed + 1 + elseif s.name == "GitSignsDelete" then act.delete = act.delete + 1 + elseif s.name == "GitSignsChangedelete" then act.changedelete = act.changedelete + 1 + elseif s.name == "GitSignsTopdelete" then act.topdelete = act.topdelete + 1 + elseif s.name == "GitSignsUntracked" then act.untracked = act.untracked + 1 + end + end + + eq(signs, act, M.inspect(buf_signs)) + end + end, interval) +end + +return M diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/highlights_spec.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/highlights_spec.lua new file mode 100644 index 0000000..63f0c16 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/highlights_spec.lua @@ -0,0 +1,128 @@ +local Screen = require('test.functional.ui.screen') +local helpers = require('test.gs_helpers') + +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local command = helpers.command +local eq = helpers.eq +local exec_capture = helpers.exec_capture + +local cleanup = helpers.cleanup +local test_config = helpers.test_config +local expectf = helpers.expectf +local match_dag = helpers.match_dag +local debug_messages = helpers.debug_messages +local p = helpers.p +local setup_gitsigns = helpers.setup_gitsigns + +local it = helpers.it(it) + +describe('highlights', function() + local screen + local config + + before_each(function() + clear() + screen = Screen.new(20, 17) + screen:attach() + + screen:set_default_attr_ids({ + [1] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray}; + [2] = {background = Screen.colors.LightMagenta}; + [3] = {background = Screen.colors.LightBlue}; + [4] = {background = Screen.colors.LightCyan1, bold = true, foreground = Screen.colors.Blue1}; + [5] = {foreground = Screen.colors.Brown}; + [6] = {foreground = Screen.colors.Blue1, bold = true}; + [7] = {bold = true}, + [8] = {foreground = Screen.colors.White, background = Screen.colors.Red}; + [9] = {foreground = Screen.colors.SeaGreen, bold = true}; + }) + + -- Make gitisigns available + exec_lua('package.path = ...', package.path) + exec_lua('gs = require("gitsigns")') + config = helpers.deepcopy(test_config) + end) + + after_each(function() + cleanup() + screen:detach() + end) + it('get set up correctly', function() + command("set termguicolors") + + config.signs.add.hl = nil + config.signs.change.hl = nil + config.signs.delete.hl = nil + config.signs.changedelete.hl = nil + config.signs.topdelete.hl = nil + config.numhl = true + config.linehl = true + + exec_lua('gs.setup(...)', config) + + expectf(function() + match_dag(debug_messages(), { + p'Deriving GitSignsAdd from DiffAdd', + p'Deriving GitSignsAddLn from DiffAdd', + p'Deriving GitSignsAddNr from GitSignsAdd', + p'Deriving GitSignsChangeLn from DiffChange', + p'Deriving GitSignsChangeNr from GitSignsChange', + p'Deriving GitSignsDelete from DiffDelete', + p'Deriving GitSignsDeleteNr from GitSignsDelete', + }) + end) + + eq('GitSignsChange xxx links to DiffChange', + exec_capture('hi GitSignsChange')) + + eq('GitSignsDelete xxx links to DiffDelete', + exec_capture('hi GitSignsDelete')) + + eq('GitSignsAdd xxx links to DiffAdd', + exec_capture('hi GitSignsAdd')) + end) + + it('update when colorscheme changes', function() + command("set termguicolors") + + config.signs.add.hl = nil + config.signs.change.hl = nil + config.signs.delete.hl = nil + config.signs.changedelete.hl = nil + config.signs.topdelete.hl = nil + config.linehl = true + + setup_gitsigns(config) + + expectf(function() + eq('GitSignsChange xxx links to DiffChange', + exec_capture('hi GitSignsChange')) + + eq('GitSignsDelete xxx links to DiffDelete', + exec_capture('hi GitSignsDelete')) + + eq('GitSignsAdd xxx links to DiffAdd', + exec_capture('hi GitSignsAdd')) + + eq('GitSignsAddLn xxx links to DiffAdd', + exec_capture('hi GitSignsAddLn')) + end) + + command('colorscheme blue') + + expectf(function() + eq('GitSignsChange xxx links to DiffChange', + exec_capture('hi GitSignsChange')) + + eq('GitSignsDelete xxx links to DiffDelete', + exec_capture('hi GitSignsDelete')) + + eq('GitSignsAdd xxx links to DiffAdd', + exec_capture('hi GitSignsAdd')) + + eq('GitSignsAddLn xxx links to DiffAdd', + exec_capture('hi GitSignsAddLn')) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/preload.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/preload.lua new file mode 100644 index 0000000..c7ec705 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/preload.lua @@ -0,0 +1,12 @@ +-- Modules loaded here will not be cleared and reloaded by Busted. +-- Busted started doing this to help provide more isolation. +local global_helpers = require('test.helpers') + +-- Bypoass CI behaviour logic +global_helpers.isCI = function(_) + return false +end + +local helpers = require('test.functional.helpers')(nil) +local gs_helpers = require('test.gs_helpers') + diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/test/unit_spec.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/test/unit_spec.lua new file mode 100644 index 0000000..c393497 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/test/unit_spec.lua @@ -0,0 +1,63 @@ +local helpers = require('test.gs_helpers') +local exec_lua = helpers.exec_lua +local setup_gitsigns = helpers.setup_gitsigns +local clear = helpers.clear + +local function get_tests(pattern) + local modules = exec_lua[[return vim.tbl_keys(package.loaded)]] + + local tests = {} + for _, mod in ipairs(modules) do + if mod:match(pattern) then + tests[mod] = exec_lua([[ + local mod = package.loaded[...] + if type(mod) == 'table' then + return vim.tbl_keys(mod._tests or {}) + end + return {} + ]], mod) + end + end + return tests +end + +local function run_test(mod, test) + return unpack(exec_lua([[ + local mod, test = ... + return {pcall(package.loaded[mod]._tests[test])} + ]], mod, test)) +end + +local function load(mod) + exec_lua([[require(...)]], mod) +end + +describe('unit test', function() + clear() + exec_lua('package.path = ...', package.path) + exec_lua('_TEST = true') + setup_gitsigns{debug_mode = true} + + -- Add modules which have unit tests + -- TODO(lewis6991): automate + load('gitsigns.test') + + local gs_tests = get_tests('^gitsigns') + + for mod, tests in pairs(gs_tests) do + for _, test in ipairs(tests) do + it(mod..':'..test, function() + local ok, err = run_test(mod, test) + + if not ok then + local msgs = helpers.debug_messages() + for _, msg in ipairs(msgs) do + print(msg) + end + error(err) + end + end) + end + + end +end) diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/tlconfig.lua b/etc/soft/nvim/+plugins/gitsigns.nvim/tlconfig.lua new file mode 100644 index 0000000..eee9a92 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/tlconfig.lua @@ -0,0 +1,10 @@ +return { + gen_target = '5.1', + gen_compat = 'off', + global_env_def = 'types', + include_dir = { + 'types', 'teal', + }, + source_dir = 'teal', + build_dir = "lua", +} diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/types/ffi.d.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/types/ffi.d.tl new file mode 100644 index 0000000..82ce840 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/types/ffi.d.tl @@ -0,0 +1,25 @@ + +local record Cdefs + xdl_diff: (function(...:any): number) +end + +-- C callback +local record CCB + free: function(CCB) + set: function(CCB, function) +end + +local record ffi + cdef: function(string) + load: function(string): any + new: function(string, ...:any): any + string: function(any, number): string + gc: function(T, function): T + C: Cdefs + cast: function(string, any): CCB + errno: function(): integer + copy: function + metatype: function(string, any) +end + +return ffi diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/types/trouble.d.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/types/trouble.d.tl new file mode 100644 index 0000000..17c77f1 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/types/trouble.d.tl @@ -0,0 +1,5 @@ +local record Trouble + open: function(mode: string) +end + +return Trouble diff --git a/etc/soft/nvim/+plugins/gitsigns.nvim/types/types.d.tl b/etc/soft/nvim/+plugins/gitsigns.nvim/types/types.d.tl new file mode 100644 index 0000000..494dfd3 --- /dev/null +++ b/etc/soft/nvim/+plugins/gitsigns.nvim/types/types.d.tl @@ -0,0 +1,671 @@ +global _: any + +global _TEST: boolean + +global loadstring: function(string): (function(): any) + +global unpack: function({T}, number, number): T... + +global getfenv: function(function): table + +global record jit + arch: string + os: string + version: string + version_num: number + off: function() + off: function(function) + on: function() + status: function(): boolean +end + +local record api + record UserCmdParams + args: string + bang: boolean + line1: integer + line2: integer + range: integer + count: number + reg: string + mods: string + + record Mods + browse : boolean + confirm : boolean + emsg_silent : boolean + hide : boolean + keepalt : boolean + keepjumps : boolean + keepmarks : boolean + keeppatterns : boolean + lockmarks : boolean + noautocmd : boolean + noswapfile : boolean + sandbox : boolean + silent : boolean + tab : integer + verbose : integer + vertical : boolean + + enum Split '' + 'aboveleft' + 'belowright' + 'topleft' + 'botright' + end + + split : Split + end + smods: Mods + end + + record UserCmdOpts + nargs: string|integer + range: boolean|string|integer + count: boolean|integer + addr: string + bang: boolean + bar: boolean + register: boolean + force: boolean + complete: string|function(arglead: string, line: string): {string} + end + nvim_create_user_command : function(string, function(UserCmdParams), UserCmdOpts) + nvim_buf_attach : function(integer, boolean, {string:any}): boolean + nvim_buf_call : function(integer, function(): T): T + nvim_buf_clear_namespace : function(integer, number, number, number) + nvim_buf_del_extmark : function(integer, number, number): boolean + nvim_buf_delete : function(integer, {string:boolean}) + nvim_buf_get_extmark_by_id : function(integer, number, number, table): {number} + + record GetExtmarOpts + limit: integer + details: boolean + end + + nvim_buf_get_extmarks: function( + buf: integer, + ns: integer, + start: integer | {integer, integer}, + eend: integer | {integer, integer}, + GetExtmarOpts + ): {{integer, integer, integer}} + + nvim_buf_get_lines : function(integer, number, number, boolean): {string} + nvim_buf_get_name : function(integer): string + nvim_buf_is_loaded : function(integer): boolean + nvim_buf_is_valid : function(integer): boolean + nvim_buf_line_count : function(integer): integer + nvim_buf_set_extmark : function(integer, integer, integer, integer, {string:any}): integer + nvim_buf_set_keymap : function(integer, string, string, string, {string:any}) + nvim_buf_set_lines : function(integer, number, number, boolean, {string}) + nvim_buf_set_name : function(integer, string) + nvim_create_buf : function(boolean, boolean): integer + nvim_create_namespace : function(string): integer + + record AugroupOpts + clear: boolean + end + + nvim__buf_redraw_range : function(integer, number, number) + + nvim_create_augroup: function(string, AugroupOpts): integer + + record AutoCmdOpts + callback: function() + command: string + group: string + pattern: string|{string} + once: boolean + nested: boolean + desc: string + buffer: integer + end + + nvim_create_autocmd: function(string|{string}, AutoCmdOpts): integer + nvim_del_current_line: function() + nvim_del_keymap: function(string, string) + nvim_del_var: function(string) + nvim_echo: function({{string}}, boolean, {string:any}) + nvim_err_write: function(string) + nvim_err_writeln: function(string) + nvim_eval: function(string): any + nvim_exec: function(string, boolean): string + + record ExecAutoCmdOpts + group: string|integer + pattern: string|{string} + buffer: integer + modeline: boolean + data: any + end + + nvim_exec_autocmds: function(string|{string}, ExecAutoCmdOpts): any + nvim_exec_lua: function(string, any): any + nvim_feedkeys: function(string, string, boolean) + nvim_get_api_info: function(): any + nvim_get_chan_info: function(number): {string:any} + nvim_get_color_by_name: function(string): number + nvim_get_color_map: function(): {string:any} + nvim_get_commands: function({string:any}): {string:any} + nvim_get_context: function({string:any}): {string:any} + nvim_get_current_buf: function(): integer + nvim_get_current_line: function(): string + nvim_get_current_tabpage: function(): any + nvim_get_current_win: function(): integer + nvim_get_hl_by_id: function(number, boolean): {string:any} + nvim_get_hl_by_name: function(string, boolean): {string:any} + nvim_get_hl_id_by_name: function(string): number + nvim_get_keymap: function(string): {{string:any}} + nvim_get_mode: function(): {string:any} + nvim_get_namespaces: function(): {string:any} + nvim_get_option: function(string): any + nvim_get_proc: function(number): any + nvim_get_proc_children: function(number): any + nvim_get_runtime_file: function(string, boolean): {string} + nvim_get_var: function(string): any + nvim_get_vvar: function(string): any + nvim_input: function(string): number + nvim_input_mouse: function(string, string, string, number, number, number) + nvim_list_bufs: function(): {integer} + nvim_list_chans: function(): any + nvim_list_runtime_paths: function(): {string} + nvim_list_tabpages: function(): {any} + nvim_list_uis: function(): any + nvim_list_wins: function(): {integer} + nvim_load_context: function({string:any}): any + nvim_open_win: function(number, boolean, {string:any}): integer + nvim_out_write: function(string) + nvim_parse_expression: function(string, string, boolean): {string:any} + nvim_paste: function(string, boolean, number): boolean + nvim_put: function({string}, string, boolean, boolean) + nvim_replace_termcodes: function(string, boolean, boolean, boolean): string + nvim_select_popupmenu_item: function(number, boolean, boolean, {string:any}) + nvim_set_client_info: function(string, {string:any}, string, {string:any}, {string:any}) + nvim_set_current_buf: function(number) + nvim_set_current_dir: function(string) + nvim_set_current_line: function(string) + nvim_set_current_tabpage: function(any) + nvim_set_current_win: function(number) + nvim_set_decoration_provider: function(number, {string:function}) + nvim_set_hl: function(integer, string, {string:any}) + nvim_set_keymap: function(string, string, string, {string:any}) + nvim_set_option: function(string, any) + nvim_set_var: function(string, any) + nvim_set_vvar: function(string, any) + nvim_strwidth: function(string): number + nvim_subscribe: function(string) + nvim_tabpage_del_var: function(any, string) + nvim_tabpage_get_number: function(any): number + nvim_tabpage_get_var: function(any, string): any + nvim_tabpage_get_win: function(any): number + nvim_tabpage_is_valid: function(any): boolean + nvim_tabpage_list_wins: function(any): {number} + nvim_tabpage_set_var: function(any, string, any) + nvim_ui_attach: function(number, number, {string:any}) + nvim_ui_detach: function() + nvim_ui_pum_set_bounds: function(number, number, number, number) + nvim_ui_pum_set_height: function(number) + nvim_ui_set_option: function(string, any) + nvim_ui_try_resize: function(number, number) + nvim_ui_try_resize_grid: function(number, number, number) + nvim_unsubscribe: function(string) + nvim_win_call: function(number, (function(): T)): T + nvim_win_close: function(number, boolean) + nvim_win_del_var: function(number, string) + nvim_win_get_buf: function(integer): integer + nvim_win_get_config: function(number): {string:any} + nvim_win_get_cursor: function(number): {integer} + nvim_win_get_height: function(integer): integer + nvim_win_get_number: function(number): number + nvim_win_get_option: function(number, string): any + nvim_win_get_position: function(number): {number} + nvim_win_get_tabpage: function(number): any + nvim_win_get_var: function(number, string): any + nvim_win_get_width: function(integer): integer + nvim_win_is_valid: function(number): boolean + nvim_win_set_buf: function(number, number) + nvim_win_set_config: function(number, {string:any}) + nvim_win_set_cursor: function(number, {number}) + nvim_win_set_height: function(number, number) + nvim_win_set_option: function(number, string, any) + nvim_win_set_var: function(number, string, any) + nvim_win_set_width: function(number, number) + + nvim__buf_redraw_range: function(number, number, number) + nvim_create_autocmd : function(string|{string}, AutoCmdOpts): integer + nvim_echo : function({{string}}, boolean, {string:any}) + nvim_get_color_by_name : function(string): number + nvim_get_current_buf : function(): integer + nvim_get_current_line : function(): string + nvim_get_current_tabpage : function(): any + nvim_get_current_win : function(): integer + nvim_get_hl_by_name : function(string, boolean): {string:any} + nvim_get_mode : function(): {string:any} + nvim_list_bufs : function(): {integer} + nvim_list_wins : function(): {integer} + nvim_open_win : function(integer, boolean, {string:any}): integer + nvim_replace_termcodes : function(string, boolean, boolean, boolean): string + nvim_set_current_buf : function(integer) + nvim_set_current_line : function(string) + nvim_set_current_win : function(integer) + nvim_set_decoration_provider : function(integer, {string:function}) + nvim_set_hl : function(integer, string, {string:any}) + nvim_set_keymap : function(string, string, string, {string:any}) + nvim_strwidth : function(string): number + nvim_win_call : function(integer, (function(): T)): T + nvim_win_close : function(integer, boolean) + nvim_win_get_buf : function(integer): integer + nvim_win_get_config : function(integer): {string:any} + nvim_win_get_cursor : function(integer): {integer} + nvim_win_get_height : function(integer): integer + nvim_win_get_width : function(integer): integer + nvim_win_is_valid : function(integer): boolean + nvim_win_set_buf : function(integer, number) + nvim_win_set_config : function(integer, {string:any}) + nvim_win_set_cursor : function(integer, {number}) + nvim_win_set_height : function(integer, number) + nvim_win_set_width : function(integer, number) +end + +global record vim + api: api + record fn + bufexists: function(string): integer + bufnr: function(string): integer + iconv: function(string, string, string): string + line: function(string): integer + join: function({any}, string): string + getpos: function(string): {integer} + executable: function(string): integer + exists: function(string): integer + expand: function(string): string + foldclosed: function(integer): integer + foldclosedend: function(integer): integer + getcwd: function(): string + input: function(string, string): string + + ['repeat#set']: function(string, integer) + + record QFItem + bufnr: integer + filename: string + lnum: integer + nr: integer + text: string + type: string + end + + record QFWhat + context: any + efm: string + id: integer + idx: integer + items: {QFItem} + lines: {string} + nr: integer + quickfixtextfunc: string + title: string + end + + setqflist: function(list: {QFItem}, action: string, what: QFWhat) + setloclist: function(nr: integer, list: {QFItem}, action: string, what: QFWhat) + + sign_unplace: function(string, {string:any}) + sign_place: function(number, string, string, string | number, {string:any}) + + record SignPlaceItem + buffer: integer + group: string + id: integer + lnum: integer + name: string + priority: integer + end + sign_placelist: function({SignPlaceItem}) + sign_getdefined: function(string): table + + record SignPlacedInfo + bufnr: integer + record SignPlacedSigns + id: integer + name: string + group: string + lnum: integer + priority: integer + end + signs: {SignPlacedSigns} + end + sign_getplaced: function(integer, table): {SignPlacedInfo} + + sign_define: function(string, table): number + sign_undefine: function(string): number + strdisplaywidth: function(string, integer): integer + stridx: function(haystack: string, needle: string, start: integer): integer + string: function(any): string + systemlist: function({string}): {string} + tempname: function(): string + type: function(any): integer + end + + call: function(string, ...:any) + cmd: function(string): any + + deepcopy: function(T): T + + defer_fn: function(function, integer): loop.Timer + + type DiffResult = {integer, integer, integer, integer} + + -- Assume result_type == 'indices' + diff: function(string|{string}, string|{string}, table): {DiffResult} + + record go + operatorfunc: string + end + + record o + diffopt: string + eventignore: string + shortmess: string + splitright: boolean + updatetime: number + wrapscan: boolean + end + + record WinOption + {WinOption} + diff: boolean + signcolumn: string + end + + wo: WinOption + + record BufOption + {BufOption} + fileformat: string + fileencoding: string + filetype: string + modifiable: boolean + modified: boolean + tabstop: integer + + enum BufHidden + '' 'hide' 'unload' 'delete' 'wipe' + end + + bufhidden: BufHidden + + enum BufType + '' 'acwrite' 'help' 'nofile' 'nowrite' 'quickfix' 'terminal' 'prompt' + end + buftype: BufType + end + + bo: BufOption + + record BufVar + {BufVar} + changedtick: integer + + gitsigns_head: string + gitsigns_status_dict: {string:any} + gitsigns_status: string + gitsigns_blame_line_dict: {string:any} + gitsigns_blame_line: string + end + + b: BufVar + + record WinVar + {WinVar} + + gitsigns_preview: string|boolean + end + + w: WinVar + + record g + gitsigns_head: string + end + + record v + vim_did_enter: integer + t_string: integer + end + + record opt + record Opt + get: function(Opt): T + end + + diffopt: Opt<{string}> + foldopen: Opt<{string}> + shortmess: Opt<{string:boolean}> + wrapscan: Opt + end + + record lsp + record util + close_preview_autocmd: function ({string}, number) + end + end + + record loop + cwd: function(): string + record Timer + userdata + + start: function(Timer, number, number, function): number + stop: function(Timer): number + close: function(Timer): number + is_closing: function(Timer): boolean + again: function(Timer): number + set_repeat: function(Timer, number): number + get_repeat: function(Timer): number + get_due_in: function(Timer): number + end + hrtime: function(): number + new_timer: function(): Timer + timer_start: function(Timer, integer, integer, function()): integer | string + + new_fs_event: function() + + record FSPollObj + userdata + is_closing: function(FSPollObj): boolean | string + close: function(FSPollObj) + start: function(FSPollObj, string, integer, function) + stop: function(FSPollObj) + getpath: function(FSPollObj): string + end + new_fs_poll: function(): FSPollObj + + record FsStatRet + dev : number + mode : number + nlink : number + uid : number + gid : number + rdev : number + ino : number + size : number + blksize : number + blocks : number + flags : number + gen : number + record atime + sec : number + nsec : number + end + record mtime + sec : number + nsec : number + end + record ctime + sec : number + nsec : number + end + record birthtime + sec : number + nsec : number + end + type : string + end + + fs_stat: function(string, function): FsStatRet + + fs_realpath: function(string): string + + new_tcp: function() + + sleep: function(integer) + + record Handle + userdata + + close: function(Handle) + is_closing: function(Handle): boolean | string + end + + record Pipe + userdata + + close: function(Pipe) + is_closing: function(Pipe): boolean | string + read_start: function(Pipe, err: any, data: string) + read_stop: function(Pipe) + write: function(Pipe, string, function()) + + open: function(any) + end + + record Process + userdata + + close: function(Process) + end + + record SpawnOpts + stdio: {Pipe, Pipe, Pipe} + args: {string} + cwd: string + env: {string} + end + + spawn: function(string, SpawnOpts, function(integer, integer)): Process, integer + + read_start: function(Pipe, function) + new_pipe: function(boolean): Pipe + shutdown: function(any, function) + close: function(any, function) + + record WorkCtx + queue: function(WorkCtx, ...:any) + end + + new_work: function(function, function): WorkCtx + end + + in_fast_event: function(): boolean + + list_extend: function({T}, {T}, integer, integer): {T} + list_slice: function({T}, integer, integer): {T} + + record keymap + record Options + buffer: boolean|integer + expr: boolean + end + set: function(string|{string}, string, string|function, Options) + end + + record log + record levels + WARN: integer + ERROR: integer + INFO: integer + DEBUG: integer + end + end + notify: function(string, integer, table) + pretty_print: function(any) + + split: function(string, string): {string} + split: function(string, string, boolean): {string} + + gsplit: function(string, string, boolean): function(): string + + pesc: function(string): string + + startswith: function(string, string): boolean + endswith: function(string, string): boolean + + schedule_wrap: function(function()): function() + schedule_wrap: function(function(...:any): any...): function(...:any): any... + + schedule: function(function) + validate: function({string:{any}}) + trim: function(string): string + + enum ExtendBehavior + 'error' + 'keep' + 'force' + end + + tbl_add_reverse_lookup: function({K:I}): {I:K} + tbl_contains: function(table, any): boolean + tbl_count: function(table): integer + tbl_deep_extend: function(ExtendBehavior, table, table, ...: table): table + tbl_extend: function(ExtendBehavior, table, table, ...: table): table + tbl_filter: function((function(any): boolean), {T}): {T} + tbl_isempty: function(table): boolean + tbl_islist: function(table): boolean + tbl_keys: function(table): table + tbl_map: function(function, table): table + + record InspectOptions + depth: number + newline: string + indent: string + process: function + end + record inspect + METATABLE: any + KEY: any + metamethod __call: function(inspect, any, InspectOptions): string + metamethod __call: function(inspect, any): string + end + + wait: function(number, function, number, boolean) + + record ui + input: function({string:any}, function(string)) + record SelectOpts + prompt: string + format_item: function(T): string + kind: string + end + select: function({T}, SelectOpts, on_choice: function(T, idx: integer)) + end + + record VersionDetails + api_compatible: integer + api_level: integer + api_prerelease: boolean + major: integer + minor: integer + patch: integer + end + + version: function(): VersionDetails + + record mpack + encode: function(any): string + decode: function(string): any + end + + is_thread: function(): boolean +end diff --git a/etc/soft/nvim/+plugins/hop.nvim/CONTRIBUTING.md b/etc/soft/nvim/+plugins/hop.nvim/CONTRIBUTING.md new file mode 100644 index 0000000..c32e598 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/CONTRIBUTING.md @@ -0,0 +1,192 @@ +# Contributing + +This document is the official contribution guide contributors must follow. It will be **greatly appreciated** if you +read it first before contributing. It will also prevent you from losing your time if you open an issue / make a PR that +doesn’t comply to this document. + + + +* [Disclaimer and why this document](#disclaimer-and-why-this-document) +* [How to make a change](#how-to-make-a-change) + * [Process](#process) +* [Conventions](#conventions) + * [Coding](#coding) + * [Git](#git) + * [Git message](#git-message) + * [Commit atomicity](#commit-atomicity) + * [Hygiene](#hygiene) +* [Release process](#release-process) + * [Overall process](#overall-process) + * [Changelogs update](#changelogs-update) + * [Git tag](#git-tag) +* [Support and donation](#support-and-donation) + + + +# Disclaimer and why this document + +People contributing is awesome. The more people contribute to Free & Open-Source software, the better the +world is to me. However, the more people contribute, the more work we have to do on our spare-time. Good +contributions are highly appreciated, especially if they thoroughly follow the conventions and guidelines of +each and every repository. However, bad contributions — that don’t follow this document, for instance — are +going to require me more work than was involved into making the actual change. It’s even worse when the contribution +actually solves a bug or add a new feature. + +So please read this document; it’s not hard and the few rules here are easy to respect. You might already do +everything in this list anyway, but reading it won’t hurt you. For more junior / less-experienced developers, it’s +very likely you will learn a bit of process that is considered good practice, especially when working with VCS like +Git. + +> Thank you! + +# How to make a change + +## Process + +The typical process is to base your work on the `master` branch. The `master` branch must always contain a stable +version of the project. It is possible to make changes by basing your work on other branches but the source +of truth is `master`. If you want to synchronize with other people on other branches, feel free to. + +The process is: + +1. (optional) Open an issue and discuss what you want to do. This is optional but highly recommended. If you + don’t open an issue first and work on something that is not in the scope of the project, or already being + made by someone else, you’ll be working for nothing. Also, keep in mind that if your change doesn’t refer to an + existing issue, I will be wondering what is the context of your change. So prepare to be asked about the motivation + and need behind your changes — it’s greatly appreciated if the commit messages, code and PR’s content already + contains this information so that people don’t have to ask. +2. Fork the project. +3. Create a branch starting from `master` – or the branch you need to work on. Even though this is not really enforced, + you’re advised to name your branch according to the _Git Flow_ naming convention: + - `fix/your-bug-here`: if you’re fixing a bug, name your branch. + - `feature/new-feature-here`: if you’re adding some work. + - Free for anything else. + - The special `release/*` branch is used to either back-port changes from newer versions to previous + versions, or to release new versions by updating files, changelogs, etc. Normally, contributors should + never have to worry about this kind of brach as their creations is often triggered when wanting to make a release. +4. Make some commits! +5. Once you’re ready, open a Pull Request (PR) to merge your work on the target branch. For instance, open a PR for + `master <- feature/something-new`. +6. (optional) Ask someone to review your code in the UI. Normally, I’m pretty reactive to notifications but it never + hurts to ask for a review. +7. Discussion and peer-review. +8. Once the CI is all green, someone (likely me [@phaazon]) will merge your code and close your PR. +9. Feel free to delete your branch. + +# Conventions + +## Coding + +N/A + +## Git + +### Git message + +Please format your git messages like so: + +> Starting with an uppercase letter, ending with a dot. #343 +> +> The #343 after the dot is appreciated to link to issues. Feel free to add, like this message, more context +> and/or precision to your git message. You don’t have to put it in the first line of the commit message, +> but if you are fixing a bug or implementing a feature thas has an issue linked, please reference it, so +> that it is easier to generate changelogs when reading the git log. + +**I’m very strict on git messages as I use them to write `CHANGELOG.md` files. Don’t be surprised if I ask you +to edit a commit message. :)** + +### Commit atomicity + +Your commits should be as atomic as possible. That means that if you make a change that touches two different +concepts / has different scopes, most of the time, you want two commits – for instance one commit for the backend code +and one commit for the interface code. There are exceptions, so this is not an absolute rule, but take some time +thinking about whether you should split your commits or not. Commits which add a feature / fix a bug _and_ add tests at +the same time are fine. + +However, here’s a non-comprehensive list of commits considered bad and that will be refused: + +- **Formatting, refactoring, cleaning, linting code in a PR that is not strictly about formatting**. If you open a PR to + fix a bug, implement a feature, change configuration, add metadata to the CI, etc. — pretty much anything — but you + also format some old code that has nothing to do with your PR, apply a linter’s suggestions (such as `clippy`), remove + old code, etc., then I will refuse your commit(s) and ask you to edit your PR. +- **Too atomic commits**. If two commits are logically connected to one another and are small, it’s likely that you want + to merge them as a single commit — unless they work on too different parts of your code. This is a bit subjective + topic, so I won’t be _too picky_ about it, but if I judge that you should split a commit into two or fixup two commits, + please don’t take it too personal. :) + +If you don’t know how to write your commits in an atomic maneer, think about how one would revert your commits if +something bad happens with your changes — like a big breaking change we need to roll back from very quickly. If your +commits are not atomic enough, rolling them back will also roll back code that has nothing to do with your changes. + +### Hygiene + +When working on a fix or a feature, it’s very likely that you will periodically need to update your branch +with the `master` branch. **Do not use merge commits**, as your contributions will be refused if you have +merge commits in them. The only case where merge commits are accepted is when you work with someone else +and are required to merge another branch into your feature branch (and even then, it is even advised to +simply rebase). If you want to synchronize your branch with `master`, please use: + +``` +git switch +git fetch origin --prune +git rebase origin/master +``` + +# Release process + +## Overall process + +Releases occur at arbitrary rates. If something is considered urgent, it is most of the time released immediately +after being merged and tested. Sometimes, several issues are being fixed at the same time (spanning on a few +days at max). Those will be gathered inside a single update. + +Feature requests might be delayed a bit to be packed together as well but eventually get released, even if +they’re small. Getting your PR merged means it will be released _soon_, but depending on the urgency of your changes, +it might take a few minutes to a few days. + +## Changelogs update + +`CHANGELOG.md` files must be updated **before any release**. Especially, they must contain: + +- The version of the release. +- The date of the release. +- How to migrate from a minor to the next major. +- Everything that a release has introduced, such as major, minor and patch changes. + +Because I don’t ask people to maintain changelogs, I have a high esteem of people knowing how to use Git and create +correct commits. Be advised that I will refuse any commit that prevents me from writing the changelog correctly. + +## Git tag + +Once a new release occurs, a Git tag is created. Git tags are formatted regarding the project they refer to, if several +projects are present in the repository. If only one project is present, tags will refer to this project by the same +naming scheme anyway: + +> -X.Y.Z + +Where `X` is the _major version_, `Y` is the _minor version_ and `Z` is the _patch version_. For instance +`project-0.37.1` is a valid Git tag, so is `project-derive-0.5.3`. + +A special kind of tag is also possible: + +> -X.Y.Z-rc.W + +Where `W` is a number starting from `1` and incrementing. This format is for _release candidates_ and occurs +when a new version (most of the time a major one) is to be released but more feedback is required. + +# Support and donation + +This project is a _free and open-source_ project. It has no financial motivation nor support. I +([@phaazon]) would like to make it very clear that: + +- Sponsorship is not available. You cannot pay me to make me do things for you. That includes issues reports, + features requests and such. +- If you still want to donate because you like the project and think I should be rewarded, you are free to + give whatever you want. +- However, keep in mind that donating doesn’t unlock any privilege people who don’t donate wouldn’t already + have. This is very important as it would bias priorities. Donations must remain anonymous. +- For this reason, no _sponsor badge_ will be shown, as it would distinguish people who donate from those + who don’t. This is a _free and open-source_ project, everybody is welcome to contribute, with or without + money. + +[@phaazon]: https://github.com/phaazon diff --git a/etc/soft/nvim/+plugins/hop.nvim/LICENSE b/etc/soft/nvim/+plugins/hop.nvim/LICENSE new file mode 100644 index 0000000..fbb6f8e --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2021-2022, Dimitri Sabadie + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Dimitri Sabadie nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/etc/soft/nvim/+plugins/hop.nvim/README.md b/etc/soft/nvim/+plugins/hop.nvim/README.md new file mode 100644 index 0000000..1d6a082 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/README.md @@ -0,0 +1,172 @@ + __ + / /_ ____ ____ + / __ \/ __ \/ __ \ + / / / / /_/ / /_/ / + /_/ /_/\____/ .___/ + /_/ + · Neovim motions on speed! · + +

+ + + + + +

+ +

+ Install · Wiki · Screenshots · Discuss +

+ +**Hop** is an [EasyMotion]-like plugin allowing you to jump anywhere in a +document with as few keystrokes as possible. It does so by annotating text in +your buffer with hints, short string sequences for which each character +represents a key to type to jump to the annotated text. Most of the time, +those sequences’ lengths will be between 1 to 3 characters, making every jump +target in your document reachable in a few keystrokes. + +

+ +

+ + + +* [Motivation](#motivation) +* [Features](#features) +* [Getting started](#getting-started) + * [Installation](#installation) + * [Important note about versioning](#important-note-about-versioning) + * [Using vim-plug](#using-vim-plug) + * [Using packer](#using-packer) + * [Nightly users](#nightly-users) +* [Usage](#usage) +* [Keybindings](#keybindings) +* [Chat](#chat) + + + +# Motivation + +**Hop** is a complete from-scratch rewrite of [EasyMotion], a famous plugin to +enhance the native motions of Vim. Even though [EasyMotion] is usable in +Neovim, it suffers from a few drawbacks making it not comfortable to use with +Neovim version >0.5 – at least at the time of writing these lines: + +- [EasyMotion] uses an old trick to annotate jump targets by saving the + contents of the buffer, replacing it with the highlighted annotations and + then restoring the initial buffer after jump. This trick is dangerous as it + will change the contents of your buffer. A UI plugin should never do anything + to existing buffers’ contents. +- Because the contents of buffers will temporarily change, other parts of the + editor and/or plugins relying on buffer change events will react and will go + mad. An example is the internal LSP client implementation of Neovim >0.5 or + its treesitter native implementation. For LSP, it means that the connected + LSP server will receive a buffer with the jump target annotations… not + ideal. + +**Hop** is a modern take implementing this concept for the latest versions of +Neovim. + +# Features + +- Go to any word in the current buffer (`:HopWord`). +- Go to any character in the current buffer (`:HopChar1`). +- Go to any bigrams in the current buffer (`:HopChar2`). +- Make an arbitrary search akin to / and go to any occurrences (`:HopPattern`). +- Go to any line and any line start (`:HopLine`, `:HopLineStart`). +- Go to anywhere (`:HopAnywhere`). +- Use Hop cross windows with multi-windows support (`:Hop*MW`). +- Use it with commands like `v`, `d`, `c`, `y` to visually select/delete/change/yank up to your new cursor position. +- Support a wide variety of user configuration options, among the possibility to alter the behavior of commands + to hint only before or after the cursor (`:Hop*BC`, `:Hop*AC`), for the current line (`:Hop*CurrentLine`), + change the dictionary keys to use for the labels, jump on sole occurrence, etc. +- Extensible: provide your own jump targets and create Hop extensions! + +# Getting started + +This section will guide you through the list of steps you must take to be able to get started with **Hop**. + +This plugin was written against Neovim 0.5, which is currently a nightly version. This plugin will not work: + +- With a version of Neovim before 0.5. +- On Vim. **No support for Vim is planned.** + +## Installation + +Whatever solution / package manager you are using, you need to ensure that the `setup` Lua function is called at some +point, otherwise the plugin will not work. If your package manager doesn’t support automatic calling of this function, +you can call it manually after your plugin is installed: + +```lua +require'hop'.setup() +``` + +To get a default experience. Feel free to customize later the `setup` invocation (`:h hop.setup`). If you do, then you +will probably want to ensure the configuration is okay by running `:checkhealth`. Various checks will be performed by +Hop to ensure everything is all good. + +### Important note about versioning + +This plugin implements [SemVer] via git branches and tags. Versions are prefixed with a `v`, and only patch versions +are git tags. Major and minor versions are git branches. You are **very strongly advised** to use a major version +dependency to be sure your config will not break when Hop gets updated. + +### Using vim-plug + +```vim +Plug 'phaazon/hop.nvim' +``` + +### Using packer + +```lua +use { + 'phaazon/hop.nvim', + branch = 'v2', -- optional but strongly recommended + config = function() + -- you can configure Hop the way you like here; see :h hop-config + require'hop'.setup { keys = 'etovxqpdygfblzhckisuran' } + end +} +``` + +### Nightly users + +Hop supports nightly releases of Neovim. However, keep in mind that if you are on a nightly version, you must be **on +the last one**. If you are not, then you are exposed to compatibility issues / breakage. + +# Usage + +See the [wiki](https://github.com/phaazon/hop.nvim/wiki). + +# Keybindings + +Hop doesn’t set any keybindings; you will have to define them by yourself. + +If you want to create a key binding from within Lua: + +```lua +-- place this in one of your configuration file(s) +local hop = require('hop') +local directions = require('hop.hint').HintDirection +vim.keymap.set('', 'f', function() + hop.hint_char1({ direction = directions.AFTER_CURSOR, current_line_only = true }) +end, {remap=true}) +vim.keymap.set('', 'F', function() + hop.hint_char1({ direction = directions.BEFORE_CURSOR, current_line_only = true }) +end, {remap=true}) +vim.keymap.set('', 't', function() + hop.hint_char1({ direction = directions.AFTER_CURSOR, current_line_only = true, hint_offset = -1 }) +end, {remap=true}) +vim.keymap.set('', 'T', function() + hop.hint_char1({ direction = directions.BEFORE_CURSOR, current_line_only = true, hint_offset = 1 }) +end, {remap=true}) +``` + +# Chat + +Join the discussion on the official [Matrix room](https://matrix.to/#/#hop.nvim:matrix.org)! + +[EasyMotion]: https://github.com/easymotion/vim-easymotion +[packer]: https://github.com/wbthomason/packer.nvim +[SemVer]: https://semver.org diff --git a/etc/soft/nvim/+plugins/hop.nvim/doc/hop.txt b/etc/soft/nvim/+plugins/hop.nvim/doc/hop.txt new file mode 100644 index 0000000..e82cd3d --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/doc/hop.txt @@ -0,0 +1,905 @@ +*hop.txt* For Neovim version 0.5 Last change: 2022 Oct 09 + + __ + / /_ ____ ____ + / __ \/ __ \/ __ \ + / / / / /_/ / /_/ / + /_/ /_/\____/ .___/ + /_/ + · Neovim motions on speed! · + v2.0.2 + +============================================================================== +CONTENTS *hop-contents* + + Introduction ·············································· |hop-introduction| + Requirements ·············································· |hop-requirements| + Usage ···························································· |hop-usage| + Commands ···················································· |hop-commands| + Lua API ······················································ |hop-lua-api| + Jump target API ······································ |hop-jump-target-api| + Configuration ··················································· |hop-config| + Extension ···················································· |hop-extension| + Highlights ·················································· |hop-highlights| + License ························································ |hop-license| + +============================================================================== +INTRODUCTION *hop* *hop-introduction* + +Hop is an “EasyMotion” like plugin allowing you to jump anywhere in a document +with as few keystrokes as possible. It does so by annotating text in your +buffer with |hints|, short string sequences for which each character represents +a key to type to jump to the annotated text. Most of the time, those +sequences’ lengths will be between 1 to 3 characters, making every jump target +in your document reachable in a few keystrokes. + +Hop is a complete from-scratch rewrite of EasyMotion, a famous plugin to +enhance the native motions of Vim. Even though EasyMotion is usable in +Neovim, it suffers from a few drawbacks making it not comfortable to use with +Neovim version >0.5 – at least at the time of writing these lines: + +- EasyMotion uses an old trick to annotate jump targets by saving the + contents of the buffer, replacing it with the highlighted annotations and + then restoring the initial buffer after jump. This trick is dangerous as it + will change the contents of your buffer. A UI plugin should never do anything + to existing buffers’ contents. +- Because the contents of buffers will temporarily change, other parts of the + editor and/or plugins relying on buffer change events will react and will go + mad. An example is the internal LSP client implementation of Neovim >0.5 or + its treesitter native implementation. For LSP, it means that the connected + LSP server will receive a buffer with the jump target annotations… not + ideal. + +============================================================================== +REQUIREMENTS *hop-requirements* + +Hop works only with Neovim and was written with Neovim-0.5, so it is highly +recommended to use Neovim version 0.5+. + +Especially, hop uses |api-extended-marks|, which are not available before +Neovim-0.5. + +============================================================================== +USAGE *hop-usage* + +Before doing anything else, you have to setup the plugin. If you are not using +a package manager or environment doing that automatically for you, you need to +call the |hop.setup| function to correctly initialize the plugin. + +For a minimal setup: + + For people using init.lua~ + In your `init.lua`, add: +> + require'hop'.setup() +< + For people using init.vim~ + In your `init.vim`, add: +> + lua << EOF + require'hop'.setup() + EOF +< +You can pass an optional argument to `setup(opts)` in order to pass {opts}. +Have a look at |hop.setup| for further details. + + *hop-health* +Healthcheck~ + +Hop has support for |:checkhealth|. If you find yourself in a situation where +something looks odd or incorrect, do not forget to run this command before +opening an issue. + + *hop-commands* +Commands~ + +You can try those commands by typing them in your command line. By default, +they will use the default options for the configuration of Hop. If you want to +customize how those commands work, have a look at |hop.setup|. Also, something +pretty important to know is that those are Vim commands. Hop tries to expose +as many features as possible via the Vim commands but ultimately, you will +have access to more features by using the Lua API directly. Have a look at +|hop-lua-api| for more documentation. + +Some of the commands have a suffix, such as `BC`, `AC` and `MW`. Those are +variations of the commands without the suffix, applying to the visible part of +the buffer before and after the cursor, and multiple windows, respectively. +Another kind of suffix (that can be mixed with `BC` and `AC`) is `CurrentLine`. This +creates a variant of the command that will only run for the current line. + +`:HopWord` *:HopWord* +`:HopWordBC` *:HopWordBC* +`:HopWordAC` *:HopWordAC* +`:HopWordCurrentLine` *:HopWordCurrentLine* +`:HopWordCurrentLineBC` *:HopWordCurrentLineBC* +`:HopWordCurrentLineAC` *:HopWordCurrentLineAC* +`:HopWordMW` *:HopWordMW* + Annotate all |word|s with key sequences. Typing a first key will visually + filter the sequences and reduce them. Continue typing key sequences until + you reduce a sequence completely, which will bring your cursor at that + position. + + This is akin to calling the |hop.hint_words| Lua function. + +`:HopPattern` *:HopPattern* +`:HopPatternBC` *:HopPatternBC* +`:HopPatternAC` *:HopPatternAC* +`:HopPatternCurrentLine` *:HopPatternCurrentLine* +`:HopPatternCurrentLineBC` *:HopPatternCurrentLineBC* +`:HopPatternCurrentLineAC` *:HopPatternCurrentLineAC* +`:HopPatternMW` *:HopPatternMW* + Ask the user for a pattern and hint the document with it. + + This is akin to calling the |hop.hint_patterns| Lua function + with no explicit pattern. + +`:HopChar1` *:HopChar1* +`:HopChar1BC` *:HopChar1BC* +`:HopChar1AC` *:HopChar1AC* +`:HopChar1CurrentLine` *:HopChar1CurrentLine* +`:HopChar1CurrentLineBC` *:HopChar1CurrentLineBC* +`:HopChar1CurrentLineAC` *:HopChar1CurrentLineAC* +`:HopChar1MW` *:HopChar1MW* + Type a key and immediately hint the document for this key. + + This is akin to calling the |hop.hint_char1| Lua Function + +`:HopChar2` *:HopChar2* +`:HopChar2BC` *:HopChar2BC* +`:HopChar2AC` *:HopChar2AC* +`:HopChar2CurrentLine` *:HopChar2CurrentLine* +`:HopChar2CurrentLineBC` *:HopChar2CurrentLineBC* +`:HopChar2CurrentLineAC` *:HopChar2CurrentLineAC* +`:HopChar2MW` *:HopChar2MW* + Type two keys and immediately hint the document for this bigram. + + This is akin to calling the |hop.hint_char2| Lua Function + +`:HopLine` *:HopLine* +`:HopLineBC` *:HopLineBC* +`:HopLineAC` *:HopLineAC* +`:HopLineCurrentLine` *:HopLineCurrentLine* +`:HopLineCurrentLineBC` *:HopLineCurrentLineBC* +`:HopLineCurrentLineAC` *:HopLineCurrentLineAC* +`:HopLineMW` *:HopLineMW* + Jump to the beginning of the line of your choice inside your buffer. + + This is akin to calling the |hop.hint_lines| Lua function. + +`:HopLineStart` *:HopLineStart* +`:HopLineStartBC` *:HopLineStartBC* +`:HopLineStartAC` *:HopLineStartAC* +`:HopLineStartCurrentLine` *:HopLineStartCurrentLine* +`:HopLineStartCurrentLineBC` *:HopLineStartCurrentLineBC* +`:HopLineStartCurrentLineAC` *:HopLineStartCurrentLineAC* +`:HopLineStartMW` *:HopLineStartMW* + Like `HopLine` but skips leading whitespace on every line. Blank lines are + skipped over. + + This is akin to calling the |hop.hint_lines_skip_whitespace| Lua function. + +`:HopVertical` *:HopVertical* +`:HopVerticalBC` *:HopVerticalBC* +`:HopVerticalAC` *:HopVerticalAC* +`:HopVerticalMW` *:HopVerticalMW* + Like `HopLine` but keeps the column position of the cursor. + + This is akin to calling the |hop.hint_vertical| Lua function. + +`:HopAnywhere` *:HopAnywhere* +`:HopAnywhereBC` *:HopAnywhereBC* +`:HopAnywhereAC` *:HopAnywhereAC* +`:HopAnywhereCurrentLine` *:HopAnywhereCurrentLine* +`:HopAnywhereCurrentLineBC` *:HopAnywhereCurrentLineBC* +`:HopAnywhereCurrentLineAC` *:HopAnywhereCurrentLineAC* +`:HopAnywhereMW` *:HopAnywhereMW* + Annotate anywhere with key sequences. + + This is akin to calling the |hop.hint_anywhere| Lua function. + + *hop-lua-api* +Lua API~ + +The Lua API comprises several modules. Even though those modules might have +more public functions than described here, you are only supposed to use the +functions in this help page. Using one that is not listed here is considered +unstable. + +`hop` Entry point and main interface. If you just want to use + Hop from within Lua via keybindings, you shouldn’t need to + read any other modules. +`hop.defaults` Default options. +`hop.hint` Various functions to create, update and reduce hints. +`hop.highlight` Highlight functions (creation / autocommands / etc.). +`hop.jump_target` Core module used to create jump targets. +`hop.perm` Permutation functions. Permutations are used as labels for + the hints. + +Main API~ + +Most of the functions and values you need to know about are in `hop`. + +`hop.setup(`{opts}`)` *hop.setup* + Setup the library with options. + + This function will setup the Lua API and commands in a way that respects + the options you pass. It is mandatory to call that function at some time + if you want to be able to use Hop. + + Note: + Some plugins will automatically call the `setup` public function of a + plugin if declared, which is the case with Hop. With such plugins, you + shouldn’t have to care too much about `setup` but focus more on the + {opts} you can pass it. + + Arguments:~ + {opts} List of options. See the |hop-config| section. + +`hop.hint_with(`{jump_target_gtr}`,` {opts}`)` *hop.hint_with* + Main entry-point of Hop, this function expects a jump target generator and + will call it with {opts} to get a list of jump targets. This function will + take care of reducing the hints for you automatically and will perform the + actual jumps. + + If you would like to use a more general version to implement different + kind of actions instead of jumping, have a look at + |hop.hint_with_callback|. + + Arguments:~ + {jump_target_gtr} Jump target generator. See |hop-jump-target-api| for + further information. + {opts} User options. + +`hop.hint_with_callback(` *hop.hint_with_callback* + {jump_target_gtr}`,` + {opts}`,` + {callback} +`)` + Main entry-point of Hop, this function expects a jump target generator and + will call it with {opts} to get a list of jump targets. This function will + take care of reducing the hints for you automatically. Once a jump target + is reduced completely, this function will call the {callback} with the + selected jump target. + + Arguments:~ + {jump_target_gtr} Jump target generator. See |hop-jump-target-api| for + further information. + {opts} User options. + + +`hop.hint_words(`{opts}`)` *hop.hint_words* + Annotate all words in the current window with key sequences. Typing a + first key will visually filter the sequences and reduce them. Continue + typing key sequences until you reduce a sequence completely, which will + bring your cursor at that position. See |hop-config| for a complete list + of the options you can pass as arguments. + + Arguments:~ + {opts} Hop options. + +`hop.hint_patterns(`{opts}`,` {pattern}`)` *hop.hint_patterns* + Annotate all matched patterns in the current window with key sequences. + + Arguments:~ + {opts} Hop options. + {pattern} (optional) The pattern to search for. + If not set, the user is prompted for the pattern to search. + +`hop.hint_char1(`{opts}`)` *hop.hint_char1* + Let the user type a key and immediately hint all of its occurrences. + + Arguments:~ + {opts} Hop options. + +`hop.hint_char2(`{opts}`)` *hop.hint_char2* + Let the user type a bigram (two concatenated keys) and immediately hint + all of its occurrences. + + This function can behave like |hop.hint_char1| in some cases. See + |hop-config-char2_fallback_key|. + + Arguments:~ + {opts} Hop options. + +`hop.hint_lines(`{opts}`)` *hop.hint_lines* + Hint the beginning of each lines currently visible in the buffer view and + allow to jump to them. + + This works with empty lines as well. + + Arguments:~ + {opts} Hop options. + +`hop.hint_lines_skip_whitespace(`{opts}`)` *hop.hint_lines_skip_whitespace* + Hint the first non-whitespace character of each lines currently visible in + the buffer view and allow to jump to them. + + This works with empty lines as well. + + Arguments:~ + {opts} Hop options. + +`hop.hint_vertical(`{opts}`)` *hop.hint_vertical* + Hint the cursor position of each lines currently visible in the buffer + view and allow to jump to them. If the line is shorter than the current + cursor column position, it will default to the end of the line. + + This works with empty lines as well. + + Arguments:~ + {opts} Hop options. + +`hop.hint_anywhere(`{opts}`)` *hop.hint_anywhere* + Annotate anywhere in the current window with key sequences. + + Arguments:~ + {opts} Hop options. + +Hint API~ + +The hint API provide the `HintDirection` and `HintPosition` + +`hop.hint.HintDirection` *hop.hint.HintDirection* + Enumeration for hinting direction. + + Use this table as a value for |hop-config-direction|. Setting it to {nil} + makes the command / function act on the whole visible part of the buffer. + + Enumeration variants:~ + {BEFORE_CURSOR} Create and apply hints before the cursor. + {AFTER_CURSOR} Create and apply hints after the cursor. + +`hop.hint.HintPosition` *hop.hint.HintPosition* + Enumeration for hinting position in match. + + Use this table as a value for |hop-config-hint_posititon|. + + Enumeration variants:~ + {BEGIN} Create and apply hints at the beginning of the match. + {MIDDLE} Create and apply hints at the middle of the match. + {END} Create and apply hints at the end of the match. + + *hop-jump-target-api* +Jump target API~ + +The jump target API is probably the most core API of all. It provides some +helper functions to help you extend Hop by providing your own jump targets. +Most importantly, you will want to read this documentation section to know +exactly which kind of format Hop expects for the jump targets when +implementing your own. + +Jump targets are locations in buffers where users might jump to. They are +wrapped in a table and provide the required information so that Hop can +associate labels and display the hints. Such a table must have the following +form: +> + { + jump_targets = {}, + indirect_jump_targets = {}, + } +> +The `jump_targets` field is a list-table of jump targets. A single jump target +is simply a location in a given window. Actually, the location will be set on +the buffer that is being displayed by that window. So you can picture a jump +target as a triple (line, column, window). +> + { + line = 0, + column = 0, + window = 0, + } +< +`indirect_jump_targets` is an optional yet highly recommended table. They +provide an indirect access to `jump_targets` to re-order them. They are for +instance used to be able to distribute hints according to their distance to +the current location of the cursor. Not providing that table (`nil`) will +result in the jump targets being considered fully ordered and will be assigned +sequences based on their index in `jump_targets`. + +Indirect jump targets are encoded as a flat list-table of pairs +(index, score). This table allows to quickly score and sort jump targets. +> + { + index = 0, + score = 0, + } +< +The `index` field gives the index in the `jump_targets` list. The `score` is any +number. The rule is that the lower the score is, the less prioritized the +jump target will be. + +So for instance, for two jump targets, a jump target generator must return +such a table: +> + { + jump_targets = { + { line = 1, column = 14, window = 0 }, + { line = 2, column = 1, window = 0 }, + }, + + indirect_jump_targets = { + { index = 0, score = 14 }, + { index = 1, score = 7 }, + }, + } +< + +If you don’t need to change the score, or if the scores are always ascending +with the indices ascending, you can completely omit the +`indirect_jump_targets` table: +> + { + jump_targets = { + { line = 1, column = 14, window = 0 }, + { line = 2, column = 1, window = 0 }, + }, + } +< +This module provides several functions, named `jump_targets_*`, which are +called jump target generators. Such functions are to be passed to +|hop.hint_with| or |hop.hint_with_callback|. Most of the Vim commands are already +doing that for you. + +`hop.jump_target.jump_targets_by_scanning_line(` *hop.jump_target.jump_targets_by_scanning_line* + {regex} +`)` + Jump target generator that scans lines of the currently visible buffer and + create the jump targets by using the input {regex}. + + Arguments:~ + {regex} Regex-like table to apply to each line. See the various + `hop.jump_target.regex*` functions below for a list of available + options. + + If you want to create your own regex-like table, you need to + provide a table with two fields: `oneshot`, a boolean value, + that is to be set to `true` if the regex should be applied + only once to each line, and `match`, which is the actual + matcher that returns the beginning / end + (inclusive / exclusive) byte indices of the match. You are + advised to use |vim.regex|. + +`hop.jump_target.jump_targets_for_current_line(` *hop.jump_target.jump_targets_for_current_line* + {regex} +`)` + Jump target generator that applies {regex} only to the current line of the + currently active window. + + Arguments:~ + {regex} Regex-like table to apply to each line. See the various + `hop.jump_target.regex*` functions below for a list of available + options. + + If you want to create your own regex-like table, you need to + provide a table with two fields: `oneshot`, a boolean value, + that is to be set to `true` if the regex should be applied to + each line only once, and `match`, which is the actual matcher + that returns the beginning / end (inclusive / exclusive) byte + indices of the match. You are advised to use |vim.regex|. + + +`hop.jump_target.sort_indirect_jump_targets(` *hop.jump_target.sort_indirect_jump_targets* + {indirect_jump_targets}`,` + {opts} +`)` + Sort {indirect_jump_targets} according to their scores. + + Arguments:~ + {indirect_jump_targets} Indirect jump targets to sort. + {opts} User options. + +`hop.jump_target.regex_by_searching(` *hop.jump_target.regex_by_searching* + {pat}`,` + {plain_search} +`)` + Buffer-line based |pattern| hint mode. This mode will highlight the + beginnings of a pattern you will be prompted for in the document and + will make the cursor jump to the one fully reduced. + + Arguments:~ + {pat} Pattern to search. + {plain_search} Should the pattern by plain-text. + +`hop.jump_target.regex_by_case_searching(` *hop.jump_target.regex_by_case_searching* + {pat}`,` + {plain_search}`,` + {opts} +`)` + Similar to |hop.jump_target.regex_by_searching|, but respects the user case + sensitivity set by 'smartcase' and |hop-config-case_insensitive|. + + Arguments:~ + {pat} Pattern to search. + {plain_search} Should the pattern by plain-text. + {opts} User options. + +`hop.jump_target.regex_by_word_start()` *hop.jump_target.regex_by_word_start* + Buffer-line based |word| hint mode. This mode will highlight the beginnings + of all the words in the window and will make the cursor jump to the one + fully reduced. + +`hop.jump_target.by_line_start()` *hop.jump_target.by_line_start* + Highlight the beginning of each line in the current window. + +`hop.jump_target.regex_by_line_start_skip_whitespace()` *hop.jump_target.regex_by_line_start_skip_whitespace* + Highlight the non-whitespace character of each line in the current window. + +`hop.jump_target.regex_by_anywhere()` *hop.jump_target.regex_by_anywhere* + Highlight the anywhere in the current window. + +`hop.jump_target.manh_dist(` *hop.jump_target.manh_dist* + {a}`,` + {b}`,` + {x_bias} +`)` + Manhattan distance between two buffer positions. Both {a} and {b} must be + tables containing two values: the first one for the line and the second one + for the column. + + {x_bias} is to be used to skew the Manhattan distance in terms of lines. + + Arguments:~ + {a} First position. + {b} Second position. + {x_bias} Optional bias applied to the line distance. Defaults to 10. + +Highlight API~ + +The highlight API gives two functions to manipulate the highlights Hop uses: + +`hop.highlight.insert_highlights()` *hop.highlight.insert_highlights* + Manually insert the highlights by calling the `highlight default` command. + See |hop-highlights| for further details about the list of highlights + currently available. + +`hop.highlight.create_autocmd` *hop.highlight.create_autocmd* + Register autocommands for the `ColorScheme` event, calling + |hop.highlight.insert_highlights|. This is called when you require Hop and + you shouldn’t need to call this function by yourself. + +Permutation API~ + +The permutation API is the core part of the algorithm used in Hop. +Permutations in Hop are made out of a source sequence of characters, called +a key set and represented with {keys}. A good choice of key set is important +to yield short and concise permutations, allowing to jump to targets with +fewer keystrokes. + +`hop.perm.permutations(`{keys}`,` {n}`,` {opts}`)` *hop.perm.permutations* + Get the first {n} permutations out of {keys}. + + Arguments:~ + {keys} Input key set to use to create permutations. + {n} Number of permutations to generate. + {opts} Hop options. + + Return:~ + The {n} first permutations for the given {keys} and + |hop-config-perm_method| set in {opts}. + +============================================================================== +CONFIGURATION *hop-config* + +The configuration can be provided in two different ways: + +- Either by explicitly passing an {opts} table to the various Lua + functions. +- Or by passing an {opts} table to the Lua |hop.setup| function. + +Not providing any of the two above will use the default values, as described +below. + +`keys` *hop-config-keys* + A string representing all the keys that can be part of a permutation. + Every character (key) used in the string will be used as part of a + permutation. The shortest permutation is a permutation of a single + character, and, depending on the content of your buffer, you might end up + with 3-character (or more) permutations in worst situations. + + However, it is important to notice that if you decide to provide `keys`, + you have to ensure to use enough characters in the string, otherwise you + might get very long sequences and a not so pleasant experience. + + Note: + This is only for the old permutation algorithm only, which is to get + deprecated soon. + + Another important aspect is that the order of the characters you put + in your `keys` is important. Depending on the value you use in + |hop-config-term_seq_bias|, the keys will be split in two and the first + part of it will be used for terminal keys and the second part for + sequence keys. + + Defaults:~ + `keys = 'asdghklqwertyuiopzxcvbnmfj'` + +`quit_key` *hop-config-quit_key* + A string representing a key that will quit Hop mode without also feeding + that key into Neovim to be treated as a normal key press. + + It is possible to quit hopping by pressing any key that is not present in + |hop-config-keys|; however, when you do this, the key normal function is + also performed. For example if, hopping in |visual-mode|, pressing + will quit hopping and also exit |visual-mode|. + + If the user presses `quit_key`, Hop will be quit without the key normal + function being performed. For example if hopping in |visual-mode| with + `quit_key` set to '', pressing will quit hopping without + quitting |visual-mode|. + + If you don't want to use a `quit_key`, set `quit_key` to an empty string. + + Note:~ + `quit_key` should only contain a single key or be an empty string. + + Note:~ + `quit_key` should not contain a key that is also present in + |hop-config-keys|. + + Defaults:~ + `quit_key = ''` + +`perm_method` *hop-config-perm_method* + Permutation method to use. + + Permutation methods allow to change the way permutations (i.e. hints + sequence labels) are generated internally. There are currently two + possible options: + + Possible algorithms~ + `TermSeqBias` + This algorithm splits your input key set (|hop-config-keys|) into + two separate sets, a terminal key set and a sequence key set. + Terminal keys will always be at the end of a sequence and then + typing them will always jump somewhere in your buffer, while + sequence keys will always be part of a prefix sequence before a + terminal key. + + Additionally to |hop-config-keys|, this algorithm uses the + |hop-config-term_seq_bias| option to determine how to split + |hop-config-keys|. A good default value is `0.5` or `3 / 4` but + feel free to experiment with values between (exclusive) `0` and + `1`. + + Note: + This algorithm will be deprecated very soon. You are strongly + advised to switch to `TrieBacktrackFilling` if you haven’t + yet. + + `TrieBacktrackFilling` + Permutation algorithm based on tries and backtrack filling. + + This algorithm uses the full potential of |hop-config-keys| by + using them all to saturate a trie, representing all the + permutations. Once a layer is saturated, this algorithm will + backtrack (from the end of the trie, deepest first) and create a + new layer in the trie, ensuring that the first permutations will + be shorter than the last ones. + + Because of the last, deepest trie insertion mechanism and trie + saturation, this algorithm yields a much better distribution + accross your buffer, and you should get 1-sequences and + 2-sequences most of the time. Each dimension grows + exponentially, so you get `keys_length²` 2-sequence keys, + `keys_length³` 3-sequence keys, etc in the worst cases. + + Default value~ + `perm_method = require'hop.perm'.TrieBacktrackFilling` + +`reverse_distribution` *hop-config-reverse_distribution* + The default behavior for key sequence distribution in your buffer is to + concentrate shorter sequences near the cursor, grouping 1-character + sequences around. As hints get further from the cursor, the dimension of + the sequences will grow, making the furthest sequences the longest ones + to type. + + Set this option to `true` to reverse the density and concentrate the + shortest sequences (1-character) around the furthest words and the longest + sequences around the cursor. + + Defaults:~ + `reverse_distribution = false` + +`teasing` *hop-config-teasing* + Boolean value stating whether Hop should tease you when you do something + you are not supposed to. + + If you find this setting annoying, feel free to turn it to `false`. + + Defaults:~ + `teasing = true` + +`jump_on_sole_occurrence` *hop-config-jump_on_sole_occurrence* + Immediately jump without displaying hints if only one occurrence exists. + + Defaults:~ + `jump_on_sole_occurrence = true` + +`case_insensitive` *hop-config-case_insensitive* + Use case-insensitive matching by default for commands requiring user + input. + + Defaults:~ + `case_insensitive = true` + +`create_hl_autocmd` *hop-config-create_hl_autocmd* + Create and set highlight autocommands to automatically apply highlights. + You will want this if you use a theme that clears all highlights before + applying its colorscheme. + + Defaults:~ + `create_hl_autocmd = true` + +`direction` *hop-config-direction* + Direction in which to hint. See |hop.hint.HintDirection| for further + details. + + Setting this in the user configuration will make all commands default to + that direction, unless overriden. + + Defaults:~ + `direction = nil` + +`hint_position` *hop-config-hint_position* + Position of hint in match. See |hop.hint.HintPosition| for further + details. + + Defaults:~ + `hint_position = require'hop.hint'.HintPosition.BEGIN` + +`hint_offset` *hop-config-hint_offset* + Offset to apply to a jump location. + + If it is non-zero, the jump target will be offset horizontally from the + selected jump position by `hint_offset` character(s). + + This option can be used for emulating the motion commands |t| and |T| where + the cursor is positioned on/before the target position. + + Defaults:~ + `hint_offset = 0` + +`current_line_only` *hop-config-current_line_only* + Apply Hop commands only to the current line. + + Note: + Trying to use this option along with |hop-config-multi_windows| is + unsound. + + Defaults:~ + `current_line_only = false` + +`uppercase_labels` *hop-config-uppercase_labels* + Display labels as uppercase. This option only affects the displayed + labels; you still select them by typing the keys on your keyboard. + + Defaults:~ + `uppercase_labels = false` + +`char2_fallback_key` *hop-config-char2_fallback_key* + Enable a special key that breaks |hop.hint_char2| if only one key is + pressed, falling back to the same behavior as |hop.hint_char1|. It is + recommended that, if you set this option, you not use a key that is in + your |hop-config-keys| (because it would make those keys unreachable), and + not a character that you might jump to. A good fallback key could be + `` or ``, for instance. + + Defaults:~ + `char2_fallback_key = nil` + +`extensions` *hop-config-extensions* + List-table of extensions to enable (names). As described in |hop-extension|, + extensions for which the name in that list must have a `register(opts)` + function in their public API for Hop to correctly initialized them. + + Defaults:~ + `extensions = nil` + +`multi_windows` *hop-config-multi_windows* + Enable cross-windows support and hint all the currently visible windows. + This behavior allows you to jump around any position in any buffer + currently visible in a window. Although a powerful a feature, remember + that enabling this will also generate many more sequence combinations, so + you could get deeper sequences to type (most of the time it should be good + if you have enough keys in |hop-config-keys|). + + Defaults:~ + `multi_windows = false` + +============================================================================== +EXTENSION *hop-extension* + +Hop supports extensions, that can provide ad hoc jump targets that are not +part of the core of Hop. If you want to write a Hop extension, this section +should provide all the required informations. + +The first important part is to know how to generate jump targets. You will +then first want to read the |hop-jump-target-api|, that describes this process +in terms of Lua protocol. + +The second part is that because you will depend on the Hop API, you need to be +sure that Hop is completely setup before initiating your Hop extension plugin. +In order to do so, your users will have to read the user guide of the package +manager they are using to mark Hop as a dependency of your plugin when +installing. You should ensure such instructions are easily accessible to them, +for instance in your help pages and README. + +Finally, Hop extension plugins do not get initialized via the regular `setup` +function — well, it’s up to you to still provide that function, but keep in +mind that Hop should not be assumed ready if you do so, so really keep setting +up things unrelated to Hop in your `setup` function. In order to setup a Hop +extension plugin, your plugin must provide the `register(opts)` function, that +takes the same {opts} arguments as `setup(opts)`. This function will be called +by Hop automatically. + +Once a user has installed both Hop and the extension plugin, they can simply +modify the configuration of Hop and pass the name of your plugin in the +`extensions` user configuration. See |hop-config-extensions| for further +details about that. + +============================================================================== +HIGHLIGHTS *hop-highlights* + +Anywhere in the hint buffer that doesn’t contain a hint, the |hl-EndOfBuffer| +highlight is used. For the rest: + +`HopNextKey` *hop-hl-HopNextkey* + Highlight used for the mono-sequence keys (i.e. sequence of 1). + +`HopNextKey1` *hop-hl-HopNextKey1* + Highlight used for the first key in a sequence. + +`HopNextKey2` *hop-hl-HopNextKey2* + Highlight used for the second and remaining keys in a sequence. + +`HopUnmatched` *hop-hl-HopUnmatched* + Highlight used for unmatched part of the buffer when running a Hop command + / Lua functions. + +`HopCursor` *hop-hl-HopCursor* + Highlight used for the fake cursor visible when running a Hop command / + Lua functions. + +`HopPreview` *hop-hl-HopPreview* + Highlight used for to preview the hint for HopPattern. + +Highlights are inserted in an augroup, `HopInitHighlight`, and an autocommand +is automatically set when initializing the plugin, unless you set +|hop-config-create_hl_autocmd| to `false`. + +============================================================================== +LICENSE *hop-license* + +Copyright (c) 2021-2022, Dimitri Sabadie + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Dimitri Sabadie nor the + names of other contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +============================================================================== +vim:tw=78:sw=4:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua b/etc/soft/nvim/+plugins/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua new file mode 100644 index 0000000..79d02a9 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/examples/hop-extension-hello-world/lua/hop-extension-hello-world/init.lua @@ -0,0 +1,32 @@ +local M = {} +M.opts = {} + +function M.hint_around_cursor(opts) + -- the jump target generator; we are simply going to retreive the cursor position and hint around it as an example + local jump_targets = function() -- opts ignored + local cursor_pos = require'hop.window'.get_window_context().cursor_pos + local line = cursor_pos[1] - 1 + local col = cursor_pos[2] + 1 + + local jump_targets = {} + + -- left + if col > 0 then + jump_targets[#jump_targets + 1] = { line = line, column = col - 1, window = 0 } + end + + -- right + jump_targets[#jump_targets + 1] = { line = line, column = col + 1, window = 0 } + + return { jump_targets = jump_targets } + end + + require'hop'.hint_with(jump_targets, opts) +end + +function M.register(opts) + vim.notify('registering the nice extension', 0) + M.opts = opts +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/defaults.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/defaults.lua new file mode 100644 index 0000000..32842f0 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/defaults.lua @@ -0,0 +1,17 @@ +local M = {} + +M.keys = 'asdghklqwertyuiopzxcvbnmfj' +M.quit_key = '' +M.perm_method = require'hop.perm'.TrieBacktrackFilling +M.reverse_distribution = false +M.teasing = true +M.jump_on_sole_occurrence = true +M.case_insensitive = true +M.create_hl_autocmd = true +M.current_line_only = false +M.uppercase_labels = false +M.multi_windows = false +M.hint_position = require'hop.hint'.HintPosition.BEGIN +M.hint_offset = 0 + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/health.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/health.lua new file mode 100644 index 0000000..887b049 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/health.lua @@ -0,0 +1,36 @@ +local M = {} +local hop = require'hop' + +-- Initialization check. +-- +-- This function will perform checks at initialization to ensure everything will work as expected. +function M.check() + local health = vim.health or require'health' + + health.report_start('Ensuring keys are unique') + local existing_keys = {} + local had_errors = false + for i = 0, #hop.opts.keys do + local key = hop.opts.keys:sub(i, i) + + if existing_keys[key] then + health.report_error(string.format('key %s appears more than once in opts.keys', key)) + had_errors = true + else + existing_keys[key] = true + end + end + + if not had_errors then + health.report_ok('Keys are unique') + end + + health.report_start('Checking for deprecated features') + had_errors = false + + if not had_errors then + health.report_ok('All good') + end +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/highlight.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/highlight.lua new file mode 100644 index 0000000..50b7341 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/highlight.lua @@ -0,0 +1,34 @@ +-- This module contains everything for highlighting Hop. +local M = {} + +-- Insert the highlights that Hop uses. +function M.insert_highlights() + -- Highlight used for the mono-sequence keys (i.e. sequence of 1). + vim.api.nvim_command('highlight default HopNextKey guifg=#ff007c gui=bold ctermfg=198 cterm=bold') + + -- Highlight used for the first key in a sequence. + vim.api.nvim_command('highlight default HopNextKey1 guifg=#00dfff gui=bold ctermfg=45 cterm=bold') + + -- Highlight used for the second and remaining keys in a sequence. + vim.api.nvim_command('highlight default HopNextKey2 guifg=#2b8db3 ctermfg=33') + + -- Highlight used for the unmatched part of the buffer. + -- ctermbg=bg is omitted because it errors if Normal does not have ctermbg set + -- Luckily guibg=bg does not seem to error even if Normal does not have guibg set so it can be used + vim.api.nvim_command('highlight default HopUnmatched guifg=#666666 guibg=bg guisp=#666666 ctermfg=242') + + -- Highlight used for the fake cursor visible when hopping. + vim.api.nvim_command('highlight default link HopCursor Cursor') + + -- Highlight used for preview pattern + vim.api.nvim_command('highlight link HopPreview IncSearch') +end + +function M.create_autocmd() + vim.api.nvim_command('augroup HopInitHighlight') + vim.api.nvim_command('autocmd!') + vim.api.nvim_command("autocmd ColorScheme * lua require'hop.highlight'.insert_highlights()") + vim.api.nvim_command('augroup end') +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/hint.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/hint.lua new file mode 100644 index 0000000..b7c6456 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/hint.lua @@ -0,0 +1,136 @@ +local perm = require'hop.perm' +local prio = require'hop.priority' + +local M = {} + +M.HintDirection = { + BEFORE_CURSOR = 1, + AFTER_CURSOR = 2, +} + +M.HintPosition = { + BEGIN = 1, + MIDDLE = 2, + END = 3, +} + +local function tbl_to_str(label) + local s = '' + + for i = 1, #label do + s = s .. label[i] + end + + return s +end + +-- Reduce a hint. +-- +-- This function will remove hints not starting with the input key and will reduce the other ones +-- with one level. +local function reduce_label(label, key) + local snd_idx = vim.fn.byteidx(label, 1) + if label:sub(1, snd_idx) == key then + label = label:sub(snd_idx + 1) + end + + if label == '' then + label = nil + end + + return label +end + +-- Reduce all hints and return the one fully reduced, if any. +function M.reduce_hints(hints, key) + local next_hints = {} + + for _, h in pairs(hints) do + local prev_label = h.label + h.label = reduce_label(h.label, key) + + if h.label == nil then + return h + elseif h.label ~= prev_label then + next_hints[#next_hints + 1] = h + end + end + + return nil, next_hints +end + +-- Create hints from jump targets. +-- +-- This function associates jump targets with permutations, creating hints. A hint is then a jump target along with a +-- label. +-- +-- If `indirect_jump_targets` is `nil`, `jump_targets` is assumed already ordered with all jump target with the same +-- score (0) +function M.create_hints(jump_targets, indirect_jump_targets, opts) + local hints = {} + local perms = perm.permutations(opts.keys, #jump_targets, opts) + + -- get or generate indirect_jump_targets + if indirect_jump_targets == nil then + indirect_jump_targets = {} + + for i = 1, #jump_targets do + indirect_jump_targets[i] = { index = i, score = 0 } + end + end + + for i, indirect in pairs(indirect_jump_targets) do + hints[indirect.index] = { + label = tbl_to_str(perms[i]), + jump_target = jump_targets[indirect.index] + } + end + + return hints +end + +-- Create the extmarks for per-line hints. +-- +-- Passing `opts.uppercase_labels = true` will display the hint as uppercase. +function M.set_hint_extmarks(hl_ns, hints, opts) + for _, hint in pairs(hints) do + local label = hint.label + if opts.uppercase_labels then + label = label:upper() + end + + local col = hint.jump_target.column - 1 + + if vim.fn.strdisplaywidth(label) == 1 then + vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, col, { + virt_text = { { label, "HopNextKey" } }, + virt_text_pos = 'overlay', + hl_mode = 'combine', + priority = prio.HINT_PRIO + }) + else + -- get the byte index of the second hint so that we can slice it correctly + local snd_idx = vim.fn.byteidx(label, 1) + vim.api.nvim_buf_set_extmark(hint.jump_target.buffer or 0, hl_ns, hint.jump_target.line, col, { + virt_text = { { label:sub(1, snd_idx), "HopNextKey1" }, { label:sub(snd_idx + 1), "HopNextKey2" } }, + virt_text_pos = 'overlay', + hl_mode = 'combine', + priority = prio.HINT_PRIO + }) + end + end +end + +function M.set_hint_preview(hl_ns, jump_targets) + for _, jt in ipairs(jump_targets) do + vim.api.nvim_buf_set_extmark(jt.buffer, hl_ns, jt.line, jt.column - 1, { + end_row = jt.line, + end_col = jt.column - 1 + jt.length, + hl_group = 'HopPreview', + hl_eol = true, + priority = prio.HINT_PRIO + }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/init.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/init.lua new file mode 100644 index 0000000..32027e5 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/init.lua @@ -0,0 +1,661 @@ +local M = {} + +-- Ensure options are sound. +-- +-- Some options cannot be used together. For instance, multi_windows and current_line_only don’t really make sense used +-- together. This function will notify the user of such ill-formed configurations. +local function check_opts(opts) + if not opts then + return + end + + if opts.multi_windows and opts.current_line_only then + vim.notify('Cannot use current_line_only across multiple windows', 3) + end + if vim.api.nvim_get_mode().mode ~= 'n' then + opts.multi_windows = false + end +end + +-- Allows to override global options with user local overrides. +local function override_opts(opts) + check_opts(opts) + return setmetatable(opts or {}, {__index = M.opts}) +end + +-- Display error messages. +local function eprintln(msg, teasing) + if teasing then + vim.api.nvim_echo({{msg, 'Error'}}, true, {}) + end +end + +-- Create hint state +-- +-- { +-- all_ctxs: All windows's context +-- buf_list: All buffers displayed in all windows +-- _ns: Required namespaces +-- } +local function create_hint_state(opts) + local window = require'hop.window' + + local hint_state = {} + + -- get all window's context and buffer list + hint_state.all_ctxs = window.get_window_context(opts.multi_windows) + hint_state.buf_list = {} + for _, bctx in ipairs(hint_state.all_ctxs) do + hint_state.buf_list[#hint_state.buf_list + 1] = bctx.hbuf + for _, wctx in ipairs(bctx.contexts) do + window.clip_window_context(wctx, opts.direction) + end + end + + -- create the highlight groups; the highlight groups will allow us to clean everything at once when Hop quits + hint_state.hl_ns = vim.api.nvim_create_namespace('hop_hl') + hint_state.dim_ns = vim.api.nvim_create_namespace('hop_dim') + + -- backup namespaces of diagnostic + if vim.fn.has("nvim-0.6") == 1 then + hint_state.diag_ns = vim.diagnostic.get_namespaces() + end + + -- Store users cursorline state + hint_state.cursorline = vim.api.nvim_win_get_option(vim.api.nvim_get_current_win(), 'cursorline') + + return hint_state +end + +-- A hack to prevent #57 by deleting twice the namespace (it’s super weird). +local function clear_namespace(buf_list, hl_ns) + for _, buf in ipairs(buf_list) do + if vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_buf_clear_namespace(buf, hl_ns, 0, -1) + vim.api.nvim_buf_clear_namespace(buf, hl_ns, 0, -1) + end + end +end + +-- Set the highlight of unmatched lines of the buffer. +-- +-- - hl_ns is the highlight namespace. +-- - top_line is the top line in the buffer to start highlighting at +-- - bottom_line is the bottom line in the buffer to stop highlighting at +local function set_unmatched_lines(buf_handle, hl_ns, top_line, bottom_line, cursor_pos, direction, current_line_only) + local hint = require'hop.hint' + local prio = require'hop.priority' + + local start_line = top_line + local end_line = bottom_line + local start_col = 0 + local end_col = nil + + if direction == hint.HintDirection.AFTER_CURSOR then + start_col = cursor_pos[2] + elseif direction == hint.HintDirection.BEFORE_CURSOR then + end_line = bottom_line - 1 + if cursor_pos[2] ~= 0 then end_col = cursor_pos[2] end + end + + if current_line_only then + if direction == hint.HintDirection.BEFORE_CURSOR then + start_line = cursor_pos[1] - 1 + end_line = cursor_pos[1] - 1 + else + start_line = cursor_pos[1] - 1 + end_line = cursor_pos[1] + end + end + + local extmark_options = { + end_line = end_line, + hl_group = 'HopUnmatched', + hl_eol = true, + priority = prio.DIM_PRIO + } + + if end_col then + local current_line = vim.api.nvim_buf_get_lines(buf_handle, cursor_pos[1] - 1, cursor_pos[1], true)[1] + local current_width = vim.fn.strdisplaywidth(current_line) + + if end_col > current_width then + end_col = current_width - 1 + end + + extmark_options.end_col = end_col + end + + vim.api.nvim_buf_set_extmark(buf_handle, hl_ns, start_line, start_col, + extmark_options) +end + +-- Dim everything out to prepare the Hop session for all windows. +local function apply_dimming(hint_state, opts) + local window = require'hop.window' + + for _, bctx in ipairs(hint_state.all_ctxs) do + for _, wctx in ipairs(bctx.contexts) do + window.clip_window_context(wctx, opts.direction) + -- dim everything out, add the virtual cursor and hide diagnostics + set_unmatched_lines(bctx.hbuf, hint_state.dim_ns, wctx.top_line, wctx.bot_line, wctx.cursor_pos, opts.direction, opts.current_line_only) + end + + if vim.fn.has("nvim-0.6") == 1 then + for ns in pairs(hint_state.diag_ns) do + vim.diagnostic.show(ns, bctx.hbuf, nil, { virtual_text = false }) + end + end + end +end + +-- Add the virtual cursor, taking care to handle the cases where: +-- - the virtualedit option is being used and the cursor is in a +-- tab character or past the end of the line +-- - the current line is empty +-- - there are multibyte characters on the line +local function add_virt_cur(ns) + local prio = require'hop.priority' + + local cur_info = vim.fn.getcurpos() + local cur_row = cur_info[2] - 1 + local cur_col = cur_info[3] - 1 -- this gives cursor column location, in bytes + local cur_offset = cur_info[4] + local virt_col = cur_info[5] - 1 + local cur_line = vim.api.nvim_get_current_line() + + -- toggle cursorline off if currently set + local cursorline_info = vim.api.nvim_win_get_option(vim.api.nvim_get_current_win(), 'cursorline') + if cursorline_info == true then + vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), 'cursorline', false) + end + + -- first check to see if cursor is in a tab char or past end of line + if cur_offset ~= 0 then + vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, { + virt_text = {{'█', 'Normal'}}, + virt_text_win_col = virt_col, + priority = prio.CURSOR_PRIO + }) + -- otherwise check to see if cursor is at end of line or on empty line + elseif #cur_line == cur_col then + vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, { + virt_text = {{'█', 'Normal'}}, + virt_text_pos = 'overlay', + priority = prio.CURSOR_PRIO + }) + else + vim.api.nvim_buf_set_extmark(0, ns, cur_row, cur_col, { + -- end_col must be column of next character, in bytes + end_col = vim.fn.byteidx(cur_line, vim.fn.charidx(cur_line, cur_col) + 1), + hl_group = 'HopCursor', + priority = prio.CURSOR_PRIO + }) + end +end + +-- Get pattern from input for hint and preview +function M.get_input_pattern(prompt, maxchar, opts) + local hint = require'hop.hint' + local jump_target = require'hop.jump_target' + + local hs = {} + if opts then + hs = create_hint_state(opts) + hs.preview_ns = vim.api.nvim_create_namespace('hop_preview') + apply_dimming(hs, opts) + add_virt_cur(hs.hl_ns) + end + + local K_Esc = vim.api.nvim_replace_termcodes('', true, false, true) + local K_BS = vim.api.nvim_replace_termcodes('', true, false, true) + local K_C_H = vim.api.nvim_replace_termcodes('', true, false, true) + local K_CR = vim.api.nvim_replace_termcodes('', true, false, true) + local K_NL = vim.api.nvim_replace_termcodes('', true, false, true) + local pat_keys = {} + local pat = '' + + while (true) do + pat = vim.fn.join(pat_keys, '') + if opts then + clear_namespace(hs.buf_list, hs.preview_ns) + if #pat > 0 then + local ok, re = pcall(jump_target.regex_by_case_searching, pat, false, opts) + if ok then + local jump_target_gtr = jump_target.jump_targets_by_scanning_lines(re) + local generated = jump_target_gtr(opts) + hint.set_hint_preview(hs.preview_ns, generated.jump_targets) + end + end + end + vim.api.nvim_echo({}, false, {}) + vim.cmd('redraw') + vim.api.nvim_echo({{prompt, 'Question'}, {pat}}, false, {}) + + local ok, key = pcall(vim.fn.getchar) + if not ok then -- Interrupted by + pat = nil + break + end + + if type(key) == 'number' then + key = vim.fn.nr2char(key) + elseif key:byte() == 128 then + -- It's a special key in string + end + + if key == K_Esc then + pat = nil + break + elseif key == K_CR or key == K_NL then + break + elseif key == K_BS or key == K_C_H then + pat_keys[#pat_keys] = nil + else + pat_keys[#pat_keys + 1] = key + end + + if maxchar and #pat_keys >= maxchar then + pat = vim.fn.join(pat_keys, '') + break + end + end + + if opts then + clear_namespace(hs.buf_list, hs.preview_ns) + -- quit only when got nothin for pattern to avoid blink of highlight + if not pat then M.quit(hs) end + end + vim.api.nvim_echo({}, false, {}) + vim.cmd('redraw') + return pat +end + +-- Move the cursor at a given location. +-- +-- Add option to shift cursor by column offset +-- +-- This function will update the jump list. +function M.move_cursor_to(w, line, column, hint_offset, direction) + -- If we do not ask for an offset jump, we don’t have to retrieve any additional lines because we will jump to the + -- actual jump target. If we do want a jump with an offset, we need to retrieve the line the jump target lies in so + -- that we can compute the offset correctly. This is linked to the fact that currently, Neovim doesn’s have an API to + -- « offset something by N visual columns. » + + -- If it is pending for operator shift column to the right by 1 + if vim.api.nvim_get_mode().mode == 'no' and direction ~= 1 then + column = column + 1 + end + + if hint_offset ~= nil and not (hint_offset == 0) then + -- Add `hint_offset` based on `charidx`. + local buf_line = vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(w), line - 1, line, false)[1] + -- Since `charidx` returns -1 when `column` is the tail, subtract 1 and add 1 to the return value to get + -- the correct value. + local char_idx = vim.fn.charidx(buf_line, column - 1) + 1 + hint_offset + column = vim.fn.byteidx(buf_line, char_idx) + end + + -- update the jump list + vim.cmd("normal! m'") + vim.api.nvim_set_current_win(w) + vim.api.nvim_win_set_cursor(w, { line, column }) +end + +function M.hint_with(jump_target_gtr, opts) + if opts == nil then + opts = override_opts(opts) + end + + M.hint_with_callback(jump_target_gtr, opts, function(jt) + M.move_cursor_to(jt.window, jt.line + 1, jt.column - 1, opts.hint_offset, opts.direction) + end) +end + +function M.hint_with_callback(jump_target_gtr, opts, callback) + local hint = require'hop.hint' + + if opts == nil then + opts = override_opts(opts) + end + + if not M.initialized then + vim.notify('Hop is not initialized; please call the setup function', 4) + return + end + + -- create hint state + local hs = create_hint_state(opts) + + -- create jump targets + local generated = jump_target_gtr(opts) + local jump_target_count = #generated.jump_targets + + local target_idx = nil + if jump_target_count == 0 then + target_idx = 0 + elseif vim.v.count > 0 then + target_idx = vim.v.count + elseif jump_target_count == 1 and opts.jump_on_sole_occurrence then + target_idx = 1 + end + + if target_idx ~= nil then + local jt = generated.jump_targets[target_idx] + if jt then + callback(jt) + else + eprintln(' -> there’s no such thing we can see…', opts.teasing) + end + + clear_namespace(hs.buf_list, hs.hl_ns) + clear_namespace(hs.buf_list, hs.dim_ns) + return + end + + -- we have at least two targets, so generate hints to display + hs.hints = hint.create_hints(generated.jump_targets, generated.indirect_jump_targets, opts) + + -- dim everything out, add the virtual cursor and hide diagnostics + apply_dimming(hs, opts) + add_virt_cur(hs.hl_ns) + hint.set_hint_extmarks(hs.hl_ns, hs.hints, opts) + vim.cmd('redraw') + + local h = nil + while h == nil do + local ok, key = pcall(vim.fn.getchar) + if not ok then + M.quit(hs) + break + end + local not_special_key = true + -- :h getchar(): "If the result of expr is a single character, it returns a + -- number. Use nr2char() to convert it to a String." Also the result is a + -- special key if it's a string and its first byte is 128. + -- + -- Note of caution: Even though the result of `getchar()` might be a single + -- character, that character might still be multiple bytes. + if type(key) == 'number' then + key = vim.fn.nr2char(key) + elseif key:byte() == 128 then + not_special_key = false + end + + if not_special_key and opts.keys:find(key, 1, true) then + -- If this is a key used in Hop (via opts.keys), deal with it in Hop + h = M.refine_hints(key, hs, callback, opts) + vim.cmd('redraw') + else + -- If it's not, quit Hop + M.quit(hs) + -- If the key captured via getchar() is not the quit_key, pass it through + -- to nvim to be handled normally (including mappings) + if key ~= vim.api.nvim_replace_termcodes(opts.quit_key, true, false, true) then + vim.api.nvim_feedkeys(key, '', true) + end + break + end + end +end + +-- Refine hints in the given buffer. +-- +-- Refining hints allows to advance the state machine by one step. If a terminal step is reached, this function jumps to +-- the location. Otherwise, it stores the new state machine. +function M.refine_hints(key, hint_state, callback, opts) + local hint = require'hop.hint' + + local h, hints = hint.reduce_hints(hint_state.hints, key) + + if h == nil then + if #hints == 0 then + eprintln('no remaining sequence starts with ' .. key, opts.teasing) + return + end + + hint_state.hints = hints + + clear_namespace(hint_state.buf_list, hint_state.hl_ns) + hint.set_hint_extmarks(hint_state.hl_ns, hints, opts) + else + M.quit(hint_state) + + -- prior to jump, register the current position into the jump list + vim.cmd("normal! m'") + + callback(h.jump_target) + return h + end +end + +-- Quit Hop and delete its resources. +function M.quit(hint_state) + clear_namespace(hint_state.buf_list, hint_state.hl_ns) + clear_namespace(hint_state.buf_list, hint_state.dim_ns) + + -- Restore users cursorline setting + if hint_state.cursorline == true then + vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), 'cursorline', true) + end + + for _, buf in ipairs(hint_state.buf_list) do + -- sometimes, buffers might be unloaded; that’s the case with floats for instance (we can invoke Hop from them but + -- then they disappear); we need to check whether the buffer is still valid before trying to do anything else with + -- it + if vim.api.nvim_buf_is_valid(buf) and vim.fn.has("nvim-0.6") == 1 then + for ns in pairs(hint_state.diag_ns) do vim.diagnostic.show(ns, buf) end + end + end +end + +function M.hint_words(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_word_start()), + opts + ) +end + +function M.hint_patterns(opts, pattern) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + -- The pattern to search is either retrieved from the (optional) argument + -- or directly from user input. + local pat + if pattern then + pat = pattern + else + vim.cmd('redraw') + vim.fn.inputsave() + pat = M.get_input_pattern('Hop pattern: ', nil, opts) + vim.fn.inputrestore() + if not pat then return end + end + + if #pat == 0 then + eprintln('-> empty pattern', opts.teasing) + return + end + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_case_searching(pat, false, opts)), + opts + ) +end + +function M.hint_char1(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local c = M.get_input_pattern('Hop 1 char: ', 1) + if not c then + return + end + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_case_searching(c, true, opts)), + opts + ) +end + +function M.hint_char2(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local c = M.get_input_pattern('Hop 2 char: ', 2) + if not c then + return + end + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_case_searching(c, true, opts)), + opts + ) +end + +function M.hint_lines(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.by_line_start()), + opts + ) +end + +function M.hint_vertical(opts) + local hint = require'hop.hint' + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + -- only makes sense as end position given movement goal. + opts.hint_position = hint.HintPosition.END + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + -- FIXME: need to exclude current and include empty lines. + M.hint_with( + generator(jump_target.regex_by_vertical()), + opts + ) +end + + +function M.hint_lines_skip_whitespace(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_line_start_skip_whitespace()), + opts + ) +end + +function M.hint_anywhere(opts) + local jump_target = require'hop.jump_target' + + opts = override_opts(opts) + + local generator + if opts.current_line_only then + generator = jump_target.jump_targets_for_current_line + else + generator = jump_target.jump_targets_by_scanning_lines + end + + M.hint_with( + generator(jump_target.regex_by_anywhere()), + opts + ) +end + +-- Setup user settings. +function M.setup(opts) + -- Look up keys in user-defined table with fallback to defaults. + M.opts = setmetatable(opts or {}, {__index = require'hop.defaults'}) + M.initialized = true + + -- Insert the highlights and register the autocommand if asked to. + local highlight = require'hop.highlight' + highlight.insert_highlights() + + if M.opts.create_hl_autocmd then + highlight.create_autocmd() + end + + -- register Hop extensions, if any + if M.opts.extensions ~= nil then + for _, ext_name in pairs(opts.extensions) do + local ok, extension = pcall(require, ext_name) + if not ok then + -- 4 is error; thanks Neovim… :( + vim.notify(string.format('extension %s wasn’t correctly loaded', ext_name), 4) + else + if extension.register == nil then + vim.notify(string.format('extension %s lacks the register function', ext_name), 4) + else + extension.register(opts) + end + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/jump_target.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/jump_target.lua new file mode 100644 index 0000000..513fc43 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/jump_target.lua @@ -0,0 +1,437 @@ +-- Jump targets. +-- +-- Jump targets are locations in buffers where users might jump to. They are wrapped in a table and provide the +-- required information so that Hop can associate label and display the hints. +-- +-- { +-- jump_targets = {}, +-- indirect_jump_targets = {}, +-- } +-- +-- The `jump_targets` field is a list-table of jump targets. A single jump target is simply a location in a given +-- buffer. So you can picture a jump target as a triple (line, column, window). +-- +-- { +-- line = 0, +-- column = 0, +-- window = 0, +-- } +-- +-- Indirect jump targets are encoded as a flat list-table of pairs (index, score). This table allows to quickly score +-- and sort jump targets. The `index` field gives the index in the `jump_targets` list. The `score` is any number. The +-- rule is that the lower the score is, the less prioritized the jump target will be. +-- +-- { +-- index = 0, +-- score = 0, +-- } +-- +-- So for instance, for two jump targets, a jump target generator must return such a table: +-- +-- { +-- jump_targets = { +-- { line = 1, column = 14, buffer = 0, window = 0 }, +-- { line = 2, column = 1, buffer = 0, window = 0 }, +-- }, +-- +-- indirect_jump_targets = { +-- { index = 0, score = 14 }, +-- { index = 1, score = 7 }, +-- }, +-- } +-- +-- This is everything you need to know to extend Hop with your own jump targets. + +local hint = require'hop.hint' +local window = require'hop.window' + +local M = {} + +-- Manhattan distance with column and row, weighted on x so that results are more packed on y. +function M.manh_dist(a, b, x_bias) + local bias = x_bias or 10 + return bias * math.abs(b[1] - a[1]) + math.abs(b[2] - a[2]) +end + +-- Mark the current line with jump targets. +-- +-- Returns the jump targets as described above. +local function mark_jump_targets_line(buf_handle, win_handle, regex, line_context, col_offset, win_width, direction_mode, hint_position) + local jump_targets = {} + local end_index = nil + + if win_width ~= nil then + end_index = col_offset + win_width + else + end_index = vim.fn.strdisplaywidth(line_context.line) + end + + local shifted_line = line_context.line:sub(1 + col_offset, vim.fn.byteidx(line_context.line, end_index)) + + -- modify the shifted line to take the direction mode into account, if any + -- FIXME: we also need to do that for the cursor + local col_bias = 0 + if direction_mode ~= nil then + local col = vim.fn.byteidx(line_context.line, direction_mode.cursor_col + 1) + if direction_mode.direction == hint.HintDirection.AFTER_CURSOR then + -- we want to change the start offset so that we ignore everything before the cursor + shifted_line = shifted_line:sub(col - col_offset) + col_bias = col - 1 + elseif direction_mode.direction == hint.HintDirection.BEFORE_CURSOR then + -- we want to change the end + shifted_line = shifted_line:sub(1, col - col_offset) + end + end + + local col = 1 + while true do + local s = shifted_line:sub(col) + local b, e = regex.match(s) + + if b == nil or (b == 0 and e == 0) then + break + end + -- Preview need a length to highlight the matched string. Zero means nothing to highlight. + local matched_length = e - b + -- As the make for jump target must be placed at a cell (but some pattern like '^' is + -- placed between cells), we should make sure e > b + if b == e then + e = e + 1 + end + + local colp = col + b + if hint_position == hint.HintPosition.MIDDLE then + colp = col + math.floor((b + e) / 2) + elseif hint_position == hint.HintPosition.END then + colp = col + e - 1 + end + jump_targets[#jump_targets + 1] = { + line = line_context.line_nr, + column = math.max(1, colp + col_offset + col_bias), + length = math.max(0, matched_length), + buffer = buf_handle, + window = win_handle, + } + + if regex.oneshot then + break + else + col = col + e + end + end + + return jump_targets +end + +-- Create jump targets for a given indexed line. +-- +-- This function creates the jump targets for the current (indexed) line and appends them to the input list of jump +-- targets `jump_targets`. +-- +-- Indirect jump targets are used later to sort jump targets by score and create hints. +local function create_jump_targets_for_line( + buf_handle, + win_handle, + jump_targets, + indirect_jump_targets, + regex, + col_offset, + win_width, + cursor_pos, + direction_mode, + hint_position, + line_context +) + -- first, create the jump targets for the ith line + local line_jump_targets = mark_jump_targets_line( + buf_handle, + win_handle, + regex, + line_context, + col_offset, + win_width, + direction_mode, + hint_position + ) + + -- then, append those to the input jump target list and create the indexed jump targets + local win_bias = math.abs(vim.api.nvim_get_current_win() - win_handle) * 1000 + for _, jump_target in pairs(line_jump_targets) do + jump_targets[#jump_targets + 1] = jump_target + + indirect_jump_targets[#indirect_jump_targets + 1] = { + index = #jump_targets, + score = M.manh_dist(cursor_pos, { jump_target.line, jump_target.column }) + win_bias + } + end +end + +-- Create jump targets by scanning lines in the currently visible buffer. +-- +-- This function takes a regex argument, which is an object containing a match function that must return the span +-- (inclusive beginning, exclusive end) of the match item, or nil when no more match is possible. This object also +-- contains the `oneshot` field, a boolean stating whether only the first match of a line should be taken into account. +-- +-- This function returns the lined jump targets (an array of N lines, where N is the number of currently visible lines). +-- Lines without jump targets are assigned an empty table ({}). For lines with jump targets, a list-table contains the +-- jump targets as pair of { line, col }. +-- +-- In addition the jump targets, this function returns the total number of jump targets (i.e. this is the same thing as +-- traversing the lined jump targets and summing the number of jump targets for all lines) as a courtesy, plus « +-- indirect jump targets. » Indirect jump targets are encoded as a flat list-table containing three values: i, for the +-- ith line, j, for the rank of the jump target, and dist, the score distance of the associated jump target. This list +-- is sorted according to that last dist parameter in order to know how to distribute the jump targets over the buffer. +function M.jump_targets_by_scanning_lines(regex) + return function(opts) + -- get the window context; this is used to know which part of the visible buffer is to hint + local all_ctxs = window.get_window_context(opts.multi_windows) + local jump_targets = {} + local indirect_jump_targets = {} + + -- Iterate all buffers + for _, bctx in ipairs(all_ctxs) do + -- Iterate all windows of a same buffer + for _, wctx in ipairs(bctx.contexts) do + window.clip_window_context(wctx, opts.direction) + -- Get all lines' context + local lines = window.get_lines_context(bctx.hbuf, wctx) + + -- in the case of a direction, we want to treat the first or last line (according to the direction) differently + if opts.direction == hint.HintDirection.AFTER_CURSOR then + -- the first line is to be checked first + create_jump_targets_for_line( + bctx.hbuf, + wctx.hwin, + jump_targets, + indirect_jump_targets, + regex, + wctx.col_offset, + wctx.win_width, + wctx.cursor_pos, + { cursor_col = wctx.cursor_pos[2], direction = opts.direction }, + opts.hint_position, + lines[1] + ) + + for i = 2, #lines do + create_jump_targets_for_line( + bctx.hbuf, + wctx.hwin, + jump_targets, + indirect_jump_targets, + regex, + wctx.col_offset, + wctx.win_width, + wctx.cursor_pos, + nil, + opts.hint_position, + lines[i] + ) + end + elseif opts.direction == hint.HintDirection.BEFORE_CURSOR then + -- the last line is to be checked last + for i = 1, #lines - 1 do + create_jump_targets_for_line( + bctx.hbuf, + wctx.hwin, + jump_targets, + indirect_jump_targets, + regex, + wctx.col_offset, + wctx.win_width, + wctx.cursor_pos, + nil, + opts.hint_position, + lines[i] + ) + end + + create_jump_targets_for_line( + bctx.hbuf, + wctx.hwin, + jump_targets, + indirect_jump_targets, + regex, + wctx.col_offset, + wctx.win_width, + wctx.cursor_pos, + { cursor_col = wctx.cursor_pos[2], direction = opts.direction }, + opts.hint_position, + lines[#lines] + ) + else + for i = 1, #lines do + create_jump_targets_for_line( + bctx.hbuf, + wctx.hwin, + jump_targets, + indirect_jump_targets, + regex, + wctx.col_offset, + wctx.win_width, + wctx.cursor_pos, + nil, + opts.hint_position, + lines[i] + ) + end + end + + end + end + + M.sort_indirect_jump_targets(indirect_jump_targets, opts) + + return { jump_targets = jump_targets, indirect_jump_targets = indirect_jump_targets } + end +end + +-- Jump target generator for regex applied only on the cursor line. +function M.jump_targets_for_current_line(regex) + return function(opts) + local context = window.get_window_context(false)[1].contexts[1] + local line_n = context.cursor_pos[1] + local line = vim.api.nvim_buf_get_lines(0, line_n - 1, line_n, false) + local jump_targets = {} + local indirect_jump_targets = {} + + create_jump_targets_for_line( + 0, + 0, + jump_targets, + indirect_jump_targets, + regex, + context.col_offset, + context.win_width, + context.cursor_pos, + { cursor_col = context.cursor_pos[2], direction = opts.direction }, + opts.hint_position, + { line_nr = line_n - 1, line = line[1] } + ) + + M.sort_indirect_jump_targets(indirect_jump_targets, opts) + + return { jump_targets = jump_targets, indirect_jump_targets = indirect_jump_targets } + end +end + +-- Apply a score function based on the Manhattan distance to indirect jump targets. +function M.sort_indirect_jump_targets(indirect_jump_targets, opts) + local score_comparison = nil + if opts.reverse_distribution then + score_comparison = function (a, b) return a.score > b.score end + else + score_comparison = function (a, b) return a.score < b.score end + end + + table.sort(indirect_jump_targets, score_comparison) +end + +-- Regex modes for the buffer-driven generator. +local function starts_with_uppercase(s) + if #s == 0 then + return false + end + + local f = s:sub(1, vim.fn.byteidx(s, 1)) + -- if it’s a space, we assume it’s not uppercase, even though Lua doesn’t agree with us; I mean, Lua is horrible, who + -- would like to argue with that creature, right? + if f == ' ' then + return false + end + + return f:upper() == f +end + +-- Regex by searching a pattern. +function M.regex_by_searching(pat, plain_search) + if plain_search then + pat = vim.fn.escape(pat, '\\/.$^~[]') + end + + local regex = vim.regex(pat) + + return { + oneshot = false, + match = function(s) + return regex:match_str(s) + end + } +end + +-- Wrapper over M.regex_by_searching to add support for case sensitivity. +function M.regex_by_case_searching(pat, plain_search, opts) + if plain_search then + pat = vim.fn.escape(pat, '\\/.$^~[]') + end + + if vim.o.smartcase then + if not starts_with_uppercase(pat) then + pat = '\\c' .. pat + end + elseif opts.case_insensitive then + pat = '\\c' .. pat + end + + local regex = vim.regex(pat) + + return { + oneshot = false, + match = function(s) + return regex:match_str(s) + end + } +end + +-- Word regex. +function M.regex_by_word_start() + return M.regex_by_searching('\\k\\+') +end + +-- Line regex. +function M.by_line_start() + local c = vim.fn.winsaveview().leftcol + + return { + oneshot = true, + match = function(s) + local l = vim.fn.strdisplaywidth(s) + if c > 0 and l == 0 then + return nil + end + + return 0, 1 + end + } +end + +-- Line regex at cursor position. +function M.regex_by_vertical() + local position = vim.api.nvim_win_get_cursor(0)[2] + local regex = vim.regex(string.format("^.\\{0,%d\\}\\(.\\|$\\)", position)) + return { + oneshot = true, + match = function(s) + return regex:match_str(s) + end + } +end + +-- Line regex skipping finding the first non-whitespace character on each line. +function M.regex_by_line_start_skip_whitespace() + local regex = vim.regex("\\S") + + return { + oneshot = true, + match = function(s) + return regex:match_str(s) + end + } +end + +-- Anywhere regex. +function M.regex_by_anywhere() + return M.regex_by_searching('\\v(<.|^$)|(.>|^$)|(\\l)\\zs(\\u)|(_\\zs.)|(#\\zs.)') +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/perm.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/perm.lua new file mode 100644 index 0000000..e1d946b --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/perm.lua @@ -0,0 +1,163 @@ +local M = {} + +-- Get the first key of a key set. +local function first_key(keys) + return keys:sub(1, vim.fn.byteidx(keys, 1)) +end + +-- Get the next key of the input key in the input key set, if any, or return nil. +local function next_key(keys, key) + local _, e = keys:find(key, 1, true) + + if e == #keys then + return nil + end + + local next = keys:sub(e + 1) + local n = next:sub(1, vim.fn.byteidx(next, 1)) + return n +end + +-- Permutation algorithm based on tries and backtrack filling. +M.TrieBacktrackFilling = {} + +-- Get the sequence encoded in a trie by a pointer. +function M.TrieBacktrackFilling:lookup_seq_trie(trie, p) + local seq = {} + local t = trie + + for _, i in pairs(p) do + local current_trie = t[i] + + seq[#seq + 1] = current_trie.key + t = current_trie.trie + end + + seq[#seq + 1] = t[#t].key + + return seq +end + +-- Add a new permutation to the trie at the current pointer by adding a key. +function M.TrieBacktrackFilling:add_trie_key(trie, p, key) + local seq = {} + local t = trie + + -- find the parent trie + for _, i in pairs(p) do + local current_trie = t[i] + + seq[#seq + 1] = current_trie.key + t = current_trie.trie + end + + t[#t + 1] = { key = key; trie = {} } + + return trie +end + +-- Maintain a trie pointer of a given dimension. +-- +-- If a pointer has components { 4, 1 } and the dimension is 4, this function will automatically complete the missing +-- dimensions by adding the last index, i.e. { 4, 1, X, X }. +local function maintain_deep_pointer(depth, n, p) + local q = vim.deepcopy(p) + + for i = #p + 1, depth do + q[i] = n + end + + return q +end + +-- Generate the next permutation with backtrack filling. +-- +-- - `keys` is the input key set. +-- - `trie` is a trie representing all the already generated permutations. +-- - `p` is the current pointer in the trie. It is a list of indices representing the parent layer in which the current +-- sequence occurs in. +-- +-- Returns `perms` added with the next permutation. +function M.TrieBacktrackFilling:next_perm(keys, trie, p) + if #trie == 0 then + return { { key = first_key(keys); trie = {} } }, p + end + + -- check whether the current sequence can have a next one + local current_seq = self:lookup_seq_trie(trie, p) + local key = next_key(keys, current_seq[#current_seq]) + + if key ~= nil then + -- we can generate the next permutation by just adding key to the current trie + self:add_trie_key(trie, p, key) + return trie, p + else + -- we have to backtrack; first, decrement the pointer if possible + local max_depth = #p + local keys_len = vim.fn.strwidth(keys) + + while #p > 0 do + local last_index = p[#p] + if last_index > 1 then + p[#p] = last_index - 1 + + p = maintain_deep_pointer(max_depth, keys_len, p) + + -- insert the first key at the new pointer after mutating the one already there + self:add_trie_key(trie, p, first_key(keys)) + self:add_trie_key(trie, p, next_key(keys, first_key(keys))) + return trie, p + else + -- we have exhausted all the permutations for the current layer; drop the layer index and try again + p[#p] = nil + end + end + + -- all layers are completely full everywhere; add a new layer at the end + p = maintain_deep_pointer(max_depth, keys_len, p) + + p[#p + 1] = #trie -- new layer + self:add_trie_key(trie, p, first_key(keys)) + self:add_trie_key(trie, p, next_key(keys, first_key(keys))) + + return trie, p + end +end + +function M.TrieBacktrackFilling:trie_to_perms(trie, perm) + local perms = {} + local p = vim.deepcopy(perm) + p[#p + 1] = trie.key + + if #trie.trie > 0 then + for _, sub_trie in pairs(trie.trie) do + vim.list_extend(perms, self:trie_to_perms(sub_trie, p)) + end + else + perms = { p } + end + + return perms +end + +function M.TrieBacktrackFilling:permutations(keys, n) + local perms = {} + local trie = {} + local p = {} + + for _ = 1, n do + trie, p = self:next_perm(keys, trie, p) + end + + for _, sub_trie in pairs(trie) do + vim.list_extend(perms, self:trie_to_perms(sub_trie, {})) + end + + return perms +end + +function M.permutations(keys, n, opts) + return opts.perm_method:permutations(keys, n, opts) +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/priority.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/priority.lua new file mode 100644 index 0000000..a41321f --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/priority.lua @@ -0,0 +1,14 @@ +-- Magic constants for highlight priorities; +-- +-- Priorities are ranged on 16-bit integers; 0 is the least priority and 2^16 - 1 is the higher. +-- We want Hop to override everything so we use a very high priority for grey (2^16 - 3 = 65533); hint +-- priorities are one level above (2^16 - 2) and the virtual cursor one level higher (2^16 - 1), which +-- is the higher. + +local M = {} + +M.DIM_PRIO = 65533 +M.HINT_PRIO = 65534 +M.CURSOR_PRIO = 65535 + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/lua/hop/window.lua b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/window.lua new file mode 100644 index 0000000..289b04a --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/lua/hop/window.lua @@ -0,0 +1,142 @@ +local hint = require'hop.hint' + +local M = {} + +local function window_context(win_handle, cursor_pos) + -- get a bunch of information about the window and the cursor + vim.api.nvim_set_current_win(win_handle) + local win_info = vim.fn.getwininfo(win_handle)[1] + local win_view = vim.fn.winsaveview() + local top_line = win_info.topline - 1 + local bot_line = win_info.botline + + -- NOTE: due to an (unknown yet) bug in neovim, the sign_width is not correctly reported when shifting the window + -- view inside a non-wrap window, so we can’t rely on this; for this reason, we have to implement a weird hack that + -- is going to disable the signs while hop is running (I’m sorry); the state is restored after jump + -- local left_col_offset = win_info.variables.context.number_width + win_info.variables.context.sign_width + local win_width = nil + + -- hack to get the left column offset in nowrap + if not vim.wo.wrap then + vim.api.nvim_win_set_cursor(win_handle, { cursor_pos[1], 0 }) + local left_col_offset = vim.fn.wincol() - 1 + vim.fn.winrestview(win_view) + win_width = win_info.width - left_col_offset + end + + return { + hwin = win_handle, + cursor_pos = cursor_pos, + top_line = top_line, + bot_line = bot_line, + win_width = win_width, + col_offset = win_view.leftcol + } +end + +-- Collect all multi-windows's context: +-- +-- { +-- { -- context list that each contains one buffer +-- hbuf = , +-- { -- windows list that display the same buffer +-- hwin = , +-- ... +-- }, +-- ... +-- }, +-- ... +-- } +function M.get_window_context(multi_windows) + local all_ctxs = {} + + -- Generate contexts of windows + local cur_hwin = vim.api.nvim_get_current_win() + local cur_hbuf = vim.api.nvim_win_get_buf(cur_hwin) + + all_ctxs[#all_ctxs + 1] = { + hbuf = cur_hbuf, + contexts = { window_context(cur_hwin, {vim.fn.line('.'), vim.fn.charcol('.')} ) }, + } + + if not multi_windows then + return all_ctxs + end + + for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + local b = vim.api.nvim_win_get_buf(w) + if w ~= cur_hwin then + + -- check duplicated buffers; the way this is done is by accessing all the already known contexts and checking that + -- the buffer we are accessing is already present in; if it is, we then append the window context to that buffer + local bctx = nil + for _, buffer_ctx in ipairs(all_ctxs) do + if b == buffer_ctx.hbuf then + bctx = buffer_ctx.contexts + break + end + end + + if bctx then + bctx[#bctx + 1] = window_context(w, vim.api.nvim_win_get_cursor(w)) + else + all_ctxs[#all_ctxs + 1] = { + hbuf = b, + contexts = { window_context(w, vim.api.nvim_win_get_cursor(w)) } + } + end + + end + end + + -- Move cursor back to current window + vim.api.nvim_set_current_win(cur_hwin) + + return all_ctxs +end + +-- Collect visible and unfold lines of window context +-- +-- { +-- { line_nr = 0, line = "" } +-- } +function M.get_lines_context(buf_handle, context) + local lines = {} + + local lnr = context.top_line + while lnr < context.bot_line do -- top_line is inclusive and bot_line is exclusive + local fold_end = vim.api.nvim_win_call(context.hwin, + function() + return vim.fn.foldclosedend(lnr + 1) -- `foldclosedend()` use 1-based line number + end) + if fold_end == -1 then + lines[#lines + 1] = { + line_nr = lnr, + line = vim.api.nvim_buf_get_lines(buf_handle, lnr, lnr + 1, false)[1], -- `nvim_buf_get_lines()` use 0-based line index + } + lnr = lnr + 1 + else + lines[#lines + 1] = { + line_nr = lnr, + line = "", + } + lnr = fold_end + end + end + + return lines +end + +-- Clip the window context based on the direction. +-- +-- If the direction is HintDirection.BEFORE_CURSOR, then everything after the cursor will be clipped. +-- If the direction is HintDirection.AFTER_CURSOR, then everything before the cursor will be clipped. +function M.clip_window_context(context, direction) + if direction == hint.HintDirection.BEFORE_CURSOR then + context.bot_line = context.cursor_pos[1] + elseif direction == hint.HintDirection.AFTER_CURSOR then + context.top_line = context.cursor_pos[1] - 1 + end +end + +return M diff --git a/etc/soft/nvim/+plugins/hop.nvim/plugin/hop.vim b/etc/soft/nvim/+plugins/hop.nvim/plugin/hop.vim new file mode 100644 index 0000000..6a2d319 --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/plugin/hop.vim @@ -0,0 +1,69 @@ +if !has('nvim-0.5.0') + echohl Error + echom 'This plugin only works with Neovim >= v0.5.0' + echohl clear + finish +endif + +" The jump-to-word command. +command! HopWord lua require'hop'.hint_words() +command! HopWordBC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopWordAC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopWordCurrentLine lua require'hop'.hint_words({ current_line_only = true }) +command! HopWordCurrentLineBC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true }) +command! HopWordCurrentLineAC lua require'hop'.hint_words({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true }) +command! HopWordMW lua require'hop'.hint_words({ multi_windows = true }) + +" The jump-to-pattern command. +command! HopPattern lua require'hop'.hint_patterns() +command! HopPatternBC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopPatternAC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopPatternCurrentLine lua require'hop'.hint_patterns({ current_line_only = true }) +command! HopPatternCurrentLineBC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true }) +command! HopPatternCurrentLineAC lua require'hop'.hint_patterns({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true }) +command! HopPatternMW lua require'hop'.hint_patterns({ multi_windows = true }) + +" The jump-to-char-1 command. +command! HopChar1 lua require'hop'.hint_char1() +command! HopChar1BC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopChar1AC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopChar1CurrentLine lua require'hop'.hint_char1({ current_line_only = true }) +command! HopChar1CurrentLineBC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true }) +command! HopChar1CurrentLineAC lua require'hop'.hint_char1({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true }) +command! HopChar1MW lua require'hop'.hint_char1({ multi_windows = true }) + +" The jump-to-char-2 command. +command! HopChar2 lua require'hop'.hint_char2() +command! HopChar2BC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopChar2AC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopChar2CurrentLine lua require'hop'.hint_char2({ current_line_only = true }) +command! HopChar2CurrentLineBC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true }) +command! HopChar2CurrentLineAC lua require'hop'.hint_char2({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true }) +command! HopChar2MW lua require'hop'.hint_char2({ multi_windows = true }) + +" The jump-to-line command. +command! HopLine lua require'hop'.hint_lines() +command! HopLineBC lua require'hop'.hint_lines({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopLineAC lua require'hop'.hint_lines({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopLineMW lua require'hop'.hint_lines({ multi_windows = true }) + +" The jump-to-line command (non-whitespace). +command! HopLineStart lua require'hop'.hint_lines_skip_whitespace() +command! HopLineStartBC lua require'hop'.hint_lines_skip_whitespace({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopLineStartAC lua require'hop'.hint_lines_skip_whitespace({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopLineStartMW lua require'hop'.hint_lines_skip_whitespace({ multi_windows = true }) + +" The vertical command (line jump preserving the column cursor position). +command! HopVertical lua require'hop'.hint_vertical() +command! HopVerticalBC lua require'hop'.hint_vertical({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopVerticalAC lua require'hop'.hint_vertical({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopVerticalMW lua require'hop'.hint_vertical({ multi_windows = true }) + +" The jump-to-anywhere command. +command! HopAnywhere lua require'hop'.hint_anywhere() +command! HopAnywhereBC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR }) +command! HopAnywhereAC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR }) +command! HopAnywhereCurrentLine lua require'hop'.hint_anywhere({ current_line_only = true }) +command! HopAnywhereCurrentLineBC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.BEFORE_CURSOR, current_line_only = true }) +command! HopAnywhereCurrentLineAC lua require'hop'.hint_anywhere({ direction = require'hop.hint'.HintDirection.AFTER_CURSOR, current_line_only = true }) +command! HopAnywhereMW lua require'hop'.hint_anywhere({ multi_windows = true }) diff --git a/etc/soft/nvim/+plugins/hop.nvim/rfcs/0001-hop-general-hint-modes.md b/etc/soft/nvim/+plugins/hop.nvim/rfcs/0001-hop-general-hint-modes.md new file mode 100644 index 0000000..c3fe61d --- /dev/null +++ b/etc/soft/nvim/+plugins/hop.nvim/rfcs/0001-hop-general-hint-modes.md @@ -0,0 +1,260 @@ +# Hop hint modes refined: an extensible model + +This document is a design document presenting a redesign of Hop’s « hint modes » to allow for a better customization +experience for people using Hop. + + + + +* [Context](#context) +* [Analysis](#analysis) +* [Prior and on-going work](#prior-and-on-going-work) +* [Solution](#solution) + * [Redesign `HintMode`](#redesign-hintmode) + * [Rewrite the public interface to support already existing modes](#rewrite-the-public-interface-to-support-already-existing-modes) + * [Part of the work that can be taken out of #123](#part-of-the-work-that-can-be-taken-out-of-123) +* [Alternatives](#alternatives) +* [Rationale](#rationale) +* [Future work](#future-work) + + +# Context + +The current code uses the concept of _hint modes_ to work. Hop goes through all the visible lines and applies the hint +mode on each line, extracting _jump targets_. The jump targets are then associated with permutations, and the sum of +those properties makes a _hint_. + +The goal is to be able to abstract away from this representation and create more general jump targets, so that the core +of Hop can be built using this new model, but also dependent users can: + +- Build other plugins using the Hop API to create their own jump target and then be able to jump to them. +- Extend the possible hint modes to provide more Hop motion without necessarily having to merge their code upstream. + This is especially true as some needs are not necessarily something that should be maintained in Hop directly, such + as Treesitter targets which are considered not really interesting. Nevertheless, if some users would like to be able + to use Treesitter as a source of targets, Hop should provide a powerful enough API to allow people to do just that. + +Currently, there is no way (besides pushing code) to extend Hop features. Because we want to let _programmers_ extend +Hop, there is no question to let _users_ extend it. What that means is that if a new motion is wanted, two possible +options are available: + +- The motion is implemented as a local Lua function / Vim command mapped in the user configuration. +- Someone makes a plugin exposing the Lua function / a Vim command and implementing the motion. +- A possible third option that is unlikely but still possible would be that the motion is small and useful enough to + merge it upstream in https://github.com/phaazon/hop.nvim. + +# Analysis + +In order to understand how the code is currently working, we can have a look at it from a user perspective. They are +likely to use, either: + +- The Vim commands, exposed in `pugin/hop.vim`. +- The Lua API public functions, in `lua/hop/init.lua`. + +The Lua functions to use start with `hint_`. For instance, `hint_words()` (`:HopWord`). `hint_words` is defined as: + +```lua +function M.hint_words(opts) + hint_with(hint.by_word_start, get_command_opts(opts)) +end +``` + +`hint_patterns`, `hint_char1`, etc. are defined in a similar fashion. `hint_with` is the current (local) function used +to build other hint modes. It takes a `HintMode` as argument and the user options, and builds applies the hint mode. +This is the function that needs to be changed. It must, first, be publicly available. Then, the way the hint modes are +applied need to change. For instance, if a user wants to use Hop with their own jump targets (without having to scan +the visible part of the buffer), they should be able to. + +The `hint_with` function is a pretty complex function that does a lot of things: + +- It extracts a bunch of information about the current visible part of the buffer. This is useful not to create hints + for text that the user cannot see. +- It supports various optinos, such as direction hinting (before cursor, after cursor, current-line-only, etc.). +- It creates the highlight groups. +- Get the buffer lines so that hint modes can be applied on. +- Call the hint modes and reduce hints until a match is found. +- Do the actual jump. + +`hint_mode`, the `HintMode` argument assed to `hint_with`, is used like this: + +- It contains a `curr_line_only` boolean that allows to know whether the hinting should be restricted to the current + line only. +- It is passed to `hop.hint.create_hints` to create the hints. To do so, it is passed to other functions that will call + the `match` function on it. What it does is to generate a pair of values allowing to pin-point where the jump + targets are. It is not really an iterator as it returns _spans_ value (i.e. beginning / end), so we have to manually + shift lines to know where and when to stop. +- It contains a `oneshot` boolean that is mostly an implementation detail for the `match` loop to work. If it’s + `oneshot`, the loop breaks at the first iteration. This is useful for line hinting for instance, where only one jump + target should exist on each line. + +So a couple of things to change here, obviously. + +# Prior and on-going work + +Some PRs have been pushed to attempt to solve this problem: + +- [#123](https://github.com/phaazon/hop.nvim/pull/123): refactor hint strategies. Unfortunately, this PR wasn’t reviewed + until late and had conflicting changes. Also, this PR changed too many things, refactoring things that don’t really + have to be at this point (or not making sense to move around). However, the work can probably be partially taken out + and rebased in other commits, so that this work is not lost. +- [#133](https://github.com/phaazon/hop.nvim/pull/133): this one is a bit weird, as its scope could be have been split + into several PRs. The multi-windows support is probably something that will come later once hint modes are refactored. + The dict support to allow to pass a dict of things is interesting but that’s also a feature that should be added + later, once the code is refactored. + +So clearly, we need to do something about #123 first. #133 will then be rebased and should be smaller. + +# Solution + +## Redesign `HintMode` + +The first thing that needs to be done is to change change `HintMode` so that it doesn’t assume to run line-by-line. The +thing is, `hint_with` should be its own hint mode (that iterates over the lines of the currently visible part of the +buffer and extract jump targets). A `HintMode` should then be: + +- A function that provides the jump targets. We need to provide some functions to be able to get visible lines for + instance for people who still want to operate on these. The idea is that once this function has run, it must provide a + dict of jump targets by buffer. Something like: + +```lua +{ + -- jump target for a buffer + { + buffer_handle = 124, + jump_targets = { + { line = 67, column = 4}, + { line = 67, column = 7}, + -- … + }, + }, + + -- another jump target for another buffer… +} +``` + +- The code that creates hints (`hop.hint.create_hints`) then must only call that function and do the regular, currently + implemented algorithm associating jump targets with permutations to generate the actual hints. +- The hint reduction can occur as it normally does. + +This solution removes `oneshot` and `match`, leaving hint modes as a simple generator function providing the list of +jump targets. However, doing this will require to move some code around to help writing those jump target generators. +For instance, the logic that goes line-by-line, extracting word patterns for instance, is not trivial and is very tricky +to implement (multi-byte, virtualedit, etc.). So this must stay around. Something we can probably do here is to provide +a function that will output a `HintMode` going line-by-line and applying the logic passed as argument. Also, I suggest +to change the name `HintMode` to `JumpTargetGenerator`, which makes more sense. + +About the actual function generating the list, something that goes to mind: should we make this a fully synchronous +function that will return all the targets at once, or should we make this an actual generator? I.e. calling it will +return the first jump target, then calling it a second time will return the next jump target, etc. I think it can have +interesting use-cases but it will probably slow everything down for probably not something super interesting. + +This design seems to be pretty similar to what was planned in #123, so there is probably some commits to extract from +that PR. + +## Rewrite the public interface to support already existing modes + +Currently, the following modes are available: + +- `HopWord`: hint words. +- `HopWordBC`: same as above, but _before cursor_. +- `HopWordAC`: same as above, but _after cursor_. +- `HopPattern`: hint pattern (manually entered by the user with `input`). +- `HopPatternBC`: same as above, but _before cursor_. +- `HopPatternAC`: same as above, but _after cursor_. +- `HopChar1`: hint the current buffer by pressing one character to select which ones to jump to. +- `HopChar1BC`: same as above but _before cursor_. +- `HopChar1AC`: same as above but _after cursor_. +- `HopChar2`: hint the current buffer by pressing two characters to select which ones to jump to. +- `HopChar2BC`: same as above but _before cursor_. +- `HopChar2AC`: same as above but _after cursor_. +- `HopLine`: hint lines (first column). +- `HopLineBC`: same as above but _before cursor_. +- `HopLineAC`: same as above but _after cursor_. +- `HopLineStart`: hint lines (first non whitespace character). +- `HopLineStartBC`: same asbove but _before_cursor_. +- `HopLineStartAC`: same asbove but _after cursor_. +- `HopChar1Line`: same as `HopChar1` but applies only to the current line. +- `HopChar1LineAC`: same asbove but _before_cursor_. +- `HopChar1LineBC`: same asbove but _after cursor_. + +All those commands need to be re-implemented with the new `JumpTargetGenerator` design. All the current commands are +based on scanning line-by-line the currently visible part of the buffer, so we will want a function creating a +`JumpTargetGenerator` that implements this logic. Its arguments should allow us to implement all of the commands above. +The important thing to understand is that the `*BC` and `*AC` variations are actually the same mode but applied with the +user configuration (i.e. `direction`). Restricting to the same line should probably also be a user-configuration option +to allow using `HopWord` only on the current line, for instance. + +## Part of the work that can be taken out of #123 + +Given all the work described here, here is a break down view of the PR: + +- [5f93a87d](https://github.com/phaazon/hop.nvim/pull/123/commits/5f93a87d57c4926ceb1f71898c96e777c2ff33d6): + refactoring / code hygiene. **Will pick**. +- [bc449524](https://github.com/phaazon/hop.nvim/pull/123/commits/bc449524605317f48aff824f55e8a0e2ed40d87e): move + `HintMode` to a new weird `constants.lua` module. This adds no value. **Will drop**. +- [adbab40e](https://github.com/phaazon/hop.nvim/pull/123/commits/adbab40ef97f1516dd301bb7c9b9e66bc3638c39): move + all the logic of getting the visible buffer part into a `get_window_context` function. This is interesting but will + require a bit of fixup work. **Will pick**. +- [30d35b94](https://github.com/phaazon/hop.nvim/pull/123/commits/30d35b9479a42feaca35707c5176d47ce7a6e58d): introduce + the concept of _aggregate_ to refactor the process of mapping jump targets (`indirect_hints`) with permutations to + yield `hints`. I’m not a huge fan of the terminology, it doesn’t really convey what the aggregates are for. We need to + change the terminology, but I will probably use that too. **Will pick**. +- [e8c84c11](https://github.com/phaazon/hop.nvim/pull/123/commits/e8c84c11a2a085d8df108772ad0c6d41571a3aa1): move out + associating permutations to jump targets. I’m mostly okay with this, but we need to change the name of the function so + that it’s clear that the function now only creates jump targets, and another function adds the permutations to it. + This function still uses the _aggregate_ concept introduced in the previous commit, for which we need to change the + terminology. **Will pick**. +- [c8fa480c](https://github.com/phaazon/hop.nvim/pull/123/commits/c8fa480ce593296dcf49dd0ff7401ed930bafcf1): change the + semantics of hint modes to use `get_hints` instead of scanning lines by lines. We need to change the name so that it’s + something like `get_jump_targets` instead. **Will pick**. +- [9fc5d517](https://github.com/phaazon/hop.nvim/pull/123/commits/9fc5d51785a2819ecc2f7ce72fbaf3f8ad092ff9): remove + length from the output of the function creating the hints. This commit might be dangerous, because the reason for + having the length is important (it allows to ensure we cut currently the hints if they overlap / are at the end of a + `wrap` window). **Not sure, probably will drop**. +- [8a482a98](https://github.com/phaazon/hop.nvim/pull/123/commits/8a482a98041433fb924807a0b53f231327db90c2): replace the + concept of _aggregate_ with _hint list_ (should be _jump target list_) and general refactoring. Not sure whether I + will use this as the rest of the design will probably be left to implement regarding this current document. **Not + sure, probably will pick**. +- [82117eab](https://github.com/phaazon/hop.nvim/pull/123/commits/82117eab12f6f3278927667623ed4c267608a22e): + documentation enhancement. **Will pick**. + +The actual commits that were picked were not the ones described just above because decisions to refactor / renames +things in a different way that matches more the overall design. + +# Alternatives + +Besides merging code upstream, there is no real alternative to this problem. We have to expose the jump target retreival +on the public API so that people can extend Hop the way they want. + +# Rationale + +This redesign should allow to people to extend Hop on their side without having to merge code upstream. Different +motivations exist, among people wanting to use Treesitter-based motions, which will _not_ end up upstream as I think +it’s not really interesting / useful / out of scope, because hints are more a visual thing than a semantics thing; +people wanting to use Hop in menus / interfaces, etc. + +The other good point of this redesign is that we still support the _user configuration_ that is very important to the +author ([@phaazon](https://github.com/phaazon)). + +Another interesting aspects of this redesign is to allow people to create Lua plugins that might end up upstream if +needed, but people wanting to create « extensions » plugins will not have to depend on the upstream to have it possible. +This is important for two reasons: + +- People can implement their workflow. +- Hop can remain small and thus is much easier to maintain. + +In order to help plugin authors to write their Hop extension, we will have to keep the documentation updated and +top of the notch. + +# Future work + +An important matter while I was writing this design doc: because we are probably going to have people implementing +extensions, they are going to use the public API of Hop, which will probably have deprecations / breaking-change at some +point. I have parallel work on going (i.e. [poesie.nvim](https://github.com/phaazon/poesie.nvim)) but ultimately, I +really want a SemVer API, so that plugin authors don’t have to worry too much about this. It’s more about the end-users: +I really dislike it when I update something and it breaks because of a deprecation somewhere. People have their lives, +they won’t update immediately, so we need SemVer to prevent that kind of problems from occurring. We need to keep that +in mind for later because this Hop extension thing is going to a perfect example about why we need this. I’m explicitely +pinging [@mjlbach](https://github.com/mjlbach) as we mentioned that quite a few times lately, and to show that Hop is +going to _really_ need this. I might probably implement a convention in poesie and givin what the core team want to do +regarding plugins (whether their version will be checked in the core or whether poesie / something else should be +responsible for it). diff --git a/etc/soft/nvim/+plugins/impatient.nvim/LICENSE b/etc/soft/nvim/+plugins/impatient.nvim/LICENSE deleted file mode 100644 index 867f510..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 Lewis Russell - -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. diff --git a/etc/soft/nvim/+plugins/impatient.nvim/Makefile b/etc/soft/nvim/+plugins/impatient.nvim/Makefile deleted file mode 100644 index 3e8ccb8..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -.DEFAULT_GOAL := test - -NEOVIM_BRANCH := master - -FILTER=.* - -NEOVIM := neovim-$(NEOVIM_BRANCH) - -.PHONY: neovim -neovim: $(NEOVIM) - -$(NEOVIM): - git clone --depth 1 https://github.com/neovim/neovim --branch $(NEOVIM_BRANCH) $@ - make -C $@ - -export VIMRUNTIME=$(PWD)/$(NEOVIM)/runtime - -.PHONY: test -test: $(NEOVIM) - $(NEOVIM)/.deps/usr/bin/busted \ - -v \ - --lazy \ - --helper=$(PWD)/test/preload.lua \ - --output test.busted.outputHandlers.nvim \ - --lpath=$(PWD)/$(NEOVIM)/?.lua \ - --lpath=$(PWD)/$(NEOVIM)/build/?.lua \ - --lpath=$(PWD)/$(NEOVIM)/runtime/lua/?.lua \ - --lpath=$(PWD)/?.lua \ - --lpath=$(PWD)/lua/?.lua \ - --filter=$(FILTER) \ - $(PWD)/test - - -@stty sane diff --git a/etc/soft/nvim/+plugins/impatient.nvim/README.md b/etc/soft/nvim/+plugins/impatient.nvim/README.md deleted file mode 100644 index f4ae90f..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/README.md +++ /dev/null @@ -1,805 +0,0 @@ -# impatient.nvim - -[![CI](https://github.com/lewis6991/impatient.nvim/workflows/CI/badge.svg?branch=main)](https://github.com/lewis6991/impatient.nvim/actions?query=workflow%3ACI) - -Speed up loading Lua modules in Neovim to improve startup time. - -## Optimisations - -This plugin does several things to speed loading Lua modules and files. - -### Implements a chunk cache - -This is done by using `loadstring` to compile the Lua modules to bytecode and stores them in a cache file. The cache is invalidated using as hash consisting of: - -- The modified time (`sec` and `nsec`) of the file path. -- The file size. - -The cache file is located in `$XDG_CACHE_HOME/nvim/luacache_chunks`. - -### Implements a module resolution cache - -This is done by maintaining a table of module name to path. The cache is invalidated only if a path no longer exists. - -The cache file is located in `$XDG_CACHE_HOME/nvim/luacache_modpaths`. - -**Note**: This optimization breaks the loading order guarantee of the paths in `'runtimepath'`. -If you rely on this ordering then you can disable this cache (`_G.__luacache_config = { modpaths = { enable = false } }`. -See configuration below for more details. - -## Requirements - -- Neovim v0.7 - -## Installation - -[packer.nvim](https://github.com/wbthomason/packer.nvim): -```lua --- Is using a standard Neovim install, i.e. built from source or using a --- provided appimage. -use 'lewis6991/impatient.nvim' -``` - -## Setup - -To use impatient, you need only to include it near the top of your `init.lua` or `init.vim`. - -init.lua: - -```lua -require('impatient') -``` - -init.vim: - -```viml -lua require('impatient') -``` - -## Commands - -`:LuaCacheClear`: - -Remove the loaded cache and delete the cache file. A new cache file will be created the next time you load Neovim. - -`:LuaCacheLog`: - -View log of impatient. - -`:LuaCacheProfile`: - -View profiling data. To enable, Impatient must be setup with: - -```viml -lua require'impatient'.enable_profile() -``` - -## Configuration - -Unlike most plugins which provide a `setup()` function, Impatient uses a configuration table stored in the global state, `_G.__luacache_config`. -If you modify the default configuration, it must be done before `require('impatient')` is run. - -Default config: - -```lua -_G.__luacache_config = { - chunks = { - enable = true, - path = vim.fn.stdpath('cache')..'/luacache_chunks', - }, - modpaths = { - enable = true, - path = vim.fn.stdpath('cache')..'/luacache_modpaths', - } -} -require('impatient') -``` - -## Performance Example - -Measured on a M1 MacBook Air. - -
-Standard - -``` -────────────┬────────────┐ - Resolve │ Load │ -────────────┼────────────┼───────────────────────────────────────────────────────────────── - Time │ Time │ Module -────────────┼────────────┼───────────────────────────────────────────────────────────────── - 54.337ms │ 34.257ms │ Total -────────────┼────────────┼───────────────────────────────────────────────────────────────── - 7.264ms │ 0.470ms │ octo.colors - 3.154ms │ 0.128ms │ diffview.bootstrap - 2.086ms │ 0.231ms │ gitsigns - 0.320ms │ 0.982ms │ octo.date - 0.296ms │ 1.004ms │ octo.writers - 0.322ms │ 0.893ms │ octo.utils - 0.293ms │ 0.854ms │ vim.diagnostic - 0.188ms │ 0.819ms │ vim.lsp.util - 0.261ms │ 0.739ms │ vim.lsp - 0.330ms │ 0.620ms │ octo.model.octo-buffer - 0.392ms │ 0.422ms │ packer.load - 0.287ms │ 0.436ms │ octo.reviews - 0.367ms │ 0.325ms │ octo - 0.309ms │ 0.381ms │ octo.graphql - 0.454ms │ 0.221ms │ octo.base64 - 0.295ms │ 0.338ms │ octo.reviews.file-panel - 0.305ms │ 0.306ms │ octo.reviews.file-entry - 0.183ms │ 0.386ms │ vim.treesitter.query - 0.418ms │ 0.149ms │ vim.uri - 0.342ms │ 0.213ms │ octo.config - 0.110ms │ 0.430ms │ nvim-lsp-installer.ui.status-win - 0.296ms │ 0.209ms │ octo.window - 0.202ms │ 0.288ms │ vim.lsp.rpc - 0.352ms │ 0.120ms │ octo.gh - 0.287ms │ 0.184ms │ octo.reviews.layout - 0.209ms │ 0.260ms │ vim.lsp.handlers - 0.108ms │ 0.360ms │ luasnip.nodes.snippet - 0.243ms │ 0.212ms │ dirvish - 0.289ms │ 0.159ms │ octo.mappings - 0.228ms │ 0.220ms │ trouble.view - 0.145ms │ 0.293ms │ plenary.job - 0.188ms │ 0.244ms │ vim.lsp.diagnostic - 0.032ms │ 0.391ms │ packer_compiled - 0.188ms │ 0.228ms │ vim.lsp.buf - 0.186ms │ 0.227ms │ vim.lsp.protocol - 0.141ms │ 0.264ms │ nvim-treesitter.install - 0.205ms │ 0.190ms │ vim.lsp._snippet - 0.114ms │ 0.281ms │ colorizer - 0.124ms │ 0.262ms │ nvim-treesitter.parsers - 0.331ms │ 0.052ms │ octo.model.body-metadata - 0.325ms │ 0.054ms │ octo.constants - 0.296ms │ 0.081ms │ octo.reviews.renderer - 0.326ms │ 0.050ms │ octo.model.thread-metadata - 0.258ms │ 0.117ms │ trouble - 0.106ms │ 0.267ms │ cmp.core - 0.286ms │ 0.085ms │ octo.completion - 0.120ms │ 0.250ms │ luasnip - 0.286ms │ 0.084ms │ octo.ui.bubbles - 0.068ms │ 0.298ms │ diffview.utils - 0.325ms │ 0.039ms │ octo.model.title-metadata - 0.126ms │ 0.234ms │ treesitter-context - 0.282ms │ 0.073ms │ octo.signs - 0.299ms │ 0.043ms │ octo.folds - 0.112ms │ 0.228ms │ luasnip.util.util - 0.181ms │ 0.156ms │ vim.treesitter.languagetree - 0.260ms │ 0.073ms │ vim.keymap - 0.101ms │ 0.231ms │ cmp.entry - 0.182ms │ 0.145ms │ vim.treesitter.highlighter - 0.191ms │ 0.121ms │ trouble.util - 0.190ms │ 0.119ms │ vim.lsp.codelens - 0.190ms │ 0.117ms │ vim.lsp.sync - 0.197ms │ 0.105ms │ vim.highlight - 0.170ms │ 0.132ms │ spellsitter - 0.086ms │ 0.213ms │ github_dark - 0.200ms │ 0.099ms │ persistence - 0.100ms │ 0.196ms │ cmp.view.custom_entries_view - 0.118ms │ 0.176ms │ nvim-treesitter.configs - 0.090ms │ 0.201ms │ gitsigns.git - 0.114ms │ 0.170ms │ nvim-lsp-installer.ui.display - 0.217ms │ 0.064ms │ plenary.async.async - 0.195ms │ 0.078ms │ vim.lsp.log - 0.191ms │ 0.081ms │ trouble.renderer - 0.122ms │ 0.150ms │ nvim-treesitter.ts_utils - 0.235ms │ 0.035ms │ plenary - 0.100ms │ 0.168ms │ cmp.source - 0.191ms │ 0.076ms │ vim.treesitter - 0.106ms │ 0.160ms │ lspconfig.util - 0.118ms │ 0.147ms │ nvim-treesitter.query - 0.088ms │ 0.176ms │ gitsigns.config - 0.108ms │ 0.150ms │ cmp - 0.193ms │ 0.063ms │ trouble.providers - 0.206ms │ 0.050ms │ tmux.version.parse - 0.103ms │ 0.151ms │ cmp.view.wildmenu_entries_view - 0.070ms │ 0.178ms │ diffview.path - 0.189ms │ 0.058ms │ trouble.providers.lsp - 0.096ms │ 0.147ms │ luasnip.util.parser - 0.093ms │ 0.150ms │ gitsigns.manager - 0.097ms │ 0.145ms │ null-ls.utils - 0.155ms │ 0.087ms │ plenary.async.control - 0.105ms │ 0.135ms │ nvim-lsp-installer.installers.std - 0.107ms │ 0.130ms │ lspconfig.configs - 0.097ms │ 0.140ms │ null-ls.helpers.generator_factory - 0.188ms │ 0.047ms │ trouble.providers.telescope - 0.191ms │ 0.040ms │ trouble.config - 0.099ms │ 0.131ms │ cmp.utils.window - 0.096ms │ 0.133ms │ luasnip.nodes.choiceNode - 0.192ms │ 0.036ms │ trouble.providers.qf - 0.104ms │ 0.124ms │ cmp.utils.keymap - 0.089ms │ 0.139ms │ gitsigns.hunks - 0.104ms │ 0.122ms │ nvim-lsp-installer.process - 0.096ms │ 0.129ms │ null-ls.sources - 0.116ms │ 0.108ms │ nvim-lsp-installer - 0.096ms │ 0.128ms │ luasnip.nodes.dynamicNode - 0.162ms │ 0.062ms │ tmux.copy - 0.197ms │ 0.025ms │ trouble.folds - 0.156ms │ 0.066ms │ plenary.async.util - 0.150ms │ 0.071ms │ cmp.utils.highlight - 0.105ms │ 0.116ms │ nvim-lsp-installer.server - 0.118ms │ 0.100ms │ nvim-treesitter.utils - 0.182ms │ 0.035ms │ trouble.providers.diagnostic - 0.103ms │ 0.114ms │ luasnip.nodes.node - 0.185ms │ 0.031ms │ trouble.colors - 0.180ms │ 0.035ms │ vim.ui - 0.162ms │ 0.053ms │ spaceless - 0.118ms │ 0.097ms │ nvim-treesitter.shell_command_selectors - 0.160ms │ 0.053ms │ tmux.wrapper.tmux - 0.182ms │ 0.031ms │ vim.treesitter.language - 0.178ms │ 0.035ms │ trouble.text - 0.157ms │ 0.054ms │ plenary.vararg.rotate - 0.106ms │ 0.104ms │ nvim-lsp-installer.installers.context - 0.181ms │ 0.028ms │ tmux - 0.158ms │ 0.050ms │ nvim-treesitter-playground - 0.067ms │ 0.140ms │ diffview.oop - 0.158ms │ 0.047ms │ tmux.resize - 0.166ms │ 0.039ms │ tmux.log.convert - 0.161ms │ 0.044ms │ tmux.layout - 0.155ms │ 0.048ms │ plenary.async.structs - 0.101ms │ 0.102ms │ cmp.view - 0.096ms │ 0.105ms │ luasnip.util.environ - 0.145ms │ 0.055ms │ plenary.async - 0.163ms │ 0.037ms │ tmux.navigation.navigate - 0.179ms │ 0.020ms │ tmux.keymaps - 0.155ms │ 0.044ms │ plenary.functional - 0.102ms │ 0.097ms │ cmp.matcher - 0.103ms │ 0.095ms │ cmp.view.ghost_text_view - 0.106ms │ 0.091ms │ colorizer.nvim - 0.168ms │ 0.029ms │ tmux.log - 0.106ms │ 0.090ms │ nvim-lsp-installer._generated.filetype_map - 0.122ms │ 0.073ms │ nvim-treesitter.info - 0.098ms │ 0.097ms │ null-ls.client - 0.105ms │ 0.089ms │ nvim-lsp-installer.log - 0.170ms │ 0.024ms │ tmux.navigation - 0.109ms │ 0.084ms │ nvim-lsp-installer.servers - 0.098ms │ 0.095ms │ null-ls.helpers.diagnostics - 0.160ms │ 0.033ms │ tmux.configuration.options - 0.100ms │ 0.091ms │ cmp.utils.misc - 0.044ms │ 0.148ms │ lewis6991 - 0.104ms │ 0.088ms │ colorizer.trie - 0.163ms │ 0.028ms │ ts_context_commentstring - 0.054ms │ 0.136ms │ cmp-rg - 0.130ms │ 0.060ms │ nvim-treesitter.query_predicates - 0.151ms │ 0.039ms │ plenary.reload - 0.096ms │ 0.094ms │ luasnip.nodes.insertNode - 0.160ms │ 0.028ms │ tmux.layout.parse - 0.096ms │ 0.093ms │ luasnip.nodes.restoreNode - 0.166ms │ 0.022ms │ tmux.configuration.validate - 0.100ms │ 0.088ms │ cmp.view.native_entries_view - 0.155ms │ 0.033ms │ plenary.tbl - 0.126ms │ 0.062ms │ lspconfig.server_configurations.sumneko_lua - 0.029ms │ 0.160ms │ cmp_buffer.buffer - 0.105ms │ 0.083ms │ cmp.utils.str - 0.162ms │ 0.025ms │ tmux.log.severity - 0.164ms │ 0.024ms │ tmux.wrapper.nvim - 0.107ms │ 0.081ms │ nvim-lsp-installer.ui.status-win.components.settings-schema - 0.021ms │ 0.167ms │ lewis6991.null-ls - 0.163ms │ 0.024ms │ tmux.configuration - 0.116ms │ 0.071ms │ nvim-treesitter.tsrange - 0.161ms │ 0.026ms │ tmux.log.channels - 0.094ms │ 0.091ms │ gitsigns.debug - 0.163ms │ 0.021ms │ plenary.vararg - 0.166ms │ 0.018ms │ tmux.version - 0.160ms │ 0.022ms │ tmux.configuration.logging - 0.155ms │ 0.026ms │ plenary.errors - 0.127ms │ 0.053ms │ nvim-treesitter - 0.094ms │ 0.085ms │ null-ls.info - 0.100ms │ 0.079ms │ cmp.config - 0.095ms │ 0.084ms │ null-ls.diagnostics - 0.055ms │ 0.123ms │ cmp_path - 0.139ms │ 0.038ms │ plenary.async.tests - 0.098ms │ 0.078ms │ null-ls.config - 0.100ms │ 0.076ms │ cmp.view.docs_view - 0.102ms │ 0.074ms │ cmp.utils.feedkeys - 0.089ms │ 0.085ms │ gitsigns.current_line_blame - 0.127ms │ 0.047ms │ null-ls - 0.107ms │ 0.066ms │ nvim-lsp-installer.installers - 0.095ms │ 0.078ms │ luasnip.util.mark - 0.106ms │ 0.066ms │ nvim-lsp-installer.fs - 0.142ms │ 0.030ms │ persistence.config - 0.100ms │ 0.070ms │ cmp.config.default - 0.078ms │ 0.091ms │ foldsigns - 0.120ms │ 0.048ms │ lua-dev - 0.113ms │ 0.053ms │ nvim-lsp-installer.ui - 0.029ms │ 0.138ms │ lewis6991.status - 0.118ms │ 0.047ms │ lspconfig - 0.113ms │ 0.051ms │ nvim-lsp-installer.jobs.outdated-servers - 0.105ms │ 0.058ms │ nvim-lsp-installer.installers.npm - 0.106ms │ 0.057ms │ nvim-lsp-installer.core.receipt - 0.101ms │ 0.061ms │ cmp.utils.char - 0.091ms │ 0.071ms │ gitsigns.signs - 0.097ms │ 0.065ms │ luasnip.nodes.util - 0.126ms │ 0.034ms │ treesitter-context.utils - 0.096ms │ 0.065ms │ lua-dev.config - 0.109ms │ 0.052ms │ nvim-lsp-installer.core.fetch - 0.103ms │ 0.055ms │ cmp.types.lsp - 0.099ms │ 0.059ms │ luasnip.nodes.functionNode - 0.090ms │ 0.067ms │ gitsigns.util - 0.110ms │ 0.047ms │ nvim-lsp-installer.jobs.outdated-servers.cargo - 0.096ms │ 0.061ms │ luasnip.config - 0.100ms │ 0.057ms │ cmp.utils.async - 0.101ms │ 0.055ms │ cmp.context - 0.091ms │ 0.064ms │ gitsigns.highlight - 0.094ms │ 0.061ms │ lua-dev.sumneko - 0.094ms │ 0.061ms │ gitsigns.subprocess - 0.067ms │ 0.088ms │ cmp_luasnip - 0.105ms │ 0.050ms │ nvim-lsp-installer.data - 0.105ms │ 0.049ms │ nvim-lsp-installer.installers.pip3 - 0.120ms │ 0.034ms │ lspconfig.server_configurations.bashls - 0.107ms │ 0.046ms │ nvim-lsp-installer.core.clients.github - 0.107ms │ 0.045ms │ nvim-lsp-installer.installers.shell - 0.099ms │ 0.053ms │ cmp.config.compare - 0.109ms │ 0.043ms │ lspconfig.server_configurations.clangd - 0.115ms │ 0.036ms │ lspconfig.server_configurations.vimls - 0.097ms │ 0.054ms │ luasnip.util.pattern_tokenizer - 0.097ms │ 0.053ms │ null-ls.helpers.make_builtin - 0.101ms │ 0.049ms │ cmp.utils.api - 0.118ms │ 0.032ms │ lspconfig.server_configurations.jedi_language_server - 0.106ms │ 0.043ms │ nvim-lsp-installer.jobs.outdated-servers.pip3 - 0.106ms │ 0.043ms │ nvim-lsp-installer.jobs.outdated-servers.gem - 0.108ms │ 0.040ms │ nvim-lsp-installer._generated.language_autocomplete_map - 0.104ms │ 0.043ms │ nvim-lsp-installer.installers.composer - 0.101ms │ 0.046ms │ cmp.config.mapping - 0.047ms │ 0.100ms │ cmp_nvim_lsp_signature_help - 0.109ms │ 0.037ms │ nvim-lsp-installer.servers.sumneko_lua - 0.115ms │ 0.028ms │ nvim-treesitter.caching - 0.096ms │ 0.047ms │ null-ls.state - 0.090ms │ 0.053ms │ gitsigns.debounce - 0.059ms │ 0.084ms │ cmp_tmux.tmux - 0.096ms │ 0.045ms │ null-ls.builtins.diagnostics.flake8 - 0.106ms │ 0.034ms │ nvim-lsp-installer.jobs.pool - 0.106ms │ 0.033ms │ nvim-lsp-installer.ui.status-win.server_hints - 0.105ms │ 0.034ms │ nvim-lsp-installer.installers.gem - 0.107ms │ 0.032ms │ nvim-lsp-installer.jobs.outdated-servers.npm - 0.106ms │ 0.031ms │ nvim-lsp-installer.jobs.outdated-servers.git - 0.114ms │ 0.022ms │ nvim-lsp-installer.servers.jedi_language_server - 0.105ms │ 0.031ms │ nvim-lsp-installer.jobs.outdated-servers.composer - 0.098ms │ 0.038ms │ null-ls.methods - 0.109ms │ 0.026ms │ nvim-lsp-installer.jobs.outdated-servers.version-check-result - 0.106ms │ 0.029ms │ nvim-lsp-installer.settings - 0.107ms │ 0.027ms │ cmp.utils.debug - 0.103ms │ 0.031ms │ cmp.types.cmp - 0.070ms │ 0.064ms │ diffview.events - 0.108ms │ 0.026ms │ nvim-lsp-installer.platform - 0.097ms │ 0.037ms │ null-ls.helpers.command_resolver - 0.104ms │ 0.029ms │ cmp.config.sources - 0.107ms │ 0.026ms │ nvim-lsp-installer.jobs.outdated-servers.github_release_file - 0.099ms │ 0.033ms │ cmp.utils.cache - 0.107ms │ 0.025ms │ nvim-lsp-installer.path - 0.101ms │ 0.030ms │ cmp.utils.autocmd - 0.097ms │ 0.034ms │ null-ls.logger - 0.100ms │ 0.031ms │ cmp.utils.event - 0.088ms │ 0.042ms │ gitsigns.cache - 0.103ms │ 0.027ms │ cmp.utils.pattern - 0.108ms │ 0.022ms │ nvim-lsp-installer.jobs.outdated-servers.jdtls - 0.103ms │ 0.027ms │ cmp.utils.buffer - 0.095ms │ 0.034ms │ luasnip.nodes.textNode - 0.096ms │ 0.033ms │ luasnip.util.dict - 0.108ms │ 0.021ms │ nvim-lsp-installer.servers.bashls - 0.108ms │ 0.021ms │ nvim-lsp-installer.ui.state - 0.110ms │ 0.018ms │ nvim-lsp-installer.servers.vimls - 0.101ms │ 0.027ms │ null-ls.helpers.range_formatting_args_factory - 0.057ms │ 0.071ms │ cmp_treesitter.lru - 0.105ms │ 0.022ms │ nvim-lsp-installer.dispatcher - 0.097ms │ 0.030ms │ luasnip.extras.filetype_functions - 0.103ms │ 0.024ms │ luasnip.session - 0.105ms │ 0.021ms │ nvim-lsp-installer.core.clients.crates - 0.105ms │ 0.021ms │ nvim-lsp-installer.jobs.outdated-servers.github_tag - 0.110ms │ 0.016ms │ cmp.types - 0.105ms │ 0.021ms │ nvim-lsp-installer.core.clients.eclipse - 0.105ms │ 0.021ms │ nvim-lsp-installer.notify - 0.089ms │ 0.036ms │ gitsigns.status - 0.096ms │ 0.029ms │ null-ls.builtins.diagnostics.teal - 0.097ms │ 0.027ms │ null-ls.builtins - 0.103ms │ 0.021ms │ cmp.types.vim - 0.060ms │ 0.062ms │ cmp_tmux.source - 0.100ms │ 0.022ms │ null-ls.helpers - 0.098ms │ 0.024ms │ null-ls.builtins.diagnostics.gitlint - 0.065ms │ 0.056ms │ cmp_treesitter - 0.024ms │ 0.097ms │ buftabline.buftab - 0.095ms │ 0.026ms │ null-ls.builtins.diagnostics.shellcheck - 0.095ms │ 0.026ms │ null-ls.builtins.diagnostics.luacheck - 0.097ms │ 0.021ms │ null-ls.helpers.formatter_factory - 0.097ms │ 0.022ms │ luasnip.util.events - 0.097ms │ 0.021ms │ luasnip.util.types - 0.096ms │ 0.022ms │ luasnip.util.functions - 0.037ms │ 0.078ms │ cmp_cmdline - 0.032ms │ 0.083ms │ cmp_buffer.source - 0.040ms │ 0.074ms │ lewis6991.cmp - 0.060ms │ 0.054ms │ cmp_treesitter.treesitter - 0.089ms │ 0.025ms │ gitsigns.message - 0.039ms │ 0.073ms │ cmp_nvim_lsp.source - 0.055ms │ 0.054ms │ buftabline.build - 0.026ms │ 0.083ms │ lewis6991.lsp - 0.051ms │ 0.055ms │ cmp_nvim_lua - 0.033ms │ 0.065ms │ cleanfold - 0.071ms │ 0.025ms │ cmp_tmux - 0.043ms │ 0.053ms │ cmp_nvim_lsp - 0.058ms │ 0.033ms │ cmp-spell - 0.043ms │ 0.037ms │ cmp_emoji - 0.029ms │ 0.049ms │ lewis6991.floating_man - 0.032ms │ 0.042ms │ cmp_buffer.timer - 0.024ms │ 0.050ms │ lewis6991.treesitter - 0.019ms │ 0.054ms │ lewis6991.cmp_gh - 0.025ms │ 0.046ms │ buftabline.buffers - 0.021ms │ 0.048ms │ lewis6991.telescope - 0.024ms │ 0.031ms │ buftabline - 0.035ms │ 0.019ms │ cmp_buffer - 0.019ms │ 0.035ms │ buftabline.utils - 0.021ms │ 0.030ms │ buftabline.highlights - 0.020ms │ 0.032ms │ buftabline.tabpage-tab - 0.019ms │ 0.030ms │ buftabline.options - 0.020ms │ 0.026ms │ buftabline.tabpages -────────────┴────────────┴───────────────────────────────────────────────────────────────── -``` -
- -Total resolve: 54.337ms, total load: 34.257ms - -
-With cache - -``` -────────────┬────────────┐ - Resolve │ Load │ -────────────┼────────────┼───────────────────────────────────────────────────────────────── - Time │ Time │ Module -────────────┼────────────┼───────────────────────────────────────────────────────────────── - 6.357ms │ 6.796ms │ Total -────────────┼────────────┼───────────────────────────────────────────────────────────────── - 0.041ms │ 2.021ms │ octo.writers - 0.118ms │ 0.160ms │ lewis6991.plugins - 0.050ms │ 0.144ms │ octo.date - 0.035ms │ 0.153ms │ octo.utils - 0.057ms │ 0.099ms │ octo.model.octo-buffer - 0.047ms │ 0.105ms │ packer - 0.058ms │ 0.080ms │ octo.colors - 0.121ms │ 0.015ms │ gitsigns.cache - 0.082ms │ 0.037ms │ packer.load - 0.107ms │ 0.008ms │ gitsigns.debounce - 0.048ms │ 0.064ms │ octo.config - 0.048ms │ 0.061ms │ octo.graphql - 0.049ms │ 0.051ms │ octo - 0.043ms │ 0.057ms │ vim.diagnostic - 0.085ms │ 0.013ms │ gitsigns.highlight - 0.065ms │ 0.032ms │ octo.base64 - 0.035ms │ 0.060ms │ vim.lsp - 0.056ms │ 0.035ms │ octo.gh - 0.045ms │ 0.045ms │ octo.mappings - 0.026ms │ 0.060ms │ octo.reviews - 0.037ms │ 0.045ms │ packer.plugin_utils - 0.030ms │ 0.049ms │ octo.reviews.file-panel - 0.018ms │ 0.056ms │ vim.lsp.util - 0.043ms │ 0.030ms │ packer.log - 0.036ms │ 0.032ms │ packer.util - 0.032ms │ 0.035ms │ octo.reviews.file-entry - 0.021ms │ 0.045ms │ packer_compiled - 0.052ms │ 0.014ms │ octo.model.body-metadata - 0.033ms │ 0.027ms │ octo.reviews.layout - 0.014ms │ 0.047ms │ nvim-treesitter.parsers - 0.035ms │ 0.024ms │ vim.lsp.handlers - 0.014ms │ 0.044ms │ nvim-lsp-installer.ui.status-win - 0.046ms │ 0.012ms │ octo.completion - 0.037ms │ 0.021ms │ octo.constants - 0.032ms │ 0.025ms │ lewis6991 - 0.040ms │ 0.017ms │ persistence - 0.030ms │ 0.026ms │ diffview.utils - 0.035ms │ 0.020ms │ packer.result - 0.015ms │ 0.040ms │ gitsigns.config - 0.031ms │ 0.024ms │ packer.async - 0.041ms │ 0.013ms │ vim.uri - 0.044ms │ 0.010ms │ octo.model.thread-metadata - 0.018ms │ 0.035ms │ gitsigns.debug - 0.023ms │ 0.030ms │ github_dark - 0.030ms │ 0.023ms │ packer.jobs - 0.039ms │ 0.013ms │ buftabline.build - 0.037ms │ 0.014ms │ octo.model.title-metadata - 0.025ms │ 0.025ms │ vim.lsp.buf - 0.022ms │ 0.027ms │ gitsigns - 0.027ms │ 0.022ms │ lewis6991.status - 0.016ms │ 0.032ms │ gitsigns.git - 0.026ms │ 0.020ms │ octo.window - 0.033ms │ 0.012ms │ octo.folds - 0.037ms │ 0.008ms │ trouble.providers.lsp - 0.016ms │ 0.028ms │ vim.lsp.protocol - 0.028ms │ 0.016ms │ octo.signs - 0.028ms │ 0.014ms │ null-ls - 0.027ms │ 0.014ms │ octo.reviews.renderer - 0.018ms │ 0.024ms │ trouble.view - 0.017ms │ 0.025ms │ luasnip.nodes.snippet - 0.023ms │ 0.018ms │ colorizer.nvim - 0.017ms │ 0.024ms │ vim.lsp._snippet - 0.015ms │ 0.025ms │ nvim-treesitter.install - 0.018ms │ 0.022ms │ plenary.async.structs - 0.018ms │ 0.021ms │ dirvish - 0.027ms │ 0.012ms │ octo.ui.bubbles - 0.019ms │ 0.020ms │ treesitter-context - 0.015ms │ 0.024ms │ vim.lsp.diagnostic - 0.016ms │ 0.023ms │ vim.lsp.rpc - 0.022ms │ 0.016ms │ trouble - 0.022ms │ 0.016ms │ null-ls.helpers.generator_factory - 0.020ms │ 0.017ms │ luasnip - 0.014ms │ 0.023ms │ plenary.job - 0.026ms │ 0.011ms │ lewis6991.cmp - 0.027ms │ 0.010ms │ trouble.providers - 0.022ms │ 0.014ms │ nvim-treesitter.query - 0.018ms │ 0.018ms │ vim.treesitter.highlighter - 0.017ms │ 0.018ms │ nvim-treesitter.shell_command_selectors - 0.014ms │ 0.021ms │ nvim-treesitter.configs - 0.025ms │ 0.010ms │ lewis6991.floating_man - 0.022ms │ 0.012ms │ vim.keymap - 0.013ms │ 0.021ms │ cmp.entry - 0.024ms │ 0.010ms │ lspconfig.server_configurations.bashls - 0.018ms │ 0.016ms │ gitsigns.hunks - 0.017ms │ 0.017ms │ gitsigns.status - 0.014ms │ 0.019ms │ cmp.core - 0.018ms │ 0.015ms │ spellsitter - 0.014ms │ 0.019ms │ colorizer - 0.024ms │ 0.009ms │ diffview.bootstrap - 0.016ms │ 0.016ms │ null-ls.utils - 0.021ms │ 0.011ms │ nvim-treesitter.info - 0.022ms │ 0.010ms │ vim.highlight - 0.016ms │ 0.016ms │ null-ls.info - 0.019ms │ 0.013ms │ cmp_path - 0.026ms │ 0.006ms │ cmp.utils.autocmd - 0.021ms │ 0.011ms │ foldsigns - 0.014ms │ 0.018ms │ lewis6991.null-ls - 0.018ms │ 0.013ms │ cmp.view - 0.017ms │ 0.014ms │ null-ls.client - 0.016ms │ 0.015ms │ gitsigns.manager - 0.013ms │ 0.018ms │ cmp.view.custom_entries_view - 0.015ms │ 0.015ms │ nvim-lsp-installer.ui.display - 0.020ms │ 0.010ms │ null-ls.methods - 0.016ms │ 0.014ms │ plenary.async.control - 0.019ms │ 0.011ms │ null-ls.diagnostics - 0.014ms │ 0.015ms │ luasnip.util.util - 0.017ms │ 0.013ms │ gitsigns.current_line_blame - 0.013ms │ 0.016ms │ buftabline.buftab - 0.015ms │ 0.015ms │ trouble.util - 0.015ms │ 0.015ms │ luasnip.config - 0.019ms │ 0.010ms │ plenary.async.async - 0.018ms │ 0.012ms │ nvim-treesitter.tsrange - 0.021ms │ 0.007ms │ cmp_nvim_lua - 0.014ms │ 0.015ms │ vim.treesitter.query - 0.015ms │ 0.014ms │ cmp.source - 0.014ms │ 0.015ms │ vim.treesitter.languagetree - 0.012ms │ 0.016ms │ nvim-lsp-installer._generated.filetype_map - 0.015ms │ 0.014ms │ nvim-lsp-installer.servers - 0.014ms │ 0.014ms │ lspconfig.util - 0.011ms │ 0.017ms │ cmp - 0.015ms │ 0.013ms │ cmp.view.wildmenu_entries_view - 0.021ms │ 0.007ms │ lspconfig.server_configurations.jedi_language_server - 0.015ms │ 0.013ms │ lua-dev - 0.018ms │ 0.010ms │ gitsigns.util - 0.014ms │ 0.014ms │ vim.lsp.codelens - 0.017ms │ 0.011ms │ plenary.async.util - 0.013ms │ 0.014ms │ null-ls.sources - 0.015ms │ 0.012ms │ nvim-treesitter.query_predicates - 0.013ms │ 0.015ms │ luasnip.nodes.choiceNode - 0.015ms │ 0.013ms │ null-ls.helpers.diagnostics - 0.017ms │ 0.011ms │ trouble.renderer - 0.015ms │ 0.013ms │ luasnip.nodes.node - 0.014ms │ 0.013ms │ lua-dev.sumneko - 0.013ms │ 0.014ms │ cmp.utils.window - 0.021ms │ 0.006ms │ treesitter-context.utils - 0.018ms │ 0.009ms │ cleanfold - 0.015ms │ 0.012ms │ nvim-treesitter.ts_utils - 0.012ms │ 0.015ms │ nvim-lsp-installer.installers.std - 0.015ms │ 0.012ms │ nvim-lsp-installer.server - 0.014ms │ 0.012ms │ lewis6991.lsp - 0.016ms │ 0.011ms │ gitsigns.signs - 0.020ms │ 0.006ms │ buftabline - 0.019ms │ 0.007ms │ plenary.tbl - 0.013ms │ 0.013ms │ nvim-lsp-installer - 0.018ms │ 0.008ms │ plenary - 0.015ms │ 0.010ms │ cmp_luasnip - 0.019ms │ 0.007ms │ null-ls.logger - 0.016ms │ 0.010ms │ vim.lsp.sync - 0.016ms │ 0.010ms │ spaceless - 0.017ms │ 0.009ms │ gitsigns.subprocess - 0.016ms │ 0.009ms │ plenary.functional - 0.016ms │ 0.010ms │ buftabline.buffers - 0.016ms │ 0.009ms │ vim.lsp.log - 0.019ms │ 0.006ms │ cmp_tmux - 0.013ms │ 0.012ms │ luasnip.nodes.dynamicNode - 0.017ms │ 0.008ms │ vim.treesitter - 0.013ms │ 0.013ms │ nvim-lsp-installer.process - 0.013ms │ 0.012ms │ luasnip.util.environ - 0.015ms │ 0.009ms │ lewis6991.treesitter - 0.015ms │ 0.010ms │ null-ls.config - 0.019ms │ 0.006ms │ ts_context_commentstring - 0.013ms │ 0.012ms │ cmp_buffer.buffer - 0.018ms │ 0.007ms │ null-ls.builtins.diagnostics.shellcheck - 0.015ms │ 0.010ms │ null-ls.helpers.make_builtin - 0.012ms │ 0.012ms │ diffview.path - 0.016ms │ 0.008ms │ null-ls.builtins.diagnostics.gitlint - 0.017ms │ 0.007ms │ trouble.providers.telescope - 0.013ms │ 0.011ms │ diffview.oop - 0.015ms │ 0.010ms │ cmp-rg - 0.013ms │ 0.011ms │ cmp.utils.keymap - 0.014ms │ 0.011ms │ nvim-treesitter - 0.018ms │ 0.007ms │ cmp.utils.highlight - 0.016ms │ 0.008ms │ lspconfig.server_configurations.sumneko_lua - 0.015ms │ 0.009ms │ colorizer.trie - 0.016ms │ 0.007ms │ plenary.vararg.rotate - 0.015ms │ 0.009ms │ trouble.config - 0.011ms │ 0.012ms │ lspconfig.configs - 0.014ms │ 0.009ms │ null-ls.helpers.command_resolver - 0.016ms │ 0.007ms │ cmp_tmux.source - 0.016ms │ 0.007ms │ lspconfig - 0.017ms │ 0.006ms │ plenary.vararg - 0.012ms │ 0.011ms │ nvim-lsp-installer.installers.context - 0.014ms │ 0.009ms │ cmp.view.native_entries_view - 0.014ms │ 0.009ms │ cmp.config.default - 0.017ms │ 0.006ms │ tmux.version.parse - 0.016ms │ 0.007ms │ gitsigns.message - 0.017ms │ 0.006ms │ persistence.config - 0.013ms │ 0.010ms │ cmp_nvim_lsp_signature_help - 0.012ms │ 0.010ms │ cmp.view.docs_view - 0.017ms │ 0.006ms │ cmp.config.sources - 0.013ms │ 0.009ms │ luasnip.nodes.restoreNode - 0.014ms │ 0.009ms │ vim.ui - 0.013ms │ 0.010ms │ luasnip.nodes.insertNode - 0.013ms │ 0.010ms │ null-ls.state - 0.014ms │ 0.008ms │ lspconfig.server_configurations.vimls - 0.016ms │ 0.006ms │ plenary.errors - 0.014ms │ 0.008ms │ null-ls.builtins.diagnostics.flake8 - 0.016ms │ 0.006ms │ null-ls.helpers - 0.015ms │ 0.008ms │ null-ls.builtins.diagnostics.luacheck - 0.014ms │ 0.008ms │ luasnip.util.mark - 0.015ms │ 0.008ms │ cmp.utils.buffer - 0.012ms │ 0.010ms │ nvim-lsp-installer.log - 0.015ms │ 0.007ms │ luasnip.nodes.util - 0.015ms │ 0.007ms │ null-ls.builtins.diagnostics.teal - 0.016ms │ 0.006ms │ null-ls.helpers.range_formatting_args_factory - 0.012ms │ 0.010ms │ nvim-treesitter.utils - 0.015ms │ 0.007ms │ cmp.utils.event - 0.013ms │ 0.009ms │ tmux.wrapper.tmux - 0.015ms │ 0.007ms │ nvim-treesitter-playground - 0.012ms │ 0.010ms │ cmp_buffer.source - 0.015ms │ 0.007ms │ cmp_treesitter - 0.013ms │ 0.009ms │ luasnip.util.parser - 0.015ms │ 0.006ms │ trouble.providers.qf - 0.014ms │ 0.008ms │ lewis6991.telescope - 0.014ms │ 0.007ms │ cmp_tmux.tmux - 0.014ms │ 0.007ms │ cmp_nvim_lsp.source - 0.015ms │ 0.006ms │ plenary.reload - 0.014ms │ 0.008ms │ buftabline.highlights - 0.015ms │ 0.006ms │ trouble.providers.diagnostic - 0.015ms │ 0.007ms │ nvim-lsp-installer.core.clients.github - 0.014ms │ 0.007ms │ nvim-lsp-installer.installers.shell - 0.016ms │ 0.005ms │ cmp-spell - 0.014ms │ 0.007ms │ null-ls.builtins - 0.013ms │ 0.008ms │ cmp_treesitter.lru - 0.016ms │ 0.005ms │ buftabline.tabpages - 0.015ms │ 0.006ms │ buftabline.options - 0.016ms │ 0.005ms │ lua-dev.config - 0.015ms │ 0.006ms │ nvim-lsp-installer.jobs.outdated-servers.cargo - 0.014ms │ 0.007ms │ diffview.events - 0.013ms │ 0.008ms │ nvim-lsp-installer.fs - 0.013ms │ 0.008ms │ cmp.utils.feedkeys - 0.013ms │ 0.007ms │ nvim-treesitter.caching - 0.013ms │ 0.008ms │ nvim-lsp-installer._generated.language_autocomplete_map - 0.013ms │ 0.007ms │ cmp.view.ghost_text_view - 0.013ms │ 0.008ms │ cmp_nvim_lsp - 0.013ms │ 0.007ms │ luasnip.nodes.functionNode - 0.013ms │ 0.007ms │ nvim-lsp-installer.jobs.outdated-servers - 0.012ms │ 0.008ms │ nvim-lsp-installer.ui.status-win.components.settings-schema - 0.012ms │ 0.009ms │ lewis6991.cmp_gh - 0.015ms │ 0.006ms │ luasnip.util.dict - 0.013ms │ 0.007ms │ plenary.async - 0.014ms │ 0.006ms │ nvim-lsp-installer.installers.composer - 0.013ms │ 0.007ms │ cmp_treesitter.treesitter - 0.014ms │ 0.006ms │ nvim-lsp-installer.jobs.outdated-servers.gem - 0.015ms │ 0.005ms │ nvim-lsp-installer.platform - 0.014ms │ 0.006ms │ buftabline.utils - 0.013ms │ 0.007ms │ trouble.text - 0.011ms │ 0.008ms │ cmp.config - 0.013ms │ 0.006ms │ trouble.colors - 0.012ms │ 0.007ms │ cmp.utils.misc - 0.012ms │ 0.008ms │ nvim-lsp-installer.installers.npm - 0.013ms │ 0.007ms │ lspconfig.server_configurations.clangd - 0.012ms │ 0.007ms │ cmp_cmdline - 0.011ms │ 0.008ms │ cmp.types.lsp - 0.014ms │ 0.006ms │ vim.treesitter.language - 0.014ms │ 0.006ms │ cmp.config.mapping - 0.015ms │ 0.004ms │ luasnip.util.events - 0.014ms │ 0.005ms │ luasnip.extras.filetype_functions - 0.012ms │ 0.007ms │ cmp.utils.async - 0.012ms │ 0.007ms │ cmp.config.compare - 0.013ms │ 0.005ms │ cmp_emoji - 0.015ms │ 0.004ms │ cmp_buffer - 0.011ms │ 0.007ms │ nvim-lsp-installer.core.receipt - 0.012ms │ 0.007ms │ nvim-lsp-installer.ui - 0.013ms │ 0.006ms │ cmp.utils.api - 0.012ms │ 0.007ms │ nvim-lsp-installer.core.fetch - 0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.pool - 0.011ms │ 0.007ms │ nvim-lsp-installer.installers - 0.012ms │ 0.007ms │ nvim-lsp-installer.data - 0.013ms │ 0.006ms │ cmp.matcher - 0.014ms │ 0.005ms │ tmux - 0.011ms │ 0.008ms │ tmux.copy - 0.013ms │ 0.005ms │ luasnip.util.types - 0.014ms │ 0.004ms │ nvim-lsp-installer.servers.jedi_language_server - 0.014ms │ 0.004ms │ nvim-lsp-installer.servers.vimls - 0.014ms │ 0.004ms │ cmp.utils.cache - 0.013ms │ 0.006ms │ luasnip.util.pattern_tokenizer - 0.012ms │ 0.006ms │ luasnip.nodes.textNode - 0.013ms │ 0.005ms │ null-ls.helpers.formatter_factory - 0.013ms │ 0.006ms │ plenary.async.tests - 0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.version-check-result - 0.012ms │ 0.005ms │ nvim-lsp-installer.settings - 0.011ms │ 0.006ms │ cmp.context - 0.011ms │ 0.006ms │ cmp.utils.str - 0.013ms │ 0.004ms │ luasnip.session - 0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.composer - 0.012ms │ 0.006ms │ nvim-lsp-installer.servers.sumneko_lua - 0.012ms │ 0.005ms │ cmp_buffer.timer - 0.011ms │ 0.006ms │ cmp.utils.char - 0.013ms │ 0.004ms │ cmp.utils.pattern - 0.011ms │ 0.006ms │ nvim-lsp-installer.installers.pip3 - 0.013ms │ 0.004ms │ luasnip.util.functions - 0.013ms │ 0.005ms │ tmux.log.channels - 0.012ms │ 0.005ms │ tmux.navigation - 0.013ms │ 0.005ms │ trouble.folds - 0.012ms │ 0.005ms │ nvim-lsp-installer.ui.status-win.server_hints - 0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.pip3 - 0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.npm - 0.011ms │ 0.006ms │ cmp.utils.debug - 0.013ms │ 0.004ms │ nvim-lsp-installer.notify - 0.011ms │ 0.006ms │ tmux.layout - 0.013ms │ 0.004ms │ nvim-lsp-installer.servers.bashls - 0.012ms │ 0.004ms │ nvim-lsp-installer.dispatcher - 0.012ms │ 0.005ms │ buftabline.tabpage-tab - 0.012ms │ 0.005ms │ nvim-lsp-installer.path - 0.010ms │ 0.006ms │ tmux.resize - 0.013ms │ 0.004ms │ cmp.types.vim - 0.012ms │ 0.004ms │ nvim-lsp-installer.ui.state - 0.011ms │ 0.005ms │ nvim-lsp-installer.installers.gem - 0.012ms │ 0.005ms │ tmux.configuration.options - 0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.git - 0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.github_release_file - 0.012ms │ 0.005ms │ cmp.types.cmp - 0.013ms │ 0.004ms │ cmp.types - 0.011ms │ 0.005ms │ tmux.log - 0.011ms │ 0.005ms │ tmux.navigation.navigate - 0.012ms │ 0.005ms │ tmux.configuration - 0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.github_tag - 0.011ms │ 0.005ms │ tmux.layout.parse - 0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.jdtls - 0.011ms │ 0.005ms │ tmux.log.convert - 0.011ms │ 0.005ms │ tmux.log.severity - 0.011ms │ 0.004ms │ tmux.version - 0.012ms │ 0.004ms │ nvim-lsp-installer.core.clients.eclipse - 0.011ms │ 0.004ms │ nvim-lsp-installer.core.clients.crates - 0.011ms │ 0.004ms │ tmux.configuration.logging - 0.011ms │ 0.004ms │ tmux.wrapper.nvim - 0.011ms │ 0.004ms │ tmux.configuration.validate - 0.011ms │ 0.004ms │ tmux.keymaps -────────────┴────────────┴───────────────────────────────────────────────────────────────── -``` - -
- -Total resolve: 6.357ms, total load: 6.796ms - -## Relevant Neovim PR's - -[libs: vendor libmpack and libmpack-lua](https://github.com/neovim/neovim/pull/15566) [merged] - -[fix(vim.mpack): rename pack/unpack => encode/decode](https://github.com/neovim/neovim/pull/16175) [merged] - -[fix(runtime): add compressed representation to &rtp](https://github.com/neovim/neovim/pull/15867) [merged] - -[fix(runtime): don't use regexes inside lua require'mod'](https://github.com/neovim/neovim/pull/15973) [merged] - -[fix(lua): restore priority of the preloader](https://github.com/neovim/neovim/pull/17302) [merged] - -[refactor(lua): call loadfile internally instead of luaL_loadfile](https://github.com/neovim/neovim/pull/17200) [merged] - -[feat(lua): startup profiling](https://github.com/neovim/neovim/pull/15436) - -## Credit - -All credit goes to @bfredl who implemented the majority of this plugin in https://github.com/neovim/neovim/pull/15436. diff --git a/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient.lua b/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient.lua deleted file mode 100644 index af6925c..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient.lua +++ /dev/null @@ -1,465 +0,0 @@ -local vim = vim -local api = vim.api -local uv = vim.loop -local _loadfile = loadfile -local get_runtime = api.nvim__get_runtime -local fs_stat = uv.fs_stat -local mpack = vim.mpack -local loadlib = package.loadlib - -local std_cache = vim.fn.stdpath('cache') - -local sep = vim.loop.os_uname().sysname:match('Windows') and '\\' or '/' - -local std_dirs = { - [''] = os.getenv('APPDIR'), - [''] = os.getenv('VIMRUNTIME'), - [''] = vim.fn.stdpath('data'), - [''] = vim.fn.stdpath('config'), -} - -local function modpath_mangle(modpath) - for name, dir in pairs(std_dirs) do - modpath = modpath:gsub(dir, name) - end - return modpath -end - -local function modpath_unmangle(modpath) - for name, dir in pairs(std_dirs) do - modpath = modpath:gsub(name, dir) - end - return modpath -end - --- Overridable by user -local default_config = { - chunks = { - enable = true, - path = std_cache .. sep .. 'luacache_chunks', - }, - modpaths = { - enable = true, - path = std_cache.. sep .. 'luacache_modpaths', - }, -} - --- State used internally -local default_state = { - chunks = { - cache = {}, - profile = nil, - dirty = false, - get = function(self, path) - return self.cache[modpath_mangle(path)] - end, - set = function(self, path, chunk) - self.cache[modpath_mangle(path)] = chunk - end - }, - modpaths = { - cache = {}, - profile = nil, - dirty = false, - get = function(self, mod) - if self.cache[mod] then - return modpath_unmangle(self.cache[mod]) - end - end, - set = function(self, mod, path) - self.cache[mod] = modpath_mangle(path) - end - }, - log = {} -} - ----@diagnostic disable-next-line: undefined-field -local M = vim.tbl_deep_extend('keep', _G.__luacache_config or {}, default_config, default_state) -_G.__luacache = M - -local function log(...) - M.log[#M.log+1] = table.concat({string.format(...)}, ' ') -end - -local function print_log() - for _, l in ipairs(M.log) do - print(l) - end -end - -local function hash(modpath) - local stat = fs_stat(modpath) - if stat then - return stat.mtime.sec..stat.mtime.nsec..stat.size - end - error('Could not hash '..modpath) -end - -local function profile(m, entry, name, loader) - if m.profile then - local mp = m.profile - mp[entry] = mp[entry] or {} - if not mp[entry].loader and loader then - mp[entry].loader = loader - end - if not mp[entry][name] then - mp[entry][name] = uv.hrtime() - end - end -end - -local function mprofile(mod, name, loader) - profile(M.modpaths, mod, name, loader) -end - -local function cprofile(path, name, loader) - if M.chunks.profile then - path = modpath_mangle(path) - end - profile(M.chunks, path, name, loader) -end - -function M.enable_profile() - local P = require('impatient.profile') - - M.chunks.profile = {} - M.modpaths.profile = {} - - loadlib = function(path, fun) - cprofile(path, 'load_start') - local f, err = package.loadlib(path, fun) - cprofile(path, 'load_end', 'standard') - return f, err - end - - P.setup(M.modpaths.profile) - - api.nvim_create_user_command('LuaCacheProfile', function() - P.print_profile(M, std_dirs) - end, {}) -end - -local function get_runtime_file_from_parent(basename, paths) - -- Look in the cache to see if we have already loaded a parent module. - -- If we have then try looking in the parents directory first. - local parents = vim.split(basename, sep) - for i = #parents, 1, -1 do - local parent = table.concat(vim.list_slice(parents, 1, i), sep) - local ppath = M.modpaths:get(parent) - if ppath then - if (ppath:sub(-9) == (sep .. 'init.lua')) then - ppath = ppath:sub(1, -10) -- a/b/init.lua -> a/b - else - ppath = ppath:sub(1, -5) -- a/b.lua -> a/b - end - - for _, path in ipairs(paths) do - -- path should be of form 'a/b/c.lua' or 'a/b/c/init.lua' - local modpath = ppath..sep..path:sub(#('lua'..sep..parent)+2) - if fs_stat(modpath) then - return modpath, 'cache(p)' - end - end - end - end -end - -local rtp = vim.split(vim.o.rtp, ',') - --- Make sure modpath is in rtp and that modpath is in paths. -local function validate_modpath(modpath, paths) - local match = false - for _, p in ipairs(paths) do - if vim.endswith(modpath, p) then - match = true - break - end - end - if not match then - return false - end - for _, dir in ipairs(rtp) do - if vim.startswith(modpath, dir) then - return fs_stat(modpath) ~= nil - end - end - return false -end - -local function get_runtime_file_cached(basename, paths) - local modpath, loader - local mp = M.modpaths - if mp.enable then - local modpath_cached = mp:get(basename) - if modpath_cached then - modpath, loader = modpath_cached, 'cache' - else - modpath, loader = get_runtime_file_from_parent(basename, paths) - end - - if modpath and not validate_modpath(modpath, paths) then - modpath = nil - - -- Invalidate - mp.cache[basename] = nil - mp.dirty = true - end - end - - if not modpath then - -- What Neovim does by default; slowest - modpath, loader = get_runtime(paths, false, {is_lua=true})[1], 'standard' - end - - if modpath then - mprofile(basename, 'resolve_end', loader) - if mp.enable and loader ~= 'cache' then - log('Creating cache for module %s', basename) - mp:set(basename, modpath) - mp.dirty = true - end - end - - return modpath -end - -local function extract_basename(pats) - local basename - - -- Deconstruct basename from pats - for _, pat in ipairs(pats) do - for i, npat in ipairs{ - -- Ordered by most specific - 'lua'.. sep ..'(.*)'..sep..'init%.lua', - 'lua'.. sep ..'(.*)%.lua' - } do - local m = pat:match(npat) - if i == 2 and m and m:sub(-4) == 'init' then - m = m:sub(0, -6) - end - if not basename then - if m then - basename = m - end - elseif m and m ~= basename then - -- matches are inconsistent - return - end - end - end - - return basename -end - -local function get_runtime_cached(pats, all, opts) - local fallback = false - if all or not opts or not opts.is_lua then - -- Fallback - fallback = true - end - - local basename - - if not fallback then - basename = extract_basename(pats) - end - - if fallback or not basename then - return get_runtime(pats, all, opts) - end - - return {get_runtime_file_cached(basename, pats)} -end - --- Copied from neovim/src/nvim/lua/vim.lua with two lines changed -local function load_package(name) - local basename = name:gsub('%.', sep) - local paths = {"lua"..sep..basename..".lua", "lua"..sep..basename..sep.."init.lua"} - - -- Original line: - -- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) - local found = {get_runtime_file_cached(basename, paths)} - if #found > 0 then - local f, err = loadfile(found[1]) - return f or error(err) - end - - local so_paths = {} - for _,trail in ipairs(vim._so_trails) do - local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash - table.insert(so_paths, path) - end - - -- Original line: - -- found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true}) - found = {get_runtime_file_cached(basename, so_paths)} - if #found > 0 then - -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is - -- a) strip prefix up to and including the first dash, if any - -- b) replace all dots by underscores - -- c) prepend "luaopen_" - -- So "foo-bar.baz" should result in "luaopen_bar_baz" - local dash = name:find("-", 1, true) - local modname = dash and name:sub(dash + 1) or name - local f, err = loadlib(found[1], "luaopen_"..modname:gsub("%.", "_")) - return f or error(err) - end - return nil -end - -local function load_from_cache(path) - local mc = M.chunks - - local cache = mc:get(path) - - if not cache then - return nil, string.format('No cache for path %s', path) - end - - local mhash, codes = unpack(cache) - - if mhash ~= hash(path) then - mc:set(path) - mc.dirty = true - return nil, string.format('Stale cache for path %s', path) - end - - local chunk = loadstring(codes) - - if not chunk then - mc:set(path) - mc.dirty = true - return nil, string.format('Cache error for path %s', path) - end - - return chunk -end - -local function loadfile_cached(path) - cprofile(path, 'load_start') - - local chunk, err - - if M.chunks.enable then - chunk, err = load_from_cache(path) - if chunk and not err then - log('Loaded cache for path %s', path) - cprofile(path, 'load_end', 'cache') - return chunk - end - log(err) - end - - chunk, err = _loadfile(path) - - if not err and M.chunks.enable then - log('Creating cache for path %s', path) - M.chunks:set(path, {hash(path), string.dump(chunk)}) - M.chunks.dirty = true - end - - cprofile(path, 'load_end', 'standard') - return chunk, err -end - -function M.save_cache() - local function _save_cache(t) - if not t.enable then - return - end - if t.dirty then - log('Updating chunk cache file: %s', t.path) - local f = assert(io.open(t.path, 'w+b')) - f:write(mpack.encode(t.cache)) - f:flush() - t.dirty = false - end - end - _save_cache(M.chunks) - _save_cache(M.modpaths) -end - -local function clear_cache() - local function _clear_cache(t) - t.cache = {} - os.remove(t.path) - end - _clear_cache(M.chunks) - _clear_cache(M.modpaths) -end - -local function init_cache() - local function _init_cache(t) - if not t.enable then - return - end - if fs_stat(t.path) then - log('Loading cache file %s', t.path) - local f = assert(io.open(t.path, 'rb')) - local ok - ok, t.cache = pcall(function() - return mpack.decode(f:read'*a') - end) - - if not ok then - log('Corrupted cache file, %s. Invalidating...', t.path) - os.remove(t.path) - t.cache = {} - end - t.dirty = not ok - end - end - - if not uv.fs_stat(std_cache) then - vim.fn.mkdir(std_cache, 'p') - end - - _init_cache(M.chunks) - _init_cache(M.modpaths) -end - -local function setup() - init_cache() - - -- Usual package loaders - -- 1. package.preload - -- 2. vim._load_package - -- 3. package.path - -- 4. package.cpath - -- 5. all-in-one - - -- Override default functions - for i, loader in ipairs(package.loaders) do - if loader == vim._load_package then - package.loaders[i] = load_package - break - end - end - vim._load_package = load_package - - vim.api.nvim__get_runtime = get_runtime_cached - loadfile = loadfile_cached - - local augroup = api.nvim_create_augroup('impatient', {}) - - api.nvim_create_user_command('LuaCacheClear', clear_cache, {}) - api.nvim_create_user_command('LuaCacheLog' , print_log , {}) - - api.nvim_create_autocmd({'VimEnter', 'VimLeave'}, { - group = augroup, - callback = M.save_cache - }) - - api.nvim_create_autocmd('OptionSet', { - group = augroup, - pattern = 'runtimepath', - callback = function() - rtp = vim.split(vim.o.rtp, ',') - end - }) - -end - -setup() - -return M diff --git a/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient/profile.lua b/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient/profile.lua deleted file mode 100644 index 0be386d..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/lua/impatient/profile.lua +++ /dev/null @@ -1,248 +0,0 @@ -local M = {} - -local sep = vim.loop.os_uname().sysname:match('Windows') and '\\' or '/' - -local api, uv = vim.api, vim.loop - -local function load_buffer(title, lines) - local bufnr = api.nvim_create_buf(false, false) - api.nvim_buf_set_lines(bufnr, 0, 0, false, lines) - api.nvim_buf_set_option(bufnr, 'bufhidden', 'wipe') - api.nvim_buf_set_option(bufnr, 'buftype', 'nofile') - api.nvim_buf_set_option(bufnr, 'swapfile', false) - api.nvim_buf_set_option(bufnr, "modifiable", false) - api.nvim_buf_set_name(bufnr, title) - api.nvim_set_current_buf(bufnr) -end - -local function time_tostr(x) - if x == 0 then - return '?' - end - return string.format('%8.3fms', x) -end - -local function mem_tostr(x) - local unit = '' - for _, u in ipairs{'K', 'M', 'G'} do - if x < 1000 then - break - end - x = x / 1000 - unit = u - end - return string.format('%1.1f%s', x, unit) -end - -function M.print_profile(I, std_dirs) - local mod_profile = I.modpaths.profile - local chunk_profile = I.chunks.profile - - if not mod_profile and not chunk_profile then - print('Error: profiling was not enabled') - return - end - - local total_resolve = 0 - local total_load = 0 - local modules = {} - - for path, m in pairs(chunk_profile) do - m.load = m.load_end - m.load_start - m.load = m.load / 1000000 - m.path = path or '?' - end - - local module_content_width = 0 - - local unloaded = {} - - for module, m in pairs(mod_profile) do - local module_dot = module:gsub(sep, '.') - m.module = module_dot - - if not package.loaded[module_dot] and not package.loaded[module] then - unloaded[#unloaded+1] = m - else - m.resolve = 0 - if m.resolve_start and m.resolve_end then - m.resolve = m.resolve_end - m.resolve_start - m.resolve = m.resolve / 1000000 - end - - m.loader = m.loader or m.loader_guess - - local path = I.modpaths.cache[module] - local path_prof = chunk_profile[path] - m.path = path or '?' - - if path_prof then - chunk_profile[path] = nil - m.load = path_prof.load - m.ploader = path_prof.loader - else - m.load = 0 - m.ploader = 'NA' - end - - total_resolve = total_resolve + m.resolve - total_load = total_load + m.load - - if #module > module_content_width then - module_content_width = #module - end - - modules[#modules+1] = m - end - end - - table.sort(modules, function(a, b) - return (a.resolve + a.load) > (b.resolve + b.load) - end) - - local paths = {} - - local total_paths_load = 0 - for _, m in pairs(chunk_profile) do - paths[#paths+1] = m - total_paths_load = total_paths_load + m.load - end - - table.sort(paths, function(a, b) - return a.load > b.load - end) - - - local lines = {} - local function add(fmt, ...) - local args = {...} - for i, a in ipairs(args) do - if type(a) == 'number' then - args[i] = time_tostr(a) - end - end - - lines[#lines+1] = string.format(fmt, unpack(args)) - end - - local time_cell_width = 12 - local loader_cell_width = 11 - local time_content_width = time_cell_width - 2 - local loader_content_width = loader_cell_width - 2 - local module_cell_width = module_content_width + 2 - - local tcwl = string.rep('─', time_cell_width) - local lcwl = string.rep('─', loader_cell_width) - local mcwl = string.rep('─', module_cell_width+2) - - local n = string.rep('─', 200) - - local module_cell_format = '%-'..module_cell_width..'s' - local loader_format = '%-'..loader_content_width..'s' - local line_format = '%s │ %s │ %s │ %s │ %s │ %s' - - local row_fmt = line_format:format( - ' %'..time_content_width..'s', - loader_format, - '%'..time_content_width..'s', - loader_format, - module_cell_format, - '%s') - - local title_fmt = line_format:format( - ' %-'..time_content_width..'s', - loader_format, - '%-'..time_content_width..'s', - loader_format, - module_cell_format, - '%s') - - local title1_width = time_cell_width+loader_cell_width-1 - local title1_fmt = ('%s │ %s │'):format( - ' %-'..title1_width..'s', '%-'..title1_width..'s') - - add('Note: this report is not a measure of startup time. Only use this for comparing') - add('between cached and uncached loads of Lua modules') - add('') - - add('Cache files:') - for _, f in ipairs{ I.chunks.path, I.modpaths.path } do - local size = vim.loop.fs_stat(f).size - add(' %s %s', f, mem_tostr(size)) - end - add('') - - add('Standard directories:') - for alias, path in pairs(std_dirs) do - add(' %-12s -> %s', alias, path) - end - add('') - - add('%s─%s┬%s─%s┐', tcwl, lcwl, tcwl, lcwl) - add(title1_fmt, 'Resolve', 'Load') - add('%s┬%s┼%s┬%s┼%s┬%s', tcwl, lcwl, tcwl, lcwl, mcwl, n) - add(title_fmt, 'Time', 'Method', 'Time', 'Method', 'Module', 'Path') - add('%s┼%s┼%s┼%s┼%s┼%s', tcwl, lcwl, tcwl, lcwl, mcwl, n) - add(row_fmt, total_resolve, '', total_load, '', 'Total', '') - add('%s┼%s┼%s┼%s┼%s┼%s', tcwl, lcwl, tcwl, lcwl, mcwl, n) - for _, p in ipairs(modules) do - add(row_fmt, p.resolve, p.loader, p.load, p.ploader, p.module, p.path) - end - add('%s┴%s┴%s┴%s┴%s┴%s', tcwl, lcwl, tcwl, lcwl, mcwl, n) - - if #paths > 0 then - add('') - add(n) - local f3 = ' %'..time_content_width..'s │ %'..loader_content_width..'s │ %s' - add('Files loaded with no associated module') - add('%s┬%s┬%s', tcwl, lcwl, n) - add(f3, 'Time', 'Loader', 'Path') - add('%s┼%s┼%s', tcwl, lcwl, n) - add(f3, total_paths_load, '', 'Total') - add('%s┼%s┼%s', tcwl, lcwl, n) - for _, p in ipairs(paths) do - add(f3, p.load, p.loader, p.path) - end - add('%s┴%s┴%s', tcwl, lcwl, n) - end - - if #unloaded > 0 then - add('') - add(n) - add('Modules which were unable to loaded') - add(n) - for _, p in ipairs(unloaded) do - lines[#lines+1] = p.module - end - add(n) - end - - load_buffer('Impatient Profile Report', lines) -end - -M.setup = function(profile) - local _require = require - - require = function(mod) - local basename = mod:gsub('%.', sep) - if not profile[basename] then - profile[basename] = {} - profile[basename].resolve_start = uv.hrtime() - profile[basename].loader_guess = '' - end - return _require(mod) - end - - -- Add profiling around all the loaders - local pl = package.loaders - for i = 1, #pl do - local l = pl[i] - pl[i] = function(mod) - local basename = mod:gsub('%.', sep) - profile[basename].loader_guess = i == 1 and 'preloader' or 'loader #'..i - return l(mod) - end - end -end - -return M diff --git a/etc/soft/nvim/+plugins/impatient.nvim/test/impatient_spec.lua b/etc/soft/nvim/+plugins/impatient.nvim/test/impatient_spec.lua deleted file mode 100644 index 639a57c..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/test/impatient_spec.lua +++ /dev/null @@ -1,280 +0,0 @@ -local helpers = require('test.functional.helpers')() - -local clear = helpers.clear -local exec_lua = helpers.exec_lua -local eq = helpers.eq -local cmd = helpers.command - -local nvim07 - -local function gen_exp(exp) - local neovim_dir = nvim07 and 'neovim-v0.7.0' or 'neovim-master' - local cwd = exec_lua('return vim.loop.cwd()') - - local exp1 = {} - for _, v in pairs(exp) do - if type(v) == 'string' then - v = v:gsub('{CWD}', cwd) - v = v:gsub('{NVIM}', neovim_dir) - exp1[#exp1+1] = v - end - end - - return exp1 -end - -local gen_exp_cold = function() - return gen_exp{ - 'Creating cache for module plugins', - 'No cache for path ./test/lua/plugins.lua', - 'Creating cache for path ./test/lua/plugins.lua', - 'Creating cache for module telescope', - 'No cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua', - 'Creating cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua', - 'Creating cache for module telescope/_extensions', - 'No cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua', - 'Creating cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua', - 'Creating cache for module gitsigns', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua', - 'Creating cache for module plenary/async/async', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua', - 'Creating cache for module plenary/vararg', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua', - 'Creating cache for module plenary/vararg/rotate', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua', - 'Creating cache for module plenary/tbl', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua', - 'Creating cache for module plenary/errors', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua', - 'Creating cache for module plenary/functional', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua', - 'Creating cache for module plenary/async/util', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua', - 'Creating cache for module plenary/async/control', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua', - 'Creating cache for module plenary/async/structs', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua', - 'Creating cache for module gitsigns/status', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua', - 'Creating cache for module gitsigns/git', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua', - 'Creating cache for module plenary/job', - 'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua', - 'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua', - 'Creating cache for module gitsigns/debug', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua', - 'Creating cache for module gitsigns/util', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua', - 'Creating cache for module gitsigns/hunks', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua', - 'Creating cache for module gitsigns/signs', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua', - 'Creating cache for module gitsigns/config', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua', - 'Creating cache for module gitsigns/manager', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua', - 'Creating cache for module gitsigns/cache', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua', - 'Creating cache for module gitsigns/debounce', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua', - 'Creating cache for module gitsigns/highlight', - 'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua', - 'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua', - 'Creating cache for module spellsitter', - 'No cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua', - 'Creating cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua', - 'Creating cache for module vim/treesitter/query', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua', - 'Creating cache for module vim/treesitter/language', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua', - 'Creating cache for module vim/treesitter', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua', - 'Creating cache for module vim/treesitter/languagetree', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua', - 'Creating cache for module colorizer', - 'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua', - 'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua', - 'Creating cache for module colorizer/nvim', - 'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua', - 'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua', - 'Creating cache for module colorizer/trie', - 'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua', - 'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua', - 'Creating cache for module lspconfig', - 'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua', - 'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua', - 'Creating cache for module lspconfig/configs', - 'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua', - 'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua', - 'Creating cache for module lspconfig/util', - 'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua', - 'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua', - 'Creating cache for module vim/lsp', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua', - 'Creating cache for module vim/lsp/handlers', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua', - 'Creating cache for module vim/lsp/log', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua', - 'Creating cache for module vim/lsp/protocol', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua', - 'Creating cache for module vim/lsp/util', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua', - 'Creating cache for module vim/lsp/_snippet', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua', - 'Creating cache for module vim/highlight', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua', - 'Creating cache for module vim/lsp/rpc', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua', - 'Creating cache for module vim/lsp/sync', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua', - 'Creating cache for module vim/lsp/buf', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua', - 'Creating cache for module vim/lsp/diagnostic', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua', - 'Creating cache for module vim/lsp/codelens', - 'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua', - 'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua', - 'Creating cache for module bufferline', - 'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua', - 'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua', - 'Creating cache for module bufferline/constants', - 'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua', - 'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua', - 'Creating cache for module bufferline/utils', - 'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua', - 'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua', - 'Updating chunk cache file: scratch/cache/nvim/luacache_chunks', - 'Updating chunk cache file: scratch/cache/nvim/luacache_modpaths' - } -end - -local gen_exp_hot = function() - return gen_exp{ - 'Loading cache file scratch/cache/nvim/luacache_chunks', - 'Loading cache file scratch/cache/nvim/luacache_modpaths', - 'Loaded cache for path ./test/lua/plugins.lua', - 'Loaded cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua', - 'Loaded cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua', - 'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua', - 'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua', - 'Loaded cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua', - 'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua', - 'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua', - 'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua', - 'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua', - 'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua', - 'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua', - 'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua', - 'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua', - 'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua', - 'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua' - } -end - -describe('impatient', function() - local function reset() - clear() - nvim07 = exec_lua('return vim.version().minor') == 7 - cmd [[set runtimepath=$VIMRUNTIME,.,./test]] - cmd [[let $XDG_CACHE_HOME='scratch/cache']] - cmd [[set packpath=]] - end - - before_each(function() - reset() - end) - - it('load plugins without impatient', function() - exec_lua([[require('plugins')]]) - end) - - local function run() - exec_lua[[ - require('impatient') - require('plugins') - _G.__luacache.save_cache() - ]] - end - - it('creates cache', function() - os.execute[[rm -rf scratch/cache]] - run() - eq(gen_exp_cold(), exec_lua("return _G.__luacache.log")) - end) - - it('loads cache', function() - run() - eq(gen_exp_hot(), exec_lua("return _G.__luacache.log")) - end) - -end) diff --git a/etc/soft/nvim/+plugins/impatient.nvim/test/lua/plugins.lua b/etc/soft/nvim/+plugins/impatient.nvim/test/lua/plugins.lua deleted file mode 100644 index 004c089..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/test/lua/plugins.lua +++ /dev/null @@ -1,43 +0,0 @@ - -local init = { - ['neovim/nvim-lspconfig'] = '2f026f21', - ['nvim-lua/plenary.nvim'] = '06266e7b', - ['nvim-lua/telescope.nvim'] = 'ac42f0c2', - ['lewis6991/gitsigns.nvim'] = 'daa233aa', - ['lewis6991/spellsitter.nvim'] = '7f9e8471', - ['norcalli/nvim-colorizer.lua'] = '36c610a9', - ['akinsho/bufferline.nvim'] = 'bede234e' -} - -local testdir = 'scratch' - -vim.fn.system{"mkdir", testdir} - -for plugin, sha in pairs(init) do - local plugin_dir = plugin:match('.*/(.*)') - local plugin_dir2 = testdir..'/'..plugin_dir - vim.fn.system{ - 'git', '-C', testdir, 'clone', - 'https://github.com/'..plugin, plugin_dir - } - - -- local rev = (vim.fn.system{ - -- 'git', '-C', plugin_dir2, - -- 'rev-list', 'HEAD', '-n', '1', '--first-parent', '--before=2021-09-05' - -- }):sub(1,-2) - - -- if sha then - -- assert(vim.startswith(rev, sha), ('Plugin sha for %s does match %s != %s'):format(plugin, rev, sha)) - -- end - - vim.fn.system{'git', '-C', plugin_dir2, 'checkout', sha} - - vim.opt.rtp:prepend(vim.loop.fs_realpath("scratch/"..plugin_dir)) -end - -require'telescope' -require'gitsigns' -require'spellsitter' -require'colorizer' -require'lspconfig' -require'bufferline' diff --git a/etc/soft/nvim/+plugins/impatient.nvim/test/preload.lua b/etc/soft/nvim/+plugins/impatient.nvim/test/preload.lua deleted file mode 100644 index 7facdde..0000000 --- a/etc/soft/nvim/+plugins/impatient.nvim/test/preload.lua +++ /dev/null @@ -1,10 +0,0 @@ --- Modules loaded here will not be cleared and reloaded by Busted. --- Busted started doing this to help provide more isolation. -local global_helpers = require('test.helpers') - --- Bypoass CI behaviour logic -global_helpers.isCI = function(_) - return false -end - -local helpers = require('test.functional.helpers')() diff --git a/etc/soft/nvim/+plugins/marks.nvim/LICENSE b/etc/soft/nvim/+plugins/marks.nvim/LICENSE new file mode 100644 index 0000000..1ef641a --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/LICENSE @@ -0,0 +1,7 @@ +Copyright 2021 Tony Chen + +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. diff --git a/etc/soft/nvim/+plugins/marks.nvim/README.md b/etc/soft/nvim/+plugins/marks.nvim/README.md new file mode 100644 index 0000000..eb5462c --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/README.md @@ -0,0 +1,212 @@ +# marks.nvim +A better user experience for interacting with and manipulating Vim marks. +Requires Neovim 0.5+. + +![](../assets/marks-demo.gif) + +Screenshot: + +![](../assets/demo_screenshot.png) + +## Features + +- view marks in the sign column +- quickly add, delete, and toggle marks +- cycle between marks +- preview marks in floating windows +- extract marks to quickfix/location list +- set bookmarks with sign/virtual text annotations for quick navigation across buffers + +## Installation + +I recommend you use your favorite vim plugin manager, like vim-plug, or packer. + +For example, using vim-plug, you would add the following line: + +`Plug 'chentoast/marks.nvim'` + +If you want to manually install, you can clone this repository, and add the path +to the cloned repo to your runtimepath: `set rtp+=/path/to/cloned/repo`. + +## Setup + +```lua +require'marks'.setup { + -- whether to map keybinds or not. default true + default_mappings = true, + -- which builtin marks to show. default {} + builtin_marks = { ".", "<", ">", "^" }, + -- whether movements cycle back to the beginning/end of buffer. default true + cyclic = true, + -- whether the shada file is updated after modifying uppercase marks. default false + force_write_shada = false, + -- how often (in ms) to redraw signs/recompute mark positions. + -- higher values will have better performance but may cause visual lag, + -- while lower values may cause performance penalties. default 150. + refresh_interval = 250, + -- sign priorities for each type of mark - builtin marks, uppercase marks, lowercase + -- marks, and bookmarks. + -- can be either a table with all/none of the keys, or a single number, in which case + -- the priority applies to all marks. + -- default 10. + sign_priority = { lower=10, upper=15, builtin=8, bookmark=20 }, + -- disables mark tracking for specific filetypes. default {} + excluded_filetypes = {}, + -- marks.nvim allows you to configure up to 10 bookmark groups, each with its own + -- sign/virttext. Bookmarks can be used to group together positions and quickly move + -- across multiple buffers. default sign is '!@#$%^&*()' (from 0 to 9), and + -- default virt_text is "". + bookmark_0 = { + sign = "⚑", + virt_text = "hello world", + -- explicitly prompt for a virtual line annotation when setting a bookmark from this group. + -- defaults to false. + annotate = false, + }, + mappings = {} +} +``` + +See `:help marks-setup` for all of the keys that can be passed to the setup function. + +## Mappings + +The following default mappings are included: + +``` + mx Set mark x + m, Set the next available alphabetical (lowercase) mark + m; Toggle the next available mark at the current line + dmx Delete mark x + dm- Delete all marks on the current line + dm Delete all marks in the current buffer + m] Move to next mark + m[ Move to previous mark + m: Preview mark. This will prompt you for a specific mark to + preview; press to preview the next mark. + + m[0-9] Add a bookmark from bookmark group[0-9]. + dm[0-9] Delete all bookmarks from bookmark group[0-9]. + m} Move to the next bookmark having the same type as the bookmark under + the cursor. Works across buffers. + m{ Move to the previous bookmark having the same type as the bookmark under + the cursor. Works across buffers. + dm= Delete the bookmark under the cursor. +``` + +Set `default_mappings = false` in the setup function if you don't want to have these mapped. + +You can change the keybindings by setting the `mapping` table in the setup function: + +```lua +require'marks'.setup { + mappings = { + set_next = "m,", + next = "m]", + preview = "m:", + set_bookmark0 = "m0", + prev = false -- pass false to disable only this default mapping + } +} +``` + +The following keys are available to be passed to the mapping table: + +``` + set_next Set next available lowercase mark at cursor. + toggle Toggle next available mark at cursor. + delete_line Deletes all marks on current line. + delete_buf Deletes all marks in current buffer. + next Goes to next mark in buffer. + prev Goes to previous mark in buffer. + preview Previews mark (will wait for user input). press to just preview the next mark. + set Sets a letter mark (will wait for input). + delete Delete a letter mark (will wait for input). + + set_bookmark[0-9] Sets a bookmark from group[0-9]. + delete_bookmark[0-9] Deletes all bookmarks from group[0-9]. + delete_bookmark Deletes the bookmark under the cursor. + next_bookmark Moves to the next bookmark having the same type as the + bookmark under the cursor. + prev_bookmark Moves to the previous bookmark having the same type as the + bookmark under the cursor. + next_bookmark[0-9] Moves to the next bookmark of of the same group type. Works by + first going according to line number, and then according to buffer + number. + prev_bookmark[0-9] Moves to the previous bookmark of of the same group type. Works by + first going according to line number, and then according to buffer + number. + annotate Prompts the user for a virtual line annotation that is then placed + above the bookmark. Requires neovim 0.6+ and is not mapped by default. +``` + +marks.nvim also provides a list of `` mappings for you, in case you want to map things via vimscript. The list of provided mappings are: + +``` +(Marks-set) +(Marks-setnext) +(Marks-toggle) +(Marks-delete) +(Marks-deleteline) +(Marks-deletebuf) +(Marks-preview) +(Marks-next) +(Marks-prev) + +(Marks-delete-bookmark) +(Marks-next-bookmark) +(Marks-prev-bookmark) +(Marks-set-bookmark[0-9]) +(Marks-delete-bookmark[0-9]) +(Marks-next-bookmark[0-9]) +(Marks-prev-bookmark[0-9]) +``` + +See `:help marks-mappings` for more information. + +## Highlights and Commands + +marks.nvim defines the following highlight groups: + +`MarkSignHL` The highlight group for displayed mark signs. + +`MarkSignNumHL` The highlight group for the number line in a signcolumn. + +`MarkVirtTextHL` The highlight group for bookmark virtual text annotations. + +marks.nvim also defines the following commands: + +`:MarksToggleSigns[ buffer]` Toggle signs globally. Also accepts an optional + buffer number to toggle signs for that buffer only. + +`:MarksListBuf` Fill the location list with all marks in the current buffer. + +`:MarksListGlobal` Fill the location list with all global marks in open buffers. + +`:MarksListAll` Fill the location list with all marks in all open buffers. + +`:BookmarksList group_number` Fill the location list with all bookmarks of group "group_number". + +`:BookmarksListAll` Fill the location list with all bookmarks, across all groups. + +There are also corresponding commands for those who prefer the quickfix list: + +`:MarksQFListBuf` + +`:MarksQFListGlobal` + +`:MarksQFListAll` + +`:BookmarksQFList group_number` + +`:BookmarksQFListAll` + +## See Also + +[vim-signature](https://github.com/kshenoy/vim-signature) + +[vim-bookmarks](https://github.com/MattesGroeger/vim-bookmarks) + +## Todos + +- Operator pending mappings and count aware movement mappings diff --git a/etc/soft/nvim/+plugins/marks.nvim/doc/marks-nvim.txt b/etc/soft/nvim/+plugins/marks.nvim/doc/marks-nvim.txt new file mode 100644 index 0000000..d9c6135 --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/doc/marks-nvim.txt @@ -0,0 +1,355 @@ +============================================================================== + *marks-nvim* +A plugin for viewing, interacting with, and manipulating built in vim marks + +Author: Tony Chen + +View marks in the sign column, quickly add, delete, and toggle marks, cycle +between marks, preview marks in floating windows, and much more. + +============================================================================== +CONTENTS *marks.nvim* + + 1. Introduction .................... |marks-introduction| + 2. Setup ........................... |marks-setup| + 3. Bookmarks ....................... |marks-bookmarks| + 3. Mappings ........................ |marks-mappings| + 4. Highlights/Commands ............. |marks-cmds| + 5. Contributing .................... |marks-contrib| + 6. License ......................... |marks-license| + +============================================================================== +1. Introduction *marks-introduction* + +This plugin enhances vim's built in marks functionality. Among other things, +it allows you to: + + view marks in the sign column + quikcly add, delete, and toggle marks + cycle between marks + preview marks in floating windows + extract marks to quickfix/location list + place bookmarks with sign/virtual text annotations + +This plugin requires neovim 0.5 or higher. Since this plugin is mainly written +in lua, it will not work with vim. + +============================================================================== +2. Setup *marks-setup* + +Setup is done through lua. To initialize the plugin with all of its defaults, +put the following in your init.vim: + + Example: > + + require'marks'.setup { + default_mappings = true, + signs = true, + mappings = {} + } +< +The setup function takes in a table of keys that control the behavior of this +plugin. The following options are defined: + + *marks-builtin* + builtin_marks: table + (default {}) + + Which builtin marks to track and show. If set, these marks will also show + up in the signcolumn and will update on |CursorMoved| + + Supported values: + "'" + "^" + "." + "<" + ">" + + Example: > + + require'marks'.setup { + builtin_marks = { "'", "<", ">", "." } + } +< + *marks-default_mappings* + default_mappings: bool + (default true) + + Whether to use the default plugin mappings or not. See |marks-mappings| for more + + *marks-signs* + signs: bool + (default true) + + Whether to show marks in the signcolumn or not. If set to true, its + recommended to also set |signcolumn| to "auto", for cases where multiple + marks are placed on the same line. + + *marks-cyclic* + cyclic: bool + (default true) + + Whether forward/backwards movement should cycle back to the beginning/end + of buffer. + + *marks-force_write_shada* + force_write_shada: bool + (default false) + + If true, then deleting global (uppercase) marks will also update the + |shada| file accordingly and force deletion of the mark permanently. + This option can be destructive and should be set only after reading more + about the shada file. + + *marks-refresh_interval* + refresh_interval: integer + (default 150) + + How often (in ms) marks.nvim should update the marks list and recompute + mark positions/redraw signs. Lower values means that mark positions and + signs will refresh much quicker, but may incur a higher performance + penalty, whereas higher values may result in better performance, but may + also cause noticable lag in signs updating. + + *marks-sign_priority* + sign_priority: integer | table + (default 10) + + The sign priority to be used for marks. Can either be a number, in which + case the priority applies to all types of marks, or a table with some or + all of the following keys: + + lower: sign priority for lowercase marks + upper: sign priority for uppercase marks + builtin: sign priority for builtin marks + bookmark: sign priority for bookmarks + + excluded_filetypes: table *marks-excluded_filetypes* + (default {}) + + Which filetypes to ignore. If a buffer with this filetype is opened, then + marks.nvim will not track any marks set in this buffer, and will not + display any signs. Setting and moving to marks with ` or ' will still work, but + movement commands like "m]" or "m[" will not. + + bookmark_[0-9]: table *marks-bookmark_config* + (default {}) + + Configuration table for each bookmark group (see |marks-bookmarks|). + The table has the following keys: + + sign: the character to use in the signcolumn for this bookmark group. + defaults to "!@#$%^&*()" - in order from group 1 to 0. set to false to + turn off signs for this bookmark. + virt_text: virtual text annotations to place at the eol of a bookmark. + defaults to nil, meaning no virtual text. + annotate: when true, explicitly prompts the user for an annotation that + will be displayed above the bookmark. defaults to false. + +============================================================================== +2. Bookmarks *marks-bookmarks* + +marks.nvim also introduces the idea of a "bookmark" - a unnamed marker that is +tied to a particular (buffer, line, col) triple. Bookmarks can optionally have +signs or virtual text annotations attached to them, and can be useful for +remembering positions in a particular buffer without having to set an +uppercase mark. For example, you might set two bookmarks to quickly toggle +back and forth between a function and its corresponding unit test in another file. + +marks.nvim supports up to 10 bookmark groups, each of which optionally has its +own sign text and virtual text. marks.nvim provides functionality to set, +delete, and cycle between bookmarks of a particular group - see +|marks-mappings| for more information. + +============================================================================== +2. Mappings *marks-mappings* + +The following mappings are defined by default: + + mx Set mark x + m, Set the next available alphabetical (lowercase) mark + m; Toggle the next available mark at the current line + dmx Delete mark x + dm- Delete all marks on the current line + dm Delete all marks in the current buffer + m] Move to next mark + m[ Move to previous mark + m: Preview mark. This will prompt you for a specific mark to + preview; press to preview the next mark. + m[0-9] Add a bookmark from bookmark group[0-9]. + dm[0-9] Delete all bookmarks from bookmark group[0-9]. + m} Move to the next bookmark having the same type as the bookmark under + the cursor. Works across buffers. + m{ Move to the previous bookmark having the same type as the bookmark under + the cursor. Works across buffers. + dm= Delete the bookmark under the cursor. + +Mappings can be controlled via the `setup` function, by passing in a mappings +table, with commands as keys, and the desired mappings as values. + +The following are the available keys of the mapping table (see above as well): + + set_next Set the next available alphabetical mark + set Set a named mark (will wait for input). + toggle Toggle a mark at the cursor. + next Move to the next mark + prev Move to the previous mark. + delete Delete a mark (will wait for input). + delete_line Delete the marks on the current line. + delete_buf Delete all marks in the buffer. + preview Preview a mark. + + set_bookmark[0-9] Sets a bookmark from group[0-9]. + delete_bookmark[0-9] Deletes all bookmarks from group[0-9]. + delete_bookmark Deletes the bookmark under the cursor. + next_bookmark Moves to the next bookmark having the same type as the + bookmark under the cursor. + prev_bookmark Moves to the previous bookmark having the same type as the + bookmark under the cursor. + next_bookmark[0-9] Moves to the next bookmark of of the same group type. Works by + first going according to line number, and then according to buffer + number. + prev_bookmark[0-9] Moves to the previous bookmark of of the same group type. Works by + first going according to line number, and then according to buffer + number. + annotate Prompts the user for a virtual line annotation that is then placed + above the bookmark. Requires neovim 0.6+ and is not mapped by default. + + Example: > + + require'marks'.setup { + mappings = { + set = "n", + delete = "dn" + } + } +< + + means that setting mark "a" is done by pressing "na", and deleting mark "a" + is done by pressing "dna". + + Example: > + + require'marks'.setup { + mappings = { + set_next = "m." + } + } +< + means that setting a mark on the current cursor is done by pressing "m." + + Mappings can be disabled by setting them to false: + + Example: > + require'marks'.setup { + mappings = { + next = false, + } + } +< + disables the default mappings of "m]" for next. + +Alternatively, mappings are provided by marks.nvim, allowing you +to map keybindings outside of the `setup` function. + +The following mappings are provided (see above for more details +on the functionality of each mapping): + + (Marks-set) + (Marks-setnext) + (Marks-toggle) + (Marks-delete) + (Marks-deleteline) + (Marks-deletebuf) + (Marks-preview) + (Marks-next) + (Marks-prev) + (Marks-delete-bookmark) + (Marks-next-bookmark) + (Marks-prev-bookmark) + (Marks-set-bookmark[0-9]) + (Marks-delete-bookmark[0-9]) + (Marks-next-bookmark[0-9]) + (Marks-prev-bookmark[0-9]) + + Example: + + `nmap n (Marks-set)` + + Means that setting mark "a" is done with "na". + +============================================================================== +4. Highlights and Commands *marks-cmds* + +marks.nvim defines the following highlight groups. For more information, +see |sign_define()|. + +*MarkSignHL* + (defaults to |Identifier|) + + The highlight group for displayed mark signs. + +*MarkSignNumHL* + (defaults to |LineNr|) + + The highlight group for the number line in a signcolumn: see + |sign_define()|. + +*MarkVirtTextHL* + (defaults to |Comment|) + + The highlight group for bookmark virtual text annotations. + +marks.nvim also defines the following commands: + +*:MarksToggleSigns* [ buffer] + +Toggle signs for marks and bookmarks. By default, globally toggles signs, but +also Accepts an optional buffer number argument to toggle signs for that +buffer only. + +*:MarksListBuf* + +Fill the location list with all marks in the current buffer. + +*:MarksListGlobal* + +Fill the location list with all global marks in open buffers. + +*:MarksListAll* + +Fill the location list with all marks in all open buffers. + +*:BookmarksList* group_number + +Fill the location list with all bookmarks of group "group_number". + +*:BookmarksListAll* + +Fill the location list with all bookmarks, across all groups. + +There are also corresponding commands for those who prefer the quickfix list: + +*:MarksQFListBuf* + +*:MarksQFListGlobal* + +*:MarksQFListAll* + +*:BookmarksQFList* group_number + +*:BookmarksQFListAll* + +============================================================================== +5. Contributing *marks-contrib* + +If you would like to contribute a PR or issue, please do so at +https://github.com/chentau/marks.nvim + + +============================================================================== +6. License *marks-license* + +MIT License + +vim:tw=78:ts=2:et:sts=2:sw=2:ft=help diff --git a/etc/soft/nvim/+plugins/marks.nvim/lua/marks/bookmark.lua b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/bookmark.lua new file mode 100644 index 0000000..c8a8063 --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/bookmark.lua @@ -0,0 +1,347 @@ +local utils = require'marks.utils' +local a = vim.api + +local Bookmarks = {} + +-- self.groups is an array of mark groups, with indexes from 1 to 10. +-- each element is a table with the following keys: +-- - ns: nvim namespace +-- - sign: the sign to use for this group +-- - virt_text: the virtual text to place at each mark +-- - marks: a table of marks, indexed by buffer number, then line number. +-- each mark is represented by a table with the following keys: +-- line, col, sign_id, extmark_id +-- +local function group_under_cursor(groups, bufnr, pos) + bufnr = bufnr or a.nvim_get_current_buf() + pos = pos or a.nvim_win_get_cursor(0) + + for group_nr, group in pairs(groups) do + if group.marks[bufnr] and group.marks[bufnr][pos[1]] then + return group_nr + end + end + return nil +end + +local function flatten(marks) + local ret = {} + + for _, buf_marks in pairs(marks) do + for _, mark in pairs(buf_marks) do + table.insert(ret, mark) + end + end + + local function comparator(x, y) + return (x.buf == y.buf and x.line < y.line) or (x.buf < y.buf) + end + + table.sort(ret, comparator) + return ret +end + +function Bookmarks:init(group_nr) + local ns = a.nvim_create_namespace("Bookmarks" .. group_nr) + local sign = self.signs[group_nr] + local virt_text = self.virt_text[group_nr] + + self.groups[group_nr] = { ns = ns, sign = sign, virt_text = virt_text, marks = {} } +end + +function Bookmarks:place_mark(group_nr, bufnr) + bufnr = bufnr or a.nvim_get_current_buf() + local group = self.groups[group_nr] + + if not group then + self:init(group_nr) + group = self.groups[group_nr] + end + + local pos = a.nvim_win_get_cursor(0) + + if group.marks[bufnr] and group.marks[bufnr][pos[1]] then + -- disallow multiple bookmarks on a single line + return + end + + local data = { buf = bufnr, line = pos[1], col = pos[2], sign_id = -1} + + local display_signs = utils.option_nil(self.opt.buf_signs[bufnr], self.opt.signs) + if display_signs and group.sign then + local id = group.sign:byte() * 100 + pos[1] + self:add_sign(bufnr, group.sign, pos[1], id) + data.sign_id = id + end + + local opts = {} + if group.virt_text then + opts.virt_text = {{ group.virt_text, "MarkVirtTextHL" }} + opts.virt_text_pos = "eol" + end + + local extmark_id = a.nvim_buf_set_extmark(bufnr, group.ns, pos[1]-1, pos[2], opts) + + data.extmark_id = extmark_id + + if not group.marks[bufnr] then + group.marks[bufnr] = {} + end + group.marks[bufnr][pos[1]] = data + + if self.prompt_annotate[group_nr] then + self:annotate(group_nr) + end +end + +function Bookmarks:delete_mark(group_nr, bufnr, line) + bufnr = bufnr or a.nvim_get_current_buf() + line = line or a.nvim_win_get_cursor(0)[1] + local group = self.groups[group_nr] + + if not group then + return + end + + local mark = group.marks[bufnr][line] + + if not mark then + return + end + + if mark.sign_id then + utils.remove_sign(bufnr, mark.sign_id, "BookmarkSigns") + end + + a.nvim_buf_del_extmark(bufnr, group.ns, mark.extmark_id) + group.marks[bufnr][line] = nil +end + +function Bookmarks:delete_mark_cursor() + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + local group_nr = group_under_cursor(self.groups, bufnr, pos) + if not group_nr then + return + end + + self:delete_mark(group_nr, bufnr, pos[1]) +end + +function Bookmarks:delete_all(group_nr) + local group = self.groups[group_nr] + if not group then + return + end + + for bufnr, buf_marks in pairs(group.marks) do + for _, mark in pairs(buf_marks) do + if mark.sign_id then + utils.remove_sign(bufnr, mark.sign_id, "BookmarkSigns") + end + + a.nvim_buf_del_extmark(bufnr, group.ns, mark.extmark_id) + end + group.marks[bufnr] = nil + end +end + +function Bookmarks:next(group_nr) + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + if not group_nr then + group_nr = group_under_cursor(self.groups, bufnr, pos) + end + + local group = self.groups[group_nr] + if not group then + return + end + + local marks = flatten(group.marks) + + if vim.tbl_isempty(marks) then + return + end + + local function comparator(x, y, _) + if (x.line > y.line and x.buf == y.buf) or (x.buf > y.buf) then + return true + end + + return false + end + + local next = utils.search(marks, {buf = bufnr, line=pos[1]}, + {buf=math.huge, line=math.huge}, comparator, false) + + if not next then + next = marks[1] + end + + if next.buf ~= bufnr then + vim.cmd("silent b" .. next.buf) + end + a.nvim_win_set_cursor(0, { next.line, next.col }) +end + +function Bookmarks:prev(group_nr) + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + if not group_nr then + group_nr = group_under_cursor(self.groups, bufnr, pos) + end + + local group = self.groups[group_nr] + if not group then + return + end + + local marks = flatten(group.marks) + + if vim.tbl_isempty(marks) then + return + end + + local function comparator(x, y, _) + if (x.line < y.line and x.buf == y.buf) or (x.buf < y.buf) then + return true + end + + return false + end + + local prev = utils.search(marks, {buf = bufnr, line=pos[1]}, + {buf=-1, line=-1}, comparator, false) + + if not prev then + prev = marks[#marks] + end + + if prev.buf ~= bufnr then + vim.cmd("silent b" .. prev.buf) + end + a.nvim_win_set_cursor(0, { prev.line, prev.col }) +end + +function Bookmarks:annotate(group_nr) + if vim.fn.has("nvim-0.6") ~= 1 then + error("virtual line annotations requires neovim 0.6 or higher") + end + + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + group_nr = group_nr or group_under_cursor(self.groups, bufnr, pos) + + if not group_nr then + return + end + + local bookmark = self.groups[group_nr].marks[bufnr][pos[1]] + + if not bookmark then + return + end + + local text = vim.fn.input("annotation: ") + + if text ~= "" then + a.nvim_buf_set_extmark(bufnr, self.groups[group_nr].ns, bookmark.line-1, bookmark.col, { + id = bookmark.extmark_id, virt_lines = {{{text, "MarkVirtTextHL"}}}, + virt_lines_above=true, + }) + else + a.nvim_buf_del_extmark(bufnr, self.groups[group_nr].ns, bookmark.extmark_id) + + local opts = {} + if self.groups[group_nr].virt_text then + opts.virt_text = {{ self.groups[group_nr].virt_text, "MarkVirtTextHL" }} + opts.virt_text_pos = "eol" + end + bookmark.extmark_id = a.nvim_buf_set_extmark(bufnr, self.groups[group_nr].ns, bookmark.line-1, + bookmark.col, opts) + end +end + +function Bookmarks:refresh() + local bufnr = a.nvim_get_current_buf() + + -- if we delete and undo really quickly, the extmark's position will be + -- the same, but the sign will no longer be there. so clear and restore all + -- signs. + + local buf_marks + local display_signs + utils.remove_buf_signs(bufnr, "BookmarkSigns") + for _, group in pairs(self.groups) do + buf_marks = group.marks[bufnr] + if buf_marks then + for _, mark in pairs(vim.tbl_values(buf_marks)) do + local line = a.nvim_buf_get_extmark_by_id(bufnr, group.ns, + mark.extmark_id, {})[1] + + if line + 1 ~= mark.line then + buf_marks[line + 1] = mark + buf_marks[mark.line] = nil + buf_marks[line + 1].line = line + 1 + end + display_signs = utils.option_nil(self.opt.buf_signs[bufnr], self.opt.signs) + if display_signs and group.sign then + self:add_sign(bufnr, group.sign, line + 1, mark.sign_id) + end + end + end + end +end + +function Bookmarks:to_list(list_type, group_nr) + if not group_nr or not self.groups[group_nr] then + return + end + + list_type = list_type or "loclist" + local list_fn = utils.choose_list(list_type) + + local items = {} + for bufnr, buffer_marks in pairs(self.groups[group_nr].marks) do + for line, mark in pairs(buffer_marks) do + local text = a.nvim_buf_get_lines(bufnr, line-1, line, true)[1] + table.insert(items, { bufnr=bufnr, lnum=line, col=mark.col + 1, text=text }) + end + end + + list_fn(items, "r") +end + +function Bookmarks:all_to_list(list_type) + list_type = list_type or "loclist" + local list_fn = utils.choose_list(list_type) + + local items = {} + for group_nr, group in pairs(self.groups) do + for bufnr, buffer_marks in pairs(group.marks) do + for line, mark in pairs(buffer_marks) do + local text = a.nvim_buf_get_lines(bufnr, line-1, line, true)[1] + table.insert(items, { bufnr=bufnr, lnum=line, col=mark.col + 1, + text="bookmark group "..group_nr..": "..text }) + end + end + end + + list_fn(items, "r") +end + +function Bookmarks:add_sign(bufnr, text, line, id) + utils.add_sign(bufnr, text, line, id, "BookmarkSigns", self.priority) +end + +function Bookmarks.new() + return setmetatable({signs = {"!", "@", "#", "$", "%", "^", "&", "*", "(", [0]=")"}, + virt_text = {}, groups = {}, prompt_annotate = {}, opt = {}}, {__index = Bookmarks}) +end + +return Bookmarks diff --git a/etc/soft/nvim/+plugins/marks.nvim/lua/marks/init.lua b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/init.lua new file mode 100644 index 0000000..32dbc96 --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/init.lua @@ -0,0 +1,253 @@ +local mark = require'marks.mark' +local bookmark = require'marks.bookmark' +local utils = require'marks.utils' +local M = {} + +function M.set() + local err, input = pcall(function() + return string.char(vim.fn.getchar()) + end) + if not err then + return + end + + if utils.is_valid_mark(input) then + if not M.excluded_fts[vim.bo.ft] then + M.mark_state:place_mark_cursor(input) + end + vim.cmd("normal! m" .. input) + end +end + +function M.set_next() + if not M.excluded_fts[vim.bo.ft] then + M.mark_state:place_next_mark_cursor() + end +end + +function M.toggle() + if not M.excluded_fts[vim.bo.ft] then + M.mark_state:toggle_mark_cursor() + end +end + +function M.delete() + local err, input = pcall(function() + return string.char(vim.fn.getchar()) + end) + if not err then + return + end + + if utils.is_valid_mark(input) then + M.mark_state:delete_mark(input) + return + end +end + +function M.delete_line() + M.mark_state:delete_line_marks() +end + +function M.delete_buf() + M.mark_state:delete_buf_marks() +end + +function M.preview() + M.mark_state:preview_mark() +end + +function M.next() + M.mark_state:next_mark() +end + +function M.prev() + M.mark_state:prev_mark() +end + +function M.annotate() + M.bookmark_state:annotate() +end + +function M.refresh(force_reregister) + if M.excluded_fts[vim.bo.ft] then + return + end + + force_reregister = force_reregister or false + M.mark_state:refresh(nil, force_reregister) + M.bookmark_state:refresh() +end + +function M._on_delete() + local bufnr = tonumber(vim.fn.expand("")) + + if not bufnr then + return + end + + M.mark_state.buffers[bufnr] = nil + for _, group in pairs(M.bookmark_state.groups) do + group.marks[bufnr] = nil + end +end + +function M.toggle_signs(bufnr) + if not bufnr then + M.mark_state.opt.signs = not M.mark_state.opt.signs + M.bookmark_state.opt.signs = not M.bookmark_state.opt.signs + + for buf, _ in pairs(M.mark_state.opt.buf_signs) do + M.mark_state.opt.buf_signs[buf] = M.mark_state.opt.signs + end + + for buf, _ in pairs(M.bookmark_state.opt.buf_signs) do + M.bookmark_state.opt.buf_signs[buf] = M.bookmark_state.opt.signs + end + else + M.mark_state.opt.buf_signs[bufnr] = not utils.option_nil( + M.mark_state.opt.buf_signs[bufnr], M.mark_state.opt.signs) + M.bookmark_state.opt.buf_signs[bufnr] = not utils.option_nil( + M.bookmark_state.opt.buf_signs[bufnr], M.bookmark_state.opt.signs) + end + + M.refresh(true) +end + +-- set_group[0-9] functions +for i=0,9 do + M["set_bookmark" .. i] = function() M.bookmark_state:place_mark(i) end + M["delete_bookmark" .. i] = function() M.bookmark_state:delete_all(i) end + M["next_bookmark" .. i] = function() M.bookmark_state:next(i) end + M["prev_bookmark" .. i] = function() M.bookmark_state:prev(i) end +end + +function M.delete_bookmark() + M.bookmark_state:delete_mark_cursor() +end + +function M.next_bookmark() + M.bookmark_state:next() +end + +function M.prev_bookmark() + M.bookmark_state:prev() +end + +M.mappings = { + set = "m", + set_next = "m,", + toggle = "m;", + next = "m]", + prev = "m[", + preview = "m:", + next_bookmark = "m}", + prev_bookmark = "m{", + delete = "dm", + delete_line = "dm-", + delete_bookmark = "dm=", + delete_buf = "dm" +} + +for i=0,9 do + M.mappings["set_bookmark" .. i] = "m"..tostring(i) + M.mappings["delete_bookmark" .. i] = "dm"..tostring(i) +end + +local function user_mappings(config) + for cmd, key in pairs(config.mappings) do + if key ~= false then + M.mappings[cmd] = key + else + M.mappings[cmd] = nil + end + end +end + +local function apply_mappings() + for cmd, key in pairs(M.mappings) do + vim.cmd("nnoremap "..key.." lua require'marks'."..cmd.."()") + end +end + +local function setup_mappings(config) + if not config.default_mappings then + M.mappings = {} + end + if config.mappings then + user_mappings(config) + end + apply_mappings() +end + +local function setup_autocommands() + vim.cmd [[augroup Marks_autocmds + autocmd! + autocmd BufEnter * lua require'marks'.refresh(true) + autocmd BufDelete * lua require'marks'._on_delete() + augroup end]] +end + +function M.setup(config) + config = config or {} + + M.mark_state = mark.new() + M.mark_state.builtin_marks = config.builtin_marks or {} + + M.bookmark_state = bookmark.new() + + local bookmark_config + for i=0,9 do + bookmark_config = config["bookmark_" .. i] + if bookmark_config then + if bookmark_config.sign == false then + M.bookmark_state.signs[i] = nil + else + M.bookmark_state.signs[i] = bookmark_config.sign or M.bookmark_state.signs[i] + end + M.bookmark_state.virt_text[i] = bookmark_config.virt_text or + M.bookmark_state.virt_text[i] + M.bookmark_state.prompt_annotate[i] = bookmark_config.annotate + end + end + + local excluded_fts = {} + for _, ft in ipairs(config.excluded_filetypes or {}) do + excluded_fts[ft] = true + end + + M.excluded_fts = excluded_fts + + M.bookmark_state.opt.signs = true + M.bookmark_state.opt.buf_signs = {} + + config.default_mappings = utils.option_nil(config.default_mappings, true) + setup_mappings(config) + setup_autocommands() + + M.mark_state.opt.signs = utils.option_nil(config.signs, true) + M.mark_state.opt.buf_signs = {} + M.mark_state.opt.force_write_shada = utils.option_nil(config.force_write_shada, false) + M.mark_state.opt.cyclic = utils.option_nil(config.cyclic, true) + + M.mark_state.opt.priority = { 10, 10, 10 } + local mark_priority = M.mark_state.opt.priority + if type(config.sign_priority) == "table" then + mark_priority[1] = config.sign_priority.lower or mark_priority[1] + mark_priority[2] = config.sign_priority.upper or mark_priority[2] + mark_priority[3] = config.sign_priority.builtin or mark_priority[3] + M.bookmark_state.priority = config.sign_priority.bookmark or 10 + elseif type(config.sign_priority) == "number" then + mark_priority[1] = config.sign_priority + mark_priority[2] = config.sign_priority + mark_priority[3] = config.sign_priority + M.bookmark_state.priority = config.sign_priority + end + + local refresh_interval = utils.option_nil(config.refresh_interval, 150) + + local timer = vim.loop.new_timer() + timer:start(0, refresh_interval, vim.schedule_wrap(M.refresh)) +end + +return M diff --git a/etc/soft/nvim/+plugins/marks.nvim/lua/marks/mark.lua b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/mark.lua new file mode 100644 index 0000000..8c3607d --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/mark.lua @@ -0,0 +1,390 @@ +local a = vim.api +local utils = require'marks.utils' + +local Mark = {} + +-- basic structure: self.buffers is an array of tables indexed by bufnr, +-- where each table has the following keys: +-- +-- placed_marks: a table of currently placed/registered marks in the buffer. +-- indexed by mark name and contains information about mark position and sign id. +-- +-- marks_by_line: a table of lines that have marks on them. indexed by line number, +-- and contains an array of all marks currently set on that line. +-- +-- lowest_available_mark: the next lowest alphabetical mark that is available. + +function Mark:register_mark(mark, line, col, bufnr) + col = col or 1 + bufnr = bufnr or a.nvim_get_current_buf() + local buffer = self.buffers[bufnr] + + if not buffer then + return + end + + if buffer.placed_marks[mark] then + -- mark already exists: remove it first + self:delete_mark(mark, false) + end + + if buffer.marks_by_line[line] then + table.insert(buffer.marks_by_line[line], mark) + else + buffer.marks_by_line[line] = { mark } + end + buffer.placed_marks[mark] = { line = line, col = col, id = -1 } + + local display_signs = utils.option_nil(self.opt.buf_signs[bufnr], self.opt.signs) + if display_signs then + local id = mark:byte() * 100 + buffer.placed_marks[mark].id = id + self:add_sign(bufnr, mark, line, id) + end + + if not utils.is_lower(mark) or + mark:byte() > buffer.lowest_available_mark:byte() then + return + end + + while self.buffers[bufnr].placed_marks[mark] do + mark = string.char(mark:byte() + 1) + end + self.buffers[bufnr].lowest_available_mark = mark +end + +function Mark:place_mark_cursor(mark) + local bufnr = a.nvim_get_current_buf() + + local pos = a.nvim_win_get_cursor(0) + self:register_mark(mark, pos[1], pos[2], bufnr) +end + +function Mark:place_next_mark(line, col) + local bufnr = a.nvim_get_current_buf() + if not self.buffers[bufnr] then + self.buffers[bufnr] = {placed_marks = {}, + marks_by_line = {}, + lowest_available_mark = "a" } + end + + local mark = self.buffers[bufnr].lowest_available_mark + self:register_mark(mark, line, col, bufnr) + + vim.cmd("normal! m" .. mark) +end + +function Mark:place_next_mark_cursor() + local pos = a.nvim_win_get_cursor(0) + self:place_next_mark(pos[1], pos[2]) +end + +function Mark:delete_mark(mark, clear) + clear = utils.option_nil(clear, true) + local bufnr = a.nvim_get_current_buf() + local buffer = self.buffers[bufnr] + + if (not buffer) or (not buffer.placed_marks[mark]) then + return + end + + if buffer.placed_marks[mark].id ~= -1 then + utils.remove_sign(bufnr, buffer.placed_marks[mark].id) + end + + local line = buffer.placed_marks[mark].line + for key, tmp_mark in pairs(buffer.marks_by_line[line]) do + if tmp_mark == mark then + buffer.marks_by_line[line][key] = nil + break + end + end + + if vim.tbl_isempty(buffer.marks_by_line[line]) then + buffer.marks_by_line[line] = nil + end + + buffer.placed_marks[mark] = nil + + if clear then + vim.cmd("delmark " .. mark) + end + + if self.opt.force_write_shada then + vim.cmd("wshada!") + end + + -- only adjust lowest_available_mark if it is lowercase + if not utils.is_lower(mark) then + return + end + + if mark:byte() < buffer.lowest_available_mark:byte() then + buffer.lowest_available_mark = mark + end +end + +function Mark:delete_line_marks() + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + if not self.buffers[bufnr].marks_by_line[pos[1]] then + return + end + + -- delete_mark modifies the table, so make a copy + local copy = vim.tbl_values(self.buffers[bufnr].marks_by_line[pos[1]]) + for _, mark in pairs(copy) do + self:delete_mark(mark) + end +end + +function Mark:toggle_mark_cursor() + local bufnr = a.nvim_get_current_buf() + local pos = a.nvim_win_get_cursor(0) + + if self.buffers[bufnr].marks_by_line[pos[1]] then + self:delete_line_marks() + else + self:place_next_mark(pos[1], pos[2]) + end +end + +function Mark:delete_buf_marks(clear) + clear = utils.option_nil(clear, true) + local bufnr = a.nvim_get_current_buf() + self.buffers[bufnr] = { placed_marks = {}, + marks_by_line = {}, + lowest_available_mark = "a" } + + utils.remove_buf_signs(bufnr) + if clear then + vim.cmd("delmarks!") + end +end + +function Mark:next_mark() + local bufnr = a.nvim_get_current_buf() + + if not self.buffers[bufnr] then + return + end + + local line = a.nvim_win_get_cursor(0)[1] + local marks = {} + for mark, data in pairs(self.buffers[bufnr].placed_marks) do + if utils.is_letter(mark) then + marks[mark] = data + end + end + + if vim.tbl_isempty(marks) then + return + end + + local function comparator(x, y, _) + return x.line > y.line + end + + local next = utils.search(marks, {line=line}, {line=math.huge}, comparator, self.opt.cyclic) + + if next then + a.nvim_win_set_cursor(0, { next.line, next.col }) + end +end + +function Mark:prev_mark() + local bufnr = a.nvim_get_current_buf() + + if not self.buffers[bufnr] then + return + end + + local line = a.nvim_win_get_cursor(0)[1] + local marks = {} + for mark, data in pairs(self.buffers[bufnr].placed_marks) do + if utils.is_letter(mark) then + marks[mark] = data + end + end + + if vim.tbl_isempty(marks) then + return + end + + local function comparator(x, y, _) + return x.line < y.line + end + local prev = utils.search(marks, {line=line}, {line=-1}, comparator, self.opt.cyclic) + + if prev then + a.nvim_win_set_cursor(0, { prev.line, prev.col }) + end +end + +function Mark:preview_mark() + local bufnr = a.nvim_get_current_buf() + + local mark = vim.fn.getchar() + if mark == 13 then -- + mark = self:next_mark(bufnr, a.nvim_win_get_cursor(0)[2]) + else + mark = string.char(mark) + end + + local pos = vim.fn.getpos("'" .. mark) + if pos[2] == 0 then + return + end + + local width = a.nvim_win_get_width(0) + local height = a.nvim_win_get_height(0) + + a.nvim_open_win(pos[1], true, { + relative = "win", + win = 0, + width = math.floor(width / 2), + height = math.floor(height / 2), + col = math.floor(width / 4), + row = math.floor(height / 8), + border = "single" + }) + vim.cmd("normal! `" .. mark) + vim.cmd("normal! zz") +end + +function Mark:buffer_to_list(list_type, bufnr) + list_type = list_type or "loclist" + + local list_fn = utils.choose_list(list_type) + + bufnr = bufnr or a.nvim_get_current_buf() + if not self.buffers[bufnr] then + return + end + + local items = {} + for mark, data in pairs(self.buffers[bufnr].placed_marks) do + local text = a.nvim_buf_get_lines(bufnr, data.line-1, data.line, true)[1] + table.insert(items, { bufnr = bufnr, lnum = data.line, col = data.col + 1, + text = "mark " .. mark .. ": " .. text}) + end + + list_fn(items, "r") +end + +function Mark:all_to_list(list_type) + list_type = list_type or "loclist" + + local list_fn = utils.choose_list(list_type) + + local items = {} + for bufnr, buffer_state in pairs(self.buffers) do + for mark, data in pairs(buffer_state.placed_marks) do + local text = a.nvim_buf_get_lines(bufnr, data.line-1, data.line, true)[1] + table.insert(items, { bufnr = bufnr, lnum = data.line, col = data.col + 1, + text = "mark " .. mark .. ": " .. text}) + end + end + + list_fn(items, "r") +end + +function Mark:global_to_list(list_type) + list_type = list_type or "loclist" + + local list_fn = utils.choose_list(list_type) + + local items = {} + for bufnr, buffer_state in pairs(self.buffers) do + for mark, data in pairs(buffer_state.placed_marks) do + if utils.is_upper(mark) then + local text = a.nvim_buf_get_lines(bufnr, data.line-1, data.line, true)[1] + table.insert(items, { bufnr = bufnr, lnum = data.line, col = data.col + 1, + text = "mark " .. mark .. ": " .. text}) + end + end + end + + list_fn(items, "r") +end + +function Mark:refresh(bufnr, force) + force = force or false + bufnr = bufnr or a.nvim_get_current_buf() + + if not self.buffers[bufnr] then + self.buffers[bufnr] = { placed_marks = {}, + marks_by_line = {}, + lowest_available_mark = "a" } + end + + -- first, remove all marks that were deleted + for mark, _ in pairs(self.buffers[bufnr].placed_marks) do + if a.nvim_buf_get_mark(bufnr, mark)[1] == 0 then + self:delete_mark(mark, false) + end + end + + local mark + local pos + local cached_mark + + -- uppercase marks + for _, data in ipairs(vim.fn.getmarklist()) do + mark = data.mark:sub(2,3) + pos = data.pos + cached_mark = self.buffers[bufnr].placed_marks[mark] + + if utils.is_upper(mark) and pos[1] == bufnr and (force or not cached_mark or + pos[2] ~= cached_mark.line) then + self:register_mark(mark, pos[2], pos[3], bufnr) + end + end + + -- lowercase + for _, data in ipairs(vim.fn.getmarklist("%")) do + mark = data.mark:sub(2, 3) + pos = data.pos + cached_mark = self.buffers[bufnr].placed_marks[mark] + + if utils.is_lower(mark) and (force or not cached_mark or + pos[2] ~= cached_mark.line) then + self:register_mark(mark, pos[2], pos[3], bufnr) + end + end + + -- builtin marks + for _, char in pairs(self.builtin_marks) do + pos = vim.fn.getpos("'" .. char) + cached_mark = self.buffers[bufnr].placed_marks[char] + -- check: + -- mark located in current buffer? (0-9 marks return absolute bufnr instead of 0) + -- valid (lnum != 0) + -- force is true, or first time seeing mark, or mark line position has changed + if (pos[1] == 0 or pos[1] == bufnr) and pos[2] ~= 0 and + (force or not cached_mark or + pos[2] ~= cached_mark.line) then + self:register_mark(char, pos[2], pos[3], bufnr) + end + end + return +end + +function Mark:add_sign(bufnr, text, line, id) + local priority + if utils.is_lower(text) then + priority = self.opt.priority[1] + elseif utils.is_upper(text) then + priority = self.opt.priority[2] + else -- builtin + priority = self.opt.priority[3] + end + utils.add_sign(bufnr, text, line, id, "MarkSigns", priority) +end + +function Mark.new() + return setmetatable({ buffers = {}, opt = {} }, { __index = Mark }) +end + +return Mark diff --git a/etc/soft/nvim/+plugins/marks.nvim/lua/marks/utils.lua b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/utils.lua new file mode 100644 index 0000000..52dce31 --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/lua/marks/utils.lua @@ -0,0 +1,89 @@ +local M = { sign_cache = {} } +local builtin_marks = { ["."] = true, ["^"] = true, ["`"] = true, ["'"] = true, + ['"'] = true, ["<"] = true, [">"] = true, ["["] = true, + ["]"] = true } +for i = 0,9 do + builtin_marks[tostring(i)] = true +end + +function M.add_sign(bufnr, text, line, id, group, priority) + priority = priority or 10 + local sign_name = "Marks_" .. text + if not M.sign_cache[sign_name] then + M.sign_cache[sign_name] = true + vim.fn.sign_define(sign_name, { text = text, texthl = "MarkSignHL", + numhl = "MarkSignNumHL" }) + end + vim.fn.sign_place(id, group, sign_name, bufnr, { lnum = line, priority = priority }) +end + +function M.remove_sign(bufnr, id, group) + group = group or "MarkSigns" + vim.fn.sign_unplace(group, { buffer = bufnr, id = id }) +end + +function M.remove_buf_signs(bufnr, group) + group = group or "MarkSigns" + vim.fn.sign_unplace(group, { buffer = bufnr }) +end + +function M.search(marks, start_data, init_values, cmp, cyclic) + local min_next = init_values + local min_next_set = false + -- if we need to wrap around + local min = init_values + + for mark, data in pairs(marks) do + if cmp(data, start_data, mark) and not cmp(data, min_next, mark) then + min_next = data + min_next_set = true + end + if cyclic and not cmp(data, min, mark) then + min = data + end + end + if not cyclic then + return min_next_set and min_next or nil + end + return min_next_set and min_next or min +end + +function M.is_valid_mark(char) + return M.is_letter(char) or builtin_marks[char] +end + +function M.is_special(char) + return builtin_marks[char] ~= nil +end + +function M.is_letter(char) + return M.is_upper(char) or M.is_lower(char) +end + +function M.is_upper(char) + return (65 <= char:byte() and char:byte() <= 90) +end + +function M.is_lower(char) + return (97 <= char:byte() and char:byte() <= 122) +end + +function M.option_nil(option, default) + if option == nil then + return default + else + return option + end +end + +function M.choose_list(list_type) + local list_fn + if list_type == "loclist" then + list_fn = function(items, flags) vim.fn.setloclist(0, items, flags) end + elseif list_type == "quickfixlist" then + list_fn = vim.fn.setqflist + end + return list_fn +end + +return M diff --git a/etc/soft/nvim/+plugins/marks.nvim/plugin/marks.vim b/etc/soft/nvim/+plugins/marks.nvim/plugin/marks.vim new file mode 100644 index 0000000..e15c4b5 --- /dev/null +++ b/etc/soft/nvim/+plugins/marks.nvim/plugin/marks.vim @@ -0,0 +1,43 @@ +if exists("g:loaded_marks") + finish +endif +let g:loaded_marks = 1 + +hi default link MarkSignHL Identifier +" hi default link MarkSignLineHL Normal +hi default link MarkSignNumHL CursorLineNr +hi default link MarkVirtTextHL Comment + +command! -nargs=? MarksToggleSigns silent lua require'marks'.toggle_signs() +command! MarksListBuf exe "lua require'marks'.mark_state:buffer_to_list()" | lopen +command! MarksListGlobal exe "lua require'marks'.mark_state:global_to_list()" | lopen +command! MarksListAll exe "lua require'marks'.mark_state:all_to_list()" | lopen +command! MarksQFListBuf exe "lua require'marks'.mark_state:buffer_to_list('quickfixlist')" | copen +command! MarksQFListGlobal exe "lua require'marks'.mark_state:global_to_list('quickfixlist')" | copen +command! MarksQFListAll exe "lua require'marks'.mark_state:all_to_list('quickfixlist')" | copen + +command! -nargs=1 BookmarksList exe "lua require'marks'.bookmark_state:to_list('loclist', "....")" | lopen +command! BookmarksListAll exe "lua require'marks'.bookmark_state:all_to_list()" | lopen + +command! -nargs=1 BookmarksQFList exe "lua require'marks'.bookmark_state:to_list('quickfixlist', "....")" | copen +command! BookmarksQFListAll exe "lua require'marks'.bookmark_state:all_to_list('quickfixlist')" | copen + +nnoremap (Marks-set) lua require'marks'.set() +nnoremap (Marks-setnext) lua require'marks'.set_next() +nnoremap (Marks-toggle) lua require'marks'.toggle() +nnoremap (Marks-delete) lua require'marks'.delete() +nnoremap (Marks-deleteline) lua require'marks'.delete_line() +nnoremap (Marks-deletebuf) lua require'marks'.delete_buf() +nnoremap (Marks-preview) lua require'marks'.preview() +nnoremap (Marks-next) lua require'marks'.next() +nnoremap (Marks-prev) lua require'marks'.prev() +nnoremap (Marks-delete-bookmark) lua require'marks'.delete_bookmark() +nnoremap (Marks-next-bookmark) lua require'marks'.next_bookmark() +nnoremap (Marks-prev-bookmark) lua require'marks'.prev_bookmark() + +for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + exe "nnoremap (Marks-set-bookmark"..i..") lua require'marks'.set_bookmark"..i.."()" + exe "nnoremap (Marks-delete-bookmark"..i..") lua require'marks'.delete_bookmark"..i.."()" + exe "nnoremap (Marks-next-bookmark"..i..") lua require'marks'.next_bookmark"..i.."()" + exe "nnoremap (Marks-prev-bookmark"..i..") lua require'marks'.prev_bookmark"..i.."()" +endfor diff --git a/etc/soft/nvim/+plugins/nnn.nvim/LICENSE b/etc/soft/nvim/+plugins/nnn.nvim/LICENSE new file mode 100644 index 0000000..f5db9b4 --- /dev/null +++ b/etc/soft/nvim/+plugins/nnn.nvim/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Luuk van Baal +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/etc/soft/nvim/+plugins/nnn.nvim/README.md b/etc/soft/nvim/+plugins/nnn.nvim/README.md new file mode 100644 index 0000000..41b7237 --- /dev/null +++ b/etc/soft/nvim/+plugins/nnn.nvim/README.md @@ -0,0 +1,190 @@ +# nnn.nvim + +File manager for Neovim powered by [nnn](https://github.com/jarun/nnn). + + + +![img](https://user-images.githubusercontent.com/31730729/140781823-6810811c-9bd8-4ade-a1fe-5f225cb53c76.png) + + + +## Install + +Requires nnn to be installed, follow the [instructions](https://github.com/jarun/nnn/wiki/Usage#installation). + +**NOTE:** Explorer mode requires nnn version v4.3. +If your distribution doesn't provide version v4.3 from its repositories, install one of the provided [static binaries](https://github.com/jarun/nnn/releases/tag/v4.3), [OBS packages](https://software.opensuse.org//download.html?project=home%3Astig124%3Annn&package=nnn) or [build from source](https://github.com/jarun/nnn/wiki/Usage#from-source). + + + +Then install the plugin using your plugin manager: + +Install with [vim-plug](https://github.com/junegunn/vim-plug): + +```vim +Plug 'luukvbaal/nnn.nvim' +call plug#end() + +lua << EOF +require("nnn").setup() +EOF +``` + +Install with [packer](https://github.com/wbthomason/packer.nvim): + +```lua +use { + "luukvbaal/nnn.nvim", + config = function() require("nnn").setup() end +} +``` + + + +## Usage + +The plugin offers two possible modes of operation. + +### Explorer Mode + +Run command `:NnnExplorer` to open nnn in a vertical split simliar to `NERDTree`/`nvim-tree`. + +In this mode, the plugin makes use of nnn's `-F` flag to listen for opened files. Pressing Enter on a file will open that file in a new buffer, while keeping the nnn window open. + + +### Picker Mode + +Run command `:NnnPicker` to open nnn in a floating window. + +In this mode nnn's `-p` flag is used to listen for opened files on program exit. Picker mode implies only a single selection will be made before quitting nnn and thus the floating window. + +### Selection + +In both modes it's possible to [select](https://github.com/jarun/nnn/wiki/concepts#selection) multiple files before pressing Enter. Doing so will open the entire selection all at once, excluding the hovered file. + +### Bindings + +Bind `NnnExplorer/NnnPicker` to toggle the plugin on/off in normal and terminal mode. The commands accept a path as optional argument. To always open nnn in the directory of the currently active buffer, use `%:p:h` as argument: + +```vim +tnoremap NnnExplorer +nnoremap NnnExplorer %:p:h +tnoremap NnnPicker +nnoremap NnnPicker +``` + +## Configuration + +### Default options + +```lua +local cfg = { + explorer = { + cmd = "nnn", -- command overrride (-F1 flag is implied, -a flag is invalid!) + width = 24, -- width of the vertical split + side = "topleft", -- or "botright", location of the explorer window + session = "", -- or "global" / "local" / "shared" + tabs = true, -- seperate nnn instance per tab + }, + picker = { + cmd = "nnn", -- command override (-p flag is implied) + style = { + width = 0.9, -- percentage relative to terminal size when < 1, absolute otherwise + height = 0.8, -- ^ + xoffset = 0.5, -- ^ + yoffset = 0.5, -- ^ + border = "single"-- border decoration for example "rounded"(:h nvim_open_win) + }, + session = "", -- or "global" / "local" / "shared" + }, + auto_open = { + setup = nil, -- or "explorer" / "picker", auto open on setup function + tabpage = nil, -- or "explorer" / "picker", auto open when opening new tabpage + empty = false, -- only auto open on empty buffer + ft_ignore = { -- dont auto open for these filetypes + "gitcommit", + } + }, + auto_close = false, -- close tabpage/nvim when nnn is last window + replace_netrw = nil, -- or "explorer" / "picker" + mappings = {}, -- table containing mappings, see below + windownav = { -- window movement mappings to navigate out of nnn + left = "h", + right = "l", + next = "w", + prev = "W", + }, + buflisted = false, -- whether or not nnn buffers show up in the bufferlist + quitcd = nil, -- or "cd" / tcd" / "lcd", command to run on quitcd file if found + offset = false, -- whether or not to write position offset to tmpfile(for use in preview-tui) +} +``` + +Edit (part of) this table to your preferences and pass it to the `setup()` function i.e.: + +```lua +require("nnn").setup({ + picker = { + cmd = "tmux new-session nnn -Pp", + style = { border = "rounded" }, + session = "shared", + }, + replace_netrw = "picker", + windownav = "" +}) +``` + +### Mappings + +It's possible to map custom lua functions to keys which are passed the selected file or active nnn selection. +A set of builtin functions is provided which can be used as follows: + +```lua + local builtin = require("nnn").builtin + mappings = { + { "", builtin.open_in_tab }, -- open file(s) in tab + { "", builtin.open_in_split }, -- open file(s) in split + { "", builtin.open_in_vsplit }, -- open file(s) in vertical split + { "", builtin.open_in_preview }, -- open file in preview split keeping nnn focused + { "", builtin.copy_to_clipboard }, -- copy file(s) to clipboard + { "", builtin.cd_to_path }, -- cd to file directory + { "", builtin.populate_cmdline }, -- populate cmdline (:) with file(s) + } +``` + +To create your own function mapping follow the function signature of the builtin functions which are passed a table of file names. + +Note that in both picker and explorer mode, the mapping will execute on the nnn selection if it exists. + +### Session + +You can enable persistent sessions in nnn(`-S` flag) by setting picker and explorer mode session to one of `""`(disabled), `"global"` or `"local"`. + +Alternatively you can set the session `"shared"` to share the same session between both explorer and picker mode (setting either one to "shared" will make the session shared). + +### Colors + +Three highlight groups `NnnNormal`, `NnnNormalNC` and `NnnBorder` are available to configure the colors for the active, inactive and picker window borders respectively. + +## Tips and tricks + +### Git status + +[Build](https://github.com/jarun/nnn/tree/master/patches#list-of-patches) and install nnn with the [gitstatus](https://github.com/jarun/nnn/blob/master/patches/gitstatus/mainline.diff) enable git status symbols in detail mode. Add the `-G` flag to your command override to also enable symbols in normal mode. + + + +![img](https://user-images.githubusercontent.com/31730729/140726345-0d4005e4-0ed3-494f-9c51-bdac19f277f3.png) + + + +### preview-tui + +Setting the command override for picker mode to for example `tmux new-session nnn -P` will open `tmux` inside the picker window and can be used to open [`preview-tui`](https://github.com/jarun/nnn/blob/master/plugins/preview-tui) inside the floating window: + + + +![img](https://user-images.githubusercontent.com/31730729/140781363-fc81ccd0-c4f3-4cb8-a771-1c221dee603f.gif) + + +Include option `offset = true` in your config to write the offset of the `NnnPicker` window to a temporary file. This will allow `preview-tui` to correctly draw ueberzug image previews, accounting for said offset. diff --git a/etc/soft/nvim/+plugins/nnn.nvim/doc/nnn.txt b/etc/soft/nvim/+plugins/nnn.nvim/doc/nnn.txt new file mode 100644 index 0000000..3664582 --- /dev/null +++ b/etc/soft/nvim/+plugins/nnn.nvim/doc/nnn.txt @@ -0,0 +1,200 @@ +*nnn.txt* File manager powered by lua and nnn + +============================================================================== +Table of Contents *nnn-table-of-contents* + +1. nnn.nvim |nnn-nnn.nvim| + - Install |nnn-install| + - Usage |nnn-usage| + - Configuration |nnn-configuration| + - Tips and tricks |nnn-tips-and-tricks| + +============================================================================== +1. nnn.nvim *nnn-nnn.nvim* + +File manager for Neovim powered by nnn . + +INSTALL *nnn-install* + +Requires nnn to be installed, follow the instructions +. + +**NOTE:** Explorer mode requires nnn version v4.3. If your distribution +doesn’t provide version v4.3 from its repositories, install one of the +provided static binaries , OBS +packages + +or build from source . + +USAGE *nnn-usage* + +The plugin offers two possible modes of operation. + +EXPLORER MODE ~ + +Run command `:NnnExplorer` to open nnn in a vertical split simliar to +`NERDTree`/`nvim-tree`. + +In this mode, the plugin makes use of nnn’s `-F` flag to listen for opened +files. Pressing Enter on a file will open that file in a new buffer, +while keeping the nnn window open. + +PICKER MODE ~ + +Run command `:NnnPicker` to open nnn in a floating window. + +In this mode nnn’s `-p` flag is used to listen for opened files on program +exit. Picker mode implies only a single selection will be made before quitting +nnn and thus the floating window. + +SELECTION ~ + +In both modes it’s possible to select + multiple files before +pressing Enter. Doing so will open the entire selection all at once, +excluding the hovered file. + +BINDINGS ~ + +Bind `NnnExplorer/NnnPicker` to toggle the plugin on/off in normal and terminal +mode. The commands accept a path as optional argument. To always open nnn in +the directory of the currently active buffer, use `%:p:h` as argument: + +> + tnoremap NnnExplorer + nnoremap NnnExplorer %:p:h + tnoremap NnnPicker + nnoremap NnnPicker +< + + +CONFIGURATION *nnn-configuration* + +DEFAULT OPTIONS ~ + +> + local cfg = { + explorer = { + cmd = "nnn", -- command overrride (-F1 flag is implied, -a flag is invalid!) + width = 24, -- width of the vertical split + side = "topleft", -- or "botright", location of the explorer window + session = "", -- or "global" / "local" / "shared" + tabs = true, -- seperate nnn instance per tab + }, + picker = { + cmd = "nnn", -- command override (-p flag is implied) + style = { + width = 0.9, -- percentage relative to terminal size when < 1, absolute otherwise + height = 0.8, -- ^ + xoffset = 0.5, -- ^ + yoffset = 0.5, -- ^ + border = "single"-- border decoration for example "rounded"(:h nvim_open_win) + }, + session = "", -- or "global" / "local" / "shared" + }, + auto_open = { + setup = nil, -- or "explorer" / "picker", auto open on setup function + tabpage = nil, -- or "explorer" / "picker", auto open when opening new tabpage + empty = false, -- only auto open on empty buffer + ft_ignore = { -- dont auto open for these filetypes + "gitcommit", + } + }, + auto_close = false, -- close tabpage/nvim when nnn is last window + replace_netrw = nil, -- or "explorer" / "picker" + mappings = {}, -- table containing mappings, see below + windownav = { -- window movement mappings to navigate out of nnn + left = "h", + right = "l", + next = "w", + prev = "W", + }, + buflisted = false, -- whether or not nnn buffers show up in the bufferlist + quitcd = nil, -- or "cd" / tcd" / "lcd", command to run on quitcd file if found + offset = false, -- whether or not to write position offset to tmpfile(for use in preview-tui) + } +< + + +Edit (part of) this table to your preferences and pass it to the `setup()` +function i.e.: + +> + require("nnn").setup({ + picker = { + cmd = "tmux new-session nnn -Pp", + style = { border = "rounded" }, + session = "shared", + }, + replace_netrw = "picker", + windownav = "" + }) +< + + +MAPPINGS ~ + +It’s possible to map custom lua functions to keys which are passed the +selected file or active nnn selection. A set of builtin functions is provided +which can be used as follows: + +> + local builtin = require("nnn").builtin + mappings = { + { "", builtin.open_in_tab }, -- open file(s) in tab + { "", builtin.open_in_split }, -- open file(s) in split + { "", builtin.open_in_vsplit }, -- open file(s) in vertical split + { "", builtin.open_in_preview }, -- open file in preview split keeping nnn focused + { "", builtin.copy_to_clipboard }, -- copy file(s) to clipboard + { "", builtin.cd_to_path }, -- cd to file directory + { "", builtin.populate_cmdline }, -- populate cmdline (:) with file(s) + } +< + + +To create your own function mapping follow the function signature of the +builtin functions which are passed a table of file names. + +Note that in both picker and explorer mode, the mapping will execute on the nnn +selection if it exists. + +SESSION ~ + +You can enable persistent sessions in nnn(`-S` flag) by setting picker and +explorer mode session to one of `""`(disabled), `"global"` or `"local"`. + +Alternatively you can set the session `"shared"` to share the same session +between both explorer and picker mode (setting either one to "shared" will make +the session shared). + +COLORS ~ + +Three highlight groups `NnnNormal`, `NnnNormalNC` and `NnnBorder` are available +to configure the colors for the active, inactive and picker window borders +respectively. + +TIPS AND TRICKS *nnn-tips-and-tricks* + +GIT STATUS ~ + +Build and +install nnn with the gitstatus + +enable git status symbols in detail mode. Add the `-G` flag to your command +override to also enable symbols in normal mode. + +PREVIEW-TUI ~ + +Setting the command override for picker mode to for example `tmux new-session +nnn -P` will open `tmux` inside the picker window and can be used +to open `preview-tui` + inside the +floating window: + +Include option `offset = true` in your config to write the offset of the +`NnnPicker` window to a temporary file. This will allow `preview-tui` to +correctly draw ueberzug image previews, accounting for said offset. + +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nnn.nvim/lua/nnn.lua b/etc/soft/nvim/+plugins/nnn.nvim/lua/nnn.lua new file mode 100644 index 0000000..81c14e1 --- /dev/null +++ b/etc/soft/nvim/+plugins/nnn.nvim/lua/nnn.lua @@ -0,0 +1,563 @@ +local a = vim.api +local o = vim.o +local u = vim.loop +local c = vim.cmd +local f = vim.fn +local S = vim.schedule +local min = math.min +local max = math.max +local floor = math.floor +-- forward declarations +local action, stdout, startdir, oppside, bufopts +local targetwin = { win = a.nvim_get_current_win(), buf = a.nvim_get_current_buf() } +local state = { explorer = {}, picker = {} } +local M = { builtin = {} } +-- initialization +local pickertmp = f.tempname().."-picker" +local explorertmp = f.tempname().."-explorer" +local nnnopts = os.getenv("NNN_OPTS") +local nnntmpfile = os.getenv("NNN_TMPFILE") or + (os.getenv("XDG_CONFIG_HOME") or os.getenv("HOME").."/.config").."/nnn/.lastd" +local tmpdir = os.getenv("TMPDIR") or "/tmp" +local term = os.getenv("TERM") +local exploreropts = nnnopts and nnnopts:gsub("a", "") or "" + +local cfg = { + explorer = { + cmd = "nnn", + width = 24, + side = "topleft", + session = "", + tabs = true + }, + picker = { + cmd = "nnn", + style = { width = 0.9, height = 0.8, xoffset = 0.5, yoffset = 0.5, border = "single" }, + session = "", + }, + auto_open = { + setup = nil, + tabpage = nil, + empty = false, + ft_ignore = { "gitcommit" } + }, + auto_close = false, + replace_netrw = nil, + mappings = {}, + windownav = { left = "h", right = "l", next = "w", prev = "W" }, + buflisted = false, + quitcd = nil, + offset = false, +} + +local winopts = { + number = false, + relativenumber = false, + wrap = false, + winfixwidth = true, + winfixheight = true, + winhighlight = "Normal:NnnNormal,NormalNC:NnnNormalNC,FloatBorder:NnnBorder", +} + +-- Close nnn window(keeping buffer) and create new buffer if none left +local function close(mode, tab) + if a.nvim_win_get_buf(state[mode][tab].win) ~= state[mode][tab].buf then + a.nvim_win_set_buf(state[mode][tab].win, state[mode][tab].buf) + return + end + + if #a.nvim_tabpage_list_wins(0) == 1 then + a.nvim_win_set_buf(state[mode][tab].win, a.nvim_create_buf(false, false)) + else + a.nvim_win_hide(state[mode][tab].win) + end + + state[mode][tab].win = nil + -- restore last known active window + if targetwin.win then a.nvim_set_current_win(targetwin.win) end +end + +local function handle_files(iter, mode, tab) + local files = {} + local empty, notnnn + local _, targetwintab = pcall(a.nvim_win_get_tabpage, targetwin.win) + + -- find window containing empty or non-nnn buffer + if not targetwin.win or targetwintab ~= a.nvim_get_current_tabpage() then + targetwin.win = nil + for _, win in pairs(a.nvim_tabpage_list_wins(0)) do + if a.nvim_buf_get_name(a.nvim_win_get_buf(win)) == "" then + empty = win + break + end + + if a.nvim_buf_get_option(0, "filetype") ~= "nnn" then + notnnn = win + end + end + + if not empty and not notnnn then -- create new win + c(oppside.. o.columns - cfg.explorer.width - 1 .."vsplit") + targetwin.win = a.nvim_get_current_win() + end + end + + a.nvim_set_current_win(targetwin.win or empty or notnnn) + + for file in iter do + if action then + files[#files + 1] = f.fnameescape(file) + else + pcall(c, "edit "..f.fnameescape(file)) + end + end + + if mode == "explorer" and state[mode][tab].fs then + a.nvim_win_close(state[mode][tab].win, { force = true }) + M.toggle("explorer", false, false) + c("vert resize +1 | vert resize -1 | wincmd p") + end + + if action then + S(function() + action(files) + action = nil + end) + end +end + +-- Read fifo for explorer asynchronously with vim.loop +local function read_fifo() + local tab = cfg.explorer.tabs and a.nvim_get_current_tabpage() + u.fs_open(explorertmp, "r+", 438, function(ferr, fd) + if ferr then + S(function() print(ferr) end) + else + local fpipe = u.new_pipe(false) + fpipe:open(fd) + fpipe:read_start(function(rerr, chunk) + if not rerr and chunk then + S(function() + handle_files(chunk:gmatch("[^\n]+"), "explorer", tab) + end) + else + fpipe:close() + end + end) + end + end) +end + +local function stat(name, type) + local stats = u.fs_stat(name) + return stats and stats.type == type +end + +-- on_exit callback for termopen +local function on_exit(id, code) + local tabpage, win + local mode = state.picker[1] and state.picker[1].id == id and "picker" or "explorer" + + if mode == "picker" then + tabpage = 1 + win = state.picker[1].win + else + for tab, nstate in pairs(state.explorer) do + if nstate.id == id then + tabpage = tab + win = nstate.win + break + end + end + end + if not tabpage then return end + state[mode][tabpage] = {} + + if code > 0 then + S(function() print(stdout and stdout[1]:sub(1, -2)) end) + else + if cfg.quitcd then + local fd, _ = io.open(nnntmpfile, "r") + if fd then + c(cfg.quitcd..f.fnameescape(fd:read():sub(5, -2))) + fd:close() + os.remove(nnntmpfile) + end + end + + if a.nvim_win_is_valid(win) then + if #a.nvim_tabpage_list_wins(0) == 1 then + c("split") + end + a.nvim_win_hide(win) + -- Delete empty buffer, not sure what creates it. + local bufs = a.nvim_list_bufs() + if a.nvim_buf_get_name(bufs[#bufs]) == "" then + a.nvim_buf_delete(bufs[#bufs], {}) + end + end + + if mode == "picker" and stat(pickertmp, "file") then + handle_files(io.lines(pickertmp), "picker", 1) + end + end + -- restore last known active window + if targetwin then a.nvim_set_current_win(targetwin.win) end +end + +-- on_stdout callback for error catching +local function on_stdout(_, data, _) + stdout = data +end + +local function feedkeys(keys) + a.nvim_feedkeys(a.nvim_replace_termcodes(keys, true, true, true), "n", true) +end + +local function buffer_setup() + for opt, val in pairs(bufopts) do + a.nvim_buf_set_option(0, opt, val) + end + + for i, mapping in ipairs(cfg.mappings) do + a.nvim_buf_set_keymap(0, "t", mapping[1], "lua require('nnn').handle_mapping("..i..")", {}) + end + + a.nvim_buf_set_keymap(0, "t", cfg.windownav.left, "h", {}) + a.nvim_buf_set_keymap(0, "t", cfg.windownav.right, "l", {}) + a.nvim_buf_set_keymap(0, "t", cfg.windownav.next, "w", {}) + a.nvim_buf_set_keymap(0, "t", cfg.windownav.prev, "W", {}) +end + + -- Restore buffer to previous state +local function restore_buffer(win, buf) + a.nvim_win_call(win, function() + c((a.nvim_buf_is_valid(targetwin.buf) and targetwin.buf ~= buf) and targetwin.buf.."buffer" or "enew") + end) +end + +-- Calculate window size and return table +local function get_win_size(fullscreen) + local lines = o.lines + local columns = o.columns + local wincfg = { relative = "editor", style = "minimal", height = lines, width = columns, row = 0, col = 0 } + local style = cfg.picker.style + + if not fullscreen then + wincfg.height = min(max(0, floor(style.height > 1 and style.height or (lines * style.height))), lines) - 1 + wincfg.width = min(max(0, floor(style.width > 1 and style.width or (columns * style.width))), columns) - 1 + + local row = floor(style.yoffset > 1 and style.yoffset or (style.yoffset * (lines - wincfg.height))) - 1 + local col = floor(style.xoffset > 1 and style.xoffset or (style.xoffset * (columns - wincfg.width))) - 1 + + wincfg.row = min(max(0, row), lines - wincfg.height) + wincfg.col = min(max(0, col), columns - wincfg.width) + wincfg.border = cfg.picker.style.border + end + + if cfg.offset then + local file = io.open(tmpdir.."/nnn-preview-tui-posoffset", "w") + if file then + file:write((wincfg.col + 1).." "..(wincfg.row + 1).."\n") + file:close() + end + end + + return wincfg +end + +-- Create window and return window/buffer id +local function create_win(mode, tab, is_dir, fullscreen) + local buf = state[mode][tab] and state[mode][tab].buf + local new = not buf + local win, wincfg + + if mode == "picker" or fullscreen then + if new then + buf = is_dir and a.nvim_get_current_buf() or a.nvim_create_buf(true, false) + end + wincfg = get_win_size(fullscreen) + win = a.nvim_open_win(buf, true, wincfg) + else + if new then + c(cfg.explorer.side.." "..cfg.explorer.width..(is_dir and "vsplit" or "vnew")) + buf = a.nvim_get_current_buf() + else + c(cfg.explorer.side.." "..cfg.explorer.width.."vsplit") + end + win = a.nvim_get_current_win() + end + + return win, buf, new +end + +-- Open nnn in terminal window and set window/buffer options. +local function open(mode, tab, is_dir, empty) + local id = state[mode][tab] and state[mode][tab].id + local curwin = a.nvim_get_current_win() + local fs = #a.nvim_tabpage_list_wins(0) == 1 and empty + local win, buf, new = create_win(mode, tab, is_dir, fs) + + if new then + id = f.termopen(cfg[mode].cmd..startdir, { + env = (mode == "picker" and { TERM = term } or + { TERM = term, NNN_OPTS = exploreropts, NNN_FIFO = explorertmp }), + on_exit = on_exit, + on_stdout = on_stdout, + stdout_buffered = true + }) + + buffer_setup() + if mode == "explorer" then read_fifo() end + else + a.nvim_win_set_buf(win, buf) + end + + for opt, val in pairs(winopts) do + a.nvim_win_set_option(0, opt, val) + end + + state[mode][tab] = { win = win, buf = buf, id = id, fs = fs } + c("startinsert") + + if is_dir then + restore_buffer(curwin, buf) + end +end + +-- Toggle explorer/picker windows, keeping buffers +function M.toggle(mode, dir, auto) + local bufname = a.nvim_buf_get_name(0) + local is_dir = stat(bufname, "directory") + local empty = (is_dir and f.bufname("#") or bufname) == "" + local tab = mode == "explorer" and cfg.explorer.tabs and a.nvim_get_current_tabpage() or 1 + + if auto == "netrw" then + if not is_dir then return end + if state[mode][tab] and state[mode][tab].buf then + a.nvim_buf_delete(state[mode][tab].buf, { force = true }) + state[mode][tab] = {} + end + elseif (auto == "setup" or auto == "tab") and (cfg.auto_open.empty and (not empty and not is_dir) or + vim.tbl_contains(cfg.auto_open.ft_ignore, a.nvim_buf_get_option(0, "filetype"))) then return + end + + startdir = " "..f.fnameescape(dir and f.expand(dir) or is_dir and bufname or f.getcwd()).." " + local win = state[mode][tab] and state[mode][tab].win + win = cfg.explorer.tabs and win or vim.tbl_contains(a.nvim_tabpage_list_wins(0), win) + + if win and a.nvim_win_is_valid(win) then + close(mode, tab) + else + open(mode, tab, is_dir, empty) + end +end + +-- Handle user defined mappings +function M.handle_mapping(key) + action = cfg.mappings[key][2] + feedkeys("i") +end + +-- WinEnter callback to save target window filtering out nnn windows +function M.win_enter() + S(function() + if a.nvim_buf_get_option(a.nvim_win_get_buf(0), "filetype") ~= "nnn" then + targetwin.win = a.nvim_get_current_win() + targetwin.buf = a.nvim_get_current_buf() + elseif #a.nvim_tabpage_list_wins(0) == 1 then + targetwin.win = nil + end + end) +end + +-- WinClosed callback for auto_close to close tabpage or quit vim +function M.win_closed() + if a.nvim_win_get_config(0).zindex then return end + S(function() + if a.nvim_buf_get_option(0, "filetype") ~= "nnn" then return end + local wins = 0 + -- Count non-floating windows + for _, win in ipairs(a.nvim_tabpage_list_wins(0)) do + if not a.nvim_win_get_config(win).zindex then + wins = wins + 1 + end + end + -- Close if last non-floating window + if wins == 1 then feedkeys("q") end + end) +end + +-- TabClosed callback to clear tab from state +function M.tab_closed(tab) + local buf = state.explorer[tab] and state.explorer[tab].buf + if buf and a.nvim_buf_is_valid(buf) then + a.nvim_buf_delete(buf, { force = true }) + end +end + +-- VimResized callback to resize picker window +function M.vim_resized() + local win = state and state.picker and state.picker[1].win + if win then a.nvim_win_set_config(win, get_win_size()) end +end + +-- Builtin mapping functions +local function open_in(files, command) + for _, file in ipairs(files) do + c(command.." "..file) + end +end + +function M.builtin.open_in_split(files) open_in(files, "split") end +function M.builtin.open_in_vsplit(files) open_in(files, "vsplit") end +function M.builtin.open_in_tab(files) + c("tabnew") + open_in(files, "edit") + feedkeys("h") +end + +function M.builtin.open_in_preview(files) + local previewbuf = a.nvim_get_current_buf() + local previewname = a.nvim_buf_get_name(previewbuf) + + if previewname == files[1] then return end + c("edit "..files[1]) + if previewname ~= "" then a.nvim_buf_delete(previewbuf, {}) end + c("wincmd p") +end + +function M.builtin.copy_to_clipboard(files) + files = table.concat(files, "\n") + f.setreg("+", files) + vim.defer_fn(function() print(files:gsub("\n", ", ").." copied to register") end, 0) +end + +function M.builtin.cd_to_path(files) + local dir = files[1]:match(".*/"):sub(0, -2) + local read = io.open(dir:gsub("\\", ""), "r") + + if read ~= nil then + io.close(read) + f.execute("cd "..dir) + vim.defer_fn(function() print("working directory changed to: "..dir) end, 0) + end +end + +function M.builtin.populate_cmdline(files) + feedkeys(": "..table.concat(files, "\n"):gsub("\n", " ").."") +end + +function M.setup(setup_cfg) + if setup_cfg then cfg = vim.tbl_deep_extend("force", cfg, setup_cfg) end + + bufopts = { + buftype = "terminal", + filetype = "nnn", + buflisted = cfg.buflisted + } + + -- Replace netrw plugin if config is set + if cfg.replace_netrw then + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 + vim.g.loaded_netrwSettings = 1 + vim.g.loaded_netrwFileHandlers = 1 + pcall(a.nvim_clear_autocmds, { group = "FileExplorer" }) + + S(function() + M.toggle(cfg.replace_netrw, nil, "netrw") + a.nvim_create_autocmd({ "BufEnter", "BufNewFile" }, { callback = function() + require("nnn").toggle(cfg.replace_netrw, nil, "netrw") + end}) + end) + end + + -- Setup sessionfile name and remove on exit + local pickersession, explorersession + local sessionfile = os.getenv("XDG_CONFIG_HOME") + sessionfile = (sessionfile and sessionfile or (os.getenv("HOME").."/.config")).. + "/nnn/sessions/nnn.nvim-"..os.date("%Y-%m-%d_%H-%M-%S") + + if cfg.picker.session == "shared" or cfg.explorer.session == "shared" then + pickersession = " -S -s "..sessionfile + explorersession = pickersession + a.nvim_create_autocmd("VimLeavePre", { command = "call delete(fnameescape('"..sessionfile.."'))" }) + else + if cfg.picker.session == "global" then + pickersession = " -S " + elseif cfg.picker.session == "local" then + pickersession = " -S -s "..sessionfile.."-picker " + a.nvim_create_autocmd("VimLeavePre", { command = "call delete(fnameescape('"..sessionfile.."-picker'))" }) + else + pickersession = " " + end + + if cfg.explorer.session == "global" then + explorersession = " -S " + elseif cfg.explorer.session == "local" then + explorersession = " -S -s "..sessionfile.."-explorer " + a.nvim_create_autocmd("VimLeavePre", { command = "call delete(fnameescape('"..sessionfile.."-explorer'))" }) + else + explorersession = " " + end + end + + if not stat(explorertmp, "fifo") then os.execute("mkfifo "..explorertmp) end + + oppside = cfg.explorer.side:match("to") and "botright " or "topleft " + cfg.picker.cmd = cfg.picker.cmd.." -p "..pickertmp..pickersession + cfg.explorer.cmd = cfg.explorer.cmd.." -F1 "..explorersession + + if cfg.auto_open.setup and not (cfg.replace_netrw and stat(a.nvim_buf_get_name(0), "directory")) then + S(function() M.toggle(cfg.auto_open.setup, nil, "setup") end) + end + + if cfg.auto_close then + a.nvim_create_autocmd("WinClosed", { callback = function() + require("nnn").win_closed() + end}) + end + + if cfg.auto_open.tabpage then + a.nvim_create_autocmd("TabNewEntered", { callback = function() + vim.schedule(function() require("nnn").toggle(cfg.auto_open.tabpage, nil, "tab") end) + end}) + end + + a.nvim_create_user_command("NnnPicker", function(opts) + require("nnn").toggle("picker", opts.args) + end, { nargs = "*" }) + a.nvim_create_user_command("NnnExplorer", function(opts) + require("nnn").toggle("explorer", opts.args) + end, { nargs = "*" }) + + local group = a.nvim_create_augroup("nnn", { clear = true }) + a.nvim_create_autocmd("WinEnter", { group = group, callback = function() + require("nnn").win_enter() + end}) + a.nvim_create_autocmd("TermClose", { group = group, callback = function() + if a.nvim_buf_get_option(0, "filetype") == "nnn" then + a.nvim_buf_delete(0, { force = true }) + end + end}) + a.nvim_create_autocmd("BufEnter", { group = group, callback = function() + if a.nvim_buf_get_option(0, "filetype") == "nnn" then + vim.cmd("startinsert") + end + end}) + a.nvim_create_autocmd("VimResized", { group = group, callback = function() + if a.nvim_buf_get_option(0, "filetype") == "nnn" then + require("nnn").vim_resized() + end + end}) + a.nvim_create_autocmd("TabClosed", { group = group, callback = function(args) + require("nnn").tab_closed(tonumber(args.file)) + end}) + + a.nvim_set_hl(0, "NnnBorder", { link = "FloatBorder", default = true }) + a.nvim_set_hl(0, "NnnNormal", { link = "Normal", default = true }) + a.nvim_set_hl(0, "NnnNormalNC", { link = "Normal", default = true }) +end + +return M diff --git a/etc/soft/nvim/+plugins/nnn.vim/LICENSE b/etc/soft/nvim/+plugins/nnn.vim/LICENSE deleted file mode 100644 index 9391b3a..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2018-2021, Michael Chris Lopez -Copyright (c) 2018-2021, Arun Prakash Jana -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/etc/soft/nvim/+plugins/nnn.vim/README.md b/etc/soft/nvim/+plugins/nnn.vim/README.md deleted file mode 100644 index c36de04..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/README.md +++ /dev/null @@ -1,291 +0,0 @@ -# nnn.vim - -File manager for vim/neovim powered by n³. - -https://user-images.githubusercontent.com/7200153/127453278-3e638e33-707a-49c8-b34e-225c225906b1.mov - -

- colorscheme yin -

- -## Requirements - -1. n³ -2. `has('nvim') || has('terminal')` i.e. terminal support -3. Optional `has('nvim-0.5') || has('popupwin')` for floating window -4. Optional n³ v4.3(+) needed for file-explorer mode - -## Install - -Install n³. Instructions -[here](https://github.com/jarun/nnn/wiki/Usage#installation). - -Install the plugin using the built-in package manager: - -Vim: - -```bash -git clone --depth 1 https://github.com/mcchrish/nnn.vim\ - ~/.vim/pack/nnn/start/nnn.vim -``` - -Neovim: - -```bash -git clone --depth 1 https://github.com/mcchrish/nnn.vim\ - "${XDG_DATA_HOME:-~/.local/share}"/nvim/site/pack/nnn/start/nnn.vim -``` - -Or install the plugin using other plugin manager: - -```vim -" using vim-plug -Plug 'mcchrish/nnn.vim' -``` - -## Usage - -### Picker - -To open n³ as a file picker in vim/neovim, use the command `:NnnPicker` or the -key-binding `n`. The command accepts an optional path to open e.g. -`:NnnPicker path/to/somewhere`. - -Run the plugin, -[select file(s)](https://github.com/jarun/nnn/wiki/concepts#selection) and press -Enter to quit the n³ window. Now vim will open the first selected -file and add the remaining files to the arg list/buffer list. - -Pressing Enter on a file in n³ will pick any earlier selection (or -the hovered file if no selection exists) and exit n³. - -### Explorer - -To open n³ as a file-explorer use the command `:NnnExplorer`. The command -accepts optional path similar to `:NnnPicker`. In explorer mode pressing -Enter will pick a file but keep the n³ window open. Running -`:NnnExplorer` while an explorer window is active on that tab will toggle/close -it. - -**NOTE**: In order to use explorer mode n³ version 4.3 (or above) must be -installed. - ---- - -**NOTE**: Pressing l or Right on a file would open it -instead of picking. Use `-o` via [nnn#command](#command-override) to disable -this. - -To discard selection and/or exit, press q. - -#### `cd` on quit - -Press c-g to quit n³ and `cd` into the last directory. - -`set hidden` may be required for the floating windows to work. - -For complete plugin documentation see `:help nnn`. - -## Configuration - -### Custom mappings - -```vim -" Disable default mappings -let g:nnn#set_default_mappings = 0 - -" Set custom mappings -nnoremap nn :NnnPicker - -" Start n³ in the current file's directory -nnoremap n :NnnPicker %:p:h -``` - -### Layout - -```vim -" Opens the n³ window in a split -let g:nnn#layout = 'new' " or vnew, tabnew etc. - -" Or pass a dictionary with window size -let g:nnn#layout = { 'left': '~20%' } " or right, up, down - -" Floating window. This is the default -let g:nnn#layout = { 'window': { 'width': 0.9, 'height': 0.6, 'highlight': 'Comment' } } -``` - -`g:nnn#explorer_layout` is same as `g:nnn#layout` but for the explorer mode. - -### Action - -It's possible to set extra key-bindings for opening files in various ways. No -default is set so that n³'s key-bindings are not overridden. - -```vim -let g:nnn#action = { - \ '': 'tab split', - \ '': 'split', - \ '': 'vsplit' } -``` - -With the above example, when inside an n³ window, pressing ^T will -open the selected file in a tab instead of the current window. ^X -will open in a split an so on. Multi-selected files will be loaded in the buffer -list. - -An example assigning a function to an action: - -```vim -function! CdSelectedFile(lines) - let dir = a:lines[-1] - if filereadable(dir) - let dir = fnamemodify(dir, ':h') - endif - execute 'cd' dir -endfunction - -let g:nnn#action = { '': function('CdSelectedFile') } -``` - -In this example, pressing c-w will select the file and `cd` into its -directory. - -### Persistent session - -n³ sessions can be used to remember the location when it is reopened. - -```vim -" use the same n³ session within a vim session -let g:nnn#session = 'local' - -" use the same n³ session everywhere (including outside vim) -let g:nnn#session = 'global' -``` - -**NOTE**: If desired, an n³ session can be disabled temporarily by passing -`session: 0` as an option to `nnn#pick()`. - -### Command override - -It's possible to override the default n³ command and add some extra program -options. - -```vim -" to start n³ in detail mode: -let g:nnn#command = 'nnn -d' - -" OR, to pass env variables -let g:nnn#command = 'NNN_TRASH=1 nnn -d' -``` - -### `nnn#pick()` and `nnn#explorer()` - -The `nnn#pick([][,])` and `nnn#explorer([][,])` functions -can be called with a custom directory and additional options such as opening -file in splits or tabs. They are more configurable versions of the `:NnnPicker` -and `:NnnExplorer` commands. - -```vim -call nnn#pick('~/some-directory', { 'edit': 'vertical split' }) -" Then add custom mappings -``` - -`opts` can be: - -- `edit` - type of window the select file will be open. -- `layout` - same as `g:nnn#layout` and overrides it if specified. - -### Environment variables - -n³ will detect env variables defined in `vimrc`. - -```vim -let $NNN_TRASH=1 -``` - -### Explorer FAQ - -- How to auto start explorer when vim opens? - -```vim -" Start NnnExplorer and leave the cursor in it. -autocmd VimEnter * call nnn#explorer() - -" Start NnnExplorer and put the cursor back in the other window. -autocmd VimEnter * call nnn#explorer() | wincmd p | stopinsert - -" If a file is specified, start NnnExplorer and move the cursor to the file window. -autocmd StdinReadPre * let s:std_in=1 -autocmd VimEnter * if argc() > 0 || exists("s:std_in") | call nnn#explorer() | wincmd p | stopinsert | endif -``` - -- How to auto close vim when explorer is the last window remaining? - -```vim -" Exit Vim if NnnExplorer is the only window remaining in the only tab. -autocmd BufEnter * if tabpagenr('$') == 1 && winnr('$') == 1 && &filetype ==# 'nnn' | quit! | endif - -" Close the tab if NnnExplorer is the only window remaining in it. -autocmd BufEnter * if winnr('$') == 1 && &filetype ==# 'nnn' | quit! | endif -``` - -### Setup for `init.lua` - -Use the same option names as you would in Vim script, e.g.: - -```lua -local function copy_to_clipboard(lines) - local joined_lines = table.concat(lines, "\n") - vim.fn.setreg("+", joined_lines) -end - -require("nnn").setup({ - command = "nnn -o -C", - set_default_mappings = 0, - replace_netrw = 1, - action = { - [""] = "tab split", - [""] = "split", - [""] = "vsplit", - [""] = copy_to_clipboard, - }, -}) -``` - -## Troubleshooting - -These are some common problems that one might run into. Follow the instruction -and add relevant code snippet into your `vimrc` or `init.vim` to fix them. - -- Files being renamed randomly: This can happen when using `AutoComplPop` - plugin. - -```vim -function! AutoCmpNNN() - call acp#disable() - autocmd BufLeave call acp#enable() - autocmd BufEnter call acp#disable() -endfunction -autocmd FileType nnn call AutoCmpNNN() -``` - -- Explorer buffer gets wiped when opening a file: This can happen when using - `miniBufExpl` plugin. The workaround is to make sure `miniBufExpl` is open - before calling `:NnnExplorer`. - -```vim -let g:miniBufExplBuffersNeeded = 1 -``` - -- Can't execute `wqa`: This issue exists in both vim and neovim. When you try - to quit using the command `wqa` you see the error: - `E948: Job still running`. Crude workaround: - -```vim -command Z w | qa -cabbrev wqa Z -``` - -## Credits - -Main [n³ repository](https://github.com/jarun/nnn). diff --git a/etc/soft/nvim/+plugins/nnn.vim/autoload/nnn.vim b/etc/soft/nvim/+plugins/nnn.vim/autoload/nnn.vim deleted file mode 100644 index 487d716..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/autoload/nnn.vim +++ /dev/null @@ -1,408 +0,0 @@ -let s:temp_file = '' -let s:action = '' -let s:nnn_conf_dir = (!empty($XDG_CONFIG_HOME) ? $XDG_CONFIG_HOME : $HOME.'/.config') . '/nnn' -" The fifo used by the persistent explorer -let s:explorer_fifo = "" - -let s:local_ses = 'nnn_vim_' -" Add timestamp for convenience -" :h strftime() -- strftime is not portable -if exists('*strftime') - let s:local_ses .= strftime('%Y_%m_%dT%H_%M_%SZ') -else - " HACK: cannot use / in a session name - let s:local_ses .= substitute(tempname(), '/', '_', 'g') -endif - -" nnn highlight groups -if hlexists('FloatBorder') - highlight default link NnnBorder FloatBorder -else - highlight default link NnnBorder Comment -endif -highlight default link NnnNormal Normal -highlight default link NnnNormalNC Normal -highlight default link NnnNormalFloat Normal -highlight default link NnnVertSplit VertSplit - -function! nnn#select_action(key) abort - let s:action = g:nnn#action[a:key] - " quit nnn - if has('nvim') - call feedkeys("i\") - else - call term_sendkeys(b:tbuf, "\") - endif -endfunction - -function! s:present(dict, ...) - if type(a:dict) != v:t_dict - return 0 - endif - for key in a:000 - if !empty(get(a:dict, key, '')) - return 1 - endif - endfor - return 0 -endfunction - -function! s:calc_size(val, max) - let l:val = substitute(a:val, '^\~', '', '') - if val =~ '%$' - return a:max * str2nr(val[:-2]) / 100 - else - return min([a:max, str2nr(val)]) - endif -endfunction - -function! s:extra_selections() - if !filereadable(s:temp_file) - return [] - endif - - let l:files = readfile(s:temp_file) - if empty(l:files) - return [] - endif - - call uniq(l:files) - - if empty(l:files) || strlen(l:files[0]) <= 0 - return [] - endif - - return l:files -endfunction - -function! s:eval_temp_file(opts) - let l:Cmd = type(s:action) == v:t_func || strlen(s:action) > 0 ? s:action : a:opts.edit - - call s:switch_back(a:opts, l:Cmd) - - let l:names = s:extra_selections() - " When exiting without any selection - if empty(l:names) - return - endif - - " Action passed is function - if (type(l:Cmd) == v:t_func) - call l:Cmd(l:names) - else - " Remove directories and unreadable files - call filter(l:names, {_, val -> !isdirectory(val) && filereadable(val) }) - call reverse(l:names) - " Edit the first item. - execute(join([l:Cmd,fnameescape(fnamemodify(l:names[0], ':.'))], ' ')) - " Add any remaining items to the arg list/buffer list. - for l:name in l:names[1:] - execute(join(['argadd', fnameescape(fnamemodify(l:name, ':.'))], ' ')) - endfor - endif - - let s:action = '' " reset action -endfunction - -function! s:popup(opts, term_opts) - " Size and position - let width = min([max([0, float2nr(&columns * a:opts.width)]), &columns]) - let height = min([max([0, float2nr(&lines * a:opts.height)]), &lines - has('nvim')]) - let yoffset = get(a:opts, 'yoffset', 0.5) - let xoffset = get(a:opts, 'xoffset', 0.5) - let row = float2nr(yoffset * (&lines - height)) - let col = float2nr(xoffset * (&columns - width)) - - " Managing the differences - let row = min([max([0, row]), &lines - has('nvim') - height]) - let col = min([max([0, col]), &columns - width]) - let row += !has('nvim') - let col += !has('nvim') - - let l:border = get(a:opts, 'border', 'rounded') - let l:highlight = get(a:opts, 'highlight', 'NnnBorder') - - if has('nvim') - let l:borderchars = l:border ==# 'none' ? 'none' : map(l:border ==# 'rounded' - \ ? ['╭', '─' ,'╮', '│', '╯', '─', '╰', '│' ] - \ : ['┌', '─' ,'┐', '│', '┘', '─', '└', '│' ], - \ {_, val -> [v:val, l:highlight]}) - - let l:win = nvim_open_win(nvim_create_buf(v:false, v:true), v:true, { - \ 'row': row, - \ 'col': col, - \ 'width': width, - \ 'height': height, - \ 'border': l:borderchars, - \ 'relative': 'editor', - \ 'style': 'minimal' - \ }) - call setwinvar(l:win, '&winhighlight', 'NormalFloat:NnnNormalFloat') - call setwinvar(l:win, 'is_nnn_float', v:true) - return { 'buf': s:create_term_buf(a:term_opts), 'winhandle': l:win } - else - let l:buf = s:create_term_buf(extend(a:term_opts, #{ curwin: 0, hidden: 1, term_rows: height, term_cols: width })) - let l:borderchars = l:border ==# 'rounded' - \ ? ['─', '│', '─', '│', '╭', '╮','╯' , '╰'] - \ : ['─', '│', '─', '│', '┌', '┐', '┘', '└'] - let l:win = popup_create(l:buf, #{ - \ line: row, - \ col: col, - \ minwidth: width, - \ minheight: height, - \ highlight: 'NnnNormalFloat', - \ border: l:border ==# 'none' ? [0, 0, 0, 0] : [], - \ borderhighlight: [l:highlight], - \ borderchars: l:borderchars, - \ }) - call setwinvar(l:win, 'is_nnn_float', v:true) - return #{ buf: l:buf, winhandle: l:win } - endif -endfunction - -function! s:switch_back(opts, cmd) - let l:buf = a:opts.ppos.buf - let l:layout = a:opts.layout - let l:term = a:opts.term - - " when split explorer - if type(l:layout) == v:t_string && l:layout ==# 'enew' && bufexists(l:buf) - execute 'keepalt b' l:buf - elseif s:present(l:layout, 'window') - if type(l:layout.window) != v:t_dict - throw 'Invalid layout' - endif - " Making sure we close the windows when sometimes they linger - if has('nvim') && nvim_win_is_valid(l:term.winhandle) - call nvim_win_close(l:term.winhandle, v:false) - else - call popup_close(l:term.winhandle) - endif - endif - - " don't switch when action = 'edit' and just retain the window - " don't switch when layout = 'enew' for split explorer feature - if (type(a:cmd) == v:t_string && a:cmd !=# 'edit') - \ || (type(l:layout) != v:t_string || (type(l:layout) == v:t_string && l:layout !=# 'enew')) - call win_gotoid(a:opts.ppos.winid) - endif - - if bufexists(l:term.buf) - execute 'bwipeout!' l:term.buf - endif -endfunction - -function! s:create_term_buf(opts) - let l:shell = get(g:, 'nnn#shell', &shell) - if has('nvim') - call termopen([l:shell, &shellcmdflag, a:opts.cmd], { - \ 'env': { 'NNN_SEL': s:temp_file, 'TERM': $TERM }, - \ 'on_exit': a:opts.on_exit - \ }) - startinsert - return bufnr('') - else - let l:aux = { - \ 'curwin': get(a:opts, 'curwin', 1), - \ 'hidden': get(a:opts, 'hidden', 0), - \ 'env': { 'NNN_SEL': s:temp_file, 'TERM': $TERM }, - \ 'exit_cb': a:opts.on_exit, - \ 'term_kill': 'term' - \ } - - if has_key(a:opts, 'term_rows') - call extend(l:aux, #{term_rows: get(a:opts, 'term_rows')}) - endif - - if has_key(a:opts, 'term_cols') - call extend(l:aux, #{term_cols: get(a:opts, 'term_cols')}) - endif - - return term_start([l:shell, &shellcmdflag, a:opts.cmd], l:aux) - endif -endfunction - -function! s:create_on_exit_callback(opts) - let l:opts = a:opts - function! s:callback(id, code, ...) closure - if a:code != 0 - echohl ErrorMsg | echo 'nnn exited with '.a:code | echohl None - return - endif - - call s:eval_temp_file(l:opts) - - let fname = s:nnn_conf_dir.'/.lastd' - if !empty(glob(fname)) - let firstline = readfile(fname)[0] - let lastd = split(firstline, '"')[1] - execute 'cd' fnameescape(lastd) - call delete(fnameescape(fname)) - endif - endfunction - return function('s:callback') -endfunction - -function! s:build_window(layout, term_opts) - if s:present(a:layout, 'window') - if type(a:layout.window) == v:t_dict - if !g:nnn#has_floating_window_support - throw 'Your vim/neovim version does not support popup/floating window.' - endif - return s:popup(a:layout.window, a:term_opts) - else - throw 'Invalid layout' - endif - endif - - if type(a:layout) == v:t_string - execute 'keepalt' a:layout - return { 'buf': s:create_term_buf(a:term_opts), 'winhandle': win_getid() } - endif - - let l:directions = { - \ 'up': ['topleft', 'resize', &lines], - \ 'down': ['botright', 'resize', &lines], - \ 'left': ['vertical topleft', 'vertical resize', &columns], - \ 'right': ['vertical botright', 'vertical resize', &columns] } - - for key in ['up', 'down', 'left', 'right'] - if s:present(a:layout, key) - let l:size = a:layout[key] - let [l:cmd, l:resz, l:max]= l:directions[key] - execute l:cmd . s:calc_size(l:size, l:max) . 'new' - return { 'buf': s:create_term_buf(a:term_opts), 'winhandle': win_getid() } - endif - endfor - - throw 'Invalid layout' -endfunction - -" adapted from NERDTree source code -function! s:explorer_jump_to_buffer(fname) - let l:winnr = bufwinnr('^' . a:fname . '$') - - if l:winnr !=# -1 - " jump to the window if it's already displayed - execute l:winnr . 'wincmd w' - else - " if not, there are some options here: we can allow the user to choose, - " but a sane default is to open it in the previous window - " NOTE: this can go to "special" windows like qflist - let l:winnr = winnr('#') - execute l:winnr . 'wincmd w' - execute 'edit ' . a:fname - endif -endfunction - -function! s:explorer_on_output(...) - let l:fname = has('nvim') ? a:2[0] : a:2 - if l:fname ==# '' - return - endif - call s:explorer_jump_to_buffer(l:fname) -endfunction - -function! s:explorer_job() - let l:watcher_cmd = 'cat '.s:explorer_fifo - let l:shell = get(g:, 'nnn#shell', &shell) - if has('nvim') - let l:opts = { 'on_stdout': function('s:explorer_on_output') } - call jobstart([l:shell, &shellcmdflag, l:watcher_cmd], l:opts) - else - let l:opts = { 'out_cb': function('s:explorer_on_output') } - call job_start([l:shell, &shellcmdflag, l:watcher_cmd], l:opts) - endif -endfunction - -function! s:explorer_create_on_exit_callback() - function! s:explorer_callback(id, code, ...) closure - let l:term = t:explorer_term - call delete(fnameescape(s:explorer_fifo)) - " same code as in the bottom of s:switch_back() - try - if has('nvim') - if nvim_win_is_valid(l:term.winhandle) - call nvim_win_close(l:term.winhandle, v:false) - endif - else - call win_execute(l:term.winhandle, 'close') - endif - catch /E444: Cannot close last window/ - " In case Vim complains it is the last window, fail silently. - endtry - if bufexists(l:term.buf) - execute 'bwipeout!' l:term.buf - endif - let t:explorer_winid = 0 - endfunction - return function('s:explorer_callback') -endfunction - -function! nnn#pick(...) abort - let l:directory = get(a:, 1, '') - let l:default_opts = { 'edit': 'edit' } - let l:opts = extend(l:default_opts, get(a:, 2, {})) - let s:temp_file = tempname() - - if g:nnn#session ==# 'none' || get(l:opts, 'session', 0) - let l:sess_cfg = ' ' - elseif g:nnn#session ==# 'global' - let l:sess_cfg = ' -S ' - elseif g:nnn#session ==# 'local' - let l:sess_cfg = ' -S -s '.s:local_ses.' ' - let session_file = s:nnn_conf_dir.'/sessions/'.s:local_ses - execute 'augroup NnnSession | autocmd! VimLeavePre * call delete(fnameescape("'.session_file.'")) | augroup End' - else - let l:sess_cfg = ' ' - endif - - let l:cmd = g:nnn#command.l:sess_cfg.' -p '.shellescape(s:temp_file).' '.(l:directory != '' ? shellescape(l:directory): '') - let l:layout = exists('l:opts.layout') ? l:opts.layout : g:nnn#layout - - let l:opts.layout = l:layout - let l:opts.ppos = { 'buf': bufnr(''), 'winid': win_getid() } - - let l:opts.term = s:build_window(l:layout, { 'cmd': l:cmd, 'on_exit': s:create_on_exit_callback(l:opts) }) - let b:tbuf = l:opts.term.buf - let b:is_nnn_picker = v:true - setfiletype nnn -endfunction - -function! nnn#explorer(...) abort - " toggle/close the explorer if it already exists - if exists('t:explorer_winid') && t:explorer_winid > 0 - execute win_id2win(t:explorer_winid) . 'close!' - return - endif - - let l:directory = get(a:, 1, '') - let l:default_opts = { 'edit': 'edit' } - let l:opts = extend(l:default_opts, get(a:, 2, {})) - let s:explorer_fifo = tempname() - - let l:cmd = 'NNN_FIFO='.shellescape(s:explorer_fifo).' ' - " explorer won't work if -a is set. we need to filter that out. - if exists("$NNN_OPTS") - let l:cmd .= 'NNN_OPTS='.substitute($NNN_OPTS, '\Ca', '', 'g').' ' - endif - let l:cmd .= substitute(g:nnn#command, '\Ca', '', 'g').' ' - " we need -F 1 so that picked files are written to the fifo - let l:cmd .= '-F 1 '.(l:directory != '' ? shellescape(l:directory): '') - - let l:layout = exists('l:opts.layout') ? l:opts.layout : g:nnn#explorer_layout - let l:opts.layout = l:layout - - " create the fifo ourselves since otherwise nnn might not create it on time - call system('mkfifo '.shellescape(s:explorer_fifo)) - let l:On_exit = s:explorer_create_on_exit_callback() - let b:tbuf = s:build_window(l:layout, { 'cmd': l:cmd, 'on_exit': l:On_exit }) - - let t:explorer_winid = b:tbuf.winhandle - let t:explorer_term = b:tbuf - - call s:explorer_job() - setfiletype nnn -endfunction - -" vim: set sts=4 sw=4 ts=4 et : diff --git a/etc/soft/nvim/+plugins/nnn.vim/doc/nnn.txt b/etc/soft/nvim/+plugins/nnn.vim/doc/nnn.txt deleted file mode 100644 index 8634704..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/doc/nnn.txt +++ /dev/null @@ -1,359 +0,0 @@ -*nnn.vim* File manager for vim/neovim powered by n³. -*nnn* -> - __ __ __ __ __ __ __ __ __ __ __ - /\ "-.\ \ /\ "-.\ \ /\ "-.\ \ /\ \ / / /\ \ /\ "-./ \ - \ \ \-. \ \ \ \-. \ \ \ \-. \ \ \ \'/ \ \ \ \ \ \-./\ \ - \ \_\\"\_\ \ \_\\"\_\ \ \_\\"\_\ \ \__| \ \_\ \ \_\ \ \_\ - \/_/ \/_/ \/_/ \/_/ \/_/ \/_/ \/_/ \/_/ \/_/ \/_/ -< - -============================================================================= -CONTENTS *nnn-help* - - Introduction .......................... |nnn-intro| - Requirements .......................... |nnn-req| - Usage ................................. |nnn-usage| - Commands .............................. |nnn-commands| - Mappings .............................. |nnn-mappings| - Configurations ........................ |nnn-config| - Functions ............................. |nnn-func| - Highlights ............................ |nnn-hl| - Miscellaneous ......................... |nnn-misc| - Explorer-FAQ .......................... |nnn-explorer-faq| - Troubleshooting ....................... |nnn-troubleshooting| - Credits ............................... |nnn-credits| - -============================================================================= -Introduction *nnn-intro* - -This plugins attempts to make n³ the file manager companion for vim. Use it -to create, remove, move files or to quickly select files in n³ and open them -in vim all without leaving vim. It achieves these but utilizing the built-in -terminal feature of vim and neovim. - ------------------------------------------------------------------------------ -Requirements *nnn-req* - -1. n³ -2. `has('nvim') || has('terminal')` i.e. terminal support -3. Optional `has('nvim-0.5') || has('popupwin')` for floating window -4. Optional n³ v4.3(+) needed for file-explorer mode - -============================================================================= -Usage *nnn-usage* - ------------------------------------------------------------------------------ -Picker *Picker* - -By running |:NnnPicker| or calling the |nnn#pick()| function, a new terminal -buffer opens running an n³ process in picker mode. - -Once you select (see https://github.com/jarun/nnn/wiki/concepts#selection) -one or more files and press enter, vim quits the n³ window and opens the -first selected file and add the remaining files to the arg list/buffer -list. If |nnn#session| is enabled, then n³ will remember where you left off of -when you reopen it. - -Pressing enter on a file in n³ will pick any earlier selection (or the hovered -file if no selection exists) and exit n³. - ------------------------------------------------------------------------------ -Explorer *Explorer* - -To open n³ as a file-explorer use the command |:NnnExplorer|. The command -accepts optional path similar to |:NnnPicker|. In explorer mode pressing -`Enter` will pick a file but keep the n³ window open. Running |:NnnExplorer| -while an explorer window is active on that tab will toggle/close it. - -NOTE: In order to use explorer mode n³ version 4.3 (or above) must be -installed. - ------------------------------------------------------------------------------ - -NOTE: Pressing `l` or on a file would open it instead of picking. -Use `-o` via |nnn#command| to disable this. - -You may have to set `set hidden` to make floating window work. - -To discard selection and/or exit, press q. - -Press to quit n³ and `cd` into the last directory. - ------------------------------------------------------------------------------ -Commands *nnn-commands* - -:NnnPicker *:NnnPicker* - Opens an n³ window. -:NnnExplorer *:NnnExplorer* - Opens an n³ file-explorer. - ------------------------------------------------------------------------------ -Mappings *nnn-mappings* - -Default mappings. Can be disabled by setting |g:nnn#set_default_mappings| to 0. - -n - Runs |:NnnPicker|. - - Window navigation prefix key. Same as or - in normal mode. - - ------------------------------------------------------------------------------ -Configurations *nnn-config* - -g:nnn#set_default_mappings *g:nnn#set_default_mappings* - Default: 1 - Enable default mappings. - Examples: -> - " Disable default mappings - let g:nnn#set_default_mappings = 0 - - " Set your own - nnoremap nn :NnnPicker - - " Or override - " Start n³ in the current file's directory - nnoremap n :NnnPicker '%:p:h' -< - -g:nnn#layout *g:nnn#layout* - Default: { 'window': { 'width': 0.9, 'height': 0.6 } } - Display type for the n³ buffer. Default is a floating - window. |enew| has a special behavior to act as a - "split explorer" reusing the current window and brings back - the last buffer if nothing is selected. - Examples: -> - " Opens the n³ window in a split - let g:nnn#layout = 'new' " or vnew, tabnew etc. - - " Or pass a dictionary with window size - let g:nnn#layout = { 'left': '~20%' } " or right, up, down - - " Floating window - let g:nnn#layout = { 'window': { 'width': 0.9, 'height': 0.6, 'highlight': 'Debug' } } - " Options: - " width, height, yoffset, xoffset : number - " highlight : highlight group assign to the border - " border : border style - 'rounded' / 'sharp' / 'none' -< -g:nnn#explorer_layout *g:nnn#explorer_layout* - - Default: { 'left': '20%' } - - Same as g:nnn#layout but for the explorer mode. - - -g:nnn#action *g:nnn#action* - Default: {} - Additional key-binding set for the n³ buffer for opening - files in different ways. Nothing is set by default to not - override n³'s own key-bindings. - Examples: -> - function! CopyLinestoRegister(lines) - let joined_lines = join(a:lines, "\n") - if len(a:lines) > 1 - let joined_lines .= "\n" - endif - echom joined_lines - let @+ = joined_lines - endfunction - - let g:nnn#action = { - \ '': 'tab split', - \ '': 'split', - \ '': 'vsplit', - \ '': function('CopyLinestoRegister') } - - For example, when inside an n³ window, pressing will - open the selected file in a tab, instead of the current - window. will open in a split an so on. Meanwhile for - multi selected files will be loaded in the buffer list. - - And finally you can pass a FuncRef and the array of selected - lines will be passed to that function. - - Another example, cd into selected file's directory: - - function! CdSelectedFile(lines) - let dir = a:lines[-1] - if filereadable(dir) - let dir = fnamemodify(dir, ':h') - endif - execute 'cd' dir - endfunction - - let g:nnn#action = { '': function('CdSelectedFile') } -< - -g:nnn#session *g:nnn#session* - Default: 'none' - How n³ should utilize sessions. Default is 'none' meaning - no persistent sessions. Other options are 'local' for the - same n³ session across a vim session, or 'global' for the - same n³ session across everywhere. -> - " use the same nnn session within a vim session, for - " example, so that consecutive uses of :Np will remember - " where you last left off - let g:nnn#session = 'local' -< - -g:nnn#command *g:nnn#command* - Default: 'nnn' - When you want to override the default n³ command and add - some extra flags. - Example you want to start n³ in detail mode. -> - let g:nnn#command = 'nnn -d' - - " or pass some env variables - let g:nnn#command = 'NNN_TRASH=1 nnn -d' -< - -g:nnn#replace_netrw *g:nnn#replace_netrw* - Default: 0 - Open n³ instead of netrw when opening a directory. - - -g:nnn#statusline *g:nnn#statusline* - Default: 1 - The n³ statusline. Set to 0 to disable. - -g:nnn#shell *g:nnn#shell* - Default: &shell - The shell to run the terminal window. - -require('nnn').setup{} *nnn-lua-setup* - Default: same as Vimscript defaults - An alternative way to configure n³ more suitable for - init.lua users. For example: -> - require('nnn').setup{ - set_default_mappings = false, - session = 'global', - layout = { left = '20%' } - } -< - - ------------------------------------------------------------------------------ -Functions *nnn-func* - -nnn#pick([{dir}][,{opts}]) *nnn#pick()* - Can be called with custom directory and additional options - such as opening file in splits or tabs. Basically a more - configurable version of |:NnnPicker| command. - - {dir} : (string) a directory. - {opts}: (dict) can be: - edit : type of window the select file will be open. Or - pass a FuncRef and array of selected files will - be passed to that function. - layout : same as |g:nnn#layout| and overrides it if - specified. - session: set to 0 to disable session regardless of - |g:nnn#session| value. - - Example: -> - call nnn#pick('~/some-directory', { 'edit': 'vertical split' }) -< -nnn#explorer([{dir}][,{opts}]) *nnn#explorer()* - Same as |nnn#pick()| but for explorer mode. - ------------------------------------------------------------------------------ -Highlights *nnn-hl* - -The following are the highlight groups that are used: - -NnnNormal Window. Linked to |hl-Normal|. - -NnnNormalNC Non-current window. Linked to |hl-Normal|. - -NnnNormalFloat Floating window. Linked to |hl-Normal|. - -NnnBorder Floating window border. Linked to |hl-FloatBorder| or - |hl-Comment|. - -NnnVertSplit (depecated. for < nvim-0.7) Explorer window VertSplit. Linked - to |hl-VertSplit|. -NnnWinSeparator Explorer window WinSeparator. Linked to |hl-WinSeparator|. - (requires nvim-0.7 and above). - -============================================================================= -Miscellaneous *nnn-misc* - -You can define env variables in `vimrc` and n³ will detect it. -> - let $NNN_TRASH=1 -< -============================================================================= -Explorer FAQ *nnn-explorer-faq* - -* How to auto start explorer when vim opens? - -> - " Start NnnExplorer and leave the cursor in it. - autocmd VimEnter * call nnn#explorer() - - " Start NnnExplorer and put the cursor back in the other window. - autocmd VimEnter * call nnn#explorer() | wincmd p | stopinsert - - " If a file is specified, start NnnExplorer and move the cursor to the file window. - autocmd StdinReadPre * let s:std_in=1 - autocmd VimEnter * if argc() > 0 || exists("s:std_in") | call nnn#explorer() | wincmd p | stopinsert | endif -< - -* How to auto close vim when explorer is the last window remaining? - -> - " Exit Vim if NnnExplorer is the only window remaining in the only tab. - autocmd BufEnter * if tabpagenr('$') == 1 && winnr('$') == 1 && &filetype ==# 'nnn' | quit! | endif - - " Close the tab if NnnExplorer is the only window remaining in it. - autocmd BufEnter * if winnr('$') == 1 && &filetype ==# 'nnn' | quit! | endif -< -============================================================================= -Troubleshooting *nnn-troubleshooting* - -These are some common problems that one might run into. Follow the instruction -and add relevant code snippet into your `vimrc` or `init.vim` to fix them. - -* Files being renamed randomly: This can happen when using `AutoComplPop` -plugin. -> - function! AutoCmpNNN() - call acp#disable() - autocmd BufLeave call acp#enable() - autocmd BufEnter call acp#disable() - endfunction - autocmd FileType nnn call AutoCmpNNN() -< - -* Explorer buffer gets wiped when opening a file: This can happen when using -`miniBufExpl` plugin. The workaround is to make sure `miniBufExpl` is open -before calling `:NnnExplorer`. -> - let g:miniBufExplBuffersNeeded = 1 -< - -============================================================================= -Credits *nnn-credits* - -Main n³ program by Arun Prakash Jana -github: https://github.com/jarun - -Vim/neovim plugin maintained by Michael Chris Lopez -github: https://github.com/mcchrish - -Source codes: -n³: https://github.com/jarun/nnn -vim plugin: https://github.com/mcchrish/nnn.vim - -============================================================================= -vim:tw=78:ts=2:et:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nnn.vim/ftplugin/nnn.vim b/etc/soft/nvim/+plugins/nnn.vim/ftplugin/nnn.vim deleted file mode 100644 index 9daf847..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/ftplugin/nnn.vim +++ /dev/null @@ -1,45 +0,0 @@ -if exists('b:nnn_ftplugin') - finish -endif -let b:nnn_ftplugin = 1 - -for key in keys(g:nnn#action) - execute 'tnoremap ' key ':call nnn#select_action("'.substitute(key, '<', '', 'g').'")' -endfor - -if g:nnn#set_default_mappings - tnoremap -endif - -if !exists('w:is_nnn_float') - if has('nvim-0.7') - setl winhighlight=Normal:NnnNormal,NormalNC:NnnNormalNC,WinSeparator:NnnWinSeparator - elseif has('nvim') - setl winhighlight=Normal:NnnNormal,NormalNC:NnnNormalNC,VertSplit:NnnVertSplit - else - setl wincolor=NnnNormal - augroup NnnSetWincolor - autocmd! - autocmd BufEnter setl wincolor=NnnNormal - autocmd BufLeave setl wincolor=NnnNormalNC - augroup END - endif -endif - -if has('nvim') - autocmd BufEnter startinsert -else - autocmd BufEnter if term_getstatus(bufnr()) =~# 'normal' | call feedkeys('i', 't') | endif -endif - -if !exists('g:nnn#statusline') || g:nnn#statusline - if exists('b:is_nnn_picker') - setl statusline=\ nnn%=[picker] - else - setl statusline=\ nnn%=[explorer] - endif -endif - - -setl nospell bufhidden=wipe nobuflisted nonumber norelativenumber wrap nocursorline nocursorcolumn -" vim: set sts=4 sw=4 ts=4 et : diff --git a/etc/soft/nvim/+plugins/nnn.vim/lua/nnn.lua b/etc/soft/nvim/+plugins/nnn.vim/lua/nnn.lua deleted file mode 100644 index c6c1061..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/lua/nnn.lua +++ /dev/null @@ -1,24 +0,0 @@ -local M = {} - -M.valid_options = { - "set_default_mappings", - "layout", - "explorer_layout", - "action", - "session", - "command", - "replace_netrw", - "statusline", - "shell", -} - -function M.setup(config) - for k, v in pairs(config) do - if not vim.tbl_contains(M.valid_options, k) then - error("Invalid option to nnn setup(): " .. vim.inspect(k)) - end - vim.g["nnn#" .. k] = v - end -end - -return M diff --git a/etc/soft/nvim/+plugins/nnn.vim/plugin/nnn.vim b/etc/soft/nvim/+plugins/nnn.vim/plugin/nnn.vim deleted file mode 100644 index 0a6e455..0000000 --- a/etc/soft/nvim/+plugins/nnn.vim/plugin/nnn.vim +++ /dev/null @@ -1,61 +0,0 @@ -if exists('g:nnn#loaded') - finish -endif -let g:nnn#loaded = 1 - -let g:nnn#has_floating_window_support = has('nvim-0.5') || has('popupwin') - -if !exists('g:nnn#layout') - if g:nnn#has_floating_window_support - let g:nnn#layout = { 'window': { 'width': 0.9, 'height': 0.6 } } - else - let g:nnn#layout = 'enew' - endif -endif - -if !exists('g:nnn#explorer_layout') - let g:nnn#explorer_layout = { 'left': '20%' } -endif - -if !exists('g:nnn#action') - let g:nnn#action = {} -endif - -if !exists('g:nnn#command') - let g:nnn#command = 'nnn' -endif - -if !exists('g:nnn#session') - let g:nnn#session = "none" -endif - -if !exists('g:nnn#set_default_mappings') - let g:nnn#set_default_mappings = 1 -endif - -if g:nnn#set_default_mappings - nnoremap n :NnnPicker -endif - -if !exists('g:nnn#replace_netrw') - let g:nnn#replace_netrw = 0 -endif - -" To open nnn when vim load a directory -if g:nnn#replace_netrw - function! s:nnn_pick_on_load_dir(argv_path) - let l:path = expand(a:argv_path) - bdelete! - call nnn#pick(l:path, {'layout': 'enew'}) - endfunction - - augroup ReplaceNetrwByNnnVim - autocmd VimEnter * silent! autocmd! FileExplorer - autocmd BufEnter * if isdirectory(expand("%")) | call nnn_pick_on_load_dir("%") | endif - augroup END -endif - -command! -bar -nargs=? -complete=dir NnnPicker call nnn#pick() -command! -bar -nargs=? -complete=dir NnnExplorer call nnn#explorer() - -" vim: set sts=4 sw=4 ts=4 et : diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/LICENSE b/etc/soft/nvim/+plugins/nvim-autopairs/LICENSE new file mode 100644 index 0000000..ba890c3 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 windwp + +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. + diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/Makefile b/etc/soft/nvim/+plugins/nvim-autopairs/Makefile new file mode 100644 index 0000000..284268c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/Makefile @@ -0,0 +1,5 @@ +test: + nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}" + +test-file: + nvim --headless --noplugin -u tests/minimal.vim -c "lua require(\"plenary.busted\").run(\"$(FILE)\")" diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/README.md b/etc/soft/nvim/+plugins/nvim-autopairs/README.md new file mode 100644 index 0000000..a53e800 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/README.md @@ -0,0 +1,393 @@ +## nvim-autopairs + +A super powerful autopair plugin for Neovim that supports multiple characters. + +Requires neovim 0.7 + +## Installation + +Install the plugin with your preferred package manager: + +### [vim-plug](https://github.com/junegunn/vim-plug) + +```vim +Plug 'windwp/nvim-autopairs' + +lua << EOF +require("nvim-autopairs").setup {} +EOF +``` + +### [packer](https://github.com/wbthomason/packer.nvim) + +```lua +use { + "windwp/nvim-autopairs", + config = function() require("nvim-autopairs").setup {} end +} +``` + +## Default values + +``` lua +local disable_filetype = { "TelescopePrompt" } +local disable_in_macro = false -- disable when recording or executing a macro +local disable_in_visualblock = false -- disable when insert after visual block mode +local disable_in_replace_mode = true +local ignored_next_char = [=[[%w%%%'%[%"%.%`%$]]=] +local enable_moveright = true +local enable_afterquote = true -- add bracket pairs after quote +local enable_check_bracket_line = true --- check bracket in same line +local enable_bracket_in_quote = true -- +local enable_abbr = false -- trigger abbreviation +local break_undo = true -- switch for basic rule break undo sequence +local check_ts = false +local map_cr = true +local map_bs = true -- map the key +local map_c_h = false -- Map the key to delete a pair +local map_c_w = false -- map to delete a pair if possible + +``` + +### Override default values + +``` lua +require('nvim-autopairs').setup({ + disable_filetype = { "TelescopePrompt" , "vim" }, +}) +``` + + +#### Mapping `` +``` +Before Input After +------------------------------------ +{|} { + | + } +------------------------------------ +``` + +
+nvim-cmp +

+You need to add mapping `CR` on nvim-cmp setup. +Check readme.md on nvim-cmp repo. +

+ +``` lua +-- If you want insert `(` after select function or method item +local cmp_autopairs = require('nvim-autopairs.completion.cmp') +local cmp = require('cmp') +cmp.event:on( + 'confirm_done', + cmp_autopairs.on_confirm_done() +) +``` + +You can customize the kind of completion to add `(` or any character. + +```lua +local handlers = require('nvim-autopairs.completion.handlers') + +cmp.event:on( + 'confirm_done', + cmp_autopairs.on_confirm_done({ + filetypes = { + -- "*" is a alias to all filetypes + ["*"] = { + ["("] = { + kind = { + cmp.lsp.CompletionItemKind.Function, + cmp.lsp.CompletionItemKind.Method, + }, + handler = handlers["*"] + } + }, + lua = { + ["("] = { + kind = { + cmp.lsp.CompletionItemKind.Function, + cmp.lsp.CompletionItemKind.Method + }, + ---@param char string + ---@param item item completion + ---@param bufnr buffer number + handler = function(char, item, bufnr) + -- Your handler function. Inpect with print(vim.inspect{char, item, bufnr}) + end + } + }, + -- Disable for tex + tex = false + } + }) +) +``` + +Don't use `nil` to disable a filetype. If a filetype is `nil` then `*` is used as fallback. + +
+
+coq_nvim + +``` lua +local remap = vim.api.nvim_set_keymap +local npairs = require('nvim-autopairs') + +npairs.setup({ map_bs = false, map_cr = false }) + +vim.g.coq_settings = { keymap = { recommended = false } } + +-- these mappings are coq recommended mappings unrelated to nvim-autopairs +remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) +remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) +remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) +remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) + +-- skip it, if you use another global object +_G.MUtils= {} + +MUtils.CR = function() + if vim.fn.pumvisible() ~= 0 then + if vim.fn.complete_info({ 'selected' }).selected ~= -1 then + return npairs.esc('') + else + return npairs.esc('') .. npairs.autopairs_cr() + end + else + return npairs.autopairs_cr() + end +end +remap('i', '', 'v:lua.MUtils.CR()', { expr = true, noremap = true }) + +MUtils.BS = function() + if vim.fn.pumvisible() ~= 0 and vim.fn.complete_info({ 'mode' }).mode == 'eval' then + return npairs.esc('') .. npairs.autopairs_bs() + else + return npairs.autopairs_bs() + end +end +remap('i', '', 'v:lua.MUtils.BS()', { expr = true, noremap = true }) +``` +
+
+without completion plugin + +```lua +-- add option map_cr +npairs.setup({ map_cr = true }) +``` +
+ +[another completion plugin](https://github.com/windwp/nvim-autopairs/wiki/Completion-plugin) + +If you have a problem with indent after you press ` ` +please check the settings of treesitter indent or install a plugin that has indent support for your filetype. + +### Rule + +nvim-autopairs uses rules with conditions to check pairs. + +``` lua +local Rule = require('nvim-autopairs.rule') +local npairs = require('nvim-autopairs') + +npairs.add_rule(Rule("$$","$$","tex")) + +-- you can use some built-in conditions + +local cond = require('nvim-autopairs.conds') +print(vim.inspect(cond)) + +npairs.add_rules({ + Rule("$", "$",{"tex", "latex"}) + -- don't add a pair if the next character is % + :with_pair(cond.not_after_regex("%%")) + -- don't add a pair if the previous character is xxx + :with_pair(cond.not_before_regex("xxx", 3)) + -- don't move right when repeat character + :with_move(cond.none()) + -- don't delete if the next character is xx + :with_del(cond.not_after_regex("xx")) + -- disable adding a newline when you press + :with_cr(cond.none()) + }, + -- disable for .vim files, but it work for another filetypes + Rule("a","a","-vim") +) + +npairs.add_rules({ + Rule("$$","$$","tex") + :with_pair(function(opts) + print(vim.inspect(opts)) + if opts.line=="aa $$" then + -- don't add pair on that line + return false + end + end) + } +) + +-- you can use regex +-- press u1234 => u1234number +npairs.add_rules({ + Rule("u%d%d%d%d$", "number", "lua") + :use_regex(true) +}) + + + +-- press x1234 => x12341234 +npairs.add_rules({ + Rule("x%d%d%d%d$", "number", "lua") + :use_regex(true) + :replace_endpair(function(opts) + -- print(vim.inspect(opts)) + return opts.prev_char:sub(#opts.prev_char - 3,#opts.prev_char) + end) +}) + + +-- you can do anything with regex +special key +-- example press tab to uppercase text: +-- press b1234s => B1234S1234S + +npairs.add_rules({ + Rule("b%d%d%d%d%w$", "", "vim") + :use_regex(true,"") + :replace_endpair(function(opts) + return + opts.prev_char:sub(#opts.prev_char - 4,#opts.prev_char) + .."viwU" + end) +}) + +-- you can exclude filetypes +npairs.add_rule( + Rule("$$","$$") + :with_pair(cond.not_filetypes({"lua"})) +) +--- check ./lua/nvim-autopairs/rules/basic.lua + +``` +[Rules API](https://github.com/windwp/nvim-autopairs/wiki/Rules-API) + +### Treesitter +You can use treesitter to check for a pair. + +```lua +local npairs = require("nvim-autopairs") +local Rule = require('nvim-autopairs.rule') + +npairs.setup({ + check_ts = true, + ts_config = { + lua = {'string'},-- it will not add a pair on that treesitter node + javascript = {'template_string'}, + java = false,-- don't check treesitter on java + } +}) + +local ts_conds = require('nvim-autopairs.ts-conds') + + +-- press % => %% only while inside a comment or string +npairs.add_rules({ + Rule("%", "%", "lua") + :with_pair(ts_conds.is_ts_node({'string','comment'})), + Rule("$", "$", "lua") + :with_pair(ts_conds.is_not_ts_node({'function'})) +}) +``` + +### Don't add pairs if it already has a close pair in the same line +if **next character** is a close pair and it doesn't have an open pair in same line, then it will not add a close pair + +``` text +Before Input After +------------------------------------ +( |)) ( ( (|)) + +``` + +``` lua +require('nvim-autopairs').setup({ + enable_check_bracket_line = false +}) +``` + +### Don't add pairs if the next char is alphanumeric + +You can customize how nvim-autopairs will behave if it encounters a specific +character +``` lua +require('nvim-autopairs').setup({ + ignored_next_char = "[%w%.]" -- will ignore alphanumeric and `.` symbol +}) +``` + +``` text +Before Input After +------------------------------------ +|foobar ( (|foobar +|.foobar ( (|.foobar +``` + +### Plugin Integration +``` lua + require('nvim-autopairs').disable() + require('nvim-autopairs').enable() + require('nvim-autopairs').remove_rule('(') -- remove rule ( + require('nvim-autopairs').clear_rules() -- clear all rules + -- get rule " then modify it. It can return a list of rule or just a rule + require('nvim-autopairs').get_rule('"') +``` + +* Sample +```lua +-- remove add single quote on filetype scheme or lisp +require("nvim-autopairs").get_rule("'")[1].not_filetypes = { "scheme", "lisp" } +require("nvim-autopairs").get_rule("'")[1]:with_pair(cond.not_after_text("["})) +``` + +### FastWrap + +``` text +Before Input After +-------------------------------------------------- +(|foobar then press $ (|foobar) +(|)(foobar) then press q (|(foobar)) +``` + +```lua +-- put this to setup function and press to use fast_wrap +npairs.setup({ + fast_wrap = {}, +}) + +-- change default fast_wrap +npairs.setup({ + fast_wrap = { + map = '', + chars = { '{', '[', '(', '"', "'" }, + pattern = [=[[%'%"%)%>%]%)%}%,]]=], + end_key = '$', + keys = 'qwertyuiopzxcvbnmasdfghjkl', + check_comma = true, + highlight = 'Search', + highlight_grey='Comment' + }, +}) +``` + +### autotag html and tsx + +[autotag](https://github.com/windwp/nvim-ts-autotag) + +### Endwise + +[endwise](https://github.com/windwp/nvim-autopairs/wiki/Endwise) + +### Custom rules +[rules](https://github.com/windwp/nvim-autopairs/wiki/Custom-rules) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs-rules.txt b/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs-rules.txt new file mode 100644 index 0000000..bc1a89f --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs-rules.txt @@ -0,0 +1,349 @@ +*nvim-autopairs-rules.txt* nvim-autopairs rules + +============================================================================== +Table of Contents *nvim-autopairs-rules-table-of-contents* + +1. Rule Basics |nvim-autopairs-rules-rule-basics| +2. Controlling rule behavior |nvim-autopairs-rules-controlling-rule-behavior| + - Method Overview |nvim-autopairs-rules-method-overview| + - Conditions |nvim-autopairs-rules-conditions| +3. Method Explanations |nvim-autopairs-rules-method-explanations| + - The `with_*` methods |nvim-autopairs-rules-the-`with_*`-methods| + - The `use_*` methods |nvim-autopairs-rules-the-`use_*`-methods| + - Shorthand methods |nvim-autopairs-rules-shorthand-methods| + - Advanced methods |nvim-autopairs-rules-advanced-methods| + +============================================================================== +1. Rule Basics *nvim-autopairs-rules-rule-basics* + +At its core, a rule consists of two things: a **pair definition** and an +optional **declaration of filetypes** where the rule is in effect. A pair +definition has an opening part and a closing part. Each of these parts can be +as simple as a single character like a pair of parenthesis, or multiple +characters like Markdown code fences. Defining a rule is straightforward: + +> + Rule(begin_pair, end_pair, filetypes) +< + + +Where `begin_pair` is the opening part of the pair and `end_pair` is the +closing part. `filetypes` may be specified in multiple ways: + +> + Rule("(", ")") -- Enabled for all filetypes + Rule("(", ")", "markdown") -- As a string + Rule("(", ")", {"markdown", "vim"}) -- As a table +< + + +Additionally, it is possible to specify filetypes where the rule should **not** +be enabled by prefixing it with a `-` character: + +> + Rule("(", ")", "-markdown") -- All filetypes *except* markdown +< + + +============================================================================== +2. Controlling rule behavior *nvim-autopairs-rules-controlling-rule-behavior* + +By default, rules are very simple and will always complete a pair the moment +the opening part is typed. This is fine and in some cases desirable, but the +rules API allows you to control the manner and context in which pairs are +completed; this is done by attaching **conditions** (predicates) to **events** +and adding **modifiers** to the rule. `Rule` objects expose a variety of +methods to add these predicates and modifiers to the rule. + +METHOD OVERVIEW *nvim-autopairs-rules-method-overview* + +These methods allow control over if, when, and how rules perform completion of +pairs. Each method returns the `Rule` object so that they may be chained +together to easily define complex rules. + +│ method │ usage │ +│with_pair(cond) │add condition to check during pair event │ +│with_move(cond) │add condition to check during move right event │ +│with_cr(cond) │add condition to check during line break event │ +│with_del(cond) │add condition to check during delete pair event │ +│only_cr(cond) │enable _only_ the line break event; disable everything │ +│ │else │ +│use_regex(bool, "") │ey │ +│use_key("") │set trigger key │ +│replace_endpair(fun│define ending part with a function; optionally add with│ +│c, check_pair) │_pair │ +│set_end_pair_length│override offset used to position the cursor between the│ +│(number) │ pair when replace_endpair is used │ +│replace_map_cr(func│change the mapping for used for during the line br│ +│) │eak event │ +│end_wise(cond) │make the rule an end-wise rule │ + + +AIDING UNDERSTANDING: "WHEN" INSTEAD OF "WITH" ~ + +It may be helpful to think of the `with_` functions as reading more like +`when_` instead, as the condition is checked **when** `` happens +(or wants to happen). This naming scheme more accurately describes how the +`Rule` is affected and reads more intuitively when reading a rule definition. + +For example, given a rule definition `Rule("(", ")")`, each method has a +certain effect on how and when the ending part of the pair, the closing +parenthesis, is completed. The ending part is only completed **when** +associated conditions are met upon typing the opening part of the pair. + +CONDITIONS *nvim-autopairs-rules-conditions* + +nvim-autopairs comes with a variety of common predicates ready to use simply by +including: + +> + local cond = require('nvim-autopairs.conds') +< + + +│ function │ Usage │ +│none() │always false │ +│done() │always true │ +│before_text(text) │text exists before opening part │ +│after_text(text) │text exists after opening part │ +│before_regex(regex, length│regex matches before opening part │ +│) │ │ +│after_regex(regex, length)│regex matches after opening part │ +│ │ │ +│not_before_text(text) │text is not before opening part │ +│not_after_text(text) │text is not after opening part │ +│not_before_regex(regex, le│regex doesn’t match before opening part │ +│ngth) │ │ +│not_after_regex(regex, len│regex doesn’t match after opening part │ +│gth) │ │ +│not_inside_quote() │not currently within quotation marks │ +│is_inside_quote() │currently within quotation marks │ +│not_filetypes({table}) │current filetype is not inside table │ +│is_bracket_in_quote() │check the next char is quote and cursor is insi│ +│ │de quote │ + + +**N.B.** While `cond.not_filetypes` is available, it’s better to use the +minus syntax on the desired filetype in the initial rule declaration, since +then the rule is completely removed from the buffer. + +TREESITTER CONDITIONS ~ + +Predicates based on the state of the Treesitter graph can be used by including: + +> + local ts_conds = require('nvim-autopairs.ts-conds') +< + + +│ function │ Usage │ +│is_ts_node({node_table}) │check current treesitter node│ +│is_not_ts_node({node_table})│check not in treesitter node │ + + +============================================================================== +3. Method Explanations *nvim-autopairs-rules-method-explanations* + +This section explains each method in more detail: their signatures and how they +modify the rule’s behavior are all outlined here. + +THE `WITH_*` METHODS *nvim-autopairs-rules-the-`with_*`-methods* + +Calling these methods on a `Rule` will add predicate functions to their +corresponding event, which determines whether the effect of the event actually +takes place. There are no predicates if you don’t define any, and so any +events without predicates behave as if they had a single predicate that always +returns true. + +A `Rule` may have more than one predicate defined for a given event, and the +order that they are defined will be the order that they are checked. However, +the **first** non-`nil` value returned by a predicate is used and the remaining +predicates (if any) are **not** executed. In other words, predicates defined +earlier have priority over predicates defined later. + +`WITH_PAIR(COND, POS)` ~ + +After typing the opening part, `cond` will fire and the ending part will only +be added if `cond` returned true. `with_pair` may be called more than once, and +by default, each predicate is appended to a list. When the "pair" event fires, +the _first_ predicate to return non-nil is used as the condition result. +Specifying `pos` allows explicit control over the order of the predicates. + +`WITH_MOVE(COND)` ~ + +If `cond` is true, the cursor is simply moved right when typing the ending part +of the pair and the next character is also the ending part, e.g. `|" -> "|` +when typing `"`. If `cond` returns false, the ending part is inserted as normal +instead. + +`WITH_CR(COND)` ~ + +If `cond` is true, then move the ending part of the pair to a new line below +the cursor after pressing `` while the cursor is between the pair (think +curly braces opening a block). Otherwise `` behaves as normal. For example: + +> + {|} +< + + +Typing `` produces the following when `cond` is true: + +> + { + | + } +< + + +`WITH_DEL(COND)` ~ + +If `cond` is true, when the cursor is between the pair, pressing `` to +delete the opening part of the pair will delete the ending part as well. + +THE `USE_*` METHODS *nvim-autopairs-rules-the-`use_*`-methods* + +The `use_*` functions alter how auto-pairing is triggered. Normally, the first +argument to `Rule` is taken literally as the opening part of the pair and as +soon as it is typed the "pair" event fires. + +`USE_KEY(KEY)` ~ + +The pair is only completed when `key` is pressed, instead of the moment that +the opening part is typed. This is particularly useful in `use_regex`. + +`USE_REGEX(BOOL, KEY)` ~ + +Causes the opening part to be interpreted as a lua pattern that triggers +insertion of the ending part when matched. If `key` is specified, then it acts +as an implicit `use_key`. + +SHORTHAND METHODS *nvim-autopairs-rules-shorthand-methods* + +These methods exist as convenient shortcuts for defining certain behaviors. + +`END_WISE(FUNC)` ~ + +This method is used to make "end-wise" rules, which is terminology that should +be familiar to users of other auto-pair plugins, e.g. Lexima. Specifically, +this method makes it so that the ending part of the pair will be completed +_only upon pressing `` after the opening part_, in which case the "newline" +event is fired as usual. + +This behavior is useful for languages with statement constructs like Lua and +Bash. For example, defining the following `Rule`: + +> + Rule('then', 'end'):end_wise(function(opts)) + -- Add any context checks here, e.g. line starts with "if" + return string.match(opts.line, '^%s*if') ~= nil + end) +< + + +And then pressing `` at the following cursor position: + +> + if foo == bar then| +< + + +Would be completed as this (assuming some kind of automatic indent is enabled): + +> + if foo == bar then + | + end +< + + +`ONLY_CR(COND)` ~ + +This shortcut method disables the "pair", "del", and "move" events by setting a +single predicate for each that is always false. Additionally, the effect of any +`use_key` modifiers are removed as well. If `cond` is specified, a "newline" +predicate is set as if `with_cr` were called. + +This method is convenient for defining _simple_ end-wise rules. As an example, +a default rule is defined with `only_cr` for Markdown code blocks with an +explicit language; the closing triple-backtick is not completed until you press +`` after specifying the language: + +> + ```lua <-- pressed here + | + ``` +< + + +ADVANCED METHODS *nvim-autopairs-rules-advanced-methods* + +These methods allow you to define more complex and dynamic rules. When combined +with `with_*` and `use_*` methods, it is possible to create very powerful +auto-pairs. + +`REPLACE_ENDPAIR(FUNC, CHECK_PAIR)` ~ + +Facilitates the creation of dynamic ending parts. When the "pair" event fires +and the ending part is to be completed, `func` is called with a single `opts` +argument and should return a string. The returned string will be sent to +`nvim_feedkeys` to insert the ending part of the pair. + +The `opts` parameter is a table that provides context for the current pair +completion, and can be useful for determining what to return. Note that because +`nvim_feedkeys` is used, arbitrary Vim functionality can be leveraged, such as +including `` to be able to send normal mode commands. + + *nvim-autopairs-rules-Optional-`check_pair`-parameter* + +Optional `check_pair` parameter The `check_pair` parameter is optional, + and can either be a boolean or function. + If `check_pair` is a function, it is + passed as-is to `with_pair` to create a + "pair" predicate. If `check_pair` is + true, then an implicit + `with_pair(cond.after_text(rule.end_pair))` + predicate is added, where + `rule.end_pair` is the second argument + to the `Rule` constructor. If + `check_pair` is false, an "always false" + `with_pair` predicate is added. + + +As an example, these two rule definitions are equivalent: + +> + -- This... + Rule("(", ")") + :use_key("") + :replace_endpair(function() return "" end, true) + + -- ...is shorthand for this + Rule("(", "") + :use_key("") + :with_pair(cond.after_text(")")) -- check that text after cursor is `)` + :replace_endpair(function() return "" end) +< + + +`SET_END_PAIR_LENGTH(LEN)` ~ + +When completing the ending part of a pair, the cursor necessarily moves +backward so that is in between the opening part and the closing part. In order +to do this, the `Rule` must know the length of the ending part, which by +default is trivially determined. However, if you would like to override where +the cursor is placed after completion, i.e. using `replace_endpair`, you can +explicitly set the ending part length with this method. + +`REPLACE_MAP_CR(FUNC)` ~ + +This method allows you to set a custom mapping for the "newline" (``) event +that will be used instead of the normal behavior. This can be helpful for +enforcing certain styles or or adding additional edits. `func` is called with a +single `opts` argument and should return a string specifying the mapping for +``. The default mapping is: `uO`. + +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs.txt b/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs.txt new file mode 100644 index 0000000..4841fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/doc/nvim-autopairs.txt @@ -0,0 +1,421 @@ +*nvim-autopairs.txt* A super powerful autopair for Neovim. + +============================================================================== +Table of Contents *nvim-autopairs-table-of-contents* + + - nvim-autopairs |nvim-autopairs-nvim-autopairs| + - Installation |nvim-autopairs-installation| + - Default values |nvim-autopairs-default-values| + +NVIM-AUTOPAIRS *nvim-autopairs-nvim-autopairs* + +A super powerful autopair plugin for Neovim that supports multiple characters. + +Requires neovim 0.5+ + +INSTALLATION *nvim-autopairs-installation* + +Install the plugin with your preferred package manager: + +VIM-PLUG ~ + +> + Plug 'windwp/nvim-autopairs' + + lua << EOF + require("nvim-autopairs").setup {} + EOF +< + + +PACKER ~ + +> + use { + "windwp/nvim-autopairs", + config = function() require("nvim-autopairs").setup {} end + } +< + + +DEFAULT VALUES *nvim-autopairs-default-values* + +> + local disable_filetype = { "TelescopePrompt" } + local disable_in_macro = false -- disable when recording or executing a macro + local disable_in_visualblock = false -- disable when insert after visual block mode + local ignored_next_char = [=[[%w%%%'%[%"%.]]=] + local enable_moveright = true + local enable_afterquote = true -- add bracket pairs after quote + local enable_check_bracket_line = true --- check bracket in same line + local enable_bracket_in_quote = true -- + local break_undo = true -- switch for basic rule break undo sequence + local check_ts = false + local map_cr = true + local map_bs = true -- map the key + local map_c_h = false -- Map the key to delete a pair + local map_c_w = false -- map to delete a pair if possible +< + + +OVERRIDE DEFAULT VALUES ~ + +> + require('nvim-autopairs').setup({ + disable_filetype = { "TelescopePrompt" , "vim" }, + }) +< + + + *nvim-autopairs-Mapping-``* + +> + Before Input After + ------------------------------------ + {|} { + | + } + ------------------------------------ +< + + +nvim-cmp ~ + +

+ +You need to add mapping `CR` on nvim-cmp setup. +Check readme.md on nvim-cmp repo. + +

+ +> + -- If you want insert `(` after select function or method item + local cmp_autopairs = require('nvim-autopairs.completion.cmp') + local cmp = require('cmp') + cmp.event:on( + 'confirm_done', + cmp_autopairs.on_confirm_done() + ) +< + + +Mapping `` You can customize the kind of completion + to add `(` or any character. + + +> + local handlers = require('nvim-autopairs.completion.handlers') + + cmp.event:on( + 'confirm_done', + cmp_autopairs.on_confirm_done({ + filetypes = { + -- "*" is a alias to all filetypes + ["*"] = { + ["("] = { + kind = { + cmp.lsp.CompletionItemKind.Function, + cmp.lsp.CompletionItemKind.Method, + }, + handler = handlers["*"] + } + }, + lua = { + ["("] = { + kind = { + cmp.lsp.CompletionItemKind.Function, + cmp.lsp.CompletionItemKind.Method + }, + ---@param char string + ---@param item item completion + ---@param bufnr buffer number + handler = function(char, item, bufnr) + -- Your handler function. Inpect with print(vim.inspect{char, item, bufnr}) + end + } + }, + -- Disable for tex + tex = false + } + }) + ) +< + + +Don’t use `nil` to disable a filetype. If a filetype is `nil` then `*` is +used as fallback. + +coq_nvim ~ + +> + local remap = vim.api.nvim_set_keymap + local npairs = require('nvim-autopairs') + + npairs.setup({ map_bs = false, map_cr = false }) + + vim.g.coq_settings = { keymap = { recommended = false } } + + -- these mappings are coq recommended mappings unrelated to nvim-autopairs + remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) + remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) + remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) + remap('i', '', [[pumvisible() ? "" : ""]], { expr = true, noremap = true }) + + -- skip it, if you use another global object + _G.MUtils= {} + + MUtils.CR = function() + if vim.fn.pumvisible() ~= 0 then + if vim.fn.complete_info({ 'selected' }).selected ~= -1 then + return npairs.esc('') + else + return npairs.esc('') .. npairs.autopairs_cr() + end + else + return npairs.autopairs_cr() + end + end + remap('i', '', 'v:lua.MUtils.CR()', { expr = true, noremap = true }) + + MUtils.BS = function() + if vim.fn.pumvisible() ~= 0 and vim.fn.complete_info({ 'mode' }).mode == 'eval' then + return npairs.esc('') .. npairs.autopairs_bs() + else + return npairs.autopairs_bs() + end + end + remap('i', '', 'v:lua.MUtils.BS()', { expr = true, noremap = true }) +< + + +without completion plugin ~ + +> + -- add option map_cr + npairs.setup({ map_cr = true }) +< + + +another completion plugin + + +If you have a problem with indent after you press `` please check the +settings of treesitter indent or install a plugin that has indent support for +your filetype. + +RULE ~ + +nvim-autopairs uses rules with conditions to check pairs. + +> + local Rule = require('nvim-autopairs.rule') + local npairs = require('nvim-autopairs') + + npairs.add_rule(Rule("$$","$$","tex")) + + -- you can use some built-in conditions + + local cond = require('nvim-autopairs.conds') + print(vim.inspect(cond)) + + npairs.add_rules({ + Rule("$", "$",{"tex", "latex"}) + -- don't add a pair if the next character is % + :with_pair(cond.not_after_regex("%%")) + -- don't add a pair if the previous character is xxx + :with_pair(cond.not_before_regex("xxx", 3)) + -- don't move right when repeat character + :with_move(cond.none()) + -- don't delete if the next character is xx + :with_del(cond.not_after_regex("xx")) + -- disable adding a newline when you press + :with_cr(cond.none()) + }, + -- disable for .vim files, but it work for another filetypes + Rule("a","a","-vim") + ) + + npairs.add_rules({ + Rule("$$","$$","tex") + :with_pair(function(opts) + print(vim.inspect(opts)) + if opts.line=="aa $$" then + -- don't add pair on that line + return false + end + end) + } + ) + + -- you can use regex + -- press u1234 => u1234number + npairs.add_rules({ + Rule("u%d%d%d%d$", "number", "lua") + :use_regex(true) + }) + + + + -- press x1234 => x12341234 + npairs.add_rules({ + Rule("x%d%d%d%d$", "number", "lua") + :use_regex(true) + :replace_endpair(function(opts) + -- print(vim.inspect(opts)) + return opts.prev_char:sub(#opts.prev_char - 3,#opts.prev_char) + end) + }) + + + -- you can do anything with regex +special key + -- example press tab to uppercase text: + -- press b1234s => B1234S1234S + + npairs.add_rules({ + Rule("b%d%d%d%d%w$", "", "vim") + :use_regex(true,"") + :replace_endpair(function(opts) + return + opts.prev_char:sub(#opts.prev_char - 4,#opts.prev_char) + .."viwU" + end) + }) + + -- you can exclude filetypes + npairs.add_rule( + Rule("$$","$$") + :with_pair(cond.not_filetypes({"lua"})) + ) + --- check ./lua/nvim-autopairs/rules/basic.lua +< + + +Rules API + +TREESITTER ~ + +You can use treesitter to check for a pair. + +> + local npairs = require("nvim-autopairs") + local Rule = require('nvim-autopairs.rule') + + npairs.setup({ + check_ts = true, + ts_config = { + lua = {'string'},-- it will not add a pair on that treesitter node + javascript = {'template_string'}, + java = false,-- don't check treesitter on java + } + }) + + local ts_conds = require('nvim-autopairs.ts-conds') + + + -- press % => %% only while inside a comment or string + npairs.add_rules({ + Rule("%", "%", "lua") + :with_pair(ts_conds.is_ts_node({'string','comment'})), + Rule("$", "$", "lua") + :with_pair(ts_conds.is_not_ts_node({'function'})) + }) +< + + +DON’T ADD PAIRS IF IT ALREADY HAS A CLOSE PAIR IN THE SAME LINE ~ + +if **next character** is a close pair and it doesn’t have an open pair in +same line, then it will not add a close pair + +> + Before Input After + ------------------------------------ + ( |)) ( ( (|)) +< + + +> + require('nvim-autopairs').setup({ + enable_check_bracket_line = false + }) +< + + +DON’T ADD PAIRS IF THE NEXT CHAR IS ALPHANUMERIC ~ + +You can customize how nvim-autopairs will behave if it encounters a specific +character + +> + require('nvim-autopairs').setup({ + ignored_next_char = "[%w%.]" -- will ignore alphanumeric and `.` symbol + }) +< + + +> + Before Input After + ------------------------------------ + |foobar ( (|foobar + |.foobar ( (|.foobar +< + + +PLUGIN INTEGRATION ~ + +> + require('nvim-autopairs').disable() + require('nvim-autopairs').enable() + require('nvim-autopairs').remove_rule('(') -- remove rule ( + require('nvim-autopairs').clear_rules() -- clear all rules + require('nvim-autopairs').get_rule('"') -- get rule " then modify it +< + + +FASTWRAP ~ + +> + Before Input After + -------------------------------------------------- + (|foobar then press $ (|foobar) + (|)(foobar) then press q (|(foobar)) +< + + +> + -- put this to setup function and press to use fast_wrap + npairs.setup({ + fast_wrap = {}, + }) + + -- change default fast_wrap + npairs.setup({ + fast_wrap = { + map = '', + chars = { '{', '[', '(', '"', "'" }, + pattern = [=[[%'%"%)%>%]%)%}%,]]=], + end_key = '$', + keys = 'qwertyuiopzxcvbnmasdfghjkl', + check_comma = true, + highlight = 'Search', + highlight_grey='Comment' + }, + }) +< + + +AUTOTAG HTML AND TSX ~ + +autotag + +ENDWISE ~ + +endwise + +CUSTOM RULES ~ + +rules + +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs.lua new file mode 100644 index 0000000..3135811 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs.lua @@ -0,0 +1,651 @@ +local log = require('nvim-autopairs._log') +local utils = require('nvim-autopairs.utils') +local basic_rule = require('nvim-autopairs.rules.basic') +local api = vim.api +local highlighter = nil +local M = {} + +M.state = { + disabled = false, + rules = {}, + buf_ts = {}, +} + +local default = { + map_bs = true, + map_c_h = false, + map_c_w = false, + map_cr = true, + disable_filetype = { 'TelescopePrompt', 'spectre_panel' }, + disable_in_macro = false, + disable_in_visualblock = false, + disable_in_replace_mode = true, + ignored_next_char = [=[[%w%%%'%[%"%.%`%$]]=], + break_undo = true, + check_ts = false, + enable_moveright = true, + enable_afterquote = true, + enable_check_bracket_line = true, + enable_bracket_in_quote = true, + enable_abbr = false, + ts_config = { + lua = { 'string', 'source' }, + javascript = { 'string', 'template_string' }, + }, +} + +M.setup = function(opt) + M.config = vim.tbl_deep_extend('force', default, opt or {}) + if M.config.fast_wrap then + require('nvim-autopairs.fastwrap').setup(M.config.fast_wrap) + end + M.config.rules = basic_rule.setup(M.config) + + if M.config.check_ts then + local ok, ts_rule = pcall(require, 'nvim-autopairs.rules.ts_basic') + if ok then + highlighter = require "vim.treesitter.highlighter" + M.config.rules = ts_rule.setup(M.config) + end + end + + if M.config.map_cr then + M.map_cr() + end + + M.force_attach() + local group = api.nvim_create_augroup('autopairs_buf', { clear = true }) + api.nvim_create_autocmd({ 'BufEnter', 'BufWinEnter' }, { + group = group, pattern = '*', + callback = function() M.on_attach() end + }) + api.nvim_create_autocmd('BufDelete', { + group = group, pattern = '*', + callback = function(data) + M.set_buf_rule(nil, tonumber(data.buf) or 0) + end, + }) + api.nvim_create_autocmd('FileType', { + group = group, pattern = '*', + callback = function() M.force_attach() end + }) +end + +M.add_rule = function(rule) + M.add_rules({ rule }) +end + +M.get_rule = function(start_pair) + local tbl = {} + for _, r in pairs(M.config.rules) do + if r.start_pair == start_pair then + table.insert(tbl, r) + end + end + if #tbl == 1 then + return tbl[1] + end + return tbl +end + +M.remove_rule = function(pair) + local tbl = {} + for _, r in pairs(M.config.rules) do + if r.start_pair ~= pair then + table.insert(tbl, r) + end + end + M.config.rules = tbl + if M.state.rules then + local state_tbl = {} + local rules = M.get_buf_rules() + for _, r in pairs(rules) do + if r.start_pair ~= pair then + table.insert(state_tbl, r) + elseif r.key_map and r.key_map ~= '' then + api.nvim_buf_del_keymap(0, 'i', r.key_map) + end + end + M.set_buf_rule(state_tbl, 0) + end + M.force_attach() +end + +M.add_rules = function(rules) + for _, rule in pairs(rules) do + table.insert(M.config.rules, rule) + end + M.force_attach() +end + +M.clear_rules = function() + M.state.rules = {} + M.config.rules = {} +end + +M.disable = function() + M.state.disabled = true +end + +M.enable = function() + M.state.disabled = false +end + +--- force remap key to buffer +M.force_attach = function(bufnr) + utils.set_attach(bufnr, 0) + M.on_attach(bufnr) +end + +local del_keymaps = function() + local status, autopairs_keymaps = pcall(api.nvim_buf_get_var, 0, 'autopairs_keymaps') + if status and autopairs_keymaps and #autopairs_keymaps > 0 then + for _, key in pairs(autopairs_keymaps) do + pcall(api.nvim_buf_del_keymap, 0, 'i', key) + end + end +end + +local function is_disable() + if M.state.disabled then + return true + end + + if vim.bo.filetype == '' and api.nvim_win_get_config(0).relative ~= '' then + -- disable for any floating window without filetype + return true + end + + if vim.bo.modifiable == false then + return true + end + + if M.config.disable_in_macro + and (vim.fn.reg_recording() ~= '' or vim.fn.reg_executing() ~= '') + then + return true + end + + if M.config.disable_in_replace_mode and vim.api.nvim_get_mode().mode == "R" then + return true + end + + if M.config.disable_in_visualblock and utils.is_block_wise_mode() then + return true + end + + if utils.check_filetype(M.config.disable_filetype, vim.bo.filetype) then + del_keymaps() + M.set_buf_rule({}, 0) + return true + end + return false +end + +---@return table +M.get_buf_rules = function(bufnr) + return M.state.rules[bufnr or api.nvim_get_current_buf()] or {} +end + +---@param rules nil|table list or rule +---@param bufnr number buffer number +M.set_buf_rule = function(rules, bufnr) + if bufnr == 0 or bufnr == nil then + bufnr = api.nvim_get_current_buf() + end + M.state.rules[bufnr] = rules +end + +M.on_attach = function(bufnr) + -- log.debug('on_attach' .. vim.bo.filetype) + if is_disable() then + return + end + bufnr = bufnr or api.nvim_get_current_buf() + + local rules = {} + for _, rule in pairs(M.config.rules) do + if utils.check_filetype(rule.filetypes, vim.bo.filetype) + and utils.check_not_filetype(rule.not_filetypes, vim.bo.filetype) + then + table.insert(rules, rule) + end + end + -- sort by length + table.sort(rules, function(a, b) + if a.start_pair == b.start_pair then + if not b.key_map then + return a.key_map and 1 + end + if not a.key_map then + return b.key_map and -1 + end + return #a.key_map < #b.key_map + end + if #a.start_pair == #b.start_pair then + return string.byte(a.start_pair) > string.byte(b.start_pair) + end + return #a.start_pair > #b.start_pair + end) + + M.set_buf_rule(rules, bufnr) + + if M.config.check_ts then + if highlighter and highlighter.active[bufnr] then + M.state.ts_node = M.config.ts_config[vim.bo.filetype] + else + M.state.ts_node = nil + end + end + + if utils.is_attached(bufnr) then + return + end + del_keymaps() + local enable_insert_auto = false + local autopairs_keymaps = {} + local expr_map = function(key) + api.nvim_buf_set_keymap(bufnr, 'i', key, '', { + expr = true, + noremap = true, + desc = "autopairs map key", + callback = function() return M.autopairs_map(bufnr, key) end, + }) + table.insert(autopairs_keymaps, key) + end + for _, rule in pairs(rules) do + if rule.key_map ~= nil then + if rule.is_regex == false then + if rule.key_map == '' then + rule.key_map = rule.start_pair:sub(#rule.start_pair) + end + expr_map(rule.key_map) + local key_end = rule.key_end or rule.end_pair:sub(1, 1) + if #key_end >= 1 and key_end ~= rule.key_map and rule.move_cond ~= nil then + expr_map(key_end) + end + else + if rule.key_map ~= '' then + expr_map(rule.key_map) + elseif rule.is_endwise == false then + enable_insert_auto = true + end + end + end + end + api.nvim_buf_set_var(bufnr, 'autopairs_keymaps', autopairs_keymaps) + + if enable_insert_auto then + -- capture all key use it to trigger regex pairs + -- it can make an issue with paste from register + api.nvim_create_autocmd('InsertCharPre', { + group = api.nvim_create_augroup(string.format("autopairs_insert_%d", bufnr), { clear = true }), + buffer = bufnr, + callback = function() + M.autopairs_insert(bufnr, vim.v.char) + end + }) + end + + if M.config.fast_wrap and M.config.fast_wrap.map then + api.nvim_buf_set_keymap( + bufnr, + 'i', + M.config.fast_wrap.map, + "llua require('nvim-autopairs.fastwrap').show()", + { noremap = true } + ) + end + + if M.config.map_bs then + api.nvim_buf_set_keymap( + bufnr, + 'i', + '', + string.format('v:lua.MPairs.autopairs_bs(%d)', bufnr), + { expr = true, noremap = true } + ) + end + + if M.config.map_c_h then + api.nvim_buf_set_keymap( + bufnr, + "i", + utils.key.c_h, + string.format("v:lua.MPairs.autopairs_c_h(%d)", bufnr), + { expr = true, noremap = true } + ) + end + + if M.config.map_c_w then + api.nvim_buf_set_keymap( + bufnr, + 'i', + '', + string.format('v:lua.MPairs.autopairs_c_w(%d)', bufnr), + { expr = true, noremap = true } + ) + end + api.nvim_buf_set_var(bufnr, 'nvim-autopairs', 1) +end + +local autopairs_delete = function(bufnr, key) + if is_disable() then + return utils.esc(key) + end + bufnr = bufnr or api.nvim_get_current_buf() + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + local rules = M.get_buf_rules(bufnr) + for _, rule in pairs(rules) do + if rule.start_pair then + local prev_char, next_char = utils.text_cusor_line( + line, + col, + #rule.start_pair, + #rule.end_pair, + rule.is_regex + ) + if utils.compare(rule.start_pair, prev_char, rule.is_regex) + and utils.compare(rule.end_pair, next_char, rule.is_regex) + and rule:can_del({ + ts_node = M.state.ts_node, + rule = rule, + bufnr = bufnr, + prev_char = prev_char, + next_char = next_char, + line = line, + col = col, + }) + then + local input = '' + for _ = 1, api.nvim_strwidth(rule.start_pair), 1 do + input = input .. utils.key.bs + end + for _ = 1, api.nvim_strwidth(rule.end_pair), 1 do + input = input .. utils.key.del + end + return utils.esc('U' .. input) + end + end + end + return utils.esc(key) +end + +M.autopairs_c_w = function(bufnr) + return autopairs_delete(bufnr, 'U') +end + +M.autopairs_c_h = function(bufnr) + return autopairs_delete(bufnr, utils.key.c_h) +end + +M.autopairs_bs = function(bufnr) + return autopairs_delete(bufnr, utils.key.bs) +end + +M.autopairs_map = function(bufnr, char) + if is_disable() then + return char + end + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + local new_text = '' + local add_char = 1 + local rules = M.get_buf_rules(bufnr) + for _, rule in pairs(rules) do + if rule.start_pair then + if char:match('<.*>') then + new_text = line + add_char = 0 + else + new_text = line:sub(1, col) .. char .. line:sub(col + 1, #line) + add_char = rule.key_map and #rule.key_map or 1 + end + + -- log.debug("new_text:[" .. new_text .. "]") + local prev_char, next_char = utils.text_cusor_line( + new_text, + col + add_char, + #rule.start_pair, + #rule.end_pair, + rule.is_regex + ) + local cond_opt = { + ts_node = M.state.ts_node, + text = new_text, + rule = rule, + bufnr = bufnr, + col = col + 1, + char = char, + line = line, + prev_char = prev_char, + next_char = next_char, + } + -- log.debug("start_pair" .. rule.start_pair) + -- log.debug('prev_char' .. prev_char) + -- log.debug('next_char' .. next_char) + if utils.compare(rule.end_pair, next_char, rule.is_regex) + and rule:can_move(cond_opt) + then + local end_pair = rule:get_end_pair(cond_opt) + local end_pair_length = rule:get_end_pair_length(end_pair) + return utils.esc(utils.repeat_key(utils.key.join_right, end_pair_length)) + end + + if rule.key_map == char + and utils.compare(rule.start_pair, prev_char, rule.is_regex) + and rule:can_pair(cond_opt) + then + local end_pair = rule:get_end_pair(cond_opt) + local end_pair_length = rule:get_end_pair_length(end_pair) + local move_text = utils.repeat_key(utils.key.join_left, end_pair_length) + if add_char == 0 then + move_text = '' + char = '' + end + if end_pair:match('<.*>') then + end_pair = utils.esc(end_pair) + end + local result = char .. end_pair .. utils.esc(move_text) + if rule.is_undo then + result = utils.esc(utils.key.undo_sequence) .. result .. utils.esc(utils.key.undo_sequence) + end + if M.config.enable_abbr then + result = utils.esc(utils.key.abbr) .. result + end + log.debug("key_map :" .. result) + return result + end + end + end + return M.autopairs_afterquote(new_text, utils.esc(char)) +end + +M.autopairs_insert = function(bufnr, char) + if is_disable() then + return char + end + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + local new_text = line:sub(1, col) .. char .. line:sub(col + 1, #line) + local rules = M.get_buf_rules(bufnr) + for _, rule in pairs(rules) do + if rule.start_pair and rule.is_regex and rule.key_map == '' then + local prev_char, next_char = utils.text_cusor_line( + new_text, + col + 1, + #rule.start_pair, + #rule.end_pair, + rule.is_regex + ) + local cond_opt = { + ts_node = M.state.ts_node, + text = new_text, + rule = rule, + bufnr = bufnr, + col = col + 1, + char = char, + line = line, + prev_char = prev_char, + next_char = next_char, + } + -- log.debug("start_pair" .. rule.start_pair) + -- log.debug('prev_char' .. prev_char) + -- log.debug('next_char' .. next_char) + if next_char == rule.end_pair and rule:can_move(cond_opt) then + utils.set_vchar('') + vim.schedule(function() + utils.feed(utils.key.right, -1) + end) + return false + end + + if utils.compare(rule.start_pair, prev_char, rule.is_regex) + and rule:can_pair(cond_opt) + then + local end_pair = rule:get_end_pair(cond_opt) + utils.set_vchar(char .. end_pair) + vim.schedule(function() + utils.feed(utils.key.left, rule:get_end_pair_length(end_pair)) + end) + return + end + end + end + return char +end + +M.autopairs_cr = function(bufnr) + if is_disable() then + return utils.esc('') + end + bufnr = bufnr or api.nvim_get_current_buf() + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + -- log.debug("on_cr") + local rules = M.get_buf_rules(bufnr) + for _, rule in pairs(rules) do + if rule.start_pair then + local prev_char, next_char = utils.text_cusor_line( + line, + col, + #rule.start_pair, + #rule.end_pair, + rule.is_regex + ) + + local cond_opt = { + ts_node = M.state.ts_node, + check_endwise_ts = true, + rule = rule, + bufnr = bufnr, + col = col, + line = line, + prev_char = prev_char, + next_char = next_char, + } + -- log.debug('prev_char' .. rule.start_pair) + -- log.debug('prev_char' .. prev_char) + -- log.debug('next_char' .. next_char) + if rule.is_endwise + and utils.compare(rule.start_pair, prev_char, rule.is_regex) + and rule:can_cr(cond_opt) + then + local end_pair = rule:get_end_pair(cond_opt) + local end_pair_length = rule:get_end_pair_length(end_pair) + return utils.esc( + end_pair + .. utils.repeat_key(utils.key.join_left, end_pair_length) + -- FIXME do i need to re indent twice #118 + .. '====O' + ) + end + + cond_opt.check_endwise_ts = false + + if utils.compare(rule.start_pair, prev_char, rule.is_regex) + and utils.compare(rule.end_pair, next_char, rule.is_regex) + and rule:can_cr(cond_opt) + then + log.debug('do_cr') + return utils.esc(rule:get_map_cr({ rule = rule, line = line, color = col, bufnr = bufnr })) + end + end + end + return utils.esc('') +end + +--- add bracket pairs after quote (|"aaaaa" => (|"aaaaaa") +M.autopairs_afterquote = function(line, key_char) + if M.config.enable_afterquote and not utils.is_block_wise_mode() then + line = line or utils.text_get_current_line(0) + local _, col = utils.get_cursor() + local prev_char, next_char = utils.text_cusor_line(line, col + 1, 1, 1, false) + if utils.is_bracket(prev_char) + and utils.is_quote(next_char) + and not utils.is_in_quotes(line, col, next_char) + then + local count = 0 + local index = 0 + local is_prev_slash = false + local char_end = '' + for i = col, #line, 1 do + local char = line:sub(i, i + #next_char - 1) + if not is_prev_slash and char == next_char then + count = count + 1 + char_end = line:sub(i + 1, i + #next_char) + index = i + end + is_prev_slash = char == '\\' + end + if count == 2 and index >= (#line - 2) then + local rules = M.get_buf_rules(api.nvim_get_current_buf()) + for _, rule in pairs(rules) do + if rule.start_pair == prev_char and char_end ~= rule.end_pair then + local new_text = line:sub(0, index) + .. rule.end_pair + .. line:sub(index + 1, #line) + M.state.expr_quote = new_text + local append = 'a' + if col > 0 then + append = 'la' + end + return utils.esc( + 'lua MPairs.autopairs_closequote_expr()' .. append + ) + end + end + end + end + end + return key_char +end + +M.autopairs_closequote_expr = function() + vim.fn.setline('.', M.state.expr_quote) +end + +M.check_break_line_char = function() + return M.autopairs_cr() +end + +M.map_cr = function() + M.completion_confirm = function() + if vim.fn.pumvisible() ~= 0 then + return M.esc("") + else + return M.autopairs_cr() + end + end + api.nvim_set_keymap( + 'i', + '', + 'v:lua.MPairs.completion_confirm()', + { expr = true, noremap = true } + ) +end + +M.esc = utils.esc +_G.MPairs = M +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/_log.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/_log.lua new file mode 100644 index 0000000..25061ef --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/_log.lua @@ -0,0 +1,13 @@ +---@diagnostic disable: undefined-field +local empty = { + debug = function(_) end, + info = function(_) end, + error = function(_) end, +} +if _G.__is_log then + return require('plenary.log').new { + plugin = 'nvim-autopairs', + level = (_G.__is_log == true and 'debug') or 'warn', + } or empty +end +return empty diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/cmp.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/cmp.lua new file mode 100644 index 0000000..d658e3e --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/cmp.lua @@ -0,0 +1,76 @@ +local autopairs = require('nvim-autopairs') +local handlers = require('nvim-autopairs.completion.handlers') +local cmp = require('cmp') + +local Kind = cmp.lsp.CompletionItemKind + +local M = {} + +M.filetypes = { + -- Alias to all filetypes + ["*"] = { + ["("] = { + kind = { Kind.Function, Kind.Method }, + handler = handlers["*"] + } + }, + clojure = { + ["("] = { + kind = { Kind.Function, Kind.Method }, + handler = handlers.lisp + } + }, + clojurescript = { + ["("] = { + kind = { Kind.Function, Kind.Method }, + handler = handlers.lisp + } + }, + fennel = { + ["("] = { + kind = { Kind.Function, Kind.Method }, + handler = handlers.lisp + } + }, + janet = { + ["("] = { + kind = { Kind.Function, Kind.Method }, + handler = handlers.lisp + } + }, + tex = false +} + +M.on_confirm_done = function(opts) + opts = vim.tbl_deep_extend('force', { + filetypes = M.filetypes + }, opts or {}) + + return function(evt) + local entry = evt.entry + local commit_character = evt.commit_character + local bufnr = vim.api.nvim_get_current_buf() + local filetype = vim.api.nvim_buf_get_option(bufnr, 'filetype') + local item = entry:get_completion_item() + + -- Without options and fallback + if not opts.filetypes[filetype] and not opts.filetypes["*"] then + return + end + + if opts.filetypes[filetype] == false then + return + end + + -- If filetype is nil then use * + local completion_options = opts.filetypes[filetype] or opts.filetypes["*"] + + for char, value in pairs(completion_options) do + if vim.tbl_contains(value.kind, item.kind) then + value.handler(char, item, bufnr, commit_character) + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/compe.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/compe.lua new file mode 100644 index 0000000..333e2ea --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/compe.lua @@ -0,0 +1,86 @@ +local npairs = require('nvim-autopairs') +local Completion = require('compe.completion') +local utils = require('nvim-autopairs.utils') + +local method_kind = nil +local function_kind = nil + +local options = {} + +_G.MPairs.completion_done = function() + local line = utils.text_get_current_line(0) + local _, col = utils.get_cursor() + local prev_char, next_char = utils.text_cusor_line(line, col, 1, 1, false) + + local filetype = vim.bo.filetype + local char = options.map_char[filetype] or options.map_char["all"] or '(' + if char == '' then return end + + if prev_char ~= char and next_char ~= char then + if method_kind == nil then + method_kind = require('vim.lsp.protocol').CompletionItemKind[2] + function_kind = require('vim.lsp.protocol').CompletionItemKind[3] + end + local item = Completion._confirm_item + if item.kind == method_kind or item.kind == function_kind then + -- check insert text have ( from snippet + local completion_item = item.user_data.compe.completion_item + if + ( + completion_item.textEdit + and completion_item.textEdit.newText + and completion_item.textEdit.newText:match('[%(%[%$]') + ) + or (completion_item.insertText and completion_item.insertText:match('[%(%[%$]')) + then + return + end + vim.api.nvim_feedkeys(char, 'i', true) + end + end +end + +local M = {} +M.setup = function(opt) + opt = opt or { map_cr = true, map_complete = true, auto_select = false, map_char = {all = '('}} + if not opt.map_char then opt.map_char = {} end + options = opt + local map_cr = opt.map_cr + local map_complete = opt.map_complete + vim.g.completion_confirm_key = '' + if map_cr then + vim.api.nvim_set_keymap( + 'i', + '', + 'v:lua.MPairs.completion_confirm()', + { expr = true, noremap = true } + ) + end + if opt.auto_select then + _G.MPairs.completion_confirm = function() + if vim.fn.pumvisible() ~= 0 then + return vim.fn['compe#confirm']({ keys = '', select = true }) + else + return npairs.autopairs_cr() + end + end + else + _G.MPairs.completion_confirm = function() + if vim.fn.pumvisible() ~= 0 and vim.fn.complete_info()['selected'] ~= -1 then + return vim.fn['compe#confirm'](npairs.esc('')) + else + return npairs.autopairs_cr() + end + end + end + + if map_complete then + vim.cmd([[ + augroup autopairs_compe + autocmd! + autocmd User CompeConfirmDone call v:lua.MPairs.completion_done() + augroup end + ]]) + end +end +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/handlers.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/handlers.lua new file mode 100644 index 0000000..c02681f --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/completion/handlers.lua @@ -0,0 +1,52 @@ +-- local autopairs = require('nvim-autopairs') +local utils = require('nvim-autopairs.utils') + +local M = {} + +---@param char string +---@param item table +---@param bufnr number +M["*"] = function(char, item, bufnr, commit_character) + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + local char_before, char_after = utils.text_cusor_line(line, col, 1, 1, false) + + if char == '' or char_before == char or char_after == char + or (item.data and type(item.data) == 'table' and item.data.funcParensDisabled) + or (item.textEdit and item.textEdit.newText and item.textEdit.newText:match "[%(%[%$]") + or (item.insertText and item.insertText:match "[%(%[%$]") + or char == commit_character + then + return + end + + vim.api.nvim_feedkeys(char, "i", true) +end + +---Handler with "clojure", "clojurescript", "fennel", "janet +M.lisp = function (char, item, bufnr, commit_character) + local line = utils.text_get_current_line(bufnr) + local _, col = utils.get_cursor() + local char_before, char_after = utils.text_cusor_line(line, col, 1, 1, false) + local length = #item.label + + if char == '' or char_before == char or char_after == char + or (item.data and item.data.funcParensDisabled) + or (item.textEdit and item.textEdit.newText and item.textEdit.newText:match "[%(%[%$]") + or (item.insertText and item.insertText:match "[%(%[%$]") + or char == commit_character + then + return + end + + if utils.text_sub_char(line, col - length, 1) == "(" then + utils.feed("") + return + end + utils.feed(utils.key.left, length) + utils.feed(char) + utils.feed(utils.key.right, length) + utils.feed("") +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/conds.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/conds.lua new file mode 100644 index 0000000..1e81b33 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/conds.lua @@ -0,0 +1,364 @@ +local utils = require('nvim-autopairs.utils') +local log = require('nvim-autopairs._log') +---@class CondOpts +---@field ts_node table +---@field text string +---@field rule table +---@field bufnr number +---@field col number +---@field char string +---@field line string +---@field prev_char string +---@field next_char string +---@field is_endwise string + +local cond = {} + +-- cond +-- @return false when it is not correct +-- true when it is correct +-- nil when it is not determine +-- stylua: ignore +cond.none = function() + return function() return false end +end +-- stylua: ignore +cond.done = function() + return function() return true end +end + +cond.invert = function(func) + return function(...) + local result = func(...) + if result ~= nil then + return not result + end + return nil + end +end + +cond.before_regex = function(regex, length) + length = length or 1 + if not regex then + return cond.none() + end + ---@param opts CondOpts + return function(opts) + log.debug('before_regex') + if length < 0 then + length = opts.col + end + local str = utils.text_sub_char(opts.line, opts.col - 1, -length) + if str:match(regex) then + return true + end + return false + end +end + +cond.before_text = function(text) + local length = #text + ---@param opts CondOpts + return function(opts) + log.debug('before_text') + local str = utils.text_sub_char(opts.line, opts.col - 1, -length) + if str == text then + return true + end + return false + end +end + +cond.after_text = function(text) + local length = #text + ---@param opts CondOpts + return function(opts) + log.debug('after_text') + local str = utils.text_sub_char(opts.line, opts.col, length) + if str == text then + return true + end + return false + end +end + +cond.after_regex = function(regex, length) + length = length or 1 + if not regex then + return cond.none() + end + ---@param opts CondOpts + return function(opts) + log.debug('after_regex') + if length < 0 then + length = #opts.line + end + local str = utils.text_sub_char(opts.line, opts.col, length) + if str:match(regex) then + return true + end + return false + end +end + +cond.not_before_text = function(text) + local length = #text + return function(opts) + log.debug('not_before_text') + local str = utils.text_sub_char(opts.line, opts.col - 1, -length) + if str == text then + return false + end + end +end + +cond.not_after_text = function(text) + local length = #text + ---@param opts CondOpts + return function(opts) + log.debug('not_after_text') + local str = utils.text_sub_char(opts.line, opts.col, length) + if str == text then + return false + end + end +end + +cond.not_before_regex = function(regex, length) + length = length or 1 + if not regex then + return cond.none() + end + ---@param opts CondOpts + return function(opts) + log.debug('not_before_regex') + if length < 0 then + length = opts.col + end + local str = utils.text_sub_char(opts.line, opts.col - 1, -length) + if str:match(regex) then + return false + end + end +end + +cond.not_after_regex = function(regex, length) + length = length or 1 + if not regex then + return cond.none() + end + ---@param opts CondOpts + return function(opts) + log.debug('not_after_regex') + if length < 0 then + length = #opts.line + end + local str = utils.text_sub_char(opts.line, opts.col, length) + if str:match(regex) then + return false + end + end +end + +local function count_bracket_char(line, prev_char, next_char) + local count_prev_char = 0 + local count_next_char = 0 + for i = 1, #line, 1 do + local c = line:sub(i, i) + if c == prev_char then + count_prev_char = count_prev_char + 1 + elseif c == next_char then + count_next_char = count_next_char + 1 + end + end + return count_prev_char, count_next_char +end + +-- Checks if bracket chars are balanced around specific postion. +---@param line string +---@param open_char string +---@param close_char string +---@param col integer position +local function is_brackets_balanced_around_position(line, open_char, close_char, col) + local balance = 0 + for i = 1, #line, 1 do + local c = line:sub(i, i) + if c == open_char then + balance = balance + 1 + elseif balance > 0 and c == close_char then + balance = balance - 1 + if col <= i and balance == 0 then + break + end + end + end + return balance == 0 +end + +cond.is_bracket_line = function() + ---@param opts CondOpts + return function(opts) + log.debug('is_bracket_line') + if utils.is_bracket(opts.char) and opts.next_char == opts.rule.end_pair then + -- (( many char |)) => add + -- ( many char |)) => not add + local count_prev_char, count_next_char = count_bracket_char( + opts.line, + opts.char, + opts.next_char + ) + if count_prev_char ~= count_next_char then + return false + end + end + end +end + +cond.is_bracket_line_move = function() + ---@param opts CondOpts + return function(opts) + log.debug('is_bracket_line_move') + if utils.is_close_bracket(opts.char) + and opts.char == opts.rule.end_pair + then + -- (( many char |)) => move + -- (( many char |) => not move + local is_balanced = is_brackets_balanced_around_position( + opts.line, + opts.rule.start_pair, + opts.char, + opts.col + ) + return is_balanced + end + end +end + +cond.not_inside_quote = function() + ---@param opts CondOpts + return function(opts) + log.debug('not_inside_quote') + if utils.is_in_quotes(opts.text, opts.col - 1) then + return false + end + end +end + +cond.is_inside_quote = function() + ---@param opts CondOpts + return function(opts) + log.debug('is_inside_quote') + if utils.is_in_quotes(opts.text, opts.col - 1) then + return true + end + end +end + +cond.not_add_quote_inside_quote = function() + ---@param opts CondOpts + return function(opts) + log.debug('not_add_quote_inside_quote') + if utils.is_quote(opts.char) + and utils.is_in_quotes(opts.text, opts.col - 1) + then + return false + end + end +end + +cond.move_right = function() + ---@param opts CondOpts + return function(opts) + log.debug('move_right') + if opts.next_char == opts.char then + if utils.is_close_bracket(opts.char) then + return + end + -- move right when have quote on end line or in quote + -- situtaion |" => "| + if utils.is_quote(opts.char) then + if opts.col == string.len(opts.line) then + return + end + -- ("|") => (""|) + -- "" |" " => "" "| " + if utils.is_in_quotes(opts.line, opts.col - 1, opts.char) then + return + end + end + end + return false + end +end + +cond.is_end_line = function() + ---@param opts CondOpts + return function(opts) + log.debug('is_end_line') + local end_text = opts.line:sub(opts.col + 1) + -- end text is blank + if end_text ~= '' and end_text:match('^%s+$') == nil then + return false + end + end +end + +--- Check the next char is quote and cursor is inside quote +cond.is_bracket_in_quote = function() + ---@param opts CondOpts + return function(opts) + log.debug("is_bracket_in_quote") + if utils.is_bracket(opts.char) + and utils.is_quote(opts.next_char) + and utils.is_in_quotes(opts.line, opts.col - 1, opts.next_char) + then + return true + end + end +end + +cond.not_filetypes = function(filetypes) + return function() + log.debug('not_filetypes') + for _, filetype in pairs(filetypes) do + if vim.bo.filetype == filetype then + return false + end + end + end +end + +--- Check the character before the cursor is not equal +---@param char string character to compare +---@param index number the position of character before current curosr +cond.not_before_char = function(char, index) + index = index or 1 + ---@param opts CondOpts + return function(opts) + log.debug('not_before_char') + local match_char = #opts.line > index + and opts.line:sub(#opts.line - index, #opts.line - index) or '' + if match_char == char and match_char ~= "" then + return false + end + end +end + +---@deprecated +cond.not_after_regex_check = cond.not_after_regex +---@deprecated +cond.after_regex_check = cond.after_regex +---@deprecated +cond.before_regex_check = cond.before_regex +---@deprecated +cond.not_before_regex_check = cond.not_before_regex +---@deprecated +cond.after_text_check = cond.after_text +---@deprecated +cond.not_after_text_check = cond.not_after_text +---@deprecated +cond.before_text_check = cond.before_text +---@deprecated +cond.not_before_text_check = cond.not_before_text + +return cond diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/fastwrap.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/fastwrap.lua new file mode 100644 index 0000000..da0d032 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/fastwrap.lua @@ -0,0 +1,148 @@ +local utils = require('nvim-autopairs.utils') +local log = require('nvim-autopairs._log') +local npairs = require('nvim-autopairs') +local M = {} + +local default_config = { + map = '', + chars = { '{', '[', '(', '"', "'" }, + pattern = [=[[%'%"%)%>%]%)%}%,]]=], + end_key = '$', + keys = 'qwertyuiopzxcvbnmasdfghjkl', + highlight = 'Search', + highlight_grey = 'Comment', +} + +M.ns_fast_wrap = vim.api.nvim_create_namespace('autopairs_fastwrap') + +local config = {} + +M.setup = function(cfg) + if config.chars == nil then + config = vim.tbl_extend('force', default_config, cfg or {}) or {} + npairs.config.fast_wrap = config + end +end + +function M.getchar_handler() + local ok, key = pcall(vim.fn.getchar) + if not ok then + return nil + end + if type(key) == 'number' then + local key_str = vim.fn.nr2char(key) + return key_str + end + return nil +end + +M.show = function(line) + line = line or utils.text_get_current_line(0) + log.debug(line) + local row, col = utils.get_cursor() + local prev_char = utils.text_cusor_line(line, col, 1, 1, false) + local end_pair = '' + if utils.is_in_table(config.chars, prev_char) then + local rules = npairs.get_buf_rules() + for _, rule in pairs(rules) do + if rule.start_pair == prev_char then + end_pair = rule.end_pair + end + end + if end_pair == '' then + return + end + local list_pos = {} + local index = 1 + local str_length = #line + local offset = -1 + for i = col + 2, #line, 1 do + local char = line:sub(i, i) + local char2 = line:sub(i - 1, i) + if string.match(char, config.pattern) + or (char == ' ' and string.match(char2, '%w')) + then + local key = config.keys:sub(index, index) + index = index + 1 + if utils.is_quote(char) + or ( + utils.is_close_bracket(char) + and utils.is_in_quotes(line, col, prev_char) + ) + then + offset = 0 + end + + table.insert( + list_pos, + { col = i + offset, key = key, char = char, pos = i } + ) + end + end + + table.insert( + list_pos, + { col = str_length + 1, key = config.end_key, pos = str_length + 1 } + ) + + M.highlight_wrap(list_pos, row, col, #line) + vim.defer_fn(function() + local char = #list_pos == 1 and config.end_key or M.getchar_handler() + vim.api.nvim_buf_clear_namespace(0, M.ns_fast_wrap, row, row + 1) + for _, pos in pairs(list_pos) do + if char == pos.key then + M.move_bracket(line, pos.col, end_pair, false) + break + end + if char == string.upper(pos.key) then + M.move_bracket(line, pos.col, end_pair, true) + break + end + end + vim.cmd('startinsert') + end, 10) + return + end + vim.cmd('startinsert') +end + +M.move_bracket = function(line, target_pos, end_pair, change_pos) + log.debug(target_pos) + line = line or utils.text_get_current_line(0) + local row, col = utils.get_cursor() + local _, next_char = utils.text_cusor_line(line, col, 1, 1, false) + -- remove an autopairs if that exist + if next_char == end_pair then + line = line:sub(1, col) .. line:sub(col + 2, #line) + target_pos = target_pos - 1 + end + + line = line:sub(1, target_pos) .. end_pair .. line:sub(target_pos + 1, #line) + vim.api.nvim_set_current_line(line) + if change_pos then + vim.api.nvim_win_set_cursor(0, { row + 1, target_pos }) + end +end + +M.highlight_wrap = function(tbl_pos, row, col, end_col) + local bufnr = vim.api.nvim_win_get_buf(0) + if config.highlight_grey then + vim.highlight.range( + bufnr, + M.ns_fast_wrap, + config.highlight_grey, + { row, col }, + { row, end_col }, + {} + ) + end + for _, pos in ipairs(tbl_pos) do + vim.api.nvim_buf_set_extmark(bufnr, M.ns_fast_wrap, row, pos.pos - 1, { + virt_text = { { pos.key, config.highlight } }, + virt_text_pos = 'overlay', + hl_mode = 'blend', + }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rule.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rule.lua new file mode 100644 index 0000000..643e9c3 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rule.lua @@ -0,0 +1,221 @@ +local Cond = require('nvim-autopairs.conds') + +--- @class Rule +--- @field start_pair string +--- @field end_pair string +--- @field end_pair_func function dynamic change end_pair +--- @field map_cr_func function dynamic change mapping_cr +--- @field end_pair_length number change end_pair length for key map like +--- @field key_map string|nil equal nil mean it will skip on autopairs map +--- @field filetypes table|nil +--- @field not_filetypes table|nil +--- @field is_regex boolean use regex to compare +--- @field is_multibyte boolean +--- @field is_endwise boolean only use on end_wise +--- @field is_undo boolean add break undo sequence +local Rule = {} +Rule.__index = Rule + +---@return Rule +function Rule.new(...) + local params = { ... } + local opt = {} + if type(params[1]) == 'table' then + opt = params[1] + else + opt.start_pair = params[1] + opt.end_pair = params[2] + if type(params[3]) == 'string' then + opt.filetypes = { params[3] } + else + opt.filetypes = params[3] + end + end + opt = vim.tbl_extend('force', { + key_map = "", + start_pair = nil, + end_pair = nil, + end_pair_func = false, + filetypes = nil, + not_filetypes = nil, + move_cond = nil, + del_cond = {}, + cr_cond = {}, + pair_cond = {}, + is_endwise = false, + is_regex = false, + is_multibyte = false, + end_pair_length = nil, + }, opt) or {} + + ---@param rule Rule + local function constructor(rule) + -- check multibyte + if #rule.start_pair ~= vim.api.nvim_strwidth(rule.start_pair) then + rule:use_multibyte() + end + -- check filetypes and not_filetypes + -- if have something like "-vim" it will add to not_filetypes + if rule.filetypes then + local ft, not_ft = {}, {} + for _, value in pairs(rule.filetypes) do + if value:sub(1, 1) == '-' then + table.insert(not_ft, value:sub(2, #value)) + else + table.insert(ft, value) + end + end + rule.filetypes = #ft > 0 and ft or nil + rule.not_filetypes = #not_ft > 0 and not_ft or nil + end + return rule + end + + local r = setmetatable(opt, { __index = Rule }) + return constructor(r) +end + +function Rule:use_regex(value, key_map) + self.is_regex = value + self.key_map = key_map or '' + return self +end + +function Rule:use_key(key_map) + self.key_map = key_map or '' + return self +end + +function Rule:use_undo(value) + self.is_undo = value + return self +end + +function Rule:use_multibyte() + self.is_multibyte = true + self.end_pair_length = vim.fn.strdisplaywidth(self.end_pair) + self.key_map = string.match(self.start_pair, "[^\128-\191][\128-\191]*$") + self.key_end = string.match(self.end_pair, "[%z\1-\127\194-\244][\128-\191]*") + return self +end + +function Rule:get_end_pair(opts) + if self.end_pair_func then + return self.end_pair_func(opts) + end + return self.end_pair +end + +function Rule:get_map_cr(opts) + if self.map_cr_func then + return self.map_cr_func(opts) + end + return 'uO' +end + +function Rule:replace_map_cr(value) + self.map_cr_func = value + return self +end + +function Rule:get_end_pair_length(opts) + if self.end_pair_length then + return self.end_pair_length + end + if type(opts) == 'string' then + return #opts + end + return #self.get_end_pair(opts) +end + +function Rule:replace_endpair(value, check_pair) + self.end_pair_func = value + if check_pair ~= nil then + if check_pair == true then + self:with_pair(Cond.after_text(self.end_pair)) + else + self:with_pair(check_pair) + end + end + return self +end + +function Rule:set_end_pair_length(length) + self.end_pair_length = length + return self +end + +function Rule:with_move(cond) + if self.move_cond == nil then self.move_cond = {} end + table.insert(self.move_cond, cond) + return self +end + +function Rule:with_del(cond) + if self.del_cond == nil then self.del_cond = {} end + table.insert(self.del_cond, cond) + return self +end + +function Rule:with_cr(cond) + if self.cr_cond == nil then self.cr_cond = {} end + table.insert(self.cr_cond, cond) + return self +end + +---add condition to rule +---@param cond any +---@param pos number|nil = 1. It have higher priority to another condition +---@return Rule +function Rule:with_pair(cond, pos) + if self.pair_cond == nil then self.pair_cond = {} end + self.pair_cond[pos or (#self.pair_cond+1)] = cond + return self +end + +function Rule:only_cr(cond) + self.key_map = nil + self.pair_cond = false + self.move_cond = false + self.del_cond = false + if cond then return self:with_cr(cond) end + return self +end + +function Rule:end_wise(cond) + self.is_endwise = true + return self:only_cr(cond) +end + +local function can_do(conds, opt) + if type(conds) == 'table' then + for _, cond in pairs(conds) do + local result = cond(opt) + if result ~= nil then + return result + end + end + return true + elseif type(conds) == 'function' then + return conds(opt) == true + end + return false +end + +function Rule:can_pair(opt) + return can_do(self.pair_cond, opt) +end + +function Rule:can_move(opt) + return can_do(self.move_cond, opt) +end + +function Rule:can_del(opt) + return can_do(self.del_cond, opt) +end + +function Rule:can_cr(opt) + return can_do(self.cr_cond, opt) +end + +return Rule.new diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/basic.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/basic.lua new file mode 100644 index 0000000..b5c347f --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/basic.lua @@ -0,0 +1,65 @@ +local Rule = require('nvim-autopairs.rule') +local cond = require('nvim-autopairs.conds') + +local function setup(opt) + local basic = function(...) + local move_func = opt.enable_moveright and cond.move_right or cond.none + local rule = Rule(...) + :with_move(move_func()) + :with_pair(cond.not_add_quote_inside_quote()) + + if #opt.ignored_next_char > 1 then + rule:with_pair(cond.not_after_regex(opt.ignored_next_char)) + end + rule:use_undo(opt.break_undo) + return rule + end + + local bracket = function(...) + local rule = basic(...) + if opt.enable_check_bracket_line == true then + rule + :with_pair(cond.is_bracket_line()) + :with_move(cond.is_bracket_line_move()) + end + if opt.enable_bracket_in_quote then + -- still add bracket if text is quote "|" and next_char have " + rule:with_pair(cond.is_bracket_in_quote(), 1) + end + return rule + end + + -- stylua: ignore + local rules = { + Rule("", 'html'):with_cr(cond.none()), + Rule("```", "```", { 'markdown', 'vimwiki', 'rmarkdown', 'rmd', 'pandoc' }), + Rule("```.*$", "```", { 'markdown', 'vimwiki', 'rmarkdown', 'rmd', 'pandoc' }) + :only_cr() + :use_regex(true), + Rule('"""', '"""', { 'python', 'elixir', 'julia', 'kotlin' }) + :with_pair(cond.not_before_char('"', 3)), + Rule("'''", "'''", { 'python' }) + :with_pair(cond.not_before_char('"', 3)), + basic("'", "'", '-rust') + :with_pair(cond.not_before_regex("%w")), + basic("'", "'", 'rust') + :with_pair(cond.not_before_regex("[%w<&]")) + :with_pair(cond.not_after_text(">")), + basic("`", "`"), + basic('"', '"', '-vim'), + basic('"', '"', 'vim') + :with_pair(cond.not_before_regex("^%s*$", -1)), + bracket("(", ")"), + bracket("[", "]"), + bracket("{", "}"), + Rule(">[%w%s]*$", "^%s*$', 'end', 'elixir', nil), +} + +return rules diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-lua.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-lua.lua new file mode 100644 index 0000000..9105e0c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-lua.lua @@ -0,0 +1,9 @@ +local endwise = require('nvim-autopairs.ts-rule').endwise + +local rules = { + endwise('then$', 'end', 'lua', 'if_statement'), + endwise('function.*%(.*%)$', 'end', 'lua', {'function_declaration', 'local_function', 'function'}), +} + + +return rules diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-ruby.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-ruby.lua new file mode 100644 index 0000000..03386ab --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/endwise-ruby.lua @@ -0,0 +1,17 @@ +local endwise = require('nvim-autopairs.ts-rule').endwise + +local rules = { + endwise('%sdo$', 'end', 'ruby', nil), + endwise('%sdo%s|.*|$', 'end', 'ruby', nil), + endwise('begin$', 'end', 'ruby', nil), + endwise('def%s.+$', 'end', 'ruby', nil), + endwise('module%s.+$', 'end', 'ruby', nil), + endwise('class%s.+$', 'end', 'ruby', nil), + endwise('[%s=]%sif%s.+$', 'end', 'ruby', nil), + endwise('[%s=]%sunless%s.+$', 'end', 'ruby', nil), + endwise('[%s=]%scase%s.+$', 'end', 'ruby', nil), + endwise('[%s=]%swhile%s.+$', 'end', 'ruby', nil), + endwise('[%s=]%suntil%s.+$', 'end', 'ruby', nil), +} + +return rules diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/ts_basic.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/ts_basic.lua new file mode 100644 index 0000000..4105fff --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/rules/ts_basic.lua @@ -0,0 +1,22 @@ +local basic = require('nvim-autopairs.rules.basic') +local utils = require('nvim-autopairs.utils') +local ts_conds = require('nvim-autopairs.ts-conds') +local ts_extend = { + "'", + '"', + '(', + '[', + '{', + '`', +} +return { + setup = function (config) + local rules=basic.setup(config) + for _, rule in pairs(rules) do + if utils.is_in_table(ts_extend, rule.start_pair) then + rule:with_pair(ts_conds.is_not_ts_node_comment()) + end + end + return rules + end +} diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-conds.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-conds.lua new file mode 100644 index 0000000..2d577d7 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-conds.lua @@ -0,0 +1,150 @@ +local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') + +local log = require('nvim-autopairs._log') +local parsers = require'nvim-treesitter.parsers' +local utils = require('nvim-autopairs.utils') +local ts_query = vim.treesitter.query + +local conds = {} + +conds.is_endwise_node = function(nodes) + if nodes == nil then return function() return true end end + if type(nodes) == 'string' then nodes = {nodes} end + + return function (opts) + log.debug('is_endwise_node') + if not opts.check_endwise_ts then return true end + if nodes == nil then return true end + if #nodes == 0 then return true end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(nodes, target:type()) then + local text = ts_query.get_node_text(target) or {""} + local last = text[#text]:match(opts.rule.end_pair) + -- check last character is match with end_pair + if last == nil then + return true + end + -- log.debug('last:' .. last) + -- if match then we need tocheck parent node + -- some time treesiter is group 2 node then we need check that + local begin_target,_, end_target = target:range() + local begin_parent,_, end_parent = target:parent():range() + -- log.debug(target:range()) + -- log.debug(ts_query.get_node_text(target)) + -- log.debug(target:parent():range()) + -- log.debug(ts_query.get_node_text(target:parent())) + if + ( + begin_target ~= begin_parent + and end_target == end_parent + ) + or + (end_parent - end_target == 1) + then + return true + end + -- return true + else + end + return false + end +end + +conds.is_in_range = function(callback, position) + assert( + type(callback) == 'function' and type(position) == 'function', + 'callback and position should be a function' + ) + return function(opts) + log.debug('is_in_range') + if not parsers.has_parser() then + return + end + local cursor = position() + assert( + type(cursor) == 'table' and #cursor == 2, + 'position should be return a table like {line, col}' + ) + local line = cursor[1] + local col = cursor[2] + + local bufnr = 0 + local root_lang_tree = parsers.get_parser(bufnr) + local lang_tree = root_lang_tree:language_for_range({ line, col, line, col }) + + local result + + for _, tree in ipairs(lang_tree:trees()) do + local root = tree:root() + if root and ts_utils.is_in_node_range(root, line, col) then + local node = root:named_descendant_for_range(line, col, line, col) + local anonymous_node = root:descendant_for_range( + line, + col, + line, + col + ) + + result = { + node = node, + lang = lang_tree:lang(), + type = node:type(), + cursor = vim.api.nvim_win_get_cursor(0), + line = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, true)[1], + range = { node:range() }, + anonymous = anonymous_node:type(), + } + end + end + + return callback(result) + end +end + +conds.is_ts_node = function(nodes) + if type(nodes) == 'string' then nodes = {nodes} end + assert(nodes ~= nil, "ts nodes should be string or table") + return function (opts) + log.debug('is_ts_node') + if #nodes == 0 then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(nodes, target:type()) then + return true + end + return false + end +end + +conds.is_not_ts_node = function(nodes) + if type(nodes) == 'string' then nodes = {nodes} end + assert(nodes ~= nil, "ts nodes should be string or table") + return function (opts) + log.debug('is_not_ts_node') + if #nodes == 0 then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(nodes, target:type()) then + return false + end + end +end + +conds.is_not_ts_node_comment = function() + return function(opts) + log.debug('not_in_ts_node_comment') + if not opts.ts_node then return end + + parsers.get_parser():parse() + local target = ts_utils.get_node_at_cursor() + if target ~= nil and utils.is_in_table(opts.ts_node, target:type()) then + return false + end + end +end + +return conds diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-rule.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-rule.lua new file mode 100644 index 0000000..3e2ebcc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-rule.lua @@ -0,0 +1,18 @@ +local Rule = require('nvim-autopairs.rule') +local cond = require('nvim-autopairs.conds') +local ts_conds = require('nvim-autopairs.ts-conds') + +return { + endwise = function (...) + local params = {...} + local rule = Rule(...) + :use_regex(true) + :end_wise(cond.is_end_line()) + if params[4] then + -- rule:with_cr(ts_conds.is_endwise_node(params[4])) + rule:with_cr(ts_conds.is_ts_node(params[4])) + end + return rule + end + +} diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-utils.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-utils.lua new file mode 100644 index 0000000..4e1d86d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/ts-utils.lua @@ -0,0 +1,12 @@ +local ts_query = vim.treesitter.query +local M = {} + +function M.get_tag_name(node) + local tag_name = nil + if node ~=nil then + tag_name = ts_query.get_node_text(node) + end + return tag_name +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/utils.lua b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/utils.lua new file mode 100644 index 0000000..1a9f9ec --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/lua/nvim-autopairs/utils.lua @@ -0,0 +1,188 @@ +local M={} +local api = vim.api +local log = require('nvim-autopairs._log') + +M.key = { + del = "", + bs = "", + c_h = "", + left = "", + right = "", + join_left = "U", + join_right = "U", + undo_sequence = "u", + noundo_sequence = "U", + abbr = "" +} + +M.set_vchar = function(text) + text = text:gsub('"', '\\"') + vim.cmd(string.format([[let v:char = "%s"]],text)) +end + + +M.is_quote = function (char) + return char == "'" or char == '"' or char == '`' +end + +M.is_bracket = function (char) + return char == "(" or char == '[' or char == '{' +end + + +M.is_close_bracket = function (char) + return char == ")" or char == ']' or char == '}' +end + +M.compare = function (value, text, is_regex) + if is_regex and string.match(text, value) then + return true + elseif text == value then + return true + end + return false +end + +---check cursor is inside a quote +---@param line string +---@param pos number positin in line +---@param quote_type nil|string specify a quote +---@return boolean +M.is_in_quotes = function (line, pos, quote_type) + local cIndex = 0 + local result = false + local last_char = quote_type or '' + + while cIndex < string.len(line) and cIndex < pos do + cIndex = cIndex + 1 + local char = line:sub(cIndex, cIndex) + if + result == true and + char == last_char and + line:sub(cIndex -1, cIndex -1) ~= "\\" + then + result = false + last_char = quote_type or '' + elseif result == false and M.is_quote(char) + and (not quote_type or char == quote_type) + then + last_char = quote_type or char + result = true + end + end + return result +end + +M.is_attached = function(bufnr) + local _, check = pcall(api.nvim_buf_get_var, bufnr or 0, "nvim-autopairs") + return check == 1 +end + + +M.set_attach = function(bufnr,value) + api.nvim_buf_set_var(bufnr or 0, "nvim-autopairs", value) +end + +M.is_in_table = function(tbl, val) + if tbl == nil then return false end + for _, value in pairs(tbl) do + if val== value then return true end + end + return false +end + +M.check_filetype = function(tbl, filetype) + if tbl == nil then return true end + return M.is_in_table(tbl, filetype) +end + +M.check_not_filetype = function(tbl, filetype) + if tbl == nil then return true end + return not M.is_in_table(tbl, filetype) +end + +M.is_in_range = function(row, col, range) + local start_row, start_col, end_row, end_col = unpack(range) + + return (row > start_row or (start_row == row and col >= start_col)) + and (row < end_row or (row == end_row and col <= end_col)) +end + +M.get_cursor = function(bufnr) + local row, col = unpack(api.nvim_win_get_cursor(bufnr or 0)) + return row - 1, col +end +M.text_get_line = function(bufnr, lnum) + return api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, false)[1] or '' +end + +M.text_get_current_line = function(bufnr) + local row = unpack(api.nvim_win_get_cursor(0)) or 1 + return M.text_get_line(bufnr, row - 1) +end + +M.repeat_key = function(key, num) + local text='' + for _ = 1, num, 1 do + text=text..key + end + return text +end +--- cut text from position with number character +---@param line string text +---@param col number position of text +---@param prev_count number number char previous +---@param next_count number number char next +---@param is_regex boolean if it is regex then will cut all +M.text_cusor_line = function(line, col, prev_count, next_count, is_regex) + if is_regex then + prev_count = col + next_count = #line - col + end + local prev = M.text_sub_char(line, col, - prev_count) + local next = M.text_sub_char(line, col + 1, next_count) + return prev, next +end + +M.text_sub_char = function(line, start, num) + local finish = start + if num < 0 then + start = start + num + 1 + else + finish = start + num -1 + end + return string.sub(line, start, finish) +end + +-- P(M.text_sub_char("aa'' aaa", 3, -1)) +M.insert_char = function(text) + api.nvim_put({text}, "c", false, true) +end + +M.feed = function(text, num) + num = num or 1 + if num < 1 then num = 1 end + local result = '' + for _ = 1, num, 1 do + result = result .. text + end + log.debug("result" .. result) + api.nvim_feedkeys(api.nvim_replace_termcodes( + result, true, false, true), + "n", true) +end + +M.esc = function(cmd) + return vim.api.nvim_replace_termcodes(cmd, true, false, true) +end + +M.is_block_wise_mode = function () + return vim.fn.visualmode() == '' +end + +--- get prev_char with out key_map +M.get_prev_char = function(opt) + return opt.line:sub(opt.col -1, opt.col + #opt.rule.start_pair -2) +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/style.toml b/etc/soft/nvim/+plugins/nvim-autopairs/style.toml new file mode 100644 index 0000000..be4a33a --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/style.toml @@ -0,0 +1,4 @@ +column_width = 85 +indent_type = "Spaces" +quote_style = "AutoPreferSingle" +indent_width = 4 diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/afterquote_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/afterquote_spec.lua new file mode 100644 index 0000000..8d0847e --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/afterquote_spec.lua @@ -0,0 +1,102 @@ +local npairs = require('nvim-autopairs') + +_G.npairs = npairs + +npairs.setup({ + enable_afterquote = true, +}) + +local data = { + { + name = 'add bracket after quote ', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[const abc=|"test" ]], + after = [[const abc=(|"test") ]], + }, + { + name = 'add bracket after quote ', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[|"test"]], + after = [[(|"test")]], + }, + { + name = 'check quote without any text on end similar', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[ const [template, setTemplate] = useState|'')]], + after = [[ const [template, setTemplate] = useState(|'')]], + }, + + { + name = 'add bracket after quote ', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[{]], + before = [[(|"test") ]], + after = [[({|"test"}) ]], + }, + { + name = 'add bracket after quote ', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[const abc=|"visu\"dsa" ]], + after = [[const abc=(|"visu\"dsa") ]], + }, + { + name = 'not add on exist quote', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[const abc=|"visu\"dsa") ]], + after = [[const abc=(|"visu\"dsa") ]], + }, + + { + name = 'test add close quote on match', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[const abc=|"visu\"dsa" ]], + after = [[const abc=(|"visu\"dsa") ]], + }, + { + name = 'not add bracket with quote have comma', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[|"data", abcdef]], + after = [[(|"data", abcdef]], + }, + { + name = 'not add bracket with quote have comma', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[(]], + before = [[|"data", "abcdef"]], + after = { [[(|"data", "abcdef"]] }, + }, +} + +local run_data = _G.Test_filter(data) + +local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') +_G.TU = ts_utils + +describe('[afterquote tag]', function() + _G.Test_withfile(run_data, {}) +end) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/init.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/init.lua new file mode 100644 index 0000000..15adc1d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/init.lua @@ -0,0 +1,22 @@ +local data1, data2 +local function wind() + + vim.api.nvim_get_current_buf() + + + + + + + + + + + + + + + + +end + diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/javascript.js b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/javascript.js new file mode 100644 index 0000000..64b1239 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/javascript.js @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/main.rs b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/main.rs new file mode 100644 index 0000000..9d301b4 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/main.rs @@ -0,0 +1,15 @@ +fn main() { + + + + + + + + + + + + + +} diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/ruby.rb b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/ruby.rb new file mode 100644 index 0000000..b5fd1be --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/ruby.rb @@ -0,0 +1,4 @@ +module MyFirstModule + + +end diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/sample.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/sample.lua new file mode 100644 index 0000000..1bfe111 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise/sample.lua @@ -0,0 +1,18 @@ +local M={} + +M.autopairs_bs = function(rules) + for _, rule in pairs(rules) do + + if rule.start_pair then +end + + + if rule.start_pair then + + end + + + + end +end +return M diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise_spec.lua new file mode 100644 index 0000000..1eca915 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/endwise_spec.lua @@ -0,0 +1,86 @@ +local npairs = require('nvim-autopairs') +local ts = require('nvim-treesitter.configs') +local log = require('nvim-autopairs._log') + +ts.setup({ + ensure_installed = { 'lua' }, + highlight = { enable = true }, +}) +_G.npairs = npairs +vim.api.nvim_set_keymap( + 'i', + '', + 'v:lua.npairs.autopairs_cr()', + { expr = true, noremap = true } +) + +local data = { + { + name = 'lua function add endwise', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[]], + before = [[function a()| ]], + after = { + [[function a() ]], + [[| ]], + [[ end ]], + }, + }, + { + name = 'lua function add endwise', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[]], + before = [[function a()|x ab ]], + after = { + [[function a() ]], + [[|x ab]], + }, + }, + { + name = 'add if endwise', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[]], + before = [[if data== 'fdsafdsa' then| ]], + after = { + [[if data== 'fdsafdsa' then ]], + [[|]], + [[end ]], + }, + }, + { + name = 'undo on key', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[{u]], + before = [[local abc = | ]], + after = { + [[local abc = {|} ]], + [[]], + [[]], + }, + }, +} + +local run_data = _G.Test_filter(data) + +local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') +_G.TU = ts_utils + +describe('[endwise tag]', function() + _G.Test_withfile(run_data, { + -- need to understand this ??? new line make change cursor zzz + cursor_add = 1, + before_each = function(value) + npairs.add_rules( + require('nvim-autopairs.rules.endwise-' .. value.filetype) + ) + end, + }) +end) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/fastwrap_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/fastwrap_spec.lua new file mode 100644 index 0000000..aafcb5d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/fastwrap_spec.lua @@ -0,0 +1,56 @@ +-- local npairs = require('nvim-autopairs') + +-- _G.npairs = npairs + +-- npairs.setup({ +-- enable_afterquote = true, +-- fast_wrap = true, +-- }) +-- local data = { +-- { +-- only = true, +-- name = 'move end wise after quote ', +-- filepath = './tests/endwise/init.lua', +-- filetype = 'lua', +-- linenr = 5, +-- key = [[]], +-- before = [[const abc=(|"test",data ]], +-- after = [[const abc=(|"test"),data ]], +-- }, +-- { +-- name = 'move end wise after quote ', +-- filepath = './tests/endwise/init.lua', +-- filetype = 'lua', +-- linenr = 5, +-- key = [[]], +-- before = [[const abc=(|"test"),data ]], +-- after = [[const abc=(|"test",data) ]], +-- }, +-- { +-- name = 'move end wise after quote ', +-- filepath = './tests/endwise/init.lua', +-- filetype = 'lua', +-- linenr = 5, +-- key = [[]], +-- before = [[const abc=(|"test",data),dadas ]], +-- after = [[const abc=(|"test",data,dadas) ]], +-- }, +-- { +-- name = 'move end wise after quote ', +-- filepath = './tests/endwise/init.lua', +-- filetype = 'lua', +-- linenr = 5, +-- key = [[]], +-- before = [[Plug {(|'dsfdsa',) on = 'aaa'} ]], +-- after = [[Plug {('dsfdsa', on = 'aaa')} ]], +-- }, +-- } + +-- local run_data = _G.Test_filter(data) + +-- local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') +-- _G.TU = ts_utils + +-- describe('[endwise tag]', function() +-- _G.Test_withfile(run_data, {}) +-- end) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/minimal.vim b/etc/soft/nvim/+plugins/nvim-autopairs/tests/minimal.vim new file mode 100644 index 0000000..eb44287 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/minimal.vim @@ -0,0 +1,30 @@ +set rtp +=. +set rtp +=../plenary.nvim/ +set rtp +=../nvim-treesitter +set rtp +=../playground/ + +lua _G.__is_log = true +lua vim.fn.setenv("DEBUG_PLENARY", true) +runtime! plugin/plenary.vim +runtime! plugin/nvim-treesitter.vim +runtime! plugin/playground.vim +runtime! plugin/nvim-autopairs.vim + +set noswapfile +set nobackup + +filetype indent off +set expandtab +set shiftwidth=4 +set nowritebackup +set noautoindent +set nocindent +set nosmartindent +set indentexpr= + +lua << EOF +require("plenary/busted") +require("nvim-treesitter").setup() +vim.cmd[[luafile ./tests/test_utils.lua]] +require("nvim-autopairs").setup() +EOF diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/nvim-autopairs_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/nvim-autopairs_spec.lua new file mode 100644 index 0000000..1edaa46 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/nvim-autopairs_spec.lua @@ -0,0 +1,713 @@ +local helpers = {} +local npairs = require('nvim-autopairs') +local Rule = require('nvim-autopairs.rule') +local cond = require('nvim-autopairs.conds') + +local log = require('nvim-autopairs._log') +_G.log = log +local utils = require('nvim-autopairs.utils') +_G.npairs = npairs; + +-- use only = true to test 1 case +local data = { + { + -- only = true, + name = "add normal bracket" , + key = [[{]], + before = [[x| ]], + after = [[x{|} ]] + }, + + { + name = "add bracket inside bracket" , + key = [[{]], + before = [[{|} ]], + after = [[{{|}} ]] + }, + { + name = "test single quote ", + filetype = "lua", + key = "'", + before = [[data,|) ]], + after = [[data,'|') ]] + }, + { + name = "add normal bracket" , + key = [[(]], + before = [[aaaa| x ]], + after = [[aaaa(|) x ]] + }, + { + name = "add normal quote" , + key = [["]], + before = [[aa| aa]], + after = [[aa"|" aa]] + }, + { + name = "add python quote" , + filetype = "python", + key = [["]], + before = [[""| ]], + after = [["""|""" ]] + }, + { + name = "don't repeat python quote" , + filetype = "python", + key = [["]], + before = [[a"""|""" ]], + after = [[a""""|"" ]] + }, + + { + name = "add markdown quote" , + filetype = "markdown", + key = [[`]], + before = [[``| ]], + after = [[```|``` ]] + }, + { + name = "don't add single quote with previous alphabet char" , + key = [[']], + before = [[aa| aa ]], + after = [[aa'| aa ]] + }, + { + name = "don't add single quote with alphabet char" , + key = [[']], + before = [[a|x ]], + after = [[a'|x ]] + }, + { + name = "don't add single quote on end line", + key = [[']], + before = [[c aa|]], + after = [[c aa'|]] + }, + { + name = "don't add quote after alphabet char" , + key = [["]], + before = [[aa |aa]], + after = [[aa "|aa]] + }, + { + name = "don't add quote inside quote" , + key = [["]], + before = [["aa | aa]], + after = [["aa "| aa]] + }, + { + name = "add quote if not inside quote" , + key = [["]], + before = [["aa " | aa]], + after = [["aa " "|" aa]] + }, + { + name = "don't add pair after alphabet char" , + key = [[(]], + before = [[aa |aa]], + after = [[aa (|aa]] + }, + { + name = "don't add pair after dot char" , + key = [[(]], + before = [[aa |.aa]], + after = [[aa (|.aa]] + }, + { + name = "don't add bracket have open bracket in same line" , + key = [[(]], + before = [[( many char |))]], + after = [[( many char (|))]] + }, + { + filetype = 'vim', + name='add bracket inside quote when nextchar is ignore', + key = [[{]], + before = [["|"]], + after = [["{|}"]] + }, + { + filetype = '', + name='add bracket inside quote when next char is ignore', + key = [[{]], + before = [[" |"]], + after = [[" {|}"]] + }, + { + name = "move right on quote line " , + key = [["]], + before = [["|"]], + after = [[""|]] + }, + { + name = "move right end line " , + key = [["]], + before = [[aaaa|"]], + after = [[aaaa"|]] + }, + { + name = "move right when inside quote" , + key = [["]], + before = [[("abcd|")]], + after = [[("abcd"|)]] + }, + + { + name = "move right when inside quote" , + key = [["]], + before = [[foo("|")]], + after = [[foo(""|)]] + }, + { + name = "move right square bracket" , + key = [[)]], + before = [[("abcd|) ]], + after = [[("abcd)| ]] + }, + { + name = "move right bracket" , + key = [[}]], + before = [[("abcd|}} ]], + after = [[("abcd}|} ]] + }, + { + name = "move right when inside grave with special slash" , + key = [[`]], + before = [[(`abcd\"|`)]], + after = [[(`abcd\"`|)]] + }, + { + name = "move right when inside quote with special slash" , + key = [["]], + before = [[("abcd\"|")]], + after = [[("abcd\""|)]] + }, + { + filetype = 'rust', + name = 'move right double quote after single quote', + key = [["]], + before = [[ ('x').expect("|");]], + after = [[ ('x').expect(""|);]], + }, + { + filetype = 'rust', + name = "move right, should not move when bracket not closing", + key = [[}]], + before = [[{{ |} ]], + after = [[{{ }|} ]] + }, + + { + filetype = 'rust', + name = "move right, should move when bracket closing", + key = [[}]], + before = [[{ }|} ]], + after = [[{ }}| ]] + }, + { + name = "delete bracket", + filetype="javascript", + key = [[]], + before = [[aaa(|) ]], + after = [[aaa| ]] + }, + { + name = "breakline on {" , + filetype="javascript", + key = [[]], + before = [[a{|}]], + after = { + "a{", + "|", + "}" + } + }, + { + name = "breakline on (" , + filetype="javascript", + key = [[]], + before = [[a(|)]], + after = { + "a(", + "|", + ")" + } + }, + { + name = "breakline on ]" , + filetype="javascript", + key = [[]], + before = [[a[|] ]], + after = { + "a[", + "|", + "]" + } + }, + { + name = "move ) inside nested function call" , + filetype="javascript", + key = [[)]], + before = { + "fn(fn(|))", + }, + after = { + "fn(fn()|)", + } + }, + { + name = "move } inside singleline function's params" , + filetype="javascript", + key = [[}]], + before = { + "({|}) => {}", + }, + after = { + "({}|) => {}", + } + }, + { + name = "move } inside multiline function's params" , + filetype="javascript", + key = [[}]], + before = { + "({|}) => {", + "", + "}", + }, + after = { + "({}|) => {", + "", + "}", + } + }, + { + name = "breakline on markdown " , + filetype="markdown", + key = [[]], + before = [[``` lua|```]], + after = { + [[``` lua]], + [[|]], + [[```]] + } + }, + { + name = "breakline on < html" , + filetype = "html", + key = [[]], + before = [[
|
]], + after = { + [[
]], + [[|]], + [[
]] + } + }, + { + name = "breakline on < html with text" , + filetype = "html", + key = [[]], + before = [[
ads |
]], + after = { + [[
ads]], + [[|]], + [[
]] + }, + }, + { + name = "breakline on < html with space after cursor" , + filetype = "html", + key = [[]], + before = [[
ads |
]], + after = { + [[
ads]], + [[|]], + [[
]] + }, + }, + { + name = "do not mapping on > html" , + filetype = "html", + key = [[>]], + before = [[| ]] + }, + { + name = "press multiple key" , + filetype = "html", + key = [[((((]], + before = [[a| ]], + after = [[a((((|)))) ]] + }, + { + setup_func = function() + npairs.add_rules({ + Rule('u%d%d%d%d$', 'number', 'lua'):use_regex(true), + }) + end, + name = "text regex", + filetype = "lua", + key="4", + before = [[u123| ]], + after = [[u1234|number ]] + }, + { + + setup_func = function () + npairs.add_rules({ + Rule('x%d%d%d%d$', 'number', 'lua'):use_regex(true):replace_endpair(function(opts) + return opts.prev_char:sub(#opts.prev_char - 3, #opts.prev_char) + end), + }) + end, + name = "text regex with custom end_pair", + filetype = "lua", + key = "4", + before = [[x123| ]], + after = [[x1234|1234 ]] + }, + { + setup_func = function () + npairs.add_rules({ + Rule('b%d%d%d%d%w$', '', 'vim'):use_regex(true, ''):replace_endpair(function(opts) + return opts.prev_char:sub(#opts.prev_char - 4, #opts.prev_char) .. 'viwUi' + end), + }) + end, + name="text regex with custom key", + filetype = "vim", + key="", + before = [[b1234s| ]], + after = [[B|1234S1234S ]] + + }, + { + setup_func = function () + npairs.add_rules({ + Rule('b%d%d%d%d%w$', '', 'vim'):use_regex(true, ''):replace_endpair(function(opts) + return opts.prev_char:sub(#opts.prev_char - 4, #opts.prev_char) .. 'viwUi' + end), + }) + end, + name="test move right custom char", + filetype="vim", + key="", + before = [[b1234s| ]], + after = [[B|1234S1234S ]] + }, + { + setup_func = function() + npairs.add_rules({ + Rule("-","+","vim") + :with_move(function(opt) + return utils.get_prev_char(opt) == "x" end) + :with_move(cond.done()) + }) + end, + name = "test move right custom char plus", + filetype="vim", + key="+", + before = [[x|+ ]], + after = [[x+| ]] + }, + { + setup_func = function() + npairs.add_rules({ + Rule("/**", "**/", "javascript") + :with_move(cond.none()) + }) + end, + name="test javascript comment", + filetype = "javascript", + key="*", + before = [[/*| ]], + after = [[/**|**/ ]] + }, + { + setup_func = function() + npairs.add_rules({ + Rule("(",")") + :use_key("") + :replace_endpair(function() return "" end, true) + }) + end, + name = "test map custom key" , + filetype = "latex", + key = [[]], + before = [[ abcde(|) ]], + after = [[ abcde| ]], + }, + { + setup_func = function() + npairs.add_rules { + Rule(' ', ' '):with_pair(function(opts) + local pair = opts.line:sub(opts.col, opts.col + 1) + return vim.tbl_contains({'()', '[]', '{}'}, pair) + end), + Rule('( ',' )') + :with_pair(function() return false end) + :with_del(function() return false end) + :with_move(function() return true end) + :use_regex(false,")") + } + end, + name = "test multiple move right" , + filetype = "latex", + key = [[)]], + before = [[( | ) ]], + after = [[( )| ]], + }, + { + setup_func = function() + npairs.setup({ + enable_check_bracket_line=false + }) + end, + name = "test disable check bracket line" , + filetype = "latex", + key = [[(]], + before = [[(|))) ]], + after = [[((|)))) ]], + }, + { + setup_func = function() + npairs.add_rules({ + Rule("<", ">",{"rust"}) + :with_pair(cond.before_text("Vec")) + }) + end, + name = "test disable check bracket line" , + filetype = "rust", + key = [[<]], + before = [[Vec| ]], + after = [[Vec<|> ]], + }, + { + setup_func = function() + npairs.add_rule(Rule("!", "!"):with_pair(cond.not_filetypes({"lua"}))) + end, + name="disable pairs in lua", + filetype="lua", + key="!", + before = [[x| ]], + after = [[x!| ]] + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rules({ + Rule("%(.*%)%s*%=>", " { }", {"typescript", "typescriptreact","javascript"}) + :use_regex(true) + :set_end_pair_length(2) + }) + end, + name = "mapping regex with custom end_pair_length", + filetype="typescript", + key=">", + before = [[(o)=| ]], + after = [[(o)=> { | } ]] + + }, + { + setup_func = function() + npairs.add_rules({ + Rule('(', ')'):use_key(''):replace_endpair(function() + return '' + end, true), + Rule('(', ')'):use_key(''):replace_endpair(function() + return '' + end, true), + }) + end, + name = "mapping same pair with different key", + filetype="typescript", + key="(", + before = [[(test|) ]], + after = [[(test(|)) ]] + + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("„","”")) + end, + name = "multibyte character from custom keyboard", + not_replace_term_code = true, + key = "„", + before = [[a | ]], + after = [[a „|” ]], + end_cursor = 3 + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("„","”"):with_move(cond.done())) + end, + name = "multibyte character move_right", + not_replace_term_code = true, + key = "”", + before = [[a „|”xx ]], + after = [[a „”|xx ]], + end_cursor = 6 + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("„", "”"):with_move(cond.done())) + end, + name = "multibyte character delete", + key = "", + before = [[a „|” ]], + after = [[a | ]], + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("a„", "”b"):with_move(cond.done())) + end, + not_replace_term_code = true, + name = "multibyte character and multiple ", + key = "„", + before = [[a| ]], + after = [[a„|”b ]], + end_cursor = 2 + }, + { + name = [[a quote with single quote string]], + key = "'", + before = [[{{("It doesn't name %s", ''), 'ErrorMsg'| }}, ]], + after = [[{{("It doesn't name %s", ''), 'ErrorMsg''|' }}, ]], + end_cursor = 41 + }, + { + setup_func = function() + npairs.setup({map_c_h = true}) + end, + name = "map ", + key = "", + before = [[aa'|' ]], + after = [[aa| ]], + }, + { + setup_func = function() + npairs.setup({ + map_c_w = true + }) + end, + name = "map ", + key = "", + before = [[aa'|' ]], + after = [[aa| ]], + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("x", "x",{'-vim','-rust'})) + end, + filetype = 'vim', + name = "disable filetype vim", + key = [[x]], + before = [[a | ]], + after = [[a x| ]] + }, + { + filetype = 'vim', + name='undo on quote', + key = [[{123u]], + end_cursor = 12, + before = [[local abc=| ]], + after = [[local abc={|} ]] + }, + { + filetype = 'vim', + name='undo on bracket', + key = [['123u]], + end_cursor = 12, + before = [[local abc=| ]], + after = [[local abc='|' ]] + }, + { + filetype = 'vim', + name='double quote on vim after char', + key = [["ab]], + before = [[echo | ]], + after = [[echo "ab|" ]] + }, + { + filetype = 'vim', + name='double quote on vim on begin', + key = [["ab]], + before = [[ | aaa]], + after = [[ "ab| aaa]] + }, + { + setup_func = function() + npairs.add_rule( + Rule('struct%s[a-zA-Z]+%s?{$', '};' ) + :use_regex(true, "{") + ) + end, + filetype = 'javascript', + name = 'custom endwise rule', + key = [[{]], + before = [[struct abc | ]], + after = [[struct abc {|};]], + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule(Rule("{", "}"):end_wise()) + end, + filetype = 'javascript', + name = 'custom endwise rule', + key = [[]], + before = [[function () {| ]], + after = { + [[function () {]], + [[|]], + [[}]], + }, + }, + { + setup_func = function() + npairs.clear_rules() + npairs.add_rule( + Rule("{", "") + :replace_endpair(function () + return "}" + end) + :end_wise() + ) + end, + filetype = 'javascript', + name = 'custom endwise rule with custom end_pair', + key = [[]], + before = [[function () {| ]], + after = { + [[function () {]], + [[|]], + [[}]], + }, + }, + { + name='open bracker on back tick', + key = [[(]], + before = [[ |`abcd`]], + after = [[ (`abcd`) ]] + } +} + +local run_data = _G.Test_filter(data) + +describe('autopairs ', function() + _G.Test_withfile(run_data, { + cursor_add = 0, + before_each = function(value) + npairs.setup() + if value.setup_func then + value.setup_func() + end + + end, + }) +end) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/test_utils.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/test_utils.lua new file mode 100644 index 0000000..87c85d1 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/test_utils.lua @@ -0,0 +1,185 @@ +local utils = require('nvim-autopairs.utils') +local log = require('nvim-autopairs._log') +local api = vim.api +local ts_query = vim.treesitter.query + +local helpers = {} + +function helpers.feed(text, feed_opts, is_replace) + feed_opts = feed_opts or 'n' + if not is_replace then + text = vim.api.nvim_replace_termcodes(text, true, false, true) + end + vim.api.nvim_feedkeys(text, feed_opts, true) +end + +function helpers.insert(text, is_replace) + helpers.feed('i' .. text, 'x', is_replace) +end + +utils.insert_char = function(text) + api.nvim_put({ text }, 'c', true, true) +end + +utils.feed = function(text, num) + local result = '' + for _ = 1, num, 1 do + result = result .. text + end + api.nvim_feedkeys( + ---@diagnostic disable-next-line: param-type-mismatch + api.nvim_replace_termcodes(result, true, false, true), + 'x', + true + ) +end + +_G.eq = assert.are.same + +_G.Test_filter = function(data) + local run_data = {} + for _, value in pairs(data) do + if value.only == true then + table.insert(run_data, value) + break + end + end + if #run_data == 0 then + run_data = data + end + return run_data +end + +local compare_text = function(linenr, text_after, name, cursor_add, end_cursor) + cursor_add = cursor_add or 0 + local new_text = vim.api.nvim_buf_get_lines( + 0, + linenr - 1, + linenr + #text_after - 1, + true + ) + for i = 1, #text_after, 1 do + local t = string.gsub(text_after[i], '%|', '') + if t + and new_text[i] + and t:gsub('%s+$', '') ~= new_text[i]:gsub('%s+$', '') + then + eq(t, new_text[i], '\n\n text error: ' .. name .. '\n') + end + local p_after = string.find(text_after[i], '%|') + if p_after then + local row, col = utils.get_cursor() + if end_cursor then + eq(row, linenr + i - 2, '\n\n cursor row error: ' .. name .. '\n') + eq( + col + 1, + end_cursor, + '\n\n end cursor column error : ' .. name .. '\n' + ) + else + eq(row, linenr + i - 2, '\n\n cursor row error: ' .. name .. '\n') + p_after = p_after + cursor_add + eq( + col, + math.max(p_after - 2, 0), + '\n\n cursor column error : ' .. name .. '\n' + ) + end + end + end + return true +end + +_G.Test_withfile = function(test_data, cb) + for _, value in pairs(test_data) do + it('test ' .. value.name, function(_) + local text_before = {} + value.linenr = value.linenr or 1 + local pos_before = { + linenr = value.linenr, + colnr = 0, + } + if not vim.tbl_islist(value.before) then + value.before = { value.before } + end + for index, text in pairs(value.before) do + local txt = string.gsub(tostring(text), '%|', '') + table.insert(text_before, txt) + if string.match(tostring(text), '%|') then + if string.find(tostring(text), '%|') then + pos_before.colnr = string.find(tostring(text), '%|') + pos_before.linenr = value.linenr + index - 1 + end + end + end + if not vim.tbl_islist(value.after) then + value.after = { value.after } + end + vim.bo.filetype = value.filetype or 'text' + vim.cmd(':bd!') + if cb.before_each then + cb.before_each(value) + end + ---@diagnostic disable-next-line: missing-parameter + if vim.fn.filereadable(vim.fn.expand(value.filepath)) == 1 then + vim.cmd(':e ' .. value.filepath) + if value.filetype then + vim.bo.filetype = value.filetype + end + vim.cmd(':e') + else + vim.cmd(':new') + if value.filetype then + vim.bo.filetype = value.filetype + end + end + vim.api.nvim_buf_set_lines( + 0, + value.linenr - 1, + value.linenr + #text_before, + false, + text_before + ) + vim.api.nvim_win_set_cursor( + 0, + { pos_before.linenr, pos_before.colnr - 1 } + ) + if type(value.key) == "function" then + log.debug("call key") + value.key() + else + log.debug('insert:' .. value.key) + helpers.insert(value.key, value.not_replace_term_code) + vim.wait(2) + helpers.feed('') + end + compare_text( + value.linenr, + value.after, + value.name, + cb.cursor_add, + value.end_cursor + ) + if cb.after_each then + cb.after_each(value) + end + vim.cmd(':bd!') + end) + end +end + +_G.dump_node = function(node) + local text = ts_query.get_node_text(node) + for _, txt in pairs(text) do + print(txt) + end +end + +_G.dump_node_text = function(target) + for node in target:iter_children() do + local node_type = node:type() + local text = ts_query.get_node_text(node) + log.debug('type:' .. node_type .. ' ') + log.debug(text) + end +end diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/treesitter_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/treesitter_spec.lua new file mode 100644 index 0000000..5422e27 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/treesitter_spec.lua @@ -0,0 +1,117 @@ +local npairs = require('nvim-autopairs') +local ts = require('nvim-treesitter.configs') +local log = require('nvim-autopairs._log') +local Rule = require('nvim-autopairs.rule') +local ts_conds = require('nvim-autopairs.ts-conds') + +_G.npairs = npairs +vim.api.nvim_set_keymap( + 'i', + '', + 'v:lua.npairs.check_break_line_char()', + { expr = true, noremap = true } +) + +ts.setup({ + ensure_installed = { 'lua', 'javascript', 'rust' }, + highlight = { enable = true }, + autopairs = { enable = true }, +}) + +local data = { + { + name = 'treesitter lua quote', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [["]], + before = { + [[ [[ aaa| ]], + [[ ]], + ']]', + }, + after = [[ [[ aaa"| ]], + }, + + { + name = 'treesitter javascript quote', + filepath = './tests/endwise/javascript.js', + filetype = 'javascript', + linenr = 5, + key = [[(]], + before = { + [[ const data= `aaa | ]], + [[ ]], + '`', + }, + after = [[ const data= `aaa (| ]], + }, + { + setup_func = function() + npairs.add_rules({ + Rule('%', '%', 'lua'):with_pair( + ts_conds.is_ts_node({ 'string', 'comment' }) + ), + }) + end, + name = 'ts_conds is_ts_node quote', + filepath = './tests/endwise/init.lua', + filetype = 'lua', + linenr = 5, + key = [[%]], + before = { + [[ [[ abcde | ]], + [[ ]], + ']]', + }, + after = [[ [[ abcde %|% ]], + }, + { + name = 'ts_conds is_ts_node failed', + filepath = './tests/endwise/init.lua', + linenr = 5, + filetype = 'lua', + key = '%', + before = { [[local abcd| = ' visual ']] }, + after = [[local abcd%| = ' visual ']], + }, + { + setup_func = function() + npairs.add_rules({ + Rule('<', '>', 'rust'):with_pair(ts_conds.is_ts_node({ + 'type_identifier', + 'let_declaration', + 'parameters', + })), + }) + end, + name = 'ts_conds is_ts_node failed', + filepath = './tests/endwise/main.rs', + linenr = 5, + filetype = 'rust', + key = '<', + before = [[pub fn noop(_inp: Vec|) {]], + after = [[pub fn noop(_inp: Vec<|>) {]], + }, +} + +local run_data = _G.Test_filter(data) + +local _, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') +_G.TU = ts_utils + +describe('[treesitter check]', function() + _G.Test_withfile(run_data, { + before_each = function(value) + npairs.setup({ + check_ts = true, + ts_config = { + javascript = { 'template_string', 'comment' }, + }, + }) + if value.setup_func then + value.setup_func() + end + end, + }) +end) diff --git a/etc/soft/nvim/+plugins/nvim-autopairs/tests/utils_spec.lua b/etc/soft/nvim/+plugins/nvim-autopairs/tests/utils_spec.lua new file mode 100644 index 0000000..14564da --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-autopairs/tests/utils_spec.lua @@ -0,0 +1,65 @@ +local utils = require('nvim-autopairs.utils') +local log = require('nvim-autopairs._log') + +local eq = assert.are.same + +local data = { + { + text = "add normal bracket", + start = 2, + num = 2, + result = 'dd' + }, + + + { + text = "iood", + start = 1, + num = 2, + result = 'io' + }, + { + text = "add normal bracket", + start = 0, + num = -2, + result = '' + }, + + { + text = "add normal bracket", + start = 3, + num = -2, + result = 'dd' + }, + { + text = [["""]], + start = 3, + num = -3, + result = '"""' + }, + + { + text = [["""]], + start = 3, + num = 3, + result = '"' + }, + + { + text = [["""]], + start = 2, + num = 2, + result = '""' + }, +} + +describe('utils test substring ', function() + for _, value in pairs(data) do + it('test sub: ' .. value.text, function() + local result = utils.text_sub_char(value.text, value.start, value.num) + eq(value.result, result, 'start ' .. value.start .. ' num' .. value.num) + end) + end +end) + +vim.wait(100) diff --git a/etc/soft/nvim/+plugins/nvim-base16/LICENSE b/etc/soft/nvim/+plugins/nvim-base16/LICENSE new file mode 100644 index 0000000..fdfcbca --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Adam Regasz-Rethy + +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. diff --git a/etc/soft/nvim/+plugins/nvim-base16/README.md b/etc/soft/nvim/+plugins/nvim-base16/README.md new file mode 100644 index 0000000..d6d7533 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/README.md @@ -0,0 +1,256 @@ +# nvim-base16 + +Neovim plugin for building base16 colorschemes with support for Neovim's +builtin LSP and Treesitter. + +https://user-images.githubusercontent.com/21000943/199322658-ecbf8113-fa4b-409b-a562-be4a100de844.mov + +```lua +-- All builtin colorschemes can be accessed with |:colorscheme|. +vim.cmd('colorscheme base16-gruvbox-dark-soft') + +-- Alternatively, you can provide a table specifying your colors to the setup function. +require('base16-colorscheme').setup({ + base00 = '#16161D', base01 = '#2c313c', base02 = '#3e4451', base03 = '#6c7891', + base04 = '#565c64', base05 = '#abb2bf', base06 = '#9a9bb3', base07 = '#c5c8e6', + base08 = '#e06c75', base09 = '#d19a66', base0A = '#e5c07b', base0B = '#98c379', + base0C = '#56b6c2', base0D = '#0184bc', base0E = '#c678dd', base0F = '#a06949', +}) +``` + +# Builtin Colorschemes + +```txt +base16-3024 +base16-apathy +base16-apprentice +base16-ashes +base16-atelier-cave-light +base16-atelier-cave +base16-atelier-dune-light +base16-atelier-dune +base16-atelier-estuary-light +base16-atelier-estuary +base16-atelier-forest-light +base16-atelier-forest +base16-atelier-heath-light +base16-atelier-heath +base16-atelier-lakeside-light +base16-atelier-lakeside +base16-atelier-plateau-light +base16-atelier-plateau +base16-atelier-savanna-light +base16-atelier-savanna +base16-atelier-seaside-light +base16-atelier-seaside +base16-atelier-sulphurpool-light +base16-atelier-sulphurpool +base16-atlas +base16-ayu-dark +base16-ayu-light +base16-ayu-mirage +base16-bespin +base16-black-metal-bathory +base16-black-metal-burzum +base16-black-metal-dark-funeral +base16-black-metal-gorgoroth +base16-black-metal-immortal +base16-black-metal-khold +base16-black-metal-marduk +base16-black-metal-mayhem +base16-black-metal-nile +base16-black-metal-venom +base16-black-metal +base16-blueforest +base16-blueish +base16-brewer +base16-bright +base16-brogrammer +base16-brushtrees-dark +base16-brushtrees +base16-catppuccin +base16-chalk +base16-circus +base16-classic-dark +base16-classic-light +base16-codeschool +base16-colors +base16-cupcake +base16-cupertino +base16-danqing +base16-da-one-black +base16-da-one-gray +base16-da-one-ocean +base16-da-one-paper +base16-da-one-sea +base16-da-one-white +base16-darcula +base16-darkmoss +base16-darktooth +base16-darkviolet +base16-decaf +base16-default-dark +base16-default-light +base16-dirtysea +base16-dracula +base16-edge-dark +base16-edge-light +base16-eighties +base16-embers +base16-emil +base16-equilibrium-dark +base16-equilibrium-gray-dark +base16-equilibrium-gray-light +base16-equilibrium-light +base16-espresso +base16-eva-dim +base16-eva +base16-everforest +base16-flat +base16-framer +base16-fruit-soda +base16-gigavolt +base16-github +base16-google-dark +base16-google-light +base16-gotham +base16-grayscale-dark +base16-grayscale-light +base16-greenscreen +base16-gruber +base16-gruvbox-dark-hard +base16-gruvbox-dark-medium +base16-gruvbox-dark-pale +base16-gruvbox-dark-soft +base16-gruvbox-light-hard +base16-gruvbox-light-medium +base16-gruvbox-light-soft +base16-gruvbox-material-dark-hard +base16-gruvbox-material-dark-medium +base16-gruvbox-material-dark-soft +base16-gruvbox-material-light-hard +base16-gruvbox-material-light-medium +base16-gruvbox-material-light-soft +base16-hardcore +base16-harmonic-dark +base16-harmonic-light +base16-heetch-light +base16-heetch +base16-helios +base16-hopscotch +base16-horizon-dark +base16-horizon-light +base16-horizon-terminal-dark +base16-horizon-terminal-light +base16-humanoid-dark +base16-humanoid-light +base16-ia-dark +base16-ia-light +base16-icy +base16-irblack +base16-isotope +base16-kanagawa +base16-katy +base16-kimber +base16-lime +base16-macintosh +base16-marrakesh +base16-material-darker +base16-material-lighter +base16-material-palenight +base16-material-vivid +base16-material +base16-materia +base16-mellow-purple +base16-mexico-light +base16-mocha +base16-monokai +base16-nebula +base16-nord +base16-nova +base16-oceanicnext +base16-ocean +base16-onedark +base16-one-light +base16-outrun-dark +base16-pandora +base16-papercolor-dark +base16-papercolor-light +base16-paraiso +base16-pasque +base16-phd +base16-pico +base16-pinky +base16-pop +base16-porple +base16-primer-dark-dimmed +base16-primer-dark +base16-primer-light +base16-purpledream +base16-qualia +base16-railscasts +base16-rebecca +base16-rose-pine-dawn +base16-rose-pine-moon +base16-rose-pine +base16-sagelight +base16-sakura +base16-sandcastle +base16-seti +base16-shadesmear-dark +base16-shadesmear-light +base16-shades-of-purple +base16-shapeshifter +base16-silk-dark +base16-silk-light +base16-snazzy +base16-solarflare-light +base16-solarflare +base16-solarized-dark +base16-solarized-light +base16-spaceduck +base16-spacemacs +base16-stella +base16-still-alive +base16-summercamp +base16-summerfruit-dark +base16-summerfruit-light +base16-synth-midnight-dark +base16-synth-midnight-light +base16-tango +base16-tender +base16-tokyo-city-dark +base16-tokyo-city-light +base16-tokyo-city-terminal-dark +base16-tokyo-city-terminal-light +base16-tokyodark-terminal +base16-tokyodark +base16-tokyo-night-dark +base16-tokyo-night-light +base16-tokyo-night-storm +base16-tokyo-night-terminal-dark +base16-tokyo-night-terminal-light +base16-tokyo-night-terminal-storm +base16-tomorrow-night-eighties +base16-tomorrow-night +base16-tomorrow +base16-tube +base16-twilight +base16-unikitty-dark +base16-unikitty-light +base16-unikitty-reversible +base16-uwunicorn +base16-vice +base16-vulcan +base16-windows-10-light +base16-windows-10 +base16-windows-95-light +base16-windows-95 +base16-windows-highcontrast-light +base16-windows-highcontrast +base16-windows-nt-light +base16-windows-nt +base16-woodland +base16-xcode-dusk +base16-zenburn +``` diff --git a/etc/soft/nvim/+plugins/nvim-base16/colors/base16-google-light.vim b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-google-light.vim new file mode 100644 index 0000000..56d4793 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-google-light.vim @@ -0,0 +1,7 @@ +let g:colors_name = 'base16-google-light' +lua require('base16-colorscheme').setup({ + \ base00 = '#ffffff', base01 = '#e0e0e0', base02 = '#c5c8c6', base03 = '#b4b7b4', + \ base04 = '#969896', base05 = '#373b41', base06 = '#282a2e', base07 = '#1d1f21', + \ base08 = '#cc342b', base09 = '#f96a38', base0A = '#cf05fc', base0B = '#198844', + \ base0C = '#3971ed', base0D = '#3971ed', base0E = '#a36ac7', base0F = '#3971ed' + \}) diff --git a/etc/soft/nvim/+plugins/nvim-base16/colors/base16-summerfruit-light.vim b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-summerfruit-light.vim new file mode 100644 index 0000000..b149627 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-summerfruit-light.vim @@ -0,0 +1,7 @@ +let g:colors_name = 'base16-summerfruit-light' +lua require('base16-colorscheme').setup({ + \ base00 = '#ffffff', base01 = '#e0e0e0', base02 = '#d0d0d0', base03 = '#b0b0b0', + \ base04 = '#000000', base05 = '#101010', base06 = '#151515', base07 = '#202020', + \ base08 = '#ff0086', base09 = '#fd8900', base0A = '#aba800', base0B = '#00c918', + \ base0C = '#1faaaa', base0D = '#3777e6', base0E = '#ad00a1', base0F = '#cc6633' + \}) diff --git a/etc/soft/nvim/+plugins/nvim-base16/colors/base16-windows-95-light.vim b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-windows-95-light.vim new file mode 100644 index 0000000..3582597 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/colors/base16-windows-95-light.vim @@ -0,0 +1,7 @@ +let g:colors_name = 'base16-windows-95-light' +lua require('base16-colorscheme').setup({ + \ base00 = '#fcfcfc', base01 = '#e0e0e0', base02 = '#c4c4c4', base03 = '#a8a8a8', + \ base04 = '#7e7e7e', base05 = '#545454', base06 = '#2a2a2a', base07 = '#000000', + \ base08 = '#a80000', base09 = '#fcfc54', base0A = '#a85400', base0B = '#00a800', + \ base0C = '#00a8a8', base0D = '#0000a8', base0E = '#a800a8', base0F = '#54fc54' + \}) diff --git a/etc/soft/nvim/+plugins/nvim-base16/doc/colorscheme.txt b/etc/soft/nvim/+plugins/nvim-base16/doc/colorscheme.txt new file mode 100644 index 0000000..5c97267 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/doc/colorscheme.txt @@ -0,0 +1,161 @@ +*nvim-base16.txt* Neovim plugin for building base16 colorschemes. + +Author: Adam P. Regasz-Rethy (RRethy) + +INTRODUCTION *nvim-base16* +=========================================================================== + +All builtin colorschemes can be accessed with |:colorscheme|. +> + :colorscheme base16-gruvbox-dark-soft +< +Alternatively, you can provide a table specifying your colors to the setup +function. +> + local colorscheme = require('colorscheme') + colorscheme.setup({ + base00 = '#16161D', base01 = '#2c313c', base02 = '#3e4451', base03 = '#6c7891', + base04 = '#565c64', base05 = '#abb2bf', base06 = '#9a9bb3', base07 = '#c5c8e6', + base08 = '#e06c75', base09 = '#d19a66', base0A = '#e5c07b', base0B = '#98c379', + base0C = '#56b6c2', base0D = '#0184bc', base0E = '#c678dd', base0F = '#a06949', + }) + +BUILTIN COLORSCHEMES *nvim-base16-builtin-colorschemes* +=========================================================================== + +Here is a list of all builtin colorschemes. + +> + 3024 + apathy + ashes + atelier-cave-light + atelier-cave + atelier-dune-light + atelier-dune + atelier-estuary-light + atelier-estuary + atelier-forest-light + atelier-forest + atelier-heath-light + atelier-heath + atelier-lakeside-light + atelier-lakeside + atelier-plateau-light + atelier-plateau + atelier-savanna-light + atelier-savanna + atelier-seaside-light + atelier-seaside + atelier-sulphurpool-light + atelier-sulphurpool + atlas + bespin + black-metal-bathory + black-metal-burzum + black-metal-dark-funeral + black-metal-gorgoroth + black-metal-immortal + black-metal-khold + black-metal-marduk + black-metal-mayhem + black-metal-nile + black-metal-venom + black-metal + brewer + bright + brogrammer + brushtrees-dark + brushtrees + chalk + circus + classic-dark + classic-light + codeschool + cupcake + cupertino + darktooth + default-dark + default-light + darcula + dracula + eighties + embers + flat + fruit-soda + github + google-dark + google-light + grayscale-dark + grayscale-light + greenscreen + gruvbox-dark-hard + gruvbox-dark-medium + gruvbox-dark-pale + gruvbox-dark-soft + gruvbox-light-hard + gruvbox-light-medium + gruvbox-light-soft + harmonic-dark + harmonic-light + heetch-light + heetch + helios + hopscotch + horizon-dark + ia-dark + ia-light + icy + irblack + isotope + macintosh + marrakesh + materia + material-darker + material-lighter + material-palenight + material-vivid + material + mellow-purple + mexico-light + mocha + monokai + nord + ocean + oceanicnext + one-light + onedark + outrun-dark + papercolor-dark + papercolor-light + paraiso + phd + pico + pop + porple + railscasts + rebecca + schemer-dark + schemer-medium + seti + shapeshifter + snazzy + solarflare + solarized-dark + solarized-light + spacemacs + summerfruit-dark + summerfruit-light + synth-midnight-dark + tomorrow-night-eighties + tomorrow-night + tomorrow + tube + twilight + unikitty-dark + unikitty-light + woodland + xcode-dusk + zenburn + +vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nvim-base16/lua/base16-colorscheme.lua b/etc/soft/nvim/+plugins/nvim-base16/lua/base16-colorscheme.lua new file mode 100644 index 0000000..385938a --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/lua/base16-colorscheme.lua @@ -0,0 +1,554 @@ +-- Some useful links for making your own colorscheme: +-- https://github.com/chriskempson/base16 +-- https://colourco.de/ +-- https://color.adobe.com/create/color-wheel +-- http://vrl.cs.brown.edu/color + +local M = {} +local hex_re = vim.regex('#\\x\\x\\x\\x\\x\\x') + +local HEX_DIGITS = { + ['0'] = 0, + ['1'] = 1, + ['2'] = 2, + ['3'] = 3, + ['4'] = 4, + ['5'] = 5, + ['6'] = 6, + ['7'] = 7, + ['8'] = 8, + ['9'] = 9, + ['a'] = 10, + ['b'] = 11, + ['c'] = 12, + ['d'] = 13, + ['e'] = 14, + ['f'] = 15, + ['A'] = 10, + ['B'] = 11, + ['C'] = 12, + ['D'] = 13, + ['E'] = 14, + ['F'] = 15, +} + +local function hex_to_rgb(hex) + return HEX_DIGITS[string.sub(hex, 1, 1)] * 16 + HEX_DIGITS[string.sub(hex, 2, 2)], + HEX_DIGITS[string.sub(hex, 3, 3)] * 16 + HEX_DIGITS[string.sub(hex, 4, 4)], + HEX_DIGITS[string.sub(hex, 5, 5)] * 16 + HEX_DIGITS[string.sub(hex, 6, 6)] +end + +local function rgb_to_hex(r, g, b) + return bit.tohex(bit.bor(bit.lshift(r, 16), bit.lshift(g, 8), b), 6) +end + +local function darken(hex, pct) + pct = 1 - pct + local r, g, b = hex_to_rgb(string.sub(hex, 2)) + r = math.floor(r * pct) + g = math.floor(g * pct) + b = math.floor(b * pct) + return string.format("#%s", rgb_to_hex(r, g, b)) +end + +-- This is a bit of syntactic sugar for creating highlight groups. +-- +-- local colorscheme = require('colorscheme') +-- local hi = colorscheme.highlight +-- hi.Comment = { guifg='#ffffff', guibg='#000000', gui='italic', guisp=nil } +-- hi.LspDiagnosticsDefaultError = 'DiagnosticError' -- Link to another group +-- +-- This is equivalent to the following vimscript +-- +-- hi Comment guifg=#ffffff guibg=#000000 gui=italic +-- hi! link LspDiagnosticsDefaultError DiagnosticError +M.highlight = setmetatable({}, { + __newindex = function(_, hlgroup, args) + if ('string' == type(args)) then + vim.cmd(('hi! link %s %s'):format(hlgroup, args)) + return + end + + local guifg, guibg, gui, guisp = args.guifg or nil, args.guibg or nil, args.gui or nil, args.guisp or nil + local cmd = { 'hi', hlgroup } + if guifg then table.insert(cmd, 'guifg=' .. guifg) end + if guibg then table.insert(cmd, 'guibg=' .. guibg) end + if gui then table.insert(cmd, 'gui=' .. gui) end + if guisp then table.insert(cmd, 'guisp=' .. guisp) end + vim.cmd(table.concat(cmd, ' ')) + end +}) + +function M.with_config(config) + M.config = vim.tbl_extend("force", { + telescope = true, + indentblankline = true, + notify = true, + ts_rainbow = true, + cmp = true, + illuminate = true, + }, config or M.config or {}) +end + +--- Creates a base16 colorscheme using the colors specified. +-- +-- Builtin colorschemes can be found in the M.colorschemes table. +-- +-- The default Vim highlight groups (including User[1-9]), highlight groups +-- pertaining to Neovim's builtin LSP, and highlight groups pertaining to +-- Treesitter will be defined. +-- +-- It's worth noting that many colorschemes will specify language specific +-- highlight groups like rubyConstant or pythonInclude. However, I don't do +-- that here since these should instead be linked to an existing highlight +-- group. +-- +-- @param colors (table) table with keys 'base00', 'base01', 'base02', +-- 'base03', 'base04', 'base05', 'base06', 'base07', 'base08', 'base09', +-- 'base0A', 'base0B', 'base0C', 'base0D', 'base0E', 'base0F'. Each key should +-- map to a valid 6 digit hex color. If a string is provided, the +-- corresponding table specifying the colorscheme will be used. +function M.setup(colors, config) + M.with_config(config) + + if type(colors) == 'string' then + colors = M.colorschemes[colors] + end + + if vim.fn.exists('syntax_on') then + vim.cmd('syntax reset') + end + vim.cmd('set termguicolors') + + M.colors = colors or M.colorschemes[vim.env.BASE16_THEME] or M.colorschemes['schemer-dark'] + local hi = M.highlight + + -- Vim editor colors + hi.Normal = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.Bold = { guifg = nil, guibg = nil, gui = 'bold', guisp = nil } + hi.Debug = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.Directory = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.Error = { guifg = M.colors.base00, guibg = M.colors.base08, gui = nil, guisp = nil } + hi.ErrorMsg = { guifg = M.colors.base08, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.Exception = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.FoldColumn = { guifg = M.colors.base0C, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.Folded = { guifg = M.colors.base03, guibg = M.colors.base01, gui = nil, guisp = nil } + hi.IncSearch = { guifg = M.colors.base01, guibg = M.colors.base09, gui = 'none', guisp = nil } + hi.Italic = { guifg = nil, guibg = nil, gui = 'none', guisp = nil } + hi.Macro = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.MatchParen = { guifg = nil, guibg = M.colors.base03, gui = nil, guisp = nil } + hi.ModeMsg = { guifg = M.colors.base0B, guibg = nil, gui = nil, guisp = nil } + hi.MoreMsg = { guifg = M.colors.base0B, guibg = nil, gui = nil, guisp = nil } + hi.Question = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.Search = { guifg = M.colors.base01, guibg = M.colors.base0A, gui = nil, guisp = nil } + hi.Substitute = { guifg = M.colors.base01, guibg = M.colors.base0A, gui = 'none', guisp = nil } + hi.SpecialKey = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.TooLong = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.Underlined = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.Visual = { guifg = nil, guibg = M.colors.base02, gui = nil, guisp = nil } + hi.VisualNOS = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.WarningMsg = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.WildMenu = { guifg = M.colors.base08, guibg = M.colors.base0A, gui = nil, guisp = nil } + hi.Title = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.Conceal = { guifg = M.colors.base0D, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.Cursor = { guifg = M.colors.base00, guibg = M.colors.base05, gui = nil, guisp = nil } + hi.NonText = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.LineNr = { guifg = M.colors.base04, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.SignColumn = { guifg = M.colors.base04, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.StatusLine = { guifg = M.colors.base05, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.StatusLineNC = { guifg = M.colors.base04, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.WinBar = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.WinBarNC = { guifg = M.colors.base04, guibg = nil, gui = 'none', guisp = nil } + hi.VertSplit = { guifg = M.colors.base05, guibg = M.colors.base00, gui = 'none', guisp = nil } + hi.ColorColumn = { guifg = nil, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.CursorColumn = { guifg = nil, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.CursorLine = { guifg = nil, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.CursorLineNr = { guifg = M.colors.base04, guibg = M.colors.base01, gui = nil, guisp = nil } + hi.QuickFixLine = { guifg = nil, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.PMenu = { guifg = M.colors.base05, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.PMenuSel = { guifg = M.colors.base01, guibg = M.colors.base05, gui = nil, guisp = nil } + hi.TabLine = { guifg = M.colors.base03, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.TabLineFill = { guifg = M.colors.base03, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.TabLineSel = { guifg = M.colors.base0B, guibg = M.colors.base01, gui = 'none', guisp = nil } + + -- Standard syntax highlighting + hi.Boolean = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.Character = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.Comment = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.Conditional = { guifg = M.colors.base0E, guibg = nil, gui = nil, guisp = nil } + hi.Constant = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.Define = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.Delimiter = { guifg = M.colors.base0F, guibg = nil, gui = nil, guisp = nil } + hi.Float = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.Function = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.Identifier = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.Include = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.Keyword = { guifg = M.colors.base0E, guibg = nil, gui = nil, guisp = nil } + hi.Label = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.Number = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.Operator = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.PreProc = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.Repeat = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.Special = { guifg = M.colors.base0C, guibg = nil, gui = nil, guisp = nil } + hi.SpecialChar = { guifg = M.colors.base0F, guibg = nil, gui = nil, guisp = nil } + hi.Statement = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.StorageClass = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.String = { guifg = M.colors.base0B, guibg = nil, gui = nil, guisp = nil } + hi.Structure = { guifg = M.colors.base0E, guibg = nil, gui = nil, guisp = nil } + hi.Tag = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.Todo = { guifg = M.colors.base0A, guibg = M.colors.base01, gui = nil, guisp = nil } + hi.Type = { guifg = M.colors.base0A, guibg = nil, gui = 'none', guisp = nil } + hi.Typedef = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + + -- Diff highlighting + hi.DiffAdd = { guifg = M.colors.base0B, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffChange = { guifg = M.colors.base03, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffDelete = { guifg = M.colors.base08, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffText = { guifg = M.colors.base0D, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffAdded = { guifg = M.colors.base0B, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffFile = { guifg = M.colors.base08, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffNewFile = { guifg = M.colors.base0B, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffLine = { guifg = M.colors.base0D, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.DiffRemoved = { guifg = M.colors.base08, guibg = M.colors.base00, gui = nil, guisp = nil } + + -- Git highlighting + hi.gitcommitOverflow = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitSummary = { guifg = M.colors.base0B, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitComment = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitUntracked = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitDiscarded = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitSelected = { guifg = M.colors.base03, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitHeader = { guifg = M.colors.base0E, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitSelectedType = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitUnmergedType = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitDiscardedType = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitBranch = { guifg = M.colors.base09, guibg = nil, gui = 'bold', guisp = nil } + hi.gitcommitUntrackedFile = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.gitcommitUnmergedFile = { guifg = M.colors.base08, guibg = nil, gui = 'bold', guisp = nil } + hi.gitcommitDiscardedFile = { guifg = M.colors.base08, guibg = nil, gui = 'bold', guisp = nil } + hi.gitcommitSelectedFile = { guifg = M.colors.base0B, guibg = nil, gui = 'bold', guisp = nil } + + -- GitGutter highlighting + hi.GitGutterAdd = { guifg = M.colors.base0B, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.GitGutterChange = { guifg = M.colors.base0D, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.GitGutterDelete = { guifg = M.colors.base08, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.GitGutterChangeDelete = { guifg = M.colors.base0E, guibg = M.colors.base00, gui = nil, guisp = nil } + + -- Spelling highlighting + hi.SpellBad = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base08 } + hi.SpellLocal = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0C } + hi.SpellCap = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0D } + hi.SpellRare = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0E } + + hi.DiagnosticError = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.DiagnosticWarn = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.DiagnosticInfo = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.DiagnosticHint = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.DiagnosticUnderlineError = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base08 } + hi.DiagnosticUnderlineWarning = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0E } + hi.DiagnosticUnderlineWarn = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0E } + hi.DiagnosticUnderlineInformation = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0F } + hi.DiagnosticUnderlineHint = { guifg = nil, guibg = nil, gui = 'undercurl', guisp = M.colors.base0C } + + hi.LspReferenceText = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.LspReferenceRead = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.LspReferenceWrite = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.LspDiagnosticsDefaultError = 'DiagnosticError' + hi.LspDiagnosticsDefaultWarning = 'DiagnosticWarn' + hi.LspDiagnosticsDefaultInformation = 'DiagnosticInfo' + hi.LspDiagnosticsDefaultHint = 'DiagnosticHint' + hi.LspDiagnosticsUnderlineError = 'DiagnosticUnderlineError' + hi.LspDiagnosticsUnderlineWarning = 'DiagnosticUnderlineWarning' + hi.LspDiagnosticsUnderlineInformation = 'DiagnosticUnderlineInformation' + hi.LspDiagnosticsUnderlineHint = 'DiagnosticUnderlineHint' + + hi.TSAnnotation = { guifg = M.colors.base0F, guibg = nil, gui = 'none', guisp = nil } + hi.TSAttribute = { guifg = M.colors.base0A, guibg = nil, gui = 'none', guisp = nil } + hi.TSBoolean = { guifg = M.colors.base09, guibg = nil, gui = 'none', guisp = nil } + hi.TSCharacter = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSComment = { guifg = M.colors.base03, guibg = nil, gui = 'italic', guisp = nil } + hi.TSConstructor = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.TSConditional = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.TSConstant = { guifg = M.colors.base09, guibg = nil, gui = 'none', guisp = nil } + hi.TSConstBuiltin = { guifg = M.colors.base09, guibg = nil, gui = 'italic', guisp = nil } + hi.TSConstMacro = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSError = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSException = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSField = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSFloat = { guifg = M.colors.base09, guibg = nil, gui = 'none', guisp = nil } + hi.TSFunction = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.TSFuncBuiltin = { guifg = M.colors.base0D, guibg = nil, gui = 'italic', guisp = nil } + hi.TSFuncMacro = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSInclude = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.TSKeyword = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.TSKeywordFunction = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.TSKeywordOperator = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.TSLabel = { guifg = M.colors.base0A, guibg = nil, gui = 'none', guisp = nil } + hi.TSMethod = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.TSNamespace = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSNone = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSNumber = { guifg = M.colors.base09, guibg = nil, gui = 'none', guisp = nil } + hi.TSOperator = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSParameter = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSParameterReference = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSProperty = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSPunctDelimiter = { guifg = M.colors.base0F, guibg = nil, gui = 'none', guisp = nil } + hi.TSPunctBracket = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSPunctSpecial = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSRepeat = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.TSString = { guifg = M.colors.base0B, guibg = nil, gui = 'none', guisp = nil } + hi.TSStringRegex = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.TSStringEscape = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.TSSymbol = { guifg = M.colors.base0B, guibg = nil, gui = 'none', guisp = nil } + hi.TSTag = { guifg = M.colors.base0A, guibg = nil, gui = 'none', guisp = nil } + hi.TSTagDelimiter = { guifg = M.colors.base0F, guibg = nil, gui = 'none', guisp = nil } + hi.TSText = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.TSStrong = { guifg = nil, guibg = nil, gui = 'bold', guisp = nil } + hi.TSEmphasis = { guifg = M.colors.base09, guibg = nil, gui = 'italic', guisp = nil } + hi.TSUnderline = { guifg = M.colors.base00, guibg = nil, gui = 'underline', guisp = nil } + hi.TSStrike = { guifg = M.colors.base00, guibg = nil, gui = 'strikethrough', guisp = nil } + hi.TSTitle = { guifg = M.colors.base0D, guibg = nil, gui = 'none', guisp = nil } + hi.TSLiteral = { guifg = M.colors.base09, guibg = nil, gui = 'none', guisp = nil } + hi.TSURI = { guifg = M.colors.base09, guibg = nil, gui = 'underline', guisp = nil } + hi.TSType = { guifg = M.colors.base0A, guibg = nil, gui = 'none', guisp = nil } + hi.TSTypeBuiltin = { guifg = M.colors.base0A, guibg = nil, gui = 'italic', guisp = nil } + hi.TSVariable = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.TSVariableBuiltin = { guifg = M.colors.base08, guibg = nil, gui = 'italic', guisp = nil } + + hi.TSDefinition = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.TSDefinitionUsage = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.TSCurrentScope = { guifg = nil, guibg = nil, gui = 'bold', guisp = nil } + + if vim.fn.has('nvim-0.8.0') then + hi['@comment'] = 'TSComment' + hi['@error'] = 'TSError' + hi['@none'] = 'TSNone' + hi['@preproc'] = 'PreProc' + hi['@define'] = 'Define' + hi['@operator'] = 'TSOperator' + hi['@punctuation.delimiter'] = 'TSPunctDelimiter' + hi['@punctuation.bracket'] = 'TSPunctBracket' + hi['@punctuation.special'] = 'TSPunctSpecial' + hi['@string'] = 'TSString' + hi['@string.regex'] = 'TSStringRegex' + hi['@string.escape'] = 'TSStringEscape' + hi['@string.special'] = 'SpecialChar' + hi['@character'] = 'TSCharacter' + hi['@character.special'] = 'SpecialChar' + hi['@boolean'] = 'TSBoolean' + hi['@number'] = 'TSNumber' + hi['@float'] = 'TSFloat' + hi['@function'] = 'TSFunction' + hi['@function.call'] = 'TSFunction' + hi['@function.builtin'] = 'TSFuncBuiltin' + hi['@function.macro'] = 'TSFuncMacro' + hi['@method'] = 'TSMethod' + hi['@method.call'] = 'TSMethod' + hi['@constructor'] = 'TSConstructor' + hi['@parameter'] = 'TSParameter' + hi['@keyword'] = 'TSKeyword' + hi['@keyword.function'] = 'TSKeywordFunction' + hi['@keyword.operator'] = 'TSKeywordOperator' + hi['@keyword.return'] = 'TSKeyword' + hi['@conditional'] = 'TSConditional' + hi['@repeat'] = 'TSRepeat' + hi['@debug'] = 'Debug' + hi['@label'] = 'TSLabel' + hi['@include'] = 'TSInclude' + hi['@exception'] = 'TSException' + hi['@type'] = 'TSType' + hi['@type.builtin'] = 'TSTypeBuiltin' + hi['@type.qualifier'] = 'TSType' + hi['@type.definition'] = 'TSType' + hi['@storageclass'] = 'StorageClass' + hi['@attribute'] = 'TSAttribute' + hi['@field'] = 'TSField' + hi['@property'] = 'TSProperty' + hi['@variable'] = 'TSVariable' + hi['@variable.builtin'] = 'TSVariableBuiltin' + hi['@constant'] = 'TSConstant' + hi['@constant.builtin'] = 'TSConstant' + hi['@constant.macro'] = 'TSConstant' + hi['@namespace'] = 'TSNamespace' + hi['@symbol'] = 'TSSymbol' + hi['@text'] = 'TSText' + hi['@text.strong'] = 'TSStrong' + hi['@text.emphasis'] = 'TSEmphasis' + hi['@text.underline'] = 'TSUnderline' + hi['@text.strike'] = 'TSStrike' + hi['@text.title'] = 'TSTitle' + hi['@text.literal'] = 'TSLiteral' + hi['@text.uri'] = 'TSUri' + hi['@text.math'] = 'Number' + hi['@text.environment'] = 'Macro' + hi['@text.environment.name'] = 'Type' + hi['@text.reference'] = 'TSParameterReference' + hi['@text.todo'] = 'Todo' + hi['@text.note'] = 'Tag' + hi['@text.warning'] = 'DiagnosticWarn' + hi['@text.danger'] = 'DiagnosticError' + hi['@tag'] = 'TSTag' + hi['@tag.attribute'] = 'TSAttribute' + hi['@tag.delimiter'] = 'TSTagDelimiter' + end + + if M.config.ts_rainbow then + hi.rainbowcol1 = { guifg = M.colors.base06 } + hi.rainbowcol2 = { guifg = M.colors.base09 } + hi.rainbowcol3 = { guifg = M.colors.base0A } + hi.rainbowcol4 = { guifg = M.colors.base07 } + hi.rainbowcol5 = { guifg = M.colors.base0C } + hi.rainbowcol6 = { guifg = M.colors.base0D } + hi.rainbowcol7 = { guifg = M.colors.base0E } + end + + hi.NvimInternalError = { guifg = M.colors.base00, guibg = M.colors.base08, gui = 'none', guisp = nil } + + hi.NormalFloat = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.FloatBorder = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.NormalNC = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.TermCursor = { guifg = M.colors.base00, guibg = M.colors.base05, gui = 'none', guisp = nil } + hi.TermCursorNC = { guifg = M.colors.base00, guibg = M.colors.base05, gui = nil, guisp = nil } + + hi.User1 = { guifg = M.colors.base08, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User2 = { guifg = M.colors.base0E, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User3 = { guifg = M.colors.base05, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User4 = { guifg = M.colors.base0C, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User5 = { guifg = M.colors.base05, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User6 = { guifg = M.colors.base05, guibg = M.colors.base01, gui = 'none', guisp = nil } + hi.User7 = { guifg = M.colors.base05, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User8 = { guifg = M.colors.base00, guibg = M.colors.base02, gui = 'none', guisp = nil } + hi.User9 = { guifg = M.colors.base00, guibg = M.colors.base02, gui = 'none', guisp = nil } + + hi.TreesitterContext = { guifg = nil, guibg = M.colors.base01, gui = 'italic', guisp = nil } + + if M.config.telescope then + if hex_re:match_str(M.colors.base00) and hex_re:match_str(M.colors.base01) and hex_re:match_str(M.colors.base02) then + local darkerbg = darken(M.colors.base00, 0.1) + local darkercursorline = darken(M.colors.base01, 0.1) + local darkerstatusline = darken(M.colors.base02, 0.1) + hi.TelescopeBorder = { guifg = darkerbg, guibg = darkerbg, gui = nil, guisp = nil } + hi.TelescopePromptBorder = { guifg = darkerstatusline, guibg = darkerstatusline, gui = nil, guisp = nil } + hi.TelescopePromptNormal = { guifg = M.colors.base05, guibg = darkerstatusline, gui = nil, guisp = nil } + hi.TelescopePromptPrefix = { guifg = M.colors.base08, guibg = darkerstatusline, gui = nil, guisp = nil } + hi.TelescopeNormal = { guifg = nil, guibg = darkerbg, gui = nil, guisp = nil } + hi.TelescopePreviewTitle = { guifg = darkercursorline, guibg = M.colors.base0B, gui = nil, guisp = nil } + hi.TelescopePromptTitle = { guifg = darkercursorline, guibg = M.colors.base08, gui = nil, guisp = nil } + hi.TelescopeResultsTitle = { guifg = darkerbg, guibg = darkerbg, gui = nil, guisp = nil } + hi.TelescopeSelection = { guifg = nil, guibg = darkerstatusline, gui = nil, guisp = nil } + hi.TelescopePreviewLine = { guifg = nil, guibg = M.colors.base01, gui = 'none', guisp = nil } + end + end + + if M.config.notify then + hi.NotifyERRORBorder = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyWARNBorder = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyINFOBorder = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyDEBUGBorder = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyTRACEBorder = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyERRORIcon = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyWARNIcon = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyINFOIcon = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyDEBUGIcon = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyTRACEIcon = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyERRORTitle = { guifg = M.colors.base08, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyWARNTitle = { guifg = M.colors.base0E, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyINFOTitle = { guifg = M.colors.base05, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyDEBUGTitle = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyTRACETitle = { guifg = M.colors.base0C, guibg = nil, gui = 'none', guisp = nil } + hi.NotifyERRORBody = 'Normal' + hi.NotifyWARNBody = 'Normal' + hi.NotifyINFOBody = 'Normal' + hi.NotifyDEBUGBody = 'Normal' + hi.NotifyTRACEBody = 'Normal' + end + + if M.config.indentblankline then + hi.IndentBlanklineChar = { guifg = M.colors.base02, gui = 'nocombine' } + hi.IndentBlanklineContextChar = { guifg = M.colors.base04, gui = 'nocombine' } + end + + if M.config.cmp then + hi.CmpDocumentationBorder = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.CmpDocumentation = { guifg = M.colors.base05, guibg = M.colors.base00, gui = nil, guisp = nil } + hi.CmpItemAbbr = { guifg = M.colors.base05, guibg = M.colors.base01, gui = nil, guisp = nil } + hi.CmpItemAbbrDeprecated = { guifg = M.colors.base03, guibg = nil, gui = 'strikethrough', guisp = nil } + hi.CmpItemAbbrMatch = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemAbbrMatchFuzzy = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindDefault = { guifg = M.colors.base05, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemMenu = { guifg = M.colors.base04, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindKeyword = { guifg = M.colors.base0E, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindVariable = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindConstant = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindReference = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindValue = { guifg = M.colors.base09, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindFunction = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindMethod = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindConstructor = { guifg = M.colors.base0D, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindClass = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindInterface = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindStruct = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindEvent = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindEnum = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindUnit = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindModule = { guifg = M.colors.base05, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindProperty = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindField = { guifg = M.colors.base08, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindTypeParameter = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindEnumMember = { guifg = M.colors.base0A, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindOperator = { guifg = M.colors.base05, guibg = nil, gui = nil, guisp = nil } + hi.CmpItemKindSnippet = { guifg = M.colors.base04, guibg = nil, gui = nil, guisp = nil } + end + + if M.config.illuminate then + hi.IlluminatedWordText = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.IlluminatedWordRead = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + hi.IlluminatedWordWrite = { guifg = nil, guibg = nil, gui = 'underline', guisp = M.colors.base04 } + end + + vim.g.terminal_color_0 = M.colors.base00 + vim.g.terminal_color_1 = M.colors.base08 + vim.g.terminal_color_2 = M.colors.base0B + vim.g.terminal_color_3 = M.colors.base0A + vim.g.terminal_color_4 = M.colors.base0D + vim.g.terminal_color_5 = M.colors.base0E + vim.g.terminal_color_6 = M.colors.base0C + vim.g.terminal_color_7 = M.colors.base05 + vim.g.terminal_color_8 = M.colors.base03 + vim.g.terminal_color_9 = M.colors.base08 + vim.g.terminal_color_10 = M.colors.base0B + vim.g.terminal_color_11 = M.colors.base0A + vim.g.terminal_color_12 = M.colors.base0D + vim.g.terminal_color_13 = M.colors.base0E + vim.g.terminal_color_14 = M.colors.base0C + vim.g.terminal_color_15 = M.colors.base07 +end + +function M.available_colorschemes() + return vim.tbl_keys(M.colorschemes) +end + +M.colorschemes = {} +setmetatable(M.colorschemes, { + __index = function(t, key) + t[key] = require(string.format('colors.%s', key)) + return t[key] + end, +}) + +-- #16161D is called eigengrau and is kinda-ish the color your see when you +-- close your eyes. It makes for a really good background. +M.colorschemes['schemer-dark'] = { + base00 = '#16161D', base01 = '#3e4451', base02 = '#2c313c', base03 = '#565c64', + base04 = '#6c7891', base05 = '#abb2bf', base06 = '#9a9bb3', base07 = '#c5c8e6', + base08 = '#e06c75', base09 = '#d19a66', base0A = '#e5c07b', base0B = '#98c379', + base0C = '#56b6c2', base0D = '#0184bc', base0E = '#c678dd', base0F = '#a06949', +} +M.colorschemes['schemer-medium'] = { + base00 = '#212226', base01 = '#3e4451', base02 = '#2c313c', base03 = '#565c64', + base04 = '#6c7891', base05 = '#abb2bf', base06 = '#9a9bb3', base07 = '#c5c8e6', + base08 = '#e06c75', base09 = '#d19a66', base0A = '#e5c07b', base0B = '#98c379', + base0C = '#56b6c2', base0D = '#0184bc', base0E = '#c678dd', base0F = '#a06949', +} + +return M diff --git a/etc/soft/nvim/+plugins/nvim-base16/lua/colorscheme.lua b/etc/soft/nvim/+plugins/nvim-base16/lua/colorscheme.lua new file mode 100644 index 0000000..ebb0909 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/lua/colorscheme.lua @@ -0,0 +1 @@ +return require('base16-colorscheme') diff --git a/etc/soft/nvim/+plugins/nvim-base16/tools/.gitignore b/etc/soft/nvim/+plugins/nvim-base16/tools/.gitignore new file mode 100644 index 0000000..361e08d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/tools/.gitignore @@ -0,0 +1 @@ +schemes* diff --git a/etc/soft/nvim/+plugins/nvim-base16/tools/README.md b/etc/soft/nvim/+plugins/nvim-base16/tools/README.md new file mode 100644 index 0000000..1939cb6 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/tools/README.md @@ -0,0 +1,16 @@ +# Import Latest Base16 Schemes + +A simple script to import the latest Base16 Schemes. Requirements were made as +light as possible. + +## Requirements + +* curl +* git +* zsh + +## How Does it Work + +The script pulls the latest _schemas.yaml_ for Base16 and then pulls down each +specified git repo. Using the scheme values for **base00** -> **base0F**, it +generates the colorscheme lua or vimls wrapper files. diff --git a/etc/soft/nvim/+plugins/nvim-base16/tools/build.zsh b/etc/soft/nvim/+plugins/nvim-base16/tools/build.zsh new file mode 100755 index 0000000..6702648 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/tools/build.zsh @@ -0,0 +1,93 @@ +#!/usr/bin/env zsh + +setopt extendedglob + +DIRNAME=${0:A:h} +SCHEMES_SOURCE=https://github.com/base16-project/base16-schemes +SCHEMES_DIR=${DIRNAME}/schemes +LUA_DIR=${DIRNAME:h}/lua/colors +VIM_DIR=${DIRNAME:h}/colors + +function get_schemes() { + if [ ! -d ${SCHEMES_DIR}/base16-schemes/.git ] + then + git clone -q --depth=1 ${SCHEMES_SOURCE} ${SCHEMES_DIR}/base16-schemes & + else + git \ + --git-dir=${SCHEMES_DIR}/base16-schemes/.git \ + --work-tree=${SCHEMES_DIR}/base16-schemes \ + pull -q & + fi + + printf "Cloning base16-schemes..." + wait + echo "Done" +} + + +function process_vim() { + local color + local scheme=${1} + local i=1 + echo "let g:colors_name = 'base16-${scheme:t:r}'" + echo "lua require('base16-colorscheme').setup({" + for color in base0{0..9} base0{A..F} + do + [[ ${i} -eq 1 ]] && printf " \ " || printf " " + local value=$(sed -ne 's/'"${color}"': "\(.*\)".*/\1/p' ${scheme}) + printf "${color} = '#${value:l}'" + [[ ${color} != "base0F" ]] && printf ',' + [[ ${i} -eq 4 ]] && echo && i=1 || ((i++)) + done + echo " \})" +} + +function process_lua() { + local color + local scheme=${1} + local i=1 + echo "return {" + for color in base0{0..9} base0{A..F} + do + [[ ${i} -eq 1 ]] && printf " " || printf " " + local value=$(sed -ne 's/'"${color}"': "\(.*\)".*/\1/p' ${scheme}) + printf "${color} = '#${value:l}'" + [[ ${color} != "base0F" ]] && printf ',' + [[ ${i} -eq 4 ]] && echo && i=1 || ((i++)) + done + echo "}" +} + +function lua_init() { + local file name + printf "local M = {}\n\n" + for file in ${LUA_DIR}/*.lua~*/init.lua + do + name=${file:t:r} + echo "M['${name}'] = require('colors.${name}')" + done + printf "\nreturn M\n" +} + +function process() { + local name scheme + printf "Processing scheme files..." + rm -rf ${LUA_DIR} + mkdir -p ${LUA_DIR} ${VIM_DIR} + for scheme in ${SCHEMES_DIR}/*/*.yaml + do + name=${scheme:t:r} + process_lua ${scheme} > ${LUA_DIR}/${name}.lua & + process_vim ${scheme} > ${VIM_DIR}/base16-${name}.vim & + done + wait + lua_init > ${LUA_DIR}/init.lua + echo "Done" +} + +function main() { + get_schemes + process +} + +main diff --git a/etc/soft/nvim/+plugins/nvim-base16/video_script.lua b/etc/soft/nvim/+plugins/nvim-base16/video_script.lua new file mode 100644 index 0000000..f9235fd --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-base16/video_script.lua @@ -0,0 +1,57 @@ +local colors = { + 'base16-3024', + 'base16-apathy', + 'base16-apprentice', + 'base16-ashes', + 'base16-atelier-cave', + 'base16-atelier-dune', + 'base16-atelier-estuary', + 'base16-atelier-forest', + 'base16-atelier-heath', + 'base16-atelier-lakeside', + 'base16-atelier-plateau', + 'base16-atelier-savanna', + 'base16-atelier-seaside', + 'base16-atelier-sulphurpool', + 'base16-danqing', + 'base16-darcula', + 'base16-darkmoss', + 'base16-darkviolet', + 'base16-default-dark', + 'base16-dracula', + 'base16-eighties', + 'base16-espresso', + 'base16-gruvbox-dark-hard', + 'base16-gruvbox-dark-medium', + 'base16-gruvbox-dark-pale', + 'base16-gruvbox-dark-soft', + 'base16-materia', + 'base16-material', + 'base16-mocha', + 'base16-monokai', + 'base16-rose-pine-moon', + 'base16-sandcastle', + 'base16-solarized-dark', + 'base16-spaceduck', + 'base16-spacemacs', + 'base16-tokyo-night-dark', + 'base16-tokyodark', + 'base16-tomorrow-night-eighties', +} +local i = 1 +local timer = vim.loop.new_timer() +timer:start(0, 300, vim.schedule_wrap(function() + vim.cmd('colorscheme ' .. colors[i]) + i = i + 1 + if i > #colors then + timer:close() + end +end)) +local augroup = 'lua_sandbox' +vim.api.nvim_create_augroup(augroup, { clear = true }) +vim.api.nvim_create_autocmd({ 'VimLeave' }, { + group = augroup, + callback = function() + timer:close() + end, +}) diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/LICENSE.md b/etc/soft/nvim/+plugins/nvim-neoclip.lua/LICENSE.md new file mode 100644 index 0000000..55cc57e --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/LICENSE.md @@ -0,0 +1 @@ +Copyright (c) Axel Dahlberg. Distributed under the same terms as Vim itself. See `:help license`. diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/Makefile b/etc/soft/nvim/+plugins/nvim-neoclip.lua/Makefile new file mode 100644 index 0000000..7e36a7b --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/Makefile @@ -0,0 +1,6 @@ +TESTS_DIR=tests/plenary/ +TESTS_INIT=tests/init.lua +PLENARY_INIT=tests/plenary_init.lua + +test: + nvim --headless --noplugin -u ${PLENARY_INIT} -c "lua require('plenary.test_harness').test_directory('${TESTS_DIR}', {minimal_init='${TESTS_INIT}',sequential=true})" diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/README.md b/etc/soft/nvim/+plugins/nvim-neoclip.lua/README.md new file mode 100644 index 0000000..f78ce25 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/README.md @@ -0,0 +1,342 @@ +# nvim-neoclip.lua + +_This is a story about Bob_ 👷. + +_Bob loves vim_ ❤️. + +_Bob likes to yank_ ©️. + +_Bob knows about registers but sometimes forgets them_ ®️. + +_This is what happens to Bob everyday_ 🚧: + +* _Bob yanks some line._ 😀 +* _Bob yanks another line._ 🤔 +* _Bob realises he actually wanted the first._ 🙁 +* _But it is gone and Bob is now sad._ 😢 + +_Don't be like Bob, use neoclip!_ 🎉 + +`neoclip` is a clipboard manager for neovim inspired by for example [`clipmenu`](https://github.com/cdown/clipmenu). +It records everything that gets yanked in your vim session (up to a limit which is by default 1000 entries but can be configured). +You can then select an entry in the history using [`telescope`](https://github.com/nvim-telescope/telescope.nvim) or [`fzf-lua`](https://github.com/ibhagwan/fzf-lua) which then gets populated in a register of your choice. + +If you're on latest nightly (works if `:echo exists('##RecordingLeave')` returns `1`) `neoclip` will also keep track of any recorded macro (opt-out) which you can search for using `telescope`, put back in a register or simply replay. + +That's it! + +Oh, some more things, you can define an optional filter if you don't want some things to be saved and custom actions to take. + +Hold on, `neoclip` optionally also supports persistent history between sessions powered by [`sqlite.lua`](https://github.com/kkharji/sqlite.lua). + +![neoclip](https://user-images.githubusercontent.com/23341710/140090515-83a08f0f-85f9-4278-bcbe-48e4d8442ace.png) + +## Installation +```lua +use { + "AckslD/nvim-neoclip.lua", + requires = { + -- you'll need at least one of these + -- {'nvim-telescope/telescope.nvim'}, + -- {'ibhagwan/fzf-lua'}, + }, + config = function() + require('neoclip').setup() + end, +} +``` +When `require('neoclip').setup()` is called, only the autocommand (for `TextYankPost` event) is setup to save yanked things. This means that `telescope` is not required at this point if you lazy load it. + +If you want to use persistent history between sessions you also need [`sqlite.lua`](https://github.com/kkharji/sqlite.lua) installed, for example by: +```lua +use { + "AckslD/nvim-neoclip.lua", + requires = { + {'kkharji/sqlite.lua', module = 'sqlite'}, + -- you'll need at least one of these + -- {'nvim-telescope/telescope.nvim'}, + -- {'ibhagwan/fzf-lua'}, + }, + config = function() + require('neoclip').setup() + end, +} +``` + +## Configuration +You can configure `neoclip` by passing a table to `setup` (all are optional). +The following are the defaults and the keys are explained below: +```lua +use { + "AckslD/nvim-neoclip.lua", + config = function() + require('neoclip').setup({ + history = 1000, + enable_persistent_history = false, + length_limit = 1048576, + continuous_sync = false, + db_path = vim.fn.stdpath("data") .. "/databases/neoclip.sqlite3", + filter = nil, + preview = true, + prompt = nil, + default_register = '"', + default_register_macros = 'q', + enable_macro_history = true, + content_spec_column = false, + on_paste = { + set_reg = false, + }, + on_replay = { + set_reg = false, + }, + keys = { + telescope = { + i = { + select = '', + paste = '', + paste_behind = '', + replay = '', -- replay a macro + delete = '', -- delete an entry + custom = {}, + }, + n = { + select = '', + paste = 'p', + --- It is possible to map to more than one key. + -- paste = { 'p', '' }, + paste_behind = 'P', + replay = 'q', + delete = 'd', + custom = {}, + }, + }, + fzf = { + select = 'default', + paste = 'ctrl-p', + paste_behind = 'ctrl-k', + custom = {}, + }, + }, + }) + end, +} +``` +* `history`: The max number of entries to store (default 1000). +* `enable_persistent_history`: If set to `true` the history is stored on `VimLeavePre` using [`sqlite.lua`](https://github.com/tami5/sqlite.lua) and lazy loaded when querying. +* `length_limit`: The max number of characters of an entry to be stored (default 1MiB). If the length of the yanked string is larger than the limit, it will not be stored. +* `continuous_sync`: If set to `true`, the runtime history is synced with the persistent storage everytime it's changed or queried. + If you often use multiple sessions in parallel and wants the history synced you might want to enable this. + Of by default cause it might cause delays since the history is written to file everytime you yank something. + Although, I don't really notice a slowdown. + Alternatively see `db_pull` and `db_push` functions [below](#sync-database). +* `db_path`: The path to the sqlite database to store history if `enable_persistent_history=true`. + Defaults to `vim.fn.stdpath("data") .. "/databases/neoclip.sqlite3` which on my system is `~/.local/share/nvim/databases/neoclip.sqlite3` +* `filter`: A function to filter what entries to store (default all are stored). + This function filter should return `true` (include the yanked entry) or `false` (don't include it) based on a table as the only argument, which has the following keys: + * `event`: The event from `TextYankPost` (see `:help TextYankPost` for which keys it contains). + * `filetype`: The filetype of the buffer where the yank happened. + * `buffer_name`: The name of the buffer where the yank happened. +* `preview`: Whether to show a preview (default) of the current entry or not. + Useful for for example multiline yanks. + When yanking the filetype is recorded in order to enable correct syntax highlighting in the preview. + NOTE: in order to use the dynamic title showing the type of content and number of lines you need to configure `telescope` with the `dynamic_preview_title = true` option. +* `default_register`: What register to use by default when not specified (e.g. `Telescope neoclip`). + Can be a string such as `'"'` (single register) or a table of strings such as `{'"', '+', '*'}`. +* `default_register_macros`: What register to use for macros by default when not specified (e.g. `Telescope macroscope`). +* `enable_macro_history`: If `true` (default) any recorded macro will be saved, see [macros](#macros). +* `content_spec_colunm`: Can be set to `true` (default `false`) to use instead of the preview. + It will only show the type and number of lines next to the first line of the entry. +* `on_paste`: + * `set_reg`: if the register should be populated when pressing the key to paste directly. +* `on_replay`: + * `set_reg`: if the register should be populated when pressing the key to replay a recorded macro. +* `keys`: keys to use for the different pickers (`telescope` and `fzf-lua`). + With `telescope` normal key-syntax is supported and both insert `i` and normal mode `n`. + With `fzf-lua` only insert mode is supported and `fzf`-style key-syntax needs to be used. + You can also use the `custom` entry to specify custom actions to take on certain key-presses, see [below](#custom-actions) for more details. + NOTE: these are only set in the `telescope` buffer and you need to setup your own keybindings to for example open `telescope`. + +See screenshot section below for how the settings above might affect the looks. + +### Custom actions +You can specify custom actions in the `keys` entry in the settings. +For example you can do: +```lua +require('neoclip').setup({ + ... + keys = { + ... + n = { + ... + custom = { + [''] = function(opts) + print(vim.inspect(opts)) + end, + }, + }, + }, +}) +``` +which when pressing `` in normal mode will print something like: +``` +{ + register_names = { '"' }, + entry = { + contents = { "which when pressing `` in normal mode will print something like:" }, + filetype = "markdown", + regtype = "l" + } +} +``` +to do your custom action and also populate a register and/or paste you can call `neoclip`s built-in handlers, such as: +```lua +require('neoclip').setup({ + ... + keys = { + ... + n = { + ... + custom = { + [''] = function(opts) + -- do your stuff + -- ... + local handlers = require('neoclip.handlers') + -- optionally set the registers with the entry + -- handlers.set_registers(opts.register_names, opts.entry) + -- optionally paste entry + -- handlers.paste(opts.entry, 'p') + -- optionally paste entry behind + -- handlers.paste(opts.entry, 'P') + end, + }, + }, + }, +}) +``` + +## Usage +### Yanks +Yank all you want and then do: +```vim +:Telescope neoclip +``` +if using `telescope` or +```vim +:lua require('neoclip.fzf')() +``` +if using `fzf-lua`, which will show you a history of the yanks that happened in the current session. +If you pick (default ``) one this will then replace the current `"` (unnamed) register. + +If you instead want to directly paste it you can press by default `` in insert mode and `p` in normal. +Paste behind is by default `` and `P` respectively. + +If you want to replace another register with an entry from the history you can do for example: +```vim +:Telescope neoclip a +``` +if using `telescope` or +```vim +:lua require('neoclip.fzf')('a') +``` +if using `fzf-lua`, which will replace register `a`. +The register `[0-9a-z]` and `default` (`"`) are supported. + +The following special registers are support: +* `"`: `Telescope neoclip unnamed` +* `*`: `Telescope neoclip star` +* `+`: `Telescope neoclip plus` + +and `Telescope neoclip` (and `Telescope neoclip default`) will use what you set `default_register` in the `setup`. + +You can also specify more registers to populate in a single command with the `extra` keyword argument which +supports registers separated by comma, for example: +```vim +:Telescope neoclip a extra=star,plus,b +``` +if using `telescope` or +```vim +:lua require('neoclip.fzf')({'a', 'star', 'plus', 'b'}) +``` +if using `fzf-lua`. + +### Macros +If `enable_macro_history` is set to `true` (default) in the [`setup`](#configuration) then any recorded macro will be stored and can later be accessed using: +```vim +:Telescope macroscope +``` +or equivalently (which is probably the better way if you're lazy loading `telescope`): +```vim +:lua require('telescope').extensions.macroscope.default() +``` +The same arguments are supported as for the `neoclip` extension. + +NOTE: This feature requires latest nightly and in particular [this PR](https://github.com/neovim/neovim/pull/16684). You can check that your neovim supports this by checking that `:echo exists('##RecordingLeave')` returns `1`. If not then everything will work normally except that no macro will be saved in the history of `neoclip`. + +### Start/stop +If you temporarily don't want `neoclip` to record anything you can use the following calls: +* `:lua require('neoclip').start()` +* `:lua require('neoclip').stop()` +* `:lua require('neoclip').toggle()` + +### Sync database +If you don't want to use the setting `continuous_sync`, but still keep two instances of neovim synchronized in their `neoclip` history you can use the functions: +* `:lua require('neoclip').db_pull()`: Pulls the database (overwrites any local history in the current session). +* `:lua require('neoclip').db_push()`: Pushes to the database (overwrites any history previous saved in the database). + +### Remove entries +You can remove entries manually using the keybinds for `delete`. You can also delete the whole history with `:lua require('neoclip').clear_history()`. + +## Tips +* Duplicate yanks are not stored, but rather pushed forward in the history such that they are the first choice when searching for previous yanks. + Equality is checked using content and also type (ie charwise, linewise or blockwise), so if you have to yanks with the same content but when yanked charwise and the other linewise, these are considered two different entries. + However, the filetype in the buffer when the yanked happened is not, so if you yank `print('hello')` in a `python` file and then in a `lua` file you'll have a single entry which will be previewed using `lua` syntax. +* If you lazy load [`telescope`](https://github.com/nvim-telescope/telescope.nvim) with [`packer`](https://github.com/wbthomason/packer.nvim) with for example the key `module = telescope`, then it's better to use e.g. `:lua require('telescope').extensions.neoclip.default()` than `:Telescope neoclip` (or `:lua require('telescope').extensions.neoclip['']()` over `:Telescope neoclip `) for keybindings since it will properly load `telescope` before calling the extension. +* If you don't want to store pure whitespace yanks you could specify a filter as: + ```lua + local function is_whitespace(line) + return vim.fn.match(line, [[^\s*$]]) ~= -1 + end + + local function all(tbl, check) + for _, entry in ipairs(tbl) do + if not check(entry) then + return false + end + end + return true + end + + require('neoclip').setup{ + ... + filter = function(data) + return not all(data.event.regcontents, is_whitespace) + end, + ... + } + ``` + +## Troubleshooting +* For some plugin managers it seems necessary to do + ``` + :lua require('telescope').load_extension('neoclip') + ``` + before being able to call `:Telescope neoclip` (packer does not seem to need this). + However, `:lua require('telescope').extensions.neoclip.default()` seems to work without having to load. + It also seems that calling through `lua` seems necessary to play well with the (optional) persistent history if you're using [`vim-plug`](https://github.com/junegunn/vim-plug), see discussion [here](https://github.com/AckslD/nvim-neoclip.lua/issues/32) for details. + If you find out what is causing this, I'd be very happy to know :) +* If using [`packer`](https://github.com/wbthomason/packer.nvim), don't forget to `PackerCompile` after adding the plugin. + +## Thanks +* Thanks @cdown for the inspiration with [`clipmenu`](https://github.com/cdown/clipmenu). +* Thanks @fdschmidt93 for help understanding some [`telescope`](https://github.com/nvim-telescope/telescope.nvim) concepts. +* Thanks @ibhagwan for providing the code example to support [`fzf-lua`](https://github.com/ibhagwan/fzf-lua). + +## Screenshots +### `preview = true` and `content_spec_column = false` +![preview](https://user-images.githubusercontent.com/23341710/140090515-83a08f0f-85f9-4278-bcbe-48e4d8442ace.png) + +### `preview = false` and `content_spec_column = true` +![content_spec_column](https://user-images.githubusercontent.com/23341710/140090472-3271affa-7efd-40bd-9d20-562b2074b261.png) + +### `preview = false` and `content_spec_column = false` +![clean](https://user-images.githubusercontent.com/23341710/140090327-30bfff28-83ff-4695-82b8-8d4abfd68546.png) diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/doc/neoclip.txt b/etc/soft/nvim/+plugins/nvim-neoclip.lua/doc/neoclip.txt new file mode 100644 index 0000000..9b75496 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/doc/neoclip.txt @@ -0,0 +1,479 @@ +*neoclip.txt* neoclip is a clipboard manager for neovim + +============================================================================== +Table of Contents *neoclip-table-of-contents* + +1. nvim-neoclip.lua |neoclip-nvim-neoclip.lua| + - Installation |neoclip-installation| + - Configuration |neoclip-configuration| + - Usage |neoclip-usage| + - Tips |neoclip-tips| + - Troubleshooting |neoclip-troubleshooting| + - Thanks |neoclip-thanks| + - Screenshots |neoclip-screenshots| + +============================================================================== +1. nvim-neoclip.lua *neoclip-nvim-neoclip.lua* + +_This is a story about Bob_ 👷. + +_Bob loves vim_ ❤️. + +_Bob likes to yank_ ©️. + +_Bob knows about registers but sometimes forgets them_ ®️. + +_This is what happens to Bob everyday_ 🚧: + + +- _Bob yanks some line._ 😀 +- _Bob yanks another line._ 🤔 +- _Bob realises he actually wanted the first._ 🙁 +- _But it is gone and Bob is now sad._ 😢 + + +_Don’t be like Bob, use neoclip!_ 🎉 + +`neoclip` is a clipboard manager for neovim inspired by for example `clipmenu` +. It records everything that gets yanked in +your vim session (up to a limit which is by default 1000 entries but can be +configured). You can then select an entry in the history using `telescope` + or `fzf-lua` + which then gets populated in a register +of your choice. + +If you’re on latest nightly (works if `:echo exists('##RecordingLeave')` +returns `1`) `neoclip` will also keep track of any recorded macro (opt-out) +which you can search for using `telescope`, put back in a register or simply +replay. + +That’s it! + +Oh, some more things, you can define an optional filter if you don’t want +some things to be saved and custom actions to take. + +Hold on, `neoclip` optionally also supports persistent history between sessions +powered by `sqlite.lua` . + +
+ +

neoclip

+
+ +INSTALLATION *neoclip-installation* + +> + use { + "AckslD/nvim-neoclip.lua", + requires = { + -- you'll need at least one of these + -- {'nvim-telescope/telescope.nvim'}, + -- {'ibhagwan/fzf-lua'}, + }, + config = function() + require('neoclip').setup() + end, + } +< + + +When `require('neoclip').setup()` is called, only the autocommand (for +`TextYankPost` event) is setup to save yanked things. This means that +`telescope` is not required at this point if you lazy load it. + +If you want to use persistent history between sessions you also need +`sqlite.lua` installed, for example by: + +> + use { + "AckslD/nvim-neoclip.lua", + requires = { + {'kkharji/sqlite.lua', module = 'sqlite'}, + -- you'll need at least one of these + -- {'nvim-telescope/telescope.nvim'}, + -- {'ibhagwan/fzf-lua'}, + }, + config = function() + require('neoclip').setup() + end, + } +< + + +CONFIGURATION *neoclip-configuration* + +You can configure `neoclip` by passing a table to `setup` (all are optional). +The following are the defaults and the keys are explained below: + +> + use { + "AckslD/nvim-neoclip.lua", + config = function() + require('neoclip').setup({ + history = 1000, + enable_persistent_history = false, + length_limit = 1048576, + continuous_sync = false, + db_path = vim.fn.stdpath("data") .. "/databases/neoclip.sqlite3", + filter = nil, + preview = true, + prompt = nil, + default_register = '"', + default_register_macros = 'q', + enable_macro_history = true, + content_spec_column = false, + on_paste = { + set_reg = false, + }, + on_replay = { + set_reg = false, + }, + keys = { + telescope = { + i = { + select = '', + paste = '', + paste_behind = '', + replay = '', -- replay a macro + delete = '', -- delete an entry + custom = {}, + }, + n = { + select = '', + paste = 'p', + --- It is possible to map to more than one key. + -- paste = { 'p', '' }, + paste_behind = 'P', + replay = 'q', + delete = 'd', + custom = {}, + }, + }, + fzf = { + select = 'default', + paste = 'ctrl-p', + paste_behind = 'ctrl-k', + custom = {}, + }, + }, + }) + end, + } +< + + + +- `history`: The max number of entries to store (default 1000). +- `enable_persistent_history`: If set to `true` the history is stored on `VimLeavePre` using `sqlite.lua` and lazy loaded when querying. +- `length_limit`: The max number of characters of an entry to be stored (default 1MiB). If the length of the yanked string is larger than the limit, it will not be stored. +- `continuous_sync`: If set to `true`, the runtime history is synced with the persistent storage everytime it’s changed or queried. + If you often use multiple sessions in parallel and wants the history synced you might want to enable this. + Of by default cause it might cause delays since the history is written to file everytime you yank something. + Although, I don’t really notice a slowdown. + Alternatively see `db_pull` and `db_push` functions |neoclip-below|. +- `db_path`: The path to the sqlite database to store history if `enable_persistent_history=true`. + Defaults to `vim.fn.stdpath("data") .. "/databases/neoclip.sqlite3` which on my system is `~/.local/share/nvim/databases/neoclip.sqlite3` +- `filter`: A function to filter what entries to store (default all are stored). + This function filter should return `true` (include the yanked entry) or `false` (don’t include it) based on a table as the only argument, which has the following keys: + - `event`: The event from `TextYankPost` (see `:help TextYankPost` for which keys it contains). + - `filetype`: The filetype of the buffer where the yank happened. + - `buffer_name`: The name of the buffer where the yank happened. +- `preview`: Whether to show a preview (default) of the current entry or not. + Useful for for example multiline yanks. + When yanking the filetype is recorded in order to enable correct syntax highlighting in the preview. + NOTE: in order to use the dynamic title showing the type of content and number of lines you need to configure `telescope` with the `dynamic_preview_title = true` option. +- `default_register`: What register to use by default when not specified (e.g. `Telescope neoclip`). + Can be a string such as `'"'` (single register) or a table of strings such as `{'"', '+', '*'}`. +- `default_register_macros`: What register to use for macros by default when not specified (e.g. `Telescope macroscope`). +- `enable_macro_history`: If `true` (default) any recorded macro will be saved, see |neoclip-macros|. +- `content_spec_colunm`: Can be set to `true` (default `false`) to use instead of the preview. + It will only show the type and number of lines next to the first line of the entry. +- `on_paste`: + - `set_reg`: if the register should be populated when pressing the key to paste directly. +- `on_replay`: + - `set_reg`: if the register should be populated when pressing the key to replay a recorded macro. +- `keys`: keys to use for the different pickers (`telescope` and `fzf-lua`). + With `telescope` normal key-syntax is supported and both insert `i` and normal mode `n`. + With `fzf-lua` only insert mode is supported and `fzf`-style key-syntax needs to be used. + You can also use the `custom` entry to specify custom actions to take on certain key-presses, see |neoclip-below| for more details. + NOTE: these are only set in the `telescope` buffer and you need to setup your own keybindings to for example open `telescope`. + + +See screenshot section below for how the settings above might affect the looks. + +CUSTOM ACTIONS ~ + +You can specify custom actions in the `keys` entry in the settings. For example +you can do: + +> + require('neoclip').setup({ + ... + keys = { + ... + n = { + ... + custom = { + [''] = function(opts) + print(vim.inspect(opts)) + end, + }, + }, + }, + }) +< + + +which when pressing `` in normal mode will print something like: + +> + { + register_names = { '"' }, + entry = { + contents = { "which when pressing `` in normal mode will print something like:" }, + filetype = "markdown", + regtype = "l" + } + } +< + + +to do your custom action and also populate a register and/or paste you can call +`neoclip`s built-in handlers, such as: + +> + require('neoclip').setup({ + ... + keys = { + ... + n = { + ... + custom = { + [''] = function(opts) + -- do your stuff + -- ... + local handlers = require('neoclip.handlers') + -- optionally set the registers with the entry + -- handlers.set_registers(opts.register_names, opts.entry) + -- optionally paste entry + -- handlers.paste(opts.entry, 'p') + -- optionally paste entry behind + -- handlers.paste(opts.entry, 'P') + end, + }, + }, + }, + }) +< + + +USAGE *neoclip-usage* + +YANKS ~ + +Yank all you want and then do: + +> + :Telescope neoclip +< + + +if using `telescope` or + +> + :lua require('neoclip.fzf')() +< + + +if using `fzf-lua`, which will show you a history of the yanks that happened in +the current session. If you pick (default ``) one this will then replace +the current `"` (unnamed) register. + +If you instead want to directly paste it you can press by default `` in +insert mode and `p` in normal. Paste behind is by default `` and `P` +respectively. + +If you want to replace another register with an entry from the history you can +do for example: + +> + :Telescope neoclip a +< + + +if using `telescope` or + +> + :lua require('neoclip.fzf')('a') +< + + +if using `fzf-lua`, which will replace register `a`. The register `[0-9a-z]` +and `default` (`"`) are supported. + +The following special registers are support: * `"`: `Telescope neoclip unnamed` +* `*`: `Telescope neoclip star` * `+`: `Telescope neoclip plus` + +and `Telescope neoclip` (and `Telescope neoclip default`) will use what you set +`default_register` in the `setup`. + +You can also specify more registers to populate in a single command with the +`extra` keyword argument which supports registers separated by comma, for +example: + +> + :Telescope neoclip a extra=star,plus,b +< + + +if using `telescope` or + +> + :lua require('neoclip.fzf')({'a', 'star', 'plus', 'b'}) +< + + +if using `fzf-lua`. + +MACROS ~ + +If `enable_macro_history` is set to `true` (default) in the |neoclip-`setup`| +then any recorded macro will be stored and can later be accessed using: + +> + :Telescope macroscope +< + + +or equivalently (which is probably the better way if you’re lazy loading +`telescope`): + +> + :lua require('telescope').extensions.macroscope.default() +< + + +The same arguments are supported as for the `neoclip` extension. + +NOTE: This feature requires latest nightly and in particular this PR +. You can check that your neovim +supports this by checking that `:echo exists('##RecordingLeave')` returns `1`. +If not then everything will work normally except that no macro will be saved in +the history of `neoclip`. + +START/STOP ~ + +If you temporarily don’t want `neoclip` to record anything you can use the +following calls: * `:lua require('neoclip').start()` * `:lua +require('neoclip').stop()` * `:lua require('neoclip').toggle()` + +SYNC DATABASE ~ + +If you don’t want to use the setting `continuous_sync`, but still keep two +instances of neovim synchronized in their `neoclip` history you can use the +functions: * `:lua require('neoclip').db_pull()`: Pulls the database +(overwrites any local history in the current session). * `:lua +require('neoclip').db_push()`: Pushes to the database (overwrites any history +previous saved in the database). + +REMOVE ENTRIES ~ + +You can remove entries manually using the keybinds for `delete`. You can also +delete the whole history with `:lua require('neoclip').clear_history()`. + +TIPS *neoclip-tips* + + +- Duplicate yanks are not stored, but rather pushed forward in the history such + that they are the first choice when searching for previous yanks. Equality is + checked using content and also type (ie charwise, linewise or blockwise), so if + you have to yanks with the same content but when yanked charwise and the other + linewise, these are considered two different entries. However, the filetype in + the buffer when the yanked happened is not, so if you yank `print('hello')` in + a `python` file and then in a `lua` file you’ll have a single entry which + will be previewed using `lua` syntax. +- If you lazy load `telescope` + with `packer` with for example the + key `module = telescope`, then it’s better to use e.g. `:lua + require('telescope').extensions.neoclip.default()` than `:Telescope neoclip` + (or `:lua require('telescope').extensions.neoclip['']()` over `:Telescope + neoclip `) for keybindings since it will properly load `telescope` before + calling the extension. +- If you don’t want to store pure whitespace yanks you could specify a filter + as: + > + local function is_whitespace(line) + return vim.fn.match(line, [[^\s*$]]) ~= -1 + end + + local function all(tbl, check) + for _, entry in ipairs(tbl) do + if not check(entry) then + return false + end + end + return true + end + + require('neoclip').setup{ + ... + filter = function(data) + return not all(data.event.regcontents, is_whitespace) + end, + ... + } + < + + +TROUBLESHOOTING *neoclip-troubleshooting* + + +- For some plugin managers it seems necessary to do + > + :lua require('telescope').load_extension('neoclip') + < + before being able to call `:Telescope neoclip` (packer does not seem to need + this). However, `:lua require('telescope').extensions.neoclip.default()` seems + to work without having to load. It also seems that calling through `lua` seems + necessary to play well with the (optional) persistent history if you’re using + `vim-plug` , see discussion here + for details. If you find + out what is causing this, I’d be very happy to know :) +- If using `packer` , don’t forget + to `PackerCompile` after adding the plugin. + + +THANKS *neoclip-thanks* + + +- Thanks cdow for the inspiration with `clipmenu` . +- Thanks fdschmidt9 for help understanding some `telescope` concepts. +- Thanks ibhagwa for providing the code example to support `fzf-lua` . + + +SCREENSHOTS *neoclip-screenshots* + +`PREVIEW = TRUE` AND `CONTENT_SPEC_COLUMN = FALSE` ~ + +
+ +

preview

+
+ +`PREVIEW = FALSE` AND `CONTENT_SPEC_COLUMN = TRUE` ~ + +
+ +

content_spec_column

+
+ +`PREVIEW = FALSE` AND `CONTENT_SPEC_COLUMN = FALSE` ~ + +
+ +

clean

+
+ +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip.lua new file mode 100644 index 0000000..c559265 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip.lua @@ -0,0 +1,56 @@ +local M = {} + +local settings = require('neoclip.settings') + +M.stopped = false + +local function setup_auto_command() + local commands = { + 'augroup neoclip', + 'autocmd!', + 'autocmd TextYankPost * :lua require("neoclip.handlers").handle_yank_post()', + 'autocmd VimLeavePre * :lua require("neoclip.handlers").on_exit()', + } + if vim.fn.exists('##RecordingLeave') ~= 0 and settings.get().enable_macro_history then + table.insert( + commands, + 'autocmd RecordingLeave * :lua require("neoclip.handlers").handle_macro_post()' + ) + end + table.insert( + commands, + 'augroup end' + ) + vim.cmd(table.concat(commands, '\n')) +end + +M.stop = function() + M.stopped = true +end + +M.start = function() + M.stopped = false +end + +M.toggle = function() + M.stopped = not M.stopped +end + +M.db_pull = function() + require('neoclip.storage').pull() +end + +M.db_push = function() + require('neoclip.storage').push() +end + +M.clear_history = function() + require('neoclip.storage').clear() +end + +M.setup = function(opts) + settings.setup(opts) + setup_auto_command() +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/db.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/db.lua new file mode 100644 index 0000000..5799773 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/db.lua @@ -0,0 +1,70 @@ +local settings = require('neoclip.settings').get() +local warn = require('neoclip.warn').warn + +local M = {} + +local has_sqlite, sqlite = pcall(require, "sqlite") +if not has_sqlite then + warn("Couldn't find sqlite.lua. Cannot use persistent history") + return nil +end + +local function dirname(str) + return string.match(str, '(.*[/\\])') +end + +local function make_db_dir(db_path) + vim.fn.mkdir(dirname(db_path), 'p') +end + +local function get_tbl(name) + local db_path = settings.db_path + make_db_dir(db_path) + local db = sqlite.new(db_path) + db:open() + local tbl = db:tbl(name, { + regtype = "text", + contents = "luatable", + filetype = "text", + }) + + return tbl +end + +M.tables = { + yanks = get_tbl('neoclip'), + macros = get_tbl('macros'), +} + +M.read = function(query) + local storage = {} + for key, tbl in pairs(M.tables) do + local success, entries = pcall(tbl.get, tbl, query) + if success then + storage[key] = entries + else + warn(string.format("Couldn't load (%s) history since: %s", key, entries)) + return {} + end + end + return storage +end + +M.write = function(storage) + for key, tbl in pairs(M.tables) do + local success, msg = pcall(tbl.remove, tbl) + if not success then + warn(string.format("Couldn't remove clear database since: %s", msg)) + return + end + if #storage[key] > 0 then -- Don't insert if empty since it causes an error for some reason + success, msg = pcall(tbl.insert, tbl, storage[key]) + if not success then + warn(string.format("Couldn't insert in database since: %s", msg)) + return + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/fzf.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/fzf.lua new file mode 100644 index 0000000..6db9043 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/fzf.lua @@ -0,0 +1,130 @@ +local handlers = require('neoclip.handlers') +local settings = require('neoclip.settings').get() +local picker_utils = require('neoclip.picker_utils') + +-- This needs to be filled dynamically with each call (#80) +local _storage = nil + +local function get_idx(item) + return tonumber(item:match("^%d+%.")) +end + +local function parse_entry(entry_str) + local idx = get_idx(entry_str) + assert(_storage and _storage[idx]) + return _storage[idx] +end + +local function get_set_register_handler(register_names) + return function(selected, _) + handlers.set_registers(register_names, parse_entry(selected[1])) + end +end + +local function get_paste_handler(register_names, op) + return function(selected, _) + local entry = parse_entry(selected[1]) + if settings.on_paste.set_reg then + handlers.set_registers(register_names, entry) + end + handlers.paste(entry, op) + end +end + +local function get_custom_action_handler(register_names, action) + return function(selected, _) + local entry = parse_entry(selected[1]) + action({ + register_names=register_names, + entry = { + contents=entry.contents, + filetype=entry.filetype, + regtype=entry.regtype, + } + }) + end +end + +local function make_actions(register_names) + local keys = settings.keys['fzf'] + local actions = { + [keys.select] = get_set_register_handler(register_names), + [keys.paste] = get_paste_handler(register_names, 'p'), + [keys.paste_behind] = get_paste_handler(register_names, 'P'), + } + if keys.custom ~= nil then + for key, action in pairs(keys.custom) do + actions[key] = get_custom_action_handler(register_names, action) + end + end + for key, _ in pairs(actions) do + if not key then + actions[key] = nil + end + end + return actions +end + +-- Previewer class inherits from base previewer +-- the only required method is 'populate_preview_buf' +local Previewer = require('fzf-lua.previewer.builtin').base:extend() + +function Previewer:new(o, opts, fzf_win) + self.super.new(self, o, opts, fzf_win) + setmetatable(self, Previewer) + return self +end + +-- remove line numbering from preview buffer, comment +-- entrire function or change 'number=true' to revert +function Previewer:gen_winopts() + local winopts = { + wrap = self.win.preview_wrap, + number = false + } + return vim.tbl_extend("keep", winopts, self.winopts) +end + +function Previewer:populate_preview_buf(entry_str) + local entry = parse_entry(entry_str) + vim.api.nvim_buf_set_lines(self.preview_bufnr, 0, -1, false, entry.contents) + vim.api.nvim_buf_set_option(self.preview_bufnr, 'filetype', entry.filetype) + self.win:update_scrollbar() +end + +-- this function feeds elements into fzf +-- each call to `fzf_cb()` equals one line +-- `fzf_cb(nil)` closes the pipe and marks EOL to fzf +local fn = function(fzf_cb) + -- reload current _storage local var, without + -- this call yanks are never refreshed (#80) + _storage = require('neoclip.storage').get().yanks + for i, e in ipairs(_storage) do + fzf_cb(("%d. %s"):format(i, table.concat(e.contents, '\\n'))) + end + fzf_cb(nil) +end + +local function neoclip(register_names) + if register_names == nil then + register_names = settings.default_register + end + if type(register_names) == 'string' then + register_names = {register_names} + end + local actions = make_actions(register_names) + require('fzf-lua').fzf_exec(fn, { + prompt = settings.prompt or 'Prompt❯ ', + previewer = Previewer, + actions = actions, + fzf_opts = { + ["--header"] = vim.fn.shellescape(picker_utils.make_prompt_title(register_names)), + ["--delimiter"] = [[\\.]], + -- comment `--nth` if you want to enable + -- fuzzy matching the index number + ["--with-nth"] = '2..', + }, + }) +end + +return neoclip diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/handlers.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/handlers.lua new file mode 100644 index 0000000..eb2f924 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/handlers.lua @@ -0,0 +1,107 @@ +local M = {} + +local neoclip = require('neoclip') +local storage = require('neoclip.storage') +local settings = require('neoclip.settings').get() + +local function should_add(event) + if settings.length_limit then + local length = #event.regcontents - 1 + for _,line in ipairs(event.regcontents) do + length = length + #line + if length > settings.length_limit then + return false + end + end + end + if settings.filter ~= nil then + local data = { + event = event, + filetype = vim.bo.filetype, + buffer_name = vim.api.nvim_buf_get_name(0), + } + if not settings.filter(data) then + return false + end + end + return true +end + +local function get_regtype(regtype) + if regtype == 'v' then + return 'c' + elseif regtype == 'V' then + return 'l' + else + return 'b' + end +end + +M.handle_yank_post = function() + if neoclip.stopped then + return + end + local event = vim.v.event + if should_add(event) then + storage.insert({ + regtype = get_regtype(event.regtype), + contents = event.regcontents, + filetype = vim.bo.filetype, + }, 'yanks') + end +end + +M.handle_macro_post = function() + if neoclip.stopped then + return + end + local event = vim.v.event + storage.insert({ + regtype = 'c', + contents = {event.regcontents}, + filetype = nil, + }, 'macros') +end + +M.on_exit = function() + storage.on_exit() +end + +local function set_register(register_name, entry) + vim.fn.setreg(register_name, entry.contents, entry.regtype) +end + +M.set_registers = function(register_names, entry) + for _, register_name in ipairs(register_names) do + set_register(register_name, entry) + end +end + +local function temporary_reg_usage(entry, callback) + local register_name = '"' + local current_contents = vim.fn.getreg(register_name) + local current_regtype = vim.fn.getregtype(register_name) + set_register(register_name, entry) + callback(register_name) + vim.fn.setreg(register_name, current_contents, current_regtype) +end + +-- TODO can this be done without setting the register? +M.paste = function(entry, op) + temporary_reg_usage(entry, function(register_name) + vim.cmd(string.format('normal! "%s%s', register_name, op)) + end) +end + +-- TODO can this be done without setting the register? +M.replay = function(entry) + temporary_reg_usage(entry, function(register_name) + vim.cmd(string.format('normal! @%s', register_name)) + end) +end + +M.delete = function(typ, entry) + storage.delete(typ, entry) +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/linked_list.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/linked_list.lua new file mode 100644 index 0000000..9aed665 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/linked_list.lua @@ -0,0 +1,100 @@ +local M = {} + +--- Clear the list +local clear = function(self) + self.size = 0 + self.head = nil + self.tail = nil +end + +--- Push a new value to the end of the list +local push = function(self, value) + if not self.tail then + self.tail = { + value = value, + } + self.head = self.tail + else + local current_tail = self.tail + self.tail = { + prev = current_tail, + value = value, + } + current_tail.next = self.tail + end + self.size = self.size + 1 +end + +--- Pop the first entry in the list +local pop = function(self) + if not self.head then + return + end + local value = self.head.value + self:remove(self.head) + return value +end + +--- The length of the list +local len = function(self) + return self.size +end + +--- Returns the list as a plain table in _reversed_ order, starting from tail +-- @param opts optional table with keys: +-- reversed: which if `true` returns the values in reversed order +local values = function(self, opts) + local reversed = opts and opts.reversed + local values = {} + local node + if reversed then + node = self.tail + else + node = self.head + end + while node do + table.insert(values, node.value) + if reversed then + node = node.prev + else + node = node.next + end + end + return values +end + +--- Removes a node in the list +-- XXX does not check if node is actually in the list, use responsibly! +-- If the node is not in the list, the size will later be wrong. +-- @param node should be a node in the list (not a value). For example self.head. +local remove = function(self, node) + self.size = self.size - 1 + if self.head == node then + self.head = node.next + end + if self.tail == node then + self.tail = node.prev + end + if node.prev then + node.prev.next = node.next + end + if node.next then + node.next.prev = node.prev + end +end + +--- Creates a new double-linked list +M.new = function() + return { + size = 0, + -- methods + clear = clear, + push = push, + pop = pop, + len = len, + values = values, + remove = remove, + } +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/picker_utils.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/picker_utils.lua new file mode 100644 index 0000000..7434975 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/picker_utils.lua @@ -0,0 +1,7 @@ +local M = {} + +M.make_prompt_title = function(register_names) + return string.format("Pick new entry for registers %s", table.concat(register_names, ',')) +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/settings.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/settings.lua new file mode 100644 index 0000000..4e65bf9 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/settings.lua @@ -0,0 +1,102 @@ +local M = {} + +local settings = { + history = 1000, + enable_persistent_history = false, + length_limit = 1048576, + continuous_sync = false, + db_path = vim.fn.stdpath("data") .. "/databases/neoclip.sqlite3", + filter = nil, + preview = true, + default_register = '"', + default_register_macros = 'q', + enable_macro_history = true, + content_spec_column = false, + on_paste = { + set_reg = false, + }, + on_replay = { + set_reg = false, + }, + keys = { + telescope = { + i = { + select = '', + paste = '', + paste_behind = '', + replay = '', + delete = '', + custom = {}, + }, + n = { + select = '', + paste = 'p', + paste_behind = 'P', + replay = 'q', + delete = 'd', + custom = {}, + }, + }, + fzf = { + select = 'default', + paste = 'ctrl-p', + paste_behind = 'ctrl-k', + custom = {}, + }, + }, +} + +M.get = function() + return settings +end + +local function warn(msg) + vim.cmd(string.format('echohl WarningMsg | echo "Neoclip Warning: %s" | echohl None', msg)) +end + +local function check_keys() + local keys = settings.keys + if keys.i ~= nil or keys.n ~= nil then + warn('Using settings.keys without specifying \'telescope\' or \'fzf\' will not be supported in the future, see #29.') + keys.telescope = keys + end +end + +local function check_persistant() + if settings.enable_persistant_history ~= nil then + warn(''.. + 'Using settings.enable_persistant_history will not be supported in the future, see #44. '.. + 'Use settings.enable_persistent_history instead.' + ) + settings.enable_persistent_history = settings.enable_persistant_history + end +end + +local function check_deprecated_entries() + check_keys() + check_persistant() +end + +local function is_dict_like (tbl) + return type(tbl) == 'table' and not vim.tbl_islist(tbl) +end + +local function setup(opts, subsettings) + if opts == nil then + opts = {} + end + for key, value in pairs(opts) do + if is_dict_like(subsettings[key]) then + setup(value, subsettings[key]) + else + subsettings[key] = value + end + end + check_deprecated_entries() +end + +M.setup = function(opts) + setup(opts, settings) +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/sorted_set.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/sorted_set.lua new file mode 100644 index 0000000..c82c419 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/sorted_set.lua @@ -0,0 +1,93 @@ +local M = {} + +local ll = require('neoclip.linked_list') + +local hash = function(entry) + return vim.inspect({ + regtype = entry.regtype, + contents = entry.contents, + }) +end + +local remove_by_key = function(self, key) + local node = self.entries[key] + self.ll:remove(node) + self.entries[key] = nil +end + +local _insert = function(self, entry, opts) + local key = hash(entry) + if self.entries[key] then + if opts and opts.keep_position then + return + end + remove_by_key(self, key) + end + self.ll:push(entry) + self.entries[key] = self.ll.tail +end + +-- remove and return the last inserted entry +local pop = function(self) + local entry = self.ll:pop() + if entry then + self.entries[hash(entry)] = nil + end + return entry +end + +--- insert a new entry +-- @param entry the new entry +-- @param opts optional table with keys: +-- * keep_position: if true then duplicate entries are not moved forward +local insert = function(self, entry, opts) + _insert(self, entry, opts) + while self.max_size and self.ll:len() > self.max_size do + pop(self) + end +end + +--- adds the entries from a plain table +local update = function(self, entries) + for _, entry in ipairs(entries) do + self:insert(entry, {keep_position = true}) + end +end + +--- removes an entry by value +local remove = function(self, entry) + remove_by_key(self, hash(entry)) +end + +--- returns a plain table with then entries +local values = function(self, opts) + return self.ll:values(opts) +end + +--- number of entries in the set +local len = function(self) + return self.ll:len() +end + +--- clear the set +local clear = function(self) + self.ll:clear() + self.entries = {} +end + +M.new = function(max_size) + return { + max_size = max_size, + ll = ll:new(), + entries = {}, + -- methods + insert = insert, + update = update, + remove = remove, + values = values, + len = len, + clear = clear, + } +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/storage.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/storage.lua new file mode 100644 index 0000000..26e57d8 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/storage.lua @@ -0,0 +1,85 @@ +local M = {} + +local ss = require('neoclip.sorted_set') +local settings = require('neoclip.settings').get() + +local storage = { + yanks = ss.new(settings.history), + macros = ss.new(settings.history), +} + +local update_from_tbl = function(tbl) + storage.yanks:update(tbl.yanks) + storage.macros:update(tbl.macros) +end + +local update_from_db = function() + local db = require('neoclip.db').read() + update_from_tbl(db) +end + +if settings.enable_persistent_history then + update_from_db() +end + +M.as_tbl = function(opts) + return { + yanks = storage.yanks:values(opts), + macros = storage.macros:values(opts), + } +end + +local pre_get = function() + if settings.enable_persistent_history and settings.continuous_sync then + M.pull() + end +end + +local post_change = function() + if settings.enable_persistent_history and settings.continuous_sync then + M.push() + end +end + +M.get = function() + pre_get() + return M.as_tbl({reversed = true}) +end + +M.insert = function(contents, typ) + storage[typ]:insert(contents) + post_change() +end + +local clear = function() + for _, entries in pairs(storage) do + entries:clear() + end +end + +M.clear = function() + clear() + post_change() +end + +M.delete = function(typ, entry) + storage[typ]:remove(entry) + post_change() +end + +M.push = function() + require('neoclip.db').write(M.as_tbl()) +end + +M.pull = function() + clear() + update_from_db() +end + +M.on_exit = function() + if settings.enable_persistent_history then + M.push() + end +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/telescope.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/telescope.lua new file mode 100644 index 0000000..73052d2 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/telescope.lua @@ -0,0 +1,244 @@ +local M = {} + +local finders = require("telescope.finders") +local pickers = require("telescope.pickers") +local config = require("telescope.config").values +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") +local entry_display = require "telescope.pickers.entry_display" +local previewers = require('telescope.previewers') + +local handlers = require('neoclip.handlers') +local storage = require('neoclip.storage') +local settings = require('neoclip.settings').get() +local utils = require('neoclip.utils') +local picker_utils = require('neoclip.picker_utils') + +local function get_set_register_handler(register_names) + return function(prompt_bufnr) + local entry = action_state.get_selected_entry() + actions.close(prompt_bufnr) + handlers.set_registers(register_names, entry) + end +end + +local function get_paste_handler(register_names, op) + return function(prompt_bufnr) + local entry = action_state.get_selected_entry() + -- TODO if we can know the bufnr "behind" telescope we wouldn't need to close + -- and have it optional + actions.close(prompt_bufnr) + if settings.on_paste.set_reg then + handlers.set_registers(register_names, entry) + end + handlers.paste(entry, op) + end +end + +local function get_replay_recording_handler(register_names) + return function(prompt_bufnr) + local entry = action_state.get_selected_entry() + -- TODO if we can know the bufnr "behind" telescope we wouldn't need to close + -- and have it optional + actions.close(prompt_bufnr) + if settings.on_replay.set_reg then + handlers.set_registers(register_names, entry) + end + handlers.replay(entry) + end +end + +local function get_custom_action_handler(register_names, action) + return function(prompt_bufnr) + local entry = action_state.get_selected_entry() + actions.close(prompt_bufnr) + action({ + register_names=register_names, + entry = { + contents=entry.contents, + filetype=entry.filetype, + regtype=entry.regtype, + } + }) + end +end + +local function get_delete_handler(typ) + return function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:delete_selection(function(selection) + handlers.delete(typ, selection) + end) + end +end + +local displayer = entry_display.create { + separator = " ", + items = { + { width = 60 }, + { remaining = true }, + }, +} + +local spec_per_regtype = { + c = 'charwise', + l = 'linewise', + b = 'blockwise', +} + +local function spec_from_entry(entry) + local spec = spec_per_regtype[entry.regtype] + local num_lines = #entry.contents + if num_lines > 1 then + spec = string.format('%s (%d lines)', spec, num_lines) + end + return spec +end + +local function make_display(entry) + local to_display = {entry.contents[1]} + if settings.content_spec_column then + table.insert(to_display, {spec_from_entry(entry), "Comment"}) + end + return displayer(to_display) +end + +local function entry_maker(entry) + return { + display = make_display, + contents = entry.contents, + regtype = entry.regtype, + filetype = entry.filetype, + ordinal = table.concat(entry.contents, '\n'), + -- TODO seem to be needed + name = 'name', + value = 'value', -- TODO what to put value to, affects sorting? + } +end + +local special_registers = { + unnamed = '"', + star = '*', + plus = '+', +} + +local function parse_extra(extra) + local registers = {} + for _, r in ipairs(vim.fn.split(extra, ',')) do + if special_registers[r] == nil then + table.insert(registers, r) + else + table.insert(registers, special_registers[r]) + end + end + return registers +end + +local function map_if_set(map, mode, keys, desc, handler) + if not keys then + return + end + + if type(keys) ~= 'table' then + keys = { keys } + end + + for _, key in pairs(keys) do + map(mode, key, setmetatable({desc}, { + __call = function(_, ...) + return handler(...) + end, + })) + end +end + +local function get_export(register_names, typ) + if type(register_names) == 'string' then + register_names = {register_names} + end + return function(opts) + local previewer = false + if settings.preview then + previewer = previewers.new_buffer_previewer({ + define_preview = function(self, entry, status) + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, true, entry.contents) + if entry.filetype ~= nil then + vim.bo[self.state.bufnr].filetype = entry.filetype + end + end, + dyn_title = function(self, entry) + return spec_from_entry(entry) + end + }) + end + if opts ~= nil and opts.extra ~= nil then + register_names = utils.join(register_names, parse_extra(opts.extra)) + end + local results = storage.get({reversed = true})[typ] + pickers.new(opts, { + prompt_title = picker_utils.make_prompt_title(register_names), + prompt_prefix = settings.prompt or nil, + finder = finders.new_table({ + results = results, + entry_maker = entry_maker, + }), + previewer = previewer, + sorter = config.generic_sorter(opts), + attach_mappings = function(_, map) + for _, mode in ipairs({'i', 'n'}) do + local keys = settings.keys.telescope[mode] + map_if_set(map, mode, keys.select, 'select', get_set_register_handler(register_names)) + map_if_set(map, mode, keys.paste, 'paste', get_paste_handler(register_names, 'p')) + map_if_set(map, mode, keys.paste_behind, 'paste_behind', get_paste_handler(register_names, 'P')) + map_if_set(map, mode, keys.replay, 'replay', get_replay_recording_handler(register_names)) + map_if_set(map, mode, keys.delete, 'delete', get_delete_handler(typ)) + if keys.custom ~= nil then + for key, action in pairs(keys.custom) do + map(mode, key, get_custom_action_handler(register_names, action)) + end + end + end + return true + end, + }):find() + end +end + +local function register_names() + local names = {} + for name, reg in pairs(special_registers) do + names[reg] = name + end + for i = 1, 9 do -- [0-9] + local reg = string.format('%d', i) + names[reg] = reg + end + for c = 97, 122 do -- [a-z] + local reg = string.char(c) + names[reg] = reg + end + return names +end + +M.get_exports = function(typ) + local exports = {} + for reg, name in pairs(register_names()) do + local export = get_export(reg, typ) + exports[name] = export + end + local command_name + local reg + if typ == 'macros' then + reg = settings.default_register_macros + command_name = 'macroscope' + else + reg = settings.default_register + command_name = 'neoclip' + end + local default = get_export(reg, typ) + exports['default'] = default + exports[command_name] = default + return exports +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/utils.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/utils.lua new file mode 100644 index 0000000..2120821 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/utils.lua @@ -0,0 +1,14 @@ +local M = {} + +M.join = function(t1, t2) + local new_t = {} + for _, e in ipairs(t1) do + table.insert(new_t, e) + end + for _, e in ipairs(t2) do + table.insert(new_t, e) + end + return new_t +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/warn.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/warn.lua new file mode 100644 index 0000000..897643b --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/neoclip/warn.lua @@ -0,0 +1,8 @@ +local M = {} + +M.warn = function(msg) + msg = vim.fn.escape(msg, '"') + vim.cmd(string.format('echohl WarningMsg | echomsg "Warning: %s" | echohl None', msg)) +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/macroscope.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/macroscope.lua new file mode 100644 index 0000000..a3c1e9d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/macroscope.lua @@ -0,0 +1,6 @@ +local telescope = require("telescope") +local get_exports = require("neoclip.telescope").get_exports + +return telescope.register_extension { + exports = get_exports('macros'), +} diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/neoclip.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/neoclip.lua new file mode 100644 index 0000000..314a141 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/lua/telescope/_extensions/neoclip.lua @@ -0,0 +1,6 @@ +local telescope = require("telescope") +local get_exports = require("neoclip.telescope").get_exports + +return telescope.register_extension { + exports = get_exports('yanks'), +} diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/init.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/init.lua new file mode 100644 index 0000000..93e961d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/init.lua @@ -0,0 +1,10 @@ +vim.o.runtimepath = vim.o.runtimepath .. ',./rtps/plenary.nvim' +vim.o.runtimepath = vim.o.runtimepath .. ',./rtps/telescope.nvim' +vim.o.runtimepath = vim.o.runtimepath .. ',./rtps/sqlite.lua' +vim.o.runtimepath = vim.o.runtimepath .. ',.' + +_G.assert_equal_tables = function(tbl1, tbl2) + assert(vim.deep_equal(tbl1, tbl2), string.format("%s ~= %s", vim.inspect(tbl1), vim.inspect(tbl2))) +end + +require('neoclip').setup() diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/linked_list_spec.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/linked_list_spec.lua new file mode 100644 index 0000000..73f40f9 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/linked_list_spec.lua @@ -0,0 +1,40 @@ +local ll = require('neoclip.linked_list') + +describe("linked_list", function() + it("push", function() + local l = ll.new() + l:push(1) + l:push(3) + l:push(5) + l:push(10) + assert.are.equal(l.head.value, 1) + assert.are.equal(l.tail.value, 10) + assert.are.equal(l:len(), 4) + assert_equal_tables(l:values(), {1, 3, 5, 10}) + end) + it("reversed", function() + local l = ll.new() + l:push(1) + l:push(3) + l:push(5) + l:push(10) + assert_equal_tables(l:values({reversed = true}), {10, 5, 3, 1}) + end) + it("pop", function() + local l = ll.new() + l:push(1) + assert.are.equal(l:pop(), 1) + l:push(3) + l:push(5) + l:push(10) + assert.are.equal(l:pop(), 3) + assert.are.equal(l.head.value, 5) + assert.are.equal(l.tail.value, 10) + assert.are.equal(l:len(), 2) + assert_equal_tables(l:values(), {5, 10}) + assert.are.equal(l:pop(), 5) + assert.are.equal(l:pop(), 10) + assert.are.equal(l:len(), 0) + assert_equal_tables(l:values(), {}) + end) +end) diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/neoclip_spec.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/neoclip_spec.lua new file mode 100644 index 0000000..f96bae4 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/neoclip_spec.lua @@ -0,0 +1,740 @@ +local function escape_keys(keys) + return vim.api.nvim_replace_termcodes(keys, true, false, true) +end + +local function feedkeys(keys) + vim.api.nvim_feedkeys(escape_keys(keys), 'xmt', true) +end + +local function assert_scenario(scenario) + if scenario.initial_buffer then + vim.api.nvim_buf_set_lines(0, 0, -1, true, vim.fn.split(scenario.initial_buffer, '\n')) + end + if scenario.setup then scenario.setup() end + if scenario.feedkeys then + for _, raw_keys in ipairs(scenario.feedkeys) do + if type(raw_keys) == 'string' then + feedkeys(raw_keys) + else + if raw_keys.before then raw_keys.before() end + feedkeys(raw_keys.keys) + if raw_keys.after then raw_keys.after() end + end + end + end + if scenario.interlude then scenario.interlude() end + if scenario.assert then scenario.assert() end + if scenario.expected_buffer then + local current_buffer = vim.fn.join(vim.api.nvim_buf_get_lines(0, 0, -1, true), '\n') + assert.are.equal(current_buffer, scenario.expected_buffer) + end +end + +local function unload(name) + for pkg, _ in pairs(package.loaded) do + if vim.fn.match(pkg, name) ~= -1 then + package.loaded[pkg] = nil + end + end +end + +describe("neoclip", function() + after_each(function() + require('neoclip.storage').clear() + unload('neoclip') + unload('telescope') + vim.api.nvim_buf_set_lines(0, 0, -1, true, {}) + end) + it("storage", function() + assert_scenario{ + setup = function() + require('neoclip').setup() + end, + initial_buffer = [[ +some line +another line +multiple lines +multiple lines +multiple lines +multiple lines +some chars +a block +a block +]], + feedkeys = { + "jyy", + "jyy", + "jV3jy", + "4jv$y", + "jj$", + + }, + assert = function() + assert_equal_tables( + { + { + contents = {"a block", ""}, + filetype = "", + regtype = "c" + }, + { + contents = {"multiple lines", "multiple lines", "multiple lines", "some chars"}, + filetype = "", + regtype = "l" + }, + { + contents = {"multiple lines"}, + filetype = "", + regtype = "l" + }, + { + contents = {"another line"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + end, + } + end) + it("storage max", function() + assert_scenario{ + initial_buffer = [[ +a +b +c +d +]], + setup = function() + require('neoclip').setup({ + history = 2, + }) + end, + feedkeys = { + "jyy", + "jyy", + "jyy", + "jyy", + }, + assert = function() + assert_equal_tables( + { + { + contents = {"d"}, + filetype = "", + regtype = "l" + }, + { + contents = {"c"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + end, + } + end) + it("duplicates", function() + assert_scenario{ + initial_buffer = [[some line]], + setup = function() + require('neoclip').setup() + end, + feedkeys = { + "yy", + "yy", + "Y", + }, + assert = function() + assert_equal_tables( + { + { + contents = {"some line"}, + filetype = "", + regtype = "c" + }, + { + contents = {"some line"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + end, + } + end) + it("continuous_sync push", function() + local called = false + assert_scenario{ + initial_buffer = [[some line]], + setup = function() + require('neoclip').setup({ + enable_persistent_history = true, + continuous_sync = true, + }) + -- mock the push + require('neoclip.storage').push = function() + called = true + end + end, + feedkeys = { + "yy", + }, + assert = function() + assert(called) + end, + } + end) + it("continuous_sync pull", function() + local called = false + assert_scenario{ + initial_buffer = [[some line]], + setup = function() + require('neoclip').setup({ + enable_persistent_history = true, + continuous_sync = true, + }) + -- mock the pull + require('neoclip.storage').pull = function() + called = true + end + end, + feedkeys = { + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip()]], + after = function() + vim.wait(100, function() end) + end, + }, + "", + }, + assert = function() + assert(called) + end, + } + end) + it("persistent history", function() + assert_scenario{ + initial_buffer = [[some line]], + setup = function() + require('neoclip').setup({ + enable_persistent_history = true, + db_path = '/tmp/nvim/databases/neoclip.sqlite3', + }) + vim.fn.system('rm /tmp/nvim/databases/neoclip.sqlite3') + end, + feedkeys = {"yy"}, + interlude = function() + -- emulate closing and starting neovim + vim.cmd('doautocmd VimLeavePre') + unload('neoclip') + require('neoclip.settings').get().enable_persistent_history = true + require('neoclip.settings').get().db_path = '/tmp/nvim/databases/neoclip.sqlite3' + end, + assert = function() + assert_equal_tables( + { + { + contents = {"some line"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + assert(vim.fn.filereadable('/tmp/nvim/databases/neoclip.sqlite3')) + end, + } + end) + it("persistant history", function() + assert_scenario{ + initial_buffer = [[some line]], + setup = function() + require('neoclip').setup({ + enable_persistant_history = true, + }) + end, + assert = function() + assert.are.equal(require('neoclip.settings').get().enable_persistent_history, true) + end, + } + end) + it("filter (whitespace)", function() + assert_scenario{ + initial_buffer = '\nsome line\n\n\t\n', + setup = function() + local function is_whitespace(line) + return vim.fn.match(line, [[^\s*$]]) ~= -1 + end + + local function all(tbl, check) + for _, entry in ipairs(tbl) do + if not check(entry) then + return false + end + end + return true + end + + require('neoclip').setup({ + filter = function(data) + return not all(data.event.regcontents, is_whitespace) + end, + }) + end, + feedkeys = { + "yy", + "jyy", + "jyy", + "jyy", + }, + assert = function() + assert_equal_tables( + { + { + contents = {"some line"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + end, + } + end) + it("basic telescope usage", function() + assert_scenario{ + initial_buffer = [[some line +another line]], + feedkeys = { + "yy", + "jyy", + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip()]], + after = function() + vim.wait(100, function() end) + end, + }, + "k", + "p", + }, + assert = function() + assert.are.equal(vim.fn.getreg('"'), 'some line\n') + end, + expected_buffer = [[some line +another line +some line]], + } + end) + it("paste directly", function() + assert_scenario{ + initial_buffer = [[some line +another line]], + feedkeys = { + "yy", + "jyy", + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip()]], + after = function() + vim.wait(100, function() end) + end, + }, + "kp", + }, + assert = function() + assert.are.equal(vim.fn.getreg('"'), 'another line\n') + end, + expected_buffer = [[some line +another line +some line]], + } + end) + it("set reg on paste", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + on_paste = { + set_reg = true, + } + }) + end, + initial_buffer = [[some line +another line]], + feedkeys = { + "yy", + "jyy", + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip()]], + after = function() + vim.wait(100, function() end) + end, + }, + "kp", + }, + assert = function() + assert.are.equal(vim.fn.getreg('"'), 'some line\n') + end, + expected_buffer = [[some line +another line +some line]], + } + end) + it("default register", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + default_register = 'a', + }) + end, + initial_buffer = [[some line]], + feedkeys = { + "yy", + { + keys=[[:lua require('telescope').extensions.neoclip.default()]], + after = function() + vim.wait(100, function() end) + end, + }, + "", + }, + assert = function() + assert.are.equal(vim.fn.getreg('a'), 'some line\n') + end, + } + end) + it("multiple default registers", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + default_register = {'a', 'b'}, + }) + end, + initial_buffer = [[some line]], + feedkeys = { + "yy", + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip()]], + after = function() + vim.wait(100, function() end) + end, + }, + "", + }, + assert = function() + assert.are.equal(vim.fn.getreg('a'), 'some line\n') + assert.are.equal(vim.fn.getreg('b'), 'some line\n') + end, + } + end) + it("macro", function() + assert_scenario{ + setup = function() + require('neoclip').setup() + end, + feedkeys = { + "qq", + "yy", + "q", + }, + assert = function() + assert_equal_tables( + { + { + contents = {"yy"}, + regtype = "c" + }, + }, + require('neoclip.storage').get().macros + ) + end, + } + end) + it("macro disabled", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + enable_macro_history = false, + }) + end, + feedkeys = { + "qq", + "yy", + "q", + }, + assert = function() + assert.are.equal(vim.fn.getreg('q'), 'yy') + assert_equal_tables( + {}, + require('neoclip.storage').get().macros + ) + end, + } + end) + it("set reg on replay", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + on_replay = { + set_reg = true, + } + }) + end, + initial_buffer = [[some line +another line]], + feedkeys = { + "qq", + "yyp", + "q", + "qq", + "j", + "q", + { + keys=[[:lua require('telescope').extensions.macroscope.default()]], + after = function() + vim.wait(100, function() end) + end, + }, + "kq", + }, + assert = function() + assert.are.equal(vim.fn.getreg('q'), 'yyp') + end, + expected_buffer = [[some line +some line +another line +another line]], + } + end) + it("macro default register", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + default_register_macros = 'a', + }) + end, + initial_buffer = [[some line]], + feedkeys = { + "qq", + "yy", + "q", + { + keys=[[:lua require('telescope').extensions.macroscope.macroscope()]], + after = function() + vim.wait(100, function() end) + end, + }, + "", + }, + assert = function() + assert.are.equal(vim.fn.getreg('a'), 'yy') + end, + } + end) + it("multiple default registers", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + default_register_macros = {'a', 'b'}, + }) + end, + initial_buffer = [[some line]], + feedkeys = { + "qq", + "yy", + "q", + { + keys=[[:lua require('telescope').extensions.macroscope.macroscope()]], + after = function() + vim.wait(100, function() end) + end, + }, + "", + }, + assert = function() + assert.are.equal(vim.fn.getreg('a'), 'yy') + assert.are.equal(vim.fn.getreg('b'), 'yy') + end, + } + end) + it("extra", function() + assert_scenario{ + initial_buffer = [[some line +another line]], + feedkeys = { + "yy", + "jyy", + { + keys=[[:lua require('telescope').extensions.neoclip.neoclip({extra='a,b,c'})]], + after = function() + vim.wait(100, function() end) + end, + }, + "k", + "p", + }, + assert = function() + for _, reg in ipairs({'"', 'a', 'b', 'c'}) do + assert.are.equal(vim.fn.getreg(reg), 'some line\n') + end + end, + expected_buffer = [[some line +another line +some line]], + } + end) + it("keybinds", function() + local keys = { + telescope = { + i = { + select = '', + paste = '', + paste_behind = '', + replay = '', + delete = '', + custom = { + [''] = function(opts) + return opts + end + }, + }, + n = { + select = 'a', + paste = 'b', + paste_behind = 'c', + replay = 'd', + delete = 'e', + custom = { + f = function(opts) + return opts + end + }, + }, + }, + fzf = { + select = '', + paste = '', + paste_behind = '', + custom = { + [''] = function(opts) + return opts + end + }, + }, + } + + assert_scenario{ + setup = function() + require('neoclip').setup({ + keys = keys, + }) + end, + assert = function() + assert_equal_tables(require('neoclip.settings').get().keys, keys) + end, + } + end) + it("keybinds (deprecated)", function() + local keys = { + i = { + select = '', + }, + } + + assert_scenario{ + setup = function() + require('neoclip').setup({ + keys = keys, + }) + end, + assert = function() + assert.are.equal(require('neoclip.settings').get().keys.telescope.i.select, '') + end, + } + end) + it("length limit", function() + assert_scenario{ + setup = function() + require('neoclip').setup({ + length_limit = 8, + }) + end, + initial_buffer = [[1234 +567 + +123456789 +]], + feedkeys = { + "yy", + "yj", + "y2j", + "3j", + "y8l", + "y9l", + "yy", + }, + assert = function() + assert_equal_tables( + { + { + contents = {"12345678"}, + filetype = "", + regtype = "c" + }, + { + contents = {"1234", "567"}, + filetype = "", + regtype = "l" + }, + { + contents = {"1234"}, + filetype = "", + regtype = "l" + }, + }, + require('neoclip.storage').get().yanks + ) + end, + } + end) +end) + +-- TODO why does this needs it's own thing? +describe("neoclip", function() + after_each(function() + require('neoclip.storage').clear() + unload('neoclip') + unload('telescope') + vim.api.nvim_buf_set_lines(0, 0, -1, true, {}) + end) + it("replay directly", function() + assert_scenario{ + initial_buffer = [[some line +another line]], + feedkeys = { + "qq", + "yyp", + "q", + "qq", + "j", + "q", + { + keys=[[:lua require('telescope').extensions.macroscope.default()]], + after = function() + vim.wait(100, function() end) + end, + }, + "kq", + }, + assert = function() + assert.are.equal(vim.fn.getreg('q'), 'j') + end, + expected_buffer = [[some line +some line +another line +another line]], + } + end) +end) diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/sorted_set_spec.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/sorted_set_spec.lua new file mode 100644 index 0000000..33693c7 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary/sorted_set_spec.lua @@ -0,0 +1,106 @@ +local ss = require('neoclip.sorted_set') + +local foo = { + regtype = 'c', + contents = {'foo'}, + filetype = 'lua', +} + +local bar = { + regtype = 'c', + contents = {'bar'}, + filetype = 'lua', +} + +local foo_python = { + regtype = 'c', + contents = {'foo'}, + filetype = 'python', +} + +local foo_linewise = { + regtype = 'l', + contents = {'foo'}, + filetype = 'lua', +} + +describe("storage", function() + it("add single", function() + local s = ss.new() + local entry = { + regtype = 'c', + contents = {'foo', 'bar'}, + filetype = 'lua', + } + s:insert(entry) + + assert_equal_tables(s:values(), {entry}) + end) + it("ordering", function() + local s = ss.new() + s:insert(foo) + s:insert(bar) + + assert_equal_tables(s:values(), {foo, bar}) + end) + it("reversed", function() + local s = ss.new() + s:insert(foo) + s:insert(bar) + + assert_equal_tables(s:values({reversed = true}), {bar, foo}) + end) + it("uniqueness", function() + local s = ss.new() + s:insert(foo) + s:insert(foo_python) + s:insert(foo_linewise) + + assert_equal_tables(s:values(), {foo_python, foo_linewise}) + end) + it("max size", function() + local max_size = 2 + local s = ss.new(max_size) + local a = { + regtype = 'c', + contents = {'a'}, + filetype = 'lua', + } + local b = { + regtype = 'c', + contents = {'b'}, + filetype = 'python', + } + local c = { + regtype = 'c', + contents = {'c'}, + filetype = 'python', + } + s:insert(a) + s:insert(b) + s:insert(c) + + assert_equal_tables(s:values(), {b, c}) + end) + it("remove", function() + local s = ss.new() + s:insert(foo) + s:insert(bar) + assert_equal_tables(s:values(), {foo, bar}) + s:remove(foo) + assert_equal_tables(s:values(), {bar}) + s:insert(foo) + assert_equal_tables(s:values(), {bar, foo}) + s:remove(foo) + assert_equal_tables(s:values(), {bar}) + end) + it("clear", function() + local s = ss.new() + s:insert(foo) + s:insert(bar) + s:clear() + assert_equal_tables(s:values(), {}) + s:insert(foo) + assert_equal_tables(s:values(), {foo}) + end) +end) diff --git a/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary_init.lua b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary_init.lua new file mode 100644 index 0000000..ad6433c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-neoclip.lua/tests/plenary_init.lua @@ -0,0 +1,2 @@ +vim.o.runtimepath = vim.o.runtimepath .. ',./rtps/plenary.nvim' +vim.cmd('runtime! plugin/plenary.vim') diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/CONTRIBUTING.md b/etc/soft/nvim/+plugins/nvim-treesitter/CONTRIBUTING.md index 9ae9751..e5d39e4 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/CONTRIBUTING.md +++ b/etc/soft/nvim/+plugins/nvim-treesitter/CONTRIBUTING.md @@ -89,131 +89,183 @@ effect on highlighting. We will work on improving highlighting in the near futur #### Misc -``` -@comment -@error for error `ERROR` nodes. -@none to disable completely the highlight -@punctuation.delimiter for `;` `.` `,` -@punctuation.bracket for `()` or `{}` -@punctuation.special for symbols with special meaning like `{}` in string interpolation. +```scheme +@comment ; line and block comments +@error ; syntax/parser errors +@none ; completely disable the highlight +@preproc ; various preprocessor directives & shebangs +@define ; preprocessor definition directives +@operator ; symbolic operators (e.g. `+` / `*`) ``` -#### Constants +#### Punctuation +```scheme +@punctuation.delimiter ; delimiters (e.g. `;` / `.` / `,`) +@punctuation.bracket ; brackets (e.g. `()` / `{}` / `[]`) +@punctuation.special ; special symbols (e.g. `{}` in string interpolation) ``` -@constant -@constant.builtin -@constant.macro -@string -@string.regex -@string.escape -@string.special -@character -@number -@boolean -@float + +#### Literals + +```scheme +@string ; string literals +@string.regex ; regular expressions +@string.escape ; escape sequences +@string.special ; other special strings (e.g. dates) + +@character ; character literals +@character.special ; special characters (e.g. wildcards) + +@boolean ; boolean literals +@number ; numeric literals +@float ; floating-point number literals ``` #### Functions -``` -@function -@function.builtin -@function.macro -@parameter +```scheme +@function ; function definitions +@function.builtin ; built-in functions +@function.call ; function calls +@function.macro ; preprocessor macros -@method -@field -@property +@method ; method definitions +@method.call ; method calls -@constructor +@constructor ; constructor calls and definitions +@parameter ; parameters of a function ``` #### Keywords +```scheme +@keyword ; various keywords +@keyword.function ; keywords that define a function (e.g. `func` in Go, `def` in Python) +@keyword.operator ; operators that are English words (e.g. `and` / `or`) +@keyword.return ; keywords like `return` and `yield` + +@conditional ; keywords related to conditionals (e.g. `if` / `else`) +@repeat ; keywords related to loops (e.g. `for` / `while`) +@debug ; keywords related to debugging +@label ; GOTO and other labels (e.g. `label:` in C) +@include ; keywords for including modules (e.g. `import` / `from` in Python) +@exception ; keywords related to exceptions (e.g. `throw` / `catch`) ``` -@conditional (e.g. `if`, `else`) -@repeat (e.g. `for`, `while`) -@label for C/Lua-like labels -@keyword -@keyword.function (keyword to define a function, e.g. `func` in Go, `def` in Python) -@keyword.operator (for operators that are English words, e.g. `and`, `or`) -@keyword.return -@operator (for symbolic operators, e.g. `+`, `*`) -@exception (e.g. `throw`, `catch`) -@include keywords for including modules (e.g. import/from in Python) - -@type -@type.builtin -@namespace for identifiers referring to namespaces -@symbol for identifiers referring to symbols -@attribute for e.g. Python decorators -``` -#### Variables +#### Types + +```scheme +@type ; type or class definitions and annotations +@type.builtin ; built-in types +@type.definition ; type definitions (e.g. `typedef` in C) +@type.qualifier ; type qualifiers (e.g. `const`) +@storageclass ; visibility/life-time modifiers +@storageclass.lifetime ; life-time modifiers (e.g. `static`) +@attribute ; attribute annotations (e.g. Python decorators) +@field ; object and struct fields +@property ; similar to `@field` ``` -@variable -@variable.builtin + +#### Identifiers + +```scheme +@variable ; various variable names +@variable.builtin ; built-in variable names (e.g. `this`) + +@constant ; constant identifiers +@constant.builtin ; built-in constant values +@constant.macro ; constants defined by the preprocessor + +@namespace ; modules or namespaces +@symbol ; symbols or atoms ``` #### Text Mainly for markup languages. -``` -@text -@text.strong -@text.emphasis -@text.underline -@text.strike -@text.title -@text.literal -@text.uri -@text.math (e.g. for LaTeX math environments) -@text.environment (e.g. for text environments of markup languages) -@text.environment.name (e.g. for the name/the string indicating the type of text environment) -@text.reference (for footnotes, text references, citations) - -@text.note -@text.warning -@text.danger +```scheme +@text ; non-structured text +@text.strong ; bold text +@text.emphasis ; text with emphasis +@text.underline ; underlined text +@text.strike ; strikethrough text +@text.title ; text that is part of a title +@text.literal ; literal or verbatim text +@text.uri ; URIs (e.g. hyperlinks) +@text.math ; math environments (e.g. `$ ... $` in LaTeX) +@text.environment ; text environments of markup languages +@text.environment.name ; text indicating the type of an environment +@text.reference ; text references, footnotes, citations, etc. + +@text.todo ; todo notes +@text.note ; info notes +@text.warning ; warning notes +@text.danger ; danger/error notes + +@text.diff.add ; added text (for diff files) +@text.diff.delete ; deleted text (for diff files) ``` #### Tags -Used for xml-like tags +Used for XML-like tags. +```scheme +@tag ; XML tag names +@tag.attribute ; XML tag attributes +@tag.delimiter ; XML tag delimiters ``` -@tag -@tag.attribute -@tag.delimiter + +#### Conceal + + +```scheme +@conceal ; for captures that are only used for concealing ``` -### Locals +`@conceal` must be followed by `(#set! conceal "")`. + +#### Spell + +```scheme +@spell ; for defining regions to be spellchecked +@nospell ; for defining regions that should _not_ be spellchecked +``` + +#### Non-standard + +These captures are used by some languages but don't have any default highlights. +They fall back to the parent capture if they are not manually defined. +```scheme +@variable.global ``` -@definition for various definitions -@definition.constant -@definition.function -@definition.method -@definition.var -@definition.parameter -@definition.macro -@definition.type -@definition.field -@definition.enum -@definition.namespace for modules or C++ namespaces -@definition.import for imported names - -@definition.associated to determine the type of a variable -@definition.doc for documentation adjacent to a definition. E.g. - -@scope -@reference -@constructor + +### Locals + +```scheme +@definition ; various definitions +@definition.constant ; constants +@definition.function ; functions +@definition.method ; methods +@definition.var ; variables +@definition.parameter ; parameters +@definition.macro ; preprocessor macros +@definition.type ; types or classes +@definition.field ; fields or properties +@definition.enum ; enumerations +@definition.namespace ; modules or namespaces +@definition.import ; imported names +@definition.associated ; the associated type of a variable + +@scope ; scope block +@reference ; identifier reference ``` + #### Definition Scope You can set the scope of a definition by setting the `scope` property on the definition. @@ -244,8 +296,8 @@ Possible scope values are: You can define folds for a given language by adding a `folds.scm` query : -``` -@fold +```scheme +@fold ; fold this node ``` If the `fold.scm` query is not present, this will fallback to the `@scope` captures in the `locals` @@ -259,25 +311,26 @@ You can directly use the name of the language that you want to inject (e.g. `@ht If you want to dynamically detect the language (e.g. for Markdown blocks) use the `@language` to capture the node describing the language and `@content` to describe the injection region. -``` -@{language} ; e.g. @html to describe a html region +```scheme +@{lang} ; e.g. @html to describe a html region -@language ; dynamic detection of the injection language (i.e. the text of the captured node describes the language). -@content ; region for the dynamically detected language. -@combined ; This will combine all matches of a pattern as one single block of content. +@language ; dynamic detection of the injection language (i.e. the text of the captured node describes the language) +@content ; region for the dynamically detected language +@combined ; combine all matches of a pattern as one single block of content ``` ### Indents -``` -@indent ; Indent children when matching this node -@indent_end ; Marks the end of indented block -@aligned_indent ; Behaves like python aligned/hanging indent -@dedent ; Dedent children when matching this node -@branch ; Dedent itself when matching this node -@ignore ; Do not indent in this node -@auto ; Behaves like 'autoindent' buffer option +```scheme +@indent ; indent children when matching this node +@indent_end ; marks the end of indented block +@aligned_indent ; behaves like python aligned/hanging indent +@dedent ; dedent children when matching this node +@branch ; dedent itself when matching this node +@ignore ; do not indent in this node +@auto ; behaves like 'autoindent' buffer option +@zero_indent ; sets this node at position 0 (no indent) ``` -[Zulip]: nvim-treesitter.zulipchat.com +[Zulip]: https://nvim-treesitter.zulipchat.com [Matrix channel]: https://matrix.to/#/#nvim-treesitter:matrix.org diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/README.md b/etc/soft/nvim/+plugins/nvim-treesitter/README.md index 260bbb1..a72c5e2 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/README.md +++ b/etc/soft/nvim/+plugins/nvim-treesitter/README.md @@ -31,7 +31,7 @@ The goal of `nvim-treesitter` is both to provide a simple and easy way to use the interface for [tree-sitter](https://github.com/tree-sitter/tree-sitter) in Neovim and to provide some basic functionality such as highlighting based on it: -![cpp example](assets/example-cpp.png) +![example-cpp](https://user-images.githubusercontent.com/2361214/202753610-e923bf4e-e88f-494b-bb1e-d22a7688446f.png) Traditional highlighting (left) vs Treesitter-based highlighting (right). More examples can be found in [our gallery](https://github.com/nvim-treesitter/nvim-treesitter/wiki/Gallery). @@ -63,7 +63,7 @@ For more detailed information on setting these up, see ["Advanced setup"](#advan ## Requirements -- Neovim latest stable version or [nightly](https://github.com/neovim/neovim#install-from-source) +- **Neovim 0.8.0 or later** built with **tree-sitter 0.20.3+** (latest [nightly](https://github.com/neovim/neovim#install-from-source) recommended) - `tar` and `curl` in your path (or alternatively `git`) - A C compiler in your path and libstdc++ installed ([Windows users please read this!](https://github.com/nvim-treesitter/nvim-treesitter/wiki/Windows-support)). @@ -98,26 +98,44 @@ To make sure a parser is at the latest compatible version (as specified in `nvim Each module provides a distinct tree-sitter-based feature such as [highlighting](#highlight), [indentation](#indentation), or [folding](#folding); see [`:h nvim-treesitter-modules`](doc/nvim-treesitter.txt) or ["Available modules"](#available-modules) below for a list of modules and their options. -All modules are disabled by default and need to be activated explicitly in your `init.vim`, e.g., via +Following examples assume that you are configuring neovim with lua. If you are using vimscript, see `:help lua-heredoc`. +All modules are disabled by default and need to be activated explicitly in your `init.lua`, e.g., via -```vim -lua < Advanced Setup) + -- parser_install_dir = "/some/path/to/store/parsers", -- Remember to run vim.opt.runtimepath:append("/some/path/to/store/parsers")! + highlight = { -- `false` will disable the whole extension enable = true, + -- NOTE: these are the names of the parsers and not the filetype. (for example if you want to + -- disable highlighting for the `tex` filetype, you need to include `latex` in this list as this is + -- the name of the parser) -- list of language that will be disabled disable = { "c", "rust" }, + -- Or use a function for more flexibility, e.g. to disable slow treesitter highlight for large files + disable = function(lang, buf) + local max_filesize = 100 * 1024 -- 100 KB + local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf)) + if ok and stats and stats.size > max_filesize then + return true + end + end, -- Setting this to true will run `:h syntax` and tree-sitter at the same time. -- Set this to `true` if you depend on 'syntax' being enabled (like for indentation). @@ -126,7 +144,6 @@ require'nvim-treesitter.configs'.setup { additional_vim_regex_highlighting = false, }, } -EOF ``` Each module can also be enabled or disabled interactively through the following commands: @@ -134,8 +151,8 @@ Each module can also be enabled or disabled interactively through the following ```vim :TSBufEnable {module} " enable module on current buffer :TSBufDisable {module} " disable module on current buffer -:TSEnableAll {module} [{ft}] " enable module on every buffer. If filetype is specified, enable only for this filetype. -:TSDisableAll {module} [{ft}] " disable module on every buffer. If filetype is specified, disable only for this filetype. +:TSEnable {module} [{ft}] " enable module on every buffer. If filetype is specified, enable only for this filetype. +:TSDisable {module} [{ft}] " disable module on every buffer. If filetype is specified, disable only for this filetype. :TSModuleInfo [{module}] " list information about modules state for each filetype ``` @@ -148,34 +165,42 @@ For `nvim-treesitter` to support a specific feature for a specific language requ The following is a list of languages for which a parser can be installed through `:TSInstall`; a checked box means that `nvim-treesitter` also contains queries at least for the `highlight` module. -Experimental parsers are parsers that are maintained, but not stable enough for -daily use yet. They are excluded from automatic installation when -`ensure_installed` is set to `"maintained"`. +Experimental parsers are parsers that have a maintainer but are not stable enough for +daily use yet. We are looking for maintainers to add more parsers and to write query files for their languages. Check our [tracking issue](https://github.com/nvim-treesitter/nvim-treesitter/issues/2282) for open language requests. +- [x] [agda](https://github.com/AusCyberman/tree-sitter-agda) (maintained by @Decodetalkers) +- [x] [arduino](https://github.com/ObserverOfTime/tree-sitter-arduino) (maintained by @ObserverOfTime) +- [x] [astro](https://github.com/virchau13/tree-sitter-astro) (maintained by @virchau13) +- [ ] [awk](https://github.com/Beaglefoot/tree-sitter-awk) - [x] [bash](https://github.com/tree-sitter/tree-sitter-bash) (maintained by @TravonteD) - [x] [beancount](https://github.com/polarmutex/tree-sitter-beancount) (maintained by @polarmutex) -- [x] [bibtex](https://github.com/latex-lsp/tree-sitter-bibtex) (maintained by @theHamsta by asking @clason) +- [x] [bibtex](https://github.com/latex-lsp/tree-sitter-bibtex) (maintained by @theHamsta, @clason) +- [x] [blueprint](https://gitlab.com/gabmus/tree-sitter-blueprint.git) (experimental, maintained by @gabmus) - [x] [c](https://github.com/tree-sitter/tree-sitter-c) (maintained by @vigoux) - [x] [c_sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) (maintained by @Luxed) - [x] [clojure](https://github.com/sogaiu/tree-sitter-clojure) (maintained by @sogaiu) - [x] [cmake](https://github.com/uyha/tree-sitter-cmake) (maintained by @uyha) - [x] [comment](https://github.com/stsewd/tree-sitter-comment) (maintained by @stsewd) - [x] [commonlisp](https://github.com/theHamsta/tree-sitter-commonlisp) (maintained by @theHamsta) +- [x] [cooklang](https://github.com/addcninblue/tree-sitter-cooklang) (maintained by @addcninblue) - [x] [cpp](https://github.com/tree-sitter/tree-sitter-cpp) (maintained by @theHamsta) - [x] [css](https://github.com/tree-sitter/tree-sitter-css) (maintained by @TravonteD) - [x] [cuda](https://github.com/theHamsta/tree-sitter-cuda) (maintained by @theHamsta) - [x] [d](https://github.com/CyberShadow/tree-sitter-d) (experimental, maintained by @nawordar) - [x] [dart](https://github.com/UserNobody14/tree-sitter-dart) (maintained by @Akin909) - [x] [devicetree](https://github.com/joelspadin/tree-sitter-devicetree) (maintained by @jedrzejboczar) +- [x] [diff](https://github.com/the-mikedavis/tree-sitter-diff) (maintained by @gbprod) - [x] [dockerfile](https://github.com/camdencheek/tree-sitter-dockerfile) (maintained by @camdencheek) - [x] [dot](https://github.com/rydesun/tree-sitter-dot) (maintained by @rydesun) - [x] [eex](https://github.com/connorlay/tree-sitter-eex) (maintained by @connorlay) -- [x] [elixir](https://github.com/elixir-lang/tree-sitter-elixir) (maintained by @jonatanklosko) +- [x] [elixir](https://github.com/elixir-lang/tree-sitter-elixir) (maintained by @jonatanklosko, @connorlay) - [ ] [elm](https://github.com/elm-tooling/tree-sitter-elm) +- [x] [elvish](https://github.com/ckafi/tree-sitter-elvish) (maintained by @ckafi) +- [ ] [embedded_template](https://github.com/tree-sitter/tree-sitter-embedded-template) - [x] [erlang](https://github.com/AbstractMachinesLab/tree-sitter-erlang) (maintained by @ostera) - [x] [fennel](https://github.com/travonted/tree-sitter-fennel) (maintained by @TravonteD) - [x] [fish](https://github.com/ram02z/tree-sitter-fish) (maintained by @ram02z) @@ -183,6 +208,10 @@ We are looking for maintainers to add more parsers and to write query files for - [ ] [fortran](https://github.com/stadelmanma/tree-sitter-fortran) - [x] [fusion](https://gitlab.com/jirgn/tree-sitter-fusion.git) (maintained by @jirgn) - [x] [Godot (gdscript)](https://github.com/PrestonKnopp/tree-sitter-gdscript) (maintained by @Shatur95) +- [x] [git_rebase](https://github.com/the-mikedavis/tree-sitter-git-rebase) (maintained by @gbprod) +- [x] [gitattributes](https://github.com/ObserverOfTime/tree-sitter-gitattributes) (maintained by @ObserverOfTime) +- [x] [gitignore](https://github.com/shunsambongi/tree-sitter-gitignore) (maintained by @theHamsta) +- [x] [gleam](https://github.com/J3RN/tree-sitter-gleam) (maintained by @connorlay) - [x] [Glimmer and Ember](https://github.com/alexlafroscia/tree-sitter-glimmer) (maintained by @alexlafroscia) - [x] [glsl](https://github.com/theHamsta/tree-sitter-glsl) (maintained by @theHamsta) - [x] [go](https://github.com/tree-sitter/tree-sitter-go) (maintained by @theHamsta, @WinWisely268) @@ -194,72 +223,102 @@ We are looking for maintainers to add more parsers and to write query files for - [ ] [haskell](https://github.com/tree-sitter/tree-sitter-haskell) - [x] [hcl](https://github.com/MichaHoffmann/tree-sitter-hcl) (maintained by @MichaHoffmann) - [x] [heex](https://github.com/connorlay/tree-sitter-heex) (maintained by @connorlay) +- [x] [help](https://github.com/neovim/tree-sitter-vimdoc) (maintained by @vigoux) - [x] [hjson](https://github.com/winston0410/tree-sitter-hjson) (maintained by @winston0410) +- [x] [hlsl](https://github.com/theHamsta/tree-sitter-hlsl) (maintained by @theHamsta) - [x] [hocon](https://github.com/antosha417/tree-sitter-hocon) (maintained by @antosha417) - [x] [html](https://github.com/tree-sitter/tree-sitter-html) (maintained by @TravonteD) -- [x] [http](https://github.com/NTBBloodbath/tree-sitter-http) (maintained by @NTBBloodbath) +- [x] [http](https://github.com/rest-nvim/tree-sitter-http) (maintained by @NTBBloodbath) - [x] [java](https://github.com/tree-sitter/tree-sitter-java) (maintained by @p00f) - [x] [javascript](https://github.com/tree-sitter/tree-sitter-javascript) (maintained by @steelsojka) +- [ ] [jq](https://github.com/flurie/tree-sitter-jq) - [x] [jsdoc](https://github.com/tree-sitter/tree-sitter-jsdoc) (maintained by @steelsojka) - [x] [json](https://github.com/tree-sitter/tree-sitter-json) (maintained by @steelsojka) - [x] [json5](https://github.com/Joakker/tree-sitter-json5) (maintained by @Joakker) - [x] [JSON with comments](https://gitlab.com/WhyNotHugo/tree-sitter-jsonc.git) (maintained by @WhyNotHugo) +- [x] [jsonnet](https://github.com/sourcegraph/tree-sitter-jsonnet) (maintained by @nawordar) - [x] [julia](https://github.com/tree-sitter/tree-sitter-julia) (maintained by @mroavi, @theHamsta) - [x] [kotlin](https://github.com/fwcd/tree-sitter-kotlin) (maintained by @SalBakraa) -- [x] [latex](https://github.com/latex-lsp/tree-sitter-latex) (maintained by @theHamsta by asking @clason) +- [x] [lalrpop](https://github.com/traxys/tree-sitter-lalrpop) (maintained by @traxys) +- [x] [latex](https://github.com/latex-lsp/tree-sitter-latex) (maintained by @theHamsta, @clason) - [x] [ledger](https://github.com/cbarrete/tree-sitter-ledger) (maintained by @cbarrete) - [x] [llvm](https://github.com/benwilliamgraham/tree-sitter-llvm) (maintained by @benwilliamgraham) - [x] [lua](https://github.com/MunifTanjim/tree-sitter-lua) (maintained by @muniftanjim) +- [x] [m68k](https://github.com/grahambates/tree-sitter-m68k) (maintained by @grahambates) - [x] [make](https://github.com/alemuller/tree-sitter-make) (maintained by @lewis6991) -- [ ] [markdown](https://github.com/MDeiml/tree-sitter-markdown) +- [x] [markdown](https://github.com/MDeiml/tree-sitter-markdown) (experimental, maintained by @MDeiml) +- [x] [markdown_inline](https://github.com/MDeiml/tree-sitter-markdown) (experimental, maintained by @MDeiml) +- [x] [menhir](https://github.com/Kerl13/tree-sitter-menhir) (maintained by @Kerl13) +- [ ] [mermaid](https://github.com/monaqa/tree-sitter-mermaid) (experimental) +- [x] [meson](https://github.com/Decodetalkers/tree-sitter-meson) (maintained by @Decodetalkers) +- [ ] [nickel](https://github.com/nickel-lang/tree-sitter-nickel) - [x] [ninja](https://github.com/alemuller/tree-sitter-ninja) (maintained by @alemuller) - [x] [nix](https://github.com/cstrahan/tree-sitter-nix) (maintained by @leo60228) - [x] [norg](https://github.com/nvim-neorg/tree-sitter-norg) (maintained by @JoeyGrajciar, @vhyrro, @mrossinek) - [x] [ocaml](https://github.com/tree-sitter/tree-sitter-ocaml) (maintained by @undu) - [x] [ocaml_interface](https://github.com/tree-sitter/tree-sitter-ocaml) (maintained by @undu) - [x] [ocamllex](https://github.com/atom-ocaml/tree-sitter-ocamllex) (maintained by @undu) +- [ ] [org](https://github.com/milisims/tree-sitter-org) - [x] [pascal](https://github.com/Isopod/tree-sitter-pascal.git) (maintained by @isopod) - [x] [perl](https://github.com/ganezdragon/tree-sitter-perl) (maintained by @ganezdragon) - [x] [php](https://github.com/tree-sitter/tree-sitter-php) (maintained by @tk-shirasaka) - [x] [phpdoc](https://github.com/claytonrcarter/tree-sitter-phpdoc) (experimental, maintained by @mikehaertl) - [x] [pioasm](https://github.com/leo60228/tree-sitter-pioasm) (maintained by @leo60228) - [x] [prisma](https://github.com/victorhqc/tree-sitter-prisma) (maintained by @elianiva) -- [x] [pug](https://github.com/zealot128/tree-sitter-pug) (maintained by @zealot128) +- [x] [proto](https://github.com/mitchellh/tree-sitter-proto) (maintained by @fsouza) +- [x] [pug](https://github.com/zealot128/tree-sitter-pug) (experimental, maintained by @zealot128) - [x] [python](https://github.com/tree-sitter/tree-sitter-python) (maintained by @stsewd, @theHamsta) - [x] [ql](https://github.com/tree-sitter/tree-sitter-ql) (maintained by @pwntester) +- [x] [qmljs](https://github.com/yuja/tree-sitter-qmljs) (maintained by @yuja) - [x] [Tree-sitter query language](https://github.com/nvim-treesitter/tree-sitter-query) (maintained by @steelsojka) - [x] [r](https://github.com/r-lib/tree-sitter-r) (maintained by @jimhester) +- [x] [racket](https://github.com/6cdh/tree-sitter-racket) (maintained by @6cdh) - [x] [rasi](https://github.com/Fymyte/tree-sitter-rasi) (maintained by @Fymyte) - [x] [regex](https://github.com/tree-sitter/tree-sitter-regex) (maintained by @theHamsta) +- [x] [rego](https://github.com/FallenAngel97/tree-sitter-rego) (maintained by @FallenAngel97) +- [x] [rnoweb](https://github.com/bamonroe/tree-sitter-rnoweb) (maintained by @bamonroe) - [x] [rst](https://github.com/stsewd/tree-sitter-rst) (maintained by @stsewd) - [x] [ruby](https://github.com/tree-sitter/tree-sitter-ruby) (maintained by @TravonteD) - [x] [rust](https://github.com/tree-sitter/tree-sitter-rust) (maintained by @vigoux) - [x] [scala](https://github.com/tree-sitter/tree-sitter-scala) (maintained by @stevanmilic) +- [x] [scheme](https://github.com/6cdh/tree-sitter-scheme) (maintained by @6cdh) - [x] [scss](https://github.com/serenadeai/tree-sitter-scss) (maintained by @elianiva) +- [x] [slint](https://github.com/jrmoulton/tree-sitter-slint) (experimental, maintained by @jrmoulton) +- [x] [solidity](https://github.com/YongJieYongJie/tree-sitter-solidity) (maintained by @YongJieYongJie) - [x] [sparql](https://github.com/BonaBeavis/tree-sitter-sparql) (maintained by @bonabeavis) +- [x] [sql](https://github.com/derekstride/tree-sitter-sql) (maintained by @derekstride) - [x] [supercollider](https://github.com/madskjeldgaard/tree-sitter-supercollider) (maintained by @madskjeldgaard) - [x] [surface](https://github.com/connorlay/tree-sitter-surface) (maintained by @connorlay) - [x] [svelte](https://github.com/Himujjal/tree-sitter-svelte) (maintained by @elianiva) -- [ ] [swift](https://github.com/alex-pinkus/tree-sitter-swift) +- [x] [swift](https://github.com/alex-pinkus/tree-sitter-swift) (maintained by @alex-pinkus) +- [x] [sxhkdrc](https://github.com/RaafatTurki/tree-sitter-sxhkdrc) (maintained by @RaafatTurki) - [x] [teal](https://github.com/euclidianAce/tree-sitter-teal) (maintained by @euclidianAce) -- [x] [tlaplus](https://github.com/tlaplus-community/tree-sitter-tlaplus) (maintained by @ahelwer) +- [x] [tiger](https://github.com/ambroisie/tree-sitter-tiger) (maintained by @ambroisie) +- [x] [tlaplus](https://github.com/tlaplus-community/tree-sitter-tlaplus) (maintained by @ahelwer, @susliko) +- [x] [todotxt](https://github.com/arnarg/tree-sitter-todotxt.git) (experimental, maintained by @arnarg) - [x] [toml](https://github.com/ikatyang/tree-sitter-toml) (maintained by @tk-shirasaka) - [x] [tsx](https://github.com/tree-sitter/tree-sitter-typescript) (maintained by @steelsojka) - [x] [turtle](https://github.com/BonaBeavis/tree-sitter-turtle) (maintained by @bonabeavis) +- [x] [twig](https://github.com/gbprod/tree-sitter-twig) (maintained by @gbprod) - [x] [typescript](https://github.com/tree-sitter/tree-sitter-typescript) (maintained by @steelsojka) -- [x] [verilog](https://github.com/tree-sitter/tree-sitter-verilog) (experimental, maintained by @zegervdv) +- [x] [v](https://github.com/vlang/vls) (maintained by @tami5) +- [x] [vala](https://github.com/vala-lang/tree-sitter-vala) (maintained by @Prince781, @vala-lang) +- [x] [verilog](https://github.com/tree-sitter/tree-sitter-verilog) (maintained by @zegervdv) +- [x] [vhs](https://github.com/charmbracelet/tree-sitter-vhs) (maintained by @caarlos0, @maaslalani) - [x] [vim](https://github.com/vigoux/tree-sitter-viml) (maintained by @vigoux) - [x] [vue](https://github.com/ikatyang/tree-sitter-vue) (maintained by @WhyNotHugo) +- [x] [wgsl](https://github.com/szebniok/tree-sitter-wgsl) (maintained by @szebniok) - [x] [yaml](https://github.com/ikatyang/tree-sitter-yaml) (maintained by @stsewd) - [x] [yang](https://github.com/Hubro/tree-sitter-yang) (maintained by @Hubro) - [x] [zig](https://github.com/maxxnino/tree-sitter-zig) (maintained by @maxxnino) +For related information on the supported languages, including related plugins, see [this wiki page](https://github.com/nvim-treesitter/nvim-treesitter/wiki/Supported-Languages-Information). # Available modules Modules provide the top-level features of `nvim-treesitter`. -The following is a list of modules included in `nvim-treesitter` and their configuration via `init.vim` (where multiple modules can be combined in a single call to `setup`). +The following is a list of modules included in `nvim-treesitter` and their configuration via `init.lua` (where multiple modules can be combined in a single call to `setup`). Note that not all modules work for all languages (depending on the queries available for them). Additional modules can be provided as [external plugins](https://github.com/nvim-treesitter/nvim-treesitter/wiki/Extra-modules-and-plugins). @@ -267,15 +326,10 @@ Additional modules can be provided as [external plugins](https://github.com/nvim Consistent syntax highlighting. -```vim -lua < for installation instructions. 2. Run `tree-sitter generate` in this directory (followed by `tree-sitter test` for good measure). -3. Add the following snippet to your `init.vim`: +3. Add the following snippet to your `init.lua`: -```vim -lua < diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/query.vim b/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/query.vim deleted file mode 100644 index a13b726..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/query.vim +++ /dev/null @@ -1 +0,0 @@ -setlocal commentstring=;\ %s diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/surface.vim b/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/surface.vim deleted file mode 100644 index 7bbb073..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/after/ftplugin/surface.vim +++ /dev/null @@ -1 +0,0 @@ -setlocal commentstring={!--\ %s\ --} diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/assets/example-cpp.png b/etc/soft/nvim/+plugins/nvim-treesitter/assets/example-cpp.png deleted file mode 100644 index a35d1d3..0000000 Binary files a/etc/soft/nvim/+plugins/nvim-treesitter/assets/example-cpp.png and /dev/null differ diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/autoload/health/nvim_treesitter.vim b/etc/soft/nvim/+plugins/nvim-treesitter/autoload/health/nvim_treesitter.vim deleted file mode 100644 index 53e187c..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/autoload/health/nvim_treesitter.vim +++ /dev/null @@ -1,3 +0,0 @@ -function! health#nvim_treesitter#check() - lua require 'nvim-treesitter.health'.check() -endfunction diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/autoload/nvim_treesitter.vim b/etc/soft/nvim/+plugins/nvim-treesitter/autoload/nvim_treesitter.vim index 1216e99..9095398 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/autoload/nvim_treesitter.vim +++ b/etc/soft/nvim/+plugins/nvim-treesitter/autoload/nvim_treesitter.vim @@ -1,5 +1,5 @@ function! nvim_treesitter#statusline(...) abort - return luaeval("require'nvim-treesitter'.statusline(_A)", get(a:, 1, {})) + return luaeval("require'nvim-treesitter.statusline'.statusline(_A)", get(a:, 1, {})) endfunction function! nvim_treesitter#foldexpr() abort @@ -7,11 +7,11 @@ function! nvim_treesitter#foldexpr() abort endfunction function! nvim_treesitter#installable_parsers(arglead, cmdline, cursorpos) abort - return join(luaeval("require'nvim-treesitter.parsers'.available_parsers()") + ['all', 'maintained'], "\n") + return join(luaeval("require'nvim-treesitter.parsers'.available_parsers()") + ['all'], "\n") endfunction function! nvim_treesitter#installed_parsers(arglead, cmdline, cursorpos) abort - return join(luaeval("require'nvim-treesitter.info'.installed_parsers()") + ['all', 'maintained'], "\n") + return join(luaeval("require'nvim-treesitter.info'.installed_parsers()") + ['all'], "\n") endfunction function! nvim_treesitter#available_modules(arglead, cmdline, cursorpos) abort diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/contrib/nvim-treesitter-scm-1.rockspec b/etc/soft/nvim/+plugins/nvim-treesitter/contrib/nvim-treesitter-scm-1.rockspec new file mode 100644 index 0000000..cacc9eb --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/contrib/nvim-treesitter-scm-1.rockspec @@ -0,0 +1,32 @@ +local MODREV, SPECREV = "scm", "-1" +rockspec_format = "3.0" +package = "nvim-treesitter" +version = MODREV .. SPECREV + +description = { + summary = "Nvim Treesitter configurations and abstraction layer", + labels = { "neovim"}, + homepage = "https://github.com/nvim-treesitter/nvim-treesitter", + license = "Apache-2.0", +} + +dependencies = { + "lua >= 5.1, < 5.4", +} + +source = { + url = "http://github.com/nvim-treesitter/nvim-treesitter/archive/v" .. MODREV .. ".zip", +} + +if MODREV == 'scm' then + source = { + url = 'git://github.com/nvim-treesitter/nvim-treesitter', + } +end + +build = { + type = "builtin", + copy_directories = { + 'plugin' + } +} diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/doc/nvim-treesitter.txt b/etc/soft/nvim/+plugins/nvim-treesitter/doc/nvim-treesitter.txt index 63e3cec..4feabb7 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/doc/nvim-treesitter.txt +++ b/etc/soft/nvim/+plugins/nvim-treesitter/doc/nvim-treesitter.txt @@ -35,18 +35,26 @@ To get a list of supported languages < By default, everything is disabled. -To enable supported features, put this in your `init.vim` file: +To enable supported features, put this in your `init.lua` file: > - lua < - lua < - lua < - lua < - lua < + lua < - lua < - lua <` and `*` in C. - - *hl-TSParameter* -`TSParameter` -Parameters of a function. - - *hl-TSParameterReference* -`TSParameterReference` -References to parameters of a function. - - *hl-TSProperty* -`TSProperty` -Same as `TSField`. - - *hl-TSPunctDelimiter* -`TSPunctDelimiter` -Punctuation delimiters: Periods, commas, semicolons, etc. - - *hl-TSPunctBracket* -`TSPunctBracket` -Brackets, braces, parentheses, etc. - - *hl-TSPunctSpecial* -`TSPunctSpecial` -Special punctuation that doesn't fit into the previous categories. - - *hl-TSRepeat* -`TSRepeat` -Keywords related to loops: `for`, `while`, etc. - - *hl-TSString* -`TSString` -String literals. - - *hl-TSStringRegex* -`TSStringRegex` -Regular expression literals. - - *hl-TSStringEscape* -`TSStringEscape` -Escape characters within a string: `\n`, `\t`, etc. - - *hl-TSStringSpecial* -`TSStringSpecial` -Strings with special meaning that don't fit into the previous categories. - - *hl-TSSymbol* -`TSSymbol` -Identifiers referring to symbols or atoms. - - *hl-TSTag* -`TSTag` -Tags like HTML tag names. - - *hl-TSTagAttribute* -`TSTagAttribute` -HTML tag attributes. - - *hl-TSTagDelimiter* -`TSTagDelimiter` -Tag delimiters like `<` `>` `/`. - - *hl-TSText* -`TSText` -Non-structured text. Like text in a markup language. - - *hl-TSSTrong* -`TSStrong` -Text to be represented in bold. - - *hl-TSEmphasis* -`TSEmphasis` -Text to be represented with emphasis. - - *hl-TSUnderline* -`TSUnderline` -Text to be represented with an underline. - - *hl-TSStrike* -`TSStrike` -Strikethrough text. - - *hl-TSTitle* -`TSTitle` -Text that is part of a title. - - *hl-TSLiteral* -`TSLiteral` -Literal or verbatim text. - - *hl-TSURI* -`TSURI` -URIs like hyperlinks or email addresses. - - *hl-TSMath* -`TSMath` -Math environments like LaTeX's `$ ... $` - - *hl-TSTextReference* -`TSTextReference` -Footnotes, text references, citations, etc. - - *hl-TSEnvironment* -`TSEnvironment` -Text environments of markup languages. - - *hl-TSEnvironmentName* -`TSEnvironmentName` -Text/string indicating the type of text environment. Like the name of a -`\begin` block in LaTeX. - - *hl-TSNote* -`TSNote` -Text representation of an informational note. - - *TSWarning* -`TSWarning` -Text representation of a warning note. - - *TSDanger* -`TSDanger` -Text representation of a danger note. - - *hl-TSType* -`TSType` -Type (and class) definitions and annotations. - - *hl-TSTypeBuiltin* -`TSTypeBuiltin` -Built-in types: `i32` in Rust. - - *hl-TSVariable* -`TSVariable` -Variable names that don't fit into other categories. - - *hl-TSVariableBuiltin* -`TSVariableBuiltin` -Variable names defined by the language: `this` or `self` in Javascript. - -============================================================================== PERFORMANCE *nvim-treesitter-performance* `nvim-treesitter` checks the 'runtimepath' on startup in order to discover diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/fusion.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/fusion.vim deleted file mode 100644 index 7a7839b..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/fusion.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.fusion setfiletype fusion diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdresource.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdresource.vim deleted file mode 100644 index df6b5b0..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdresource.vim +++ /dev/null @@ -1,2 +0,0 @@ -autocmd BufRead,BufNewFile *.tscn setlocal ft=gdresource -autocmd BufRead,BufNewFile *.tres setlocal ft=gdresource diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdscript.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdscript.vim deleted file mode 100644 index eaae36f..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gdscript.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.gd set ft=gdscript diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glimmer.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glimmer.vim deleted file mode 100644 index 510b0c3..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glimmer.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.hbs set ft=handlebars diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glsl.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glsl.vim deleted file mode 100644 index 6d74fdd..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/glsl.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.glsl set filetype=glsl diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gowork.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gowork.vim deleted file mode 100644 index 47edcb6..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/gowork.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile go.work set filetype=gowork diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/graphql.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/graphql.vim deleted file mode 100644 index c4f3b75..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/graphql.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.graphql,*.graphqls,*.gql setfiletype graphql diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hack.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hack.vim deleted file mode 100644 index e835a27..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hack.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.hack,*.hackpartial setfiletype hack diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hcl.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hcl.vim deleted file mode 100644 index cee17d8..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hcl.vim +++ /dev/null @@ -1,2 +0,0 @@ -autocmd BufRead,BufNewFile *.hcl set filetype=terraform -autocmd BufRead,BufNewFile *.tf,*.tfvars set filetype=terraform diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/heex.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/heex.vim deleted file mode 100644 index 6c76588..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/heex.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile *.heex set filetype=heex diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hjson.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hjson.vim deleted file mode 100644 index 65ed946..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/hjson.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.hjson set filetype=hjson diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/json5.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/json5.vim deleted file mode 100644 index ba74f21..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/json5.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufNewFile,BufRead *.json5 set ft=json5 diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ledger.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ledger.vim deleted file mode 100644 index 808e77e..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ledger.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.ldg,*.ledger,*.journal setfiletype ledger diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/nix.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/nix.vim deleted file mode 100644 index 96bff17..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/nix.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.nix setfiletype nix diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/prisma.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/prisma.vim deleted file mode 100644 index 704717e..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/prisma.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.prisma set filetype=prisma diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/pug.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/pug.vim deleted file mode 100644 index 3ca05c4..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/pug.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile *.pug setlocal filetype=pug diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ql.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ql.vim deleted file mode 100644 index b3dd575..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/ql.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.ql,*.qll setfiletype ql diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/query.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/query.vim deleted file mode 100644 index 8339b6b..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/query.vim +++ /dev/null @@ -1,11 +0,0 @@ -" Last Change: 2020 Sep 01 - -function! s:shouldFt(path) - let l:q_dir = fnamemodify(a:path, ":p:h:h:t") - - if l:q_dir =~? "queries" - setlocal ft=query - endif -endfunction - -autocmd BufNewFile,BufRead *.scm call s:shouldFt(expand("%")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/surface.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/surface.vim deleted file mode 100644 index 6fb2a69..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/surface.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile *.sface set filetype=surface diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/teal.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/teal.vim deleted file mode 100644 index ffc63f1..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/teal.vim +++ /dev/null @@ -1 +0,0 @@ -autocmd BufRead,BufNewFile *.tl setfiletype teal diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/tlaplus.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/tlaplus.vim deleted file mode 100644 index a60919b..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/tlaplus.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile *.tla set filetype=tla diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/yang.vim b/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/yang.vim deleted file mode 100644 index 295e7c3..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/ftdetect/yang.vim +++ /dev/null @@ -1 +0,0 @@ -au BufRead,BufNewFile *.yang set filetype=yang diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lockfile.json b/etc/soft/nvim/+plugins/nvim-treesitter/lockfile.json index f90339e..75f671d 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lockfile.json +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lockfile.json @@ -1,72 +1,99 @@ { + "agda": { + "revision": "80ea622cf952a0059e168e5c92a798b2f1925652" + }, + "arduino": { + "revision": "c473c09f23b13d9404ec5208fad3eddea89d70f8" + }, + "astro": { + "revision": "947e93089e60c66e681eba22283f4037841451e7" + }, + "awk": { + "revision": "e559793754c60c2cdf00cbb0409842d75f0a41dc" + }, "bash": { - "revision": "275effdfc0edce774acf7d481f9ea195c6c403cd" + "revision": "77cf8a7cab8904baf1a721762e012644ac1d4c7b" }, "beancount": { - "revision": "78b8ddca3ab774573a4e3bf64eabd79e9452cea9" + "revision": "4cbd1f09cd07c1f1fabf867c2cf354f9da53cc4c" }, "bibtex": { "revision": "ccfd77db0ed799b6c22c214fe9d2937f47bc8b34" }, + "blueprint": { + "revision": "6ef91ca8270f0112b9c6d27ecb9966c741a5d103" + }, "c": { - "revision": "e348e8ec5efd3aac020020e4af53d2ff18f393a9" + "revision": "7175a6dd5fc1cee660dce6fe23f6043d75af424a" }, "c_sharp": { - "revision": "352a4630c81a7a5cbd3bc67327743bd8d38f2dd2" + "revision": "3ef3f7f99e16e528e6689eae44dff35150993307" }, "clojure": { - "revision": "39bf0977d223879436c1425fe6bfeb3bcfd86f92" + "revision": "087bac78c53fe1387756cd5b8e68a69b3f6d7244" }, "cmake": { - "revision": "f6616f1e417ee8b62daf251aa1daa5d73781c596" + "revision": "6e51463ef3052dd3b328322c22172eda093727ad" }, "comment": { - "revision": "6975eb268f42df2afc313f96c0693e284685dba7" + "revision": "a37ca370310ac6f89b6e0ebf2b86b2219780494e" }, "commonlisp": { - "revision": "4fd115d3bb7046cd094f21bfe5766c302dbf64cd" + "revision": "c7e814975ab0d0d04333d1f32391c41180c58919" + }, + "cooklang": { + "revision": "5e113412aadb78955c27010daa4dbe1d202013cf" }, "cpp": { - "revision": "656d7ea44b2b0daece78791e30281e283f30001e" + "revision": "5ead1e26c6ab71919db0f1880c46a278a93bc5ea" }, "css": { - "revision": "a03f1d2d1dfbf6f8e0fdca5f9ff030228241eb57" + "revision": "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }, "cuda": { - "revision": "14cd86e18ba45e327017de5b3e0f8d8f7f8e98ec" + "revision": "7f1a79e612160aa02be87f1a24469ae3655fe818" }, "d": { "revision": "c2fbf21bd3aa45495fe13247e040ad5815250032" }, "dart": { - "revision": "6a25376685d1d47968c2cef06d4db8d84a70025e" + "revision": "53485a8f301254e19c518aa20c80f1bcf7cf5c62" }, "devicetree": { - "revision": "fa70098cd70393f84785f85cdc6a45299b59cd5b" + "revision": "ea30a05d0f0446a96d8b096ad11828ad4f8ad849" + }, + "diff": { + "revision": "330eb648bbc257b4e91621e82a85372be7dde27a" }, "dockerfile": { - "revision": "28ac8596bab00b2dac9a76deaa9c4cb2b22642fd" + "revision": "f913be9bb8689af22114605012693146fbe9ddaa" }, "dot": { - "revision": "92877bac7033e409ccfb3f07fe28ef1dfd359457" + "revision": "9ab85550c896d8b294d9b9ca1e30698736f08cea" }, "eex": { "revision": "f742f2fe327463335e8671a87c0b9b396905d1d1" }, "elixir": { - "revision": "de20391afe5cb03ef1e8a8e43167e7b58cc52869" + "revision": "b20eaa75565243c50be5e35e253d8beb58f45d56" }, "elm": { - "revision": "bd50ccf66b42c55252ac8efc1086af4ac6bab8cd" + "revision": "28bb193640d916dfaf947912c1413cebb0484841" + }, + "elvish": { + "revision": "f32711e31e987fd5c2c002f3daba02f25c68672f" + }, + "embedded_template": { + "revision": "91fc5ae1140d5c9d922312431f7d251a48d7b8ce" }, "erlang": { - "revision": "9d5fd0c329280a156bf7614a49dc5e8c58cc037c" + "revision": "3a9c769444f08bbccce03845270efac0c641c5e7" }, "fennel": { - "revision": "fce4331731a960077ff5f98939bc675179f1908a" + "revision": "517195970428aacca60891b050aa53eabf4ba78d" }, "fish": { - "revision": "04e54ab6585dfd4fee6ddfe5849af56f101b6d4f" + "revision": "84436cf24c2b3176bfbb220922a0fdbd0141e406" }, "foam": { "revision": "fdb7f14b885abfc4df57728c9b2a2f2ad24d3cb7" @@ -80,62 +107,83 @@ "gdscript": { "revision": "2a6abdaa47fcb91397e09a97c7433fd995ea46c6" }, + "git_rebase": { + "revision": "127f5b56c1ad3e8a449a7d6e0c7412ead7f7724c" + }, + "gitattributes": { + "revision": "577a075d46ea109905c5cb6179809df88da61ce9" + }, + "gitignore": { + "revision": "f4685bf11ac466dd278449bcfe5fd014e94aa504" + }, + "gleam": { + "revision": "cfcbca3f8f734773878e00d7bfcedea98eb10be2" + }, "glimmer": { - "revision": "2644d7db571fe36204fdfcf8eed7bfa97f32c25a" + "revision": "abcc9970da0ed0645741bf52ea70232374bc9e52" }, "glsl": { - "revision": "ffb93961426926554a0ba4a389ea6e9d6fafdea9" + "revision": "a743ada24fa17da9acc5665133f07d56e03530be" }, "go": { - "revision": "0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c75" + "revision": "05900faa3cdb5d2d8c8bd5e77ee698487e0a8611" }, "godot_resource": { "revision": "b6ef0768711086a86b3297056f9ffb5cc1d77b4a" }, "gomod": { - "revision": "3cbcb572109ea0bc476a292208722c326c9e6c3a" + "revision": "4a65743dbc2bb3094114dd2b43da03c820aa5234" }, "gowork": { - "revision": "6dd9dd79fb51e9f2abc829d5e97b15015b6a8ae2" + "revision": "949a8a470559543857a62102c84700d291fc984c" }, "graphql": { "revision": "5e66e961eee421786bdda8495ed1db045e06b5fe" }, "hack": { - "revision": "4770eb21a36307c156cfd2555ddd8e10c304fdc3" + "revision": "b7bd6928532ada34dddb1dece4a158ab62c6e783" }, "haskell": { - "revision": "d6ccd2d9c40bdec29fee0027ef04fe5ff1ae4ceb" + "revision": "aee3725d02cf3bca5f307b35dd3a96a97e109b4e" }, "hcl": { - "revision": "3cb7fc28247efbcb2973b97e71c78838ad98a583" + "revision": "45ce22c16ec924e34517cf785e23c07952e45893" }, "heex": { - "revision": "d8b5b9f016cd3c7b0ee916cf031d9a2188c0fc44" + "revision": "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" + }, + "help": { + "revision": "49cdef52ded4a886bf34bc474876b09f9270d48f" }, "hjson": { "revision": "02fa3b79b3ff9a296066da6277adfc3f26cbc9e0" }, + "hlsl": { + "revision": "384b26ec65e10f26cf147bfcde772c50ca5ef0d0" + }, "hocon": { - "revision": "5b4688cc57c773e69fe7dfc3f6b83c054557f4f1" + "revision": "c390f10519ae69fdb03b3e5764f5592fb6924bcc" }, "html": { - "revision": "161a92474a7bb2e9e830e48e76426f38299d99d1" + "revision": "29f53d8f4f2335e61bf6418ab8958dac3282077a" }, "http": { - "revision": "bfddd16b1cf78e0042fd1f6846a179f76a254e20" + "revision": "30a9c1789d64429a830802cde5b1760ff1064312" }, "java": { - "revision": "a24ae7d16de3517bff243a87d087d0b4877a65c5" + "revision": "09d650def6cdf7f479f4b78f595e9ef5b58ce31e" }, "javascript": { - "revision": "fdeb68ac8d2bd5a78b943528bb68ceda3aade2eb" + "revision": "936d976a782e75395d9b1c8c7c7bf4ba6fe0d86b" + }, + "jq": { + "revision": "13990f530e8e6709b7978503da9bc8701d366791" }, "jsdoc": { "revision": "189a6a4829beb9cdbe837260653b4a3dfb0cc3db" }, "json": { - "revision": "203e239408d642be83edde8988d6e7b20a19f0e8" + "revision": "73076754005a460947cafe8e03a8cf5fa4fa2938" }, "json5": { "revision": "5dd5cdc418d9659682556b6adca2dd9ace0ac6d2" @@ -143,144 +191,222 @@ "jsonc": { "revision": "02b01653c8a1c198ae7287d566efa86a135b30d5" }, + "jsonnet": { + "revision": "0475a5017ad7dc84845d1d33187f2321abcb261d" + }, "julia": { - "revision": "12ea597262125fc22fd2e91aa953ac69b19c26ca" + "revision": "628713553c42f30595a3b0085bb587e9359b986a" }, "kotlin": { - "revision": "a4f71eb9b8c9b19ded3e0e9470be4b1b77c2b569" + "revision": "b953dbdd05257fcb2b64bc4d9c1578fac12e3c28" + }, + "lalrpop": { + "revision": "7744b56f03ac1e5643fad23c9dd90837fe97291e" }, "latex": { - "revision": "6f796b700c69a8af28132e84ed6d0c8f0c17a5e2" + "revision": "8c75e93cd08ccb7ce1ccab22c1fbd6360e3bcea6" }, "ledger": { - "revision": "0cdeb0e51411a3ba5493662952c3039de08939ca" + "revision": "47b8971448ce5e9abac865f450c1b14fb3b6eee9" }, "llvm": { - "revision": "3b213925b9c4f42c1acfe2e10bfbb438d9c6834d" + "revision": "e9948edc41e9e5869af99dddb2b5ff5cc5581af6" }, "lua": { - "revision": "2e372ad0af8c6a68ba39f107a2edc9e3fc19ecf1" + "revision": "fb30e8cb605e2ebd6c643e6981325a63fbbde320" + }, + "m68k": { + "revision": "d097b123f19c6eaba2bf181c05420d88b9fc489d" }, "make": { "revision": "a4b9187417d6be349ee5fd4b6e77b4172c6827dd" }, "markdown": { - "revision": "8bee14c30ecadd55c2d65633973b4e81f93525e0" + "revision": "272e080bca0efd19a06a7f4252d746417224959e" + }, + "markdown_inline": { + "revision": "272e080bca0efd19a06a7f4252d746417224959e" + }, + "menhir": { + "revision": "db7953acb0d5551f207373c81fa07a57d7b085cb" + }, + "mermaid": { + "revision": "d787c66276e7e95899230539f556e8b83ee16f6d" + }, + "meson": { + "revision": "153d22588fb5c1eee16a165a084f9ea30f29d941" + }, + "nickel": { + "revision": "9d83db400b6c11260b9106f131f93ddda8131933" }, "ninja": { "revision": "0a95cfdc0745b6ae82f60d3a339b37f19b7b9267" }, "nix": { - "revision": "6d6aaa50793b8265b6a8b6628577a0083d3b923d" + "revision": "6b71a810c0acd49b980c50fc79092561f7cee307" }, "norg": { - "revision": "c4be6addec0a8ada234684ced6c928189fd399af" + "revision": "8ad20059c6f128861c4506fff866150ffee1d6f4" }, "ocaml": { - "revision": "23d419ba45789c5a47d31448061557716b02750a" + "revision": "cc26b1ef111100f26a137bcbcd39fd4e35be9a59" }, "ocaml_interface": { - "revision": "23d419ba45789c5a47d31448061557716b02750a" + "revision": "cc26b1ef111100f26a137bcbcd39fd4e35be9a59" }, "ocamllex": { "revision": "ac1d5957e719d49bd6acd27439b79843e4daf8ed" }, + "org": { + "revision": "081179c52b3e8175af62b9b91dc099d010c38770" + }, "pascal": { "revision": "2fd40f477d3e2794af152618ccfac8d92eb72a66" }, "perl": { - "revision": "ab2b39439f2fc82fd5ea0b7e08509760d4cbacd5" + "revision": "749d26fe13fb131b92e6515416096e572575b981" }, "php": { - "revision": "57f855461aeeca73bd4218754fb26b5ac143f98f" + "revision": "ab2e72179ceb8bb0b249c8ac9162a148e911b3dc" }, "phpdoc": { - "revision": "52c0fbf581d0fc2d29696dbbd4fca99a73082210" + "revision": "2f4d16c861b5a454b577d057f247f9902d7b47f5" }, "pioasm": { "revision": "924aadaf5dea2a6074d72027b064f939acf32e20" }, "prisma": { - "revision": "74a721e8eed1a4a25cf495d45974ba24f315f81a" + "revision": "17a59236ac25413b81b1613ea6ba5d8d52d7cd6c" + }, + "proto": { + "revision": "42d82fa18f8afe59b5fc0b16c207ee4f84cb185f" }, "pug": { - "revision": "5875f9a7d94836708119b0a1102bb5792e8bf673" + "revision": "63e214905970e75f065688b1e8aa90823c3aacdc" }, "python": { - "revision": "24b530ca158d2782ea9046e756057a412e16b52f" + "revision": "b14614e2144b8f9ee54deed5a24f3c6f51f9ffa8" }, "ql": { - "revision": "8e7fd7e638d4a0ec7a792ee16b19dbc6407aa810" + "revision": "bd087020f0d8c183080ca615d38de0ec827aeeaf" + }, + "qmljs": { + "revision": "0b2b25bcaa7d4925d5f0dda16f6a99c588a437f1" }, "query": { - "revision": "5217c6805c09f8fc00ed13d17d5fcb791437aee6" + "revision": "0695cd0760532de7b54f23c667d459b5d1332b44" }, "r": { - "revision": "d9868735e401e4870a3d4422790b585fea3faec8" + "revision": "80efda55672d1293aa738f956c7ae384ecdc31b4" + }, + "racket": { + "revision": "09cb27a06415bce529a26774a842f5a80d50d362" }, "rasi": { - "revision": "e2961f02244c068a67549adf896b0779e4a29516" + "revision": "12391343979463a2484e6353e5afb6dcb8c31e8b" }, "regex": { "revision": "e1cfca3c79896ff79842f057ea13e529b66af636" }, + "rego": { + "revision": "b2667c975f07b33be3ceb83bea5cfbad88095866" + }, + "rnoweb": { + "revision": "502c1126dc6777f09af5bef16e72a42f75bd081e" + }, "rst": { - "revision": "b74770c0166f28c1a0ab293513a78712ca1c338b" + "revision": "25e6328872ac3a764ba8b926aea12719741103f1" }, "ruby": { - "revision": "888e2e563ed3b43c417f17e57f7e29c39ce9aeea" + "revision": "c91960320d0f337bdd48308a8ad5500bd2616979" }, "rust": { - "revision": "eeb0702ebdac504b97196577b1dac43c80913d7b" + "revision": "0431a2c60828731f27491ee9fdefe25e250ce9c9" }, "scala": { - "revision": "0a3dd53a7fc4b352a538397d054380aaa28be54c" + "revision": "140c96cf398693189d4e50f76d19ddfcd8a018f8" + }, + "scheme": { + "revision": "bdcd2c8496701153506a9e3e1b76dfed852873ba" }, "scss": { - "revision": "f3174d3d131eb776f86dfa3d90fe6f7325c0ad9a" + "revision": "c478c6868648eff49eb04a4df90d703dc45b312a" + }, + "slint": { + "revision": "d422300f5d6ccce8f9a617dfed57aafb636fadb2" + }, + "solidity": { + "revision": "52ed0880c0126df2f2c7693f215fe6f38e4a2e0a" }, "sparql": { "revision": "05f949d3c1c15e3261473a244d3ce87777374dec" }, + "sql": { + "revision": "4f1b91246b43190e34957d9de9a0f3625879ba33" + }, "supercollider": { - "revision": "a7201b61779be59ac0fc0d118746c886dbc3edbd" + "revision": "90c6d9f777d2b8c4ce497c48b5f270a44bcf3ea0" }, "surface": { "revision": "f4586b35ac8548667a9aaa4eae44456c1f43d032" }, "svelte": { - "revision": "98274d94ec33e994e8354d9ddfdef58cca471294" + "revision": "52e122ae68b316d3aa960a0a422d3645ba717f42" }, "swift": { - "revision": "9ee2d8c29600425b64e7b173b2224033a0f99d0c" + "revision": "25f8de356e3c33099ed691bd3b8b5c0fe3a11e15" + }, + "sxhkdrc": { + "revision": "440d5f913d9465c9c776a1bd92334d32febcf065" }, "teal": { - "revision": "fcc5f6f4d194dede4e676834ff28a506e39e17b4" + "revision": "1ae8c68e90523b26b93af56feb7868fe4214e2b2" + }, + "tiger": { + "revision": "eb1d3714998977ae76ca7c6a102b10ee37efc2b5" }, "tlaplus": { - "revision": "f99f369f4b907108ac35cc49a56b7c8602f8332d" + "revision": "deaf0e5c573ad4e2bbfc9a29abb7b6dcb572556e" + }, + "todotxt": { + "revision": "0207f6a4ab6aeafc4b091914d31d8235049a2578" }, "toml": { "revision": "8bd2056818b21860e3d756b5a58c4f6e05fb744e" }, "tsx": { - "revision": "e8e8e8dc2745840b036421b4e43286750443cb13" + "revision": "0ab9d99867435a7667c5548a6617a6bf73dbd830" }, "turtle": { "revision": "085437f5cb117703b7f520dd92161140a684f092" }, + "twig": { + "revision": "035f549ec8c043e734f04341d7ccdc669bb2ba91" + }, "typescript": { - "revision": "e8e8e8dc2745840b036421b4e43286750443cb13" + "revision": "0ab9d99867435a7667c5548a6617a6bf73dbd830" + }, + "v": { + "revision": "66b92a89ef1e149300df79c0b2a934ad959c8eec" + }, + "vala": { + "revision": "8f690bfa639f2b83d1fb938ed3dd98a7ba453e8b" }, "verilog": { - "revision": "8f6b1f357d1231c420404b5f7a368a73c25adfa2" + "revision": "4457145e795b363f072463e697dfe2f6973c9a52" + }, + "vhs": { + "revision": "2f87b9d973597e69552ecf6a4fe16470fbd8c44e" }, "vim": { - "revision": "bc573ef552adf8bed9e36eb687a0cccf43158634" + "revision": "4ae7bd67706d7e10afed827ce2ded884ab41650f" }, "vue": { "revision": "91fe2754796cd8fba5f229505a23fa08f3546c06" }, + "wgsl": { + "revision": "af16e7d9e230004888fb52d33599ad38b4cf6052" + }, "yaml": { "revision": "0e36bed171768908f331ff7dff9d956bae016efb" }, @@ -288,6 +414,6 @@ "revision": "8e9d175982afcefa3dac8ca20d40d1643accd2bd" }, "zig": { - "revision": "93331b8bd8b4ebee2b575490b2758f16ad4e9f30" + "revision": "d90d38d28ce8cc27bfea8b4e0c75211e9e2398ca" } } diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter.lua index af46bd0..963fe73 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter.lua @@ -1,13 +1,8 @@ -if not pcall(require, "vim.treesitter.languagetree") then - error "nvim-treesitter requires a more recent Neovim nightly version!" -end - local install = require "nvim-treesitter.install" local utils = require "nvim-treesitter.utils" -local ts_utils = require "nvim-treesitter.ts_utils" local info = require "nvim-treesitter.info" local configs = require "nvim-treesitter.configs" -local parsers = require "nvim-treesitter.parsers" +local statusline = require "nvim-treesitter.statusline" -- Registers all query predicates require "nvim-treesitter.query_predicates" @@ -21,68 +16,7 @@ function M.setup() configs.init() end -function M.define_modules(...) - configs.define_modules(...) -end - -local get_line_for_node = function(node, type_patterns, transform_fn) - local node_type = node:type() - local is_valid = false - for _, rgx in ipairs(type_patterns) do - if node_type:find(rgx) then - is_valid = true - break - end - end - if not is_valid then - return "" - end - local line = transform_fn(vim.trim(ts_utils.get_node_text(node)[1] or "")) - -- Escape % to avoid statusline to evaluate content as expression - return line:gsub("%%", "%%%%") -end - --- Trim spaces and opening brackets from end -local transform_line = function(line) - return line:gsub("%s*[%[%(%{]*%s*$", "") -end - -function M.statusline(opts) - if not parsers.has_parser() then - return - end - local options = opts or {} - if type(opts) == "number" then - options = { indicator_size = opts } - end - local indicator_size = options.indicator_size or 100 - local type_patterns = options.type_patterns or { "class", "function", "method" } - local transform_fn = options.transform_fn or transform_line - local separator = options.separator or " -> " - - local current_node = ts_utils.get_node_at_cursor() - if not current_node then - return "" - end - - local lines = {} - local expr = current_node - - while expr do - local line = get_line_for_node(expr, type_patterns, transform_fn) - if line ~= "" and not vim.tbl_contains(lines, line) then - table.insert(lines, 1, line) - end - expr = expr:parent() - end - - local text = table.concat(lines, separator) - local text_len = #text - if text_len > indicator_size then - return "..." .. text:sub(text_len - indicator_size, text_len) - end - - return text -end +M.define_modules = configs.define_modules +M.statusline = statusline.statusline return M diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/configs.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/configs.lua index 5fddd16..6db2935 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/configs.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/configs.lua @@ -8,22 +8,46 @@ local caching = require "nvim-treesitter.caching" local M = {} +---@class TSConfig +---@field modules {[string]:TSModule} +---@field sync_install boolean +---@field ensure_installed string[]|string +---@field ignore_install string[] +---@field auto_install boolean +---@field update_strategy string +---@field parser_install_dir string|nil + +---@type TSConfig local config = { modules = {}, sync_install = false, ensure_installed = {}, + auto_install = false, ignore_install = {}, update_strategy = "lockfile", + parser_install_dir = nil, } -- List of modules that need to be setup on initialization. local queued_modules_defs = {} -- Whether we've initialized the plugin yet. local is_initialized = false + +---@class TSModule +---@field module_path string +---@field enable boolean|string[]|function(string): boolean +---@field disable boolean|string[]|function(string): boolean +---@field is_supported function(string): boolean +---@field attach function(string) +---@field detach function(string) +---@field enabled_buffers table + +---@type {[string]: TSModule} local builtin_modules = { highlight = { module_path = "nvim-treesitter.highlight", - enable = false, + -- @deprecated: use `highlight.set_custom_captures` instead custom_captures = {}, + enable = false, is_supported = function(lang) return queries.has_highlights(lang) end, @@ -51,7 +75,9 @@ local builtin_modules = { local attached_buffers_by_module = caching.create_buffer_cache() --- Resolves a module by requiring the `module_path` or using the module definition. +---Resolves a module by requiring the `module_path` or using the module definition. +---@param mod_name string +---@return TSModule|nil local function resolve_module(mod_name) local config_mod = M.get_module(mod_name) @@ -66,94 +92,121 @@ local function resolve_module(mod_name) end end --- Enables and attaches the module to a buffer for lang. --- @param mod path to module --- @param bufnr buffer number, defaults to current buffer --- @param lang language, defaults to current language +---Enables and attaches the module to a buffer for lang. +---@param mod string path to module +---@param bufnr integer|nil buffer number, defaults to current buffer +---@param lang string|nil language, defaults to current language local function enable_module(mod, bufnr, lang) - local bufnr = bufnr or api.nvim_get_current_buf() - local lang = lang or parsers.get_buf_lang(bufnr) + local module = M.get_module(mod) + if not module then + return + end + + bufnr = bufnr or api.nvim_get_current_buf() + lang = lang or parsers.get_buf_lang(bufnr) + + if not module.enable then + if module.enabled_buffers then + module.enabled_buffers[bufnr] = true + else + module.enabled_buffers = { [bufnr] = true } + end + end + M.attach_module(mod, bufnr, lang) end --- Enables autocomands for the module. --- After the module is loaded `loaded` will be set to true for the module. --- @param mod path to module +---Enables autocomands for the module. +---After the module is loaded `loaded` will be set to true for the module. +---@param mod string path to module local function enable_mod_conf_autocmd(mod) local config_mod = M.get_module(mod) if not config_mod or config_mod.loaded then return end - local cmd = string.format("lua require'nvim-treesitter.configs'.reattach_module('%s')", mod) - api.nvim_command(string.format("autocmd NvimTreesitter FileType * %s", cmd)) + api.nvim_create_autocmd("FileType", { + group = api.nvim_create_augroup("NvimTreesitter-" .. mod, {}), + callback = function() + require("nvim-treesitter.configs").reattach_module(mod) + end, + desc = "Reattach module", + }) config_mod.loaded = true end --- Enables the module globally and for all current buffers. --- After enabled, `enable` will be set to true for the module. --- @param mod path to module +---Enables the module globally and for all current buffers. +---After enabled, `enable` will be set to true for the module. +---@param mod string path to module local function enable_all(mod) local config_mod = M.get_module(mod) if not config_mod then return end + enable_mod_conf_autocmd(mod) + config_mod.enable = true + config_mod.enabled_buffers = nil + for _, bufnr in pairs(api.nvim_list_bufs()) do enable_module(mod, bufnr) end - - enable_mod_conf_autocmd(mod) - config_mod.enable = true end --- Disables and detaches the module for a buffer. --- @param mod path to module --- @param bufnr buffer number, defaults to current buffer +---Disables and detaches the module for a buffer. +---@param mod string path to module +---@param bufnr integer buffer number, defaults to current buffer local function disable_module(mod, bufnr) - local bufnr = bufnr or api.nvim_get_current_buf() + local module = M.get_module(mod) + if not module then + return + end + + bufnr = bufnr or api.nvim_get_current_buf() + if module.enabled_buffers then + module.enabled_buffers[bufnr] = false + end M.detach_module(mod, bufnr) end --- Disables autocomands for the module. --- After the module is unloaded `loaded` will be set to false for the module. --- @param mod path to module +---Disables autocomands for the module. +---After the module is unloaded `loaded` will be set to false for the module. +---@param mod string path to module local function disable_mod_conf_autocmd(mod) local config_mod = M.get_module(mod) if not config_mod or not config_mod.loaded then return end - -- TODO(kyazdani): detach the correct autocmd... doesn't work when using %s, cmd. - -- This will remove all autocomands! - api.nvim_command "autocmd! NvimTreesitter FileType *" + api.nvim_clear_autocmds { event = "FileType", group = "NvimTreesitter-" .. mod } config_mod.loaded = false end --- Disables the module globally and for all current buffers. --- After disabled, `enable` will be set to false for the module. --- @param mod path to module +---Disables the module globally and for all current buffers. +---After disabled, `enable` will be set to false for the module. +---@param mod string path to module local function disable_all(mod) local config_mod = M.get_module(mod) - if not config_mod or not config_mod.enable then + if not config_mod then return end + config_mod.enabled_buffers = nil + disable_mod_conf_autocmd(mod) + config_mod.enable = false + for _, bufnr in pairs(api.nvim_list_bufs()) do disable_module(mod, bufnr) end - - disable_mod_conf_autocmd(mod) - config_mod.enable = false end --- Toggles a module for a buffer --- @param mod path to module --- @param bufnr buffer number, defaults to current buffer --- @param lang language, defaults to current language +---Toggles a module for a buffer +---@param mod string path to module +---@param bufnr integer buffer number, defaults to current buffer +---@param lang string language, defaults to current language local function toggle_module(mod, bufnr, lang) - local bufnr = bufnr or api.nvim_get_current_buf() - local lang = lang or parsers.get_buf_lang(bufnr) + bufnr = bufnr or api.nvim_get_current_buf() + lang = lang or parsers.get_buf_lang(bufnr) if attached_buffers_by_module.has(mod, bufnr) then disable_module(mod, bufnr) @@ -177,12 +230,12 @@ local function toggle_all(mod) end end --- Recurses through all modules including submodules --- @param accumulator function called for each module --- @param root root configuration table to start at --- @param path prefix path +---Recurses through all modules including submodules +---@param accumulator function called for each module +---@param root {[string]: TSModule} root configuration table to start at +---@param path string|nil prefix path local function recurse_modules(accumulator, root, path) - local root = root or config.modules + root = root or config.modules for name, module in pairs(root) do local new_path = path and (path .. "." .. name) or name @@ -195,9 +248,9 @@ local function recurse_modules(accumulator, root, path) end end --- Shows current configuration of all nvim-treesitter modules --- @param process_function function used as the `process` parameter --- for vim.inspect (https://github.com/kikito/inspect.lua#optionsprocess) +---Shows current configuration of all nvim-treesitter modules +---@param process_function function used as the `process` parameter +--- for vim.inspect (https://github.com/kikito/inspect.lua#optionsprocess) local function config_info(process_function) process_function = process_function or function(item, path) @@ -212,29 +265,8 @@ local function config_info(process_function) print(vim.inspect(config, { process = process_function })) end -if not vim.ui then - vim.ui = { - select = function(items, opts, on_choice) - vim.validate { - items = { items, "table", false }, - on_choice = { on_choice, "function", false }, - } - opts = opts or {} - local choices = { opts.prompt or "Select one of:" } - local format_item = opts.format_item or tostring - for i, item in pairs(items) do - table.insert(choices, string.format("%d: %s", i, format_item(item))) - end - local choice = vim.fn.inputlist(choices) - if choice < 1 or choice > #items then - on_choice(nil, nil) - else - on_choice(items[choice], choice) - end - end, - } -end - +---@param query_group string +---@param lang string function M.edit_query_file(query_group, lang) lang = lang or parsers.get_buf_lang() local files = ts_query.get_query_files(lang, query_group, true) @@ -252,6 +284,8 @@ function M.edit_query_file(query_group, lang) end end +---@param query_group string +---@param lang string function M.edit_query_file_user_after(query_group, lang) lang = lang or parsers.get_buf_lang() local folder = utils.join_path(vim.fn.stdpath "config", "after", "queries", lang) @@ -290,21 +324,21 @@ M.commands = { "-complete=custom,nvim_treesitter#available_modules", }, }, - TSEnableAll = { + TSEnable = { run = enable_all, args = { "-nargs=+", "-complete=custom,nvim_treesitter#available_modules", }, }, - TSDisableAll = { + TSDisable = { run = disable_all, args = { "-nargs=+", "-complete=custom,nvim_treesitter#available_modules", }, }, - TSToggleAll = { + TSToggle = { run = toggle_all, args = { "-nargs=+", @@ -333,11 +367,11 @@ M.commands = { }, } --- @param mod: module (string) --- @param lang: the language of the buffer (string) --- @param bufnr: the bufnr (number) +---@param mod string module +---@param lang string the language of the buffer +---@param bufnr integer the bufnr function M.is_enabled(mod, lang, bufnr) - if not parsers.list[lang] or not parsers.has_parser(lang) then + if not parsers.has_parser(lang) then return false end @@ -346,7 +380,9 @@ function M.is_enabled(mod, lang, bufnr) return false end - if not module_config.enable or not module_config.is_supported(lang) then + local buffer_enabled = module_config.enabled_buffers and module_config.enabled_buffers[bufnr] + local config_enabled = module_config.enable or buffer_enabled + if not config_enabled or not module_config.is_supported(lang) then return false end @@ -367,11 +403,20 @@ function M.is_enabled(mod, lang, bufnr) return true end --- Setup call for users to override module configurations. --- @param user_data module overrides +---Setup call for users to override module configurations. +---@param user_data TSConfig module overrides function M.setup(user_data) config.modules = vim.tbl_deep_extend("force", config.modules, user_data) config.ignore_install = user_data.ignore_install or {} + config.parser_install_dir = user_data.parser_install_dir or nil + if config.parser_install_dir then + config.parser_install_dir = vim.fn.expand(config.parser_install_dir, ":p") + end + + config.auto_install = user_data.auto_install or false + if config.auto_install then + require("nvim-treesitter.install").setup_auto_install() + end local ensure_installed = user_data.ensure_installed or {} if #ensure_installed > 0 then @@ -383,6 +428,7 @@ function M.setup(user_data) end config.modules.ensure_installed = nil + config.ensure_installed = ensure_installed recurse_modules(function(_, _, new_path) local data = utils.get_at_path(config.modules, new_path) @@ -392,32 +438,33 @@ function M.setup(user_data) end, config.modules) end --- Defines a table of modules that can be attached/detached to buffers --- based on language support. A module consist of the following properties: --- * @enable Whether the modules is enabled. Can be true or false. --- * @disable A list of languages to disable the module for. Only relevant if enable is true. --- * @keymaps A list of user mappings for a given module if relevant. --- * @is_supported A function which, given a ft, will return true if the ft works on the module. --- * @module_path A string path to a module file using `require`. The exported module must contain --- an `attach` and `detach` function. This path is not required if `attach` and `detach` --- functions are provided directly on the module definition. --- * @attach An attach function that is called for each buffer that the module is enabled for. This is required --- if a `module_path` is not specified. --- * @detach A detach function that is called for each buffer that the module is enabled for. This is required --- if a `module_path` is not specified. --- Modules are not setup until `init` is invoked by the plugin. This allows modules to be defined in any order --- and can be loaded lazily. --- @example --- require"nvim-treesitter".define_modules { --- my_cool_module = { --- attach = function() --- do_some_cool_setup() --- end, --- detach = function() --- do_some_cool_teardown() --- end --- } --- } +---Defines a table of modules that can be attached/detached to buffers +---based on language support. A module consist of the following properties: +---* @enable Whether the modules is enabled. Can be true or false. +---* @disable A list of languages to disable the module for. Only relevant if enable is true. +---* @keymaps A list of user mappings for a given module if relevant. +---* @is_supported A function which, given a ft, will return true if the ft works on the module. +---* @module_path A string path to a module file using `require`. The exported module must contain +--- an `attach` and `detach` function. This path is not required if `attach` and `detach` +--- functions are provided directly on the module definition. +---* @attach An attach function that is called for each buffer that the module is enabled for. This is required +--- if a `module_path` is not specified. +---* @detach A detach function that is called for each buffer that the module is enabled for. This is required +--- if a `module_path` is not specified. +---Modules are not setup until `init` is invoked by the plugin. This allows modules to be defined in any order +---and can be loaded lazily. +---@example +---require"nvim-treesitter".define_modules { +--- my_cool_module = { +--- attach = function() +--- do_some_cool_setup() +--- end, +--- detach = function() +--- do_some_cool_teardown() +--- end +--- } +---} +---@param mod_defs TSModule[] function M.define_modules(mod_defs) if not is_initialized then table.insert(queued_modules_defs, mod_defs) @@ -444,13 +491,13 @@ function M.define_modules(mod_defs) end end --- Attaches a module to a buffer --- @param mod_name the module name --- @param bufnr the bufnr --- @param lang the language of the buffer +---Attaches a module to a buffer +---@param mod_name string the module name +---@param bufnr integer the bufnr +---@param lang string the language of the buffer function M.attach_module(mod_name, bufnr, lang) - local bufnr = bufnr or api.nvim_get_current_buf() - local lang = lang or parsers.get_buf_lang(bufnr) + bufnr = bufnr or api.nvim_get_current_buf() + lang = lang or parsers.get_buf_lang(bufnr) local resolved_mod = resolve_module(mod_name) if resolved_mod and not attached_buffers_by_module.has(mod_name, bufnr) and M.is_enabled(mod_name, lang, bufnr) then @@ -459,12 +506,12 @@ function M.attach_module(mod_name, bufnr, lang) end end --- Detaches a module to a buffer --- @param mod_name the module name --- @param bufnr the bufnr +---Detaches a module to a buffer +---@param mod_name string the module name +---@param bufnr integer the bufnr function M.detach_module(mod_name, bufnr) local resolved_mod = resolve_module(mod_name) - local bufnr = bufnr or api.nvim_get_current_buf() + bufnr = bufnr or api.nvim_get_current_buf() if resolved_mod and attached_buffers_by_module.has(mod_name, bufnr) then attached_buffers_by_module.remove(mod_name, bufnr) @@ -472,17 +519,17 @@ function M.detach_module(mod_name, bufnr) end end --- Same as attach_module, but if the module is already attached, detach it first. --- @param mod_name the module name --- @param bufnr the bufnr --- @param lang the language of the buffer +---Same as attach_module, but if the module is already attached, detach it first. +---@param mod_name string the module name +---@param bufnr integer the bufnr +---@param lang string the language of the buffer function M.reattach_module(mod_name, bufnr, lang) M.detach_module(mod_name, bufnr) M.attach_module(mod_name, bufnr, lang) end --- Gets available modules --- @param root root table to find modules +---Gets available modules +---@param root {[string]:TSModule} table to find modules function M.available_modules(root) local modules = {} @@ -493,25 +540,26 @@ function M.available_modules(root) return modules end --- Gets a module config by path --- @param mod_path path to the module --- @returns the module or nil +---Gets a module config by path +---@param mod_path string path to the module +---@return TSModule|nil the module or nil function M.get_module(mod_path) local mod = utils.get_at_path(config.modules, mod_path) return M.is_module(mod) and mod or nil end --- Determines whether the provided table is a module. --- A module should contain an attach and detach function. --- @param mod the module table +---Determines whether the provided table is a module. +---A module should contain an attach and detach function. +---@param mod table the module table +---@return boolean function M.is_module(mod) return type(mod) == "table" and ((type(mod.attach) == "function" and type(mod.detach) == "function") or type(mod.module_path) == "string") end --- Initializes built-in modules and any queued modules --- registered by plugins or the user. +---Initializes built-in modules and any queued modules +---registered by plugins or the user. function M.init() is_initialized = true M.define_modules(builtin_modules) @@ -521,6 +569,39 @@ function M.init() end end +---If parser_install_dir is not nil is used or created. +---If parser_install_dir is nil try the package dir of the nvim-treesitter +---plugin first, followed by the "site" dir from "runtimepath". "site" dir will +---be created if it doesn't exist. Using only the package dir won't work when +---the plugin is installed with Nix, since the "/nix/store" is read-only. +---@param folder_name string +---@return string|nil, string|nil +function M.get_parser_install_dir(folder_name) + folder_name = folder_name or "parser" + + local install_dir + if config.parser_install_dir then + install_dir = config.parser_install_dir + else + install_dir = utils.get_package_path() + end + local parser_dir = utils.join_path(install_dir, folder_name) + + return utils.create_or_reuse_writable_dir( + parser_dir, + utils.join_space("Could not create parser dir '", parser_dir, "': "), + utils.join_space( + "Parser dir '", + parser_dir, + "' should be read/write (see README on how to configure an alternative install location)" + ) + ) +end + +function M.get_parser_info_dir() + return M.get_parser_install_dir "parser-info" +end + function M.get_update_strategy() return config.update_strategy end @@ -529,4 +610,8 @@ function M.get_ignored_parser_installs() return config.ignore_install or {} end +function M.get_ensure_installed_parsers() + return config.ensure_installed or {} +end + return M diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/fold.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/fold.lua index 01a7c08..5027262 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/fold.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/fold.lua @@ -39,8 +39,13 @@ local folds_levels = tsutils.memoize_by_buf_tick(function(bufnr) local min_fold_lines = api.nvim_win_get_option(0, "foldminlines") - for _, node in ipairs(matches) do - local start, _, stop, stop_col = node.node:range() + for _, match in ipairs(matches) do + local start, stop, stop_col + if match.metadata and match.metadata.range then + start, _, stop, stop_col = unpack(match.metadata.range) + else + start, _, stop, stop_col = match.node:range() + end if stop_col == 0 then stop = stop - 1 @@ -97,6 +102,8 @@ local folds_levels = tsutils.memoize_by_buf_tick(function(bufnr) return levels end) +---@param lnum integer +---@return string function M.get_fold_indic(lnum) if not parsers.has_parser() or not lnum then return "0" diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/health.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/health.lua index b26a9ef..e308889 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/health.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/health.lua @@ -7,25 +7,26 @@ local shell = require "nvim-treesitter.shell_command_selectors" local install = require "nvim-treesitter.install" local utils = require "nvim-treesitter.utils" -local health_start = vim.fn["health#report_start"] -local health_ok = vim.fn["health#report_ok"] -local health_error = vim.fn["health#report_error"] -local health_warn = vim.fn["health#report_warn"] +local health = vim.health or require "health" local M = {} local NVIM_TREESITTER_MINIMUM_ABI = 13 local function install_health() - health_start "Installation" + health.report_start "Installation" + + if fn.has "nvim-0.7" == 0 then + health.report_error "Nvim-treesitter requires Neovim 0.7.0+" + end if fn.executable "tree-sitter" == 0 then - health_warn( + health.report_warn( "`tree-sitter` executable not found (parser generator, only needed for :TSInstallFromGrammar," .. " not required for :TSInstall)" ) else - health_ok( + health.report_ok( "`tree-sitter` found " .. (utils.ts_cli_version() or "(unknown version)") .. " (parser generator, only needed for :TSInstallFromGrammar)" @@ -33,7 +34,7 @@ local function install_health() end if fn.executable "node" == 0 then - health_warn( + health.report_warn( "`node` executable not found (only needed for :TSInstallFromGrammar," .. " not required for :TSInstall)" ) else @@ -41,21 +42,21 @@ local function install_health() local result = handle:read "*a" handle:close() local version = vim.split(result, "\n")[1] - health_ok("`node` found " .. version .. " (only needed for :TSInstallFromGrammar)") + health.report_ok("`node` found " .. version .. " (only needed for :TSInstallFromGrammar)") end if fn.executable "git" == 0 then - health_error("`git` executable not found.", { + health.report_error("`git` executable not found.", { "Install it with your package manager.", "Check that your `$PATH` is set correctly.", }) else - health_ok "`git` executable found." + health.report_ok "`git` executable found." end local cc = shell.select_executable(install.compilers) if not cc then - health_error("`cc` executable not found.", { + health.report_error("`cc` executable not found.", { "Check that any of " .. vim.inspect(install.compilers) .. " is in your $PATH" @@ -63,7 +64,7 @@ local function install_health() }) else local version = vim.fn.systemlist(cc .. (cc == "cl" and "" or " --version"))[1] - health_ok( + health.report_ok( "`" .. cc .. "` executable found. Selected from " @@ -73,7 +74,7 @@ local function install_health() end if vim.treesitter.language_version then if vim.treesitter.language_version >= NVIM_TREESITTER_MINIMUM_ABI then - health_ok( + health.report_ok( "Neovim was compiled with tree-sitter runtime ABI version " .. vim.treesitter.language_version .. " (required >=" @@ -81,7 +82,7 @@ local function install_health() .. "). Parsers must be compatible with runtime ABI." ) else - health_error( + health.report_error( "Neovim was compiled with tree-sitter runtime ABI version " .. vim.treesitter.language_version .. ".\n" @@ -127,7 +128,7 @@ function M.check() table.insert(error_collection, { parser_name, query_group, err }) end end - table.insert(parser_installation, out) + table.insert(parser_installation, vim.fn.trim(out, " ", 2)) end end local legend = [[ @@ -137,12 +138,30 @@ function M.check() x) errors found in the query, try to run :TSUpdate {lang}]] table.insert(parser_installation, legend) -- Finally call the report function - health_start(table.concat(parser_installation, "\n")) + health.report_start(table.concat(parser_installation, "\n")) if #error_collection > 0 then - health_start "The following errors have been detected:" + health.report_start "The following errors have been detected:" for _, p in ipairs(error_collection) do local lang, type, err = unpack(p) - health_error(lang .. "(" .. type .. "): " .. err) + local lines = {} + table.insert(lines, lang .. "(" .. type .. "): " .. err) + local files = vim.treesitter.query.get_query_files(lang, type) + if #files > 0 then + table.insert(lines, lang .. "(" .. type .. ") is concatenated from the following files:") + for _, file in ipairs(files) do + local fd = io.open(file, "r") + if fd then + local ok, file_err = pcall(vim.treesitter.query.parse_query, lang, fd:read "*a") + if ok then + table.insert(lines, '| [OK]:"' .. file .. '"') + else + table.insert(lines, '| [ERROR]:"' .. file .. '", failed to load: ' .. file_err) + end + fd:close() + end + end + end + health.report_error(table.concat(lines, "\n")) end end end diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/highlight.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/highlight.lua index 734a17f..5f7dead 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/highlight.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/highlight.lua @@ -6,127 +6,49 @@ local configs = require "nvim-treesitter.configs" local M = {} -local hlmap = vim.treesitter.highlighter.hl_map - --- nvim-treesitter Highlight Group Mappings --- Note: Some highlight groups may not be applied upstream, some may be experimental - -hlmap["annotation"] = "TSAnnotation" - -hlmap["attribute"] = "TSAttribute" - -hlmap["boolean"] = "TSBoolean" - -hlmap["character"] = "TSCharacter" - -hlmap["comment"] = "TSComment" - -hlmap["conditional"] = "TSConditional" - -hlmap["constant"] = "TSConstant" -hlmap["constant.builtin"] = "TSConstBuiltin" -hlmap["constant.macro"] = "TSConstMacro" - -hlmap["constructor"] = "TSConstructor" - -hlmap["error"] = "TSError" -hlmap["exception"] = "TSException" - -hlmap["field"] = "TSField" - -hlmap["float"] = "TSFloat" - -hlmap["function"] = "TSFunction" -hlmap["function.builtin"] = "TSFuncBuiltin" -hlmap["function.macro"] = "TSFuncMacro" - -hlmap["include"] = "TSInclude" - -hlmap["keyword"] = "TSKeyword" -hlmap["keyword.function"] = "TSKeywordFunction" -hlmap["keyword.operator"] = "TSKeywordOperator" -hlmap["keyword.return"] = "TSKeywordReturn" - -hlmap["label"] = "TSLabel" - -hlmap["method"] = "TSMethod" - -hlmap["namespace"] = "TSNamespace" - -hlmap["none"] = "TSNone" -hlmap["number"] = "TSNumber" - -hlmap["operator"] = "TSOperator" - -hlmap["parameter"] = "TSParameter" -hlmap["parameter.reference"] = "TSParameterReference" - -hlmap["property"] = "TSProperty" - -hlmap["punctuation.delimiter"] = "TSPunctDelimiter" -hlmap["punctuation.bracket"] = "TSPunctBracket" -hlmap["punctuation.special"] = "TSPunctSpecial" - -hlmap["repeat"] = "TSRepeat" - -hlmap["string"] = "TSString" -hlmap["string.regex"] = "TSStringRegex" -hlmap["string.escape"] = "TSStringEscape" -hlmap["string.special"] = "TSStringSpecial" - -hlmap["symbol"] = "TSSymbol" - -hlmap["tag"] = "TSTag" -hlmap["tag.attribute"] = "TSTagAttribute" -hlmap["tag.delimiter"] = "TSTagDelimiter" - -hlmap["text"] = "TSText" -hlmap["text.strong"] = "TSStrong" -hlmap["text.emphasis"] = "TSEmphasis" -hlmap["text.underline"] = "TSUnderline" -hlmap["text.strike"] = "TSStrike" -hlmap["text.title"] = "TSTitle" -hlmap["text.literal"] = "TSLiteral" -hlmap["text.uri"] = "TSURI" -hlmap["text.math"] = "TSMath" -hlmap["text.reference"] = "TSTextReference" -hlmap["text.environment"] = "TSEnvironment" -hlmap["text.environment.name"] = "TSEnvironmentName" - -hlmap["text.note"] = "TSNote" -hlmap["text.warning"] = "TSWarning" -hlmap["text.danger"] = "TSDanger" - -hlmap["type"] = "TSType" -hlmap["type.builtin"] = "TSTypeBuiltin" - -hlmap["variable"] = "TSVariable" -hlmap["variable.builtin"] = "TSVariableBuiltin" +---@param config table +---@param lang string +---@return boolean +local function should_enable_vim_regex(config, lang) + local additional_hl = config.additional_vim_regex_highlighting + local is_table = type(additional_hl) == "table" + + return additional_hl and (not is_table or vim.tbl_contains(additional_hl, lang)) +end -function M.attach(bufnr, lang) - local parser = parsers.get_parser(bufnr, lang) - local config = configs.get_module "highlight" +---@param bufnr integer +local function enable_syntax(bufnr) + api.nvim_buf_set_option(bufnr, "syntax", "ON") +end - for k, v in pairs(config.custom_captures) do - hlmap[k] = v +---@param bufnr integer +function M.stop(bufnr) + if ts.highlighter.active[bufnr] then + ts.highlighter.active[bufnr]:destroy() end +end +---@param bufnr integer +---@param lang string +function M.start(bufnr, lang) + local parser = parsers.get_parser(bufnr, lang) ts.highlighter.new(parser, {}) +end - local is_table = type(config.additional_vim_regex_highlighting) == "table" - if - config.additional_vim_regex_highlighting - and (not is_table or vim.tbl_contains(config.additional_vim_regex_highlighting, lang)) - then - api.nvim_buf_set_option(bufnr, "syntax", "ON") +---@param bufnr integer +---@param lang string +function M.attach(bufnr, lang) + local config = configs.get_module "highlight" + M.start(bufnr, lang) + if config and should_enable_vim_regex(config, lang) then + enable_syntax(bufnr) end end +---@param bufnr integer function M.detach(bufnr) - if ts.highlighter.active[bufnr] then - ts.highlighter.active[bufnr]:destroy() - end - api.nvim_buf_set_option(bufnr, "syntax", "ON") + M.stop(bufnr) + enable_syntax(bufnr) end return M diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/incremental_selection.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/incremental_selection.lua index 857f4a4..5deaaf8 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/incremental_selection.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/incremental_selection.lua @@ -120,17 +120,33 @@ function M.node_decremental() ts_utils.update_selection(buf, node) end +local FUNCTION_DESCRIPTIONS = { + init_selection = "Start selecting nodes with nvim-treesitter", + node_incremental = "Increment selection to named node", + scope_incremental = "Increment selection to surrounding scope", + node_decremental = "Shrink selection to previous named node", +} + function M.attach(bufnr) local config = configs.get_module "incremental_selection" for funcname, mapping in pairs(config.keymaps) do local mode + local rhs if funcname == "init_selection" then mode = "n" + rhs = M[funcname] else mode = "x" + -- We need to move to command mode to access marks '< (visual area start) and '> (visual area end) which are not + -- properly accessible in visual mode. + rhs = string.format(":lua require'nvim-treesitter.incremental_selection'.%s()", funcname) end - local cmd = string.format(":lua require'nvim-treesitter.incremental_selection'.%s()", funcname) - api.nvim_buf_set_keymap(bufnr, mode, mapping, cmd, { silent = true, noremap = true }) + vim.keymap.set( + mode, + mapping, + rhs, + { buffer = bufnr, silent = true, noremap = true, desc = FUNCTION_DESCRIPTIONS[funcname] } + ) end end @@ -138,9 +154,9 @@ function M.detach(bufnr) local config = configs.get_module "incremental_selection" for f, mapping in pairs(config.keymaps) do if f == "init_selection" then - api.nvim_buf_del_keymap(bufnr, "n", mapping) + vim.keymap.del("n", mapping, { buffer = bufnr }) else - api.nvim_buf_del_keymap(bufnr, "x", mapping) + vim.keymap.del("x", mapping, { buffer = bufnr }) end end end diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/indent.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/indent.lua index eca649b..0eac0a9 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/indent.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/indent.lua @@ -2,6 +2,18 @@ local parsers = require "nvim-treesitter.parsers" local queries = require "nvim-treesitter.query" local tsutils = require "nvim-treesitter.ts_utils" +local M = {} + +M.avoid_force_reparsing = { + yaml = true, +} + +M.comment_parsers = { + comment = true, + jsdoc = true, + phpdoc = true, +} + local function get_first_node_at_line(root, lnum) local col = vim.fn.indent(lnum) return root:descendant_for_range(lnum - 1, col, lnum - 1, col) @@ -12,21 +24,17 @@ local function get_last_node_at_line(root, lnum) return root:descendant_for_range(lnum - 1, col, lnum - 1, col) end -local function get_matching_prev_sibling(anchor, start, matcher) - local start_row, start_col = start[1], start[2] - local node = anchor:descendant_for_range(start_row, start_col, start_row, start_col) - local pos = 1 - -- TODO: reconsider this 999 limit or do something differently in future. - -- if anchor has more than 999 children, this would not work. - while pos < 999 and node and not matcher(node) do - node = node:prev_sibling() - pos = pos + 1 +local function find_delimiter(bufnr, node, delimiter) + for child, _ in node:iter_children() do + if child:type() == delimiter then + local linenr = child:start() + local line = vim.api.nvim_buf_get_lines(bufnr, linenr, linenr + 1, false)[1] + local end_char = { child:end_() } + return child, #line == end_char[2] + end end - return node, pos end -local M = {} - local get_indents = tsutils.memoize_by_buf_tick(function(bufnr, root, lang) local map = { auto = {}, @@ -36,6 +44,7 @@ local get_indents = tsutils.memoize_by_buf_tick(function(bufnr, root, lang) branch = {}, ignore = {}, aligned_indent = {}, + zero_indent = {}, } for name, node, metadata in queries.iter_captures(bufnr, "indents", root, lang) do @@ -52,13 +61,34 @@ end, { ---@param lnum number (1-indexed) function M.get_indent(lnum) - local parser = parsers.get_parser() + local bufnr = vim.api.nvim_get_current_buf() + local parser = parsers.get_parser(bufnr) if not parser or not lnum then return -1 end - -- get_root_for_position is 0-based. - local root, _, lang_tree = tsutils.get_root_for_position(lnum - 1, 0, parser) + local root_lang = parsers.get_buf_lang(bufnr) + + -- some languages like Python will actually have worse results when re-parsing at opened new line + if not M.avoid_force_reparsing[root_lang] then + -- Reparse in case we got triggered by ":h indentkeys" + parser:parse() + end + + -- Get language tree with smallest range around node that's not a comment parser + local root, lang_tree + parser:for_each_tree(function(tstree, tree) + if not tstree or M.comment_parsers[tree:lang()] then + return + end + local local_root = tstree:root() + if tsutils.is_in_node_range(local_root, lnum - 1, 0) then + if not root or tsutils.node_length(root) >= tsutils.node_length(local_root) then + root = local_root + lang_tree = tree + end + end + end) -- Not likely, but just in case... if not root then @@ -80,7 +110,8 @@ function M.get_indent(lnum) local indent_size = vim.fn.shiftwidth() local indent = 0 - if root:start() ~= 0 then + local _, _, root_start = root:start() + if root_start ~= 0 then -- injected tree indent = vim.fn.indent(root:start() + 1) end @@ -88,6 +119,10 @@ function M.get_indent(lnum) -- tracks to ensure multiple indent levels are not applied for same line local is_processed_by_row = {} + if q.zero_indent[node:id()] then + return 0 + end + while node do -- do 'autoindent' if not marked as @indent if not q.indent[node:id()] and q.auto[node:id()] and node:start() < lnum - 1 and lnum - 1 <= node:end_() then @@ -114,31 +149,42 @@ function M.get_indent(lnum) end -- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) - if not is_processed_by_row[srow] and (q.indent[node:id()] and srow ~= erow and srow ~= lnum - 1) then + local should_process = not is_processed_by_row[srow] + local is_in_err = false + if should_process then + local parent = node:parent() + is_in_err = parent and parent:has_error() + end + if + should_process + and ( + q.indent[node:id()] + and (srow ~= erow or is_in_err) + and (srow ~= lnum - 1 or q.indent[node:id()].start_at_same_line) + ) + then indent = indent + indent_size is_processed = true end - if q.aligned_indent[node:id()] and srow ~= erow then + -- do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + if q.aligned_indent[node:id()] and srow ~= erow and (srow ~= lnum - 1) then local metadata = q.aligned_indent[node:id()] - local opening_delimiter = metadata.delimiter:sub(1, 1) - local o_delim_node, pos = get_matching_prev_sibling(node, { srow, #vim.fn.getline(srow + 1) - 1 }, function(n) - return n:type() == opening_delimiter - end) + local o_delim_node, is_last_in_line + if metadata.delimiter then + local opening_delimiter = metadata.delimiter and metadata.delimiter:sub(1, 1) + o_delim_node, is_last_in_line = find_delimiter(bufnr, node, opening_delimiter) + else + o_delim_node = node + end if o_delim_node then - if pos == 1 then + if is_last_in_line then -- hanging indent (previous line ended with starting delimiter) indent = indent + indent_size * 1 else local _, o_scol = o_delim_node:start() - local aligned_indent = math.max(indent, 0) + o_scol - if indent > 0 then - indent = aligned_indent - else - indent = aligned_indent + 1 -- extra space for starting delimiter - end - is_processed = true + return math.max(indent, 0) + o_scol + (metadata.increment or 1) end end end @@ -156,7 +202,6 @@ local indent_funcs = {} function M.attach(bufnr) indent_funcs[bufnr] = vim.bo.indentexpr vim.bo.indentexpr = "nvim_treesitter#indent()" - vim.api.nvim_command("au Filetype " .. vim.bo.filetype .. " setlocal indentexpr=nvim_treesitter#indent()") end function M.detach(bufnr) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/info.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/info.lua index ed5a90b..eaeb975 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/info.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/info.lua @@ -14,11 +14,13 @@ local function install_info() local parser_list = parsers.available_parsers() table.sort(parser_list) - for _, ft in pairs(parser_list) do - local is_installed = #api.nvim_get_runtime_file("parser/" .. ft .. ".so", false) > 0 - api.nvim_out_write(ft .. string.rep(" ", max_len - #ft + 1)) + for _, lang in pairs(parser_list) do + local is_installed = #api.nvim_get_runtime_file("parser/" .. lang .. ".so", false) > 0 + api.nvim_out_write(lang .. string.rep(" ", max_len - #lang + 1)) if is_installed then api.nvim_out_write "[✓] installed\n" + elseif pcall(vim.treesitter.inspect_lang, lang) then + api.nvim_out_write "[✗] not installed (but still loaded. Restart Neovim!)\n" else api.nvim_out_write "[✗] not installed\n" end @@ -115,21 +117,24 @@ local function print_info_modules(parserlist, module) api.nvim_buf_set_option(curbuf, "modified", false) api.nvim_buf_set_option(curbuf, "buftype", "nofile") - api.nvim_exec( - [[ - syntax match TSModuleInfoGood /✓/ - syntax match TSModuleInfoBad /✗/ - syntax match TSModuleInfoHeader /^>>.*$/ contains=TSModuleInfoNamespace - syntax match TSModuleInfoNamespace /^>> \w*/ contained - syntax match TSModuleInfoParser /^[^> ]*\ze / - highlight default TSModuleInfoGood guifg=LightGreen gui=bold - highlight default TSModuleInfoBad guifg=Crimson - highlight default link TSModuleInfoHeader Type - highlight default link TSModuleInfoNamespace Statement - highlight default link TSModuleInfoParser Identifier - ]], - false - ) + vim.cmd [[ + syntax match TSModuleInfoGood /✓/ + syntax match TSModuleInfoBad /✗/ + syntax match TSModuleInfoHeader /^>>.*$/ contains=TSModuleInfoNamespace + syntax match TSModuleInfoNamespace /^>> \w*/ contained + syntax match TSModuleInfoParser /^[^> ]*\ze / + ]] + + local highlights = { + TSModuleInfoGood = { fg = "LightGreen", bold = true, default = true }, + TSModuleInfoBad = { fg = "Crimson", default = true }, + TSModuleInfoHeader = { link = "Type", default = true }, + TSModuleInfoNamespace = { link = "Statement", default = true }, + TSModuleInfoParser = { link = "Identifier", default = true }, + } + for k, v in pairs(highlights) do + api.nvim_set_hl(0, k, v) + end end local function module_info(module) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/install.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/install.lua index b161439..48810b1 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/install.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/install.lua @@ -42,11 +42,14 @@ local function get_job_status() .. "]" end +---@param lang string +---@param validate boolean|nil +---@return InstallInfo local function get_parser_install_info(lang, validate) local parser_config = parsers.get_parser_configs()[lang] if not parser_config then - return error("Parser not available for language " .. lang) + error('Parser not available for language "' .. lang .. '"') end local install_info = parser_config.install_info @@ -79,30 +82,39 @@ local function get_revision(lang) return (lockfile[lang] and lockfile[lang].revision) end +---@param lang string +---@return string|nil local function get_installed_revision(lang) - local lang_file = utils.join_path(utils.get_parser_info_dir(), lang .. ".revision") + local lang_file = utils.join_path(configs.get_parser_info_dir(), lang .. ".revision") if vim.fn.filereadable(lang_file) == 1 then return vim.fn.readfile(lang_file)[1] end end +---@param lang string +---@return boolean local function is_installed(lang) return #api.nvim_get_runtime_file("parser/" .. lang .. ".so", false) > 0 end +---@param lang string +---@return boolean local function needs_update(lang) local revision = get_revision(lang) return not revision or revision ~= get_installed_revision(lang) end +---@return table local function outdated_parsers() return vim.tbl_filter(function(lang) return needs_update(lang) end, info.installed_parsers()) end +---@param handle userdata +---@param is_stderr boolean local function onread(handle, is_stderr) - return function(err, data) + return function(_, data) if data then if is_stderr then complete_error_output[handle] = (complete_error_output[handle] or "") .. data @@ -147,6 +159,7 @@ function M.iter_cmd(cmd_list, i, lang, success_message) local stdout = luv.new_pipe(false) local stderr = luv.new_pipe(false) attr.opts.stdio = { nil, stdout, stderr } + ---@type userdata handle = luv.spawn( attr.cmd, attr.opts, @@ -225,6 +238,12 @@ local function iter_cmd_sync(cmd_list) return true end +---@param cache_folder string +---@param install_folder string +---@param lang string +---@param repo InstallInfo +---@param with_sync boolean +---@param generate_from_grammar boolean local function run_install(cache_folder, install_folder, lang, repo, with_sync, generate_from_grammar) parsers.reset_cache() @@ -242,10 +261,14 @@ local function run_install(cache_folder, install_folder, lang, repo, with_sync, if from_local_path then compile_location = repo.url else - local repo_location = string.gsub(repo.location or project_name, "/", path_sep) - compile_location = cache_folder .. path_sep .. repo_location + local repo_location = project_name + if repo.location then + repo_location = repo_location .. "/" .. repo.location + end + repo_location = repo_location:gsub("/", path_sep) + compile_location = utils.join_path(cache_folder, repo_location) end - local parser_lib_name = install_folder .. path_sep .. lang .. ".so" + local parser_lib_name = utils.join_path(install_folder, lang) .. ".so" generate_from_grammar = repo.requires_generate_from_grammar or generate_from_grammar @@ -329,7 +352,7 @@ local function run_install(cache_folder, install_folder, lang, repo, with_sync, shell.select_mv_cmd("parser.so", parser_lib_name, compile_location), { cmd = function() - vim.fn.writefile({ revision or "" }, utils.join_path(utils.get_parser_info_dir(), lang .. ".revision")) + vim.fn.writefile({ revision or "" }, utils.join_path(configs.get_parser_info_dir(), lang .. ".revision")) end, }, { -- auto-attach modules after installation @@ -357,6 +380,12 @@ local function run_install(cache_folder, install_folder, lang, repo, with_sync, end end +---@param lang string +---@param ask_reinstall boolean +---@param cache_folder string +---@param install_folder string +---@param with_sync boolean +---@param generate_from_grammar boolean local function install_lang(lang, ask_reinstall, cache_folder, install_folder, with_sync, generate_from_grammar) if is_installed(lang) and ask_reinstall ~= "force" then if not ask_reinstall then @@ -370,11 +399,22 @@ local function install_lang(lang, ask_reinstall, cache_folder, install_folder, w end end - local install_info = get_parser_install_info(lang, true) + local ok, install_info = pcall(get_parser_install_info, lang, true) + if not ok then + vim.notify("Installation not possible: " .. install_info, vim.log.levels.ERROR) + if not parsers.get_parser_configs()[lang] then + vim.notify( + "See https://github.com/nvim-treesitter/nvim-treesitter/#adding-parsers on how to add a new parser!", + vim.log.levels.INFO + ) + end + return + end run_install(cache_folder, install_folder, lang, install_info, with_sync, generate_from_grammar) end +---@return function local function install(options) options = options or {} local with_sync = options.with_sync @@ -391,20 +431,20 @@ local function install(options) if err then return api.nvim_err_writeln(err) end + assert(cache_folder) - local install_folder, err = utils.get_parser_install_dir() + local install_folder + install_folder, err = configs.get_parser_install_dir() if err then return api.nvim_err_writeln(err) end + assert(install_folder) local languages local ask if ... == "all" then languages = parsers.available_parsers() ask = false - elseif ... == "maintained" then - languages = parsers.maintained_parsers() - ask = false else languages = vim.tbl_flatten { ... } ask = ask_reinstall @@ -424,6 +464,18 @@ local function install(options) end end +function M.setup_auto_install() + vim.api.nvim_create_autocmd("FileType", { + pattern = { "*" }, + callback = function() + local lang = parsers.get_buf_lang() + if parsers.get_parser_configs()[lang] and not is_installed(lang) then + install() { lang } + end + end, + }) +end + function M.update(options) options = options or {} return function(...) @@ -462,37 +514,67 @@ function M.update(options) end function M.uninstall(...) - local path_sep = "/" - if fn.has "win32" == 1 then - path_sep = "\\" - end - - if vim.tbl_contains({ "all", "maintained" }, ...) then + if vim.tbl_contains({ "all" }, ...) then reset_progress_counter() local installed = info.installed_parsers() - if ... == "maintained" then - local maintained = parsers.maintained_parsers() - installed = vim.tbl_filter(function(l) - return vim.tbl_contains(maintained, l) - end, installed) - end for _, langitem in pairs(installed) do M.uninstall(langitem) end elseif ... then + local ensure_installed_parsers = configs.get_ensure_installed_parsers() + if ensure_installed_parsers == "all" then + ensure_installed_parsers = parsers.available_parsers() + end + ensure_installed_parsers = utils.difference(ensure_installed_parsers, configs.get_ignored_parser_installs()) + local languages = vim.tbl_flatten { ... } for _, lang in ipairs(languages) do - local install_dir, err = utils.get_parser_install_dir() + local install_dir, err = configs.get_parser_install_dir() if err then return api.nvim_err_writeln(err) end - local parser_lib = install_dir .. path_sep .. lang .. ".so" + if vim.tbl_contains(ensure_installed_parsers, lang) then + vim.notify( + "Uninstalling " + .. lang + .. '. But the parser is still configured in "ensure_installed" setting of nvim-treesitter.' + .. " Please consider updating your config!", + vim.log.levels.ERROR + ) + end - local command_list = { - shell.select_rm_file_cmd(parser_lib, "Uninstalling parser for " .. lang), - } - M.iter_cmd(command_list, 1, lang, "Treesitter parser for " .. lang .. " has been uninstalled") + local parser_lib = utils.join_path(install_dir, lang) .. ".so" + local all_parsers = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true) + if vim.fn.filereadable(parser_lib) == 1 then + local command_list = { + shell.select_rm_file_cmd(parser_lib, "Uninstalling parser for " .. lang), + { + cmd = function() + local all_parsers_after_deletion = vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", true) + if #all_parsers_after_deletion > 0 then + vim.notify( + "Tried to uninstall parser for " + .. lang + .. "! But the parser is still installed (not by nvim-treesitter)." + .. " Please delete the following files manually: " + .. table.concat(all_parsers_after_deletion, ", "), + vim.log.levels.ERROR + ) + end + end, + }, + } + M.iter_cmd(command_list, 1, lang, "Treesitter parser for " .. lang .. " has been uninstalled") + elseif #all_parsers > 0 then + vim.notify( + "Parser for " + .. lang + .. " is installed! But not by nvim-treesitter! Please manually remove the following files: " + .. table.concat(all_parsers, ", "), + vim.log.levels.ERROR + ) + end end end end @@ -514,7 +596,17 @@ function M.write_lockfile(verbose, skip_langs) for _, v in ipairs(sorted_parsers) do if not vim.tbl_contains(skip_langs, v.name) then -- I'm sure this can be done in aync way with iter_cmd - local sha = vim.split(vim.fn.systemlist("git ls-remote " .. v.parser.install_info.url)[1], "\t")[1] + local sha + if v.parser.install_info.branch then + sha = vim.split( + vim.fn.systemlist( + "git ls-remote " .. v.parser.install_info.url .. " | grep refs/heads/" .. v.parser.install_info.branch + )[1], + "\t" + )[1] + else + sha = vim.split(vim.fn.systemlist("git ls-remote " .. v.parser.install_info.url)[1], "\t")[1] + end lockfile[v.name] = { revision = sha } if verbose then print(v.name .. ": " .. sha) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/locals.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/locals.lua index e0da2d0..dcfba26 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/locals.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/locals.lua @@ -4,6 +4,7 @@ local queries = require "nvim-treesitter.query" local ts_utils = require "nvim-treesitter.ts_utils" +local ts_query = vim.treesitter.query local api = vim.api local M = {} @@ -162,7 +163,7 @@ M.get_definitions_lookup_table = ts_utils.memoize_by_buf_tick(function(bufnr) local scopes = M.get_definition_scopes(node_entry.node, bufnr, node_entry.scope) -- Always use the highest valid scope local scope = scopes[#scopes] - local node_text = ts_utils.get_node_text(node_entry.node, bufnr)[1] + local node_text = ts_query.get_node_text(node_entry.node, bufnr) local id = M.get_definition_id(scope, node_text) result[id] = node_entry @@ -210,7 +211,7 @@ end function M.find_definition(node, bufnr) local def_lookup = M.get_definitions_lookup_table(bufnr) - local node_text = ts_utils.get_node_text(node, bufnr)[1] + local node_text = ts_query.get_node_text(node, bufnr) for scope in M.iter_scope_tree(node, bufnr) do local id = M.get_definition_id(scope, node_text) @@ -231,7 +232,7 @@ end -- @returns a list of nodes function M.find_usages(node, scope_node, bufnr) local bufnr = bufnr or api.nvim_get_current_buf() - local node_text = ts_utils.get_node_text(node, bufnr)[1] + local node_text = ts_query.get_node_text(node, bufnr) if not node_text or #node_text < 1 then return {} @@ -244,7 +245,7 @@ function M.find_usages(node, scope_node, bufnr) if match.reference and match.reference.node - and ts_utils.get_node_text(match.reference.node, bufnr)[1] == node_text + and ts_query.get_node_text(match.reference.node, bufnr) == node_text then local def_node, _, kind = M.find_definition(match.reference.node, bufnr) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/parsers.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/parsers.lua index a2d030c..0c22f27 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/parsers.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/parsers.lua @@ -1,17 +1,41 @@ local api = vim.api local ts = vim.treesitter -local ft_to_parsername = {} - -local function update_ft_to_parsername(name, parser) - if type(parser.used_by) == "table" then - for _, ft in pairs(parser.used_by) do - ft_to_parsername[ft] = name - end - end - ft_to_parsername[parser.filetype or name] = name -end - +local filetype_to_parsername = { + javascriptreact = "javascript", + ecma = "javascript", + jsx = "javascript", + PKGBUILD = "bash", + html_tags = "html", + ["typescript.tsx"] = "tsx", + terraform = "hcl", + ["html.handlebars"] = "glimmer", + systemverilog = "verilog", + cls = "latex", + sty = "latex", + OpenFOAM = "foam", + pandoc = "markdown", + rmd = "markdown", + cs = "c_sharp", + tape = "vhs", +} + +---@class InstallInfo +---@field url string +---@field branch string|nil +---@field revision string|nil +---@field files string[] +---@field generate_requires_npm boolean|nil +---@field requires_generate_from_grammar boolean|nil +---@field location string|nil + +---@class ParserInfo +---@field install_info InstallInfo +---@field filetype string +---@field maintainers string[] +---@field experimental boolean|nil + +---@type ParserInfo[] local list = setmetatable({}, { __newindex = function(table, parsername, parserconfig) rawset( @@ -20,7 +44,11 @@ local list = setmetatable({}, { setmetatable(parserconfig, { __newindex = function(parserconfigtable, key, value) if key == "used_by" then - ft_to_parsername[value] = parsername + require("nvim-treesitter.utils").notify( + "used_by is deprecated, please use 'filetype_to_parsername'", + vim.log.levels.WARN + ) + filetype_to_parsername[value] = parsername else rawset(parserconfigtable, key, value) end @@ -28,19 +56,76 @@ local list = setmetatable({}, { }) ) - update_ft_to_parsername(parsername, parserconfig) + filetype_to_parsername[parserconfig.filetype or parsername] = parsername end, }) +list.agda = { + install_info = { + url = "https://github.com/AusCyberman/tree-sitter-agda", + branch = "master", + files = { "src/parser.c", "src/scanner.cc" }, + generate_requires_npm = true, + }, + filetype = "agda", + maintainers = { "@Decodetalkers" }, +} + +list.meson = { + install_info = { + url = "https://github.com/Decodetalkers/tree-sitter-meson", + branch = "master", + files = { "src/parser.c" }, + }, + filetype = "meson", + maintainers = { "@Decodetalkers" }, +} + +list.qmljs = { + install_info = { + url = "https://github.com/yuja/tree-sitter-qmljs", + branch = "master", + files = { "src/parser.c", "src/scanner.c" }, + }, + filetype = "qmljs", + maintainers = { "@yuja" }, +} + +list.racket = { + install_info = { + url = "https://github.com/6cdh/tree-sitter-racket", + branch = "main", + files = { "src/parser.c", "src/scanner.cc" }, + }, + maintainers = { "@6cdh" }, +} + +list.scheme = { + install_info = { + url = "https://github.com/6cdh/tree-sitter-scheme", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@6cdh" }, +} + list.javascript = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-javascript", files = { "src/parser.c", "src/scanner.c" }, }, - used_by = { "javascriptreact", "ecma", "jsx" }, maintainers = { "@steelsojka" }, } +list.rego = { + install_info = { + url = "https://github.com/FallenAngel97/tree-sitter-rego", + files = { "src/parser.c" }, + }, + maintainers = { "@FallenAngel97" }, + filetype = "rego", +} + list.c = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-c", @@ -49,6 +134,14 @@ list.c = { maintainers = { "@vigoux" }, } +list.embedded_template = { + install_info = { + url = "https://github.com/tree-sitter/tree-sitter-embedded-template", + files = { "src/parser.c" }, + }, + filetype = "eruby", +} + list.clojure = { install_info = { url = "https://github.com/sogaiu/tree-sitter-clojure", @@ -73,7 +166,6 @@ list.cpp = { files = { "src/parser.c", "src/scanner.cc" }, generate_requires_npm = true, }, - used_by = { "arduino" }, maintainers = { "@theHamsta" }, } @@ -106,6 +198,15 @@ list.glsl = { maintainers = { "@theHamsta" }, } +list.hlsl = { + install_info = { + url = "https://github.com/theHamsta/tree-sitter-hlsl", + files = { "src/parser.c", "src/scanner.cc" }, + generate_requires_npm = true, + }, + maintainers = { "@theHamsta" }, +} + list.dockerfile = { install_info = { url = "https://github.com/camdencheek/tree-sitter-dockerfile", @@ -224,7 +325,6 @@ list.bash = { url = "https://github.com/tree-sitter/tree-sitter-bash", files = { "src/parser.c", "src/scanner.cc" }, }, - used_by = { "PKGBUILD" }, filetype = "sh", maintainers = { "@TravonteD" }, } @@ -245,18 +345,6 @@ list.php = { maintainers = { "@tk-shirasaka" }, } -list.phpdoc = { - install_info = { - url = "https://github.com/claytonrcarter/tree-sitter-phpdoc", - files = { "src/parser.c" }, - -- parser.c in the repo still based on TS 0.17 due to other dependencies - requires_generate_from_grammar = true, - generate_requires_npm = true, - }, - maintainers = { "@mikehaertl" }, - experimental = true, -} - list.java = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-java", @@ -279,7 +367,6 @@ list.html = { url = "https://github.com/tree-sitter/tree-sitter-html", files = { "src/parser.c", "src/scanner.cc" }, }, - used_by = { "html_tags" }, maintainers = { "@TravonteD" }, } @@ -299,6 +386,14 @@ list.json = { maintainers = { "@steelsojka" }, } +list.jsonnet = { + install_info = { + url = "https://github.com/sourcegraph/tree-sitter-jsonnet", + files = { "src/parser.c", "src/scanner.c" }, + }, + maintainers = { "@nawordar" }, +} + list.css = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-css", @@ -330,7 +425,16 @@ list.elixir = { files = { "src/parser.c", "src/scanner.cc" }, branch = "main", }, - maintainers = { "@jonatanklosko" }, + maintainers = { "@jonatanklosko", "@connorlay" }, +} + +list.gleam = { + install_info = { + url = "https://github.com/J3RN/tree-sitter-gleam", + files = { "src/parser.c" }, + branch = "main", + }, + maintainers = { "@connorlay" }, } list.surface = { @@ -367,7 +471,7 @@ list.ocaml = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-ocaml", files = { "src/parser.c", "src/scanner.cc" }, - location = "tree-sitter-ocaml/ocaml", + location = "ocaml", }, maintainers = { "@undu" }, } @@ -376,7 +480,7 @@ list.ocaml_interface = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-ocaml", files = { "src/parser.c", "src/scanner.cc" }, - location = "tree-sitter-ocaml_interface/interface", + location = "interface", }, maintainers = { "@undu" }, filetype = "ocamlinterface", @@ -391,13 +495,31 @@ list.ocamllex = { maintainers = { "@undu" }, } +list.menhir = { + install_info = { + url = "https://github.com/Kerl13/tree-sitter-menhir", + files = { "src/parser.c", "src/scanner.cc" }, + }, + maintainers = { "@Kerl13" }, + filetype = "menhir", +} + +list.org = { + install_info = { + url = "https://github.com/milisims/tree-sitter-org", + branch = "main", + files = { "src/parser.c", "src/scanner.cc" }, + }, +} + list.swift = { install_info = { url = "https://github.com/alex-pinkus/tree-sitter-swift", + branch = "main", files = { "src/parser.c", "src/scanner.c" }, requires_generate_from_grammar = true, - generate_requires_npm = true, }, + maintainers = { "@alex-pinkus" }, } list.c_sharp = { @@ -409,11 +531,22 @@ list.c_sharp = { maintainers = { "@Luxed" }, } +list.todotxt = { + install_info = { + url = "https://github.com/arnarg/tree-sitter-todotxt.git", + files = { "src/parser.c" }, + branch = "main", + }, + filetype = "todotxt", + maintainers = { "@arnarg" }, + experimental = true, +} + list.typescript = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-typescript", files = { "src/parser.c", "src/scanner.c" }, - location = "tree-sitter-typescript/typescript", + location = "typescript", generate_requires_npm = true, }, maintainers = { "@steelsojka" }, @@ -423,10 +556,9 @@ list.tsx = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-typescript", files = { "src/parser.c", "src/scanner.c" }, - location = "tree-sitter-tsx/tsx", + location = "tsx", generate_requires_npm = true, }, - used_by = { "typescript.tsx" }, filetype = "typescriptreact", maintainers = { "@steelsojka" }, } @@ -449,6 +581,16 @@ list.supercollider = { filetype = "supercollider", } +list.slint = { + install_info = { + url = "https://github.com/jrmoulton/tree-sitter-slint", + files = { "src/parser.c" }, + branch = "main", + }, + maintainers = { "@jrmoulton" }, + experimental = true, +} + list.haskell = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-haskell", @@ -464,15 +606,29 @@ list.hcl = { }, maintainers = { "@MichaHoffmann" }, filetype = "hcl", - used_by = { "terraform" }, } list.markdown = { install_info = { url = "https://github.com/MDeiml/tree-sitter-markdown", + location = "tree-sitter-markdown", files = { "src/parser.c", "src/scanner.cc" }, - branch = "main", + branch = "split_parser", + readme_name = "markdown (basic highlighting)", + }, + maintainers = { "@MDeiml" }, + experimental = true, +} + +list.markdown_inline = { + install_info = { + url = "https://github.com/MDeiml/tree-sitter-markdown", + location = "tree-sitter-markdown-inline", + files = { "src/parser.c", "src/scanner.cc" }, + branch = "split_parser", + readme_name = "markdown_inline (needs to be installed for full markdown highlighting)", }, + maintainers = { "@MDeiml" }, experimental = true, } @@ -481,7 +637,7 @@ list.tlaplus = { url = "https://github.com/tlaplus-community/tree-sitter-tlaplus", files = { "src/parser.c", "src/scanner.cc" }, }, - maintainers = { "@ahelwer" }, + maintainers = { "@ahelwer", "@susliko" }, filetype = "tla", } @@ -503,7 +659,6 @@ list.glimmer = { readme_name = "Glimmer and Ember", maintainers = { "@alexlafroscia" }, filetype = "handlebars", - used_by = { "html.handlebars" }, } list.pug = { @@ -513,6 +668,7 @@ list.pug = { }, maintainers = { "@zealot128" }, filetype = "pug", + experimental = true, } list.vue = { @@ -621,10 +777,7 @@ list.verilog = { files = { "src/parser.c" }, generate_requires_npm = true, }, - used_by = { "systemverilog" }, maintainers = { "@zegervdv" }, - -- The parser still uses API version 12, because it does not compile with 13 - experimental = true, } list.pascal = { @@ -636,6 +789,16 @@ list.pascal = { } -- Parsers for injections +list.phpdoc = { + install_info = { + url = "https://github.com/claytonrcarter/tree-sitter-phpdoc", + files = { "src/parser.c", "src/scanner.c" }, + generate_requires_npm = true, + }, + maintainers = { "@mikehaertl" }, + experimental = true, +} + list.regex = { install_info = { url = "https://github.com/tree-sitter/tree-sitter-regex", @@ -678,6 +841,14 @@ list.sparql = { maintainers = { "@bonabeavis" }, } +list.sql = { + install_info = { + url = "https://github.com/derekstride/tree-sitter-sql", + files = { "src/parser.c" }, + }, + maintainers = { "@derekstride" }, +} + list.gdscript = { install_info = { url = "https://github.com/PrestonKnopp/tree-sitter-gdscript", @@ -730,7 +901,7 @@ list.svelte = { list.r = { install_info = { url = "https://github.com/r-lib/tree-sitter-r", - files = { "src/parser.c" }, + files = { "src/parser.c", "src/scanner.cc" }, }, maintainers = { "@jimhester" }, } @@ -738,20 +909,28 @@ list.r = { list.beancount = { install_info = { url = "https://github.com/polarmutex/tree-sitter-beancount", - files = { "src/parser.c" }, + files = { "src/parser.c", "src/scanner.cc" }, branch = "master", }, maintainers = { "@polarmutex" }, } +list.rnoweb = { + install_info = { + url = "https://github.com/bamonroe/tree-sitter-rnoweb", + files = { "src/parser.c", "src/scanner.c" }, + }, + filetype = "rnoweb", + maintainers = { "@bamonroe" }, +} + list.latex = { install_info = { url = "https://github.com/latex-lsp/tree-sitter-latex", - files = { "src/parser.c" }, + files = { "src/parser.c", "src/scanner.c" }, }, filetype = "tex", - used_by = { "cls", "sty" }, - maintainers = { "@theHamsta by asking @clason" }, + maintainers = { "@theHamsta, @clason" }, } list.bibtex = { @@ -760,7 +939,7 @@ list.bibtex = { files = { "src/parser.c" }, }, filetype = "bib", - maintainers = { "@theHamsta by asking @clason" }, + maintainers = { "@theHamsta, @clason" }, } list.zig = { @@ -797,6 +976,15 @@ list.vim = { maintainers = { "@vigoux" }, } +list.help = { + install_info = { + url = "https://github.com/neovim/tree-sitter-vimdoc", + files = { "src/parser.c" }, + }, + filetype = "help", + maintainers = { "@vigoux" }, +} + list.json5 = { install_info = { url = "https://github.com/Joakker/tree-sitter-json5", @@ -844,7 +1032,7 @@ list.llvm = { list.http = { install_info = { - url = "https://github.com/NTBBloodbath/tree-sitter-http", + url = "https://github.com/rest-nvim/tree-sitter-http", branch = "main", files = { "src/parser.c" }, generate_requires_npm = true, @@ -887,7 +1075,6 @@ list.foam = { }, maintainers = { "@FoamScience" }, filetype = "foam", - used_by = { "OpenFOAM" }, -- Queries might change over time on the grammar's side -- Otherwise everything runs fine experimental = true, @@ -912,17 +1099,241 @@ list.norg = { maintainers = { "@JoeyGrajciar", "@vhyrro", "@mrossinek" }, } +list.vala = { + install_info = { + url = "https://github.com/vala-lang/tree-sitter-vala", + branch = "master", + files = { "src/parser.c" }, + }, + maintainers = { "@Prince781", "@vala-lang" }, +} + +list.lalrpop = { + install_info = { + url = "https://github.com/traxys/tree-sitter-lalrpop", + branch = "master", + files = { "src/parser.c", "src/scanner.c" }, + }, + maintainers = { "@traxys" }, +} + +list.solidity = { + install_info = { + url = "https://github.com/YongJieYongJie/tree-sitter-solidity", + branch = "with-generated-c-code", + files = { "src/parser.c" }, + }, + maintainers = { "@YongJieYongJie" }, +} + +list.cooklang = { + install_info = { + url = "https://github.com/addcninblue/tree-sitter-cooklang", + branch = "master", + files = { "src/parser.c", "src/scanner.cc" }, + }, + maintainers = { "@addcninblue" }, +} + +list.elvish = { + install_info = { + url = "https://github.com/ckafi/tree-sitter-elvish", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@ckafi" }, +} + +list.astro = { + install_info = { + url = "https://github.com/virchau13/tree-sitter-astro", + branch = "master", + files = { "src/parser.c", "src/scanner.cc" }, + }, + maintainers = { "@virchau13" }, +} + +list.wgsl = { + install_info = { + url = "https://github.com/szebniok/tree-sitter-wgsl", + files = { "src/parser.c" }, + }, + maintainers = { "@szebniok" }, + filetype = "wgsl", +} + +list.m68k = { + install_info = { + url = "https://github.com/grahambates/tree-sitter-m68k", + files = { "src/parser.c" }, + }, + maintainers = { "@grahambates" }, + filetype = "asm68k", +} + +list.proto = { + install_info = { + url = "https://github.com/mitchellh/tree-sitter-proto", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@fsouza" }, + filetype = "proto", +} + +list.v = { + install_info = { + url = "https://github.com/vlang/vls", + files = { "src/parser.c", "src/scanner.c" }, + location = "tree_sitter_v", + generate_requires_npm = false, + requires_generate_from_grammar = false, + }, + filetype = "vlang", + maintainers = { "@tami5" }, +} + +list.tiger = { + install_info = { + url = "https://github.com/ambroisie/tree-sitter-tiger", + files = { "src/parser.c", "src/scanner.c" }, + branch = "main", + generate_requires_npm = false, + requires_generate_from_grammar = false, + }, + filetype = "tiger", + maintainers = { "@ambroisie" }, +} + +list.sxhkdrc = { + install_info = { + url = "https://github.com/RaafatTurki/tree-sitter-sxhkdrc", + files = { "src/parser.c" }, + branch = "master", + generate_requires_npm = false, + requires_generate_from_grammar = false, + filetype = "sxhkdrc", + }, + maintainers = { "@RaafatTurki" }, +} + +list.gitignore = { + install_info = { + url = "https://github.com/shunsambongi/tree-sitter-gitignore", + files = { "src/parser.c" }, + branch = "main", + requires_generate_from_grammar = true, + }, + maintainers = { "@theHamsta" }, +} + +list.nickel = { + install_info = { + url = "https://github.com/nickel-lang/tree-sitter-nickel", + files = { "src/parser.c", "src/scanner.cc" }, + branch = "main", + }, +} + +list.gitattributes = { + install_info = { + url = "https://github.com/ObserverOfTime/tree-sitter-gitattributes", + files = { "src/parser.c" }, + }, + maintainers = { "@ObserverOfTime" }, +} + +list.git_rebase = { + install_info = { + url = "https://github.com/the-mikedavis/tree-sitter-git-rebase", + files = { "src/parser.c" }, + branch = "main", + }, + filetype = "gitrebase", + maintainers = { "@gbprod" }, +} + +list.blueprint = { + install_info = { + url = "https://gitlab.com/gabmus/tree-sitter-blueprint.git", + files = { "src/parser.c" }, + }, + maintainers = { "@gabmus" }, + experimental = true, +} + +list.twig = { + install_info = { + url = "https://github.com/gbprod/tree-sitter-twig", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@gbprod" }, + filetype = "twig", +} + +list.diff = { + install_info = { + url = "https://github.com/the-mikedavis/tree-sitter-diff", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@gbprod" }, + filetype = "gitdiff", +} + +list.vhs = { + install_info = { + url = "https://github.com/charmbracelet/tree-sitter-vhs", + branch = "main", + files = { "src/parser.c" }, + }, + maintainers = { "@caarlos0", "@maaslalani" }, + filetype = "tape", +} + +list.awk = { + install_info = { + url = "https://github.com/Beaglefoot/tree-sitter-awk", + files = { "src/parser.c", "src/scanner.c" }, + }, +} + +list.arduino = { + install_info = { + url = "https://github.com/ObserverOfTime/tree-sitter-arduino", + files = { "src/parser.c", "src/scanner.cc" }, + }, + maintainers = { "@ObserverOfTime" }, +} + +list.jq = { + install_info = { + url = "https://github.com/flurie/tree-sitter-jq", + files = { "src/parser.c" }, + }, +} + +list.mermaid = { + install_info = { + url = "https://github.com/monaqa/tree-sitter-mermaid", + files = { "src/parser.c" }, + }, + experimental = true, +} + local M = { list = list, + filetype_to_parsername = filetype_to_parsername, } function M.ft_to_lang(ft) - local result = ft_to_parsername[ft] + local result = filetype_to_parsername[ft] if result then return result else ft = vim.split(ft, ".", true)[1] - return ft_to_parsername[ft] or ft + return filetype_to_parsername[ft] or ft end end @@ -936,15 +1347,6 @@ function M.available_parsers() end end -function M.maintained_parsers() - local has_tree_sitter_cli = vim.fn.executable "tree-sitter" == 1 and vim.fn.executable "node" == 1 - return vim.tbl_filter(function(lang) - return M.list[lang].maintainers - and not M.list[lang].experimental - and (has_tree_sitter_cli or not M.list[lang].install_info.requires_generate_from_grammar) - end, M.available_parsers()) -end - function M.get_parser_configs() return M.list end @@ -963,7 +1365,7 @@ end M.reset_cache() function M.has_parser(lang) - local lang = lang or M.get_buf_lang(api.nvim_get_current_buf()) + lang = lang or M.get_buf_lang(api.nvim_get_current_buf()) if not lang or #lang == 0 then return false @@ -976,8 +1378,8 @@ function M.has_parser(lang) end function M.get_parser(bufnr, lang) - local buf = bufnr or api.nvim_get_current_buf() - local lang = lang or M.get_buf_lang(buf) + bufnr = bufnr or api.nvim_get_current_buf() + lang = lang or M.get_buf_lang(bufnr) if M.has_parser(lang) then return ts.get_parser(bufnr, lang) @@ -987,8 +1389,7 @@ end -- @deprecated This is only kept for legacy purposes. -- All root nodes should be accounted for. function M.get_tree_root(bufnr) - local bufnr = bufnr or api.nvim_get_current_buf() - + bufnr = bufnr or api.nvim_get_current_buf() return M.get_parser(bufnr):parse()[1]:root() end diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query.lua index 7009e9f..bc80d51 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query.lua @@ -11,8 +11,10 @@ local EMPTY_ITER = function() end M.built_in_query_groups = { "highlights", "locals", "folds", "indents", "injections" } --- Creates a function that checks whether a given query exists --- for a specific language. +--- Creates a function that checks whether a given query exists +--- for a specific language. +---@param query string +---@return function(string): boolean local function get_query_guard(query) return function(lang) return M.has_query_files(lang, query) @@ -23,6 +25,7 @@ for _, query in ipairs(M.built_in_query_groups) do M["has_" .. query] = get_query_guard(query) end +---@return string[] function M.available_query_groups() local query_files = api.nvim_get_runtime_file("queries/*/*.scm", true) local groups = {} @@ -57,11 +60,19 @@ do end end +---@param lang string +---@param query_name string +---@return string[] local function runtime_queries(lang, query_name) return api.nvim_get_runtime_file(string.format("queries/%s/%s.scm", lang, query_name), true) or {} end +---@type table> local query_files_cache = {} + +---@param lang string +---@param query_name string +---@return boolean function M.has_query_files(lang, query_name) if not query_files_cache[lang] then query_files_cache[lang] = {} @@ -86,6 +97,8 @@ do local cache = setmetatable({}, mt) --- Same as `vim.treesitter.query` except will return cached values + ---@param lang string + ---@param query_name string function M.get_query(lang, query_name) if cache[lang][query_name] == nil then cache[lang][query_name] = tsq.get_query(lang, query_name) @@ -98,6 +111,8 @@ do --- If lang and query_name is both present, will reload for only the lang and query_name. --- If only lang is present, will reload all query_names for that lang --- If none are present, will reload everything + ---@param lang string + ---@param query_name string function M.invalidate_query_cache(lang, query_name) if lang and query_name then cache[lang][query_name] = nil @@ -106,14 +121,14 @@ do end elseif lang and not query_name then query_files_cache[lang] = nil - for query_name, _ in pairs(cache[lang]) do - M.invalidate_query_cache(lang, query_name) + for query_name0, _ in pairs(cache[lang]) do + M.invalidate_query_cache(lang, query_name0) end elseif not lang and not query_name then query_files_cache = {} - for lang, _ in pairs(cache) do - for query_name, _ in pairs(cache[lang]) do - M.invalidate_query_cache(lang, query_name) + for lang0, _ in pairs(cache) do + for query_name0, _ in pairs(cache[lang0]) do + M.invalidate_query_cache(lang0, query_name0) end end else @@ -123,11 +138,23 @@ do end --- This function is meant for an autocommand and not to be used. Only use if file is a query file. +---@param fname string function M.invalidate_query_file(fname) local fnamemodify = vim.fn.fnamemodify M.invalidate_query_cache(fnamemodify(fname, ":p:h:t"), fnamemodify(fname, ":t:r")) end +---@class QueryInfo +---@field root LanguageTree +---@field source integer +---@field start integer +---@field stop integer + +---@param bufnr integer +---@param query_name string +---@param root LanguageTree +---@param root_lang string|nil +---@return Query|nil, QueryInfo|nil local function prepare_query(bufnr, query_name, root, root_lang) local buf_lang = parsers.get_buf_lang(bufnr) @@ -181,6 +208,10 @@ local function prepare_query(bufnr, query_name, root, root_lang) } end +---@param query Query +---@param bufnr integer +---@param start_row integer +---@param end_row integer function M.iter_prepared_matches(query, qnode, bufnr, start_row, end_row) -- A function that splits a string on '.' local function split(string) @@ -209,7 +240,7 @@ function M.iter_prepared_matches(query, qnode, bufnr, start_row, end_row) local matches = query:iter_matches(qnode, bufnr, start_row, end_row) local function iterator() - local pattern, match = matches() + local pattern, match, metadata = matches() if pattern ~= nil then local prepared_match = {} @@ -219,6 +250,8 @@ function M.iter_prepared_matches(query, qnode, bufnr, start_row, end_row) if name ~= nil then local path = split(name .. ".node") insert_to_path(prepared_match, path, node) + local metadata_path = split(name .. ".metadata") + insert_to_path(prepared_match, metadata_path, metadata[id]) end end @@ -247,22 +280,23 @@ function M.iter_prepared_matches(query, qnode, bufnr, start_row, end_row) end --- Return all nodes corresponding to a specific capture path (like @definition.var, @reference.type) --- Works like M.get_references or M.get_scopes except you can choose the capture --- Can also be a nested capture like @definition.function to get all nodes defining a function. --- --- @param bufnr the buffer --- @param captures a single string or a list of strings --- @param query_group the name of query group (highlights or injections for example) --- @param root (optional) node from where to start the search --- @param lang (optional) the language from where to get the captures. --- Root nodes can have several languages. +---Works like M.get_references or M.get_scopes except you can choose the capture +---Can also be a nested capture like @definition.function to get all nodes defining a function. +--- +---@param bufnr integer the buffer +---@param captures string|string[] +---@param query_group string the name of query group (highlights or injections for example) +---@param root LanguageTree|nil node from where to start the search +---@param lang string|nil the language from where to get the captures. +--- Root nodes can have several languages. +---@return table|nil function M.get_capture_matches(bufnr, captures, query_group, root, lang) if type(captures) == "string" then captures = { captures } end local strip_captures = {} for i, capture in ipairs(captures) do - if not capture:sub(1, 1) == "@" then + if capture:sub(1, 1) ~= "@" then error 'Captures must start with "@"' return end @@ -287,6 +321,7 @@ function M.iter_captures(bufnr, query_name, root, lang) if not query then return EMPTY_ITER end + assert(params) local iter = query:iter_captures(params.root, params.source, params.start, params.stop) @@ -334,16 +369,17 @@ function M.find_best_match(bufnr, capture_string, query_group, filter_predicate, return best end --- Iterates matches from a query file. --- @param bufnr the buffer --- @param query_group the query file to use --- @param root the root node --- @param root the root node lang, if known +---Iterates matches from a query file. +---@param bufnr integer the buffer +---@param query_group string the query file to use +---@param root LanguageTree the root node +---@param root_lang string|nil the root node lang, if known function M.iter_group_results(bufnr, query_group, root, root_lang) local query, params = prepare_query(bufnr, query_group, root, root_lang) if not query then return EMPTY_ITER end + assert(params) return M.iter_prepared_matches(query, params.root, params.source, params.start, params.stop) end @@ -358,18 +394,25 @@ function M.collect_group_results(bufnr, query_group, root, lang) return matches end +---@alias CaptureResFn function(string, LanguageTree, LanguageTree): string, string + --- Same as get_capture_matches except this will recursively get matches for every language in the tree. --- @param bufnr The bufnr --- @param capture_or_fn The capture to get. If a function is provided then that --- function will be used to resolve both the capture and query argument. --- The function can return `nil` to ignore that tree. --- @param query_type The query to get the capture from. This is ignore if a function is provided --- for the captuer argument. +---@param bufnr integer The bufnr +---@param capture_or_fn string|CaptureResFn The capture to get. If a function is provided then that +--- function will be used to resolve both the capture and query argument. +--- The function can return `nil` to ignore that tree. +---@param query_type string The query to get the capture from. This is ignore if a function is provided +--- for the captuer argument. function M.get_capture_matches_recursively(bufnr, capture_or_fn, query_type) - local type_fn = type(capture_or_fn) == "function" and capture_or_fn - or function() + ---@type CaptureResFn + local type_fn + if type(capture_or_fn) == "function" then + type_fn = capture_or_fn + else + type_fn = function(_, _, _) return capture_or_fn, query_type end + end local parser = parsers.get_parser(bufnr) local matches = {} diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query_predicates.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query_predicates.lua index be71330..0638b6e 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query_predicates.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/query_predicates.lua @@ -128,3 +128,61 @@ query.add_directive("downcase!", function(match, _, bufnr, pred, metadata) metadata[key] = string.lower(text) end end) + +query.add_directive("exclude_children!", function(match, _pattern, _bufnr, pred, metadata) + local capture_id = pred[2] + local node = match[capture_id] + local start_row, start_col, end_row, end_col = node:range() + local ranges = {} + for i = 0, node:named_child_count() - 1 do + local child = node:named_child(i) + local child_start_row, child_start_col, child_end_row, child_end_col = child:range() + if child_start_row > start_row or child_start_col > start_col then + table.insert(ranges, { + start_row, + start_col, + child_start_row, + child_start_col, + }) + end + start_row = child_end_row + start_col = child_end_col + end + if end_row > start_row or end_col > start_col then + table.insert(ranges, { start_row, start_col, end_row, end_col }) + end + metadata.content = ranges +end) + +-- Trim blank lines from end of the region +-- Arguments are the captures to trim. +query.add_directive("trim!", function(match, _, bufnr, pred, metadata) + for _, id in ipairs { select(2, unpack(pred)) } do + local node = match[id] + local start_row, start_col, end_row, end_col = node:range() + + -- Don't trim if region ends in middle of a line + if end_col ~= 0 then + return + end + + while true do + -- As we only care when end_col == 0, always inspect one line above end_row. + local end_line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)[1] + + if end_line ~= "" then + break + end + + end_row = end_row - 1 + end + + -- If this produces an invalid range, we just skip it. + if start_row < end_row or (start_row == end_row and start_col <= end_col) then + if not metadata[id] then + metadata[id] = {} + end + metadata[id].range = { start_row, start_col, end_row, end_col } + end + end +end) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/shell_command_selectors.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/shell_command_selectors.lua index 5a16b58..59bbb0e 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/shell_command_selectors.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/shell_command_selectors.lua @@ -49,6 +49,7 @@ function M.select_rm_file_cmd(file, info_msg) end end +---@return string|nil function M.select_executable(executables) return vim.tbl_filter(function(c) return c ~= vim.NIL and fn.executable(c) == 1 @@ -94,7 +95,14 @@ function M.select_compiler_args(repo, compiler) end function M.select_compile_command(repo, cc, compile_location) - if string.match(cc, "cl$") or string.match(cc, "cl.exe$") or not repo.use_makefile or fn.has "win32" == 1 then + local make = M.select_executable { "gmake", "make" } + if + string.match(cc, "cl$") + or string.match(cc, "cl.exe$") + or not repo.use_makefile + or fn.has "win32" == 1 + or not make + then return { cmd = cc, info = "Compiling...", @@ -106,14 +114,14 @@ function M.select_compile_command(repo, cc, compile_location) } else return { - cmd = "make", + cmd = make, info = "Compiling...", err = "Error during compilation", opts = { args = { "--makefile=" .. utils.join_path(utils.get_package_path(), "scripts", "compile_parsers.makefile"), "CC=" .. cc, - "CXX_STANDARD=" .. repo.cxx_standard, + "CXX_STANDARD=" .. (repo.cxx_standard or "c++14"), }, cwd = compile_location, }, @@ -171,11 +179,16 @@ function M.select_download_commands(repo, project_name, cache_folder, revision, local path_sep = utils.get_path_sep() local url = repo.url:gsub(".git$", "") + local folder_rev = revision + if is_github and revision:match "^v%d" then + folder_rev = revision:sub(2) + end + return { M.select_install_rm_cmd(cache_folder, project_name .. "-tmp"), { cmd = "curl", - info = "Downloading...", + info = "Downloading " .. project_name .. "...", err = "Error during download, please verify your internet connection", opts = { args = { @@ -192,7 +205,7 @@ function M.select_download_commands(repo, project_name, cache_folder, revision, M.select_mkdir_cmd(project_name .. "-tmp", cache_folder, "Creating temporary directory"), { cmd = "tar", - info = "Extracting...", + info = "Extracting " .. project_name .. "...", err = "Error during tarball extraction.", opts = { args = { @@ -206,7 +219,7 @@ function M.select_download_commands(repo, project_name, cache_folder, revision, }, M.select_rm_file_cmd(cache_folder .. path_sep .. project_name .. ".tar.gz"), M.select_mv_cmd( - utils.join_path(project_name .. "-tmp", url:match "[^/]-$" .. "-" .. revision), + utils.join_path(project_name .. "-tmp", url:match "[^/]-$" .. "-" .. folder_rev), project_name, cache_folder ), @@ -219,7 +232,7 @@ function M.select_download_commands(repo, project_name, cache_folder, revision, return { { cmd = "git", - info = "Downloading...", + info = "Downloading " .. project_name .. "...", err = clone_error, opts = { args = { diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/statusline.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/statusline.lua new file mode 100644 index 0000000..82be065 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/statusline.lua @@ -0,0 +1,50 @@ +local parsers = require "nvim-treesitter.parsers" +local ts_utils = require "nvim-treesitter.ts_utils" + +local M = {} + +-- Trim spaces and opening brackets from end +local transform_line = function(line) + return line:gsub("%s*[%[%(%{]*%s*$", "") +end + +function M.statusline(opts) + if not parsers.has_parser() then + return + end + local options = opts or {} + -- if type(opts) == "number" then + -- options = { indicator_size = opts } + -- end + local bufnr = options.bufnr or 0 + local indicator_size = options.indicator_size or 100 + local type_patterns = options.type_patterns or { "class", "function", "method" } + local transform_fn = options.transform_fn or transform_line + local separator = options.separator or " -> " + + local current_node = ts_utils.get_node_at_cursor() + if not current_node then + return "" + end + + local lines = {} + local expr = current_node + + while expr do + local line = ts_utils._get_line_for_node(expr, type_patterns, transform_fn, bufnr) + if line ~= "" and not vim.tbl_contains(lines, line) then + table.insert(lines, 1, line) + end + expr = expr:parent() + end + + local text = table.concat(lines, separator) + local text_len = #text + if text_len > indicator_size then + return "..." .. text:sub(text_len - indicator_size, text_len) + end + + return text +end + +return M diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/ts_utils.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/ts_utils.lua index 06b43ce..5994e4b 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/ts_utils.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/ts_utils.lua @@ -5,12 +5,8 @@ local utils = require "nvim-treesitter.utils" local M = {} ---- Gets the actual text content of a node --- @param node the node to get the text from --- @param bufnr the buffer containing the node --- @return list of lines of text of the node -function M.get_node_text(node, bufnr) - local bufnr = bufnr or api.nvim_get_current_buf() +local function get_node_text(node, bufnr) + bufnr = bufnr or api.nvim_get_current_buf() if not node then return {} end @@ -20,9 +16,12 @@ function M.get_node_text(node, bufnr) if start_row ~= end_row then local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false) + if next(lines) == nil then + return {} + end lines[1] = string.sub(lines[1], start_col + 1) -- end_row might be just after the last line. In this case the last line is not truncated. - if #lines == end_row - start_row then + if #lines == end_row - start_row + 1 then lines[#lines] = string.sub(lines[#lines], 1, end_col) end return lines @@ -33,6 +32,37 @@ function M.get_node_text(node, bufnr) end end +---@private +function M._get_line_for_node(node, type_patterns, transform_fn, bufnr) + local node_type = node:type() + local is_valid = false + for _, rgx in ipairs(type_patterns) do + if node_type:find(rgx) then + is_valid = true + break + end + end + if not is_valid then + return "" + end + local line = transform_fn(vim.trim(get_node_text(node, bufnr)[1] or "")) + -- Escape % to avoid statusline to evaluate content as expression + return line:gsub("%%", "%%%%") +end + +--- Gets the actual text content of a node +-- @deprecated Use vim.treesitter.query.get_node_text +-- @param node the node to get the text from +-- @param bufnr the buffer containing the node +-- @return list of lines of text of the node +function M.get_node_text(node, bufnr) + vim.notify_once( + "nvim-treesitter.ts_utils.get_node_text is deprecated: use vim.treesitter.query.get_node_text", + vim.log.levels.WARN + ) + return get_node_text(node, bufnr) +end + --- Determines whether a node is the parent of another -- @param dest the possible parent -- @param source the possible child node @@ -124,7 +154,7 @@ function M.get_named_children(node) return nodes end -function M.get_node_at_cursor(winnr) +function M.get_node_at_cursor(winnr, ignore_injected_langs) winnr = winnr or 0 local cursor = api.nvim_win_get_cursor(winnr) local cursor_range = { cursor[1] - 1, cursor[2] } @@ -134,7 +164,19 @@ function M.get_node_at_cursor(winnr) if not root_lang_tree then return end - local root = M.get_root_for_position(cursor_range[1], cursor_range[2], root_lang_tree) + + local root + if ignore_injected_langs then + for _, tree in ipairs(root_lang_tree:trees()) do + local tree_root = tree:root() + if tree_root and M.is_in_node_range(tree_root, cursor_range[1], cursor_range[2]) then + root = tree_root + break + end + end + else + root = M.get_root_for_position(cursor_range[1], cursor_range[2], root_lang_tree) + end if not root then return @@ -220,14 +262,14 @@ function M.update_selection(buf, node, selection_mode) selection_mode = selection_mode or "charwise" local start_row, start_col, end_row, end_col = M.get_vim_range({ M.get_node_range(node) }, buf) - vim.fn.setpos(".", { buf, start_row, start_col, 0 }) - -- Start visual selection in appropriate mode local v_table = { charwise = "v", linewise = "V", blockwise = "" } ---- Call to `nvim_replace_termcodes()` is needed for sending appropriate ---- command to enter blockwise mode local mode_string = vim.api.nvim_replace_termcodes(v_table[selection_mode] or selection_mode, true, true, true) vim.cmd("normal! " .. mode_string) + vim.fn.setpos(".", { buf, start_row, start_col, 0 }) + vim.cmd "normal! o" vim.fn.setpos(".", { buf, end_row, end_col, 0 }) end @@ -326,8 +368,8 @@ function M.swap_nodes(node_or_range1, node_or_range2, bufnr, cursor_to_second) local range1 = M.node_to_lsp_range(node_or_range1) local range2 = M.node_to_lsp_range(node_or_range2) - local text1 = M.get_node_text(node_or_range1) - local text2 = M.get_node_text(node_or_range2) + local text1 = get_node_text(node_or_range1, bufnr) + local text2 = get_node_text(node_or_range2, bufnr) local edit1 = { range = range1, newText = table.concat(text2, "\n") } local edit2 = { range = range2, newText = table.concat(text1, "\n") } diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/utils.lua b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/utils.lua index 0e83640..156ba9c 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/utils.lua +++ b/etc/soft/nvim/+plugins/nvim-treesitter/lua/nvim-treesitter/utils.lua @@ -10,23 +10,8 @@ function M.notify(msg, log_level, opts) vim.notify(msg, log_level, vim.tbl_extend("force", default_opts, opts or {})) end -function M.setup_commands(mod, commands) - for command_name, def in pairs(commands) do - local call_fn = string.format( - "lua require'nvim-treesitter.%s'.commands.%s['run']()", - mod, - command_name - ) - local parts = vim.tbl_flatten { - "command!", - def.args, - command_name, - call_fn, - } - api.nvim_command(table.concat(parts, " ")) - end -end - +-- Returns the system specific path seperator. +---@return string function M.get_path_sep() return fn.has "win32" == 1 and "\\" or "/" end @@ -45,7 +30,84 @@ end M.join_path = M.generate_join(M.get_path_sep()) -local join_space = M.generate_join " " +M.join_space = M.generate_join " " + +--- Define user defined vim command which calls nvim-treesitter module function +--- - If module name is 'mod', it should be defined in hierarchy 'nvim-treesitter.mod' +--- - A table with name 'commands' should be defined in 'mod' which needs to be passed as +--- the commands param of this function +--- +---@param mod string, Name of the module that resides in the heirarchy - nvim-treesitter.module +---@param commands table, Command list for the module +--- - {command_name} Name of the vim user defined command, Keys: +--- - {run}: (function) callback function that needs to be executed +--- - {f_args}: (string, default ) +--- - type of arguments that needs to be passed to the vim command +--- - {args}: (string, optional) +--- - vim command attributes +--- +---Example: +--- If module is nvim-treesitter.custom_mod +---
+---  M.commands = {
+---      custom_command = {
+---          run = M.module_function,
+---          f_args = "",
+---          args = {
+---              "-range"
+---          }
+---      }
+---  }
+---
+---  utils.setup_commands("custom_mod", require("nvim-treesitter.custom_mod").commands)
+---  
+--- +--- Will generate command : +---
+---  command! -range custom_command \
+---      lua require'nvim-treesitter.custom_mod'.commands.custom_command['run']()
+---  
+function M.setup_commands(mod, commands) + for command_name, def in pairs(commands) do + local f_args = def.f_args or "" + local call_fn = + string.format("lua require'nvim-treesitter.%s'.commands.%s['run'](%s)", mod, command_name, f_args) + local parts = vim.tbl_flatten { + "command!", + "-bar", + def.args, + command_name, + call_fn, + } + api.nvim_command(table.concat(parts, " ")) + end +end + +---@param dir string +---@param create_err string +---@param writeable_err string +---@return string|nil, string|nil +function M.create_or_reuse_writable_dir(dir, create_err, writeable_err) + create_err = create_err or M.join_space("Could not create dir '", dir, "': ") + writeable_err = writeable_err or M.join_space("Invalid rights, '", dir, "' should be read/write") + -- Try creating and using parser_dir if it doesn't exist + if not luv.fs_stat(dir) then + local ok, error = pcall(vim.fn.mkdir, dir, "p", "0755") + if not ok then + return nil, M.join_space(create_err, error) + end + + return dir + end + + -- parser_dir exists, use it if it's read/write + if luv.fs_access(dir, "RW") then + return dir + end + + -- parser_dir exists but isn't read/write, give up + return nil, M.join_space(writeable_err, dir, "'") +end function M.get_package_path() -- Path to this source file, removing the leading '@' @@ -64,56 +126,13 @@ function M.get_cache_dir() return "/tmp" end - return nil, join_space("Invalid cache rights,", fn.stdpath "data", "or /tmp should be read/write") + return nil, M.join_space("Invalid cache rights,", fn.stdpath "data", "or /tmp should be read/write") end -- Returns $XDG_DATA_HOME/nvim/site, but could use any directory that is in -- runtimepath function M.get_site_dir() - local path_sep = M.get_path_sep() - return M.join_path(fn.stdpath "data", path_sep, "site") -end - --- Try the package dir of the nvim-treesitter plugin first, followed by the --- "site" dir from "runtimepath". "site" dir will be created if it doesn't --- exist. Using only the package dir won't work when the plugin is installed --- with Nix, since the "/nix/store" is read-only. -function M.get_parser_install_dir(folder_name) - folder_name = folder_name or "parser" - local package_path = M.get_package_path() - local package_path_parser_dir = M.join_path(package_path, folder_name) - - -- If package_path is read/write, use that - if luv.fs_access(package_path_parser_dir, "RW") then - return package_path_parser_dir - end - - local site_dir = M.get_site_dir() - local path_sep = M.get_path_sep() - local parser_dir = M.join_path(site_dir, path_sep, folder_name) - - -- Try creating and using parser_dir if it doesn't exist - if not luv.fs_stat(parser_dir) then - local ok, error = pcall(vim.fn.mkdir, parser_dir, "p", "0755") - if not ok then - return nil, join_space("Couldn't create parser dir", parser_dir, ":", error) - end - - return parser_dir - end - - -- parser_dir exists, use it if it's read/write - if luv.fs_access(parser_dir, "RW") then - return parser_dir - end - - -- package_path isn't read/write, parser_dir exists but isn't read/write - -- either, give up - return nil, join_space("Invalid cache rights,", package_path, "or", parser_dir, "should be read/write") -end - -function M.get_parser_info_dir() - return M.get_parser_install_dir "parser-info" + return M.join_path(fn.stdpath "data", "site") end -- Gets a property at path @@ -136,12 +155,6 @@ function M.get_at_path(tbl, path) return result end --- Prints a warning message --- @param text the text message -function M.print_warning(text) - api.nvim_command(string.format([[echohl WarningMsg | echo "%s" | echohl None]], text)) -end - function M.set_jump() vim.cmd "normal! m'" end @@ -193,6 +206,7 @@ function M.to_func(a) return type(a) == "function" and a or M.constant(a) end +---@return string|nil function M.ts_cli_version() if fn.executable "tree-sitter" == 1 then local handle = io.popen "tree-sitter -V" diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.lua b/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.lua new file mode 100644 index 0000000..4ea3925 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.lua @@ -0,0 +1,34 @@ +-- Last Change: 2022 Apr 16 + +if vim.g.loaded_nvim_treesitter then + return +end +vim.g.loaded_nvim_treesitter = true + +-- setup modules +require("nvim-treesitter").setup() + +local api = vim.api + +-- define autocommands +local augroup = api.nvim_create_augroup("NvimTreesitter", {}) + +api.nvim_create_autocmd("Filetype", { + pattern = "query", + group = augroup, + callback = function() + api.nvim_clear_autocmds { + group = augroup, + event = "BufWritePost", + } + api.nvim_create_autocmd("BufWritePost", { + group = augroup, + buffer = 0, + callback = function(opts) + require("nvim-treesitter.query").invalidate_query_file(opts.file) + end, + desc = "Invalidate query file", + }) + end, + desc = "Reload query", +}) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.vim b/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.vim deleted file mode 100644 index 04e64f4..0000000 --- a/etc/soft/nvim/+plugins/nvim-treesitter/plugin/nvim-treesitter.vim +++ /dev/null @@ -1,96 +0,0 @@ -" Last Change: 2020 Aug 13 - -if exists('g:loaded_nvim_treesitter') - finish -endif - -augroup NvimTreesitter - " on every query file write we want to set an autocommand that will reload the cache - autocmd FileType query - \ autocmd! NvimTreesitter BufWritePost call v:lua.require('nvim-treesitter.query').invalidate_query_file(expand('%:p')) -augroup END - -let g:loaded_nvim_treesitter = 1 - -lua require'nvim-treesitter'.setup() - -function s:has_attr(attr, mode) - let norm_color = synIDattr(hlID('Normal'), a:attr, a:mode) - return strlen(norm_color) > 0 -endfunction - -" if the ctermfg or guifg is not known by nvim then using the -" fg or foreground highlighting value will cause an E419 error -" so we check to see if either highlight has been set if not default to NONE -let cterm_normal = s:has_attr('fg', 'cterm') ? 'fg' : 'NONE' -let gui_normal = s:has_attr('fg', 'gui') ? 'foreground' : 'NONE' - -execute 'highlight default TSNone term=NONE cterm=NONE gui=NONE guifg='.gui_normal.' ctermfg='.cterm_normal - -highlight default link TSPunctDelimiter Delimiter -highlight default link TSPunctBracket Delimiter -highlight default link TSPunctSpecial Delimiter - -highlight default link TSConstant Constant -highlight default link TSConstBuiltin Special -highlight default link TSConstMacro Define -highlight default link TSString String -highlight default link TSStringRegex String -highlight default link TSStringEscape SpecialChar -highlight default link TSStringSpecial SpecialChar -highlight default link TSCharacter Character -highlight default link TSNumber Number -highlight default link TSBoolean Boolean -highlight default link TSFloat Float - -highlight default link TSFunction Function -highlight default link TSFuncBuiltin Special -highlight default link TSFuncMacro Macro -highlight default link TSParameter Identifier -highlight default link TSParameterReference TSParameter -highlight default link TSMethod Function -highlight default link TSField Identifier -highlight default link TSProperty Identifier -highlight default link TSConstructor Special -highlight default link TSAnnotation PreProc -highlight default link TSAttribute PreProc -highlight default link TSNamespace Include -highlight default link TSSymbol Identifier - -highlight default link TSConditional Conditional -highlight default link TSRepeat Repeat -highlight default link TSLabel Label -highlight default link TSOperator Operator -highlight default link TSKeyword Keyword -highlight default link TSKeywordFunction Keyword -highlight default link TSKeywordOperator TSOperator -highlight default link TSKeywordReturn TSKeyword -highlight default link TSException Exception - -highlight default link TSType Type -highlight default link TSTypeBuiltin Type -highlight default link TSInclude Include - -highlight default link TSVariableBuiltin Special - -highlight default link TSText TSNone -highlight default TSStrong term=bold cterm=bold gui=bold -highlight default TSEmphasis term=italic cterm=italic gui=italic -highlight default TSUnderline term=underline cterm=underline gui=underline -highlight default TSStrike term=strikethrough cterm=strikethrough gui=strikethrough -highlight default link TSMath Special -highlight default link TSTextReference Constant -highlight default link TSEnvironment Macro -highlight default link TSEnvironmentName Type -highlight default link TSTitle Title -highlight default link TSLiteral String -highlight default link TSURI Underlined - -highlight default link TSComment Comment -highlight default link TSNote SpecialComment -highlight default link TSWarning Todo -highlight default link TSDanger WarningMsg - -highlight default link TSTag Label -highlight default link TSTagDelimiter Delimiter -highlight default link TSTagAttribute TSProperty diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/folds.scm new file mode 100644 index 0000000..e3258ef --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/folds.scm @@ -0,0 +1,4 @@ +[ + (record) + (module) +] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/highlights.scm new file mode 100644 index 0000000..ed208f1 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/agda/highlights.scm @@ -0,0 +1,82 @@ + +;; Constants +(integer) @number + +;; Variables and Symbols + +(typed_binding (atom (qid) @variable)) +(untyped_binding) @variable +(typed_binding (expr) @type) + +(id) @function +(bid) @function + +(function_name (atom (qid) @function)) +(field_name) @function + + +[(data_name) (record_name)] @constructor + +; Set +(SetN) @type.builtin + +(expr . (atom) @function) + +((atom) @boolean + (#any-of? @boolean "true" "false" "True" "False")) + +;; Imports and Module Declarations + +"import" @include + +(module_name) @namespace + +;; Pragmas and comments + +(pragma) @preproc + +(comment) @comment + +;; Keywords +[ + "where" + "data" + "rewrite" + "postulate" + "public" + "private" + "tactic" + "Prop" + "quote" + "renaming" + "open" + "in" + "hiding" + "constructor" + "abstract" + "let" + "field" + "mutual" + "module" + "infix" + "infixl" + "infixr" + "record" + (ARROW) +] +@keyword + +;;;(expr +;;; f_name: (atom) @function) +;; Brackets + +[ + "(" + ")" + "{" + "}"] +@punctuation.bracket + +[ + "=" +] @operator diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/folds.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/folds.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/highlights.scm new file mode 100644 index 0000000..a4e74ae --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/highlights.scm @@ -0,0 +1,111 @@ +; inherits: cpp + +((identifier) @function.builtin + (#any-of? @function.builtin + ; Digital I/O + "digitalRead" + "digitalWrite" + "pinMode" + ; Analog I/O + "analogRead" + "analogReference" + "analogWrite" + ; Zero, Due & MKR Family + "analogReadResolution" + "analogWriteResolution" + ; Advanced I/O + "noTone" + "pulseIn" + "pulseInLong" + "shiftIn" + "shiftOut" + "tone" + ; Time + "delay" + "delayMicroseconds" + "micros" + "millis" + ; Math + "abs" + "constrain" + "map" + "max" + "min" + "pow" + "sq" + "sqrt" + ; Trigonometry + "cos" + "sin" + "tan" + ; Characters + "isAlpha" + "isAlphaNumeric" + "isAscii" + "isControl" + "isDigit" + "isGraph" + "isHexadecimalDigit" + "isLowerCase" + "isPrintable" + "isPunct" + "isSpace" + "isUpperCase" + "isWhitespace" + ; Random Numbers + "random" + "randomSeed" + ; Bits and Bytes + "bit" + "bitClear" + "bitRead" + "bitSet" + "bitWrite" + "highByte" + "lowByte" + ; External Interrupts + "attachInterrupt" + "detachInterrupt" + ; Interrupts + "interrupts" + "noInterrupts" + )) + +((identifier) @type.builtin + (#any-of? @type.builtin + "Serial" + "SPI" + "Stream" + "Wire" + "Keyboard" + "Mouse" + "String" + )) + +((identifier) @constant.builtin + (#any-of? @constant.builtin + "HIGH" + "LOW" + "INPUT" + "OUTPUT" + "INPUT_PULLUP" + "LED_BUILTIN" + )) + +(function_definition + (function_declarator + declarator: (identifier) @function.builtin) + (#any-of? @function.builtin "loop" "setup")) + +(call_expression + function: (primitive_type) @function.builtin) + +(call_expression + function: (identifier) @constructor + (#any-of? @constructor "SPISettings" "String")) + +(declaration + (type_identifier) @type.builtin + (function_declarator + declarator: (identifier) @constructor) + (#eq? @type.builtin "SPISettings")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/indents.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/indents.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/injections.scm new file mode 100644 index 0000000..ce1e794 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/injections.scm @@ -0,0 +1,3 @@ +(preproc_arg) @arduino + +(comment) @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/locals.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/arduino/locals.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/folds.scm new file mode 100644 index 0000000..1f2129c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/folds.scm @@ -0,0 +1 @@ +; inherits: html diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/highlights.scm new file mode 100644 index 0000000..62e8ed2 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/highlights.scm @@ -0,0 +1,5 @@ +; inherits: html + +[ "---" ] @punctuation.delimiter + +[ "{" "}" ] @punctuation.special diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/indents.scm new file mode 100644 index 0000000..1f2129c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/indents.scm @@ -0,0 +1 @@ +; inherits: html diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/injections.scm new file mode 100644 index 0000000..8b68bee --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/injections.scm @@ -0,0 +1,7 @@ +; inherits: html + +((frontmatter + (raw_text) @typescript)) + +((interpolation + (raw_text) @tsx)) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/locals.scm new file mode 100644 index 0000000..1f2129c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/astro/locals.scm @@ -0,0 +1 @@ +; inherits: html diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/highlights.scm new file mode 100644 index 0000000..918d011 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/highlights.scm @@ -0,0 +1,154 @@ +; adapted from https://github.com/Beaglefoot/tree-sitter-awk + +[ + (identifier) + (field_ref) +] @variable +(field_ref (_) @variable) + +(number) @number + +(string) @string +(regex) @string.regex +(escape_sequence) @string.escape + +(comment) @comment @spell + +(ns_qualified_name (namespace) @namespace) +(ns_qualified_name "::" @punctuation.delimiter) + +(func_def name: (_ (identifier) @function) @function) +(func_call name: (_ (identifier) @function) @function) + +(func_def (param_list (identifier) @parameter)) + +[ + "print" + "printf" + "getline" +] @function.builtin + +[ + (delete_statement) + (break_statement) + (continue_statement) + (next_statement) + (nextfile_statement) +] @keyword + +[ + "func" + "function" +] @keyword.function + +[ + "return" + "exit" +] @keyword.return + +[ + "do" + "while" + "for" + "in" +] @repeat + +[ + "if" + "else" + "switch" + "case" + "default" +] @conditional + +[ + "@include" + "@load" +] @include + +"@namespace" @preproc + +[ + "BEGIN" + "END" + "BEGINFILE" + "ENDFILE" +] @label + +(binary_exp [ + "^" + "**" + "*" + "/" + "%" + "+" + "-" + "<" + ">" + "<=" + ">=" + "==" + "!=" + "~" + "!~" + "in" + "&&" + "||" +] @operator) + +(unary_exp [ + "!" + "+" + "-" +] @operator) + +(assignment_exp [ + "=" + "+=" + "-=" + "*=" + "/=" + "%=" + "^=" +] @operator) + +(ternary_exp [ + "?" + ":" +] @operator) + +(update_exp [ + "++" + "--" +] @operator) + +(redirected_io_statement [ + ">" + ">>" +] @operator) + +(piped_io_statement [ + "|" + "|&" +] @operator) + +(piped_io_exp [ + "|" + "|&" +] @operator) + +(field_ref "$" @punctuation.delimiter) + +(regex "/" @punctuation.delimiter) +(regex_constant "@" @punctuation.delimiter) + +[ ";" "," ] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/injections.scm new file mode 100644 index 0000000..8cbffc6 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/awk/injections.scm @@ -0,0 +1,2 @@ +(comment) @comment +(regex) @regex diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/bash/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/bash/highlights.scm index 75ebaca..d1cdb65 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/bash/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/bash/highlights.scm @@ -27,7 +27,9 @@ [ ">" + ">>" "<" + "<<" "&" "&&" "|" @@ -41,8 +43,9 @@ [ (string) (raw_string) + (ansi_c_string) (heredoc_body) -] @string +] @string @spell (variable_assignment (word) @string) @@ -76,13 +79,14 @@ (special_variable_name) @constant +; trap -l ((word) @constant.builtin - (#match? @constant.builtin "^SIG(INT|TERM|QUIT|TIN|TOU|STP|HUP)$")) + (#match? @constant.builtin "^SIG(HUP|INT|QUIT|ILL|TRAP|ABRT|BUS|FPE|KILL|USR[12]|SEGV|PIPE|ALRM|TERM|STKFLT|CHLD|CONT|STOP|TSTP|TT(IN|OU)|URG|XCPU|XFSZ|VTALRM|PROF|WINCH|IO|PWR|SYS|RTMIN([+]([1-9]|1[0-5]))?|RTMAX(-([1-9]|1[0-4]))?)$")) ((word) @boolean (#match? @boolean "^(true|false)$")) -(comment) @comment +(comment) @comment @spell (test_operator) @string (command_substitution @@ -95,12 +99,12 @@ (function_definition name: (word) @function) -(command_name (word) @function) +(command_name (word) @function.call) ((command_name (word) @function.builtin) (#any-of? @function.builtin - "cd" "echo" "eval" "exit" "getopts" - "pushd" "popd" "return" "set" "shift")) + "alias" "cd" "clear" "echo" "eval" "exit" "getopts" "popd" + "pushd" "return" "set" "shift" "shopt" "source" "test")) (command argument: [ @@ -127,3 +131,6 @@ value: (word) @parameter) (regex) @string.regex + +((program . (comment) @preproc) + (#match? @preproc "^#!/")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/beancount/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/beancount/folds.scm index b65ae01..ffe3195 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/beancount/folds.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/beancount/folds.scm @@ -1,4 +1,4 @@ [ (transaction) - (heading) + (section) ] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/blueprint/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/blueprint/highlights.scm new file mode 100644 index 0000000..3d4b482 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/blueprint/highlights.scm @@ -0,0 +1,57 @@ +(object_id) @variable + +(string) @string +(escape_sequence) @string.escape + +(comment) @comment + +(constant) @constant.builtin + +(boolean) @boolean + +(using) @include + +(template) @keyword + +(decorator) @attribute + +(property_definition (property_name) @property) + +(object) @type + +(signal_binding (signal_name) @function.builtin) +(signal_binding (function (identifier)) @function) +(signal_binding "swapped" @keyword) + +(styles_list "styles" @function.macro) +(layout_definition "layout" @function.macro) + +(gettext_string "_" @function.builtin) + +(menu_definition "menu" @keyword) +(menu_section "section" @keyword) +(menu_item "item" @function.macro) + +(template_definition (template_name_qualifier) @type.qualifier) + +(import_statement (gobject_library) @namespace) + +(import_statement (version_number) @float) + +(float) @float +(number) @number + +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/folds.scm index 9ef083e..80c3039 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/folds.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/folds.scm @@ -13,6 +13,7 @@ (preproc_else) (preproc_ifdef) (initializer_list) - (compound_statement) ] @fold + (compound_statement + (compound_statement) @fold) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/highlights.scm index 6fdc05d..5948263 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/highlights.scm @@ -1,4 +1,5 @@ -(identifier) @variable +; Lower priority to prefer @parameter when identifier appears in parameter_declaration. +((identifier) @variable (#set! "priority" 95)) [ "const" @@ -42,10 +43,16 @@ "#elif" "#endif" (preproc_directive) -] @keyword +] @preproc "#include" @include +[ ";" ":" "," ] @punctuation.delimiter + +"..." @punctuation.special + +[ "(" ")" "[" "]" "{" "}"] @punctuation.bracket + [ "=" @@ -63,6 +70,7 @@ ">>" "->" + "." "<" "<=" @@ -89,20 +97,17 @@ "++" ] @operator +;; Make sure the comma operator is given a highlight group after the comma +;; punctuator so the operator is highlighted properly. +(comma_expression [ "," ] @operator) + [ (true) (false) ] @boolean -[ "." ";" ":" "," ] @punctuation.delimiter - -"..." @punctuation.special - (conditional_expression [ "?" ":" ] @conditional) - -[ "(" ")" "[" "]" "{" "}"] @punctuation.bracket - (string_literal) @string (system_lib_string) @string (escape_sequence) @string.escape @@ -120,6 +125,7 @@ (field_identifier) @property)) @_parent (#not-has-parent? @_parent template_method function_declarator call_expression)) +(field_designator) @property (((field_identifier) @property) (#has-ancestor? @property field_declaration) (#not-has-ancestor? @property function_declarator)) @@ -137,6 +143,13 @@ ((identifier) @constant (#lua-match? @constant "^[A-Z][A-Z0-9_]+$")) +(enumerator + name: (identifier) @constant) +(case_statement + value: (identifier) @constant) + +((identifier) @constant.builtin + (#any-of? @constant.builtin "stderr" "stdin" "stdout")) ;; Preproc def / undef (preproc_def @@ -147,16 +160,16 @@ (#eq? @_u "#undef")) (call_expression - function: (identifier) @function) + function: (identifier) @function.call) (call_expression function: (field_expression - field: (field_identifier) @function)) + field: (field_identifier) @function.call)) (function_declarator declarator: (identifier) @function) (preproc_function_def name: (identifier) @function.macro) -(comment) @comment +(comment) @comment @spell ;; Parameters (parameter_declaration @@ -178,6 +191,7 @@ "_unaligned" "__unaligned" "__declspec" + (attribute_declaration) ] @attribute (ERROR) @error diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/indents.scm index 959678f..1931471 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c/indents.scm @@ -1,47 +1,49 @@ [ - (init_declarator) (compound_statement) (preproc_arg) (field_declaration_list) (case_statement) - (conditional_expression) (enumerator_list) (struct_specifier) (compound_literal_expression) - (parameter_list) (initializer_list) - (concatenated_string) (while_statement) (for_statement) (switch_statement) + (expression_statement) ] @indent (if_statement condition: (_) @indent) ((if_statement consequence: (_) @_consequence (#not-has-type? @_consequence compound_statement) ) @indent) -(init_declarator - value: [ - (binary_expression) - ] @indent) +(init_declarator) @indent (compound_statement "}" @indent_end) [ + "else" + ")" + "}" + (statement_identifier) +] @branch + +[ "#define" "#ifdef" "#if" "#else" - "else" "#endif" - ")" - "}" -] @branch +] @zero_indent [ - (comment) (preproc_arg) (string_literal) ] @ignore -(binary_expression) @auto +((ERROR (parameter_declaration)) @aligned_indent + (#set! "delimiter" "()")) +([(argument_list) (parameter_list)] @aligned_indent + (#set! "delimiter" "()")) + +(comment) @auto diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c_sharp/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c_sharp/highlights.scm index fc3600e..ca327c5 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/c_sharp/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/c_sharp/highlights.scm @@ -20,12 +20,12 @@ (invocation_expression (member_access_expression - name: (identifier) @method)) + name: (identifier) @method.call)) (invocation_expression function: (conditional_access_expression (member_binding_expression - name: (identifier) @method))) + name: (identifier) @method.call))) (namespace_declaration name: [(qualified_name) (identifier)] @namespace) @@ -34,7 +34,7 @@ (identifier) @type) (invocation_expression - (identifier) @method) + (identifier) @method.call) (field_declaration (variable_declaration @@ -74,7 +74,7 @@ (implicit_type) @keyword -(comment) @comment +(comment) @comment @spell (using_directive (identifier) @type) @@ -141,7 +141,7 @@ ; Generic Method invocation with generic type (invocation_expression function: (generic_name - . (identifier) @method)) + . (identifier) @method.call)) (invocation_expression (member_access_expression diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/clojure/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/clojure/highlights.scm index d6371c5..3dc12d5 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/clojure/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/clojure/highlights.scm @@ -42,10 +42,10 @@ ; General function calls (list_lit . - (sym_lit) @function) + (sym_lit) @function.call) (anon_fn_lit . - (sym_lit) @function) + (sym_lit) @function.call) ; Quoted symbols (quoting_lit @@ -106,7 +106,10 @@ ; Definition functions ((sym_lit) @keyword - (#lua-match? @keyword "^def.*$")) + (#any-of? @keyword + "def" "defonce" "defrecord" "defmacro" "definline" + "defmulti" "defmethod" "defstruct" "defprotocol" + "deftype")) ((sym_lit) @keyword (#eq? @keyword "declare")) ((sym_lit) @keyword.function @@ -278,19 +281,22 @@ ;; >> Context based highlighting -; def-likes -; Correctly highlight docstrings -(list_lit - . - (sym_lit) @_keyword ; Don't really want to highlight twice - (#lua-match? @_keyword "^def.*") - . - (sym_lit) - . - ;; TODO: Add @comment highlight - (str_lit)? - . - (_)) +;; def-likes +;; Correctly highlight docstrings +;(list_lit + ;. + ;(sym_lit) @_keyword ; Don't really want to highlight twice + ;(#any-of? @_keyword + ;"def" "defonce" "defrecord" "defmacro" "definline" + ;"defmulti" "defmethod" "defstruct" "defprotocol" + ;"deftype") + ;. + ;(sym_lit) + ;. + ;;; TODO: Add @comment highlight + ;(str_lit)? + ;. + ;(_)) ; Function definitions (list_lit diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cmake/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cmake/highlights.scm index d1f5996..3378480 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cmake/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cmake/highlights.scm @@ -9,7 +9,7 @@ [ (bracket_comment) (line_comment) -] @comment +] @comment @spell (normal_command (identifier) @function) @@ -51,17 +51,42 @@ ) (normal_command - (identifier) @function.builtin - . (argument) @variable - (#match? @function.builtin "\\c^(set)$")) + (identifier) @function.builtin + . (argument) @variable + (#match? @function.builtin "\\c^(set)$") +) (normal_command - (identifier) @function.builtin - . (argument) - . (argument) - (argument) @constant - (#any-of? @constant "PARENT_SCOPE" "CACHE" "FORCE") - (#match? @function.builtin "\\c^(set)$") + (identifier) @function.builtin + (#match? @function.builtin "\\c^(set)$") + ( + (argument) @constant + (#any-of? @constant "PARENT_SCOPE") + ) . +) + +(normal_command + (identifier) @function.builtin + (#match? @function.builtin "\\c^(set)$") + . (argument) + ( + (argument) @_cache @constant + . + (argument) @_type @constant + (#any-of? @_cache "CACHE") + (#any-of? @_type "BOOL" "FILEPATH" "PATH" "STRING" "INTERNAL") + ) +) +(normal_command + (identifier) @function.builtin + (#match? @function.builtin "\\c^(set)$") + . (argument) + (argument) @_cache + (#any-of? @_cache "CACHE") + ( + (argument) @_force @constant + (#any-of? @_force "FORCE") + ) . ) ((argument) @boolean @@ -98,3 +123,7 @@ (#match? @function.builtin "\\c^(add_custom_command)$") ) +(escape_sequence) @string.escape + +((source_file . (line_comment) @preproc) + (#match? @preproc "^#!/")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/comment/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/comment/highlights.scm index 6c3e043..e516c60 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/comment/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/comment/highlights.scm @@ -1,3 +1,5 @@ +(_) @spell + [ "(" ")" @@ -7,6 +9,12 @@ (tag (name) @text.note (user)? @constant) +((tag ((name) @text.note)) + (#any-of? @text.note "NOTE")) + +("text" @text.note + (#any-of? @text.note "NOTE")) + ((tag ((name) @text.warning)) (#any-of? @text.warning "TODO" "HACK" "WARNING")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cooklang/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cooklang/highlights.scm new file mode 100644 index 0000000..4ced465 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cooklang/highlights.scm @@ -0,0 +1,22 @@ +(metadata) @comment + +(ingredient + "@" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) + +(timer + "~" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) + +(cookware + "#" @tag + (name)? @text.title + (amount + (quantity)? @number + (units)? @tag.attribute)?) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/highlights.scm index c991b95..fb7ee2e 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/highlights.scm @@ -38,6 +38,8 @@ (#lua-match? @type "^[A-Z]")) ((namespace_identifier) @constant (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) +(case_statement + value: (qualified_identifier (identifier) @constant)) (namespace_definition name: (identifier) @namespace) @@ -49,21 +51,36 @@ (function_declarator declarator: (qualified_identifier name: (identifier) @function)) +(function_declarator + declarator: (qualified_identifier + name: (qualified_identifier + name: (identifier) @function))) ((function_declarator declarator: (qualified_identifier name: (identifier) @constructor)) (#lua-match? @constructor "^[A-Z]")) (operator_name) @function +"operator" @function "static_assert" @function.builtin (call_expression function: (qualified_identifier - name: (identifier) @function)) + name: (identifier) @function.call)) +(call_expression + function: (qualified_identifier + name: (qualified_identifier + name: (identifier) @function.call))) +(call_expression + function: + (qualified_identifier + name: (qualified_identifier + name: (qualified_identifier + name: (identifier) @function.call)))) (call_expression function: (field_expression - field: (field_identifier) @function)) + field: (field_identifier) @function.call)) ((call_expression function: (identifier) @constructor) @@ -141,27 +158,21 @@ "new" "delete" - ;; these keywords are not supported by the parser - ;"eq" - ;"not_eq" - ; - ;"compl" - ;"and" - ;"or" - ; - ;"bitand" - ;"bitand_eq" - ;"bitor" - ;"bitor_eq" - ;"xor" - ;"xor_eq" + "xor" + "bitand" + "bitor" + "compl" + "not" + "xor_eq" + "and_eq" + "or_eq" + "not_eq" + "and" + "or" ] @keyword.operator -[ - "<=>" - "::" -] @operator +"<=>" @operator -(attribute_declaration) @attribute +"::" @punctuation.delimiter (literal_suffix) @operator diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/indents.scm index cb02779..f1007dd 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cpp/indents.scm @@ -5,4 +5,6 @@ (condition_clause) ] @indent +((field_initializer_list) @indent + (#set! "start_at_same_line" 1)) (access_specifier) @branch diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/highlights.scm index d8439d6..18bf7f9 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/highlights.scm @@ -11,7 +11,7 @@ (important) ] @keyword -(comment) @comment +(comment) @comment @spell [ (tag_name) @@ -42,7 +42,7 @@ (attribute_selector (plain_value) @string) -(pseudo_element_selector (tag_name) @property) +(pseudo_element_selector "::" (tag_name) @property) (pseudo_class_selector (class_name) @property) [ diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/indents.scm index 8b72502..1ea8a33 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/css/indents.scm @@ -3,8 +3,7 @@ (declaration) ] @indent -[ - "}" -] @branch +(block ("}") @branch) +("}") @dedent (comment) @ignore diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cuda/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cuda/highlights.scm index 404c72a..7897c41 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/cuda/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/cuda/highlights.scm @@ -11,4 +11,5 @@ "__forceinline__" "__restrict__" "__launch_bounds__" + "__grid_constant__" ] @keyword diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/d/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/d/highlights.scm index d7787ce..9a0887f 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/d/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/d/highlights.scm @@ -16,6 +16,7 @@ "," ";" "." + ":" ] @punctuation.delimiter [ diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/diff/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/diff/highlights.scm new file mode 100644 index 0000000..4b9cbad --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/diff/highlights.scm @@ -0,0 +1,6 @@ +[(addition) (new_file)] @text.diff.add +[(deletion) (old_file)] @text.diff.delete + +(commit) @constant +(location) @attribute +(command) @function diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/dockerfile/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/dockerfile/highlights.scm index af1c088..2d1840e 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/dockerfile/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/dockerfile/highlights.scm @@ -26,7 +26,7 @@ "@" ] @operator -(comment) @comment +(comment) @comment @spell (image_spec (image_tag @@ -47,4 +47,17 @@ ((variable) @constant (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) +(arg_instruction + . (unquoted_string) @property) +(env_instruction + (env_pair . (unquoted_string) @property)) + +(expose_instruction + (expose_port) @number) + +((stopsignal_instruction) @number + (#match? @number "[0-9][0-9]?$")) + +((stopsignal_instruction) @constant.builtin + (#match? @constant.builtin "SIG(ABRT|HUP|INT|KILL|QUIT|STOP|TERM|TSTP)$")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/dot/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/dot/highlights.scm index 86bb9b3..d8b70a9 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/dot/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/dot/highlights.scm @@ -1,5 +1,14 @@ (identifier) @type -(keyword) @keyword + +[ + "strict" + "graph" + "digraph" + "subgraph" + "node" + "edge" +] @keyword + (string_literal) @string (number_literal) @number @@ -37,9 +46,10 @@ (identifier) @constant) ) -[ -(comment) -(preproc) -] @comment +(comment) @comment + +(preproc) @preproc + +(comment) @spell (ERROR) @error diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/highlights.scm index 7334bd1..aa490b3 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/highlights.scm @@ -83,11 +83,11 @@ ;-------------------------- (call_expression - function: (identifier) @function) + function: (identifier) @function.call) (call_expression function: (member_expression - property: [(property_identifier) (private_property_identifier)] @method)) + property: [(property_identifier) (private_property_identifier)] @method.call)) ; Variables ;---------- @@ -97,22 +97,36 @@ ; Literals ;--------- -(this) @variable.builtin -(super) @variable.builtin +[ + (this) + (super) +] @variable.builtin -(true) @boolean -(false) @boolean -(null) @constant.builtin [ -(comment) -(hash_bang_line) -] @comment -(string) @string -(regex) @punctuation.delimiter -(regex_pattern) @string.regex + (true) + (false) +] @boolean + +[ + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +(hash_bang_line) @preproc + +(comment) @spell + +(string) @string @spell (template_string) @string (escape_sequence) @string.escape +(regex_pattern) @string.regex +(regex "/" @punctuation.bracket) ; Regex delimiters + (number) @number +((identifier) @number + (#any-of? @number "NaN" "Infinity")) ; Punctuation ;------------ @@ -122,9 +136,9 @@ ";" @punctuation.delimiter "." @punctuation.delimiter "," @punctuation.delimiter -"?." @punctuation.delimiter (pair ":" @punctuation.delimiter) +(pair_pattern ":" @punctuation.delimiter) [ "--" diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/indents.scm index 6630562..902bf58 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/indents.scm @@ -5,10 +5,10 @@ (class_body) (export_clause) (formal_parameters) - (method_definition) (named_imports) (object) (object_pattern) + (parenthesized_expression) (return_statement) (statement_block) (switch_case) @@ -45,7 +45,11 @@ ] @branch (statement_block "{" @branch) +["}" "]"] @indent_end + [ (comment) (template_string) ] @ignore + +(ERROR) @auto diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/injections.scm index f97f9cd..6bd5da8 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/injections.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/injections.scm @@ -1,4 +1,6 @@ -(comment) @jsdoc +(((comment) @_jsdoc_comment + (#match? @_jsdoc_comment "^/\\*\\*[^\\*].*\\*/")) @jsdoc) + (comment) @comment (call_expression @@ -18,6 +20,8 @@ arguments: ((template_string) @glimmer (#offset! @glimmer 0 1 0 -1))) +((glimmer_template) @glimmer) + ; styled.div`` (call_expression function: (member_expression @@ -56,3 +60,10 @@ (#offset! @css 0 1 0 -1))) (regex_pattern) @regex + +((comment) @_gql_comment + (#eq? @_gql_comment "/* GraphQL */") + (template_string) @graphql) + +(((template_string) @_template_string + (#match? @_template_string "^`#graphql")) @graphql) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/locals.scm index 08c9b8e..4c035d2 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/locals.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/ecma/locals.scm @@ -23,7 +23,11 @@ (identifier) @definition.import) (function_declaration - ((identifier) @definition.var) + ((identifier) @definition.function) + (#set! definition.var.scope parent)) + +(method_definition + ((property_identifier) @definition.function) (#set! definition.var.scope parent)) ; References diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/highlights.scm index db9bfbb..fdb92ef 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/highlights.scm @@ -30,9 +30,11 @@ ; Comments (comment) @comment +(comment) @spell ; Strings (string) @string +(string) @spell ; Modules (alias) @type @@ -84,13 +86,13 @@ (stab_clause operator: _ @operator) ; Local Function Calls -(call target: (identifier) @function) +(call target: (identifier) @function.call) ; Remote Function Calls (call target: (dot left: [ (atom) @type (_) -] right: (identifier) @function) (arguments)) +] right: (identifier) @function.call) (arguments)) ; Definition Function Calls (call target: ((identifier) @keyword.function (#any-of? @keyword.function diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/indents.scm index 2c14192..517f669 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elixir/indents.scm @@ -1,18 +1,23 @@ [ - (arguments) + (block) (do_block) (list) (map) + (stab_clause) (tuple) + (arguments) ] @indent [ ")" "]" - "end" + "after" + "catch" + "else" + "rescue" "}" - (after_block) - (else_block) - (rescue_block) - (catch_block) -] @branch + "end" +] @indent_end @branch + +; Elixir pipelines are not indented, but other binary operator chains are +((binary_operator operator: _ @_operator) @indent (#not-eq? @_operator "|>")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/highlights.scm new file mode 100644 index 0000000..ab0ce8c --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/highlights.scm @@ -0,0 +1,70 @@ +(comment) @comment + +["if" "elif"] @conditional +(if (else "else" @conditional)) + +["while" "for"] @repeat +(while (else "else" @repeat)) +(for (else "else" @repeat)) + +["try" "catch" "finally"] @exception +(try (else "else" @exception)) + +"use" @include +(import (bareword) @string.special) + +["*" "**" "?"] @string.special + +(command argument: (bareword) @parameter) +(command head: (identifier) @function.call) +((command head: (identifier) @keyword.return) + (#eq? @keyword.return "return")) +((command (identifier) @keyword.operator) + (#any-of? @keyword.operator "and" "or" "coalesce")) +[ + "+" "-" "*" "/" "%" "<" "<=""==" "!=" ">" + ">=" "s" ">=s" +] @function.builtin + +[">" "<" ">>" "<>" "|"] @operator + +(io_port) @number + +(function_definition + "fn" @keyword.function + (identifier) @function) + +(parameter_list) @parameter +(parameter_list "|" @punctuation.bracket) + +["var" "set" "tmp" "del"] @keyword +(variable_declaration + (lhs (identifier) @variable)) + +(variable_assignment + (lhs (identifier) @variable)) + +(temporary_assignment + (lhs (identifier) @variable)) + +(variable_deletion + (identifier) @variable) + + +(number) @number +(string) @string + +(variable (identifier) @variable) +((variable (identifier) @function) + (#match? @function ".+\\~$")) +((variable (identifier) @boolean) + (#any-of? @boolean "true" "false")) +((variable (identifier) @constant.builtin) + (#any-of? @constant.builtin + "_" "after-chdir" "args" "before-chdir" "buildinfo" "nil" + "notify-bg-job-success" "num-bg-jobs" "ok" "paths" "pid" + "pwd" "value-out-indicator" "version")) + +["$" "@"] @punctuation.special +["(" ")" "[" "]" "{" "}"] @punctuation.bracket +";" @punctuation.delimiter diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/injections.scm new file mode 100644 index 0000000..4bb7d67 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/elvish/injections.scm @@ -0,0 +1 @@ +(comment) @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/highlights.scm new file mode 100644 index 0000000..0bf76a7 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/highlights.scm @@ -0,0 +1,12 @@ +(comment_directive) @comment + +[ + "<%#" + "<%" + "<%=" + "<%_" + "<%-" + "%>" + "-%>" + "_%>" +] @keyword diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/injections.scm new file mode 100644 index 0000000..d55c87e --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/embedded_template/injections.scm @@ -0,0 +1,2 @@ +(content) @html @combined +(code) @ruby @combined diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/folds.scm new file mode 100644 index 0000000..39c65c5 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/folds.scm @@ -0,0 +1,7 @@ +[ + (function_declaration) + (lambda_clause) + (expr_case) + (map) + (module_export) +] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/highlights.scm new file mode 100644 index 0000000..c51019f --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/erlang/highlights.scm @@ -0,0 +1,104 @@ +;; keywoord +[ + "fun" + "div" +] @keyword +;; bracket +[ + "(" + ")" + "{" + "}" + "[" + "]" + "#" +] @punctuation.bracket +;; conditional +[ + "receive" + "if" + "case" + "of" + "when" + "after" + "end" +] @conditional + +[ + "catch" + "try" + "throw" +] @exception +;;; module define +[ + "module" + "export" +] @include +;;; operator +[ + ":" + ":=" + "?" + "!" + "-" + "+" + "=" + "->" + "=>" + "|" + ;;;TODO + "$" + ] @operator + +(comment) @comment +(string) @string +(variable) @variable + +(module_name + (atom) @namespace +) +;;; expr_function_call +(expr_function_call + name: (computed_function_name) @function.call +) + +(expr_function_call + arguments: (atom) @variable +) + +;;; map +(map + (map_entry [ + (atom) + (variable) + ] @variable) +) + + +(tuple (atom) @variable) +(pat_tuple ( pattern (atom) @variable)) + +(computed_function_name) @function +;;; case +(case_clause + pattern: (pattern + (atom) @variable + ) +) +(case_clause + body: (atom) @variable +) + +;;; function +(qualified_function_name + module_name: (atom) @attribute + function_name: (atom) @function +) +;; function +(function_clause + name: (atom) @function) +;;;lambda +(lambda_clause + arguments: + (pattern) @variable +) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/highlights.scm index 48a3399..7322e55 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/highlights.scm @@ -93,10 +93,16 @@ (#any-of? @include "require" "require-macros" "import-macros" "include")) +[ + "collect" + "icollect" + "accumulate" +] @function.macro + ((symbol) @function.macro (#any-of? @function.macro - "->" "->>" "-?>" "-?>>" "?." "accumulate" "collect" "doto" "icollect" - "macro" "macrodebug" "partial" "pick-args" "pick-values" "with-open")) + "->" "->>" "-?>" "-?>>" "?." "doto" "macro" "macrodebug" "partial" "pick-args" + "pick-values" "with-open")) ; Lua builtins ((symbol) @constant.builtin diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/locals.scm index 31e5203..3018732 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/locals.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fennel/locals.scm @@ -14,8 +14,10 @@ "while" "if" "when" "do" "collect" "icollect" "accumulate") ) -(fn name: (symbol) @definition.function) -(lambda name: (symbol) @definition.function) +(fn name: (symbol) @definition.function + (#set! definition.function.scope "parent")) +(lambda name: (symbol) @definition.function + (#set! definition.function.scope "parent")) ; TODO: use @definition.parameter for parameters (binding (symbol) @definition.var) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fish/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fish/highlights.scm index fc5338b..10d54df 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/fish/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/fish/highlights.scm @@ -103,8 +103,10 @@ ] ) +(command_substitution_dollar "$" @punctuation.bracket) + ; non-bultin command names -(command name: (word) @function) +(command name: (word) @function.call) ; derived from builtin -n (fish 3.2.2) (command @@ -147,11 +149,15 @@ [(integer) (float)] @number (comment) @comment +(comment) @spell (test_option) @string ((word) @boolean (#any-of? @boolean "true" "false")) +((program . (comment) @preproc) + (#match? @preproc "^#!/")) + ;; Error (ERROR) @error diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/highlights.scm new file mode 100644 index 0000000..466bd2f --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/highlights.scm @@ -0,0 +1,8 @@ +((command) @keyword + (label)? @constant + (message)? @text @spell) + +(option) @operator + +(comment) @comment + diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/injections.scm new file mode 100644 index 0000000..2738861 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/git_rebase/injections.scm @@ -0,0 +1,5 @@ +((operation + (command) @_command + (message) @bash) +(#any-of? @_command "exec" "x")) + diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/highlights.scm new file mode 100644 index 0000000..ecd4109 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/highlights.scm @@ -0,0 +1,53 @@ +(dir_sep) @punctuation.delimiter + +(wildcard) @punctuation.special + +(quoted_pattern + ("\"" @character.special)) + +(range_notation) @string.special + +(range_notation + [ "[" "]" ] @punctuation.bracket) + +(range_negation) @operator + +(character_class) @constant + +(class_range ("-" @operator)) + +[ + (ansi_c_escape) + (escaped_char) +] @string.escape + +(attribute + (attr_name) @parameter) + +(attribute + (builtin_attr) @variable.builtin) + +[ + (attr_reset) + (attr_unset) + (attr_set) +] @operator + +(boolean_value) @boolean + +(string_value) @string + +(macro_tag) @preproc + +(macro_def + macro_name: (_) @property) + +[ + (pattern_negation) + (redundant_escape) + (trailing_slash) +] @error + +(ERROR) @error + +(comment) @comment @spell diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/injections.scm new file mode 100644 index 0000000..4bb7d67 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitattributes/injections.scm @@ -0,0 +1 @@ +(comment) @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitignore/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitignore/highlights.scm new file mode 100644 index 0000000..6e83ea6 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gitignore/highlights.scm @@ -0,0 +1,31 @@ +(comment) @comment @spell + +[ + (directory_separator) + (directory_separator_escaped) +] @punctuation.delimiter + +[ + (wildcard_char_single) + (wildcard_chars) + (wildcard_chars_allow_slash) + (bracket_negation) +] @operator + +(negation) @punctuation.special + +[ + (pattern_char_escaped) + (bracket_char_escaped) +] @string.escape + +;; bracket expressions +[ + "[" + "]" +] @punctuation.bracket + +(bracket_char) @constant +(bracket_range + "-" @operator) +(bracket_char_class) @constant.builtin diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/folds.scm new file mode 100644 index 0000000..ff05eeb --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/folds.scm @@ -0,0 +1,11 @@ +; Folds +[ + (case) + (expression_group) + (function) + (public_function) + (anonymous_function) + (type_definition) + (public_type_definition) + (public_opaque_type_definition) +] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/highlights.scm new file mode 100644 index 0000000..e1dc969 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/highlights.scm @@ -0,0 +1,163 @@ +; Keywords +[ + "as" + "const" + "external" + "let" + "opaque" + "pub" + "todo" + "try" +] @keyword + +; Function Keywords +[ + "fn" + "type" +] @keyword.function + +; Imports +[ + "import" +] @include + +; Conditionals +[ + "case" + "if" +] @conditional + +; Exceptions +[ + "assert" +] @exception + +; Punctuation +[ + "(" + ")" + "<<" + ">>" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "," + "." +] @punctuation.delimiter + +[ + "#" +] @punctuation.special + +; Operators +[ + "%" + "&&" + "*" + "*." + "+" + "+." + "-" + "-." + "->" + ".." + "/" + "/." + ":" + "<" + "<." + "<=" + "<=." + "=" + "==" + ">" + ">." + ">=" + ">=." + "|>" + "||" +] @operator + +; Identifiers +(identifier) @variable + +; Comments +[ + (module_comment) + (statement_comment) + (comment) +] @comment + +; Unused Identifiers +[ + (discard) + (hole) +] @comment + +; Modules & Imports +(module ("/" @namespace)?) @namespace +(import alias: ((identifier) @namespace)?) +(remote_type_identifier module: (identifier) @namespace) +(unqualified_import name: (identifier) @function) + +; Strings +(string) @string + +; Bit Strings +(bit_string_segment) @string.special + +; Numbers +[ + (integer) + (float) + (bit_string_segment_option_unit) +] @number + +; Function Parameter Labels +(function_call arguments: (arguments (argument label: (label) @symbol ":" @symbol))) +(function_parameter label: (label)? @symbol name: (identifier) @parameter (":" @parameter)?) + +; Records +(record arguments: (arguments (argument label: (label) @property ":" @property)?)) +(record_pattern_argument label: (label) @property ":" @property) +(record_update_argument label: (label) @property ":" @property) +(field_access record: (identifier) @variable field: (label) @property) + +; Type Constructors +(data_constructor_argument label: (label) @property ":" @property) + +; Type Parameters +(type_parameter) @parameter + +; Types +((type_identifier) @type (#not-any-of? @type "True" "False")) + +; Booleans +((type_identifier) @boolean (#any-of? @boolean "True" "False")) + +; Type Variables +(type_var) @type + +; Tuples +(tuple_access index: (integer) @operator) + +; Functions +(function name: (identifier) @function) +(public_function name: (identifier) @function) +(function_call function: (identifier) @function) +(function_call function: (field_access field: (label) @function)) + +; External Functions +(public_external_function name: (identifier) @function) +(external_function name: (identifier) @function) +(external_function_body (string) @namespace . (string) @function) + +; Pipe Operator +(binary_expression operator: "|>" right: (identifier) @function) + +; Parser Errors +(ERROR) @error diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/indents.scm new file mode 100644 index 0000000..e2259b6 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/indents.scm @@ -0,0 +1,33 @@ +; Gleam indents similar to Rust and JavaScript +[ + (assert) + (case) + (case_clause) + (constant) + (expression_group) + (external_function) + (function) + (import) + (let) + (list) + (public_constant) + (public_external_function) + (public_function) + (public_opaque_type_definition) + (public_type_alias) + (public_type_definition) + (todo) + (try) + (tuple) + (type_alias) + (type_definition) +] @indent + +[ + ")" + "]" + "}" +] @indent_end @branch + +; Gleam pipelines are not indented, but other binary expression chains are +((binary_expression operator: _ @_operator) @indent (#not-eq? @_operator "|>")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/injections.scm new file mode 100644 index 0000000..ed4c2de --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/injections.scm @@ -0,0 +1,6 @@ +; Comments +[ + (module_comment) + (statement_comment) + (comment) +] @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/locals.scm new file mode 100644 index 0000000..8872e51 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/gleam/locals.scm @@ -0,0 +1,24 @@ +; Let Binding Definition +(let pattern: (identifier) @definition) + +; List Pattern Definitions +(list_pattern (identifier) @definition) +(list_pattern assign: (identifier) @definition) + +; Tuple Pattern Definition +(tuple_pattern (identifier) @definition) + +; Record Pattern Definition +(record_pattern_argument pattern: (identifier) @definition) + +; Function Parameter Definition +(function_parameter name: (identifier) @definition) + +; References +(identifier) @reference + +; Function Body Scope +(function_body) @scope + +; Case Scope +(case_clause) @scope diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/folds.scm index c79d457..e1b7a83 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/folds.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/folds.scm @@ -9,5 +9,8 @@ (method_declaration) (type_declaration) (var_declaration) + (composite_literal) + (literal_element) + (block) ] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/highlights.scm index 62f6842..aaa6fe6 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/highlights.scm @@ -7,7 +7,7 @@ (type_identifier) @type (field_identifier) @property (identifier) @variable -(package_identifier) @variable +(package_identifier) @namespace (parameter_declaration (identifier) @parameter) (variadic_parameter_declaration (identifier) @parameter) @@ -15,20 +15,17 @@ ((identifier) @constant (#eq? @constant "_")) -((identifier) @constant - (#vim-match? @constant "^[A-Z][A-Z\\d_]+$")) - (const_spec name: (identifier) @constant) ; Function calls (call_expression - function: (identifier) @function) + function: (identifier) @function.call) (call_expression function: (selector_expression - field: (field_identifier) @method)) + field: (field_identifier) @method.call)) ; Function definitions @@ -38,6 +35,9 @@ (method_declaration name: (field_identifier) @method) +(method_spec + name: (field_identifier) @method) + ; Operators [ @@ -123,6 +123,7 @@ ((type_identifier) @type.builtin (#any-of? @type.builtin + "any" "bool" "byte" "complex128" @@ -184,7 +185,7 @@ ; Literals (interpreted_string_literal) @string -(raw_string_literal) @string +(raw_string_literal) @string @spell (rune_literal) @string (escape_sequence) @string.escape @@ -196,6 +197,16 @@ (false) @boolean (nil) @constant.builtin -(comment) @comment +(keyed_element + . (literal_element (identifier) @field)) +(field_declaration name: (field_identifier) @field) + +(comment) @comment @spell (ERROR) @error + +((interpreted_string_literal) @spell + (#not-has-parent? @spell + import_spec + ) +) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/indents.scm index 3d9b928..c6ac189 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/go/indents.scm @@ -1,24 +1,31 @@ [ (import_declaration) - (function_declaration) (const_declaration) (var_declaration) (type_declaration) - (composite_literal) (func_literal) (literal_value) (expression_case) - (argument_list) (default_case) (block) + (call_expression) + (parameter_list) + (struct_type) ] @indent [ - "case" - "(" - ")" - "{" "}" ] @branch +(const_declaration ")" @branch) +(import_spec_list ")" @branch) +(var_declaration ")" @branch) + +[ + "}" + ")" +] @indent_end + +(parameter_list ")" @branch) + (comment) @ignore diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hack/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hack/highlights.scm index c7e481a..0ae9a9b 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hack/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hack/highlights.scm @@ -53,7 +53,7 @@ [ "shape" - "tupe" + "tuple" (array_type) "bool" "float" @@ -177,14 +177,14 @@ (variable) @parameter) (call_expression - function: (qualified_identifier (identifier) @function .)) + function: (qualified_identifier (identifier) @function.call .)) (call_expression - function: (scoped_identifier (identifier) @function .)) + function: (scoped_identifier (identifier) @function.call .)) (call_expression function: (selection_expression - (qualified_identifier (identifier) @method .))) + (qualified_identifier (identifier) @method.call .))) (qualified_identifier (_) @namespace . diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/folds.scm new file mode 100644 index 0000000..28eadf8 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/folds.scm @@ -0,0 +1,5 @@ +[ + (exp_apply) + (exp_do) + (function) +] @fold diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/highlights.scm index 03744f5..c37c33b 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/highlights.scm @@ -81,6 +81,7 @@ "in" "class" "instance" + "pattern" "data" "newtype" "family" @@ -116,8 +117,9 @@ (exp_infix (variable) @operator) ; consider infix functions as operators -(exp_apply . (exp_name (variable) @function)) -(exp_apply . (exp_name (qualified_variable (variable) @function))) +(exp_infix (exp_name) @function.call (#set! "priority" 101)) +(exp_apply . (exp_name (variable) @function.call)) +(exp_apply . (exp_name (qualified_variable (variable) @function.call))) ;; ---------------------------------------------------------------------------- @@ -135,5 +137,12 @@ ;; ---------------------------------------------------------------------------- ;; Quasi-quotes -(quoter) @function +(quoter) @function.call ; Highlighting of quasiquote_body is handled by injections.scm + +;; ---------------------------------------------------------------------------- +;; Spell checking + +(string) @spell +(comment) @spell + diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/injections.scm index 0b231a7..206248f 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/injections.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/haskell/injections.scm @@ -56,3 +56,23 @@ (#eq? @_name "hsx") ((quasiquote_body) @html) ) + +;; ----------------------------------------------------------------------------- +;; Inline JSON from aeson + +(quasiquote + (quoter) @_name + (#eq? @_name "aesonQQ") + ((quasiquote_body) @json) +) + + +;; ----------------------------------------------------------------------------- +;; SQL + +; postgresql-simple +(quasiquote + (quoter) @_name + (#eq? @_name "sql") + ((quasiquote_body) @sql) +) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hcl/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hcl/highlights.scm index 67e85cb..6583692 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hcl/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hcl/highlights.scm @@ -63,8 +63,8 @@ ] @string [ - (heredoc_identifier) ; <" "--%>" @@ -13,20 +13,19 @@ "<%%=" "<%=" "" "{" "}" ] @tag.delimiter -; HEEx operators +; HEEx operators are highlighted as such "=" @operator ; HEEx inherits the DOCTYPE tag from HTML (doctype) @constant -; HEEx tags are highlighted as HTML tags -(tag_name) @tag - ; HEEx comments are highlighted as such (comment) @comment @@ -36,6 +35,12 @@ ; Tree-sitter parser errors (ERROR) @error +; HEEx tags and slots are highlighted as HTML +[ + (tag_name) + (slot_name) +] @tag + ; HEEx attributes are highlighted as HTML attributes (attribute_name) @tag.attribute [ diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/indents.scm index e2a0871..99fc693 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/indents.scm @@ -1,11 +1,20 @@ -; HEEx indents like HTML +; HEEx tags, components, and slots indent like HTML [ (component) + (slot) (tag) ] @indent -; Dedent at the end of each tag +; Dedent at the end of each tag, component, and slot [ - (end_tag) (end_component) -] @branch + (end_slot) + (end_tag) +] @branch @dedent + +; Self-closing tags and components should not change +; indentation level of sibling nodes +[ + (self_closing_component) + (self_closing_tag) +] @auto diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/injections.scm index af99e68..fffd1dc 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/injections.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/injections.scm @@ -7,4 +7,5 @@ ; HEEx Elixir expressions are always within a tag or component (expression (expression_value) @elixir) +; HEEx comments (comment) @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/locals.scm index 3f99132..4371bc9 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/locals.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/heex/locals.scm @@ -1,11 +1,13 @@ -; HEEx tags and components are references +; HEEx tags, components, and slots are references [ - (tag_name) (component_name) + (slot_name) + (tag_name) ] @reference -; Create a new scope within each HEEx tag or component +; Create a new scope within each HEEx tag, component, and slot [ - (tag) (component) + (slot) + (tag) ] @scope diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/help/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/help/highlights.scm new file mode 100644 index 0000000..b2ed390 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/help/highlights.scm @@ -0,0 +1,25 @@ +(h1) @text.title +(h2) @text.title +(h3) @text.title +(column_heading) @text.title +(column_heading + "~" @conceal (#set! conceal "")) +(tag + "*" @conceal (#set! conceal "") + text: (_) @label) +(taglink + "|" @conceal (#set! conceal "") + text: (_) @text.reference) +(optionlink + text: (_) @text.reference) +(codespan + "`" @conceal (#set! conceal "") + text: (_) @text.literal) +(codeblock) @text.literal +(codeblock + ">" @conceal (#set! conceal "")) +(block + "<" @conceal (#set! conceal "")) +(argument) @parameter +(keycode) @string.special +(url) @text.uri diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/folds.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/folds.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/folds.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/highlights.scm new file mode 100644 index 0000000..b93a90d --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/highlights.scm @@ -0,0 +1,35 @@ +; inherits: cpp + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "groupshared" + "discard" + "cbuffer" + "row_major" + "column_major" + "globallycoherent" + "centroid" + "noperspective" + "nointerpolation" + "sample" + "linear" + "snorm" + "unorm" + "point" + "line" + "triangleadj" + "lineadj" + "triangle" +] @keyword + +( + (identifier) @variable.builtin + (#lua-match? @variable.builtin "^SV_") +) + +(hlsl_attribute) @attribute +(hlsl_attribute ["[" "]"] @attribute) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/indents.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/indents.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/injections.scm new file mode 100644 index 0000000..537ae54 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/injections.scm @@ -0,0 +1,3 @@ +(preproc_arg) @hlsl + +(comment) @comment diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/locals.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/locals.scm new file mode 100644 index 0000000..b617fdc --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/hlsl/locals.scm @@ -0,0 +1 @@ +; inherits: cpp diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/html_tags/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/html_tags/highlights.scm index 419b456..b6a90f7 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/html_tags/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/html_tags/highlights.scm @@ -4,7 +4,7 @@ (attribute_name) @tag.attribute (attribute (quoted_attribute_value) @string) -(text) @text +(text) @text @spell ((element (start_tag (tag_name) @_tag) (text) @text.title) (#match? @_tag "^(h[0-9]|title)$")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/highlights.scm index dfac3aa..db9453d 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/highlights.scm @@ -9,7 +9,7 @@ (method_declaration name: (identifier) @method) (method_invocation - name: (identifier) @method) + name: (identifier) @method.call) (super) @function.builtin @@ -143,13 +143,13 @@ ] @float (character_literal) @character -(string_literal) @string +[(string_literal) (text_block)] @string (null_literal) @constant.builtin [ (line_comment) (block_comment) -] @comment +] @comment @spell [ (true) @@ -178,11 +178,14 @@ "open" "opens" "package" +"permits" "private" "protected" "provides" "public" "requires" +"sealed" +"non-sealed" "static" "strictfp" "synchronized" diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/indents.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/indents.scm index 578c223..8e53754 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/indents.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/java/indents.scm @@ -1,6 +1,7 @@ [ - (class_declaration) (class_body) + (enum_body) + (interface_body) (constructor_declaration) (constructor_body) (block) @@ -10,6 +11,8 @@ (formal_parameters) ] @indent +(expression_statement (method_invocation) @indent) + [ "(" ")" @@ -19,4 +22,13 @@ "]" ] @branch -[(block_comment) (line_comment)] @ignore +[ + "}" +] @indent_end + +(line_comment) @ignore + +[ + (ERROR) + (block_comment) +] @auto diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/javascript/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/javascript/highlights.scm index 5763443..b62679c 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/javascript/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/javascript/highlights.scm @@ -31,3 +31,6 @@ (formal_parameters (assignment_pattern left: (identifier) @parameter)) + +;; punctuation +(optional_chain) @punctuation.delimiter diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/highlights.scm new file mode 100644 index 0000000..0793e15 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/highlights.scm @@ -0,0 +1,329 @@ +; Variables + +(variable) @variable + +((variable) @constant.builtin + (#eq? @constant.builtin "$ENV")) + +((variable) @constant.macro + (#eq? @constant.macro "$__loc__")) + +; Properties + +(index + (identifier) @property) + +; Labels + +(query + label: (variable) @label) + +(query + break_statement: (variable) @label) + +; Literals + +(number) @number + +(string) @string + +[ + "true" + "false" +] @boolean + +("null") @type.builtin + +; Interpolation + +["\\(" ")"] @character.special + +; Format + +(format) @attribute + +; Functions + +(funcdef + (identifier) @function) + +(funcdefargs + (identifier) @parameter) + +[ + "reduce" + "foreach" +] @function.builtin + +; jq -n 'builtins | map(split("/")[0]) | unique | .[]' +((funcname) @function.builtin + (#any-of? @function.builtin + "IN" + "INDEX" + "JOIN" + "acos" + "acosh" + "add" + "all" + "any" + "arrays" + "ascii_downcase" + "ascii_upcase" + "asin" + "asinh" + "atan" + "atan2" + "atanh" + "booleans" + "bsearch" + "builtins" + "capture" + "cbrt" + "ceil" + "combinations" + "contains" + "copysign" + "cos" + "cosh" + "debug" + "del" + "delpaths" + "drem" + "empty" + "endswith" + "env" + "erf" + "erfc" + "error" + "exp" + "exp10" + "exp2" + "explode" + "expm1" + "fabs" + "fdim" + "finites" + "first" + "flatten" + "floor" + "fma" + "fmax" + "fmin" + "fmod" + "format" + "frexp" + "from_entries" + "fromdate" + "fromdateiso8601" + "fromjson" + "fromstream" + "gamma" + "get_jq_origin" + "get_prog_origin" + "get_search_list" + "getpath" + "gmtime" + "group_by" + "gsub" + "halt" + "halt_error" + "has" + "hypot" + "implode" + "in" + "index" + "indices" + "infinite" + "input" + "input_filename" + "input_line_number" + "inputs" + "inside" + "isempty" + "isfinite" + "isinfinite" + "isnan" + "isnormal" + "iterables" + "j0" + "j1" + "jn" + "join" + "keys" + "keys_unsorted" + "last" + "ldexp" + "leaf_paths" + "length" + "lgamma" + "lgamma_r" + "limit" + "localtime" + "log" + "log10" + "log1p" + "log2" + "logb" + "ltrimstr" + "map" + "map_values" + "match" + "max" + "max_by" + "min" + "min_by" + "mktime" + "modf" + "modulemeta" + "nan" + "nearbyint" + "nextafter" + "nexttoward" + "normals" + "not" + "now" + "nth" + "nulls" + "numbers" + "objects" + "path" + "paths" + "pow" + "pow10" + "range" + "recurse" + "recurse_down" + "remainder" + "repeat" + "reverse" + "rindex" + "rint" + "round" + "rtrimstr" + "scalars" + "scalars_or_empty" + "scalb" + "scalbln" + "scan" + "select" + "setpath" + "significand" + "sin" + "sinh" + "sort" + "sort_by" + "split" + "splits" + "sqrt" + "startswith" + "stderr" + "strflocaltime" + "strftime" + "strings" + "strptime" + "sub" + "tan" + "tanh" + "test" + "tgamma" + "to_entries" + "todate" + "todateiso8601" + "tojson" + "tonumber" + "tostream" + "tostring" + "transpose" + "trunc" + "truncate_stream" + "type" + "unique" + "unique_by" + "until" + "utf8bytelength" + "values" + "walk" + "while" + "with_entries" + "y0" + "y1" + "yn")) + +; Keywords + +[ + "def" + "as" + "label" + "module" + "break" +] @keyword + +[ + "import" + "include" +] @include + +[ + "if" + "then" + "else" + "end" +] @conditional + +[ + "try" + "catch" +] @exception + +[ + "or" + "and" +] @keyword.operator + +; Operators + +[ + "." + "==" + "!=" + ">" + ">=" + "<=" + "<" + "=" + "+" + "-" + "*" + "/" + "%" + "+=" + "-=" + "*=" + "/=" + "%=" + "//=" + "|" + "?" + "//" + "?//" + (recurse) ; ".." +] @operator + +; Punctuation + +[ + ";" + "," + ":" +] @punctuation.delimiter + +[ + "[" "]" + "{" "}" + "(" ")" +] @punctuation.bracket + +; Comments + +(comment) @comment @spell diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/injections.scm new file mode 100644 index 0000000..5b39139 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jq/injections.scm @@ -0,0 +1,31 @@ +(comment) @comment + +; test(val) +(query + ((funcname) @_function + (#any-of? @_function + "test" + "match" + "capture" + "scan" + "split" + "splits" + "sub" + "gsub")) + (args . (query (string) @regex))) + + +; test(regex; flags) +(query + ((funcname) @_function + (#any-of? @_function + "test" + "match" + "capture" + "scan" + "split" + "splits" + "sub" + "gsub")) + (args . (args + (query (string) @regex)))) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/json/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/json/highlights.scm index 9bf4279..f41b44a 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/json/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/json/highlights.scm @@ -6,9 +6,12 @@ (pair value: (string) @string) (array (string) @string) (string_content (escape_sequence) @string.escape) +(string_content) @spell (ERROR) @error -"," @punctuation.delimiter +["," ":"] @punctuation.delimiter "[" @punctuation.bracket "]" @punctuation.bracket "{" @punctuation.bracket "}" @punctuation.bracket + +(("\"" @conceal) (#set! conceal "")) diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonc/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonc/highlights.scm index a51c5c7..e501121 100644 --- a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonc/highlights.scm +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonc/highlights.scm @@ -1,3 +1,3 @@ ; inherits: json -(comment) @comment +(comment) @comment @spell diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonnet/highlights.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonnet/highlights.scm new file mode 100644 index 0000000..eb8d22b --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsonnet/highlights.scm @@ -0,0 +1,76 @@ +[ + (true) + (false) +] @boolean + +(comment) @comment +(id) @variable +(import) @include +(null) @constant.builtin +(number) @number +(string) @string + +(fieldname (id) @label) + +[ + "[" + "]" + "{" + "}" + "(" + ")" +] @punctuation.bracket + +[ + "." + "," + ";" + ":" + "::" + ":::" +] @punctuation.delimiter + +(expr + operator: (_) @operator) +[ + "+" + "=" +] @operator + +"in" @keyword.operator + +[ + (local) + "assert" +] @keyword + +[ + "else" + "if" + "then" +] @conditional + +[ + (dollar) + (self) +] @variable.builtin +((id) @variable.builtin + (#eq? @variable.builtin "std")) + +; Function declaration +(bind + function: (id) @function + params: (params + (param + identifier: (id) @parameter))) + +; Function call +(expr + (expr (id) @function.call) + "(" + (args + (named_argument + (id) @parameter))? + ")") + +(ERROR) @error diff --git a/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsx/injections.scm b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsx/injections.scm new file mode 100644 index 0000000..3a96931 --- /dev/null +++ b/etc/soft/nvim/+plugins/nvim-treesitter/queries/jsx/injections.scm @@ -0,0 +1,8 @@ +; Styled Jsx + + + +
+ + (1 + (2 + 3)) +
+ + + + diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.html.txt b/etc/soft/nvim/+plugins/rainbow/tests/test.html.txt new file mode 100644 index 0000000..495388e --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.html.txt @@ -0,0 +1,8 @@ + +
+ + (1 + (2 + 3)) +
+ + + diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.js b/etc/soft/nvim/+plugins/rainbow/tests/test.js new file mode 100644 index 0000000..912966f --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.js @@ -0,0 +1,5 @@ +typoo +(typoo) + +console.log({w: 200, h: 100, pos: [{x: 1, y: 2}, {x: 3, y: 4}]}) + diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.lua b/etc/soft/nvim/+plugins/rainbow/tests/test.lua new file mode 100644 index 0000000..15e369d --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.lua @@ -0,0 +1,12 @@ +(function(args) + lst = { a=function(arg) print("hello") end, + b=(1+2)*3/4, + [3+5]={ ["hello"]=("hi") }, + } + lst[ + (function() return 0 end)()] = 1 +end)("blah") + +[[ +Special lua string... +]] diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.php b/etc/soft/nvim/+plugins/rainbow/tests/test.php new file mode 100644 index 0000000..bad0d4c --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.php @@ -0,0 +1,13 @@ + 0) + { + echo "Error: " . $_FILES["file"]["error"] . "
"; + } +else + { + echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb
"; + } +?> + +hello (world) + diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.pl b/etc/soft/nvim/+plugins/rainbow/tests/test.pl new file mode 100644 index 0000000..ac48bb3 --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.pl @@ -0,0 +1,10 @@ +typoo +(typoo) + +sub test { +correct indent; +} + +if ($test) { +incorrect indent; +} diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.rb b/etc/soft/nvim/+plugins/rainbow/tests/test.rb new file mode 100644 index 0000000..c2cd371 --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.rb @@ -0,0 +1,15 @@ +{{{}}} +((())) +[[[]]] +[[[[]]]] + +def sample_function(a, b) + ((())) + [[[]]] +end + +class SampleClass + def sample_method(a, b) + [[[]]] + end +end diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.sh b/etc/soft/nvim/+plugins/rainbow/tests/test.sh new file mode 100644 index 0000000..5a765dc --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.sh @@ -0,0 +1,52 @@ +#!/bin/bash +typoo +(typoo) + +if stuff +then + somestuff + test = (()) + (()) + a = (1 + (2 + 3)) + if + then + elif + then + else + fi +else + otherstuff +fi + +if stuff + then + somestuff + else + otherstuff +fi + +function f() { + if + fi +} + + +# check if command exists +command_exists () { + type "${1}" > /dev/null 2>&1; + a = (1 + (2 + 3)) +} + +# Fetch the update +fetch() { + if type wget > /dev/null 2>&1 ; then + $debug && echo "fetching update via wget" + wget --no-check-certificate -O "${2}" "${1}" >/dev/null 2>&1 + elif type curl > /dev/null 2>&1 ; then + $debug && echo "fetching update via curl" + curl --insecure --remote-name -o "${2}" "${1}" >/dev/null 2>&1 + else + echo 'Warning: Neither wget nor curl is available. online updates unavailable' >&2 + exit 1 + fi +} diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.special-ft b/etc/soft/nvim/+plugins/rainbow/tests/test.special-ft new file mode 100644 index 0000000..ad5e11d --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.special-ft @@ -0,0 +1,5 @@ +(((()))) +[[[[]]]] +{{{{}}}} + +# vim: set ft=this-is-a--very-SPECIAL-filetype : diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.styl b/etc/soft/nvim/+plugins/rainbow/tests/test.styl new file mode 100644 index 0000000..b870224 --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.styl @@ -0,0 +1,3 @@ +div { + color: red +} diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.tex b/etc/soft/nvim/+plugins/rainbow/tests/test.tex new file mode 100644 index 0000000..a72760c --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.tex @@ -0,0 +1,11 @@ +\documentclass[]{article} + +{{{{}}}} +((((((())))))) +[[[[[[]]]]]] +\begin{document} +((((())))) +${{{{}}}}$ +{{{{}}}} +[[[[[[]]]]]] +\end{document} diff --git a/etc/soft/nvim/+plugins/rainbow/tests/test.xml b/etc/soft/nvim/+plugins/rainbow/tests/test.xml new file mode 100644 index 0000000..a851d9a --- /dev/null +++ b/etc/soft/nvim/+plugins/rainbow/tests/test.xml @@ -0,0 +1,6 @@ + +
+ +
+ + diff --git a/etc/soft/nvim/+plugins/range-highlight.nvim/LICENSE b/etc/soft/nvim/+plugins/range-highlight.nvim/LICENSE deleted file mode 100644 index 37381a6..0000000 --- a/etc/soft/nvim/+plugins/range-highlight.nvim/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Hugo Sum - -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. diff --git a/etc/soft/nvim/+plugins/range-highlight.nvim/README.md b/etc/soft/nvim/+plugins/range-highlight.nvim/README.md deleted file mode 100644 index b34f5e0..0000000 --- a/etc/soft/nvim/+plugins/range-highlight.nvim/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# range-highlight.nvim - -An extremely lightweight plugin (~ 120loc) that hightlights ranges you have entered in commandline. - -![Demo for using range-highlight](./demo.gif) - -## Features - -- Single line range highlight (`:10`) - -- Absolute range highlight (`:20,15`) - -- Semicolon separated range highlight (`:20;15`) - -- Backward range highlight (`:20,15`) - -- Shorthand range highlight (`:,15`) - -- Relative range highlight (`:+5,-2`) - -- Multiple relative range highlight (`:10+5--,5+3-2`) - -- Mark range highlight (`:'a,20`) - -- Dot range highlight (`:.,-2`, `:5,.`) - -- Last line and whole file highlight (`:4,$`, `:%`) - -- Pattern range highlight (`:/hello/d`, `?world?d`) - -## Installation - -`range-highlight.nvim` requires a minimum version of NeoVim 0.5.0. - -You can install it using any standard Vim/NeoVim package manager. For example: - -### `paq.nvim` - -```lua -paq{'winston0410/cmd-parser.nvim'} -paq{'winston0410/range-highlight.nvim'} -require'range-highlight'.setup{} -``` - -## Configuration - -This is the default configuration. It is likely that you don't need to change anything. - -```lua -require("range-highlight").setup { - highlight = "Visual", - highlight_with_out_range = { - d = true, - delete = true, - m = true, - move = true, - y = true, - yank = true, - c = true, - change = true, - j = true, - join = true, - ["<"] = true, - [">"] = true, - s = true, - subsititue = true, - sno = true, - snomagic = true, - sm = true, - smagic = true, - ret = true, - retab = true, - t = true, - co = true, - copy = true, - ce = true, - center = true, - ri = true, - right = true, - le = true, - left = true, - sor = true, - sort = true - } -} -``` - -### Range highlight not working for your command? - -If the range highlight doesn't work for your command, you can contribute it into the above list - -## Acknowledgement - -Thank you folks from [gitters](https://gitter.im/neovim/neovim) for helping me out with this plugin. diff --git a/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/helper.lua b/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/helper.lua deleted file mode 100644 index 1399428..0000000 --- a/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/helper.lua +++ /dev/null @@ -1,20 +0,0 @@ -local function mark_to_number(start_mark) - return vim.api.nvim_buf_get_mark(0, string.sub(start_mark, 2, -1))[1] -end - -local function search_to_number(config) - return function(pattern) - local pattern_text, search_options = string.sub(pattern, 2, -2), "n" - if not config.forward then search_options = "bn" end - local line_number = vim.api.nvim_call_function("searchpos", { - pattern_text, search_options - })[1] - return line_number - end -end - -return { - mark_to_number = mark_to_number, - forward_search_to_number = search_to_number {forward = true}, - backward_search_to_number = search_to_number {forward = false} -} diff --git a/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/init.lua b/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/init.lua deleted file mode 100644 index 9845043..0000000 --- a/etc/soft/nvim/+plugins/range-highlight.nvim/lua/range-highlight/init.lua +++ /dev/null @@ -1,164 +0,0 @@ -local v = vim.api -local ns = v.nvim_create_namespace("range-highlight") -local opts, cache = - { - highlight = "Visual", - -- Add command that takes range here - highlight_with_out_range = { - d = true, - delete = true, - m = true, - move = true, - y = true, - yank = true, - c = true, - change = true, - j = true, - join = true, - ["<"] = true, - [">"] = true, - s = true, - subsititue = true, - sno = true, - snomagic = true, - sm = true, - smagic = true, - ret = true, - retab = true, - t = true, - co = true, - copy = true, - ce = true, - center = true, - ri = true, - right = true, - le = true, - left = true, - sor = true, - sort = true, - }, - }, {} -local mark_to_number = require("range-highlight.helper").mark_to_number -local forward_search_to_number = require("range-highlight.helper").forward_search_to_number -local backward_search_to_number = require("range-highlight.helper").backward_search_to_number -local parse_cmd = require("cmd-parser").parse_cmd - -local function cleanup() - v.nvim_buf_clear_namespace(0, ns, 0, -1) - cache = {} -end - -local range_handlers = { - number = tonumber, - mark = mark_to_number, - forward_search = forward_search_to_number, - backward_search = backward_search_to_number, -} - -local function get_range_number(cmd) - local start_line, end_line = 0, 0 - local current_line = vim.api.nvim_win_get_cursor(0)[1] - local line_count = vim.api.nvim_buf_line_count(0) - local result = parse_cmd(cmd) - - if not result.start_range then - -- print('check command', cmd, opts.highlight_with_out_range[result.command]) - if not opts.highlight_with_out_range[result.command] then - v.nvim_buf_clear_namespace(0, ns, 0, -1) - vim.cmd("redraw") - return -1, -1 - end - end - - if result.start_range == "%" or result.end_range == "%" then - return 0, line_count - end - - if result.start_range then - if result.start_range == "$" then - start_line = line_count - elseif result.start_range == "." then - start_line = current_line - else - start_line = range_handlers[result.start_range_type](result.start_range) - end - else - start_line = current_line - end - - if result.start_increment then - start_line = start_line + result.start_increment_number - end - - if result.end_range then - if result.end_range == "$" then - end_line = line_count - elseif result.end_range == "." then - end_line = current_line - else - end_line = range_handlers[result.end_range_type](result.end_range) - end - else - end_line = start_line - end - - if result.end_increment then - end_line = end_line + result.end_increment_number - end - - -- print('check at the end or transformation', cmd, result.command, result.start_range, result.end_range) - - start_line = start_line - 1 - - return start_line, end_line -end - -local function add_highlight() - local text = vim.fn.getcmdline() - - if vim.fn.getcmdtype() ~= ":" then - return - end - - local start_line, end_line = get_range_number(text) - - -- print('check values', text, start_line, end_line) - if start_line < 0 or end_line < 0 then - return - end - - if end_line < start_line then - start_line, end_line = end_line, start_line - start_line = start_line - 1 - end_line = end_line + 1 - end - - if cache[1] == start_line and cache[2] == end_line then - return - end - - if cache[1] and cache[2] then - if cache[1] ~= start_line or cache[2] ~= end_line then - v.nvim_buf_clear_namespace(0, ns, cache[1], cache[2]) - end - end - cache[1], cache[2] = start_line, end_line - vim.highlight.range(0, ns, opts.highlight, { start_line, 0 }, { end_line, 0 }, "V", false) - vim.cmd("redraw") -end - -local function setup(user_opts) - opts = vim.tbl_extend("force", opts, user_opts or {}) - v.nvim_exec( - [[ - augroup Ranger - autocmd! - au CmdlineChanged * lua require('range-highlight').add_highlight() - au CmdlineLeave * lua require('range-highlight').cleanup() - augroup END - ]], - true - ) -end - -return { setup = setup, cleanup = cleanup, add_highlight = add_highlight } diff --git a/etc/soft/nvim/+plugins/sqlite.lua/CHANGELOG.md b/etc/soft/nvim/+plugins/sqlite.lua/CHANGELOG.md new file mode 100644 index 0000000..9c63ac1 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/CHANGELOG.md @@ -0,0 +1,257 @@ + + + +## [unreleased](https://github.com/kkharji/sqlite.lua/compare/v1.2.0...unreleased) + +### :bug: Bug Fixes + +- 74b3844 false is treat as nil when inserting (closes #123) + +
f9326aa sqlite:tbl(...) doesn't pass schema + +This what happens with dealing with function that can take self or +other. + +Refs: https://github.com/AckslD/nvim-neoclip.lua/pull/20 + +
+ + +### :construction_worker: CI Updates + +- 99e4058 fix changelog links + + + + + +## [v1.2.0](https://github.com/kkharji/sqlite.lua/compare/v1.1.0...v1.2.0) + +### :bug: Bug Fixes + +
edf642e fix Emmylua completion + +This used to work, but maybe with new versions of sumneko_lua. It +stopped working. + +
+ + +### :sparkles: Features + +- 3d89dc1 add Bookmark Manager Example + + + + + +## [v1.1.0](https://github.com/kkharji/sqlite.lua/compare/v1.0.0...v1.1.0) + +### :bug: Bug Fixes + +
43f5e0c luarocks auto-generate script (closes #115) + +Having "/" in the start of path breaks luarocks installation. + +
+ + +### :sparkles: Features + +- 5b39526 activate release workflow + + +### :zap: Performance Improvements + +- ca8233f improve handling of sqlite builtin functions (closes #114) + +
f9a1060 improve require time (#111) + +decrease require time (cost on startup) from `0.791636 ms` to `4.140682` (423% faster) + +
+ + + + + +## v1.0.0 + +### :art: Structure/Formating + +- 9be469a update changelog item format + +- 2d24f86 update changelog item format (closes #81) + + +### :boom: Breaking Changes + +
a6bd3d1 sql.schema return detailed description of table. + +Not sure if this is a wise decision or not. But it beat having the +schema returned given the key and the value the user written. + +
+ +
5b0710f (parser) change nullable to require + refactor + +Additionally, make primary reference pk. + +
+ +
8bf61d2 change sugar function namespace to sql.lib + +changes access to sugar functions and store it in lib. It was weird typing out `sql...` and abbreviating it seems harder too. + +
+ + +### :bug: Bug Fixes + +- 1778aa8 (parser) only check when type is table (closes #103) + +
1c88610 extended tables referencing mutable db object + other fixes (#100) (closes #101, #99) + +- `table.extend` reference db object instead of `db.extend`ed object. + this fix issue with calling methods that has been already modified by the user. +- remove debug stuff +- modify `table.extend` mechanism. +- stop mutating insert/update source data when processing for `sql.insert` + +
+ +- db026ee using sql functions makes parser setsome values to null (#88) (closes #87) + +
2b500b7 each map sort failing due to closed connection + +make map, sort, each, support executing sqlite queries regardless of connection status. + + - 🐛 func(row) returning nil causing error + - 🐛 running some tbl function without checking conn + +
+ + +### :construction_worker: CI Updates + +- 9173664 changelog run once every two days take 2 + +- 3447223 update changelog template (#90) (closes #82) + +- 93b0090 changelog run once every two days (closes #86) + + +### :recycle: Code Refactoring + +
4d0302f favor delete/remove accepting where key + +No breaking changes here :) + +
+ + +### :sparkles: Features + +
25748dd auto alter table key definition (#103) + +Support for modifying schema key definitions without wasting the table content. It has little support for renaming, in fact, renaming should be avoided for the time being. + +✨ New: + - `db:execute` for executing statement without any return. + - emmylua classes`SqlSchemaKeyDefinition` and `SqliteActions`. + - when a key has default, then all the columns with nulls will be replaced with the default. + - support for auto altering key to reference a foreign key. + +🐛 Fixes + - when a foreign_keys is enabled on a connection, closing and opening disables it. + +♻️ Changes + - rename `db.sqlite_opts` to `db.opts`. + +✅ Added Tests + - auto alter: simple rename with idetnical number of keys + - auto alter: simple rename with idetnical number of keys with a key turned to be required + - auto alter: more than one rename with idetnical number of keys + - auto alter: more than one rename with idetnical number of keys + default without required = true + - auto alter: transform to foreign key + - auto alter: pass sqlite.org tests + +
+ +
73d8fa6 dot access for extended tbl object (#92) + +Changes: + - Fix parsing schema key when using key value pairs. + - access tbl original methods after overwrite with appending `_` + - Inject db object later with `set_db` + - auto-completion support + + ```lua + local users = require'sql.table'("users", {...}) -- or require'sql.table'(db, "users", {...}) + users.init = function(db) -- if db isn't injected already + users.set_db(db) --- inject db object + end + users.get = function() -- overwriting method + return users._get({ where = { id = 1 } })[1].name + end + return users + ``` + +
+ +
5944a91 table each and map accept function as first argument (closes #97) + +still compatible with query as first argument ✅ + +Examples: + +```lua +tbl:each(function(row) .. end) -- execute a function on all table rows +tbl:each(function(row) .. end, {...} ) -- execute a function on all table rows match the query +-- map work the same way, but return transformed table +``` + +
+ +
88f14bf auto changelog (#80) + +Here goes nothing 🤞. Please CI don't fail me. + +
+ + +### :white_check_mark: Add/Update Test Cases + +- 91b28f6 uncomment leftout tests + + +### :zap: Performance Improvements + +
2424ea0 (sql) avoid executing sql.schema on crud + +follow up to d791f87 + +
+ +
a46ee6b (table) avoid executing sql.schema on crud + +simple performance enhancement that should have been done from the start +:smile: + +
+ +
1a36aa5 sql.extend handling of tables and connection. (#96) + +✨ New: + - add usage examples for DB:extend. + - support using different name to access a tbl object `{_name = ".."}`. + - support using pre-defined/extended sql.table object. + - 100% lazy sql object setup: No calls will be made before the first sql operation. + +♻️ Changes: + - remove init hack of controlling sql extend object table initialization (no longer needed). + - remove old tests. + +
+ + diff --git a/etc/soft/nvim/+plugins/sqlite.lua/LICENSE b/etc/soft/nvim/+plugins/sqlite.lua/LICENSE new file mode 100644 index 0000000..68268c2 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 kkharji + +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. diff --git a/etc/soft/nvim/+plugins/sqlite.lua/Makefile b/etc/soft/nvim/+plugins/sqlite.lua/Makefile new file mode 100644 index 0000000..8c3268e --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/Makefile @@ -0,0 +1,19 @@ +.PHONY: test lint docgen +test: + nvim --headless --noplugin -u test/minimal_init.vim -c "PlenaryBustedDirectory test/auto/ { minimal_init = './test/minimal_init.vim' }" + +lint: + luacheck lua/ + +stylua: + stylua --color always --check lua/ + +gen_nvimhelp: ./scripts/gen_nvimhelp.lua + nvim --headless --noplugin -u test/minimal_init.vim -c "luafile ./scripts/gen_nvimhelp.lua" -c 'qa' + +testfile: + nvim --headless --noplugin -u test/minimal_init.vim -c "PlenaryBustedDirectory test/auto/$(file)_spec.lua { minimal_init = './test/minimal_init.vim' }" + +gen_luarock: ./scripts/gen_rockspec.lua + nvim --headless --noplugin -u test/minimal_init.vim -c "luafile ./scripts/gen_rockspec.lua" -c 'qa' + diff --git a/etc/soft/nvim/+plugins/sqlite.lua/README.md b/etc/soft/nvim/+plugins/sqlite.lua/README.md new file mode 100644 index 0000000..4840e16 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/README.md @@ -0,0 +1,80 @@ +sqlite.lua 💫 +================= + + +[SQLite]/[LuaJIT] binding and a highly opinionated wrapper for storing, retrieving, caching, and persisting [SQLite] databases. +[sqlite.lua] present new possibilities for plugin development and while it's primarily created for [neovim], it support all luajit environments. + +- [Changelog](https://github.com/kkharji/sqlite.lua/blob/master/CHANGELOG.md) +- [Docs](https://github.com/kkharji/sqlite.lua/blob/master/doc/sqlite.txt) +- [Examples](https://github.com/kkharji/sqlite.lua/blob/master/lua/sqlite/examples) +- [Powered By sqlite.lua](https://github.com/kkharji/sqlite.lua#-powered-by-sqlitelua) + +![preview](https://user-images.githubusercontent.com/2361214/202757101-65735d52-2927-4de0-8d69-b078b66aaca6.svg) + +✨ Features: +------------------ +- Connect, reconnect, close sql db connections `sqlite:open/sql:close` +- Evaluate any sqlite statement and return result if any `sqlite:eval` +- Helper function over `sqlite:eval` to do all sort of operation. +- High level API with `sqlite.tbl` for better experience. +- lua tables deserialization/serialization (in helper functions and high level api) +- 90% test coverage. +- Up-to-date docs and changelog + + +🚧 Installation +----------------- + +### [Packer.nvim](https://github.com/wbthomason/packer.nvim) (Neovim) + +```lua +use { "kkharji/sqlite.lua" } +``` + +### [luarocks](https://luarocks.org/) (LuaJIT) + +```bash +luarocks install sqlite luv +``` + +**Ensure you have `sqlite3` installed locally.** (if you are on mac it might be installed already) + +#### Windows + +[Download precompiled](https://www.sqlite.org/download.html) and set `let g:sqlite_clib_path = path/to/sqlite3.dll` (note: `/`) + +#### Linux +```bash +sudo pacman -S sqlite # Arch +sudo apt-get install sqlite3 libsqlite3-dev # Ubuntu +``` + +#### Nix (home-manager) +```nix +programs.neovim.plugins = [ + { + plugin = pkgs.vimPlugins.sqlite-lua; + config = "let g:sqlite_clib_path = '${pkgs.sqlite.out}/lib/libsqlite3.so'"; + } +]; +``` + +*Notes:* + - Ensure you install `pkgs.sqlite` + - If you are using home-manager on OSX, you must replace `libsqlite3.so` with `libsqlite3.dylib` + +🔥 Powered by sqlite.lua +----------------- + +- https://github.com/kkharji/impatient.nvim +- https://github.com/nvim-telescope/telescope-smart-history.nvim +- https://github.com/nvim-telescope/telescope-frecency.nvim +- https://github.com/kkharji/lispdocs.nvim +- https://github.com/nvim-telescope/telescope-cheat.nvim + +[Installation]: #🚧_installation +[SQLite]: https://www.sqlite.org/index.html +[LuaJIT]: https://luajit.org +[sqlite.lua]: https://github.com/kkharji/sqlite.lua +[neovim]: https://github.com/neovim/neovim diff --git a/etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet-lint/err_and_warn.pp.stderr b/etc/soft/nvim/+plugins/sqlite.lua/doc/.keep similarity index 100% rename from etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet-lint/err_and_warn.pp.stderr rename to etc/soft/nvim/+plugins/sqlite.lua/doc/.keep diff --git a/etc/soft/nvim/+plugins/sqlite.lua/doc/sqlite.txt b/etc/soft/nvim/+plugins/sqlite.lua/doc/sqlite.txt new file mode 100644 index 0000000..f72df0f --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/doc/sqlite.txt @@ -0,0 +1,884 @@ +================================================================================ +README *sqlite.readme* + +SQLite/LuaJIT binding and highly opinionated wrapper for storing, retrieving, +caching, persisting, querying, and connecting to SQLite databases. + +To find out more visit https://github.com/kkharji/sqlite.lua + +Help usage in neovim: ignore || + :h |sqlite.readme| | open help readme + :h |sqlite_schema_key| | open a class or type + :h |sqlite.tbl| | show help for sqlite_tbl. + :h |sqlite.db| | show help for sqlite_tbl. + :h sqlite.db:... | show help for a sqlite_db method. + :h sqlite.tbl:... | show help for a sqlite_tbl method. + +sqlite.lua types: + +sqlite_schema_key *sqlite_schema_key* + Sqlite schema key fileds. {name} is the only required field. + + Fields: ~ + {cid} (number) column index. + {name} (string) column key. + {type} (string) column type. + {required} (boolean) whether it's required. + {primary} (boolean) whether it's a primary key. + {default} (string) default value when null. + {reference} (string) "table_name.column_key" + {on_delete} (sqlite_trigger) trigger on row delete. + {on_update} (sqlite_trigger) trigger on row updated. + + +sqlite_opts *sqlite_opts* + Sqlite3 Options (TODO: add sqlite option fields and description) + + +sqlite_query_update *sqlite_query_update* + Query fileds used when calling |sqlite:update| or |sqlite_tbl:update| + + Fields: ~ + {where} (table) filter down values using key values. + {set} (table) key and value to updated. + + +sqlite_query_select *sqlite_query_select* + Query fileds used when calling |sqlite:select| or |sqlite_tbl:get| + + Fields: ~ + {where} (table) filter down values using key values. + {keys} (table) keys to include. (default all) + {join} (table) (TODO: support) + {order_by} (table) { asc = "key", dsc = {"key", "another_key"} } + {limit} (number) the number of result to limit by + {contains} (table) for sqlite glob ex. { title = "fix*" } + + +sqlite_flags *sqlite_flags* + Sqlite3 Error Flags (TODO: add sqlite error flags value and description) + + +sqlite_db_status *sqlite_db_status* + Status returned from |sqlite:status()| + + Fields: ~ + {msg} (string) + {code} (sqlite_flags) + + + +================================================================================ +LUA *sqlite.db.lua* + +Main sqlite.lua object and methods. + +sqlite_db *sqlite_db* + Main sqlite.lua object. + + Fields: ~ + {uri} (string) database uri. it can be an environment variable + or an absolute path. default ":memory:" + {opts} (sqlite_opts) see https://www.sqlite.org/pragma.html + |sqlite_opts| + {conn} (sqlite_blob) sqlite connection c object. + {db} (sqlite_db) reference to fallback to when overwriting + |sqlite_db| methods (extended only). + + +sqlite.db.new({uri}, {opts}) *sqlite.db.new()* + Creates a new sqlite.lua object, without creating a connection to uri. + |sqlite.new| is identical to |sqlite.db:open| but it without opening sqlite + db connection (unless opts.keep_open). Its most suited for cases where the + database might be -acccess from multiple places. For neovim use cases, this + mean from different -neovim instances. + + ```lua + local db = sqlite.new("path/to/db" or "$env_var", { ... } or nil) + -- configure open mode through opts.open_mode = "ro", "rw", "rwc", default "rwc" + -- for more customize behaviour, set opts.open_mode to a list of db.flags + -- see https://sqlite.org/c3ref/open.html#urifilenamesinsqlite3open + ``` + + + Parameters: ~ + {uri} (string) uri to db file. + {opts} (sqlite_opts) (optional) see |sqlite_opts| + + Return: ~ + sqlite_db + + +sqlite.db:extend({conf}) *sqlite.db:extend()* + Extend |sqlite_db| object with extra sugar syntax and api. This is + recommended for all sqlite use case as it provide convenience. This method + is super lazy. it try its best to doing any ffi calls until the first + operation done on a table. + + In the case you want to keep db connection open and not on invocation + bases. Run |sqlite.db:open()| right after creating the object or when you + intend to use it. + + Like |sqlite_tbl| original methods can be access through pre-appending "__" + when user overwrites it. + + if { conf.opts.lazy } then only return a logical object with self-dependent + + + Parameters: ~ + {conf} (table) see 'Fields' + + Fields: ~ + {uri} (string) path to db file. + {opts} (sqlite_opts) (optional) see |sqlite_opts| + lazy (default + false), open (default false) + {tname1} (string) pointing to |sqlite_etbl| or + |sqlite_schema_dict| + {tnameN} (string) pointing to |sqlite_etbl| or + |sqlite_schema_dict| + + Return: ~ + sqlite_db + + See: ~ + |sqlite_tbl:extend()| + + +sqlite.db:open({uri}, {opts}) *sqlite.db:open()* + Creates and connect to new sqlite db object, either in memory or via a + {uri}. If it is called on pre-made |sqlite_db| object, than it should open + it. otherwise ignore. + + ```lua + -- Open db file at path or environment variable, otherwise open in memory. + local db = sqlite.db:open("./pathto/dbfile" or "$ENV_VARABLE" or nil, {...}) + -- reopen connection if closed. + db:open() + -- configure open mode through opts.open_mode = "ro", "rw", "rwc", default "rwc" + -- for more customize behaviour, set opts.open_mode to a list of db.flags + -- see https://sqlite.org/c3ref/open.html#urifilenamesinsqlite3open + ``` + + + Parameters: ~ + {uri} (string) (optional) {uri} == {nil} then in-memory db. + {opts} (sqlite_opts|nil) see |sqlite_opts| + + Return: ~ + sqlite_db + + +sqlite.db:close() *sqlite.db:close()* + Close sqlite db connection. returns true if closed, error otherwise. + + ```lua + local db = sqlite.db:open() + db:close() -- close connection + ``` + + + Return: ~ + boolean + + +sqlite.db:with_open() *sqlite.db:with_open()* + Same as |sqlite.db:open| but execute {func} then closes db connection. If + the function is called as a method to db object e.g. 'db:with_open', then + {args[1]} must be a function. Else {args[1]} need to be the uri and + {args[2]} the function. + + ```lua + -- as a function + local entries = sqlite.with_open("path/to/db", function(db) + return db:select("todos", { where = { status = "done" } }) + end) + -- as a method + local exists = db:with_open(function() + return db:exists("projects") + end) + ``` + + + + Varargs: ~ + If used as db method, then the {args[1]} should be a function, else + + Return: ~ + any + + See: ~ + |sqlite.db:open()| + + +sqlite.db:isopen() *sqlite.db:isopen()* + Predict returning true if db connection is active. + + ```lua + if db:isopen() then + db:close() + end + ``` + + + Return: ~ + boolean + + +sqlite.db:isclose() *sqlite.db:isclose()* + Predict returning true if db connection is indeed closed. + + ```lua + if db:isclose() then + error("db is closed") + end + ``` + + + Return: ~ + boolean + + +sqlite.db:status() *sqlite.db:status()* + Returns current connection status Get last error code + + ```lua + print(db:status().msg) -- get last error msg + print(db:status().code) -- get last error code. + ``` + + + Return: ~ + sqlite_db_status + + +sqlite.db:eval({statement}, {params}) *sqlite.db:eval()* + Evaluates a sql {statement} and if there are results from evaluation it + returns list of rows. Otherwise it returns a boolean indecating whether the + evaluation was successful. + + ```lua + -- evaluate without any extra arguments. + db:eval("drop table if exists todos") + -- evaluate with unamed value. + db:eval("select * from todos where id = ?", 1) + -- evaluate with named arguments. + db:eval("insert into t(a, b) values(:a, :b)", {a = "1", b = 3}) + ``` + + + Parameters: ~ + {statement} (string) SQL statement. + {params} (table|nil) params to be bind to {statement} + + Return: ~ + boolean|table + + +sqlite.db:execute({statement}) *sqlite.db:execute()* + Execute statement without any return + + ```lua + db:execute("drop table if exists todos") + db:execute("pragma foreign_keys=on") + ``` + + + Parameters: ~ + {statement} (string) statement to be executed + + Return: ~ + boolean: true if successful, error out if not. + + +sqlite.db:exists({tbl_name}) *sqlite.db:exists()* + Check if a table with {tbl_name} exists in sqlite db + ```lua + if not db:exists("todo_tbl") then + error("Table doesn't exists!!!") + end + ``` + + + Parameters: ~ + {tbl_name} (string) the table name. + + Return: ~ + boolean + + +sqlite.db:create({tbl_name}, {schema}) *sqlite.db:create()* + Create a new sqlite db table with {name} based on {schema}. if + {schema.ensure} then create only when it does not exists. similar to + 'create if not exists'. + + ```lua + db:create("todos", { + id = {"int", "primary", "key"}, + title = "text", + name = { type = "string", reference = "sometbl.id" }, + ensure = true -- create table if it doesn't already exists (THIS IS DEFUAULT) + }) + ``` + + + Parameters: ~ + {tbl_name} (string) table name + {schema} (sqlite_schema_dict) + + Return: ~ + boolean + + +sqlite.db:drop({tbl_name}) *sqlite.db:drop()* + Remove {tbl_name} from database + + ```lua + if db:exists("todos") then + db:drop("todos") + end + ``` + + + Parameters: ~ + {tbl_name} (string) table name + + Return: ~ + boolean + + +sqlite.db:schema({tbl_name}) *sqlite.db:schema()* + Get {name} table schema, if table does not exist then return an empty + table. + + ```lua + if db:exists("todos") then + inspect(db:schema("todos").project) + else + print("create me") + end + ``` + + + Parameters: ~ + {tbl_name} (string) the table name. + + Return: ~ + sqlite_schema_dict + + +sqlite.db:insert({tbl_name}, {rows}) *sqlite.db:insert()* + Insert lua table into sqlite database table. + + ```lua + --- single item. + db:insert("todos", { title = "new todo" }) + --- insert multiple items. + db:insert("items", { { name = "a"}, { name = "b" }, { name = "c" } }) + ``` + + + Parameters: ~ + {tbl_name} (string) the table name + {rows} (table) rows to insert to the table. + + Return: ~ + boolean|integer: boolean (true == success), and the last inserted row + id. + + +sqlite.db:update({tbl_name}, {specs}) *sqlite.db:update()* + Update table row with where closure and list of values returns true incase + the table was updated successfully. + + ```lua + --- update todos status linked to project "lua-hello-world" or "rewrite-neoivm-in-rust" + db:update("todos", { + where = { project = {"lua-hello-world", "rewrite-neoivm-in-rust"} }, + set = { status = "later" } + }) + + --- pass custom statement and boolean + db:update("timestamps", { + where = { id = "<" .. 4 }, -- mimcs WHERE id < 4 + set = { seen = true } -- will be converted to 0. + }) + ``` + + + Parameters: ~ + {tbl_name} (string) sqlite table + name. + {specs} (sqlite_query_update|sqlite_query_update[]) + + Return: ~ + boolean + + +sqlite.db:delete({tbl_name}, {where}) *sqlite.db:delete()* + Delete a {tbl_name} row/rows based on the {where} closure. If {where == + nil} then all the {tbl_name} content will be deleted. + + ```lua + --- delete todos table content + db:delete("todos") + --- delete row that has id as 1 + db:delete("todos", { id = 1 }) + --- delete all rows that has value of id 1 or 2 or 3 + db:delete("todos", { id = {1,2,3} }) + --- matching ids or greater than 5 + db:delete("todos", { id = {"<", 5} }) -- or {id = "<5"} + ``` + + + Parameters: ~ + {tbl_name} (string) sqlite table name + {where} (sqlite_query_delete) key value pair to where delete + operation should effect. + + Return: ~ + boolean: true if operation is successfully, false otherwise. + + +sqlite.db:select({tbl_name}, {spec}) *sqlite.db:select()* + Query from a table with where and join options + + ```lua + db:select("todos") get everything + --- get row with id of 1 + db:select("todos", { where = { id = 1 }) + --- get row with status value of later or paused + db:select("todos", { where = { status = {"later", "paused"} }) + --- get 5 items from todos table + db:select("todos", { limit = 5 }) + --- select a set of keys with computed one + db:select("timestamps", { + select = { + age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60, + "id", + "timestamp", + "entry", + }, + }) + ``` + + + Parameters: ~ + {tbl_name} (string) the name of the db table to select + on + {spec} (sqlite_query_select) + + Return: ~ + table[] + + +sqlite.db:tbl({tbl_name}, {schema}) *sqlite.db:tbl()* + Create new sqlite_tbl object. If {opts}.ensure = false, then on each run it + will drop the table and recreate it. NOTE: this might change someday to + alter the table instead. For now alteration is auto and limited to field + schema edits and renames + + ```lua + local tbl = db:tbl("todos", { + id = true, -- same as { type = "integer", required = true, primary = true } + title = "text", + category = { + type = "text", + reference = "category.id", + on_update = "cascade", -- means when category get updated update + on_delete = "null", -- means when category get deleted, set to null + }, + }) + --- or without db injected and use as wrapper for |sqlite.tbl.new| + local tbl = db.tbl("name", ...) + ``` + + + Parameters: ~ + {tbl_name} (string) the name of the table. can be new or + existing one. + {schema} (sqlite_schema_dict) {schema, ensure (defalut true)} + + Return: ~ + sqlite_tbl + + See: ~ + ||sqlite.tbl.new|()| + + +sqlite.db:table() *sqlite.db:table()* + DEPRECATED + + + +sqlite.db.lib() *sqlite.db.lib()* + Sqlite functions sugar wrappers. See `sql/strfun` + + + + +================================================================================ +LUA *sqlite.tbl.lua* + +Abstraction to produce more readable code. +```lua +local tbl = require'sqlite.tbl' +``` + +sqlite_tbl *sqlite_tbl* + Main sql table class + + Fields: ~ + {db} (sqlite_db) sqlite.lua database object. + {name} (string) table name. + {mtime} (number) db last modified time. + + +sqlite.tbl.new({name}, {schema}, {db}) *sqlite.tbl.new()* + Create new |sqlite_tbl| object. This object encouraged to be extend and + modified by the user. overwritten method can be still accessed via + pre-appending `__` e.g. redefining |sqlite_tbl:get|, result in + `sqlite_tbl:__get` available as a backup. This object can be instantiated + without a {db}, in which case, it requires 'sqlite.tbl:set_db' is called. + + Common use case might be to define tables in separate files and then + require them in file that export db object (TODO: support tbl reuse in + different dbs). + + ```lua + local t = tbl("todos", { --- or tbl.new + id = true, -- same as { "integer", required = true, primary = true } + title = "text", + since = { "date", default = sqlite.lib.strftime("%s", "now") }, + category = { + type = "text", + reference = "category.id", + on_update = "cascade", -- means when category get updated update + on_delete = "null", -- means when category get deleted, set to null + }, + }, db) + --- overwrite + t.get = function() return t:__get({ where = {...}, select = {...} })[1] end + ``` + + + Parameters: ~ + {name} (string) table name + {schema} (sqlite_schema_dict) + {db} (sqlite_db|nil) if nil, then for it to work, it needs + setting with sqlite.tbl:set_db(). + + Return: ~ + sqlite_tbl + + +sqlite.tbl:schema({schema}) *sqlite.tbl:schema()* + Create or change table schema. If no {schema} is given, then it return + current the used schema if it exists or empty table otherwise. On change + schema it returns boolean indecting success. + + ```lua + local projects = sqlite.tbl:new("", {...}) + --- get project table schema. + projects:schema() + --- mutate project table schema with droping content if not schema.ensure + projects:schema {...} + ``` + + + Parameters: ~ + {schema} (sqlite_schema_dict) + + Return: ~ + sqlite_schema_dict | boolean + + +sqlite.tbl:drop() *sqlite.tbl:drop()* + Remove table from database, if the table is already drooped then it returns + false. + + ```lua + --- drop todos table content. + todos:drop() + ``` + + + Return: ~ + boolean + + See: ~ + |sqlite.db:drop()| + + +sqlite.tbl:empty() *sqlite.tbl:empty()* + Predicate that returns true if the table is empty. + + ```lua + if todos:empty() then + print "no more todos, we are free :D" + end + ``` + + + Return: ~ + boolean + + +sqlite.tbl:exists() *sqlite.tbl:exists()* + Predicate that returns true if the table exists. + + ```lua + if goals:exists() then + error("I'm disappointed in you :D") + end + ``` + + + Return: ~ + boolean + + +sqlite.tbl:count() *sqlite.tbl:count()* + Get the current number of rows in the table + + ```lua + if notes:count() == 0 then + print("no more notes") + end + ``` + + + Return: ~ + number + + +sqlite.tbl:get({query}) *sqlite.tbl:get()* + Query the table and return results. + + ```lua + --- get everything + todos:get() + --- get row with id of 1 + todos:get { where = { id = 1 } } + --- select a set of keys with computed one + timestamps:get { + select = { + age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60, + "id", + "timestamp", + "entry", + }, + } + ``` + + + Parameters: ~ + {query} (sqlite_query_select) + + Return: ~ + table + + See: ~ + |sqlite.db:select()| + + +sqlite.tbl:each({func}, {query}) *sqlite.tbl:each()* + Iterate over table rows and execute {func}. Returns false if no row is + returned. + + ```lua + --- Execute a function on each returned row + todos:each(function(row) + print(row.title) + end, { + where = { status = "pending" }, + contains = { title = "fix*" } + }) + + todos:each({ where = { ... }}, function(row) + print(row.title) + end) + ``` + + + Parameters: ~ + {func} (function) func(row) + {query} (sqlite_query_select|nil) + + Return: ~ + boolean + + +sqlite.tbl:map({func}, {query}) *sqlite.tbl:map()* + Create a new table from iterating over a tbl rows with {func}. + + ```lua + --- transform rows. + local rows = todos:map(function(row) + row.somekey = "" + row.f = callfunction(row) + return row + end, { + where = { status = "pending" }, + contains = { title = "fix*" } + }) + --- This works too. + local titles = todos:map({ where = { ... }}, function(row) + return row.title + end) + --- no query, no problem :D + local all = todos:map(function(row) return row.title end) + ``` + + + Parameters: ~ + {func} (function) func(row) + {query} (sqlite_query_select) + + Return: ~ + table[] + + +sqlite.tbl:sort({query}, {transform}, {comp}) *sqlite.tbl:sort()* + Sorts a table in-place using a transform. Values are ranked in a custom + order of the results of running `transform (v)` on all values. `transform` + may also be a string name property sort by. `comp` is a comparison + function. Adopted from Moses.lua + + ```lua + --- return rows sort by id. + local res = t1:sort({ where = {id = {32,12,35}}}) + --- return rows sort by age + local res = t1:sort({ where = {id = {32,12,35}}}, "age")` + --- return with custom sort function (recommended) + local res = t1:sort({where = { ... }}, "age", function(a, b) return a > b end)` + ``` + + + Parameters: ~ + {query} (sqlite_query_select|nil) + {transform} (function) a `transform` function to sort + elements. Defaults to + @{identity} + {comp} (function) a comparison function, defaults + to the `<` operator + + Return: ~ + table[] + + +sqlite.tbl:insert({rows}) *sqlite.tbl:insert()* + Insert rows into a table. + + ```lua + --- single item. + todos:insert { title = "new todo" } + --- insert multiple items, using todos table as first param + tbl.insert(todos, "items", { { name = "a"}, { name = "b" }, { name = "c" } }) + ``` + + + Parameters: ~ + {rows} (table) a row or a group of rows + + Return: ~ + integer: last inserted id + + Usage: ~ + `todos:insert { title = "stop writing examples :D" }` insert single + item. + `todos:insert { { ... }, { ... } }` insert multiple items + + See: ~ + |sqlite.db:insert()| + + +sqlite.tbl:remove({where}) *sqlite.tbl:remove()* + Delete a rows/row or table content based on {where} closure. If {where == + nil} then clear table content. + + ```lua + --- delete todos table content + todos:remove() + --- delete row that has id as 1 + todos:remove { id = 1 } + --- delete all rows that has value of id 1 or 2 or 3 + todos:remove { id = {1,2,3} } + --- matching ids or greater than 5 + todos:remove { id = {"<", 5} } -- or {id = "<5"} + ``` + + + Parameters: ~ + {where} (sqlite_query_delete) + + Return: ~ + boolean + + See: ~ + |sqlite.db:delete()| + + +sqlite.tbl:update({specs}) *sqlite.tbl:update()* + Update table row with where closure and list of values returns true incase + the table was updated successfully. + + ```lua + --- update todos status linked to project "lua-hello-world" or "rewrite-neoivm-in-rust" + todos:update { + where = { project = {"lua-hello-world", "rewrite-neoivm-in-rust"} }, + set = { status = "later" } + } + --- pass custom statement and boolean + ts:update { + where = { id = "<" .. 4 }, -- mimcs WHERE id < 4 + set = { seen = true } -- will be converted to 0. + } + ``` + + + Parameters: ~ + {specs} (sqlite_query_update) + + Return: ~ + boolean + + See: ~ + |sqlite.db:update()| + |sqlite_query_update()| + + +sqlite.tbl:replace({rows}) *sqlite.tbl:replace()* + Replaces table content with a given set of {rows}. + + ```lua + --- replace project table content with a single call + todos:replace { { ... }, { ... }, { ... }, } + + --- replace everything with a single row + ts:replace { key = "val" } + ``` + + + Parameters: ~ + {rows} (table[]|table) + + Return: ~ + boolean + + See: ~ + |sqlite.db:delete()| + |sqlite.db:insert()| + + +sqlite.tbl:set_db({db}) *sqlite.tbl:set_db()* + Changes the db object to which the sqlite_tbl correspond to. If the object + is going to be passed to |sqlite.new|, then it will be set automatically at + definition. + + + Parameters: ~ + {db} (sqlite_db) + + + + vim:tw=78:sw=2:ts=2:ft=help:norl:et:listchars= diff --git a/etc/soft/nvim/+plugins/sqlite.lua/flake.lock b/etc/soft/nvim/+plugins/sqlite.lua/flake.lock new file mode 100644 index 0000000..df3f9f6 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1653893745, + "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1654230545, + "narHash": "sha256-8Vlwf0x8ow6pPOK2a04bT+pxIeRnM1+O0Xv9/CuDzRs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "236cc2971ac72acd90f0ae3a797f9f83098b17ec", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/etc/soft/nvim/+plugins/sqlite.lua/flake.nix b/etc/soft/nvim/+plugins/sqlite.lua/flake.nix new file mode 100644 index 0000000..9808f5c --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/flake.nix @@ -0,0 +1,33 @@ +{ + description = "SQLite/LuaJIT binding for lua and neovim"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}.pkgs; + in { + + devShells = { + + default = pkgs.mkShell { + + # you will also need a neovim with plenary-nvim installed to run the + # tests + buildInputs = with pkgs; [ + luarocks + lua51Packages.luacheck + stylua + ]; + + shellHook = '' + export LIBSQLITE=${pkgs.sqlite.out}/lib/libsqlite3.so + ''; + }; + }; + }); +} diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sql.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sql.lua new file mode 100644 index 0000000..2d34ba3 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sql.lua @@ -0,0 +1,3 @@ +---TODO: add deprecation warning after open pr in a number of repos depending on sql namespace +print "DEPRECATED: use require'sqlite' instead" +return require "sqlite.db" diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/assert.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/assert.lua new file mode 100644 index 0000000..d15f573 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/assert.lua @@ -0,0 +1,83 @@ +local M = {} +local u = require "sqlite.utils" +local clib = require "sqlite.defs" + +--- Functions for asseting and erroring out :D + +local errors = { + not_sqltbl = "can not execute %s, %s doesn't exists.", + close_fail = "database connection didn't get closed, ERRMSG: %s", + eval_fail = "eval has failed to execute statement, ERRMSG: %s", + failed_ops = "operation failed, ERRMSG: %s", + missing_req_key = "(insert) missing a required key: %s", + missing_db_object = "%s's db object is not set. set it with `%s:set_db(db)` and try again.", + outdated_schema = "`%s` does not exists in {`%s`}, schema is outdateset `self.db.tbl_schemas[table_name]` or reload", + auto_alter_more_less_keys = "schema defined ~= db schema. Please drop `%s` table first or set ensure to false.", +} + +for key, value in pairs(errors) do + errors[key] = "sqlite.lua: " .. value +end + +---Error out if sql table doesn't exists. +---@param db table +---@param tbl_name string +---@param method any +---@return boolean +M.is_sqltbl = function(db, tbl_name, method) + assert(db:exists(tbl_name), errors.not_sqltbl:format(method, tbl_name)) + return true +end + +---Error out if connection didn't get closed. +---This should never happen but is used just in case +---@param conn_ptr sqlite_blob* +---@return boolean +M.should_close = function(conn_ptr, did_close) + assert(did_close, errors.close_fail:format(clib.last_errmsg(conn_ptr))) + return true +end + +---Error out if statement evaluation/executation result in +---last_errorcode ~= flags.ok +---@param conn_ptr sqlite_blob* +---@return boolean +M.should_eval = function(conn_ptr) + local no_err = clib.last_errcode(conn_ptr) == clib.flags.ok + assert(no_err, errors.eval_fail:format(clib.last_errmsg(conn_ptr))) +end + +---Check if a given ret table length is more then 0 +---This because in update we insert and expect some value +---returned 'let me id or 'boolean. +---When the ret values < 0 then the function didn't do anything. +---@param status sqlite_db_status +---@return boolean +M.should_modify = function(status) + assert(status.code == 0, errors.failed_ops:format(status.msg)) + return true +end + +M.missing_req_key = function(val, key) + assert(val ~= nil, errors.missing_req_key:format(key)) + return false +end + +M.should_have_column_def = function(column_def, k, schema) + if not column_def then + error(errors.outdated_schema:format(k, u.join(u.keys(schema), ", "))) + end +end + +M.should_have_db_object = function(db, name) + assert(db ~= nil, errors.missing_db_object:format(name, name)) + return true +end + +M.auto_alter_should_have_equal_len = function(len_new, len_old, tname) + if len_new - len_old ~= 0 then + error(errors.auto_alter_more_less_keys:format(tname)) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/db.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/db.lua new file mode 100644 index 0000000..c6cd68d --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/db.lua @@ -0,0 +1,664 @@ +---@brief [[ +---Main sqlite.lua object and methods. +---@brief ]] +---@tag sqlite.db.lua + +local u = require "sqlite.utils" +local require = u.require_on_index + +local sqlite = {} + +---@class sqlite_db @Main sqlite.lua object. +---@field uri string: database uri. it can be an environment variable or an absolute path. default ":memory:" +---@field opts sqlite_opts: see https://www.sqlite.org/pragma.html |sqlite_opts| +---@field conn sqlite_blob: sqlite connection c object. +---@field db sqlite_db: reference to fallback to when overwriting |sqlite_db| methods (extended only). +sqlite.db = {} +sqlite.db.__index = sqlite.db +sqlite.db.__version = "v1.2.2" + +local clib = require "sqlite.defs" +local s = require "sqlite.stmt" +local h = require "sqlite.helpers" +local a = require "sqlite.assert" +local p = require "sqlite.parser" +local tbl = require "sqlite.tbl" + +---Creates a new sqlite.lua object, without creating a connection to uri. +---|sqlite.new| is identical to |sqlite.db:open| but it without opening sqlite db +---connection (unless opts.keep_open). Its most suited for cases where the +---database might be -acccess from multiple places. For neovim use cases, this +---mean from different -neovim instances. +--- +---
+---```lua
+--- local db = sqlite.new("path/to/db" or "$env_var", { ... } or nil)
+--- -- configure open mode through opts.open_mode = "ro", "rw", "rwc", default "rwc"
+--- -- for more customize behaviour, set opts.open_mode to a list of db.flags
+--- -- see https://sqlite.org/c3ref/open.html#urifilenamesinsqlite3open
+---```
+---
+---@param uri string: uri to db file. +---@param opts sqlite_opts: (optional) see |sqlite_opts| +---@return sqlite_db +function sqlite.db.new(uri, opts) + opts = opts or {} + local keep_open = opts.keep_open + opts.keep_open = nil + uri = type(uri) == "string" and u.expand(uri) or ":memory:" + + local o = setmetatable({ + uri = uri, + conn = nil, + closed = true, + opts = opts, + modified = false, + created = nil, + tbl_schemas = {}, + }, sqlite.db) + + if keep_open then + o:open() + end + + return o +end + +---Extend |sqlite_db| object with extra sugar syntax and api. This is recommended +---for all sqlite use case as it provide convenience. This method is super lazy. +---it try its best to doing any ffi calls until the first operation done on a table. +--- +---In the case you want to keep db connection open and not on invocation bases. +---Run |sqlite.db:open()| right after creating the object or when you +---intend to use it. +--- +---Like |sqlite_tbl| original methods can be access through pre-appending "__" +---when user overwrites it. +--- +---if { conf.opts.lazy } then only return a logical object with self-dependent +--tables, e.g. a table exists and other not because the one that +---exists, it's method was called. (default false). +---if { conf.opts.keep_open } then the sqlite extend db object will be returned +---with an open connection (default false) + +---
+---```lua
+--- local db = sqlite { -- or sqlite_db:extend
+---   uri = "path/to/db", -- path to db file
+---   entries = require'entries',  -- a pre-made |sqlite_tbl| object.
+---   category = { title = { "text", unique = true, primary = true}  },
+---   opts = {} or nil -- custom sqlite3 options, see |sqlite_opts|
+---   --- if opts.keep_open, make connection and keep it open.
+---   --- if opts.lazy, then just provide logical object
+---   --- configure open mode through opts.open_mode = "ro", "rw", "rwc", default "rwc"
+---   --- for more customize behaviour, set opts.open_mode to a list of db.flags
+---   --- see https://sqlite.org/c3ref/open.html#urifilenamesinsqlite3open
+--- }
+--- --- Overwrite method and access it using through pre-appending "__"
+--- db.select = function(...) db:__select(...) end
+---```
+---
+---@param conf table: see 'Fields' +---@field uri string: path to db file. +---@field opts sqlite_opts: (optional) see |sqlite_opts| + lazy (default false), open (default false) +---@field tname1 string: pointing to |sqlite_etbl| or |sqlite_schema_dict| +---@field tnameN string: pointing to |sqlite_etbl| or |sqlite_schema_dict| +---@see sqlite_tbl:extend +---@return sqlite_db +function sqlite.db:extend(conf) + conf.opts = conf.opts or {} + local lazy = conf.opts.lazy + conf.opts.lazy = nil + + local db = self.new(conf.uri, conf.opts) + local cls = setmetatable({ db = db }, { + __index = function(_, key, ...) + if type(key) == "string" then + key = key:sub(1, 2) == "__" and key:sub(3, -1) or key + if db[key] then + return db[key] + end + end + end, + }) + + for tbl_name, schema in pairs(conf) do + if tbl_name ~= "uri" and tbl_name ~= "opts" and tbl_name ~= "lazy" and u.is_tbl(schema) then + local name = schema._name and schema._name or tbl_name + cls[tbl_name] = schema.set_db and schema or require("sqlite.tbl").new(name, schema, not lazy and db or nil) + if not cls[tbl_name].db then + (cls[tbl_name]):set_db(db) + end + end + end + + return cls +end + +---Creates and connect to new sqlite db object, either in memory or via a {uri}. +---If it is called on pre-made |sqlite_db| object, than it should open it. otherwise ignore. +--- +---
+---```lua
+--- -- Open db file at path or environment variable, otherwise open in memory.
+--- local db = sqlite.db:open("./pathto/dbfile" or "$ENV_VARABLE" or nil, {...})
+--- -- reopen connection if closed.
+--- db:open()
+--- -- configure open mode through opts.open_mode = "ro", "rw", "rwc", default "rwc"
+--- -- for more customize behaviour, set opts.open_mode to a list of db.flags
+--- -- see https://sqlite.org/c3ref/open.html#urifilenamesinsqlite3open
+---```
+---
+---@param uri string: (optional) {uri} == {nil} then in-memory db. +---@param opts sqlite_opts|nil: see |sqlite_opts| +---@return sqlite_db +function sqlite.db:open(uri, opts, noconn) + local d = self + if not d.uri then + d = sqlite.db.new(uri, opts) + end + + if d.closed or d.closed == nil then + d.conn = clib.connect(d.uri, d.opts) + d.created = os.date "%Y-%m-%d %H:%M:%S" + d.closed = false + end + + return d +end + +---Close sqlite db connection. returns true if closed, error otherwise. +--- +---
+---```lua
+--- local db = sqlite.db:open()
+--- db:close() -- close connection
+---```
+---
+---@return boolean +function sqlite.db:close() + self.closed = self.closed or clib.close(self.conn) == 0 + a.should_close(self.conn, self.closed) + return self.closed +end + +---Same as |sqlite.db:open| but execute {func} then closes db connection. +---If the function is called as a method to db object e.g. 'db:with_open', then +---{args[1]} must be a function. Else {args[1]} need to be the uri and {args[2]} the function. +--- +---
+---```lua
+--- -- as a function
+--- local entries = sqlite.with_open("path/to/db", function(db)
+---    return db:select("todos", { where = { status = "done" } })
+--- end)
+--- -- as a method
+--- local exists = db:with_open(function()
+---   return db:exists("projects")
+---  end)
+---```
+---
+--- +---@varargs If used as db method, then the {args[1]} should be a function, else +---{args[1]} is uri and {args[2]} is function. +---@see sqlite.db:open +---@return any +function sqlite.db:with_open(...) + local args = { ... } + if type(self) == "string" or not self then + self = sqlite.db:open(self) + end + + local func = type(args[1]) == "function" and args[1] or args[2] + + if self:isclose() then + self:open() + end + + local res = func(self) + self:close() + return res +end + +---Predict returning true if db connection is active. +--- +---
+---```lua
+--- if db:isopen() then
+---   db:close()
+--- end
+---```
+---
+---@return boolean +function sqlite.db:isopen() + return not self.closed +end + +---Predict returning true if db connection is indeed closed. +--- +---
+---```lua
+--- if db:isclose() then
+---   error("db is closed")
+--- end
+---```
+---
+---@return boolean +function sqlite.db:isclose() + return self.closed +end + +---Returns current connection status +---Get last error code +--- +---
+---```lua
+--- print(db:status().msg) -- get last error msg
+--- print(db:status().code) -- get last error code.
+---```
+---
+---@return sqlite_db_status +function sqlite.db:status() + return { + msg = clib.last_errmsg(self.conn), + code = clib.last_errcode(self.conn), + } +end + +---Evaluates a sql {statement} and if there are results from evaluation it +---returns list of rows. Otherwise it returns a boolean indecating +---whether the evaluation was successful. +--- +---
+---```lua
+--- -- evaluate without any extra arguments.
+--- db:eval("drop table if exists todos")
+--- --  evaluate with unamed value.
+--- db:eval("select * from todos where id = ?", 1)
+--- -- evaluate with named arguments.
+--- db:eval("insert into t(a, b) values(:a, :b)", {a = "1", b = 3})
+---```
+---
+---@param statement string: SQL statement. +---@param params table|nil: params to be bind to {statement} +---@return boolean|table +function sqlite.db:eval(statement, params) + local res = {} + local stmt = s:parse(self.conn, statement) + + -- when the user provide simple sql statements + if not params then + stmt:each(function() + table.insert(res, stmt:kv()) + end) + stmt:reset() + + -- when the user run eval("select * from ?", "tbl_name") + elseif type(params) ~= "table" and statement:match "%?" then + local value = p.sqlvalue(params) + stmt:bind { value } + stmt:each(function(stm) + table.insert(res, stm:kv()) + end) + stmt:reset() + stmt:bind_clear() + + -- when the user provided named keys + elseif params and type(params) == "table" then + params = type(params[1]) == "table" and params or { params } + for _, v in ipairs(params) do + stmt:bind(v) + stmt:each(function(stm) + table.insert(res, stm:kv()) + end) + stmt:reset() + stmt:bind_clear() + end + end + -- clear out the parsed statement. + stmt:finalize() + + -- if no rows is returned, then check return the result of errcode == flags.ok + res = rawequal(next(res), nil) and clib.last_errcode(self.conn) == clib.flags.ok or res + + -- fix res of its table, so that select all doesn't return { [1] = {[1] = { row }} } + if type(res) == "table" and res[2] == nil and u.is_nested(res[1]) then + res = res[1] + end + + a.should_eval(self.conn) + + self.modified = true + + return res +end + +---Execute statement without any return +--- +---
+---```lua
+--- db:execute("drop table if exists todos")
+--- db:execute("pragma foreign_keys=on")
+---```
+---
+---@param statement string: statement to be executed +---@return boolean: true if successful, error out if not. +function sqlite.db:execute(statement) + local succ = clib.exec_stmt(self.conn, statement) == 0 + return succ and succ or error(clib.last_errmsg(self.conn)) +end + +---Check if a table with {tbl_name} exists in sqlite db +---
+---```lua
+--- if not db:exists("todo_tbl") then
+---   error("Table doesn't exists!!!")
+--- end
+---```
+---
+---@param tbl_name string: the table name. +---@return boolean +function sqlite.db:exists(tbl_name) + local q = self:eval("select name from sqlite_master where name= ?", tbl_name) + return type(q) == "table" and true or false +end + +---Create a new sqlite db table with {name} based on {schema}. if {schema.ensure} then +---create only when it does not exists. similar to 'create if not exists'. +--- +---
+---```lua
+--- db:create("todos", {
+---   id = {"int", "primary", "key"},
+---   title = "text",
+---   name = { type = "string", reference = "sometbl.id" },
+---   ensure = true -- create table if it doesn't already exists (THIS IS DEFUAULT)
+--- })
+---```
+---
+---@param tbl_name string: table name +---@param schema sqlite_schema_dict +---@return boolean +function sqlite.db:create(tbl_name, schema) + local req = p.create(tbl_name, schema) + if req:match "reference" then + self:execute "pragma foreign_keys = ON" + self.opts.foreign_keys = true + end + return self:eval(req) +end + +---Remove {tbl_name} from database +--- +---
+---```lua
+--- if db:exists("todos") then
+---   db:drop("todos")
+--- end
+---```
+---
+---@param tbl_name string: table name +---@return boolean +function sqlite.db:drop(tbl_name) + self.tbl_schemas[tbl_name] = nil + return self:eval(p.drop(tbl_name)) +end + +---Get {name} table schema, if table does not exist then return an empty table. +--- +---
+---```lua
+--- if db:exists("todos") then
+---   inspect(db:schema("todos").project)
+--- else
+---   print("create me")
+--- end
+---```
+---
+---@param tbl_name string: the table name. +---@return sqlite_schema_dict +function sqlite.db:schema(tbl_name) + local sch = self:eval(("pragma table_info(%s)"):format(tbl_name)) + local schema = {} + for _, v in ipairs(type(sch) == "boolean" and {} or sch) do + schema[v.name] = { + cid = v.cid, + required = v.notnull == 1, + primary = v.pk == 1, + type = v.type, + default = v.dflt_value, + } + end + return schema +end + +---Insert lua table into sqlite database table. +--- +---
+---```lua
+--- --- single item.
+--- db:insert("todos", { title = "new todo" })
+--- --- insert multiple items.
+--- db:insert("items", {  { name = "a"}, { name = "b" }, { name = "c" } })
+---```
+---
+---@param tbl_name string: the table name +---@param rows table: rows to insert to the table. +---@return boolean|integer: boolean (true == success), and the last inserted row id. +function sqlite.db:insert(tbl_name, rows, schema) + a.is_sqltbl(self, tbl_name, "insert") + local ret_vals = {} + schema = schema and schema or h.get_schema(tbl_name, self) + local items = p.pre_insert(rows, schema) + local last_rowid + clib.wrap_stmts(self.conn, function() + for _, v in ipairs(items) do + local stmt = s:parse(self.conn, p.insert(tbl_name, { values = v })) + stmt:bind(v) + stmt:step() + stmt:bind_clear() + table.insert(ret_vals, stmt:finalize()) + end + last_rowid = tonumber(clib.last_insert_rowid(self.conn)) + end) + + local succ = u.all(ret_vals, function(_, v) + return v + end) + if succ then + self.modified = true + end + return succ, last_rowid +end + +---Update table row with where closure and list of values +---returns true incase the table was updated successfully. +--- +---
+---```lua
+--- --- update todos status linked to project "lua-hello-world" or "rewrite-neoivm-in-rust"
+--- db:update("todos", {
+---   where = { project = {"lua-hello-world", "rewrite-neoivm-in-rust"} },
+---   set = { status = "later" }
+--- })
+---
+--- --- pass custom statement and boolean
+--- db:update("timestamps", {
+---   where = { id = "<" .. 4 }, -- mimcs WHERE id < 4
+---   set = { seen = true } -- will be converted to 0.
+--- })
+---```
+---
+---@param tbl_name string: sqlite table name. +---@param specs sqlite_query_update | sqlite_query_update[] +---@return boolean +function sqlite.db:update(tbl_name, specs, schema) + a.is_sqltbl(self, tbl_name, "update") + if not specs then + return false + end + + return clib.wrap_stmts(self.conn, function() + specs = u.is_nested(specs) and specs or { specs } + schema = schema and schema or h.get_schema(tbl_name, self) + + local ret_val = nil + for _, v in ipairs(specs) do + v.set = v.set and v.set or v.values + if self:select(tbl_name, { where = v.where })[1] then + local stmt = s:parse(self.conn, p.update(tbl_name, { set = v.set, where = v.where })) + stmt:bind(p.pre_insert(v.set, schema)[1]) + stmt:step() + stmt:reset() + stmt:bind_clear() + stmt:finalize() + a.should_modify(self:status()) + ret_val = true + else + ret_val = self:insert(tbl_name, u.tbl_extend("keep", v.set, v.where)) + a.should_modify(self:status()) + end + end + self.modified = true + return ret_val + end) +end + +---Delete a {tbl_name} row/rows based on the {where} closure. If {where == nil} +---then all the {tbl_name} content will be deleted. +--- +---
+---```lua
+--- --- delete todos table content
+--- db:delete("todos")
+--- --- delete row that has id as 1
+--- db:delete("todos", { id = 1 })
+--- --- delete all rows that has value of id 1 or 2 or 3
+--- db:delete("todos", { id = {1,2,3} })
+--- --- matching ids or greater than 5
+--- db:delete("todos", { id = {"<", 5} }) -- or {id = "<5"}
+---```
+---
+---@param tbl_name string: sqlite table name +---@param where sqlite_query_delete: key value pair to where delete operation should effect. +---@todo support querys with `and` +---@return boolean: true if operation is successfully, false otherwise. +function sqlite.db:delete(tbl_name, where) + a.is_sqltbl(self, tbl_name, "delete") + + if not where then + return self:execute(p.delete(tbl_name)) + end + + where = u.is_nested(where) and where or { where } + clib.wrap_stmts(self.conn, function() + for _, spec in ipairs(where) do + local _where = spec.where and spec.where or spec + local stmt = s:parse(self.conn, p.delete(tbl_name, { where = _where })) + stmt:step() + stmt:reset() + stmt:finalize() + a.should_modify(self:status()) + end + end) + + self.modified = true + + return true +end + +---Query from a table with where and join options +--- +---
+---```lua
+--- db:select("todos") get everything
+--- --- get row with id of 1
+--- db:select("todos", { where = { id = 1 })
+--- ---  get row with status value of later or paused
+--- db:select("todos", { where = { status = {"later", "paused"} })
+--- --- get 5 items from todos table
+--- db:select("todos", { limit = 5 })
+--- --- select a set of keys with computed one
+--- db:select("timestamps", {
+---   select = {
+---     age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60,
+---     "id",
+---     "timestamp",
+---     "entry",
+---     },
+---   })
+---```
+---
+---@param tbl_name string: the name of the db table to select on +---@param spec sqlite_query_select +---@return table[] +function sqlite.db:select(tbl_name, spec, schema) + a.is_sqltbl(self, tbl_name, "select") + return clib.wrap_stmts(self.conn, function() + local ret = {} + schema = schema and schema or h.get_schema(tbl_name, self) + + spec = spec or {} + spec.select = spec.keys and spec.keys or spec.select + + local stmt = s:parse(self.conn, p.select(tbl_name, spec)) + s.each(stmt, function() + table.insert(ret, s.kv(stmt)) + end) + s.reset(stmt) + if s.finalize(stmt) then + self.modified = false + end + return p.post_select(ret, schema) + end) +end + +---Create new sqlite_tbl object. +---If {opts}.ensure = false, then on each run it will drop the table and recreate it. +---NOTE: this might change someday to alter the table instead. For now +---alteration is auto and limited to field schema edits and renames +--- +---
+---```lua
+--- local tbl = db:tbl("todos", {
+---   id = true, -- same as { type = "integer", required = true, primary = true }
+---   title = "text",
+---   category = {
+---     type = "text",
+---     reference = "category.id",
+---     on_update = "cascade", -- means when category get updated update
+---     on_delete = "null", -- means when category get deleted, set to null
+---   },
+--- })
+--- --- or without db injected and use as wrapper for |sqlite.tbl.new|
+--- local tbl = db.tbl("name", ...)
+---```
+---
+---@param tbl_name string: the name of the table. can be new or existing one. +---@param schema sqlite_schema_dict: {schema, ensure (defalut true)} +---@see |sqlite.tbl.new| +---@return sqlite_tbl +function sqlite.db:tbl(tbl_name, schema) + if type(self) == "string" then + schema = tbl_name + return tbl.new(self, schema) + end + return tbl.new(tbl_name, schema, self) +end + +---DEPRECATED +function sqlite.db:table(tbl_name, opts) + print "sqlite.lua sqlite:table is deprecated use sqlite:tbl instead" + return self:tbl(tbl_name, opts) +end + +---Sqlite functions sugar wrappers. See `sql/strfun` +sqlite.db.lib = require "sqlite.strfun" + +sqlite.db = setmetatable(sqlite.db, { + __call = sqlite.db.extend, +}) + +sqlite.db.flags = clib.flags + +return sqlite.db diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/defs.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/defs.lua new file mode 100644 index 0000000..ff97a8f --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/defs.lua @@ -0,0 +1,729 @@ +local ffi = require "ffi" +local bit = require "bit" +local luv = require "luv" +local M = {} + +--- Load clib +local clib = (function() + local path, _ + + if vim then + if vim.g.sql_clib_path then + error [[ sqlite.lua: vim.g.sql_clib_path is deprecated. Use vim.g.sqlite_clib_path instead. ]] + end + path = vim.g.sqlite_clib_path + end + + if not path then + path, _ = luv.os_getenv "LIBSQLITE" + end + + local clib_path = path + or (function() --- try to find libsqlite.Linux and Macos support only. + local os = luv.os_uname() + + local file_exists = function(file_path) + local f = io.open(file_path, "r") + if f ~= nil then + io.close(f) + return true + else + return false + end + end + + if os.sysname == "Linux" then + local linux_paths = { + "/usr/lib/x86_64-linux-gnu/libsqlite3.so", + "/usr/lib64/libsqlite3.so", + "/usr/lib/libsqlite3.so", + } + for _, v in pairs(linux_paths) do + if file_exists(v) then + return v + end + end + end + + if os.sysname == "Darwin" then + return os.machine == "arm64" and "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib" + or "/usr/local/opt/sqlite3/lib/libsqlite3.dylib" + end + end)() + + return ffi.load(clib_path or "libsqlite3") +end)() + +---@type sqlite_flags +M.flags = { + -- Result codes + ["ok"] = 0, + ["error"] = 1, + ["internal"] = 2, + ["perm"] = 3, + ["abort"] = 4, + ["busy"] = 5, + ["locked"] = 6, + ["nomem"] = 7, + ["readonly"] = 8, + ["interrupt"] = 9, + ["ioerr"] = 10, + ["corrupt"] = 11, + ["notfound"] = 12, + ["full"] = 13, + ["cantopen"] = 14, + ["protocol"] = 15, + ["empty"] = 16, + ["schema"] = 17, + ["toobig"] = 18, + ["constraint"] = 19, + ["mismatch"] = 20, + ["misuse"] = 21, + ["nolfs"] = 22, + ["auth"] = 23, + ["format"] = 24, + ["range"] = 25, + ["notadb"] = 26, + ["notice"] = 27, + ["warning"] = 28, + ["row"] = 100, + ["done"] = 101, +} + +---@type sqlite_db.opts +M.valid_pargma = { + ["analysis_limit"] = true, + ["application_id"] = true, + ["auto_vacuum"] = true, + ["automatic_index"] = true, + ["busy_timeout"] = true, + + ["cache_size"] = true, + ["cache_spill"] = true, + ["case_sensitive_like"] = true, + ["cell_size_check"] = true, + ["checkpoint_fullfsync"] = true, + + ["collation_list"] = true, + ["compile_options"] = true, + ["data_version"] = true, + ["database_list"] = true, + ["encoding"] = true, + ["foreign_key_check"] = true, + + ["foreign_key_list"] = true, + ["foreign_keys"] = true, + ["freelist_count"] = true, + ["fullfsync"] = true, + ["function_list"] = true, + + ["hard_heap_limit"] = true, + ["ignore_check_constraints"] = true, + ["incremental_vacuum"] = true, + ["index_info"] = true, + ["index_list"] = true, + + ["index_xinfo"] = true, + ["integrity_check"] = true, + ["journal_mode"] = true, + ["journal_size_limit"] = true, + ["legacy_alter_table"] = true, + + ["legacy_file_format"] = true, + ["locking_mode"] = true, + ["max_page_count"] = true, + ["mmap_size"] = true, + ["module_list"] = true, + ["optimize"] = true, + + ["page_count"] = true, + ["page_size"] = true, + ["parser_trace"] = true, + ["pragma_list"] = true, + ["query_only"] = true, + ["quick_check"] = true, + + ["read_uncommitted"] = true, + ["recursive_triggers"] = true, + ["reverse_unordered_selects"] = true, + ["schema_version"] = true, + ["secure_delete"] = true, + + ["shrink_memory"] = true, + ["soft_heap_limit"] = true, + ["stats"] = true, + ["synchronous"] = true, + ["table_info"] = true, + ["table_xinfo"] = true, + ["temp_store"] = true, + + ["vdbe_trace"] = true, + ["wal_autocheckpoint"] = true, + ["wal_checkpoint"] = true, + ["writable_schema"] = true, + + ["threads"] = true, + ["trusted_schema"] = true, + ["user_version"] = true, + ["vdbe_addoptrace"] = true, + ["vdbe_debug"] = true, + ["vdbe_listing"] = true, +} + +-- Extended Result Codes +M.flags["error_missing_collseq"] = bit.bor(M.flags.error, bit.lshift(1, 8)) +M.flags["error_retry"] = bit.bor(M.flags.error, bit.lshift(2, 8)) +M.flags["error_snapshot"] = bit.bor(M.flags.error, bit.lshift(3, 8)) +M.flags["ioerr_read"] = bit.bor(M.flags.ioerr, bit.lshift(1, 8)) +M.flags["ioerr_short_read"] = bit.bor(M.flags.ioerr, bit.lshift(2, 8)) +M.flags["ioerr_write"] = bit.bor(M.flags.ioerr, bit.lshift(3, 8)) +M.flags["ioerr_fsync"] = bit.bor(M.flags.ioerr, bit.lshift(4, 8)) +M.flags["ioerr_dir_fsync"] = bit.bor(M.flags.ioerr, bit.lshift(5, 8)) +M.flags["ioerr_truncate"] = bit.bor(M.flags.ioerr, bit.lshift(6, 8)) +M.flags["ioerr_fstat"] = bit.bor(M.flags.ioerr, bit.lshift(7, 8)) +M.flags["ioerr_unlock"] = bit.bor(M.flags.ioerr, bit.lshift(8, 8)) +M.flags["ioerr_rdlock"] = bit.bor(M.flags.ioerr, bit.lshift(9, 8)) +M.flags["ioerr_delete"] = bit.bor(M.flags.ioerr, bit.lshift(10, 8)) +M.flags["ioerr_blocked"] = bit.bor(M.flags.ioerr, bit.lshift(11, 8)) +M.flags["ioerr_nomem"] = bit.bor(M.flags.ioerr, bit.lshift(12, 8)) +M.flags["ioerr_access"] = bit.bor(M.flags.ioerr, bit.lshift(13, 8)) +M.flags["ioerr_checkreservedlock"] = bit.bor(M.flags.ioerr, bit.lshift(14, 8)) +M.flags["ioerr_lock"] = bit.bor(M.flags.ioerr, bit.lshift(15, 8)) +M.flags["ioerr_close"] = bit.bor(M.flags.ioerr, bit.lshift(16, 8)) +M.flags["ioerr_dir_close"] = bit.bor(M.flags.ioerr, bit.lshift(17, 8)) +M.flags["ioerr_shmopen"] = bit.bor(M.flags.ioerr, bit.lshift(18, 8)) +M.flags["ioerr_shmsize"] = bit.bor(M.flags.ioerr, bit.lshift(19, 8)) +M.flags["ioerr_shmlock"] = bit.bor(M.flags.ioerr, bit.lshift(20, 8)) +M.flags["ioerr_shmmap"] = bit.bor(M.flags.ioerr, bit.lshift(21, 8)) +M.flags["ioerr_seek"] = bit.bor(M.flags.ioerr, bit.lshift(22, 8)) +M.flags["ioerr_delete_noent"] = bit.bor(M.flags.ioerr, bit.lshift(23, 8)) +M.flags["ioerr_mmap"] = bit.bor(M.flags.ioerr, bit.lshift(24, 8)) +M.flags["ioerr_gettemppath"] = bit.bor(M.flags.ioerr, bit.lshift(25, 8)) +M.flags["ioerr_convpath"] = bit.bor(M.flags.ioerr, bit.lshift(26, 8)) +M.flags["ioerr_vnode"] = bit.bor(M.flags.ioerr, bit.lshift(27, 8)) +M.flags["ioerr_auth"] = bit.bor(M.flags.ioerr, bit.lshift(28, 8)) +M.flags["ioerr_begin_atomic"] = bit.bor(M.flags.ioerr, bit.lshift(29, 8)) +M.flags["ioerr_commit_atomic"] = bit.bor(M.flags.ioerr, bit.lshift(30, 8)) +M.flags["ioerr_rollback_atomic"] = bit.bor(M.flags.ioerr, bit.lshift(31, 8)) +M.flags["ioerr_data"] = bit.bor(M.flags.ioerr, bit.lshift(32, 8)) +M.flags["ioerr_corruptfs"] = bit.bor(M.flags.ioerr, bit.lshift(33, 8)) +M.flags["locked_sharedcache"] = bit.bor(M.flags.locked, bit.lshift(1, 8)) +M.flags["locked_vtab"] = bit.bor(M.flags.locked, bit.lshift(2, 8)) +M.flags["busy_recovery"] = bit.bor(M.flags.busy, bit.lshift(1, 8)) +M.flags["busy_snapshot"] = bit.bor(M.flags.busy, bit.lshift(2, 8)) +M.flags["busy_timeout"] = bit.bor(M.flags.busy, bit.lshift(3, 8)) +M.flags["cantopen_notempdir"] = bit.bor(M.flags.cantopen, bit.lshift(1, 8)) +M.flags["cantopen_isdir"] = bit.bor(M.flags.cantopen, bit.lshift(2, 8)) +M.flags["cantopen_fullpath"] = bit.bor(M.flags.cantopen, bit.lshift(3, 8)) +M.flags["cantopen_convpath"] = bit.bor(M.flags.cantopen, bit.lshift(4, 8)) +M.flags["cantopen_dirtywal"] = bit.bor(M.flags.cantopen, bit.lshift(5, 8)) +M.flags["cantopen_symlink"] = bit.bor(M.flags.cantopen, bit.lshift(6, 8)) +M.flags["corrupt_vtab"] = bit.bor(M.flags.corrupt, bit.lshift(1, 8)) +M.flags["corrupt_sequence"] = bit.bor(M.flags.corrupt, bit.lshift(2, 8)) +M.flags["corrupt_index"] = bit.bor(M.flags.corrupt, bit.lshift(3, 8)) +M.flags["readonly_recovery"] = bit.bor(M.flags.readonly, bit.lshift(1, 8)) +M.flags["readonly_cantlock"] = bit.bor(M.flags.readonly, bit.lshift(2, 8)) +M.flags["readonly_rollback"] = bit.bor(M.flags.readonly, bit.lshift(3, 8)) +M.flags["readonly_dbmoved"] = bit.bor(M.flags.readonly, bit.lshift(4, 8)) +M.flags["readonly_cantinit"] = bit.bor(M.flags.readonly, bit.lshift(5, 8)) +M.flags["readonly_directory"] = bit.bor(M.flags.readonly, bit.lshift(6, 8)) +M.flags["abort_rollback"] = bit.bor(M.flags.abort, bit.lshift(2, 8)) +M.flags["constraint_check"] = bit.bor(M.flags.constraint, bit.lshift(1, 8)) +M.flags["constraint_commithook"] = bit.bor(M.flags.constraint, bit.lshift(2, 8)) +M.flags["constraint_foreignkey"] = bit.bor(M.flags.constraint, bit.lshift(3, 8)) +M.flags["constraint_function"] = bit.bor(M.flags.constraint, bit.lshift(4, 8)) +M.flags["constraint_notnull"] = bit.bor(M.flags.constraint, bit.lshift(5, 8)) +M.flags["constraint_primarykey"] = bit.bor(M.flags.constraint, bit.lshift(6, 8)) +M.flags["constraint_trigger"] = bit.bor(M.flags.constraint, bit.lshift(7, 8)) +M.flags["constraint_unique"] = bit.bor(M.flags.constraint, bit.lshift(8, 8)) +M.flags["constraint_vtab"] = bit.bor(M.flags.constraint, bit.lshift(9, 8)) +M.flags["constraint_rowid"] = bit.bor(M.flags.constraint, bit.lshift(10, 8)) +M.flags["constraint_pinned"] = bit.bor(M.flags.constraint, bit.lshift(11, 8)) +M.flags["notice_recover_wal"] = bit.bor(M.flags.notice, bit.lshift(1, 8)) +M.flags["notice_recover_rollback"] = bit.bor(M.flags.notice, bit.lshift(2, 8)) +M.flags["warning_autoindex"] = bit.bor(M.flags.warning, bit.lshift(1, 8)) +M.flags["auth_user"] = bit.bor(M.flags.auth, bit.lshift(1, 8)) +M.flags["ok_load_permanently"] = bit.bor(M.flags.ok, bit.lshift(1, 8)) +M.flags["ok_symlink"] = bit.bor(M.flags.ok, bit.lshift(2, 8)) + +-- Flags for file open operations. +M.flags["open_readonly"] = 0x00000001 +M.flags["open_readwrite"] = 0x00000002 +M.flags["open_create"] = 0x00000004 +M.flags["open_deleteonclose"] = 0x00000008 +M.flags["open_exclusive"] = 0x00000010 +M.flags["open_autoproxy"] = 0x00000020 +M.flags["open_uri"] = 0x00000040 +M.flags["open_memory"] = 0x00000080 +M.flags["open_main_db"] = 0x00000100 +M.flags["open_temp_db"] = 0x00000200 +M.flags["open_transient_db"] = 0x00000400 +M.flags["open_main_journal"] = 0x00000800 +M.flags["open_temp_journal"] = 0x00001000 +M.flags["open_subjournal"] = 0x00002000 +M.flags["open_super_journal"] = 0x00004000 +M.flags["open_nomutex"] = 0x00008000 +M.flags["open_fullmutex"] = 0x00010000 +M.flags["open_sharedcache"] = 0x00020000 +M.flags["open_privatecache"] = 0x00040000 +M.flags["open_wal"] = 0x00080000 +M.flags["open_nofollow"] = 0x01000000 + +-- Fundamental Datatypes +M.flags["integer"] = 1 +M.flags["float"] = 2 +M.flags["text"] = 3 +M.flags["blob"] = 4 +M.flags["null"] = 5 + +-- Types +ffi.cdef [[ + typedef struct sqlite3 sqlite3; + + typedef __int64 sqlite_int64; + typedef unsigned __int64 sqlite_uint64; + + typedef sqlite_int64 sqlite3_int64; + typedef sqlite_uint64 sqlite3_uint64; + + typedef struct sqlite3_file sqlite3_file; + typedef struct sqlite3_stmt sqlite3_stmt; + + typedef struct sqlite3_value sqlite3_value; + typedef struct sqlite3_context sqlite3_context; + + typedef struct sqlite3_vtab sqlite3_vtab; + typedef struct sqlite3_vtab_cursor sqlite3_vtab_cursor; + + typedef struct sqlite3_blob sqlite3_blob; + + typedef struct sqlite3_str sqlite3_str; + typedef struct sqlite3_backup sqlite3_backup; +]] + +-- Functions +ffi.cdef [[ + const char *sqlite3_libversion(void); + const char *sqlite3_sourceid(void); + int sqlite3_libversion_number(void); + + int sqlite3_threadsafe(void); + + int sqlite3_close(sqlite3*); + int sqlite3_close_v2(sqlite3*); + + int sqlite3_exec(sqlite3*, const char *sql, int (*callback)(void*,int,char**,char**), void *, char **errmsg); + + int sqlite3_initialize(void); + int sqlite3_shutdown(void); + int sqlite3_os_init(void); + int sqlite3_os_end(void); + + int sqlite3_config(int, ...); + int sqlite3_db_config(sqlite3*, int op, ...); + + int sqlite3_extended_result_codes(sqlite3*, int onoff); + + sqlite3_int64 sqlite3_last_insert_rowid(sqlite3*); + void sqlite3_set_last_insert_rowid(sqlite3*,sqlite3_int64); + + int sqlite3_changes(sqlite3*); + int sqlite3_total_changes(sqlite3*); + + void sqlite3_interrupt(sqlite3*); + + int sqlite3_complete(const char *sql); + int sqlite3_complete16(const void *sql); + + int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*); + int sqlite3_busy_timeout(sqlite3*, int ms); + + int sqlite3_get_table(sqlite3 *db, const char *zSql, char ***pazResult, int *pnRow, int *pnColumn, char **pzErrmsg); + void sqlite3_free_table(char **result); + + sqlite3_int64 sqlite3_memory_used(void); + sqlite3_int64 sqlite3_memory_highwater(int resetFlag); + + void sqlite3_randomness(int N, void *P); + + int sqlite3_set_authorizer(sqlite3*, int (*xAuth)(void*,int,const char*,const char*,const char*,const char*), + void *pUserData); + + int sqlite3_trace_v2(sqlite3*, unsigned uMask, int(*xCallback)(unsigned,void*,void*,void*), void *pCtx); + + void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*); + + int sqlite3_open(const char *filename, sqlite3 **ppDb); + int sqlite3_open16(const void *filename, sqlite3 **ppDb); + int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs); + + const char *sqlite3_uri_parameter(const char *zFilename, const char *zParam); + int sqlite3_uri_boolean(const char *zFile, const char *zParam, int bDefault); + sqlite3_int64 sqlite3_uri_int64(const char*, const char*, sqlite3_int64); + const char *sqlite3_uri_key(const char *zFilename, int N); + + const char *sqlite3_filename_database(const char*); + const char *sqlite3_filename_journal(const char*); + const char *sqlite3_filename_wal(const char*); + + sqlite3_file *sqlite3_database_file_object(const char*); + + char *sqlite3_create_filename(const char *zDatabase, const char *zJournal, const char *zWal, int nParam, + const char **azParam); + void sqlite3_free_filename(char*); + + int sqlite3_errcode(sqlite3 *db); + int sqlite3_extended_errcode(sqlite3 *db); + const char *sqlite3_errmsg(sqlite3*); + const void *sqlite3_errmsg16(sqlite3*); + const char *sqlite3_errstr(int); + + int sqlite3_limit(sqlite3*, int id, int newVal); + + int sqlite3_prepare(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail); + int sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail); + int sqlite3_prepare_v3(sqlite3 *db, const char *zSql, int nByte, unsigned int prepFlags, sqlite3_stmt **ppStmt, + const char **pzTail); + int sqlite3_prepare16(sqlite3 *db, const void *zSql, int nByte, sqlite3_stmt **ppStmt, const void **pzTail); + int sqlite3_prepare16_v2(sqlite3 *db, const void *zSql, int nByte, sqlite3_stmt **ppStmt, const void **pzTail); + int sqlite3_prepare16_v3(sqlite3 *db, const void *zSql, int nByte, unsigned int prepFlags, sqlite3_stmt **ppStmt, + const void **pzTail); + + const char *sqlite3_sql(sqlite3_stmt *pStmt); + char *sqlite3_expanded_sql(sqlite3_stmt *pStmt); + const char *sqlite3_normalized_sql(sqlite3_stmt *pStmt); + + int sqlite3_stmt_readonly(sqlite3_stmt *pStmt); + int sqlite3_stmt_isexplain(sqlite3_stmt *pStmt); + int sqlite3_stmt_busy(sqlite3_stmt*); + + int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*)); + int sqlite3_bind_blob64(sqlite3_stmt*, int, const void*, sqlite3_uint64, + void(*)(void*)); + int sqlite3_bind_double(sqlite3_stmt*, int, double); + int sqlite3_bind_int(sqlite3_stmt*, int, int); + int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64); + int sqlite3_bind_null(sqlite3_stmt*, int); + int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*)); + int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void(*)(void*)); + int sqlite3_bind_text64(sqlite3_stmt*, int, const char*, sqlite3_uint64, + void(*)(void*), unsigned char encoding); + int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*); + int sqlite3_bind_pointer(sqlite3_stmt*, int, void*, const char*,void(*)(void*)); + int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n); + int sqlite3_bind_zeroblob64(sqlite3_stmt*, int, sqlite3_uint64); + + int sqlite3_bind_parameter_count(sqlite3_stmt*); + const char *sqlite3_bind_parameter_name(sqlite3_stmt*, int); + int sqlite3_bind_parameter_index(sqlite3_stmt*, const char *zName); + int sqlite3_clear_bindings(sqlite3_stmt*); + int sqlite3_column_count(sqlite3_stmt *pStmt); + const char *sqlite3_column_name(sqlite3_stmt*, int N); + const void *sqlite3_column_name16(sqlite3_stmt*, int N); + + const char *sqlite3_column_database_name(sqlite3_stmt*,int); + const void *sqlite3_column_database_name16(sqlite3_stmt*,int); + const char *sqlite3_column_table_name(sqlite3_stmt*,int); + const void *sqlite3_column_table_name16(sqlite3_stmt*,int); + const char *sqlite3_column_origin_name(sqlite3_stmt*,int); + const void *sqlite3_column_origin_name16(sqlite3_stmt*,int); + + const char *sqlite3_column_decltype(sqlite3_stmt*,int); + const void *sqlite3_column_decltype16(sqlite3_stmt*,int); + + int sqlite3_step(sqlite3_stmt*); + int sqlite3_data_count(sqlite3_stmt *pStmt); + + const void *sqlite3_column_blob(sqlite3_stmt*, int iCol); + double sqlite3_column_double(sqlite3_stmt*, int iCol); + int sqlite3_column_int(sqlite3_stmt*, int iCol); + sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol); + const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol); + const void *sqlite3_column_text16(sqlite3_stmt*, int iCol); + sqlite3_value *sqlite3_column_value(sqlite3_stmt*, int iCol); + int sqlite3_column_bytes(sqlite3_stmt*, int iCol); + int sqlite3_column_bytes16(sqlite3_stmt*, int iCol); + int sqlite3_column_type(sqlite3_stmt*, int iCol); + + int sqlite3_finalize(sqlite3_stmt *pStmt); + int sqlite3_reset(sqlite3_stmt *pStmt); + + const void *sqlite3_value_blob(sqlite3_value*); + double sqlite3_value_double(sqlite3_value*); + int sqlite3_value_int(sqlite3_value*); + sqlite3_int64 sqlite3_value_int64(sqlite3_value*); + void *sqlite3_value_pointer(sqlite3_value*, const char*); + const unsigned char *sqlite3_value_text(sqlite3_value*); + const void *sqlite3_value_text16(sqlite3_value*); + const void *sqlite3_value_text16le(sqlite3_value*); + const void *sqlite3_value_text16be(sqlite3_value*); + int sqlite3_value_bytes(sqlite3_value*); + int sqlite3_value_bytes16(sqlite3_value*); + int sqlite3_value_type(sqlite3_value*); + int sqlite3_value_numeric_type(sqlite3_value*); + int sqlite3_value_nochange(sqlite3_value*); + int sqlite3_value_frombind(sqlite3_value*); + + unsigned int sqlite3_value_subtype(sqlite3_value*); + + sqlite3_value *sqlite3_value_dup(const sqlite3_value*); + void sqlite3_value_free(sqlite3_value*); + + void *sqlite3_aggregate_context(sqlite3_context*, int nBytes); + + void *sqlite3_user_data(sqlite3_context*); + + sqlite3 *sqlite3_context_db_handle(sqlite3_context*); + + void *sqlite3_get_auxdata(sqlite3_context*, int N); + void sqlite3_set_auxdata(sqlite3_context*, int N, void*, void (*)(void*)); + + void sqlite3_result_blob(sqlite3_context*, const void*, int, void(*)(void*)); + void sqlite3_result_blob64(sqlite3_context*,const void*, + sqlite3_uint64,void(*)(void*)); + void sqlite3_result_double(sqlite3_context*, double); + void sqlite3_result_error(sqlite3_context*, const char*, int); + void sqlite3_result_error16(sqlite3_context*, const void*, int); + void sqlite3_result_error_toobig(sqlite3_context*); + void sqlite3_result_error_nomem(sqlite3_context*); + void sqlite3_result_error_code(sqlite3_context*, int); + void sqlite3_result_int(sqlite3_context*, int); + void sqlite3_result_int64(sqlite3_context*, sqlite3_int64); + void sqlite3_result_null(sqlite3_context*); + void sqlite3_result_text(sqlite3_context*, const char*, int, void(*)(void*)); + void sqlite3_result_text64(sqlite3_context*, const char*,sqlite3_uint64, + void(*)(void*), unsigned char encoding); + void sqlite3_result_text16(sqlite3_context*, const void*, int, void(*)(void*)); + void sqlite3_result_text16le(sqlite3_context*, const void*, int,void(*)(void*)); + void sqlite3_result_text16be(sqlite3_context*, const void*, int,void(*)(void*)); + void sqlite3_result_value(sqlite3_context*, sqlite3_value*); + void sqlite3_result_pointer(sqlite3_context*, void*,const char*,void(*)(void*)); + void sqlite3_result_zeroblob(sqlite3_context*, int n); + int sqlite3_result_zeroblob64(sqlite3_context*, sqlite3_uint64 n); + + void sqlite3_result_subtype(sqlite3_context*,unsigned int); + + int sqlite3_sleep(int); + + int sqlite3_get_autocommit(sqlite3*); + sqlite3 *sqlite3_db_handle(sqlite3_stmt*); + const char *sqlite3_db_filename(sqlite3 *db, const char *zDbName); + int sqlite3_db_readonly(sqlite3 *db, const char *zDbName); + int sqlite3_txn_state(sqlite3*,const char *zSchema); + + sqlite3_stmt *sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt); + + void *sqlite3_commit_hook(sqlite3*, int(*)(void*), void*); + void *sqlite3_rollback_hook(sqlite3*, void(*)(void *), void*); + + void *sqlite3_update_hook(sqlite3*, void(*)(void *,int ,char const *,char const *,sqlite3_int64), void*); + + int sqlite3_enable_shared_cache(int); + int sqlite3_release_memory(int); + int sqlite3_db_release_memory(sqlite3*); + + sqlite3_int64 sqlite3_soft_heap_limit64(sqlite3_int64 N); + sqlite3_int64 sqlite3_hard_heap_limit64(sqlite3_int64 N); + + int sqlite3_table_column_metadata(sqlite3 *db, const char *zDbName, const char *zTableName, const char *zColumnName, + char const **pzDataType, char const **pzCollSeq, int *pNotNull, int *pPrimaryKey, int *pAutoinc); + + int sqlite3_load_extension(sqlite3 *db, const char *zFile, const char *zProc, char **pzErrMsg); + + int sqlite3_enable_load_extension(sqlite3 *db, int onoff); + int sqlite3_auto_extension(void(*xEntryPoint)(void)); + int sqlite3_cancel_auto_extension(void(*xEntryPoint)(void)); + void sqlite3_reset_auto_extension(void); + + int sqlite3_declare_vtab(sqlite3*, const char *zSQL); + int sqlite3_overload_function(sqlite3*, const char *zFuncName, int nArg); + + int sqlite3_blob_open(sqlite3*, const char *zDb, const char *zTable, const char *zColumn, sqlite3_int64 iRow, + int flags, sqlite3_blob **ppBlob); + int sqlite3_blob_reopen(sqlite3_blob *, sqlite3_int64); + int sqlite3_blob_close(sqlite3_blob *); + int sqlite3_blob_bytes(sqlite3_blob *); + int sqlite3_blob_read(sqlite3_blob *, void *Z, int N, int iOffset); + int sqlite3_blob_write(sqlite3_blob *, const void *z, int n, int iOffset); + + int sqlite3_file_control(sqlite3*, const char *zDbName, int op, void*); + int sqlite3_test_control(int op, ...); + + int sqlite3_keyword_count(void); + int sqlite3_keyword_name(int,const char**,int*); + int sqlite3_keyword_check(const char*,int); + + sqlite3_str *sqlite3_str_new(sqlite3*); + char *sqlite3_str_finish(sqlite3_str*); + + void sqlite3_str_appendf(sqlite3_str*, const char *zFormat, ...); + void sqlite3_str_vappendf(sqlite3_str*, const char *zFormat, va_list); + void sqlite3_str_append(sqlite3_str*, const char *zIn, int N); + void sqlite3_str_appendall(sqlite3_str*, const char *zIn); + void sqlite3_str_appendchar(sqlite3_str*, int N, char C); + void sqlite3_str_reset(sqlite3_str*); + + int sqlite3_str_errcode(sqlite3_str*); + int sqlite3_str_length(sqlite3_str*); + char *sqlite3_str_value(sqlite3_str*); + + int sqlite3_status(int op, int *pCurrent, int *pHighwater, int resetFlag); + int sqlite3_status64(int op, sqlite3_int64 *pCurrent, sqlite3_int64 *pHighwater, int resetFlag); + + int sqlite3_db_status(sqlite3*, int op, int *pCur, int *pHiwtr, int resetFlg); + int sqlite3_stmt_status(sqlite3_stmt*, int op,int resetFlg); + + sqlite3_backup *sqlite3_backup_init(sqlite3 *pDest, const char *zDestName, sqlite3 *pSource, const char *zSourceName); + int sqlite3_backup_step(sqlite3_backup *p, int nPage); + int sqlite3_backup_finish(sqlite3_backup *p); + int sqlite3_backup_remaining(sqlite3_backup *p); + int sqlite3_backup_pagecount(sqlite3_backup *p); + + int sqlite3_unlock_notify(sqlite3 *pBlocked, void (*xNotify)(void **apArg, int nArg), void *pNotifyArg); + + int sqlite3_stricmp(const char *, const char *); + int sqlite3_strnicmp(const char *, const char *, int); + + int sqlite3_strglob(const char *zGlob, const char *zStr); + + int sqlite3_strlike(const char *zGlob, const char *zStr, unsigned int cEsc); + + void sqlite3_log(int iErrCode, const char *zFormat, ...); + + void *sqlite3_wal_hook(sqlite3*, int(*)(void *,sqlite3*,const char*,int), void*); + + int sqlite3_wal_autocheckpoint(sqlite3 *db, int N); + int sqlite3_wal_checkpoint(sqlite3 *db, const char *zDb); + + int sqlite3_wal_checkpoint_v2(sqlite3 *db, const char *zDb, int eMode, int *pnLog, int *pnCkpt); + + int sqlite3_vtab_config(sqlite3*, int op, ...); + + int sqlite3_vtab_on_conflict(sqlite3 *); + int sqlite3_vtab_nochange(sqlite3_context*); + + int sqlite3_stmt_scanstatus(sqlite3_stmt *pStmt, int idx, int iScanStatusOp, void *pOut); + + void sqlite3_stmt_scanstatus_reset(sqlite3_stmt*); + int sqlite3_db_cacheflush(sqlite3*); + int sqlite3_system_errno(sqlite3*); + + unsigned char *sqlite3_serialize(sqlite3 *db, const char *zSchema, sqlite3_int64 *piSize, unsigned int mFlags); + int sqlite3_deserialize(sqlite3 *db, const char *zSchema, unsigned char *pData, sqlite3_int64 szDb, + sqlite3_int64 szBuf, unsigned mFlags); +]] + +---@class sqlite3 @sqlite3 db object +---@class sqlite_blob @sqlite3 blob object + +M.to_str = function(ptr, len) + if ptr == nil then + return + end + return ffi.string(ptr, len) +end + +M.type_of = function(ptr) + if ptr == nil then + return + end + return ffi.typeof(ptr) +end + +M.get_new_db_ptr = function() + return ffi.new "sqlite3*[1]" +end + +M.get_new_stmt_ptr = function() + return ffi.new "sqlite3_stmt*[1]" +end + +M.get_new_blob_ptr = function() + return ffi.new "sqlite3_blob*[1]" +end + +M.type_of_db_ptr = ffi.typeof "sqlite3*" +M.type_of_stmt_ptr = ffi.typeof "sqlite3_stmt*" +M.type_of_exec_ptr = ffi.typeof "int (*)(void*,int,char**,char**)" +M.type_of_blob_ptr = ffi.typeof "sqlite3_blob*" + +--- Wrapper around clib.exec for convenience. +---@param conn_ptr sqlite connction ptr +---@param statement string: statement to be executed. +---@return table: stmt object +M.exec_stmt = function(conn_ptr, statement) + return clib.sqlite3_exec(conn_ptr, statement, nil, nil, nil) +end + +--- Execute a manipulation sql statement within begin and commit block +---@param conn_ptr sqlite connction ptr +---@param fn func() +M.wrap_stmts = function(conn_ptr, fn) + M.exec_stmt(conn_ptr, "BEGIN") + local res = fn() + M.exec_stmt(conn_ptr, "COMMIT") + return res +end + +---Get last error msg +---@param conn_ptr sqlite connction ptr +---@return string: sqlite error msg +M.last_errmsg = function(conn_ptr) + return M.to_str(clib.sqlite3_errmsg(conn_ptr)) +end + +---Get last error code +---@param conn_ptr sqlite connction ptr +---@return number: sqlite error number +M.last_errcode = function(conn_ptr) + return clib.sqlite3_errcode(conn_ptr) +end + +-- Open Modes +M.open_modes = { + ["ro"] = bit.bor(M.flags.open_readonly, M.flags.open_uri), + ["rw"] = bit.bor(M.flags.open_readwrite, M.flags.open_uri), + ["rwc"] = bit.bor(M.flags.open_readwrite, M.flags.open_create, M.flags.open_uri), +} + +---Create new connection and modify `sqlite_db` object +---@param uri string +---@param opts sqlite_db.opts +---@return sqlite_blob* +M.connect = function(uri, opts) + opts = opts or {} + local conn = M.get_new_db_ptr() + local open_mode = opts.open_mode + opts.open_mode = nil + if type(open_mode) == "table" then + open_mode = bit.bor(unpack(open_mode)) + else + open_mode = M.open_modes[open_mode or "rwc"] + end + + local code = clib.sqlite3_open_v2(uri, conn, open_mode, nil) + + if code ~= M.flags.ok then + error(("sqlite.lua: couldn't connect to sql database, ERR: %s"):format(M.last_errmsg(conn[0]))) + end + + for k, v in pairs(opts) do + if not M.valid_pargma[k] then + error("sqlite.lua: " .. k .. " is not a valid pragma") + end + if type(k) == "boolean" then + k = "ON" + end + M.exec_stmt(conn[0], ("pragma %s = %s"):format(k, v)) + end + + return conn[0] +end + +M = setmetatable(M, { + __index = function(_, k) + return clib["sqlite3_" .. k] + end, +}) + +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/examples/bookmarks.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/examples/bookmarks.lua new file mode 100644 index 0000000..4ee54a1 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/examples/bookmarks.lua @@ -0,0 +1,295 @@ +---@brief [[ +--- +--- Web/File Bookmark Database. +--- Inspired by @sunjon implementation of https://entries.wikipedia.org/wiki/Frecency. +--- This file is meant to demonstrate what can be done with `sqlite.lua` with +--- a number of recommendations. +--- +---@brief ]] + +--- Require the module: +------------------------- + +local sqlite = require "sqlite.db" --- for constructing sql databases +local tbl = require "sqlite.tbl" --- for constructing sql tables +local uri = "/tmp/bm_db_v1" -- defined here to be deleted later + +--- sqlite builtin helper functions +local julianday, strftime = sqlite.lib.julianday, sqlite.lib.strftime + +--- Define and iterate over tables rows/schema: +------------------------------------------------- +---This upfront investment would help you have clear idea, help other +---contribute, and provide you awesome auto-completions with the help of +---EmmyLua. + +---@alias BMType '"web"' | '"file"' | '"dir"' + +--[[ Datashapes --------------------------------------------- + +---@class BMCollection +---@field title string: collection title + +---@class BMEntry +---@field id number: unique id +---@field link string: file or web link. +---@field title string: the title of the bookmark. +---@field doc number: date of creation. +---@field type BMType +---@field count number: number of times it was clicked. +---@field collection string: foreign key referencing BMCollection.title. + +---@class BMTimeStamp +---@field id number: unique id +---@field timestamp number +---@field entry number: foreign key referencing BMEntry + +--]] + +--[[ sqlite classes ------------------------------------------ + +---@class BMEntryTable: sqlite_tbl + +---@class BMDatabase: sqlite_db +---@field entries BMEntryTable +---@field collection sqlite_tbl +---@field ts sqlite_tbl + +--]] + +--- 3. Construct +--------------------------- + +---@type BMEntryTable +local entries = tbl("entries", { + id = true, -- same as { type = "integer", required = true, primary = true } + link = { "text", required = true }, + title = "text", + since = { "date", default = strftime("%s", "now") }, + count = { "number", default = 0 }, + type = { "text", required = true }, + collection = { + type = "text", + reference = "collection.title", + on_update = "cascade", -- means when collection get updated update + on_delete = "null", -- means when collection get deleted, set to null + }, +}) + +---@type BMDatabase +-- sqlite.lua db object will be injected to every table at evaluation. +-- Though no connection will be open until the first sqlite operation. +local BM = sqlite { + uri = uri, + entries = entries, + collection = { -- Yes, tables can be inlined without requiring sql.table. + title = { + "text", + required = true, + unique = true, + primary = true, + }, + }, + ts = { + _name = "timetamps", -- use this as table name not the field key. + id = true, + timestamp = { "real", default = julianday "now" }, + entry = { + type = "integer", + reference = "entries.id", + on_delete = "cascade", --- when referenced entry is deleted, delete self + }, + }, + -- custom sqlite3 options, if you really know what you want. + keep_open + lazy + opts = {}, +} + +--- Doesn't need to annotated. +local collection, ts = BM.collection, BM.ts + +--- 4. Start adding custom methods or override defaults to own it :D +-------------------------------------------------------------------- + +---Insert timestamp entry +---@param id number +function ts:insert(id) + ts:__insert { entry = id } +end + +---Get timestamp for entry.id or all +---@param id number|nil: BMEntry.id +---@return BMTimeStamp +function ts:get(id) + return ts:__get { --- use "self.__get" as backup for overriding default methods. + where = id and { + entry = id, + } or nil, + select = { + age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60, + "id", + "timestamp", + "entry", + }, + } +end + +---Trim timestamps entries +---@param id number: BMEntry.id +function ts:trim(id) + local rows = ts:get(id) -- calling t.get defined above + local trim = rows[(#rows - 10) + 1] + if trim then + ts:remove { id = "<" .. trim.id, entry = id } + return true + end + return false +end + +---Update an entry values +---@param row BMEntry +function entries:edit(id, row) + entries:update { + where = { id = id }, + set = row, + } +end + +---Increment row count by id. +function entries:inc(id) + local row = entries:where { id = id } + entries:update { + where = { id = id }, + set = { count = row.count + 1 }, + } + ts:insert(id) + ts:trim(id) +end + +---Add a row +---@param row BMEntry +function entries:add(row) + if row.collection and not collection:where { title = row.collection } then + collection:insert { title = row.collection } + end + + row.type = row.link:match "^%w+://" and "web" or (row.link:match ".-/.+(%..*)$" and "file" or "dir") + + local id = entries:insert(row) + if not row.title and row.type == "web" then + local ok, curl = pcall(require, "plenary.curl") + if ok then + curl.get { -- async function + url = row.link, + callback = function(res) + if res.status ~= 200 then + return + end + entries:update { + where = { id = id }, + set = { title = res.body:match "title>(.-)<" }, + } + end, + } + end + end + + ts:insert(id) + + return id +end + +local ages = { + [1] = { age = 240, value = 100 }, -- past 4 hours + [2] = { age = 1440, value = 80 }, -- past day + [3] = { age = 4320, value = 60 }, -- past 3 days + [4] = { age = 10080, value = 40 }, -- past week + [5] = { age = 43200, value = 20 }, -- past month + [6] = { age = 129600, value = 10 }, -- past 90 days +} + +---Get all entries. +---@param q sqlite_query_select: a query to limit the number entries returned. +---@return BMEntry +function entries:get(q) + local items = entries:map(function(entry) + local recency_score = 0 + if not entry.count or entry.count == 0 then + entry.score = 0 + return entry + end + + for _, _ts in pairs(ts:get(entry.id)) do + for _, rank in ipairs(ages) do + if _ts.age <= rank.age then + recency_score = recency_score + rank.value + goto continue + end + end + ::continue:: + end + + entry.score = entry.count * recency_score / 10 + return entry + end, q) + + table.sort(items, function(a, b) + return a.score > b.score + end) + + return items +end + +entries.demo = { + { title = "My Pull Requests", link = "https://github.com/pulls", collection = "gh" }, + { title = "My Issues", link = "https://github.com/issues", collection = "gh" }, + { title = "System Configuration", link = "~/sys/config", collection = "config" }, + { title = "Neovim Configuration", link = "~/sys/cli/nvim", collection = "config" }, + { title = "Todos Inbox", link = "~/Org/refile.org", collection = "mang" }, + { link = "https://github.com", collection = "quick access" }, +} + +function entries:seed() + ---Seed entries table + for _, row in ipairs(entries.demo) do + entries:add(row) + end + + --- Act as if we've used on those entries + for _ = 1, 5, 1 do + entries:inc(4) + end + for _ = 1, 10, 1 do + entries:inc(1) + end + for _ = 1, 3, 1 do + entries:inc(3) + end +end + +if entries:count() == 0 then + entries:seed() +end + +---Edit an entry --- simple abstraction over entries.update { where = {id = 3}, set = { title = "none" } } +entries:edit(3, { title = "none" }) + +--- Get first match +assert(entries:where({ title = "none" }).id == 3) + +--- Get all entries +vim.inspect(entries:get { where = { type = "web" } }) + +--- Delete all file or dir type bookmarks +assert(entries:remove { type = { "file", "dir" } }) + +--- Should be delete +assert(next(entries:get { where = { type = "file" } }) == nil) + +vim.wait(500) -- BLOCK TO GET WEB TITLE FOR PLENERY.CURL + +-- See all inputs +print(vim.inspect(entries:get())) + +vim.defer_fn(function() + vim.loop.fs_unlink(uri) +end, 40000) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/helpers.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/helpers.lua new file mode 100644 index 0000000..4be35b6 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/helpers.lua @@ -0,0 +1,108 @@ +local M = {} +local luv = require "luv" +local a = require "sqlite.assert" +local u = require "sqlite.utils" +local fmt = string.format +local P = require "sqlite.parser" + +---Get a table schema, or execute a given function to get it +---@param tbl_name string +---@param db sqlite_db +M.get_schema = function(tbl_name, db) + local schema = db.tbl_schemas[tbl_name] + if schema then + return schema + end + db.tbl_schemas[tbl_name] = db:schema(tbl_name) + return db.tbl_schemas[tbl_name] +end + +---Check and alter table schema, limited to simple renames and alteration of key schema +---@param o sqlite_tbl +---@param valid_schema any +M.check_for_auto_alter = function(o, valid_schema) + local with_foregin_key = false + + if not valid_schema then + return + end + + for _, def in pairs(o.tbl_schema) do + if type(def) == "table" and def.reference then + with_foregin_key = true + break + end + end + + local get = fmt("select * from sqlite_master where name = '%s'", o.name) + + local stmt = o.tbl_exists and o.db:eval(get) or nil + if type(stmt) ~= "table" then + return + end + + local origin, parsed = stmt[1].sql, P.create(o.name, o.tbl_schema, true) + if origin == parsed then + return + end + + local ok, cmd = pcall(P.table_alter_key_defs, o.name, o.tbl_schema, o.db:schema(o.name)) + if not ok then + print(cmd) + return + end + + o.db:execute(cmd) + o.db_schema = o.db:schema(o.name) + + if with_foregin_key then + o.db:execute "PRAGMA foreign_keys = ON;" + o.db.opts.foreign_keys = true + end +end + +---Run tbl functions +---@param func function: wrapped function to run +---@param o sqlite_tbl +---@return any +M.run = function(func, o) + a.should_have_db_object(o.db, o.name) + local exec = function() + local valid_schema = o.tbl_schema and next(o.tbl_schema) ~= nil + + --- Run once pre-init + if o.tbl_exists == nil then + o.tbl_exists = o.db:exists(o.name) + o.mtime = o.db.uri and (luv.fs_stat(o.db.uri) or { mtime = {} }).mtime.sec or nil + rawset( + o, + "has_content", + o.tbl_exists and o.db:eval(fmt("select count(*) from %s", o.name))[1]["count(*)"] ~= 0 or 0 + ) + -- o.has_content = + M.check_for_auto_alter(o, valid_schema) + end + + --- Run when tbl doesn't exists anymore + if o.tbl_exists == false and valid_schema then + o.tbl_schema.ensure = u.if_nil(o.tbl_schema.ensure, true) + o.db:create(o.name, o.tbl_schema) + o.db_schema = o.db:schema(o.name) + end + + --- Run once when we don't have schema + if not o.db_schema then + o.db_schema = o.db:schema(o.name) + end + + --- Run wrapped function + return func() + end + + if o.db.closed then + return o.db:with_open(exec) + end + return exec() +end + +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/init.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/init.lua new file mode 100644 index 0000000..45d8e83 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/init.lua @@ -0,0 +1,74 @@ +---@brief [[ +---SQLite/LuaJIT binding and highly opinionated wrapper for storing, +---retrieving, caching, persisting, querying, and connecting to SQLite databases. +--- +--- To find out more +--- visit https://github.com/kkharji/sqlite.lua +---
+---
+--- Help usage in neovim: ignore ||
+---   :h |sqlite.readme|         | open help readme
+---   :h |sqlite_schema_key|     | open a class or type
+---   :h |sqlite.tbl|            | show help for sqlite_tbl.
+---   :h |sqlite.db|             | show help for sqlite_tbl.
+---   :h sqlite.db:...           | show help for a sqlite_db method.
+---   :h sqlite.tbl:...          | show help for a sqlite_tbl method.
+---
+---
+--- sqlite.lua types: +---@brief ]] +---@tag sqlite.readme + +---@class sqlite_schema_key @Sqlite schema key fileds. {name} is the only required field. +---@field cid number: column index. +---@field name string: column key. +---@field type string: column type. +---@field required boolean: whether it's required. +---@field primary boolean: whether it's a primary key. +---@field default string: default value when null. +---@field reference string: "table_name.column_key" +---@field on_delete sqlite_trigger: trigger on row delete. +---@field on_update sqlite_trigger: trigger on row updated. + +---@alias sqlite_schema_dict table +---@alias sqlite_query_delete table + +---@class sqlite_opts @Sqlite3 Options (TODO: add sqlite option fields and description) + +---@class sqlite_query_update @Query fileds used when calling |sqlite:update| or |sqlite_tbl:update| +---@field where table: filter down values using key values. +---@field set table: key and value to updated. + +---@class sqlite_query_select @Query fileds used when calling |sqlite:select| or |sqlite_tbl:get| +---@field where table: filter down values using key values. +---@field keys table: keys to include. (default all) +---@field join table: (TODO: support) +---@field order_by table: { asc = "key", dsc = {"key", "another_key"} } +---@field limit number: the number of result to limit by +---@field contains table: for sqlite glob ex. { title = "fix*" } + +---@class sqlite_flags @Sqlite3 Error Flags (TODO: add sqlite error flags value and description) + +---@class sqlite_db_status @Status returned from |sqlite:status()| +---@field msg string +---@field code sqlite_flags + +---@alias sqlite_trigger +---| '"no action"' : when a parent key is modified or deleted from the database, no special action is taken. +---| '"restrict"' : prohibites from deleting/modifying a parent key when a child key is mapped to it. +---| '"null"' : when a parent key is deleted/modified, the child key that mapped to the parent key gets set to null. +---| '"default"' : similar to "null", except that sets to the column's default value instead of NULL. +---| '"cascade"' : propagates the delete or update operation on the parent key to each dependent child key. + +---@type sqlite_db +return setmetatable({}, { + __index = function(_, key) + return require("sqlite.db")[key] + end, + __newindex = function(_) + error "sqlite.lua: this shouldn't happen. Mutating sqlite base object is prohibited." + end, + __call = function(_, ...) + return require "sqlite.db"(...) + end, +}) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/json.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/json.lua new file mode 100644 index 0000000..5c2fa1f --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/json.lua @@ -0,0 +1,372 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- 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. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + ["\\"] = "\\", + ['"'] = '"', + ["\b"] = "b", + ["\f"] = "f", + ["\n"] = "n", + ["\r"] = "r", + ["\t"] = "t", +} + +local escape_char_map_inv = { ["/"] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + +local function encode_nil(val) + return val == nil and "null" +end + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then + error "circular reference" + end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error "invalid table: mixed or invalid key types" + end + n = n + 1 + end + if n ~= #val then + error "invalid table: sparse array" + end + -- Encode + for _, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error "invalid table: mixed or invalid key types" + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + +local type_func_map = { + ["nil"] = encode_nil, + ["table"] = encode_table, + ["string"] = encode_string, + ["number"] = encode_number, + ["boolean"] = tostring, +} + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + +function json.encode(val) + return (encode(val)) +end + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[select(i, ...)] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + ["true"] = true, + ["false"] = false, + ["null"] = nil, +} + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error(string.format("%s at line %d col %d", msg, line_count, col_count)) +end + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, f(n % 4096 / 64) + 128, n % 64 + 128) + end + error(string.format("invalid unicode codepoint '%x'", n)) +end + +local function parse_unicode_escape(s) + local n1 = tonumber(s:sub(1, 4), 16) + local n2 = tonumber(s:sub(7, 10), 16) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then + break + end + if chr ~= "," then + decode_error(str, i, "expected ']' or ','") + end + end + return res, i +end + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then + break + end + if chr ~= "," then + decode_error(str, i, "expected '}' or ','") + end + end + return res, i +end + +local char_func_map = { + ['"'] = parse_string, + ["0"] = parse_number, + ["1"] = parse_number, + ["2"] = parse_number, + ["3"] = parse_number, + ["4"] = parse_number, + ["5"] = parse_number, + ["6"] = parse_number, + ["7"] = parse_number, + ["8"] = parse_number, + ["9"] = parse_number, + ["-"] = parse_number, + ["t"] = parse_literal, + ["f"] = parse_literal, + ["n"] = parse_literal, + ["["] = parse_array, + ["{"] = parse_object, +} + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + +return json diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/parser.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/parser.lua new file mode 100644 index 0000000..1c677e1 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/parser.lua @@ -0,0 +1,573 @@ +local u = require "sqlite.utils" +local json = require "sqlite.json" +local tinsert = table.insert +local tconcat = table.concat +local a = require "sqlite.assert" +local M = {} + +---@brief [[ +---Internal functions for parsing sql statement from lua table. +---methods = { select, update, delete, insert, create, alter, drop } +---accepts tbl name and options. +---options = {join, keys, values, set, where, select} +---hopfully returning valid sql statement :D +---@brief ]] +---@tag parser.lua + +---handle sqlite datatype interop +M.sqlvalue = function(v) + return type(v) == "boolean" and (v == true and 1 or 0) or (v == nil and "null" or v) +end + +M.luavalue = function(v, key_type) + if key_type == "luatable" or key_type == "json" then + return json.decode(v) + elseif key_type == "boolean" then + if v == 0 then + return false + else + return true + end + -- return v == 0 and false or true + end + + return v +end + +---string.format specifier based on value type +---@param v any: the value +---@param nonbind boolean?: whether to return the specifier or just return the value. +---@return string +local specifier = function(v, nonbind) + local type = type(v) + if type == "number" then + local _, b = math.modf(v) + return b == 0 and "%d" or "%f" + elseif type == "string" and not nonbind then + return v:find "'" and [["%s"]] or "'%s'" + elseif nonbind then + return v + else + return "" + end +end + +local bind = function(o) + o = o or {} + o.s = o.s or ", " + if not o.kv then + o.v = o.v ~= nil and M.sqlvalue(o.v) or "?" + return ("%s = " .. specifier(o.v)):format(o.k, o.v) + else + local res = {} + for k, v in u.opairs(o.kv) do + k = o.k ~= nil and o.k or k + v = M.sqlvalue(v) + v = o.nonbind and ":" .. k or v + tinsert(res, ("%s" .. (o.nonbind and nil or " = ") .. specifier(v, o.nonbind)):format(k, v)) + end + return tconcat(res, o.s) + end +end + +---format glob pattern as part of where clause +local pcontains = function(defs) + if not defs then + return {} + end + local items = {} + for k, v in u.opairs(defs) do + local head = "%s glob " .. specifier(k) + + if type(v) == "table" then + local val = u.map(v, function(value) + return head:format(k, M.sqlvalue(value)) + end) + tinsert(items, tconcat(val, " or ")) + else + tinsert(items, head:format(k, v)) + end + end + + return tconcat(items, " ") +end + +---Format values part of sql statement +---@params defs table: key/value pairs defining sqlite table keys. +---@params defs kv: whether to bind by named keys. +local pkeys = function(defs, kv) + kv = kv == nil and true or kv + + if not defs or not kv then + return {} + end + + defs = u.is_nested(defs) and defs[1] or defs + + local keys = {} + for k, _ in u.opairs(defs) do + tinsert(keys, k) + end + + return ("(%s)"):format(tconcat(keys, ", ")) +end + +---Format values part of sql statement, usually used with select method. +---@params defs table: key/value pairs defining sqlite table keys. +---@params defs kv: whether to bind by named keys. +local pvalues = function(defs, kv) + kv = kv == nil and true or kv -- TODO: check if defs is key value pairs instead + if not defs or not kv then + return {} + end + + defs = u.is_nested(defs) and defs[1] or defs + + local keys = {} + for k, v in u.opairs(defs) do + if type(v) == "string" and v:match "^[%S]+%(.*%)$" then + tinsert(keys, v) + else + tinsert(keys, ":" .. k) + end + end + + return ("values(%s)"):format(tconcat(keys, ", ")) +end + +---Format where part of a sql statement. +---@params defs table: key/value pairs defining sqlite table keys. +---@params name string: the name of the sqlite table +---@params join table: used as boolean, controling whether to use name.key or just key. +local pwhere = function(defs, name, join, contains) + if not defs and not contains then + return {} + end + + local where = {} + if defs then + for k, v in u.opairs(defs) do + k = join and name .. "." .. k or k + + if type(v) ~= "table" then + if type(v) == "string" and (v:sub(1, 1) == "<" or v:sub(1, 1) == ">") then + tinsert(where, k .. " " .. v) + else + tinsert(where, bind { v = v, k = k, s = " and " }) + end + else + if type(k) == "number" then + tinsert(where, table.concat(v, " ")) + else + tinsert(where, "(" .. bind { kv = v, k = k, s = " or " } .. ")") + end + end + end + end + + if contains then + tinsert(where, pcontains(contains)) + end + return ("where %s"):format(tconcat(where, " and ")) +end + +local plimit = function(defs) + if not defs then + return {} + end + + local type = type(defs) + local istbl = (type == "table" and defs[2]) + local offset = "limit %s offset %s" + local limit = "limit %s" + + return istbl and offset:format(defs[1], defs[2]) or limit:format(type == "number" and defs or defs[1]) +end + +---Format set part of sql statement, usually used with update method. +---@params defs table: key/value pairs defining sqlite table keys. +local pset = function(defs) + if not defs then + return {} + end + + return "set " .. bind { kv = defs, nonbind = true } +end + +---Format join part of a sql statement. +---@params defs table: key/value pairs defining sqlite table keys. +---@params name string: the name of the sqlite table +local pjoin = function(defs, name) + if not defs or not name then + return {} + end + local target + + local on = (function() + for k, v in pairs(defs) do + if k ~= name then + target = k + return ("%s.%s ="):format(k, v) + end + end + end)() + + local select = (function() + for k, v in pairs(defs) do + if k == name then + return ("%s.%s"):format(k, v) + end + end + end)() + + return ("inner join %s on %s %s"):format(target, on, select) +end + +local porder_by = function(defs) + -- TODO: what if nulls? should append "nulls last" + if not defs then + return {} + end + + local fmt = "%s %s" + local items = {} + + for v, k in u.opairs(defs) do + if type(k) == "table" then + for _, _k in u.opairs(k) do + tinsert(items, fmt:format(_k, v)) + end + else + tinsert(items, fmt:format(k, v)) + end + end + + return ("order by %s"):format(tconcat(items, ", ")) +end + +local partial = function(method, tbl, opts) + opts = opts or {} + return tconcat( + u.flatten { + method, + pkeys(opts.values), + pvalues(opts.values, opts.named), + pset(opts.set), + pwhere(opts.where, tbl, opts.join, opts.contains), + porder_by(opts.order_by), + plimit(opts.limit), + }, + " " + ) +end + +local pselect = function(select) + local t = type(select) + + if t == "table" and next(select) ~= nil then + local items = {} + for k, v in pairs(select) do + if type(k) == "number" then + tinsert(items, v) + else + tinsert(items, ("%s as %s"):format(v, k)) + end + end + + return tconcat(items, ", ") + end + + return t == "string" and select or "*" +end + +---Parse select statement to extracts data from a database +---@param tbl string: table name +---@param opts table: lists of options: valid{ select, join, order_by, limit, where } +---@return string: the select sql statement. +M.select = function(tbl, opts) + opts = opts or {} + local cmd = opts.unique and "select distinct %s" or "select %s" + local select = pselect(opts.select) + local stmt = (cmd .. " from %s"):format(select, tbl) + local method = opts.join and stmt .. " " .. pjoin(opts.join, tbl) or stmt + return partial(method, tbl, opts) +end + +---Parse select statement to update data in the database +---@param tbl string: table name +---@param opts table: lists of options: valid{ set, where } +---@return string: the update sql statement. +M.update = function(tbl, opts) + local method = ("update %s"):format(tbl) + return partial(method, tbl, opts) +end + +---Parse insert statement to insert data into a database +---@param tbl string: table name +---@param opts table: lists of options: valid{ where } +---@return string: the insert sql statement. +M.insert = function(tbl, opts) + local method = ("insert into %s"):format(tbl) + return partial(method, tbl, opts) +end + +---Parse delete statement to deletes data from a database +---@param tbl string: table name +---@param opts table: lists of options: valid{ where } +---@return string: the delete sql statement. +M.delete = function(tbl, opts) + opts = opts or {} + local method = ("delete from %s"):format(tbl) + local where = pwhere(opts.where) + return type(where) == "string" and method .. " " .. where or method +end + +local format_action = function(value, update) + local stmt = update and "on update" or "on delete" + local preappend = (value:match "default" or value:match "null") and " set " or " " + + return stmt .. preappend .. value +end + +local opts_to_str = function(tbl) + local f = { + pk = function() + return "primary key" + end, + type = function(v) + return v + end, + unique = function() + return "unique" + end, + required = function(v) + if v then + return "not null" + end + end, + default = function(v) + v = (type(v) == "string" and v:match "^[%S]+%(.*%)$") and "(" .. tostring(v) .. ")" or v + local str = "default " + if tbl["required"] then + return "on conflict replace " .. str .. v + else + return str .. v + end + end, + reference = function(v) + return ("references %s"):format(v:gsub("%.", "(") .. ")") + end, + on_update = function(v) + return format_action(v, true) + end, + on_delete = function(v) + return format_action(v) + end, + } + + f.primary = f.pk + + local res = {} + + if type(tbl[1]) == "string" then + res[1] = tbl[1] + end + + local check = function(type) + local v = tbl[type] + if v then + res[#res + 1] = f[type](v) + end + end + + check "type" + check "unique" + check "required" + check "pk" + check "primary" + check "default" + check "reference" + check "on_update" + check "on_delete" + + return tconcat(res, " ") +end + +---Parse table create statement +---@param tbl string: table name +---@param defs table: keys and type pairs +---@return string: the create sql statement. +M.create = function(tbl, defs, ignore_ensure) + if not defs then + return + end + local items = {} + + tbl = (defs.ensure and not ignore_ensure) and "if not exists " .. tbl or tbl + + for k, v in u.opairs(defs) do + if k ~= "ensure" then + local t = type(v) + if t == "boolean" then + tinsert(items, k .. " integer not null primary key") + elseif t ~= "table" then + tinsert(items, string.format("%s %s", k, v)) + else + if u.is_list(v) then + tinsert(items, ("%s %s"):format(k, tconcat(v, " "))) + else + tinsert(items, ("%s %s"):format(k, opts_to_str(v))) + end + end + end + end + return ("CREATE TABLE %s(%s)"):format(tbl, tconcat(items, ", ")) +end + +---Parse table drop statement +---@param tbl string: table name +---@return string: the drop sql statement. +M.drop = function(tbl) + return "drop table " .. tbl +end + +-- local same_type = function(new, old) +-- if not new or not old then +-- return false +-- end + +-- local tnew, told = type(new), type(old) + +-- if tnew == told then +-- if tnew == "string" then +-- return new == old +-- elseif tnew == "table" then +-- if new[1] and old[1] then +-- return (new[1] == old[1]) +-- elseif new.type and old.type then +-- return (new.type == old.type) +-- elseif new.type and old[1] then +-- return (new.type == old[1]) +-- elseif new[1] and old.type then +-- return (new[1] == old.type) +-- end +-- end +-- else +-- if tnew == "table" and told == "string" then +-- if new.type == old then +-- return true +-- elseif new[1] == old then +-- return true +-- end +-- elseif tnew == "string" and told == "table" then +-- return old.type == new or old[1] == new +-- end +-- end +-- -- return false +-- end + +---Alter a given table, only support changing key definition +---@param tname string +---@param new sqlite_schema_dict +---@param old sqlite_schema_dict +M.table_alter_key_defs = function(tname, new, old, dry) + local tmpname = tname .. "_new" + local create = M.create(tmpname, new, true) + local drop = M.drop(tname) + local move = "INSERT INTO %s(%s) SELECT %s FROM %s" + local rename = ("ALTER TABLE %s RENAME TO %s"):format(tmpname, tname) + local with_foregin_key = false + + for _, def in pairs(new) do + if type(def) == "table" and def.reference then + with_foregin_key = true + end + end + + local stmt = "PRAGMA foreign_keys=off; BEGIN TRANSACTION; %s; COMMIT;" + if not with_foregin_key then + stmt = stmt .. " PRAGMA foreign_keys=on" + end + + local keys = { new = u.okeys(new), old = u.okeys(old) } + local idx = { new = {}, old = {} } + local len = { new = #keys.new, old = #keys.old } + -- local facts = { extra_key = len.new > len.old, drop_key = len.old > len.new } + + a.auto_alter_should_have_equal_len(len.new, len.old, tname) + + for _, varient in ipairs { "new", "old" } do + for k, v in pairs(keys[varient]) do + idx[varient][v] = k + end + end + + for i, v in ipairs(keys.new) do + if idx.old[v] and idx.old[v] ~= i then + local tmp = keys.old[i] + keys.old[i] = v + keys.old[idx.old[v]] = tmp + end + end + + local update_null_vals = {} + local update_null_stmt = "UPDATE %s SET %s=%s where %s IS NULL" + for key, def in pairs(new) do + if type(def) == "table" and def.default and not def.required then + tinsert(update_null_vals, update_null_stmt:format(tmpname, key, def.default, key)) + end + end + update_null_vals = #update_null_vals == 0 and "" or tconcat(update_null_vals, "; ") + + local new_keys, old_keys = tconcat(keys.new, ", "), tconcat(keys.old, ", ") + local insert = move:format(tmpname, new_keys, old_keys, tname) + stmt = stmt:format(tconcat({ create, insert, update_null_vals, drop, rename }, "; ")) + + return not dry and stmt or insert +end + +---Pre-process data insert to sql db. +---for now it's mainly used to for parsing lua tables and boolean values. +---It throws when a schema key is required and doesn't exists. +---@param rows tinserted row. +---@param schema table tbl schema with extra info +---@return table pre processed rows +M.pre_insert = function(rows, schema) + local res = {} + rows = u.is_nested(rows) and rows or { rows } + for i, row in ipairs(rows) do + res[i] = u.map(row, function(v, k) + local column_def = schema[k] + a.should_have_column_def(column_def, k, schema) + a.missing_req_key(v, column_def) + local is_json = column_def.type == "luatable" or column_def.type == "json" + return is_json and json.encode(v) or M.sqlvalue(v) + end) + end + return res +end + +---Postprocess data queried from a sql db. for now it is mainly used +---to for parsing json values to lua table. +---@param rows tinserted row. +---@param schema table tbl schema +---@return table pre processed rows +---@TODO support boolean values. +M.post_select = function(rows, schema) + local is_nested = u.is_nested(rows) + rows = is_nested and rows or { rows } + for _, row in ipairs(rows) do + for k, v in pairs(row) do + local info = schema[k] + if info then + row[k] = M.luavalue(v, info.type) + else + row[k] = v + end + end + end + + return is_nested and rows or rows[1] +end + +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/stmt.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/stmt.lua new file mode 100644 index 0000000..a2d3d3d --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/stmt.lua @@ -0,0 +1,413 @@ +---@brief [[ +--- sqlstmt is a collection of methods to deal with sqlite statements. +---@brief ]] +---@tag sqlstmt +local clib = require "sqlite.defs" +local flags = clib.flags + +---@class sqlstmt @Object to deal with sqlite statements +---@field pstmt sqlite_blob sqlite_pstmt +---@field conn sqlite_blob sqlite3 object +---@field str string original statement +local sqlstmt = {} +sqlstmt.__index = sqlstmt + +--- TODO: refactor and make parser.lua required here only. + +---Parse a statement +---@param conn sqlite3: the database connection. +---@param str string: the sqlite statement to be parsed. +---@return sqlstmt: collection of methods, applicable to the parsed statement. +---@see sqlstmt:__parse +---@usage local sqlstmt = sqlstmt:parse(db, "insert into todos (title,desc) values(:title, :desc)") +function sqlstmt:parse(conn, str) + assert(clib.type_of(conn) == clib.type_of_db_ptr, "Invalid connection passed to sqlstmt:parse") + assert(type(str) == "string", "Invalid second argument passed to sqlstmt:parse") + local o = setmetatable({ + str = str, + conn = conn, + finalized = false, + }, sqlstmt) + + local pstmt = clib.get_new_stmt_ptr() + local code = clib.prepare_v2(o.conn, o.str, #o.str, pstmt, nil) + + assert( + code == flags.ok, + ("sqlite.lua: sql statement parse, , stmt: `%s`, err: `(`%s`)`"):format(o.str, clib.last_errmsg(o.conn)) + ) + o.pstmt = pstmt[0] + return o +end + +---Resets the parsed statement. required for parsed statements to be re-executed. +---NOTE: Any statement variables that had values bound to them using the +---sqlstmt:bind functions retain their values. +---@return number: falgs.ok or errcode +---@TODO should we error out when errcode? +function sqlstmt:reset() + return clib.reset(self.pstmt) +end + +---Frees the prepared statement +---@return boolean: if no error true. +function sqlstmt:finalize() + self.errcode = clib.finalize(self.pstmt) + self.finalized = self.errcode == flags.ok + assert( + self.finalized, + string.format( + "sqlite.lua: couldn't finalize statement, ERRMSG: %s stmt = (%s)", + clib.to_str(clib.errmsg(self.conn)), + self.str + ) + ) + return self.finalized +end + +---Called before evaluating the (next iteration) of the prepared statement. +---@return sqlite_flags: Possible Flags: { flags.busy, flags.done, flags.row, flags.error, flags.misuse } +function sqlstmt:step() + local step_code = clib.step(self.pstmt) + assert( + step_code ~= flags.error or step_code ~= flags.misuse, + string.format("sqlite.lua: error in step(), ERRMSG: %s. Please report issue.", clib.to_str(clib.errmsg(self.conn))) + ) + return step_code +end + +---Number of keys/columns in results +---@return number: column count in the results. +function sqlstmt:nkeys() + return clib.column_count(self.pstmt) +end + +---Number of rows/items in results. +---@return number: rows count in the results. +function sqlstmt:nrows() + local count = 0 + self:each(function() + count = count + 1 + end) + return count +end + +---key-name/column-name at {idx} in results. +---@param idx number: (0-index) +---@return string: keyname/column name at {idx} +---@TODO should accept 1-index +function sqlstmt:key(idx) + return clib.to_str(clib.column_name(self.pstmt, idx)) +end + +---key-names/column-names in results. +---@return table: key-names/column-names. +---@see sqlstmt:nkeys +---@see sqlstmt:key +function sqlstmt:keys() + local keys = {} + for i = 0, self:nkeys() - 1 do + table.insert(keys, i + 1, self:key(i)) + end + return keys +end + +local sqlite_datatypes = { + [1] = "int", -- bind_double + [2] = "double", -- bind_text + [3] = "text", -- bind_null + [4] = "blob", -- bind_null + [5] = "null", -- bind_null +} + +---Key/Column lua datatype at {idx} +---@param idx number: (0-index) +---@return string: key/column type at {idx} +---@TODO should accept 1-index +function sqlstmt:convert_type(idx) + local convert_dt = { + ["INTEGER"] = "number", + ["FLOAT"] = "number", + ["DOUBLE"] = "number", + ["TEXT"] = "string", + ["BLOB"] = "binary", + ["NULL"] = nil, + } + return convert_dt[clib.to_str(clib.column_decltype(self.pstmt, idx))] +end + +---Keys/Columns types visible in current result. +---@return table: list of types, ordered by key location. +---@see sqlstmt:type +---@see sqlstmt:nkeys +function sqlstmt:types() + local types = {} + for i = 0, self:nkeys() - 1 do + table.insert(types, i + 1, self:convert_type(i)) + end + return types +end + +---Value at {idx} +---@param idx number: (0-index) +---@return string: value at {idx} +---@TODO should accept 1-index +function sqlstmt:val(idx) + local ktype = clib.column_type(self.pstmt, idx) + if ktype == 5 then + return + end + local val = clib["column_" .. sqlite_datatypes[ktype]](self.pstmt, idx) + return ktype == 3 and clib.to_str(val) or val +end + +---Ordered list of current result values. +---@return table: list of values, ordered by key location. +---@see sqlstmt:val +---@see sqlstmt:nkeys +function sqlstmt:vals() + local vals = {} + for i = 0, self:nkeys() - 1 do + table.insert(vals, i + 1, self:val(i)) + end + return vals +end + +---Key/value pair in current result. +---@return table: key/value pair of a row. +---@see sqlstmt:key +---@see sqlstmt:val +---@see sqlstmt:nkeys +function sqlstmt:kv() + local ret = {} + for i = 0, self:nkeys() - 1 do + ret[self:key(i)] = self:val(i) + end + return ret +end + +---Key/type pair in current result. +---@return table: key/type pair of a row. +---@see sqlstmt:key +---@see sqlstmt:val +---@see sqlstmt:nkeys +function sqlstmt:kt() + local ret = {} + for i = 0, self:nkeys() - 1 do + ret[self:key(i)] = self:convert_type(i) + end + return ret +end + +---sqlstmt:next: +---If code == flags.row it returns +---If code == flags.done it reset the parsed statement +function sqlstmt:next() + local code = self:step() + if code == flags.row then + return self + elseif code == flags.done then + self:reset() + else + return nil, code + end +end + +---sqlstmt:iter +---@see sqlstmt:next +function sqlstmt:iter() + return self:next(), self.pstmt +end + +---Loops through results with {callback} until there is no row left. +---@param callback function: a function to be called on each number of row. +---@usage sqlstmt:each(function(s) print(s:val(1)) end) +---@see sqlstmt:step +function sqlstmt:each(callback) + assert(type(callback) == "function", "sqlstmt:each expected a function, got something else.") + while self:step() == flags.row do + callback(self) + end +end + +---Loops through the results and if {callback} pass to it row, else return nested kv pairs. +---@param callback function: a function to be called with each row. +---@return table: if no callback then nested key-value pairs +---@see sqlstmt:kv +---@see sqlstmt:each +function sqlstmt:kvrows(callback) + local kv = {} + self:each(function() + local row = self:kv() + if callback then + return callback(row) + else + table.insert(kv, row) + end + end) + if not callback then + return kv + end +end + +---Like sqlstmt:kvrows but passed list of values instead of kv pairs. +---@param callback function: a function to be called with each row. +---@return table: if no callback then nested lists of values in each row. +---@see sqlstmt:vals +---@see sqlstmt:each +function sqlstmt:vrows(callback) + local vals = {} + self:each(function(s) + local row = s:vals() + if callback then + return callback(row) + else + table.insert(vals, row) + end + end) + if not callback then + return vals + end +end + +local bind_type_to_func = { + ["number"] = "double", -- bind_double + ["string"] = "text", -- bind_text + ["nil"] = "null", -- bind_null +} + +---Bind {args[2]} at {args[1]} or kv pairs {args[1]}. +---If {args[1]} is a number and {args[2]} is a value then it binds by index. +---Else first argument is a table, then it binds the table to indicies, and it +---works with named and unnamed. +---@varargs if {args[1]} number and {args[2]} or {args[1]} table +---@see sqlstmt:nparam +---@see sqlstmt:param +---@see sqlstmt:bind +function sqlstmt:bind(...) + local args = { ... } + -- bind by table + if type(args[1]) == "table" then + local names = args[1] + local parameter_index_cache = {} + local anon_indices = {} + for i = 1, self:nparam() do + local name = self:param(i) + if name == "?" then + table.insert(anon_indices, i) + else + parameter_index_cache[name:sub(2, -1)] = i + end + end + + for k, v in pairs(names) do + local index = parameter_index_cache[k] or table.remove(anon_indices, 1) + if ((type(v) == "string" and v:match "^[%S]+%(.*%)$") and flags.ok or self:bind(index, v)) ~= flags.ok then + error("sqlite.lua error at stmt:bind(), failed to bind a given value '%s'. Please report issue."):format(v) + end + end + return flags.ok + end + + -- bind by index + if type(args[1]) == "number" and args[2] then + local idx, value = args[1], args[2] + local func = bind_type_to_func[type(value)] + local len = func == "text" and #value or nil + + if not func then + return error [[ + sqlite.lua error at stmt:bind(): Unrecognized or unsupported type. + Please report issue. + ]] + end + + if len then + return clib["bind_" .. func](self.pstmt, idx, value, len, nil) + else + if value then + return clib["bind_" .. func](self.pstmt, idx, value) + else + return clib["bind_" .. func](self.pstmt, idx) + end + end + end +end + +---Binds a blob at {idx} with {size} +---@param idx number: index starting at 1 +---@param pointer sqlite_blob: blob to bind +---@param size number: pointer size +---@return sqlite_flags +function sqlstmt:bind_blob(idx, pointer, size) + return clib.bind_blob64(self.pstmt, idx, pointer, size, nil) -- Always 64? or two functions +end + +---Binds zeroblob at {idx} with {size} +---@param idx number: index starting at 1 +---@param size number: zeroblob size +---@return sqlite_flags +function sqlstmt:bind_zeroblob(idx, size) + return clib.bind_zeroblob64(self.pstmt, idx, size) +end + +---The number of parameter to bind. +---@return number: number of params in {sqlstmt.pstmt} +function sqlstmt:nparam() + if not self.parm_count then + self.parm_count = clib.bind_parameter_count(self.pstmt) + end + + return self.parm_count +end + +---The parameter key/name at {idx} +---@param idx number: index starting at 1 +---@return string: param key ":key" at {idx} +function sqlstmt:param(idx) + return clib.to_str(clib.bind_parameter_name(self.pstmt, idx)) or "?" +end + +---Parameters keys/names +---@return table: paramters key/names in {sqlstmt.pstmt} +---@see sqlstmt:nparam +---@see sqlstmt:param +function sqlstmt:params() + local res = {} + for i = 1, self:nparam() do + table.insert(res, self:param(i)) + end + return res +end + +---Clear the current bindings. +---@return sqlite_flags +function sqlstmt:bind_clear() + self.current_bind_index = nil + return clib.clear_bindings(self.pstmt) +end + +---Bind the value at the next index until all values are bound +---@param value any: value to bind +---@return sqlite_flags +---@TODO does it return sqlite_flag in all cases? @conni +function sqlstmt:bind_next(value) + if not self.current_bind_index then + self.current_bind_index = 1 + end + + if self.current_bind_index <= self:nparam() then + local ret = self:bind(self.current_bind_index, value) + self.current_bind_index = self.current_bind_index + 1 + return ret + end + return flags.error -- TODO(conni): should error out? +end + +---Expand the resulting statement after binding, used for debugging purpose. +---@return string: the resulting statement that can be finalized. +function sqlstmt:expand() + return clib.to_str(clib.expanded_sql(self.pstmt)) +end + +return sqlstmt diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/strfun.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/strfun.lua new file mode 100644 index 0000000..420c9e1 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/strfun.lua @@ -0,0 +1,135 @@ +local M = {} +local u = require "sqlite.utils" + +local customstr +customstr = function(str) + local mt = getmetatable(str) + + local wrap = function(a_, b_, sign) + local _str = ("%s %s %s"):format(a_, sign, b_) + if u.is_str(b_) and b_:match "^%a+%(.+%)$" then + _str = "(" .. _str .. ")" + end + return _str + end + + mt._a_dd = function(a_, b_) + return wrap(a_, b_, "+") + end + mt.__sub = function(a_, b_) + return wrap(a_, b_, "-") + end + mt.__mul = function(a_, b_) + return wrap(a_, b_, "*") + end + mt.__div = function(a_, b_) + return wrap(a_, b_, "/") + end + mt.__pow = function(a_, b_) + return wrap(a_, b_, "^") + end + mt.__mod = function(a_, b_) + return wrap(a_, b_, "%") + end + + return str +end + +local ts = function(ts) + local str + if ts ~= "now" and (type(ts) == "string" and not ts:match "%d") then + str = "%s" + else + str = "'%s'" + end + return str:format(ts or "now") +end + +---Format date according {format} +---@param format string the format +--- %d day of month: 00 +--- %f fractional seconds: SS.SSS +--- %H hour: 00-24 +--- %j day of year: 001-366 +--- %J Julian day number +--- %m month: 01-12 +--- %M minute: 00-59 +--- %s seconds since 1970-01-01 +--- %S seconds: 00-59 +--- %w day of week 0-6 with Sunday==0 +--- %W week of year: 00-53 +--- %Y year: 0000-9999 +--- %% % +---@param timestring string timestamp to format 'deafult now' +---@return string: string representation or TEXT when evaluated. +---@usage `sqlstrftime('%Y %m %d','now')` -> 2021 8 11 +---@usage `sqlstrftime('%H %M %S %s','now')` -> 12 40 18 1414759218 +---@usage `sqlstrftime('%s','now') - strftime('%s','2014-10-07 02:34:56')` -> 2110042 +M.strftime = function(format, timestring) + local str = [[strftime('%s', %s)]] + return customstr(str:format(format, ts(timestring))) +end + +---Return the number of days since noon in Greenwich on November 24, 4714 B.C. +---@param timestring string timestamp to format 'deafult now' +---@return string: string representation or REAL when evaluated. +---@usage `sqljulianday('now')` -> ... +---@usage `sqljulianday('now') - julianday('1947-08-15')` -> 24549.5019360879 +M.julianday = function(timestring) + local str = "julianday(%s)" + return customstr(str:format(ts(timestring))) +end + +---Returns date as "YYYY-MM-DD HH:MM:SS" +---@param timestring string timestamp to format 'deafult now' +---@return string: string representation or "YYYY-MM-DD HH:MM:SS" +---@usage `sqldatetime('now')` -> 2021-8-11 11:31:52 +M.datetime = function(timestring) + local str = [[datetime('%s')]] + return customstr(str:format(timestring or "now")) +end + +---Returns time as HH:MM:SS. +---@param timestring string timestamp to format 'deafult now' +---@param modifier string: e.g. +60 seconds, +15 minutes +---@return string: string representation or "HH:MM:SS" +---@usage `sqltime()` -> "12:50:01" +---@usage `sqltime('2014-10-07 15:45:57.005678')` -> 15:45:57 +---@usage `sqltime('now','+60 seconds')` 15:45:57 + 60 seconds +M.time = function(timestring, modifier) + local str = [[time('%s')]] + if modifier then + str = [[time('%s', '%s')]] + return customstr(str:format(timestring or "now", modifier)) + end + + return customstr(str:format(timestring or "now")) +end + +---Returns date as YYYY-MM-DD +---@param timestring string timestamp to format 'deafult now' +---@param modifier string: e.g. +2 month, +1 year +---@return string: string representation or "HH:MM:SS" +---@usage `sqldate()` -> 2021-08-31 +---@usage `sqldate('2021-08-07')` -> 2021-08-07 +---@usage `sqldate('now','+2 month')` -> 2021-10-07 +M.date = function(timestring, modifier) + local str = [[date('%s')]] + if modifier then + str = [[date('%s', '%s')]] + return customstr(str:format(timestring or "now", modifier)) + end + + return customstr(str:format(timestring or "now")) +end + +---Cast a value as something +---@param source string: the value. +---@param as string: the type. +---@usage `cast((julianday() - julianday "timestamp") * 24 * 60, "integer")` +---@return string +M.cast = function(source, as) + return string.format("cast(%s as %s)", source, as) +end + +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl.lua new file mode 100644 index 0000000..7775c84 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl.lua @@ -0,0 +1,477 @@ +---@brief [[ +---Abstraction to produce more readable code. +---
+--- ```lua
+--- local tbl = require'sqlite.tbl'
+--- ```
+---
+---@brief ]] +---@tag sqlite.tbl.lua + +local u = require "sqlite.utils" +local h = require "sqlite.helpers" + +local sqlite = {} + +---@class sqlite_tbl @Main sql table class +---@field db sqlite_db: sqlite.lua database object. +---@field name string: table name. +---@field mtime number: db last modified time. +sqlite.tbl = {} +sqlite.tbl.__index = sqlite.tbl + +---Create new |sqlite_tbl| object. This object encouraged to be extend and +---modified by the user. overwritten method can be still accessed via +---pre-appending `__` e.g. redefining |sqlite_tbl:get|, result in +---`sqlite_tbl:__get` available as a backup. This object can be instantiated +---without a {db}, in which case, it requires 'sqlite.tbl:set_db' is called. +--- +---Common use case might be to define tables in separate files and then require them in +---file that export db object (TODO: support tbl reuse in different dbs). +--- +---
+---```lua
+--- local t = tbl("todos", { --- or tbl.new
+---   id = true, -- same as { "integer", required = true, primary = true }
+---   title = "text",
+---   since = { "date", default = sqlite.lib.strftime("%s", "now") },
+---   category = {
+---     type = "text",
+---     reference = "category.id",
+---     on_update = "cascade", -- means when category get updated update
+---     on_delete = "null", -- means when category get deleted, set to null
+---   },
+--- }, db)
+--- --- overwrite
+--- t.get = function() return t:__get({ where = {...}, select = {...} })[1] end
+---```
+---
+---@param name string: table name +---@param schema sqlite_schema_dict +---@param db sqlite_db|nil : if nil, then for it to work, it needs setting with sqlite.tbl:set_db(). +---@return sqlite_tbl +function sqlite.tbl.new(name, schema, db) + schema = schema or {} + + local t = setmetatable({ + db = db, + name = name, + tbl_schema = u.if_nil(schema.schema, schema), + }, sqlite.tbl) + + if db then + h.run(function() end, t) + end + + return setmetatable({}, { + __index = function(_, key, ...) + if type(key) == "string" then + key = key:sub(1, 2) == "__" and key:sub(3, -1) or key + if t[key] then + return t[key] + end + end + end, + }) +end + +---Create or change table schema. If no {schema} is given, +---then it return current the used schema if it exists or empty table otherwise. +---On change schema it returns boolean indecting success. +--- +---
+---```lua
+--- local projects = sqlite.tbl:new("", {...})
+--- --- get project table schema.
+--- projects:schema()
+--- --- mutate project table schema with droping content if not schema.ensure
+--- projects:schema {...}
+---```
+---
+---@param schema sqlite_schema_dict +---@return sqlite_schema_dict | boolean +function sqlite.tbl:schema(schema) + return h.run(function() + local exists = self.db:exists(self.name) + if not schema then -- TODO: or table is empty + return exists and self.db:schema(self.name) or {} + end + if not exists or schema.ensure then + self.tbl_exists = self.db:create(self.name, schema) + return self.tbl_exists + end + if not schema.ensure then -- TODO: use alter + local res = exists and self.db:drop(self.name) or true + res = res and self.db:create(self.name, schema) or false + self.tbl_schema = schema + return res + end + end, self) +end + +---Remove table from database, if the table is already drooped then it returns false. +--- +---
+---```lua
+--- --- drop todos table content.
+--- todos:drop()
+---```
+---
+---@see sqlite.db:drop +---@return boolean +function sqlite.tbl:drop() + return h.run(function() + if not self.db:exists(self.name) then + return false + end + + local res = self.db:drop(self.name) + if res then + self.tbl_exists = false + self.tbl_schema = nil + end + return res + end, self) +end + +---Predicate that returns true if the table is empty. +--- +---
+---```lua
+--- if todos:empty() then
+---   print "no more todos, we are free :D"
+--- end
+---```
+---
+---@return boolean +function sqlite.tbl:empty() + return h.run(function() + if self.db:exists(self.name) then + return self.db:eval("select count(*) from " .. self.name)[1]["count(*)"] == 0 + end + end, self) +end + +---Predicate that returns true if the table exists. +--- +---
+---```lua
+--- if goals:exists() then
+---   error("I'm disappointed in you :D")
+--- end
+---```
+---
+---@return boolean +function sqlite.tbl:exists() + return h.run(function() + return self.db:exists(self.name) + end, self) +end + +---Get the current number of rows in the table +--- +---
+---```lua
+--- if notes:count() == 0 then
+---   print("no more notes")
+--- end
+---```
+---@return number
+function sqlite.tbl:count()
+  return h.run(function()
+    if not self.db:exists(self.name) then
+      return 0
+    end
+    local res = self.db:eval("select count(*) from " .. self.name)
+    return res[1]["count(*)"]
+  end, self)
+end
+
+---Query the table and return results.
+---
+---
+---```lua
+--- --- get everything
+--- todos:get()
+--- --- get row with id of 1
+--- todos:get { where = { id = 1 } }
+--- --- select a set of keys with computed one
+--- timestamps:get {
+---   select = {
+---     age = (strftime("%s", "now") - strftime("%s", "timestamp")) * 24 * 60,
+---     "id",
+---     "timestamp",
+---     "entry",
+---     },
+---   }
+---```
+---
+---@param query sqlite_query_select +---@return table +---@see sqlite.db:select +function sqlite.tbl:get(query) + -- query = query or { query = { all = 1 } } + + return h.run(function() + local res = self.db:select(self.name, query or { query = { all = 1 } }, self.db_schema) + return res + end, self) +end + +---Get first match. +--- +---
+---```lua
+--- --- get single entry. notice that we don't pass where key.
+--- tbl:where{ id = 1 }
+--- --- get row with id of 1 or 'todos.where { id = 1 }'
+---```
+---
+---@param where table: where key values +---@return nil or row +---@usage `` +---@see sqlite.db:select +function sqlite.tbl:where(where) + return where and self:get({ where = where })[1] or nil +end + +---Iterate over table rows and execute {func}. +---Returns false if no row is returned. +--- +---
+---```lua
+--- --- Execute a function on each returned row
+--- todos:each(function(row)
+---   print(row.title)
+--- end, {
+---   where = { status = "pending" },
+---   contains = { title = "fix*" }
+--- })
+---
+--- todos:each({ where = { ... }}, function(row)
+---   print(row.title)
+--- end)
+---```
+---
+---@param func function: func(row) +---@param query sqlite_query_select|nil +---@return boolean +function sqlite.tbl:each(func, query) + if type(func) == "table" then + func, query = query, func + end + + return h.run(function() + local rows = self.db:select(self.name, query or {}, self.db_schema) + if not rows then + return false + end + + for _, row in ipairs(rows) do + func(row) + end + + return rows ~= {} or type(rows) ~= "boolean" + end, self) +end + +---Create a new table from iterating over a tbl rows with {func}. +--- +---
+---```lua
+--- --- transform rows.
+--- local rows = todos:map(function(row)
+---   row.somekey = ""
+---   row.f = callfunction(row)
+---   return row
+--- end, {
+---   where = { status = "pending" },
+---   contains = { title = "fix*" }
+--- })
+--- --- This works too.
+--- local titles = todos:map({ where = { ... }}, function(row)
+---   return row.title
+--- end)
+--- --- no query, no problem :D
+--- local all = todos:map(function(row) return row.title end)
+---```
+---
+---@param func function: func(row) +---@param query sqlite_query_select +---@return table[] +function sqlite.tbl:map(func, query) + if type(func) == "table" then + func, query = query, func + end + + return h.run(function() + local res = {} + local rows = self.db:select(self.name, query or {}, self.db_schema) + if not rows then + return {} + end + for _, row in ipairs(rows) do + local ret = func(row) + if ret then + table.insert(res, func(row)) + end + end + + return res + end, self) +end + +---Sorts a table in-place using a transform. Values are ranked in a custom order of the results of +---running `transform (v)` on all values. `transform` may also be a string name property sort by. +---`comp` is a comparison function. Adopted from Moses.lua +--- +---
+---```lua
+--- --- return rows sort by id.
+--- local res = t1:sort({ where = {id = {32,12,35}}})
+--- --- return rows sort by age
+--- local res = t1:sort({ where = {id = {32,12,35}}}, "age")`
+--- --- return with custom sort function (recommended)
+--- local res = t1:sort({where = { ... }}, "age", function(a, b) return a > b end)`
+---```
+---
+---@param query sqlite_query_select|nil +---@param transform function: a `transform` function to sort elements. Defaults to @{identity} +---@param comp function: a comparison function, defaults to the `<` operator +---@return table[] +function sqlite.tbl:sort(query, transform, comp) + return h.run(function() + local res = self.db:select(self.name, query or { query = { all = 1 } }, self.db_schema) + local f = transform or function(r) + return r[u.keys(query.where)[1]] + end + if type(transform) == "string" then + f = function(r) + return r[transform] + end + end + comp = comp or function(a_, b_) + return a_ < b_ + end + table.sort(res, function(a_, b_) + return comp(f(a_), f(b_)) + end) + return res + end, self) +end + +---Insert rows into a table. +--- +---
+---```lua
+--- --- single item.
+--- todos:insert { title = "new todo" }
+--- --- insert multiple items, using todos table as first param
+--- tbl.insert(todos, "items", {  { name = "a"}, { name = "b" }, { name = "c" } })
+---```
+---
+---@param rows table: a row or a group of rows +---@see sqlite.db:insert +---@usage `todos:insert { title = "stop writing examples :D" }` insert single item. +---@usage `todos:insert { { ... }, { ... } }` insert multiple items +---@return integer: last inserted id +function sqlite.tbl:insert(rows) + return h.run(function() + local succ, last_rowid = self.db:insert(self.name, rows, self.db_schema) + if succ then + self.has_content = self:count() ~= 0 or false + end + return last_rowid + end, self) +end + +---Delete a rows/row or table content based on {where} closure. If {where == nil} +---then clear table content. +--- +---
+---```lua
+--- --- delete todos table content
+--- todos:remove()
+--- --- delete row that has id as 1
+--- todos:remove { id = 1 }
+--- --- delete all rows that has value of id 1 or 2 or 3
+--- todos:remove { id = {1,2,3} }
+--- --- matching ids or greater than 5
+--- todos:remove { id = {"<", 5} } -- or {id = "<5"}
+---```
+---
+---@param where sqlite_query_delete +---@see sqlite.db:delete +---@return boolean +function sqlite.tbl:remove(where) + return h.run(function() + return self.db:delete(self.name, where) + end, self) +end + +---Update table row with where closure and list of values +---returns true incase the table was updated successfully. +--- +---
+---```lua
+--- --- update todos status linked to project "lua-hello-world" or "rewrite-neoivm-in-rust"
+--- todos:update {
+---   where = { project = {"lua-hello-world", "rewrite-neoivm-in-rust"} },
+---   set = { status = "later" }
+--- }
+--- --- pass custom statement and boolean
+--- ts:update {
+---   where = { id = "<" .. 4 }, -- mimcs WHERE id < 4
+---   set = { seen = true } -- will be converted to 0.
+--- }
+---```
+---
+---@param specs sqlite_query_update +---@see sqlite.db:update +---@see sqlite_query_update +---@return boolean +function sqlite.tbl:update(specs) + return h.run(function() + local succ = self.db:update(self.name, specs, self.db_schema) + return succ + end, self) +end + +---Replaces table content with a given set of {rows}. +--- +---
+---```lua
+--- --- replace project table content with a single call
+--- todos:replace { { ... }, { ... }, { ... },  }
+---
+--- --- replace everything with a single row
+--- ts:replace { key = "val" }
+---```
+---
+---@param rows table[]|table +---@see sqlite.db:delete +---@see sqlite.db:insert +---@return boolean +function sqlite.tbl:replace(rows) + return h.run(function() + self.db:delete(self.name) + local succ = self.db:insert(self.name, rows, self.db_schema) + return succ + end, self) +end + +---Changes the db object to which the sqlite_tbl correspond to. If the object is +---going to be passed to |sqlite.new|, then it will be set automatically at +---definition. +---@param db sqlite_db +function sqlite.tbl:set_db(db) + self.db = db +end + +sqlite.tbl = setmetatable(sqlite.tbl, { + __call = function(_, ...) + return sqlite.tbl.new(...) + end, +}) + +return sqlite.tbl diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/cache.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/cache.lua new file mode 100644 index 0000000..94ac5cb --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/cache.lua @@ -0,0 +1,82 @@ +local u = require "sql.utils" +local luv = require "luv" + +local Cache = {} + +Cache.__index = Cache + +local parse_query = (function() + local concat = function(v) + if not u.is_list(v) and type(v) ~= "string" then + local tmp = {} + for _k, _v in u.opairs(v) do + if type(_v) == "table" then + table.insert(tmp, string.format("%s=%s", _k, table.concat(_v, ","))) + else + table.insert(tmp, string.format("%s=%s", _k, _v)) + end + end + return table.concat(tmp, "") + else + return table.concat(v, "") + end + end + + return function(query) + local items = {} + for k, v in u.opairs(query) do + if type(v) == "table" then + table.insert(items, string.format("%s=%s", k, concat(v))) + else + table.insert(items, k .. "=" .. v) + end + end + return table.concat(items, ",") + end +end)() + +-- assert(parse_query { where = { a = { 12, 99, 32 } } } == "where=a=12,99,32") +-- assert(parse_query { where = { a = 32 } } == "where=a=32") +-- print(vim.inspect(parse_query { keys = { "b", "c" }, where = { a = 1 } })) +-- "keys=bc,select=bc,where=a=1" +---Insert to cache using query definition. +---@param query table +---@param result table +function Cache:insert(query, result) + self.store[parse_query(query)] = result +end + +---Clear cache when succ is true, else skip. +function Cache:clear(succ) + if succ then + self.store = {} + self.db.modified = false + end +end + +function Cache:is_empty() + return next(self.store) == nil +end + +---Get results from cache. +---@param query sqlite_query_select +---@return table +function Cache:get(query) + local stat = luv.fs_stat(self.db.uri) + local mtime = stat and stat.mtime.sec + + if self.db.modified or mtime ~= self.mtime then + self:clear(true) + return + end + + return self.store[parse_query(query)] +end + +setmetatable(Cache, { + __call = function(self, db) + return setmetatable({ db = db, store = {} }, self) + end, +}) + +return Cache diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/extend.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/extend.lua new file mode 100644 index 0000000..b9a696f --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/tbl/extend.lua @@ -0,0 +1,113 @@ +---@type sqlite_tblext +local tbl = {} + +---Create or change table schema. If no {schema} is given, +---then it return current the used schema if it exists or empty table otherwise. +---On change schema it returns boolean indecting success. +---@param schema table: table schema definition +---@return table table | boolean +---@usage `tbl.schema()` get project table schema. +---@usage `tbl.schema({...})` mutate project table schema +---@todo do alter when updating the schema instead of droping it completely +tbl.schema = function(schema) end + +---Remove table from database, if the table is already drooped then it returns false. +---@usage `todos:drop()` drop todos table content. +---@see DB:drop +---@return boolean +tbl.drop = function() end + +---Predicate that returns true if the table is empty. +---@usage `if todos:empty() then echo "no more todos, you are free :D" end` +---@return boolean +tbl.empty = function() end + +---Predicate that returns true if the table exists. +---@usage `if not goals:exists() then error("I'm disappointed in you ") end` +---@return boolean +tbl.exists = function() end + +---Query the table and return results. +---@param query sqlite_query_select +---@return table +---@usage `tbl.get()` get a list of all rows in project table. +---@usage `tbl.get({ where = { status = "pending", client = "neovim" }})` +---@usage `tbl.get({ where = { status = "done" }, limit = 5})` get the last 5 done projects +---@see DB:select +tbl.get = function(query) end + +---Get the current number of rows in the table +---@return number +tbl.count = function() end + +---Get first match. +---@param where table: where key values +---@return nil or row +---@usage `tbl.where{id = 1}` +---@see DB:select +tbl.where = function(where) end + +---Iterate over table rows and execute {func}. +---Returns true only when rows is not emtpy. +---@param func function: func(row) +---@param query sqlite_query_select +---@usage `let query = { where = { status = "pending"}, contains = { title = "fix*" } }` +---@usage `tbl.each(function(row) print(row.title) end, query)` +---@return boolean +tbl.each = function(func, query) end + +---Create a new table from iterating over {self.name} rows with {func}. +---@param func function: func(row) +---@param query sqlite_query_select +---@usage `let query = { where = { status = "pending"}, contains = { title = "fix*" } }` +---@usage `local t = todos.map(function(row) return row.title end, query)` +---@return table[] +tbl.map = function(func, query) end + +---Sorts a table in-place using a transform. Values are ranked in a custom order of the results of +---running `transform (v)` on all values. `transform` may also be a string name property sort by. +---`comp` is a comparison function. Adopted from Moses.lua +---@param query sqlite_query_select +---@param transform function: a `transform` function to sort elements. Defaults to @{identity} +---@param comp function: a comparison function, defaults to the `<` operator +---@return table[] +---@usage `local res = tbl.sort({ where = {id = {32,12,35}}})` return rows sort by id +---@usage `local res = tbl.sort({ where = {id = {32,12,35}}}, "age")` return rows sort by age +---@usage `local res = tbl.sort({where = { ... }}, "age", function(a, b) return a > b end)` with custom function +tbl.sort = function(query, transform, comp) end + +---Same functionalities as |DB:insert()| +---@param rows table: a row or a group of rows +---@see DB:insert +---@usage `tbl.insert { title = "stop writing examples :D" }` insert single item. +---@usage `tbl.insert { { ... }, { ... } }` insert multiple items +---@return integer: last inserted id +tbl.insert = function(rows) end + +---Same functionalities as |DB:delete()| +---@param where sqlite_query_delete: key value pairs to filter rows to delete +---@see DB:delete +---@return boolean +---@usage `todos.remove()` remove todos table content. +---@usage `todos.remove{ project = "neovim" }` remove all todos where project == "neovim". +---@usage `todos.remove{{project = "neovim"}, {id = 1}}` remove all todos where project == "neovim" or id =1 +tbl.remove = function(where) end + +---Same functionalities as |DB:update()| +---@param specs sqlite_query_update +---@see DB:update +---@return boolean +tbl.update = function(specs) end + +---replaces table content with {rows} +---@param rows table: a row or a group of rows +---@see DB:delete +---@see DB:insert +---@return boolean +tbl.replace = function(rows) end + +---Set db object for the table. +---@param db sqlite_db +tbl.set_db = function(db) end + +return tbl diff --git a/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/utils.lua b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/utils.lua new file mode 100644 index 0000000..90ba5ef --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/lua/sqlite/utils.lua @@ -0,0 +1,260 @@ +local luv = require "luv" +local M = {} + +M.if_nil = function(a, b) + if a == nil then + return b + end + return a +end + +M.is_str = function(s) + return type(s) == "string" +end + +M.is_tbl = function(t) + return type(t) == "table" +end + +M.is_boolean = function(t) + return type(t) == "boolean" +end + +M.is_userdata = function(t) + return type(t) == "userdata" +end + +M.is_nested = function(t) + return t and type(t[1]) == "table" or false +end + +-- taken from: https://github.com/neovim/neovim/blob/master/runtime/lua/vim/shared.lua +M.is_list = function(t) + if type(t) ~= "table" then + return false + end + + local count = 0 + + for k, _ in pairs(t) do + if type(k) == "number" then + count = count + 1 + else + return false + end + end + + if count > 0 then + return true + else + return getmetatable(t) ~= {} + end +end + +M.okeys = function(t) + local r = {} + for k in M.opairs(t) do + r[#r + 1] = k + end + return r +end + +M.opairs = (function() + local __gen_order_index = function(t) + local orderedIndex = {} + for key in pairs(t) do + table.insert(orderedIndex, key) + end + table.sort(orderedIndex) + return orderedIndex + end + + local nextpair = function(t, state) + local key + if state == nil then + -- the first time, generate the index + t.__orderedIndex = __gen_order_index(t) + key = t.__orderedIndex[1] + else + -- fetch the next value + for i = 1, table.getn(t.__orderedIndex) do + if t.__orderedIndex[i] == state then + key = t.__orderedIndex[i + 1] + end + end + end + + if key then + return key, t[key] + end + + -- no more value to return, cleanup + t.__orderedIndex = nil + return + end + + return function(t) + return nextpair, t, nil + end +end)() + +M.expand = function(path) + local expanded + if string.find(path, "~") then + expanded = string.gsub(path, "^~", os.getenv "HOME") + elseif string.find(path, "^%.") then + expanded = luv.fs_realpath(path) + if expanded == nil then + error "Path not valid" + end + elseif string.find(path, "%$") then + local rep = string.match(path, "([^%$][^/]*)") + local val = os.getenv(string.upper(rep)) + if val then + expanded = string.gsub(string.gsub(path, rep, val), "%$", "") + else + expanded = nil + end + else + expanded = path + end + + return expanded and expanded or error "Path not valid" +end + +M.all = function(iterable, fn) + for k, v in pairs(iterable) do + if not fn(k, v) then + return false + end + end + + return true +end + +M.keys = function(t) + local r = {} + for k in pairs(t) do + r[#r + 1] = k + end + return r +end + +M.values = function(t) + local r = {} + for _, v in pairs(t) do + r[#r + 1] = v + end + return r +end + +M.map = function(t, f) + local _t = {} + for i, value in pairs(t) do + local k, kv, v = i, f(value, i) + _t[v and kv or k] = v or kv + end + return _t +end + +M.foreachv = function(t, f) + for i, v in M.opairs(t) do + f(i, v) + end +end + +M.foreach = function(t, f) + for k, v in pairs(t) do + f(k, v) + end +end + +M.mapv = function(t, f) + local _t = {} + for i, value in M.opairs(t) do + local _, kv, v = i, f(value, i) + table.insert(_t, v or kv) + end + return _t +end + +M.join = function(l, s) + return table.concat(M.map(l, tostring), s, 1) +end + +M.tbl_extend = (function() + local run_behavior = setmetatable({ + ["error"] = function(_, k, _) + error("Key already exists: ", k) + end, + ["keep"] = function() end, + ["force"] = function(t, k, v) + t[k] = v + end, + }, { + __index = function(_, k) + error(k .. " is not a valid behavior") + end, + }) + + return function(behavior, ...) + local new_table = {} + local tables = { ... } + for i = 1, #tables do + local b = tables[i] + for k, v in pairs(b) do + if v then + if new_table[k] then + run_behavior[behavior](new_table, k, v) + else + new_table[k] = v + end + end + end + end + return new_table + end +end)() + +-- Flatten taken from: https://github.com/premake/premake-core/blob/master/src/base/table.lua +M.flatten = function(tbl) + local result = {} + local function flatten(arr) + local n = #arr + for i = 1, n do + local v = arr[i] + if type(v) == "table" then + flatten(v) + elseif v then + table.insert(result, v) + end + end + end + flatten(tbl) + + return result +end + +--- taken from https://github.com/tjdevries/lazy.nvim/blob/master/lua/lazy.lua +M.require_on_exported_call = function(require_path) + return setmetatable({}, { + __index = function(_, k) + return function(...) + return require(require_path)[k](...) + end + end, + }) +end + +M.require_on_index = function(require_path) + return setmetatable({}, { + __index = function(_, key) + return require(require_path)[key] + end, + + __newindex = function(_, key, value) + require(require_path)[key] = value + end, + }) +end +return M diff --git a/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_nvimhelp.lua b/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_nvimhelp.lua new file mode 100644 index 0000000..66efcf7 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_nvimhelp.lua @@ -0,0 +1,22 @@ +local docgen = require "docgen" + +local function gen() + local input_files = { + "./lua/sqlite/init.lua", + "./lua/sqlite/db.lua", + "./lua/sqlite/tbl.lua", + } + + local output_file = "./doc/sqlite.txt" + local output_file_handle = io.open(output_file, "w") + + for _, input_file in ipairs(input_files) do + docgen.write(input_file, output_file_handle) + end + + output_file_handle:write " vim:tw=78:sw=2:ts=2:ft=help:norl:et:listchars=\n" + output_file_handle:close() + vim.cmd [[checktime]] +end + +gen() diff --git a/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_rockspec.lua b/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_rockspec.lua new file mode 100644 index 0000000..ec72f33 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/scripts/gen_rockspec.lua @@ -0,0 +1,67 @@ +local scandir = require("plenary.scandir").scan_dir +local job = require "plenary.job" +local uv = vim.loop or require "luv" +local cwd = uv.cwd() +local ins = vim.inspect +local c = function(l) + return table.concat(l, ",\n") +end + +local version = uv.os_getenv "GTAG" or "master" +local modules = {} +local description = { + summary = "SQLite/LuaJIT binding and a highly opinionated wrapper for storing, retrieving, caching, and persisting [SQLite] databases", + homepage = "https://github.com/kkharji/sqlite.lua", + labels = { "sqlite3", "binding", "luajit", "database" }, + detailed = "", + license = "MIT", +} + +local dependencies = { + "luv", +} + +--- Format Dependencies ----------------------------------- +for i, v in ipairs(dependencies) do + dependencies[i] = vim.inspect(v) +end +dependencies = c(dependencies) + +--- Format modules ---------------------------------------- +for _, v in ipairs(scandir(cwd, { search_pattern = "/lua/sqlite/[^examples]" })) do + local path = v:gsub(cwd .. "/", "") + local module = path:gsub("/", "%."):gsub(".lua.(.-).lua", "%1") + modules[module] = path +end + +local output = ([[ +rockspec_format = "3.0" +package = 'sqlite' +version = '%s-0' +description = %s +source = { + url = 'git://github.com/kkharji/sqlite.lua.git', + tag = "%s" +} +dependencies = { + %s +} +build = { + type = "builtin", + modules = %s +} +test = { + type = "command", + command = "make test" +} +test_dependencies = { + "plenary.nvim" +} +]]):format(version, ins(description), version, dependencies, ins(modules)) + +local file_handle = io.open(("%s/sqlite-%s-0.rockspec"):format(cwd, version), "w") + +file_handle:write(output) +file_handle:close() + +vim.cmd [[checktime]] diff --git a/etc/soft/nvim/+plugins/sqlite.lua/sqlite-master-0.rockspec b/etc/soft/nvim/+plugins/sqlite.lua/sqlite-master-0.rockspec new file mode 100644 index 0000000..4e221e6 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/sqlite-master-0.rockspec @@ -0,0 +1,38 @@ +rockspec_format = "3.0" +package = 'sqlite' +version = 'master-0' +description = { + detailed = "", + homepage = "https://github.com/kkharji/sqlite.lua", + labels = { "sqlite3", "binding", "luajit", "database" }, + license = "MIT", + summary = "SQLite/LuaJIT binding and a highly opinionated wrapper for storing, retrieving, caching, and persisting [SQLite] databases" +} +source = { + url = 'git://github.com/kkharji/sqlite.lua.git', + tag = "master" +} +dependencies = { + "luv" +} +build = { + type = "builtin", + modules = { + ["lua.sqlite.db.lua"] = "lua/sqlite/db.lua", + ["lua.sqlite.defs.lua"] = "lua/sqlite/defs.lua", + ["lua.sqlite.helpers.lua"] = "lua/sqlite/helpers.lua", + ["lua.sqlite.init.lua"] = "lua/sqlite/init.lua", + ["lua.sqlite.json.lua"] = "lua/sqlite/json.lua", + ["lua.sqlite.tbl.cache.lua"] = "lua/sqlite/tbl/cache.lua", + ["lua.sqlite.tbl.extend.lua"] = "lua/sqlite/tbl/extend.lua", + ["lua.sqlite.tbl.lua"] = "lua/sqlite/tbl.lua", + ["lua.sqlite.utils.lua"] = "lua/sqlite/utils.lua" +} +} +test = { + type = "command", + command = "make test" +} +test_dependencies = { + "plenary.nvim" +} diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/auto/db_spec.lua b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/db_spec.lua new file mode 100644 index 0000000..f7412b6 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/db_spec.lua @@ -0,0 +1,993 @@ +local P = require "plenary.path" +local curl = require "plenary.curl" +local eq = assert.are.same +local sql = require "sqlite.db" +local u = require "sqlite.utils" +local luv = require "luv" + +describe("sqlite.db", function() + local path = "/tmp/db.sqlite3" + luv.fs_unlink(path) + + describe("sqlfunctions:", function() + local s = sql.lib + local db = sql.new() + it("works with multiply", function() + local sugar = (s.julianday "now" - s.julianday "now") * 5 * 7 + local expected = "(julianday('now') - julianday('now')) * 5 * 7" + eq(expected, sugar, "should be equal") + end) + + it("quotes supported arugments only", function() + local sugar = s.julianday "now" - s.julianday "date" + local expected = "(julianday('now') - julianday(date))" + eq(expected, sugar, "should be equal") + + local sugar = s.julianday "now" - s.julianday "2016-10-18 16:45" + local expected = "(julianday('now') - julianday('2016-10-18 16:45'))" + eq(expected, sugar, "should be equal") + end) + + it("cast as", function() + local sugar = s.cast((s.julianday() - s.julianday "timestamp") * 24 * 60, "integer") + local expected = "cast((julianday('now') - julianday(timestamp)) * 24 * 60 as integer)" + eq(expected, sugar, "should be equal") + local sugar = s.cast(s.julianday() * 7, "integer") + local expected = "cast(julianday('now') * 7 as integer)" + eq(expected, sugar, "should be equal") + end) + db:close() + end) + + describe(".new", function() + it("should create sql.nvim without opening connection", function() + local tmp = "/tmp/db4.db" + local db = sql.new(tmp) + eq("table", type(db), "should return sql.nvim object") + eq(nil, db.conn, "connection shuld be nil") + eq("table", type(db:open()), "should return sql.nvim object") + eq(true, db:isopen(), "should be opened") + eq(0, db:status().code, "should be be zero") + eq(true, db:eval "create table if not exists a(title)", "should be create a table") + eq(true, db:eval "insert into a(title) values('new')", "should be insert stuff") + eq("table", type(db:eval "select * from a"), "should be have content") + eq(true, db:close(), "should close") + eq(true, db:isclose(), "should close") + eq(true, P.exists(P.new(tmp)), "It should created the file") + luv.fs_unlink(tmp) + end) + it("should accept pargma options", function() + local tmp = "/tmp/db5.db" + local db = sql.new(tmp, { + journal_mode = "persist", + }) + db:open() + eq("persist", db:eval("pragma journal_mode")[1].journal_mode) + db:close() + luv.fs_unlink(tmp) + end) + end) + + describe(":open/:close", function() -- todo(kkharji): change to open instead of connect. + it("creates in memory database.", function() + local db = sql:open() + eq("table", type(db), "returns new main interface object.") + eq("cdata", type(db.conn), "returns sql object.") + assert(db:close(), "returns true if the connection is closed successfully.") + end) + + it("creates new persistence database.", function() + local db = sql:open(path) + eq("cdata", type(db.conn), "returns sqlite object.") + + eq(true, db:eval "create table if not exists todo(title text, desc text, created int)") + + db:eval("insert into todo(title, desc, created) values(:title, :desc, :created) ", { + { title = "1", desc = "......", created = 2021 }, + { title = "2", desc = "......", created = 2021 }, + }) + + eq(true, db:close(), "It should close connection successfully.") + eq(true, P.exists(P.new(path)), "It should created the file") + end) + + it("connects to pre-existent database.", function() + local db = sql:open(path) + + local todos = db:eval "select * from todo" + eq("1", todos[1].title) + eq("2", todos[2].title) + + eq(true, db:close(), "It should close connection successfully.") + eq(true, P.exists(P.new(path)), "File should still exists") + luv.fs_unlink(path) + end) + + it("returns data and time of creation", function() + local db = sql:open() + eq(os.date "%Y-%m-%d %H:%M:%S", db.created) + db:close() + end) + + it("should accept pargma options", function() + local tmp = "/tmp/db912.db" + local db = sql:open(tmp, { + journal_mode = "persist", + }) + eq("persist", db:eval("pragma journal_mode")[1].journal_mode) + db:close() + luv.fs_unlink(tmp) + end) + + it("reopen db object.", function() + local db = sql:open(path) + local row = { title = "1", desc = "...." } + db:eval "create table if not exists todo(title text, desc text)" + db:eval("insert into todo(title, desc) values(:title, :desc)", row) + + db:close() + eq(true, db.closed, "should be closed.") + + db:open() + eq(false, db.closed, "should be reopend.") + + eq(path, db.uri, "uri should be identical to db.uri") + local res = db:eval "select * from todo" + eq(row, res[1], luv.fs_unlink(path), "local row should equal db:eval result.") + end) + end) + + describe(":open_with", function() + it("works with initalized sql objects.", function() + local db = sql:open(path) + local row = { title = "1", desc = "...." } + db:eval "create table if not exists todo(title text, desc text)" + db:eval("insert into todo(title, desc) values(:title, :desc)", row) + db:close() + eq(true, db.closed, "should be closed.") + + local res = db:with_open(function() + return db:eval "select * from todo" + end) + eq(true, db.closed, "should be closed.") + eq("1", res[1].title, "should pass.") + + luv.fs_unlink(path) + end) + + it("works without initalizing sql objects. (via uri)", function() + local res = sql.with_open(path, function(db) + local row = { title = "1", desc = "...." } + db:eval "create table if not exists todo(title text, desc text)" + db:eval("insert into todo(title, desc) values(:title, :desc)", row) + return db:eval "select * from todo" + end) + eq("1", res[1].title, "should pass.") + end) + end) + + describe(":isopen/isclose", function() + local db = sql:open() + it("returns true if the db connection is open", function() + assert(db:isopen()) + end) + it("returns true if the db connection is closed", function() + db:close() + assert(db:isclose()) + end) + end) + + describe(":status", function() + local db = sql:open() + local status = db:status() + it("returns status code of the last filed call", function() + eq("number", type(status.code), "it should return sqlite last flag code") + end) + + it("returns error message of the last filed call", function() + eq("string", type(status.msg), "it should return sqlite last errmsg") + end) + + it("return whether the connection is closed or still open", function() + assert(not status.closed, "it should return connection status") + db:close() + end) + end) + + describe(":eval", function() + local db = sql:open() + it("It evaluate should basic sqlite statement", function() + eq(true, db:eval "create table people(id integer primary key, name text, age integer)") + end) + + it("returns true if the operation is successful", function() + eq(true, db:eval "create table if not exists todos(id, title text, desc text, created int)") + end) + + local row = { + { + id = 1, + title = "Create something", + desc = "Don't you dare to tell conni", + created = os.time(), + }, + { + id = 2, + title = "Don't work on something else", + desc = "keep conni close by :P", + created = os.time(), + }, + } + + it("inserts lua table.", function() + eq(true, db:eval("insert into todos(id, title,desc,created) values(:id, :title, :desc, :created)", row[1])) + end) + + it("selects everything from table and return lua table if only one row.", function() + local res = db:eval "select * from todos" + eq(row[1], res[1]) + end) + + it("deletes from sql tables", function() + eq(true, db:eval "delete from todos", "It should delete table content") + end) + + it("inserts nested lua table.", function() + eq(true, db:eval("insert into todos(id,title,desc,created) values(:id, :title, :desc, :created)", row)) + eq(row, db:eval "select * from todos", "should equal") + end) + + it("return a lua table by id", function() + local res + res = db:eval("select * from todos where id = :id", { id = 1 }) + eq(row[1], res[1], "with named params") + + res = db:eval("select * from todos where id = ?", { 1 }) + eq(row[1], res[1], "with unamed params") + + res = db:eval("select * from todos where id = ?", 1) + eq(row[1], res[1], "with single value") + end) + it("return the lua table with where and or", function() + eq(row, db:eval "select * from todos where id = 1 or id = 2 or id = 3") + end) + it("update by id", function() + eq( + true, + db:eval("update todos set desc = :desc where id = :id", { + id = 1, + desc = "...", + }), + "it works with named params" + ) + eq( + true, + db:eval("update todos set desc = ? where id = ?", { + 2, + "...", + }), + "it works with unnamed params" + ) + end) + + it("delete by id", function() + eq(true, db:eval("delete from todos where id = :id", { id = 2 })) + eq(true, db:eval("delete from todos where id = :id", { id = 1 })) + eq(true, db:eval "select * from todos", "It should be empty by now.") + end) + db:close() + end) + + describe(":insert", function() + local db = sql:open() + assert(db:eval "create table todos(title text, desc text)") + local ex_last_rowid = 0 + + it("inserts a single row", function() + local rows = { + title = "TODO 1", + desc = "................", + } + local _, last_row_id = db:insert("todos", rows) + ex_last_rowid = ex_last_rowid + 1 + local results = db:eval "select rowid, * from todos" + eq(true, type(results) == "table", "It should be inserted.") + eq(last_row_id, results[1].rowid, "It should be equals to the returned last inserted rowid " .. last_row_id) + eq("TODO 1", results[1].title) + eq("................", results[1].desc) + db:eval "delete from todos" + end) + + it("inserts multiple rows", function() + local rows = { + { + title = "todo 3", + desc = "...", + }, + { + title = "todo 2", + desc = "...", + }, + { + title = "todo 1", + desc = "...", + }, + } + local _, rowid = db:insert("todos", rows) + ex_last_rowid = ex_last_rowid + #rows + local store = db:eval "select rowid, * from todos" + eq(rowid, store[#store].rowid, "It should be equals to the returned last inserted rowid " .. rowid) + eq(3, vim.tbl_count(store)) + for _, v in ipairs(store) do + eq(true, v.desc == "...") + end + db:eval "delete from todos" + end) + + assert(db:eval [[ + create table if not exists test( + id integer primary key, + title text, + name text not null, + created integer default 'today', + current timestamp default current_date, + num integer default 0) + ]]) + + it("respects string defaults", function() + db:insert("test", { + { title = "A", name = "B" }, + { title = "C", name = "D" }, + }) + local res = db:eval [[ select * from test]] + eq("today", res[1].created) -- FIXME + eq("today", res[2].created) -- FIXME + end) + + it("respects number defaults", function() + db:insert("test", { + { title = "A", name = "B" }, + { title = "C", name = "D" }, + }) + local res = db:eval [[ select * from test]] + eq(0, res[1].num) + eq(0, res[2].num) + end) + + it("respects sqlvalues defaults", function() + db:insert("test", { + { title = "A", name = "B" }, + { title = "C", name = "D" }, + }) + local res = db:eval [[ select * from test]] + eq(os.date "%Y-%m-%d", res[1].current) + eq(os.date "%Y-%m-%d", res[2].current) + end) + + it("respects fails if a key is null", function() + local ok, _ = pcall(function() + return db:insert("test", { title = "A" }) + end) + eq(false, ok, "should fail") + end) + + it("serialize lua table in sql column", function() + db:eval "drop table test" + db:eval "create table test(id integer, data luatable)" + db.tbl_schemas.test = nil + db:insert("test", { id = 1, data = { "list", "of", "lines" } }) + + local res = db:eval [[select * from test]] + eq('["list","of","lines"]', res[1].data) + db:eval "drop table test" + end) + + it("evaluates sqlite functions", function() + db:eval "create table testa(id integer primary key, date text, title text)" + db:insert("testa", { title = "fd", date = db.lib.strftime "%H:%M:%S" }) + + local res = db:eval [[select * from testa]] + eq(os.date "!%H:%M:%S", res[1].date) + eq("fd", res[1].title) + db:eval "drop table testa" + end) + db:close() + end) + + describe(":update", function() + local db = sql:open() + assert(db:eval "create table todos(title text, desc text)") + + it("works with table_name being as the first argument", function() + local row = { title = "TODO 1", desc = "................" } + db:eval("insert into todos(title, desc) values(:title, :desc)", row) + + db:update("todos", { + values = { desc = "done" }, + where = { title = "TODO 1" }, + }) + local results = db:eval "select * from todos" + eq(true, type(results) == "table", "It should be inserted.") + eq("TODO 1", results[1].title) + eq("done", results[1].desc) + db:eval "delete from todos" + end) + + it("works with table_name being a lua table key", function() + local row = { title = "TODO 1", desc = " .......... " } + db:eval("insert into todos(title, desc) values(:title, :desc)", row) + + db:update("todos", { + values = { desc = "not done" }, + where = { title = "TODO 1" }, + }) + + local results = db:eval "select * from todos" + eq(true, type(results) == "table", "It should be inserted.") + eq("TODO 1", results[1].title) + eq("not done", results[1].desc) + db:eval "delete from todos" + end) + + it("update multiple rows in a sql table", function() + db:eval "delete from todos" + local rows = { + { + title = "todo 3", + desc = "...", + }, + { + title = "todo 2", + desc = "...", + }, + { + title = "todo 1", + desc = "...", + }, + } + db:eval("insert into todos(title, desc) values(:title, :desc)", rows) + db:update("todos", { + { + values = { desc = "not done" }, + where = { title = "todo 1" }, + }, + { + values = { desc = "done" }, + where = { title = "todo 2" }, + }, + { + values = { desc = "almost done" }, + where = { title = "todo 3" }, + }, + }) + + local store = db:eval "select * from todos" + eq(3, vim.tbl_count(store)) + for _, v in ipairs(store) do + eq(true, v.desc ~= "...") + end + db:eval "delete from todos" + end) + + it("skip updating if opts is nil", function() + db:update "todos" + end) + + it("fallback to insert", function() + local tmp = "nvim.nvim" + db:update("todos", { + values = { desc = tmp }, + where = { title = "3" }, + }) + local store = db:eval "select * from todos" + eq(tmp, store[1].desc) + eq("3", store[1].title) + end) + db:close() + end) + + describe(":delete", function() + local db = sql:open() + assert(db:eval "create table todos(title text, desc text)") + + it("works with table_name being as the first argument", function() + local row = { title = "TODO 1", desc = "................" } + db:eval("insert into todos(title, desc) values(:title, :desc)", row) + db:delete "todos" + local results = db:eval "select * from todos" + eq(false, type(results) == "table", "It should be deleted.") + end) + + it("works with table_name being and where", function() + local rows = { + { title = "TODO 1", desc = "................" }, + { title = "TODO 2", desc = "................" }, + } + db:eval("insert into todos(title, desc) values(:title, :desc)", rows) + db:delete("todos", { where = { title = "TODO 1" } }) + local results = db:eval "select * from todos" + eq(true, type(results) == "table") + eq("TODO 2", results[1].title) + eq("................", results[1].desc) + end) + + it("delete multiple keys with list of ors", function() + local rows = { + { title = "TODO 1", desc = "................" }, + { title = "TODO 2", desc = "................" }, + } + db:eval("insert into todos(title, desc) values(:title, :desc)", rows) + db:delete("todos", { { where = { title = { "TODO 1", "TODO 2" } } } }) + local results = db:eval "select * from todos" + eq(false, type(results) == "table") + end) + + it("delete multiple keys with dict for each conditions. and passing only where", function() + local rows = { + { title = "TODO 1", desc = "................" }, + { title = "TODO 2", desc = "................" }, + } + db:eval("insert into todos(title, desc) values(:title, :desc)", rows) + db:delete("todos", { { title = "TODO 1" }, { title = "TODO 2" } }) + local results = db:eval "select * from todos" + eq(false, type(results) == "table") + end) + it("delete multiple keys with dict for each conditions. V2", function() + local rows = { + { title = "TODO 1", desc = "................" }, + { title = "TODO 2", desc = "................" }, + } + db:eval("insert into todos(title, desc) values(:title, :desc)", rows) + db:delete("todos", { title = { "TODO 1", "TODO 2" } }) + local results = db:eval "select * from todos" + eq(false, type(results) == "table") + end) + db:close() + end) + + describe(":select", function() + if vim.fn.executable("curl") then + pending("'curl' program is not available") + return + end + local db = sql:open(path) + local posts, users + + it(".... pre", function() + if luv.fs_stat "/tmp/posts" == nil then + curl.get("https://jsonplaceholder.typicode.com/posts", { output = "/tmp/posts" }) + curl.get("https://jsonplaceholder.typicode.com/users", { output = "/tmp/users" }) + end + + posts = vim.fn.json_decode(vim.fn.join(P.readlines "/tmp/posts", "\n")) + + eq("table", type(posts), "should have been decoded") + + assert(db:eval [[ + create table posts(id int not null primary key, userId int, title text, body text); + ]]) + + eq( + true, + db:eval( + [[ + insert into posts(id, userId, title, body) values(:id, :userId, :title, :body) + ]], + posts + ) + ) + + eq("table", type(db:eval "select * from posts"), "there should be posts") + + users = vim.fn.json_decode(vim.fn.join(P.readlines "/tmp/users", "\n")) + eq("table", type(users), "should have been decoded") + for _, user in ipairs(users) do -- Not sure how to deal with nested tables now + user["address"] = nil + user["company"] = nil + end + + assert(db:eval [[ + create table users(id int not null primary key, name text, email text, phone text, website text, username text); + ]]) + + eq( + true, + db:eval( + [[ + insert into users(id, name, email, phone, website, username) values(:id,:name,:email,:phone,:website,:username) + ]], + users + ) + ) + + eq("table", type(db:eval "select * from users"), "there should be users") + end) + + it("return everything with no params", function() + eq(posts, db:select "posts") + end) + + it("return everything that matches where closure", function() + local res = db:select("posts", { + where = { + id = 1, + }, + }) + local expected = (function() + for _, post in ipairs(posts) do + if post["id"] == 1 then + return post + end + end + end)() + + eq(expected, res[1]) + end) + + it("join tables.", function() + local res = db:select("posts", { where = { id = 1 }, join = { posts = "userId", users = "id" } }) + local expected = (function() + for _, post in ipairs(posts) do + if post["id"] == 1 then + return u.tbl_extend( + "keep", + post, + (function() + for _, user in ipairs(users) do + if user["id"] == post["userId"] then + return user + end + end + end)() + ) + end + end + end)() + + eq(expected, res[1]) + end) + + it("return selected keys only", function() + local res = db:select("posts", { where = { id = 1 }, keys = { "userId", "posts.body" } }) + local expected = (function() + for _, post in ipairs(posts) do + if post["id"] == 1 then + return { + userId = post["userId"], + body = post["body"], + } + end + end + end)() + + eq(expected, res[1]) + end) + + it("return respecting a limit", function() + local limit = 5 + local res = db:select("posts", { limit = limit }) + eq(#res, limit, "they should be the same") + end) + + it("it serialize json if the schema key datatype is json", function() + db:eval "create table test(id integer, data luatable)" + db:eval 'insert into test(id,data) values(1, \'["list","of","lines"]\')' + eq({ + { id = 1, data = { "list", "of", "lines" } }, + }, db:select "test", "they should be identical") + end) + + db:close() + end) + + describe(":schema", function() + local db = sql:open() + db:eval "create table test(a text, b int, c int not null, d text default def)" + + it("gets a sql table schema", function() + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + local sch = db:schema "test" + for k, v in pairs(sch) do + v.type = string.upper(v.type) + sch[k] = v + end + + eq({ + a = { + cid = 0, + primary = false, + required = false, + type = "TEXT", + }, + b = { + cid = 1, + primary = false, + required = false, + type = "INT", + }, + c = { + cid = 2, + primary = false, + required = true, + type = "INT", + }, + d = { + cid = 3, + default = "def", + primary = false, + required = false, + type = "TEXT", + }, + }, sch) + end) + + it("gets a sql table schema info", function() + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + local sch = db:schema "test" + for k, v in pairs(sch) do + v.type = string.upper(v.type) + sch[k] = v + end + eq({ + a = { + cid = 0, + primary = false, + required = false, + type = "TEXT", + }, + b = { + cid = 1, + primary = false, + required = false, + type = "INT", + }, + c = { + cid = 2, + primary = false, + required = true, + type = "INT", + }, + d = { + cid = 3, + primary = false, + required = false, + type = "TEXT", + default = "def", + }, + }, sch) + end) + + db:close() + end) + + describe(":create", function() + local db = sql:open() + + it("create a new sqlite table, and return true", function() + eq(false, db:exists "test") + db:create("test", { + id = { "integer", "primary", "key" }, + title = "text", + desc = "text", + created = "int", + done = { "int", "not", "null", "default", 0 }, + }) + eq(true, db:exists "test") + end) + + it("skip overriding the table schema if it exists", function() + db:create("test", { id = "not_a_type", ensure = true }) + local sch = db:schema "test" + + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + eq("TEXT", string.upper(sch.title.type), "should exists and should be still text not be nil") + end) + it("auto enable foreign_keys on usage", function() + db:create("test_keys", { + id = true, + title = "text", + project = { + type = "integer", + reference = "projects.id", + }, + }) + eq(1, db:eval("pragma foreign_keys")[1].foreign_keys) + end) + db:close() + end) + + describe(":drop", function() + local db = sql:open() + + it("should drop empty tables.", function() + db:create("test", { a = "text", b = "int" }) + eq(true, db:exists "test", "should exists") + db:drop "test" + eq(true, not db:exists "test", "shout be dropped") + end) + + it("should drop non-empty tables.", function() + db:create("test", { a = "text", b = "int" }) + db:eval("insert into test(a,b) values(?,?)", { "a", 3 }) + eq(true, db:exists "test", "should exists") + db:drop "test" + eq(true, not db:exists "test", "shout be dropped") + end) + db:close() + end) + + describe(":extend", function() + local testrui = "/tmp/extend_db_new" + local testrui2 = "/tmp/extend_db_new5" + local testrui3 = "/tmp/extend_db_new5" + local ok, manager + + it("Initialize manager", function() + ---@class ManagerLazy:sqlite_db + ---@field projects sqlite_tbl + ---@field todos sqlite_tbl + ok, manager = pcall(sql, { + uri = testrui, + projects = { + id = true, + title = "text", + objectives = "luatable", + }, + todos = { + id = true, + title = "text", + client = "integer", + status = "text", + completed = "boolean", + details = "text", + }, + opts = { + foreign_keys = true, + lazy = true, + }, + }) + eq(true, ok, manager) + end) + + it("process opts and set sql table objects", function() + eq("/tmp/extend_db_new", manager.uri, "should set self.uri.") + eq(true, manager.closed, "should construct without openning connection.") + eq(nil, manager.conn, "should not set connection object.") + eq("boolean", type(manager.opts.foreign_keys), "should have added opts to sqlite_opts.") + end) + + it("creates self.projects and self.todos", function() + eq("table", type(manager.projects), "should have added sql table object") + eq("table", type(manager.todos), "should have added sql table object for todos") + end) + + it("access normal operations without self.db", function() + eq("function", type(manager.insert), "should have added insert function.") + eq("function", type(manager.open), "should have added open function.") + eq("function", type(manager.close), "should have added close function.") + eq("function", type(manager.__insert), "should have added insert function.") + eq("function", type(manager.__with_open), "should have added with_open.") + eq("function", type(manager.__table), "should have added table.") + end) + + it("extending new object should work wihout issues", function() + local sqlnvim = { + title = "sql.nvim", + objectives = { + "Make storing data and persisting data ridiculously simple.", + "Have great documentation and real world examples.", + "More and more neovim plugins adopt sql.nvim as a data layer.", + }, + } + eq("function", type(manager.projects.insert), "should have added insert function.") + eq("function", type(manager.projects.where), "should have added with_open.") + eq(1, manager.projects:insert(sqlnvim), "should insert and return id.") + eq("cdata", type(manager.conn), "should set connection object after first call to sql api") + eq("table", type(manager.projects:where({ id = 1 }).objectives), "should return as table.") + eq("table", type(sqlnvim.objectives), "It shouldn't have mutated objectives table.") + eq(true, manager.projects:remove(), "should remove after default insert.") + + function manager.projects:insert() + return self:__insert(sqlnvim) + end + function manager.projects:get() + return manager.projects:__get({ where = { title = sqlnvim.title } })[1] + end + function manager.projects:remove_objectives() + return self:update { where = { id = 1 }, set = { objectives = {} } } + end + + eq(1, manager.projects:insert(), "should have inserted and returned id.") + + eq(sqlnvim.title, manager.projects:get().title, "should have inserted sqlnvim project") + eq(true, manager.projects:get().objectives ~= "") + eq(true, manager.projects:remove_objectives(), "should succeed at updating") + eq({}, manager.projects:get().objectives, "should return empty table") + end) + + it("set a different name for sql db table, with access using extend field", function() + local ok, db = pcall(sql, { + uri = testrui2, + s = { _name = "stable", id = true }, + }) + eq(true, ok, db) + + eq(db.s.name, "stable") + end) + + it("use pre-defined sql-table", function() + local t = require "sqlite.tbl"("sometbl", { id = true, name = "string" }) + local db + + local ok, db = pcall(sql, { + uri = testrui, + st = t, + }) + + eq(true, ok, db) + + eq("table", type(db.st), "should use that key to access t") + + db.st:insert { { name = "a" }, { name = "b" }, { name = "c" } } + + eq(3, db.st:count(), "should have inserted.") + end) + + it("Initialize manager", function() + ---@class ManagerFull:sqlite_db + ---@field projects sqlite_tbl + ---@field todos sqlite_tbl + ok, manager = pcall(sql, { + uri = testrui, + projects = { + id = true, + title = "text", + objectives = "luatable", + }, + todos = { + id = true, + title = "text", + client = "integer", + status = "text", + completed = "boolean", + details = "text", + }, + opts = { + foreign_keys = true, + keep_open = true, + }, + }) + eq(true, ok, manager) + end) + it("should exists", function() + eq(true, manager:exists "projects") + end) + + luv.fs_unlink(testrui) + luv.fs_unlink(testrui2) + luv.fs_unlink(testrui3) + end) + describe("misc/issues", function() + it("false isn't treated as nil #123", function() + ---@type sqlite_db + local db = sql { + uri = "/tmp/test_db", + tbl = { + ok_key = "text", + problem_key = { "boolean", required = true }, + }, + opts = { keep_open = true }, + } + + local ok, err = pcall(function() + return db:insert("tbl", { ok_key = "this works", problem_key = false }) + end) + + eq(true, ok, err) + eq({ ok_key = "this works", problem_key = false }, db.tbl:get()[1]) + eq({ ok_key = "this works", problem_key = false }, db:select("tbl")[1]) + db:close() + luv.fs_unlink "/tmp/test_db" + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/auto/parser_spec.lua b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/parser_spec.lua new file mode 100644 index 0000000..4ffc56f --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/parser_spec.lua @@ -0,0 +1,373 @@ +local p = require "sqlite.parser" +local eq = assert.are.same + +describe("parse", function() + local tbl = "todo" + describe("[insert]", function() + it("post function without key in values", function() + eq( + "insert into todo (date, id) values(date('now'), :id)", + p.insert(tbl, { values = { + date = "date('now')", + id = 1, + } }) + ) + end) + end) + + describe("[all]", function() + it("select all", function() + local sqlstmt = p.select(tbl) + eq("select * from todo", sqlstmt, "It should be identical") + end) + it("delete all", function() + local sqlstmt = p.delete(tbl) + eq("delete from todo", sqlstmt, "It should be identical") + end) + end) + + describe("[where]", function() + it("single key = value", function() + local where = { id = 1 } + local pselect = p.select(tbl, { where = where }) + local eselect = "select * from todo where id = 1" + eq(eselect, pselect, "It should be identical") + end) + it("multi key", function() + local where = { id = 1, act = "done" } + local pselect = p.select(tbl, { where = where }) + local eselect = "select * from todo where act = 'done' and id = 1" + eq(eselect, pselect, "It should be identical") + end) + it("with [or] for single keys", function() + local where = { act = { "done", "overdue" } } + local pselect = p.select(tbl, { where = where }) + local eselect = "select * from todo where (act = 'done' or act = 'overdue')" + eq(eselect, pselect, "It should be identical") + end) + it("with [or] for multi keys", function() + local where = { + act = { "done", "overdue" }, + name = "conni", + date = 2021, + } + local pselect = p.select(tbl, { where = where }) + local eselect = "select * from todo where (act = 'done' or act = 'overdue') and date = 2021 and name = 'conni'" + eq(eselect, pselect, "It should be identical") + end) + it("with multi [or] and multi keys", function() + local where = { + n = { 1, 2, 3 }, + act = { "a", "b" }, + date = 2021, + } + local pselect = p.select(tbl, { where = where }) + local eselect = "select * from todo where (act = 'a' or act = 'b') and date = 2021 and (n = 1 or n = 2 or n = 3)" + eq(eselect, pselect, "It should be identical") + end) + it("interop boolean", function() + local where = { + act = false, + n = true, + date = 2021, + } + local eselect = "select * from todo where act = 0 and date = 2021 and n = 1" + local pselect = p.select(tbl, { where = where }) + eq(eselect, pselect, "It should be identical") + end) + it("handles quotes", function() + local where = { a = "I'm", c = "it's" } + local eselect = [[select * from todo where a = "I'm" and c = "it's"]] + local pselect = p.select(tbl, { where = where }) + eq(eselect, pselect, "It should be identical") + end) + it("concat list and pass it as query", function() + local where = { { "date", "<", 2021 } } + local where = { date = "< 2021" } + local eselect = [[select * from todo where date < 2021]] + local pselect = p.select(tbl, { where = where }) + eq(eselect, pselect) + end) + end) + + describe("[set]", function() + it("single value and [where]", function() + local set = { date = 2021 } + local where = { id = 1 } + local pupdate = p.update(tbl, { where = where, set = set }) + local eupdate = "update todo set date = :date where id = 1" + eq(eupdate, pupdate, "should be identical") + end) + it("multiple value and where", function() + local set = { date = 2021, a = "b", c = "d" } + local where = { id = 1 } + local pupdate = p.update(tbl, { where = where, set = set }) + local eupdate = "update todo set a = :a, c = :c, date = :date where id = 1" + eq(eupdate, pupdate, "should be identical") + end) + end) + + describe("[create]", function() + it("table", function() + local defs = { + id = { "integer", "primary", "key" }, + title = "text", + desc = "text", + created = "int", + done = { "int", "not", "null", "default", 0 }, + } + local expected = + "CREATE TABLE todos(created int, desc text, done int not null default 0, id integer primary key, title text)" + local passed = p.create("todos", defs) + eq(expected, passed, "should be identical") + end) + + it("ensure that the table is created", function() + local defs = { + id = { "integer", "primary", "key" }, + name = "text", + age = "int", + ensure = true, + } + local expected = "CREATE TABLE if not exists people(age int, id integer primary key, name text)" + local passed = p.create("people", defs) + eq(expected, passed, "should be identical") + end) + + it("ensure that the table is created with a key being true", function() + local defs = { + id = true, + name = "text", + age = "int", + ensure = true, + } + local expected = "CREATE TABLE if not exists people(age int, id integer not null primary key, name text)" + local passed = p.create("people", defs) + eq(expected, passed, "should be identical") + end) + + it("key-pair: nullable & unique", function() + local defs = { + id = true, + name = { "text", required = true, unique = true }, + } + local passed = p.create("people", defs) + local expected = "CREATE TABLE people(id integer not null primary key, name text unique not null)" + eq(expected, passed, "should be identical") + end) + it("key-pair: default value", function() + local defs = { + id = true, + name = { "text", default = "unknown" }, + } + local passed = p.create("people", defs) + local expected = "CREATE TABLE people(id integer not null primary key, name text default unknown)" + eq(expected, passed, "should be identical") + end) + + it("key-pair: type", function() + local defs = { + id = true, + name = { type = "text" }, + } + local passed = p.create("people", defs) + local expected = "CREATE TABLE people(id integer not null primary key, name text)" + eq(expected, passed, "should be identical") + end) + + it("primary key", function() + local defs = { + id = { type = "integer", pk = true }, + name = { type = "text", default = "noname" }, + } + local passed = p.create("people", defs) + local expected = { + "CREATE TABLE people(", + "id integer primary key, ", + "name text default noname", + ")", + } + + eq(table.concat(expected, ""), passed, "should be identical") + end) + it("foreign key", function() + local defs = { + id = true, + name = { "text", default = "unknown" }, + job_id = { + type = "integer", + reference = "jobs.id", + }, + } + local passed = p.create("people", defs) + local expected = { + "CREATE TABLE people(", + "id integer not null primary key, ", + "job_id integer references jobs(id), ", + "name text default unknown", + ")", + } + + eq(table.concat(expected, ""), passed, "should be identical") + end) + + it("foreign key + single action", function() + local defs = { + id = true, + name = { "text", default = "unknown" }, + job_id = { + type = "integer", + reference = "jobs.id", + on_update = "cascade", + }, + } + local passed = p.create("people", defs) + local expected = { + "CREATE TABLE people(", + "id integer not null primary key, ", + "job_id integer references jobs(id) on update cascade, ", + "name text default unknown", + ")", + } + + eq(table.concat(expected, ""), passed, "should be identical") + end) + + it("foreign key + multi action", function() + local defs = { + id = true, + name = { "text", default = "unknown" }, + job_id = { + type = "integer", + reference = "jobs.id", + on_update = "cascade", + on_delete = "null", + }, + } + local passed = p.create("people", defs) + local expected = { + "CREATE TABLE people(", + "id integer not null primary key, ", + "job_id integer references jobs(id) on update cascade on delete set null, ", + "name text default unknown", + ")", + } + + eq(table.concat(expected, ""), passed, "should be identical") + end) + end) + + describe("[order by]", function() + it("works with signle table name", function() + local defs = { + select = { "id", "name" }, + order_by = { + desc = "name", + asc = "id", + }, + } + local expected = "select id, name from people order by id asc, name desc" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it("works with multiple table name", function() + local defs = { + select = { "id", "name" }, + order_by = { + asc = { "name", "age" }, + }, + } + local expected = "select id, name from people order by name asc, age asc" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it('works with [select] = "a single string"', function() + local defs = { + select = "id", + order_by = { + asc = { "name", "age" }, + }, + } + local expected = "select id from people order by name asc, age asc" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + end) + + describe("[distinct]", function() + -- remove duplicate from result set + it("with single key", function() + local defs = { -- TODO: works for a single key + select = { "id", "name" }, + unique = true, + } + local expected = "select distinct id, name from people" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + end) + + describe("[limit]", function() + it("with limit as number", function() + local defs = { + select = { "id", "name" }, + limit = 10, + } + local expected = "select id, name from people limit 10" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it("with limit as number inside list", function() + local defs = { + select = { "id", "name" }, + limit = { 10 }, + } + local expected = "select id, name from people limit 10" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it("with limit and offset", function() + local defs = { + select = { "id", "name" }, + limit = { 10, 10 }, + } + local expected = "select id, name from people limit 10 offset 10" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + end) + + describe("[glob]/contains", function() + it("with single key", function() + local defs = { + select = { "id", "name" }, + contains = { name = "%j" }, + } + local expected = "select id, name from people where name glob '%j'" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it("with single key and an list of patterns", function() + local defs = { + select = { "id", "name" }, + contains = { name = { "%j", "%a", "%b%" } }, + } + local expected = "select id, name from people where name glob '%j' or name glob '%a' or name glob '%b%'" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + it("with multi key and an list of patterns", function() + local defs = { + select = { "id", "name" }, + contains = { + name = { "%j", "%a", "%b%" }, + last = "%f", + }, + } + local expected = + "select id, name from people where last glob '%f' name glob '%j' or name glob '%a' or name glob '%b%'" + local passed = p.select("people", defs) + eq(expected, passed, "should be identical") + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/auto/stmt_spec.lua b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/stmt_spec.lua new file mode 100644 index 0000000..c9c9ad5 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/stmt_spec.lua @@ -0,0 +1,435 @@ +local clib = require "sqlite.defs" +local stmt = require "sqlite.stmt" + +local eval = function(conn, st, callback, arg1, errmsg) + return clib.exec(conn, st, callback, arg1, errmsg) +end + +local conn = function(uri) + uri = uri or ":memory:" + local conn = clib.get_new_db_ptr() + local code = clib.open(uri, conn) + if code == clib.flags.ok then + return conn[0] + else + error(string.format("sqlite.lua: couldn't connect to sql database, ERR:", code)) + end +end + +describe("stmt", function() + local eq = assert.are.same + local kill = function(s, db) + if not s.finalized then + eq(true, s:finalize()) + end + eq(0, clib.close(db)) + end + + describe(":parse() ", function() + local db = conn() + local stm = "select * from todos" + + eval(db, "create table todos(id integer primary key, title text, decs text, deadline integer);") + local s = stmt:parse(db, stm) + + it("creates new statement object.", function() + eq("table", type(s)) + end) + it("registers db connection in `s.conn`.", function() + eq("cdata", type(s.conn)) + end) + it("registers unparsed statement in `s.str`.", function() + eq(stm, s.str) + end) + it("parses sql statement in `s.pstmt`.", function() + eq("cdata", type(s.pstmt)) + end) + it("requires s:finalize() in order to close connection.", function() + kill(s, db) + end) + end) + + local db, expectedlist, expectedkv, s + db = conn() -- start of first suit test. + for _, v in pairs { + [[create table todos(id integer primary key, title text, desc text, deadline integer);]], + [[insert into todos (title,desc,deadline) values("Create something", "Don't you dare to tell conni", 2021);]], + [[insert into todos (title,desc,deadline) values("Don't work on something else", 'keep conni close by :P', 2021);]], + } do + eval(db, v) + end + + expectedkv = { + { + id = 1, + title = "Create something", + desc = "Don't you dare to tell conni", + deadline = 2021, + }, + { + id = 2, + title = "Don't work on something else", + desc = "keep conni close by :P", + deadline = 2021, + }, + } + + expectedlist = {} + for i, t in ipairs(expectedkv) do + expectedlist[i] = {} + for _, v in pairs(t) do + table.insert(expectedlist[i], v) + end + end + s = stmt:parse(db, "select * from todos") + + describe(":nkeys() ", function() + it("returns the number of columns/keys in results.", function() + eq(4, s:nkeys()) + end) + end) + + describe(":types() ", function() + it("returns the type of each columns/keys in results.", function() + -- TODO: this fails on github actions for unknown reason. + -- eq({ "number", "string", "string", "number" }, s:types()) + end) + end) + + describe(":convert_type(i) ", function() + it("returns the type of columns/keys by idx.", function() + -- TODO: this fails on github actions for unknown reason. + -- eq("string", s:convert_type(1)) + end) + end) + + describe(":keys() ", function() + it("returns key/column names", function() + eq({ "id", "title", "desc", "deadline" }, s:keys()) + end) + end) + + describe(":key(i) ", function() + it("returns key/column name by idx", function() + eq("id", s:key(0)) + end) + end) + + describe(":kt() ", function() + it("returns a dict of key name and their type.", function() + -- TODO: this fails on github actions for unknown reason. + -- eq({ + -- deadline = "number", + -- desc = "string", + -- id = "number", + -- title = "string", + -- }, s:kt()) + end) + end) + + describe(":kv() ", function() + it("returns (with stmt:each) key-value pairs of all rows", function() + local done = false + local res = {} + s:each(function(st) + table.insert(res, st:kv()) + done = true + end) + vim.wait(4000, function() + return done + end) + eq(true, not vim.tbl_isempty(res), "It should be filled.") + eq(expectedkv, res, "It should be identical.") + end) + end) + + if s.finalized then + s:__parse() + end + + describe(":val(i) ", function() + it("returns (with stmt:each) a list of vals in all the rows at idx.", function() + local res = {} + s:each(function(st) + table.insert(res, st:val(0)) + end) + eq(false, vim.tbl_isempty(res)) + eq({ 1, 2 }, res) + end) + end) + + describe(":vals() ", function() + it("returns (with stmt:each) a nested list all the vals in all the rows.", function() + local res = {} + s:each(function(st) + table.insert(res, st:vals()) + end) + eq(false, vim.tbl_isempty(res)) + -- eq(expectedlist, res) -- the order is missed up. + end) + end) + + describe(":kvrows() ", function() + it("returns a nested kv-pairs all the rows. if no callback.", function() + local res = s:kvrows() + eq(true, not vim.tbl_isempty(res), "it should be filled.") + eq("table", type(res), "it should be of the type table.") + eq("table", type(res[1]), "should be true") + eq(expectedkv, res) + if s.finalized then + s:__parse() + end + end) + end) + + describe(":vrows() ", function() + -- assert(s.finalized) + it("returns a nested list values in all the rows. if no callback.", function() + local res = s:vrows() + eq(true, not vim.tbl_isempty(res), "it should be filled.") + -- eq(expectedlist, res, "it should be identical") -- the order is mixed up + end) + end) + + describe(":nrows() ", function() + it("returns the number of rows in results", function() + eq(2, s:nrows()) + end) + end) + + kill(s, db) -- end of second suit + local bind_db = conn() -- start of third db test + eval(bind_db, [[create table todos(id integer primary key, title text, desc text, deadline integer);]]) + local bind_s = stmt:parse(bind_db, "insert into todos (title,desc,deadline) values(:title, :desc, :deadline);") + local expected_stmt = + "insert into todos (title,desc,deadline) values('Fancy todo title', 'We are doing all the things', 2021.0);" + + describe(":nparam() ", function() + it("returns the parameter count.", function() + eq(3, bind_s:nparam()) + end) + end) + + describe(":param(i) ", function() + it("returns parameter names by idx.", function() + eq(":title", bind_s:param(1)) + eq(":desc", bind_s:param(2)) + eq(":deadline", bind_s:param(3)) + end) + end) + + describe(":params() ", function() + it("returns all parameter keys/names with no idx", function() + eq({ ":title", ":desc", ":deadline" }, bind_s:params()) + end) + end) + + describe(":bind() ", function() + it("binds values by idx", function() + bind_s:bind(1, "Fancy todo title") + bind_s:bind(2, "We are doing all the things") + bind_s:bind(3, 2021) + eq(expected_stmt, bind_s:expand()) + end) + it("bind values by names.", function() + bind_s:bind_clear() + bind_s:bind { + ["title"] = "Fancy todo title", + ["desc"] = "We are doing all the things", + ["deadline"] = 2021, + } + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_next() ", function() + it("bind values by order in which they appear next", function() + bind_s:bind_clear() + bind_s:bind_next "Fancy todo title" + bind_s:bind_next "We are doing all the things" + bind_s:bind_next(2021) + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_clear()", function() + it("clear bindigns.", function() + eq(0, bind_s:bind_clear(), "it should return 0") + end) + end) + + describe(":bind_blob() ", function() + it("binds blobs by idx", function() + bind_s:bind_clear() + bind_s:bind_blob(1, clib.get_new_blob_ptr(), 10) + end) + end) + + describe(":bind_zeroblob()", function() + it("binds a zeroblob by idx", function() + bind_s:bind_clear() + bind_s:bind_zeroblob(1, 10) + eq("insert into todos (title,desc,deadline) values(zeroblob(10), NULL, NULL);", bind_s:expand()) + end) + end) + + kill(bind_s, bind_db) -- end of third test suit + bind_db = conn() -- beginning of forth test suit + eval(bind_db, [[create table todos(id integer primary key, title text, desc text, deadline integer);]]) + bind_s = stmt:parse(bind_db, "insert into todos (title,desc,deadline) values(?, ?, ?);") + expected_stmt = + "insert into todos (title,desc,deadline) values('Fancy todo title', 'We are doing all the things', 2021.0);" + + describe(":nparam() ", function() + it('does as expected with "anonymous parmas"', function() + eq(3, bind_s:nparam()) + end) + end) + + describe(":param(i) ", function() + it('does as expected with "anonymous parmas"', function() + eq("?", bind_s:param(1)) + eq("?", bind_s:param(2)) + eq("?", bind_s:param(3)) + end) + end) + + describe(":params() ", function() + it('does as expected with "anonymous parmas"', function() + eq({ "?", "?", "?" }, bind_s:params()) + end) + end) + + describe(":bind() ", function() + it('does as expected with "anonymous parmas"', function() + bind_s:bind_clear() + bind_s:bind(1, "Fancy todo title") + bind_s:bind(2, "We are doing all the things") + bind_s:bind(3, 2021) + eq(expected_stmt, bind_s:expand()) + bind_s:bind_clear() + bind_s:bind { + "Fancy todo title", + "We are doing all the things", + 2021, + } + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_next() ", function() + it('does as expected with "anonymous parmas"', function() + bind_s:bind_clear() + bind_s:bind_next "Fancy todo title" + bind_s:bind_next "We are doing all the things" + bind_s:bind_next(2021) + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_blob() ", function() + it('does as expected with "anonymous parmas"', function() + bind_s:bind_clear() + bind_s:bind_blob(1, clib.get_new_blob_ptr(), 10) + end) + end) + + describe(":bind_zeroblob()", function() + it('does as expected with "anonymous parmas"', function() + bind_s:bind_clear() + bind_s:bind_zeroblob(1, 10) + eq("insert into todos (title,desc,deadline) values(zeroblob(10), NULL, NULL);", bind_s:expand()) + end) + end) + + kill(bind_s, bind_db) -- end of forth test suit + bind_db = conn() -- beginning of fifth test suit + eval(bind_db, [[create table todos(id integer primary key, title text, desc text, deadline integer);]]) + bind_s = stmt:parse(bind_db, "insert into todos (title,desc,deadline) values(:title, ?, :deadline);") + expected_stmt = + "insert into todos (title,desc,deadline) values('Fancy todo title', 'We are doing all the things', 2021.0);" + + describe(":nparam() ", function() + it('does as expected with "anonymous params" and "named params"', function() + eq(3, bind_s:nparam()) + end) + end) + + describe(":param(i) ", function() + it('does as expected with "anonymous params" and "named params"', function() + eq(":title", bind_s:param(1)) + eq("?", bind_s:param(2)) + eq(":deadline", bind_s:param(3)) + end) + end) + + describe(":params() ", function() + it('does as expected with "anonymous params" and "named params"', function() + eq({ ":title", "?", ":deadline" }, bind_s:params()) + end) + end) + + describe(":bind() ", function() + it('does as expected with "anonymous params" and "named params"', function() + bind_s:bind_clear() + bind_s:bind(1, "Fancy todo title") + bind_s:bind(2, "We are doing all the things") + bind_s:bind(3, 2021) + eq(expected_stmt, bind_s:expand()) + bind_s:bind_clear() + bind_s:bind { + ["title"] = "Fancy todo title", + "We are doing all the things", + ["deadline"] = 2021, + } + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_next() ", function() + it('does as expected with "anonymous params" and "named params"', function() + bind_s:bind_clear() + bind_s:bind_next "Fancy todo title" + bind_s:bind_next "We are doing all the things" + bind_s:bind_next(2021) + eq(expected_stmt, bind_s:expand()) + end) + end) + + describe(":bind_blob() ", function() + it('does as expected with "anonymous params" and "named params"', function() + bind_s:bind_clear() + bind_s:bind_blob(1, clib.get_new_blob_ptr(), 10) + end) + end) + + describe(":bind_zeroblob()", function() + it('does as expected with "anonymous params" and "named params"', function() + bind_s:bind_clear() + bind_s:bind_zeroblob(1, 10) + eq("insert into todos (title,desc,deadline) values(zeroblob(10), NULL, NULL);", bind_s:expand()) + end) + end) + + kill(bind_s, bind_db) -- end of fifth test suit + bind_db = conn() -- beginning of sixth test suit + eval(bind_db, [[create table todos(id integer primary key, title text, desc text, deadline integer);]]) + bind_s = stmt:parse(bind_db, "insert into todos (id,title,desc,deadline) values(:id, :title, :desc, :deadline);") + + describe("Combined test", function() + it("passes", function() + -- [ @conni should this block be abstracted in :bind(dict)? + for _, v in ipairs(expectedkv) do + bind_s:bind(v) + bind_s:step() + bind_s:reset() + bind_s:bind_clear() + end + -- ]]]]] + bind_s:finalize() + local select = stmt:parse(bind_db, "select * from todos") + eq(expectedkv, select:kvrows()) + kill(select, bind_db) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/auto/tbl_spec.lua b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/tbl_spec.lua new file mode 100644 index 0000000..95f277e --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/auto/tbl_spec.lua @@ -0,0 +1,985 @@ +local sql = require "sqlite.db" +local tbl = require "sqlite.tbl" +local luv = require "luv" +local eq = assert.are.same +local P = require "sqlite.parser" +local demo = { + { a = 1, b = "lsf", c = "de" }, + { a = 99, b = "sdj", c = "in" }, + { a = 32, b = "sbs", c = "en" }, + { a = 12, b = "srd", c = "fn" }, + { a = 35, b = "cba", c = "qa" }, + { a = 4, b = "pef", c = "ru" }, + { a = 5, b = "sam", c = "da" }, +} + +local dbpath = "/tmp/tbl_methods_test.sql" +luv.fs_unlink(dbpath) +local db = sql.new(dbpath) + +local seed = function() + db:open() + db:create("T", { a = "integer", b = "text", c = "text", ensure = true }) + db:insert("T", demo) + return db:tbl "T", db:tbl "N" +end + +local clean = function() + db:close() + luv.fs_unlink(dbpath) +end + +clean() +describe("sqlite.tbl", function() + local t1, t2 = seed() + + describe(":new", function() + it("create new object with sql.table methods.", function() + eq("table", type(t1)) + end) + it("registers table name in self.name.", function() + eq("T", t1.name) + end) + it("registers whether the table exists in self.tbl_exists.", function() + eq(true, t1.tbl_exists) + end) + it("registers whether the table has content in self.has_content.", function() + eq(true, t1.has_content, "should be false") + end) + it("doesn't fail if table isn't created yat.", function() + eq("table", type(t2)) + end) + + local opts = { + schema = { + id = "int", + name = "text", + }, + } + local detailed_schema = { + id = { + cid = 0, + primary = false, + required = false, + type = "INT", + }, + name = { + cid = 1, + primary = false, + required = false, + type = "TEXT", + }, + } + local new = db:tbl("newtbl", opts) + + it("initalizes db with schema", function() + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + local got_schema = new:schema() + for k, v in pairs(got_schema) do + v.type = string.upper(v.type) + got_schema[k] = v + end + + eq(detailed_schema, got_schema, "should be identical") + end) + it("should not rewrite schema.", function() + local new2 = db:tbl "newtbl" + + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + local got_schema = new2:schema() + for k, v in pairs(got_schema) do + v.type = string.upper(v.type) + got_schema[k] = v + end + + eq(detailed_schema, got_schema, "should be identical") + -- eq(detailed_schema, new2.tbl_schema, "should be identical") + -- local new3 = db:tbl("newtbl", { schema = { id = "string" } }) + -- eq(detailed_schema, new3:schema(), "should be identical") + -- eq(detailed_schema, new3.tbl_schema, "should be identical") + end) + end) + + describe(":schema", function() + it("returns schema if self.tbl exists", function() + -- NOTE: Hack to ensure the casing of types always match!! due to github actions + local got_schema = t1:schema() + for k, v in pairs(got_schema) do + v.type = string.upper(v.type) + got_schema[k] = v + end + + eq({ + a = { + cid = 0, + primary = false, + required = false, + type = "INTEGER", + }, + b = { + cid = 1, + primary = false, + required = false, + type = "TEXT", + }, + c = { + cid = 2, + primary = false, + required = false, + type = "TEXT", + }, + }, got_schema) + end) + it("returns empty table if schema doesn't exists", function() + eq({}, t2:schema()) + end) + it("creates new table with schema", function() + local schema = { + a = { + cid = 0, + primary = false, + required = false, + type = "TEXT", + }, + d = { + cid = 1, + primary = false, + required = false, + type = "TEXT", + }, + id = { + cid = 2, + primary = false, + required = false, + type = "INT", + }, + } + eq(true, t2:schema(schema), "Should return true") + eq(schema, t2:schema(), "should return the schema.") + eq(true, t2.tbl_exists, "should alter self.exists value") + end) + it("should drop and recreate the table if not schema.ensure", function() + local new = { + a = { + cid = 0, + primary = false, + required = false, + type = "TEXT", + }, + f = { + cid = 1, + primary = false, + required = false, + type = "TEXT", + }, + id = { + cid = 2, + primary = false, + required = false, + type = "INT", + }, + } + eq(true, t2:schema(new), "Should return true") + eq(new, t2:schema(), "should return the schema.") + eq(true, t2.tbl_exists, "should alter self.exists value") + end) + it("should skip looking at the schema if schema.ensure", function() + local old = t2:schema() + local new = { i = "int", aaa = "text", fff = "text", ensure = true } + eq(true, t2:schema(new), "Should return true") + eq(old, t2:schema(), "should return the schema.") + end) + end) + + describe(":drop/:exists", function() + it("should drop table", function() + eq(true, t2:drop(), "should return true") + eq(false, t2:exists(), "should not exists.") + end) + it("should return false if already dropped", function() + eq(false, t2:drop(), "should be false") + end) + end) + + describe(":get", function() + it("return everything.", function() + local res = t1:get() + eq(demo, res, "should be identical") + end) + + it("runs a query and returns results (where)", function() + local res = t1:get { where = { a = 1 } } + eq(demo[1], res[1], "should be identical") + end) + + it("runs a query and returns results (where & keys)", function() + local res = t1:get { keys = { "b", "c" }, where = { a = 1 } } + demo[1].a = nil + eq(demo[1], res[1], "should be identical") + demo[1].a = 1 + end) + + it("runs a query and returns results (keys & limit)", function() + local limit = 3 + local res = t1:get { keys = { "a" }, limit = limit } + eq(res[1].b, nil, "should not have the key") + eq(res[1].c, nil, "should not have the key") + eq(#res, limit, "they should be the same") + end) + + it("run a query and returns results when connection is closed.", function() + db:close() + local res = t1:get { where = { a = 99 } } + eq(demo[2], res[1], "should be identical") + end) + + it("the db should stay closed after doing the query.", function() + eq(true, t1.db.closed, "should update the state") + eq(true, db.closed, "should update the state") + db:open() + end) + end) + + it(":where", function() + it("runs a query and returns results (where) with kv pairs", function() + local res = t1:where { a = 1 } + eq(demo[1], res, "should be identical") + end) + end) + + describe(":each", function() + it("should work func", function() + local res = {} + eq( + true, + t1:each({ where = { a = { 12, 99, 32 } } }, function(row) + table.insert(res, row.a) + end), + "the number of rows" + ) + eq(res, { 99, 32, 12 }, "should be identical") -- this might fail at some point because of the ordering. + end) + it("supports function as first argument", function() + local res = {} + eq( + true, + t1:each(function(row) + table.insert(res, row.a) + end, { where = { a = { 12, 99, 32 } } }), + "the number of rows" + ) + eq(res, { 99, 32, 12 }, "should be identical") + end) + it("supports function as first argument with no query", function() + local res = {} + eq( + true, + t1:each(function(row) + table.insert(res, row.a) + end), + "the number of rows" + ) + eq(res, { 1, 99, 32, 12, 35, 4, 5 }, "should be identical") + end) + end) + + describe(":map", function() + it("should work func", function() + local res = t1:map({ where = { a = { 12, 99, 32 } } }, function(row) + return row.a + end) + eq(res, { 99, 32, 12 }, "should be identical") -- this might fail at some point because of the ordering. + end) + it("support function as first argument", function() + local res = t1:map(function(row) + return row.a + end, { where = { a = { 12, 99, 32 } } }) + eq(res, { 99, 32, 12 }, "should be identical") -- this might fail at some point because of the ordering. + end) + it("support function as first argument with no query", function() + local res = t1:map(function(row) + return row.a + end) + eq(res, { 1, 99, 32, 12, 35, 4, 5 }, "should be identical") -- this might fail at some point because of the ordering. + end) + end) + + describe(":sort", function() + it("transform function defaults to to first where key", function() + eq( + { + { a = 4, b = "pef", c = "ru" }, + { a = 5, b = "sam", c = "da" }, + { a = 35, b = "cba", c = "qa" }, + }, + t1:sort { + where = { a = { 35, 4, 5 } }, + }, + "should be identical" + ) + end) + + it("transform function can be a string with row key ", function() + eq({ + { a = 12, b = "srd", c = "fn" }, + { a = 32, b = "sbs", c = "en" }, + { a = 35, b = "cba", c = "qa" }, + }, t1:sort({ where = { a = { 32, 12, 35 } } }, "a")) + eq({ + { a = 32, b = "sbs", c = "en" }, + { a = 12, b = "srd", c = "fn" }, + { a = 35, b = "cba", c = "qa" }, + }, t1:sort({ where = { a = { 32, 12, 35 } } }, "c")) + eq({ + { a = 35, b = "cba", c = "qa" }, + { a = 32, b = "sbs", c = "en" }, + { a = 12, b = "srd", c = "fn" }, + }, t1:sort({ where = { a = { 32, 12, 35 } } }, "b")) + end) + + it("can use a custom comparison function", function() + local comp = function(a, b) + return a > b + end + eq({ + { a = 12, b = "srd", c = "fn" }, + { a = 32, b = "sbs", c = "en" }, + { a = 35, b = "cba", c = "qa" }, + }, t1:sort({ where = { a = { 32, 12, 35 } } }, "b", comp)) + end) + end) + + describe(":remove", function() + it("removes a single row", function() + eq(true, t1:remove { where = { a = 5 } }, "should remove") + eq(true, db:eval "select * from T where a = 5", "should return boolean, if no resluts") + end) + + it("removes a multiple rows", function() + eq(true, t1:remove { where = { a = { 35, 4 } } }, "should remove") + eq(true, db:eval "select * from T where a = 35 or a = 4", "should return boolean, if no resluts") + end) + it("removes a multiple rows with where key only", function() + eq(true, t1:remove { a = { 35, 4 } }, "should remove") + eq(true, db:eval "select * from T where a = 35 or a = 4", "should return boolean, if no resluts") + end) + end) + + clean() + seed() + + describe(":update", function() + it("update a single row", function() + eq(true, t1:update { where = { a = 4 }, values = { c = "a", b = "a" } }, "should be updated") + demo[6] = { a = 4, c = "a", b = "a" } + eq({ demo[6] }, db:eval "select * from T where a = 4", "should return boolean, if no resluts") + end) + + it("update a multiple rows", function() + eq( + true, + t1:update { + { + where = { a = 35 }, + values = { c = "a", b = "a" }, + }, + { + where = { a = 5 }, + values = { c = "a", b = "a" }, + }, + }, + "should be updated" + ) + demo[7] = { a = 5, c = "a", b = "a" } + demo[5] = { a = 35, c = "a", b = "a" } + eq({ demo[7] }, db:eval "select * from T where a = 5", "should return boolean, if no resluts") + eq({ demo[5] }, db:eval "select * from T where a = 35", "should return boolean, if no resluts") + end) + end) + + clean() + seed() + + describe(":replace", function() + it("replcaes table content with new content", function() + local new_rows = { + { a = 1, b = "lsf", c = "de" }, + { a = 2, b = "sdj", c = "in" }, + { a = 3, b = "sbs", c = "en" }, + } + eq(true, t1:replace(new_rows), "it should return true") + eq(new_rows, t1:get(), "it should have the new rows") + end) + end) + + clean() + seed() + + describe(":insert", function() + local ex_last_rowid = #demo + it("inserts a row", function() + local new_row = { a = 109, b = "0", c = "0" } + ex_last_rowid = ex_last_rowid + 1 + local last_rowid = t1:insert(new_row) + eq(ex_last_rowid, last_rowid, "it should return row_id " .. ex_last_rowid) + eq({ new_row }, db:eval "select * from T where a = 109", "it should have the new rows") + end) + + it("inserts a list of rows", function() + local new_rows = { + { a = 11, b = "lsf", c = "de" }, + { a = 22, b = "sdj", c = "in" }, + { a = 33, b = "sbs", c = "en" }, + } + ex_last_rowid = ex_last_rowid + #new_rows + local last_rowid = t1:insert(new_rows) + eq(ex_last_rowid, last_rowid, "it should return row_id " .. ex_last_rowid) + eq(new_rows, db:eval "select * from T where a = 11 or a = 22 or a = 33", "it should have the new rows") + end) + end) + + clean() + seed() + + describe(":count", function() + it("retunrs the current number of rows in a db", function() + eq(7, t1:count(), "the number of rows") + end) + end) + + clean() + describe("foreign key: ", function() + local db = sql:open(nil) + local artists, tracks + it("create demo", function() + artists = db:tbl("artists", { + id = { type = "integer", pk = true }, + name = "text", + }) + tracks = db:tbl("tracks", { + id = "integer", + name = "text", + artist = { + type = "integer", + reference = "artists.id", + }, + }) + end) + + -- it("should fail", function() + -- eq({}, tracks:schema()) + -- end) + + it("seed", function() + artists:insert { + { id = 1, name = "Dean Martin" }, + { id = 2, name = "Frank Sinatra" }, + } + + tracks:insert { + { id = 11, name = "That's Amore", artist = 1 }, + { id = 12, name = "Christmas Blues", artist = 1 }, + { id = 13, name = "My Way", artist = 2 }, + } + end) + + local function try(self, action, args) + return pcall(self[action], self, args) + end + + tracks.try = try + artists.try = try + + it("foreign_keys enabled", function() + eq(1, db:eval("pragma foreign_keys")[1].foreign_keys) + end) + + it("sqlite.org foreignkeys example no.1", function() + eq( + false, + tracks:try("insert", { id = 4, name = "Mr. Bojangles", artist = 3 }), + "fails when artist with id of 3 doesn't exists" + ) + eq( + true, + tracks:try("insert", { id = 4, name = "Mr. Bojangles" }), + "passes, since artist here in null and it is nullable in schema definition." + ) + eq( + false, + tracks:try("update", { values = { artist = 3 }, where = { name = "Mr. Bojangles" } }), + "fails on update as well." + ) + eq( + true, + artists:try("insert", { id = 3, name = "Sammy Davis Jr." }), + "pases as it normall would without foregin keys" + ) + eq( + true, + tracks:try("update", { values = { artist = 3 }, where = { name = "Mr. Bojangles" } }), + "passes, now that artist of id 3 is created" + ) + eq( + true, + tracks:try("insert", { id = 15, name = "Boogie Woogie", artist = 3 }), + "passes since artist of id 3 is created" + ) + eq( + false, + artists:try("remove", { where = { name = "Frank Sinatra" } }), + "fails due to existing rows refering to it." + ) + eq( + true, + tracks:try("remove", { where = { name = "My Way" } }), + "pases, so that the artist refered by this row can be deleted." + ) + eq( + true, + artists:try("remove", { where = { name = "Frank Sinatra" } }), + "passes since nothing referencing that artist is referencing it " + ) + eq( + false, + artists:try("update", { values = { id = 4 }, where = { name = "Dean Martin" } }), + "fails since there is a record referencing it " + ) + eq( + true, + tracks:try("remove", { name = { "That's Amore", "Christmas Blues" } }), + "passes, thus enabling the artist id above to be updated" + ) + eq( + true, + artists:try("update", { values = { id = 4 }, where = { name = "Dean Martin" } }), + "fails since there is a record referencing it " + ) + end) + + it("on_update & on_delete actions", function() + tracks:schema { + id = "integer", + name = "text", + artist = { + type = "integer", + reference = "artists.id", + on_update = "cascade", + on_delete = "cascade", + }, + } + tracks:insert { + { id = 11, name = "That's Amore", artist = 3 }, + { id = 12, name = "Christmas Blues", artist = 3 }, + { id = 13, name = "My Way", artist = 4 }, + { id = 14, name = "Return to Me", artist = 4 }, + } + eq( + true, + artists:try("update", { where = { name = "Dean Martin" }, set = { id = 100 } }), + "pases without issues since we have on_update cascade." + ) + eq( + true, + next(tracks:get { where = { artist = 100 } }) ~= nil, + "changes are reflect in tracks table. This means all row referencing Dean Martin get updated" + ) + eq( + true, + artists:try("remove", { where = { name = "Dean Martin" } }), + "pases without issues since we have on_delete cascade." + ) + eq(true, next(tracks:get { where = { artist = 100 } }) == nil, "shouldn't be any rows referencing Dean Martin") + end) + end) + + describe(":extend", function() + local t + it("missing db object", function() + t = tbl("tbl_name", { id = true, name = "text" }) + eq(false, pcall(t.insert, t, { name = "tami" }), "should fail early.") + t:set_db(db) + eq(true, pcall(t.insert, t, { name = "conni" }), "should work now we have a db object to operate against.") + eq({ { id = 1, name = "conni" } }, t:get(), "only insert conni") + end) + + it("with db object", function() + t = tbl("tansactions", { id = true, amount = "real" }, db) + eq(1, t:insert { amount = 20.2 }) + eq( + "20.2", + t:map(function(row) + return tostring(row.amount) + end)[1] + ) + end) + + it("overwrite functions and fallback to t.db", function() + t.get = function(_) + return math.floor(t:__get({ where = { id = 1 } })[1].amount) + end + eq(20, t:get()) + end) + + local some + + it("create a new table", function() + some = tbl.new("somename", { + name = "text", + id = true, + count = { + type = "integer", + required = true, + default = 0, + }, + }, db) + end) + + it("access function", function() + eq("function", type(some.get), "should we still have access for") + eq("function", type(some.__get), "should be accessible.") + eq("cdata", type(some.db.conn)) + end) + + it("custom function", function() + function some:insert_or_update(name) + eq("string", type(name), "name should be a string") + local entry = self:where { name = name } + if not entry then + self:insert { name = name } + else + eq("function", type(some.update)) + self:update { + where = { id = entry.id }, + values = { count = entry.count + 2 }, + } + end + end + + eq("function", type(some.insert_or_update), "should be registered as function") + some:insert_or_update "ff" + eq("ff", some:where({ name = "ff" }).name) + some:insert_or_update "ff" + eq(2, some:where({ name = "ff" }).count) + end) + end) + + describe(":auto_alter", function() + local fmt = string.format + local alter = function(case) + local schema, data = case.schema, case.data + local tname = case.tname and case.tname + or "A" .. string.char(math.random(97, 122)) .. string.char(math.random(97, 122)) + + local new_table_cmd = P.table_alter_key_defs(tname, schema.after, schema.before, true) + + local tbefore = db:tbl(tname, schema.before) + + eq("number", type(tbefore:insert(data.before)), "should insert without issues.") + + local ok, tafter = pcall(db.tbl, db, tname, schema.after) + + eq(true, ok, fmt('\n\n\n%s:\n\n "%s"\n\n', tafter, new_table_cmd)) + + eq( + data.after, + tafter:get(), + fmt( + '\n\n\nnot matching expected data shape\n "%s"\n\nTable Schema:%s', + new_table_cmd, + vim.inspect(tafter:schema()) + ) + ) + + return tafter + end + it("case: simple rename with idetnical number of keys", function() + local t = alter { + schema = { + before = { + id = { type = "integer" }, + name = { type = "text" }, + }, + after = { + id = { type = "integer", required = false }, + some_name = { type = "text" }, + }, + }, + ---------------- + data = { + before = { + { id = 1, name = "a" }, + { id = 2, name = "b" }, + { id = 3, name = "c" }, + }, + after = { + { id = 1, some_name = "a" }, + { id = 2, some_name = "b" }, + { id = 3, some_name = "c" }, + }, + }, + } + eq(true, t:drop()) + end) + + it("case: simple rename with idetnical number of keys with a key turned to be required", function() + local t = alter { + schema = { + before = { + id = { type = "integer" }, + name = { type = "text" }, + age = { "integer" }, + }, + after = { + id = { type = "integer", required = true }, + some_name = { type = "text" }, + age = { + type = "integer", + default = 20, + required = true, + }, + }, + }, + ---------------- + data = { + before = { + { id = 1, name = "a", age = 5 }, + { id = 2, name = "b", age = 6 }, + { id = 3, name = "c", age = 7 }, + { id = 4, name = "e" }, + }, + after = { + { id = 1, some_name = "a", age = 5 }, + { id = 2, some_name = "b", age = 6 }, + { id = 3, some_name = "c", age = 7 }, + { id = 4, some_name = "e", age = 20 }, + }, + }, + } + + eq(true, t:drop()) + end) + + it("case: more than one rename with idetnical number of keys", function() + local t = alter { + schema = { + before = { id = { type = "integer" }, name = { type = "text" }, age = { "integer" } }, + after = { + id = { type = "integer", required = true }, + some_name = { type = "text" }, + since = { + type = "integer", + default = 20, + required = true, + }, + }, + }, + ---------------- + data = { + before = { + { id = 1, name = "a", age = 5 }, + { id = 2, name = "b", age = 6 }, + { id = 3, name = "c", age = 7 }, + { id = 4, name = "e" }, + }, + after = { + { id = 1, some_name = "a", since = 5 }, + { id = 2, some_name = "b", since = 6 }, + { id = 3, some_name = "c", since = 7 }, + { id = 4, some_name = "e", since = 20 }, + }, + }, + } + + eq(true, t:drop()) + end) + + it("case: more than one rename with idetnical number of keys + default without required = true", function() + local t = alter { + schema = { + before = { id = { type = "integer" }, name = { type = "text" }, age = { "integer" } }, + after = { + id = { type = "integer", required = true }, + some_name = { type = "text" }, + since = { + type = "integer", + default = 20, + }, + }, + }, + ---------------- + data = { + before = { + { id = 1, name = "a", age = 5 }, + { id = 2, name = "b", age = 6 }, + { id = 3, name = "c", age = 7 }, + { id = 4, name = "e" }, + }, + after = { + { id = 1, some_name = "a", since = 5 }, + { id = 2, some_name = "b", since = 6 }, + { id = 3, some_name = "c", since = 7 }, + { id = 4, some_name = "e", since = 20 }, + }, + }, + } + + eq(true, t:drop()) + end) + + clean() + describe("case: foreign key:", function() + local artists = db:tbl("artists", { + id = true, + name = "text", + }) + + artists:insert { + { id = 1, name = "Dean Martin" }, + { id = 2, name = "Frank Sinatra" }, + { id = 3, name = "Sammy Davis Jr." }, + } + + local tracks + it("transforms to foregin key", function() + tracks = alter { + tname = "tracks", + schema = { + before = { + id = "integer", + name = "text", + artist = "integer", + }, + after = { + id = "integer", + name = "text", + artist = { + type = "integer", + reference = "artists.id", + -- on_update = "cascade", + -- on_delete = "cascade", + }, + }, + }, + ---------------- + data = { + before = { + { id = 11, name = "That's Amore", artist = 1 }, + { id = 12, name = "Christmas Blues", artist = 1 }, + { id = 13, name = "My Way", artist = 2 }, + }, + after = { + { id = 11, name = "That's Amore", artist = 1 }, + { id = 12, name = "Christmas Blues", artist = 1 }, + { id = 13, name = "My Way", artist = 2 }, + }, + }, + } + end) + + it("pass sqlite.org tests", function() + local function try(self, action, args) + return pcall(self[action], self, args) + end + + -- db:open() + + tracks.try = try + artists.try = try + + eq( + false, + tracks:try("insert", { id = 4, name = "Mr. Bojangles", artist = 5 }), + "fails when artist with id of 5 doesn't exists" + ) + eq( + true, + tracks:try("insert", { id = 4, name = "Mr. Bojangles" }), + "passes, since artist here in null and it is nullable in schema definition." + ) + eq( + false, + tracks:try("update", { values = { artist = 5 }, where = { name = "Mr. Bojangles" } }), + "fails on update as well." + ) + eq(true, artists:try("insert", { id = 4, name = "Sammy" }), "pases as it normall would without foregin keys") + eq( + true, + tracks:try("update", { values = { artist = 3 }, where = { name = "Mr. Bojangles" } }), + "passes, now that artist of id 3 is created" + ) + eq( + true, + tracks:try("insert", { id = 15, name = "Boogie Woogie", artist = 3 }), + "passes since artist of id 3 is created" + ) + eq( + false, + artists:try("remove", { where = { name = "Frank Sinatra" } }), + "fails due to existing rows refering to it." + ) + eq( + true, + tracks:try("remove", { where = { name = "My Way" } }), + "pases, so that the artist refered by this row can be deleted." + ) + eq( + true, + artists:try("remove", { where = { name = "Frank Sinatra" } }), + "passes since nothing referencing that artist is referencing it " + ) + eq( + false, + artists:try("update", { values = { id = 4 }, where = { name = "Dean Martin" } }), + "fails since there is a record referencing it " + ) + eq( + true, + tracks:try("remove", { name = { "That's Amore", "Christmas Blues" } }), + "passes, thus enabling the artist id above to be updated" + ) + + tracks = db:tbl("tracks", { + id = "integer", + name = "text", + artist = { + type = "integer", + reference = "artists.id", + on_update = "cascade", + on_delete = "cascade", + }, + }) + + tracks.try = try + artists.try = try + + tracks:insert { + { id = 11, name = "That's Amore", artist = 3 }, + { id = 12, name = "Christmas Blues", artist = 1 }, + { id = 13, name = "My Way", artist = 4 }, + { id = 14, name = "Return to Me", artist = 1 }, + } + + eq( + true, + artists:try("update", { where = { name = "Dean Martin" }, set = { id = 100 } }), + "pases without issues since we have on_update cascade." + ) + eq( + true, + next(tracks:get { where = { artist = 100 } }) ~= nil, + "all row referencing Dean Martin get updated .. got: " + .. vim.inspect(tracks:get {}) + .. vim.inspect(artists:get()) + ) + eq( + true, + artists:try("remove", { where = { name = "Dean Martin" } }), + "pases without issues since we have on_delete cascade." + ) + eq(true, next(tracks:get { where = { artist = 100 } }) == nil, "shouldn't be any rows referencing Dean Martin") + end) + end) + end) + clean() +end) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/lazy.lua b/etc/soft/nvim/+plugins/sqlite.lua/test/lazy.lua new file mode 100644 index 0000000..2094a70 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/lazy.lua @@ -0,0 +1,71 @@ +local bunch = require'plenary.benchmark' +R('sqlite') +local sqlite = require('sqlite') +local remove_db = function () + vim.loop.fs_unlink("/tmp/uri") +end + +bunch("Logical Component vs Full initialization", { + runs = 20, + fun = { + { + "Logical Component", + function () + ---@class __ManagerA:sqlite_db + ---@field projects sqlite_tbl + ---@field todos sqlite_tbl + local db = sqlite { + uri = "/tmp/uri", + projects = { + id = true, + title = "text", + objectives = "luatable", + }, + todos = { + id = true, + title = "text", + client = "integer", + status = "text", + completed = "boolean", + details = "text", + }, + clients = { name = "text" }, + opts = { + foreign_keys = true, + }, + lazy = true + } + remove_db() + end + }, + { + "Full Initialization", + function () + ---@class __ManagerB:sqlite_db + ---@field projects sqlite_tbl + ---@field todos sqlite_tbl + local db = sqlite { + uri = "/tmp/uri", + projects = { + id = true, + title = "text", + objectives = "luatable", + }, + todos = { + id = true, + title = "text", + client = "integer", + status = "text", + completed = "boolean", + details = "text", + }, + clients = { name = "text" }, + opts = { + foreign_keys = true, + }, + } + remove_db() + end + } + } +}) diff --git a/etc/soft/nvim/+plugins/sqlite.lua/test/minimal_init.vim b/etc/soft/nvim/+plugins/sqlite.lua/test/minimal_init.vim new file mode 100644 index 0000000..56ca396 --- /dev/null +++ b/etc/soft/nvim/+plugins/sqlite.lua/test/minimal_init.vim @@ -0,0 +1,5 @@ +set rtp+=. +set rtp+=../plenary.nvim/ +set rtp+=../tree-sitter-lua/ + +runtime! plugin/plenary.vim diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/LICENSE b/etc/soft/nvim/+plugins/symbols-outline.nvim/LICENSE new file mode 100644 index 0000000..270ccf7 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Simrat + +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. diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/README.md b/etc/soft/nvim/+plugins/symbols-outline.nvim/README.md new file mode 100644 index 0000000..5fdd369 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/README.md @@ -0,0 +1,153 @@ +# symbols-outline.nvim + +**A tree like view for symbols in Neovim using the Language Server Protocol. +Supports all your favourite languages.** + +![demo](https://github.com/simrat39/rust-tools-demos/raw/master/symbols-demo.gif) + +## Prerequisites + +- `neovim 0.7+` +- Properly configured Neovim LSP client + +## Installation + +Using `packer.nvim` + +```lua +use 'simrat39/symbols-outline.nvim' +``` + +## Setup + +Put the setup call in your init.lua or any lua file that is sourced. + +```lua +require("symbols-outline").setup() +``` + +## Configuration + +Pass a table to the setup call above with your configuration options. + +```lua +local opts = { + highlight_hovered_item = true, + show_guides = true, + auto_preview = false, + position = 'right', + relative_width = true, + width = 25, + auto_close = false, + show_numbers = false, + show_relative_numbers = false, + show_symbol_details = true, + preview_bg_highlight = 'Pmenu', + autofold_depth = nil, + auto_unfold_hover = true, + fold_markers = { '', '' }, + wrap = false, + keymaps = { -- These keymaps can be a string or a table for multiple keys + close = {"", "q"}, + goto_location = "", + focus_location = "o", + hover_symbol = "", + toggle_preview = "K", + rename_symbol = "r", + code_actions = "a", + fold = "h", + unfold = "l", + fold_all = "W", + unfold_all = "E", + fold_reset = "R", + }, + lsp_blacklist = {}, + symbol_blacklist = {}, + symbols = { + File = {icon = "", hl = "TSURI"}, + Module = {icon = "", hl = "TSNamespace"}, + Namespace = {icon = "", hl = "TSNamespace"}, + Package = {icon = "", hl = "TSNamespace"}, + Class = {icon = "𝓒", hl = "TSType"}, + Method = {icon = "ƒ", hl = "TSMethod"}, + Property = {icon = "", hl = "TSMethod"}, + Field = {icon = "", hl = "TSField"}, + Constructor = {icon = "", hl = "TSConstructor"}, + Enum = {icon = "ℰ", hl = "TSType"}, + Interface = {icon = "ﰮ", hl = "TSType"}, + Function = {icon = "", hl = "TSFunction"}, + Variable = {icon = "", hl = "TSConstant"}, + Constant = {icon = "", hl = "TSConstant"}, + String = {icon = "𝓐", hl = "TSString"}, + Number = {icon = "#", hl = "TSNumber"}, + Boolean = {icon = "⊨", hl = "TSBoolean"}, + Array = {icon = "", hl = "TSConstant"}, + Object = {icon = "⦿", hl = "TSType"}, + Key = {icon = "🔐", hl = "TSType"}, + Null = {icon = "NULL", hl = "TSType"}, + EnumMember = {icon = "", hl = "TSField"}, + Struct = {icon = "𝓢", hl = "TSType"}, + Event = {icon = "🗲", hl = "TSType"}, + Operator = {icon = "+", hl = "TSOperator"}, + TypeParameter = {icon = "𝙏", hl = "TSParameter"} + } +} +``` + +| Property | Description | Type | Default | +| ---------------------- | ------------------------------------------------------------------------------ | ------------------ | ------------------------ | +| highlight_hovered_item | Whether to highlight the currently hovered symbol (high cpu usage) | boolean | true | +| show_guides | Whether to show outline guides | boolean | true | +| position | Where to open the split window | 'right' or 'left' | 'right' | +| relative_width | Whether width of window is set relative to existing windows | boolean | true | +| width | Width of window (as a % or columns based on `relative_width`) | int | 25 | +| auto_close | Whether to automatically close the window after selection | boolean | false | +| auto_preview | Show a preview of the code on hover | boolean | false | +| show_numbers | Shows numbers with the outline | boolean | false | +| show_relative_numbers | Shows relative numbers with the outline | boolean | false | +| show_symbol_details | Shows extra details with the symbols (lsp dependent) | boolean | true | +| preview_bg_highlight | Background color of the preview window | string | Pmenu | +| winblend | Pseudo-transparency of the preview window | int | 0 | +| keymaps | Which keys do what | table (dictionary) | [here](#default-keymaps) | +| symbols | Icon and highlight config for symbol icons | table (dictionary) | scroll up | +| lsp_blacklist | Which lsp clients to ignore | table (array) | {} | +| symbol_blacklist | Which symbols to ignore ([possible values](./lua/symbols-outline/symbols.lua)) | table (array) | {} | +| autofold_depth | Depth past which nodes will be folded by default | int | nil | +| auto_unfold_hover | Automatically unfold hovered symbol | boolean | true | +| fold_markers | Markers to denote foldable symbol's status | table (array) | { '', '' } | +| wrap | Whether to wrap long lines, or let them flow off the window | boolean | false | + +## Commands + +| Command | Description | +| ---------------------- | ---------------------- | +| `:SymbolsOutline` | Toggle symbols outline | +| `:SymbolsOutlineOpen` | Open symbols outline | +| `:SymbolsOutlineClose` | Close symbols outline | + +## Default keymaps + +| Key | Action | +| ---------- | -------------------------------------------------- | +| Escape | Close outline | +| Enter | Go to symbol location in code | +| o | Go to symbol location in code without losing focus | +| Ctrl+Space | Hover current symbol | +| K | Toggles the current symbol preview | +| r | Rename symbol | +| a | Code actions | +| h | Unfold symbol | +| l | Fold symbol | +| W | Fold all symbols | +| E | Unfold all symbols | +| R | Reset all folding | +| ? | Show help message | + +## Highlights + +| Highlight | Purpose | +| ----------------------- | -------------------------------------- | +| FocusedSymbol | Highlight of the focused symbol | +| Pmenu | Highlight of the preview popup windows | +| SymbolsOutlineConnector | Highlight of the table connectors | +| Comment | Highlight of the info virtual text | diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/doc/symbols-outline.txt b/etc/soft/nvim/+plugins/symbols-outline.nvim/doc/symbols-outline.txt new file mode 100644 index 0000000..6fcc43e --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/doc/symbols-outline.txt @@ -0,0 +1,180 @@ +*symbols-outline.txt* For NVIM v0.5.0 Last change: 2022 August 29 + +============================================================================== +Table of Contents *symbols-outline-table-of-contents* + +1. symbols-outline.nvim |symbols-outline-symbols-outline.nvim| + - Prerequisites |symbols-outline-prerequisites| + - Installation |symbols-outline-installation| + - Setup |symbols-outline-setup| + - Configuration |symbols-outline-configuration| + - Commands |symbols-outline-commands| + - Default keymaps |symbols-outline-default-keymaps| + - Highlights |symbols-outline-highlights| + +============================================================================== +1. symbols-outline.nvim *symbols-outline-symbols-outline.nvim* + +**A tree like view for symbols in Neovim using the Language Server Protocol. +Supports all your favourite languages.** + +
+ +

demo

+
+ +PREREQUISITES *symbols-outline-prerequisites* + + +- `neovim 0.7+` +- Properly configured Neovim LSP client + + +INSTALLATION *symbols-outline-installation* + +Using `packer.nvim` + +> + use 'simrat39/symbols-outline.nvim' +< + + +SETUP *symbols-outline-setup* + +Put the setup call in your init.lua or any lua file that is sourced. + +> + require("symbols-outline").setup() +< + + +CONFIGURATION *symbols-outline-configuration* + +Pass a table to the setup call above with your configuration options. + +> + local opts = { + highlight_hovered_item = true, + show_guides = true, + auto_preview = false, + position = 'right', + relative_width = true, + width = 25, + auto_close = false, + show_numbers = false, + show_relative_numbers = false, + show_symbol_details = true, + preview_bg_highlight = 'Pmenu', + autofold_depth = nil, + auto_unfold_hover = true, + fold_markers = { '', '' }, + wrap = false, + keymaps = { -- These keymaps can be a string or a table for multiple keys + close = {"", "q"}, + goto_location = "", + focus_location = "o", + hover_symbol = "", + toggle_preview = "K", + rename_symbol = "r", + code_actions = "a", + fold = "h", + unfold = "l", + fold_all = "W", + unfold_all = "E", + fold_reset = "R", + }, + lsp_blacklist = {}, + symbol_blacklist = {}, + symbols = { + File = {icon = "", hl = "TSURI"}, + Module = {icon = "", hl = "TSNamespace"}, + Namespace = {icon = "", hl = "TSNamespace"}, + Package = {icon = "", hl = "TSNamespace"}, + Class = {icon = "𝓒", hl = "TSType"}, + Method = {icon = "ƒ", hl = "TSMethod"}, + Property = {icon = "", hl = "TSMethod"}, + Field = {icon = "", hl = "TSField"}, + Constructor = {icon = "", hl = "TSConstructor"}, + Enum = {icon = "ℰ", hl = "TSType"}, + Interface = {icon = "ﰮ", hl = "TSType"}, + Function = {icon = "", hl = "TSFunction"}, + Variable = {icon = "", hl = "TSConstant"}, + Constant = {icon = "", hl = "TSConstant"}, + String = {icon = "𝓐", hl = "TSString"}, + Number = {icon = "#", hl = "TSNumber"}, + Boolean = {icon = "⊨", hl = "TSBoolean"}, + Array = {icon = "", hl = "TSConstant"}, + Object = {icon = "⦿", hl = "TSType"}, + Key = {icon = "🔐", hl = "TSType"}, + Null = {icon = "NULL", hl = "TSType"}, + EnumMember = {icon = "", hl = "TSField"}, + Struct = {icon = "𝓢", hl = "TSType"}, + Event = {icon = "🗲", hl = "TSType"}, + Operator = {icon = "+", hl = "TSOperator"}, + TypeParameter = {icon = "𝙏", hl = "TSParameter"} + } + } +< + + +│ Property │ Description │ Type │ Default │ +│highlight_hovered_item│Whether to highlight the currently hovered symbol (high cpu usage) │boolean │true │ +│show_guides │Whether to show outline guides │boolean │true │ +│position │Where to open the split window │'right' or 'left' │'right' │ +│relative_width │Whether width of window is set relative to existing windows │boolean │true │ +│width │Width of window (as a % or columns based on relative_width) │int │25 │ +│auto_close │Whether to automatically close the window after selection │boolean │false │ +│auto_preview │Show a preview of the code on hover │boolean │false │ +│show_numbers │Shows numbers with the outline │boolean │false │ +│show_relative_numbers │Shows relative numbers with the outline │boolean │false │ +│show_symbol_details │Shows extra details with the symbols (lsp dependent) │boolean │true │ +│preview_bg_highlight │Background color of the preview window │string │Pmenu │ +│winblend │Pseudo-transparency of the preview window │int │0 │ +│keymaps │Which keys do what │table (dictionary)│|symbols-outline-here|│ +│symbols │Icon and highlight config for symbol icons │table (dictionary)│scroll up │ +│lsp_blacklist │Which lsp clients to ignore │table (array) │{} │ +│symbol_blacklist │Which symbols to ignore (possible values <./lua/symbols-outline/symbols.lua>)│table (array) │{} │ +│autofold_depth │Depth past which nodes will be folded by default │int │nil │ +│auto_unfold_hover │Automatically unfold hovered symbol │boolean │true │ +│fold_markers │Markers to denote foldable symbol’s status │table (array) │{ '', '' } │ +│wrap │Whether to wrap long lines, or let them flow off the window │boolean │false │ + + +COMMANDS *symbols-outline-commands* + +│ Command │ Description │ +│:SymbolsOutline │Toggle symbols outline│ +│:SymbolsOutlineOpen │Open symbols outline │ +│:SymbolsOutlineClose │Close symbols outline │ + + +DEFAULT KEYMAPS *symbols-outline-default-keymaps* + +│ Key │ Action │ +│Escape │Close outline │ +│Enter │Go to symbol location in code │ +│o │Go to symbol location in code without losing focus│ +│Ctrl+Space│Hover current symbol │ +│K │Toggles the current symbol preview │ +│r │Rename symbol │ +│a │Code actions │ +│h │Unfold symbol │ +│l │Fold symbol │ +│W │Fold all symbols │ +│E │Unfold all symbols │ +│R │Reset all folding │ +│? │Show help message │ + + +HIGHLIGHTS *symbols-outline-highlights* + +│ Highlight │ Purpose │ +│FocusedSymbol │Highlight of the focused symbol │ +│Pmenu │Highlight of the preview popup windows│ +│SymbolsOutlineConnector│Highlight of the table connectors │ +│Comment │Highlight of the info virtual text │ + + +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline.lua new file mode 100644 index 0000000..09a5823 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline.lua @@ -0,0 +1,332 @@ +local parser = require 'symbols-outline.parser' +local providers = require 'symbols-outline.providers.init' +local ui = require 'symbols-outline.ui' +local writer = require 'symbols-outline.writer' +local config = require 'symbols-outline.config' +local utils = require 'symbols-outline.utils.init' +local View = require 'symbols-outline.view' +local folding = require 'symbols-outline.folding' + +local M = {} + +local function setup_global_autocmd() + if + config.options.highlight_hovered_item or config.options.auto_unfold_hover + then + vim.api.nvim_create_autocmd('CursorHold', { + pattern = '*', + callback = function() + M._highlight_current_item(nil) + end, + }) + end + + vim.api.nvim_create_autocmd({ + 'InsertLeave', + 'WinEnter', + 'BufEnter', + 'BufWinEnter', + 'TabEnter', + 'BufWritePost', + }, { + pattern = '*', + callback = M._refresh, + }) + + vim.api.nvim_create_autocmd('WinEnter', { + pattern = '*', + callback = require('symbols-outline.preview').close, + }) +end + +local function setup_buffer_autocmd() + if config.options.auto_preview then + vim.api.nvim_create_autocmd('CursorHold', { + buffer = 0, + callback = require('symbols-outline.preview').show, + }) + else + vim.api.nvim_create_autocmd('CursorMoved', { + buffer = 0, + callback = require('symbols-outline.preview').close, + }) + end +end + +------------------------- +-- STATE +------------------------- +M.state = { + outline_items = {}, + flattened_outline_items = {}, + code_win = 0, +} + +local function wipe_state() + M.state = { outline_items = {}, flattened_outline_items = {}, code_win = 0 } +end + +local function _update_lines() + M.state.flattened_outline_items = parser.flatten(M.state.outline_items) + writer.parse_and_write(M.view.bufnr, M.state.flattened_outline_items) +end + +local function _merge_items(items) + utils.merge_items_rec( + { children = items }, + { children = M.state.outline_items } + ) +end + +local function __refresh() + if M.view:is_open() then + local function refresh_handler(response) + if response == nil or type(response) ~= 'table' then + return + end + + local items = parser.parse(response) + _merge_items(items) + + M.state.code_win = vim.api.nvim_get_current_win() + + _update_lines() + end + + providers.request_symbols(refresh_handler) + end +end + +M._refresh = utils.debounce(__refresh, 100) + +function M._current_node() + local current_line = vim.api.nvim_win_get_cursor(M.view.winnr)[1] + return M.state.flattened_outline_items[current_line] +end + +local function goto_location(change_focus) + local node = M._current_node() + vim.api.nvim_win_set_cursor( + M.state.code_win, + { node.line + 1, node.character } + ) + if change_focus then + vim.fn.win_gotoid(M.state.code_win) + end + if config.options.auto_close then + M.close_outline() + end +end + +function M._set_folded(folded, move_cursor, node_index) + local node = M.state.flattened_outline_items[node_index] or M._current_node() + local changed = (folded ~= folding.is_folded(node)) + + if folding.is_foldable(node) and changed then + node.folded = folded + + if move_cursor then + vim.api.nvim_win_set_cursor(M.view.winnr, { node_index, 0 }) + end + + _update_lines() + elseif node.parent then + local parent_node = + M.state.flattened_outline_items[node.parent.line_in_outline] + + if parent_node then + M._set_folded( + folded, + not parent_node.folded and folded, + parent_node.line_in_outline + ) + end + end +end + +function M._set_all_folded(folded, nodes) + nodes = nodes or M.state.outline_items + + for _, node in ipairs(nodes) do + node.folded = folded + if node.children then + M._set_all_folded(folded, node.children) + end + end + + _update_lines() +end + +function M._highlight_current_item(winnr) + local has_provider = providers.has_provider() + + local is_current_buffer_the_outline = M.view.bufnr + == vim.api.nvim_get_current_buf() + + local doesnt_have_outline_buf = not M.view.bufnr + + local should_exit = not has_provider + or doesnt_have_outline_buf + or is_current_buffer_the_outline + + -- Make a special case if we have a window number + -- Because we might use this to manually focus so we dont want to quit this + -- function + if winnr then + should_exit = false + end + + if should_exit then + return + end + + local win = winnr or vim.api.nvim_get_current_win() + + local hovered_line = vim.api.nvim_win_get_cursor(win)[1] - 1 + + local leaf_node = nil + + local cb = function(value) + value.hovered = nil + + if + value.line == hovered_line + or (hovered_line > value.range_start and hovered_line < value.range_end) + then + value.hovered = true + leaf_node = value + end + end + + utils.items_dfs(cb, M.state.outline_items) + + _update_lines() + + if leaf_node then + for index, node in ipairs(M.state.flattened_outline_items) do + if node == leaf_node then + vim.api.nvim_win_set_cursor(M.view.winnr, { index, 1 }) + break + end + end + end +end + +local function setup_keymaps(bufnr) + local map = function(...) + utils.nmap(bufnr, ...) + end + -- goto_location of symbol and focus that window + map(config.options.keymaps.goto_location, function() + goto_location(true) + end) + -- goto_location of symbol but stay in outline + map(config.options.keymaps.focus_location, function() + goto_location(false) + end) + -- hover symbol + map( + config.options.keymaps.hover_symbol, + require('symbols-outline.hover').show_hover + ) + -- preview symbol + map( + config.options.keymaps.toggle_preview, + require('symbols-outline.preview').toggle + ) + -- rename symbol + map( + config.options.keymaps.rename_symbol, + require('symbols-outline.rename').rename + ) + -- code actions + map( + config.options.keymaps.code_actions, + require('symbols-outline.code_action').show_code_actions + ) + -- show help + map( + config.options.keymaps.show_help, + require('symbols-outline.config').show_help + ) + -- close outline + map(config.options.keymaps.close, function() + M.view:close() + end) + -- fold selection + map(config.options.keymaps.fold, function() + M._set_folded(true) + end) + -- unfold selection + map(config.options.keymaps.unfold, function() + M._set_folded(false) + end) + -- fold all + map(config.options.keymaps.fold_all, function() + M._set_all_folded(true) + end) + -- unfold all + map(config.options.keymaps.unfold_all, function() + M._set_all_folded(false) + end) + -- fold reset + map(config.options.keymaps.fold_reset, function() + M._set_all_folded(nil) + end) +end + +local function handler(response) + if response == nil or type(response) ~= 'table' then + return + end + + M.state.code_win = vim.api.nvim_get_current_win() + + M.view:setup_view() + -- clear state when buffer is closed + vim.api.nvim_buf_attach(M.view.bufnr, false, { + on_detach = function(_, _) + wipe_state() + end, + }) + + setup_keymaps(M.view.bufnr) + setup_buffer_autocmd() + + local items = parser.parse(response) + + M.state.outline_items = items + M.state.flattened_outline_items = parser.flatten(items) + + writer.parse_and_write(M.view.bufnr, M.state.flattened_outline_items) + + M._highlight_current_item(M.state.code_win) +end + +function M.toggle_outline() + if M.view:is_open() then + M.close_outline() + else + M.open_outline() + end +end + +function M.open_outline() + if not M.view:is_open() then + providers.request_symbols(handler) + end +end + +function M.close_outline() + M.view:close() +end + +function M.setup(opts) + config.setup(opts) + ui.setup_highlights() + + M.view = View:new() + setup_global_autocmd() +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/code_action.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/code_action.lua new file mode 100644 index 0000000..36f8b69 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/code_action.lua @@ -0,0 +1,34 @@ +local vim = vim + +local main = require 'symbols-outline' + +local M = {} + +local function get_action_params(node, winnr) + local bufnr = vim.api.nvim_win_get_buf(winnr) + local fn = 'file://' .. vim.api.nvim_buf_get_name(bufnr) + + local pos = { line = node.line, character = node.character } + local diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr, node.line) + return { + textDocument = { uri = fn }, + range = { start = pos, ['end'] = pos }, + context = { diagnostics = diagnostics }, + bufnr = bufnr, + } +end + +function M.show_code_actions() + local current_line = vim.api.nvim_win_get_cursor(main.state.outline_win)[1] + local node = main.state.flattened_outline_items[current_line] + + local params = get_action_params(node, main.state.code_win) + vim.lsp.buf_request( + params.bufnr, + 'textDocument/codeAction', + params, + vim.lsp.handlers['textDocument/codeAction'] + ) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/config.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/config.lua new file mode 100644 index 0000000..a7cbe5a --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/config.lua @@ -0,0 +1,137 @@ +local vim = vim + +local M = {} + +M.defaults = { + highlight_hovered_item = true, + show_guides = true, + position = 'right', + border = 'single', + relative_width = true, + width = 25, + auto_close = false, + auto_preview = false, + show_numbers = false, + show_relative_numbers = false, + show_symbol_details = true, + preview_bg_highlight = 'Pmenu', + winblend = 0, + autofold_depth = nil, + auto_unfold_hover = true, + fold_markers = { '', '' }, + wrap = false, + keymaps = { -- These keymaps can be a string or a table for multiple keys + close = { '', 'q' }, + goto_location = '', + focus_location = 'o', + hover_symbol = '', + toggle_preview = 'K', + rename_symbol = 'r', + code_actions = 'a', + show_help = '?', + fold = 'h', + unfold = 'l', + fold_all = 'W', + unfold_all = 'E', + fold_reset = 'R', + }, + lsp_blacklist = {}, + symbol_blacklist = {}, + symbols = { + File = { icon = '', hl = 'TSURI' }, + Module = { icon = '', hl = 'TSNamespace' }, + Namespace = { icon = '', hl = 'TSNamespace' }, + Package = { icon = '', hl = 'TSNamespace' }, + Class = { icon = '𝓒', hl = 'TSType' }, + Method = { icon = 'ƒ', hl = 'TSMethod' }, + Property = { icon = '', hl = 'TSMethod' }, + Field = { icon = '', hl = 'TSField' }, + Constructor = { icon = '', hl = 'TSConstructor' }, + Enum = { icon = 'ℰ', hl = 'TSType' }, + Interface = { icon = 'ﰮ', hl = 'TSType' }, + Function = { icon = '', hl = 'TSFunction' }, + Variable = { icon = '', hl = 'TSConstant' }, + Constant = { icon = '', hl = 'TSConstant' }, + String = { icon = '𝓐', hl = 'TSString' }, + Number = { icon = '#', hl = 'TSNumber' }, + Boolean = { icon = '⊨', hl = 'TSBoolean' }, + Array = { icon = '', hl = 'TSConstant' }, + Object = { icon = '⦿', hl = 'TSType' }, + Key = { icon = '🔐', hl = 'TSType' }, + Null = { icon = 'NULL', hl = 'TSType' }, + EnumMember = { icon = '', hl = 'TSField' }, + Struct = { icon = '𝓢', hl = 'TSType' }, + Event = { icon = '🗲', hl = 'TSType' }, + Operator = { icon = '+', hl = 'TSOperator' }, + TypeParameter = { icon = '𝙏', hl = 'TSParameter' }, + Component = { icon = '', hl = 'TSFunction' }, + Fragment = { icon = '', hl = 'TSConstant' }, + }, +} + +M.options = {} + +function M.has_numbers() + return M.options.show_numbers or M.options.show_relative_numbers +end + +function M.get_position_navigation_direction() + if M.options.position == 'left' then + return 'h' + else + return 'l' + end +end + +function M.get_window_width() + if M.options.relative_width then + return math.ceil(vim.o.columns * (M.options.width / 100)) + else + return M.options.width + end +end + +function M.get_split_command() + if M.options.position == 'left' then + return 'topleft vs' + else + return 'botright vs' + end +end + +local function has_value(tab, val) + for _, value in ipairs(tab) do + if value == val then + return true + end + end + + return false +end + +function M.is_symbol_blacklisted(kind) + if kind == nil then + return false + end + return has_value(M.options.symbol_blacklist, kind) +end + +function M.is_client_blacklisted(client_id) + local client = vim.lsp.get_client_by_id(client_id) + if not client then + return false + end + return has_value(M.options.lsp_blacklist, client.name) +end + +function M.show_help() + print 'Current keymaps:' + print(vim.inspect(M.options.keymaps)) +end + +function M.setup(options) + vim.g.symbols_outline_loaded = 1 + M.options = vim.tbl_deep_extend('force', {}, M.defaults, options or {}) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/folding.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/folding.lua new file mode 100644 index 0000000..df1626c --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/folding.lua @@ -0,0 +1,27 @@ +local M = {} +local config = require 'symbols-outline.config' + +M.is_foldable = function(node) + return node.children and #node.children > 0 +end + +local get_default_folded = function(depth) + local fold_past = config.options.autofold_depth + if not fold_past then + return false + else + return depth >= fold_past + end +end + +M.is_folded = function(node) + if node.folded ~= nil then + return node.folded + elseif node.hovered and config.options.auto_unfold_hover then + return false + else + return get_default_folded(node.depth) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/hover.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/hover.lua new file mode 100644 index 0000000..cc75e81 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/hover.lua @@ -0,0 +1,47 @@ +local so = require 'symbols-outline' +local util = vim.lsp.util + +local M = {} + +local function get_hover_params(node, winnr) + local bufnr = vim.api.nvim_win_get_buf(winnr) + local fn = vim.uri_from_bufnr(bufnr) + + return { + textDocument = { uri = fn }, + position = { line = node.line, character = node.character }, + bufnr = bufnr, + } +end + +-- handler yoinked from the default implementation +function M.show_hover() + local current_line = vim.api.nvim_win_get_cursor(so.view.winnr)[1] + local node = so.state.flattened_outline_items[current_line] + + local hover_params = get_hover_params(node, so.state.code_win) + + vim.lsp.buf_request( + hover_params.bufnr, + 'textDocument/hover', + hover_params, + ---@diagnostic disable-next-line: param-type-mismatch + function(_, result, _, config) + if not (result and result.contents) then + -- return { 'No information available' } + return + end + local markdown_lines = util.convert_input_to_markdown_lines( + result.contents + ) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + -- return { 'No information available' } + return + end + return util.open_floating_preview(markdown_lines, 'markdown', config) + end + ) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/markdown.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/markdown.lua new file mode 100644 index 0000000..cd909fc --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/markdown.lua @@ -0,0 +1,68 @@ +local M = {} + +-- Parses markdown files and returns a table of SymbolInformation[] which is +-- used by the plugin to show the outline. +-- We do this because markdown does not have a LSP. +-- Note that the headings won't have any hierarchy (as of now). +---@return table +function M.handle_markdown() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local level_symbols = { { children = {} } } + local max_level = 1 + local is_inside_code_block = false + + for line, value in ipairs(lines) do + if string.find(value, '^```') then + is_inside_code_block = not is_inside_code_block + end + + header, title = string.match(value, '^(#+)%s+(.*)$') + if header and not is_inside_code_block then + depth = #header + 1 + + for i = depth - 1, 1, -1 do + if level_symbols[i] ~= nil then + parent = level_symbols[i].children + break + end + end + + for i = depth, max_level do + if level_symbols[i] ~= nil then + level_symbols[i].selectionRange['end'].line = line - 1 + level_symbols[i].range['end'].line = line - 1 + level_symbols[i] = nil + end + end + max_level = depth + + entry = { + kind = 13, + name = title, + selectionRange = { + start = { character = 1, line = line - 1 }, + ['end'] = { character = 1, line = line - 1 }, + }, + range = { + start = { character = 1, line = line - 1 }, + ['end'] = { character = 1, line = line - 1 }, + }, + children = {}, + } + + parent[#parent + 1] = entry + level_symbols[depth] = entry + end + end + + for i = 2, max_level do + if level_symbols[i] ~= nil then + level_symbols[i].selectionRange['end'].line = #lines + level_symbols[i].range['end'].line = #lines + end + end + + return { [1000000] = { result = level_symbols[1].children } } +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/parser.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/parser.lua new file mode 100644 index 0000000..fe2f8d1 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/parser.lua @@ -0,0 +1,270 @@ +local symbols = require 'symbols-outline.symbols' +local ui = require 'symbols-outline.ui' +local config = require 'symbols-outline.config' +local t_utils = require 'symbols-outline.utils.table' +local folding = require 'symbols-outline.folding' + +local M = {} + +---Parses result from LSP into a table of symbols +---@param result table The result from a language server. +---@param depth number? The current depth of the symbol in the hierarchy. +---@param hierarchy table? A table of booleans which tells if a symbols parent was the last in its group. +---@param parent table? A reference to the current symbol's parent in the function's recursion +---@return table +local function parse_result(result, depth, hierarchy, parent) + local ret = {} + + for index, value in pairs(result) do + if not config.is_symbol_blacklisted(symbols.kinds[value.kind]) then + -- the hierarchy is basically a table of booleans which tells whether + -- the parent was the last in its group or not + local hir = hierarchy or {} + -- how many parents this node has, 1 is the lowest value because its + -- easier to work it + local level = depth or 1 + -- whether this node is the last in its group + local isLast = index == #result + + -- support SymbolInformation[] + -- https://microsoft.github.io/language-server-protocol/specification#textDocument_documentSymbol + local selectionRange = value.selectionRange + if value.selectionRange == nil then + selectionRange = value.location.range + end + + local range = value.range + if value.range == nil then + range = value.location.range + end + + local node = { + deprecated = value.deprecated, + kind = value.kind, + icon = symbols.icon_from_kind(value.kind), + name = value.name or value.text, + detail = value.detail, + line = selectionRange.start.line, + character = selectionRange.start.character, + range_start = range.start.line, + range_end = range['end'].line, + depth = level, + isLast = isLast, + hierarchy = hir, + parent = parent, + } + + table.insert(ret, node) + + local children = nil + if value.children ~= nil then + -- copy by value because we dont want it messing with the hir table + local child_hir = t_utils.array_copy(hir) + table.insert(child_hir, isLast) + children = parse_result(value.children, level + 1, child_hir, node) + end + + node.children = children + end + end + return ret +end + +---Sorts the result from LSP by where the symbols start. +---@param result table Result containing symbols returned from textDocument/documentSymbol +---@return table +local function sort_result(result) + ---Returns the start location for a symbol, or nil if not found. + ---@param item table The symbol. + ---@return table|nil + local function get_range_start(item) + if item.location ~= nil then + return item.location.range.start + elseif item.range ~= nil then + return item.range.start + else + return nil + end + end + + table.sort(result, function(a, b) + local a_start = get_range_start(a) + local b_start = get_range_start(b) + + -- if they both are equal, a should be before b + if a_start == nil and b_start == nil then + return false + end + + -- those with no start go first + if a_start == nil then + return true + end + if b_start == nil then + return false + end + + -- first try to sort by line. If lines are equal, sort by character instead + if a_start.line ~= b_start.line then + return a_start.line < b_start.line + else + return a_start.character < b_start.character + end + end) + + return result +end + +---Parses the response from lsp request 'textDocument/documentSymbol' using buf_request_all +---@param response table The result from buf_request_all +---@return table outline items +function M.parse(response) + local all_results = {} + + -- flatten results to one giant table of symbols + for client_id, client_response in pairs(response) do + if config.is_client_blacklisted(client_id) then + print('skipping client ' .. client_id) + goto continue + end + + local result = client_response['result'] + if result == nil or type(result) ~= 'table' then + goto continue + end + + for _, value in pairs(result) do + table.insert(all_results, value) + end + + ::continue:: + end + + local sorted = sort_result(all_results) + + return parse_result(sorted, nil, nil) +end + +function M.flatten(outline_items, ret, depth) + depth = depth or 1 + ret = ret or {} + for _, value in ipairs(outline_items) do + table.insert(ret, value) + value.line_in_outline = #ret + if value.children ~= nil and not folding.is_folded(value) then + M.flatten(value.children, ret, depth + 1) + end + end + + -- if depth == 1 then + -- for index, value in ipairs(ret) do + -- value.line_in_outline = index + -- end + -- end + + return ret +end + +function M.get_lines(flattened_outline_items) + local lines = {} + local hl_info = {} + + for node_line, node in ipairs(flattened_outline_items) do + local depth = node.depth + local marker_space = (config.options.fold_markers and 1) or 0 + + local line = t_utils.str_to_table(string.rep(' ', depth + marker_space)) + local running_length = 1 + + local function add_guide_hl(from, to) + table.insert(hl_info, { + node_line, + from, + to, + 'SymbolsOutlineConnector', + }) + end + + for index, _ in ipairs(line) do + -- all items start with a space (or two) + if config.options.show_guides then + -- makes the guides + if index == 1 then + line[index] = ' ' + -- i f index is last, add a bottom marker if current item is last, + -- else add a middle marker + elseif index == #line then + -- add fold markers + if config.options.fold_markers and folding.is_foldable(node) then + if folding.is_folded(node) then + line[index] = config.options.fold_markers[1] + else + line[index] = config.options.fold_markers[2] + end + + add_guide_hl( + running_length, + running_length + vim.fn.strlen(line[index]) - 1 + ) + + -- the root level has no vertical markers + elseif depth > 1 then + if node.isLast then + line[index] = ui.markers.bottom + add_guide_hl( + running_length, + running_length + vim.fn.strlen(ui.markers.bottom) - 1 + ) + else + line[index] = ui.markers.middle + add_guide_hl( + running_length, + running_length + vim.fn.strlen(ui.markers.middle) - 1 + ) + end + end + -- else if the parent was not the last in its group, add a + -- vertical marker because there are items under us and we need + -- to point to those + elseif not node.hierarchy[index] and depth > 1 then + line[index + marker_space] = ui.markers.vertical + add_guide_hl( + running_length - 1 + 2 * marker_space, + running_length + + vim.fn.strlen(ui.markers.vertical) + - 1 + + 2 * marker_space + ) + end + end + + line[index] = line[index] .. ' ' + + running_length = running_length + vim.fn.strlen(line[index]) + end + + local final_prefix = line + + local string_prefix = t_utils.table_to_str(final_prefix) + + table.insert(lines, string_prefix .. node.icon .. ' ' .. node.name) + + local hl_start = #string_prefix + local hl_end = #string_prefix + #node.icon + local hl_type = config.options.symbols[symbols.kinds[node.kind]].hl + table.insert(hl_info, { node_line, hl_start, hl_end, hl_type }) + + node.prefix_length = #string_prefix + #node.icon + 1 + end + return lines, hl_info +end + +function M.get_details(flattened_outline_items) + local lines = {} + for _, value in ipairs(flattened_outline_items) do + table.insert(lines, value.detail or '') + end + return lines +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/preview.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/preview.lua new file mode 100644 index 0000000..92556c2 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/preview.lua @@ -0,0 +1,250 @@ +local so = require 'symbols-outline' +local config = require 'symbols-outline.config' + +local M = {} + +local state = { + preview_buf = nil, + preview_win = nil, + hover_buf = nil, + hover_win = nil, +} + +local function is_current_win_outline() + local curwin = vim.api.nvim_get_current_win() + return curwin == so.view.winnr +end + +local function has_code_win() + local isWinValid = vim.api.nvim_win_is_valid(so.state.code_win) + if not isWinValid then + return false + end + local bufnr = vim.api.nvim_win_get_buf(so.state.code_win) + local isBufValid = vim.api.nvim_buf_is_valid(bufnr) + return isBufValid +end + +local function get_offset() + local outline_winnr = so.view.winnr + local width = 53 + local height = 0 + + if config.has_numbers() then + width = width + 4 + end + + if config.options.position == 'right' then + width = 0 - width + else + width = vim.api.nvim_win_get_width(outline_winnr) + 1 + end + return { height, width } +end + +local function get_height() + local uis = vim.api.nvim_list_uis() + return math.ceil(uis[1].height / 3) +end + +local function get_hovered_node() + local hovered_line = vim.api.nvim_win_get_cursor(so.view.winnr)[1] + local node = so.state.outline_items[hovered_line] + return node +end + +local function update_preview(code_buf) + code_buf = code_buf or vim.api.nvim_win_get_buf(so.state.code_win) + + local node = get_hovered_node() + if not node then + return + end + local lines = vim.api.nvim_buf_get_lines(code_buf, 0, -1, false) + + if state.preview_buf ~= nil then + vim.api.nvim_buf_set_lines(state.preview_buf, 0, -1, 0, lines) + vim.api.nvim_win_set_cursor( + state.preview_win, + { node.line + 1, node.character } + ) + end +end + +local function setup_preview_buf() + local code_buf = vim.api.nvim_win_get_buf(so.state.code_win) + local ft = vim.api.nvim_buf_get_option(code_buf, 'filetype') + + local function treesitter_attach() + local ts_highlight = require 'nvim-treesitter.highlight' + + ts_highlight.attach(state.preview_buf, ft) + end + + -- user might not have tree sitter installed + pcall(treesitter_attach) + + vim.api.nvim_buf_set_option(state.preview_buf, 'syntax', ft) + vim.api.nvim_buf_set_option(state.preview_buf, 'bufhidden', 'delete') + vim.api.nvim_win_set_option(state.preview_win, 'cursorline', true) + update_preview(code_buf) +end + +local function get_hover_params(node, winnr) + local bufnr = vim.api.nvim_win_get_buf(winnr) + local uri = vim.uri_from_bufnr(bufnr) + + return { + textDocument = { uri = uri }, + position = { line = node.line, character = node.character }, + bufnr = bufnr, + } +end + +local function update_hover() + if not has_code_win() then + return + end + + local node = get_hovered_node() + if not node then + return + end + + local provider = _G._symbols_outline_current_provider + local params = get_hover_params(node, so.state.code_win) + + provider.hover_info(params.bufnr, params, function(err, result) + if err then + print(vim.inspect(err)) + end + local markdown_lines = {} + if result ~= nil then + markdown_lines = vim.lsp.util.convert_input_to_markdown_lines( + result.contents + ) + end + markdown_lines = vim.lsp.util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + markdown_lines = { '###No info available!' } + end + + markdown_lines = vim.lsp.util.stylize_markdown( + state.hover_buf, + markdown_lines, + {} + ) + + if state.hover_buf ~= nil then + vim.api.nvim_buf_set_lines(state.hover_buf, 0, -1, 0, markdown_lines) + end + end) +end + +local function setup_hover_buf() + if not has_code_win() then + return + end + local code_buf = vim.api.nvim_win_get_buf(so.state.code_win) + local ft = vim.api.nvim_buf_get_option(code_buf, 'filetype') + vim.api.nvim_buf_set_option(state.hover_buf, 'syntax', ft) + vim.api.nvim_buf_set_option(state.hover_buf, 'bufhidden', 'delete') + vim.api.nvim_win_set_option(state.hover_win, 'wrap', true) + vim.api.nvim_win_set_option(state.hover_win, 'cursorline', false) + update_hover() +end + +local function set_bg_hl() + local winhi = 'Normal:' .. config.options.preview_bg_highlight + vim.api.nvim_win_set_option(state.preview_win, 'winhighlight', winhi) + vim.api.nvim_win_set_option(state.hover_win, 'winhighlight', winhi) + local winblend = config.options.winblend + vim.api.nvim_win_set_option(state.preview_win, 'winblend', winblend) + vim.api.nvim_win_set_option(state.hover_win, 'winblend', winblend) +end + +local function show_preview() + if state.preview_win == nil and state.preview_buf == nil then + state.preview_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_attach(state.preview_buf, false, { + on_detach = function() + state.preview_buf = nil + state.preview_win = nil + end, + }) + local offsets = get_offset() + state.preview_win = vim.api.nvim_open_win(state.preview_buf, false, { + relative = 'win', + width = 50, + height = get_height(), + bufpos = { 0, 0 }, + row = offsets[1], + col = offsets[2], + border = config.options.border, + }) + setup_preview_buf() + else + update_preview() + end +end + +local function show_hover() + if state.hover_win == nil and state.hover_buf == nil then + state.hover_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_attach(state.hover_buf, false, { + on_detach = function() + state.hover_buf = nil + state.hover_win = nil + end, + }) + local offsets = get_offset() + local height = get_height() + state.hover_win = vim.api.nvim_open_win(state.hover_buf, false, { + relative = 'win', + width = 50, + height = height, + bufpos = { 0, 0 }, + row = offsets[1] + height + 2, + col = offsets[2], + border = config.options.border, + }) + setup_hover_buf() + else + update_hover() + end +end + +function M.show() + if not is_current_win_outline() or #vim.api.nvim_list_wins() < 2 then + return + end + + show_preview() + show_hover() + set_bg_hl() +end + +function M.close() + if has_code_win() then + if + state.preview_win ~= nil and vim.api.nvim_win_is_valid(state.preview_win) + then + vim.api.nvim_win_close(state.preview_win, true) + end + if + state.hover_win ~= nil and vim.api.nvim_win_is_valid(state.hover_win) + then + vim.api.nvim_win_close(state.hover_win, true) + end + end +end + +function M.toggle() + if state.preview_win ~= nil then + M.close() + else + M.show() + end +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/coc.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/coc.lua new file mode 100644 index 0000000..0b3e11f --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/coc.lua @@ -0,0 +1,95 @@ +local M = {} + +function M.should_use_provider(_) + local not_coc_installed = vim.fn.exists '*CocActionAsync' == 0 + local not_coc_service_initialized = vim.g.coc_service_initialized == 0 + + if not_coc_installed or not_coc_service_initialized then + return + end + + local coc_attached = vim.fn.call('CocAction', { 'ensureDocument' }) + local has_symbols = vim.fn.call('CocHasProvider', { 'documentSymbol' }) + + return coc_attached and has_symbols +end + +function M.hover_info(_, _, on_info) + on_info(nil, { + contents = { + kind = 'markdown', + contents = { 'No extra information availaible!' }, + }, + }) +end + +---@param result table +local function convert_symbols(result) + local s = {} + local kinds_index = {} + -- create a inverse indexing of symbols.kind + local symbols = require("symbols-outline.symbols") + for k, v in pairs(symbols.kinds) do + kinds_index[v] = k + end + -- rebuild coc.nvim symbol list hierarchy according to the 'level' key + for _, value in pairs(result) do + value.children = {} + value.kind = kinds_index[value.kind] + if #s == 0 then + table.insert(s, value) + goto continue + end + if value.level == s[#s].level then + if value.level == 0 then + table.insert(s, value) + goto continue + end + local tmp = s[#s] + table.remove(s) + table.insert(s[#s].children, tmp) + table.insert(s, value) + elseif value.level == s[#s].level + 1 then + table.insert(s[#s].children, value) + elseif value.level == s[#s].level + 2 then + local tmp = s[#s].children[#(s[#s].children)] + table.remove(s[#s].children) + table.insert(s, tmp) + table.insert(s[#s].children, value) + elseif value.level < s[#s].level then + while value.level < s[#s].level do + local tmp = s[#s] + table.remove(s) + table.insert(s[#s].children, tmp) + end + if s[#s].level ~= 0 then + local tmp = s[#s] + table.remove(s) + table.insert(s[#s].children, tmp) + table.insert(s, value) + else + table.insert(s, value) + end + end + ::continue:: + end + local top = s[#s] + while top.level ~= 0 do + table.remove(s) + table.insert(s[#s].children, top) + top = s[#s] + end + return s +end + +---@param on_symbols function +function M.request_symbols(on_symbols) + vim.fn.call('CocActionAsync', { + 'documentSymbols', + function(_, symbols) + on_symbols { [1000000] = { result = convert_symbols(symbols) } } + end, + }) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/init.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/init.lua new file mode 100644 index 0000000..f3228e5 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/init.lua @@ -0,0 +1,36 @@ +local M = {} + +local providers = { + 'symbols-outline/providers/jsx', + 'symbols-outline/providers/nvim-lsp', + 'symbols-outline/providers/coc', + 'symbols-outline/providers/markdown', +} + +_G._symbols_outline_current_provider = nil + +function M.has_provider() + local ret = false + for _, value in ipairs(providers) do + local provider = require(value) + if provider.should_use_provider(0) then + ret = true + break + end + end + return ret +end + +---@param on_symbols function +function M.request_symbols(on_symbols) + for _, value in ipairs(providers) do + local provider = require(value) + if provider.should_use_provider(0) then + _G._symbols_outline_current_provider = provider + provider.request_symbols(on_symbols) + break + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/jsx.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/jsx.lua new file mode 100644 index 0000000..3944b24 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/jsx.lua @@ -0,0 +1,144 @@ +local M = {} + +local SYMBOL_COMPONENT = 27 +local SYMBOL_FRAGMENT = 28 + +function M.should_use_provider(bufnr) + local ft = vim.api.nvim_buf_get_option(bufnr, 'ft') + local has_ts, parsers = pcall(require, 'nvim-treesitter.parsers') + local _, has_parser = pcall(function() + if has_ts then + return parsers.get_parser(bufnr) ~= nil + end + + return false + end) + + return has_ts + and has_parser + and ( + string.match(ft, 'typescriptreact') + or string.match(ft, 'javascriptreact') + ) +end + +function M.hover_info(_, _, on_info) + on_info(nil, { + contents = { + kind = 'nvim-lsp-jsx', + contents = { 'No extra information availaible!' }, + }, + }) +end + +local function get_open_tag(node) + if node:type() == 'jsx_element' then + for _, outer in ipairs(node:field 'open_tag') do + if outer:type() == 'jsx_opening_element' then + return outer + end + end + end + + return nil +end + +local function jsx_node_detail(node, buf) + node = get_open_tag(node) or node + + local param_nodes = node:field 'attribute' + if #param_nodes == 0 then + return nil + end + + local res = '{ ' + .. table.concat( + vim.tbl_map(function(el) + local a, b, c, d = el:range() + local text = vim.api.nvim_buf_get_text(buf, a, b, c, d, {}) + return text[1] + end, param_nodes), + ' ' + ) + .. ' }' + + return res +end + +local function jsx_node_tagname(node, buf) + local tagnode = get_open_tag(node) or node + + local identifier = nil + + for _, val in ipairs(tagnode:field 'name') do + if val:type() == 'identifier' then + identifier = val + end + end + + if identifier then + local a, b, c, d = identifier:range() + local text = vim.api.nvim_buf_get_text(buf, a, b, c, d, {}) + local name = table.concat(text) + return name + end +end + +local function convert_ts(child, children, bufnr) + local is_frag = (child:type() == 'jsx_fragment') + + local a, b, c, d = child:range() + local range = { + start = { line = a, character = b }, + ['end'] = { line = c, character = d }, + } + + local converted = { + name = (not is_frag and (jsx_node_tagname(child, bufnr) or '')) + or 'fragment', + children = (#children > 0 and children) or nil, + kind = (is_frag and SYMBOL_FRAGMENT) or SYMBOL_COMPONENT, + detail = jsx_node_detail(child, bufnr), + range = range, + selectionRange = range, + } + + return converted +end + +local function parse_ts(root, children, bufnr) + children = children or {} + + for child in root:iter_children() do + if + vim.tbl_contains( + { 'jsx_element', 'jsx_self_closing_element' }, + child:type() + ) + then + local new_children = {} + + parse_ts(child, new_children, bufnr) + + table.insert(children, convert_ts(child, new_children, bufnr)) + else + parse_ts(child, children, bufnr) + end + end + + return children +end + +function M.request_symbols(on_symbols) + local parsers = require 'nvim-treesitter.parsers' + local bufnr = 0 + + local parser = parsers.get_parser(bufnr) + local root = parser:parse()[1]:root() + + local symbols = parse_ts(root, nil, bufnr) + -- local symbols = convert_ts(ctree) + on_symbols { [1000000] = { result = symbols } } +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/markdown.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/markdown.lua new file mode 100644 index 0000000..b1ffba3 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/markdown.lua @@ -0,0 +1,24 @@ +local md_parser = require 'symbols-outline.markdown' + +local M = {} + +-- probably change this +function M.should_use_provider(bufnr) + return string.match(vim.api.nvim_buf_get_option(bufnr, 'ft'), 'markdown') +end + +function M.hover_info(_, _, on_info) + on_info(nil, { + contents = { + kind = 'markdown', + contents = { 'No extra information availaible!' }, + }, + }) +end + +---@param on_symbols function +function M.request_symbols(on_symbols) + on_symbols(md_parser.handle_markdown()) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/nvim-lsp.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/nvim-lsp.lua new file mode 100644 index 0000000..a067872 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/providers/nvim-lsp.lua @@ -0,0 +1,67 @@ +local config = require 'symbols-outline.config' + +local M = {} + +local function getParams() + return { textDocument = vim.lsp.util.make_text_document_params() } +end + +function M.hover_info(bufnr, params, on_info) + local clients = vim.lsp.buf_get_clients(bufnr) + local used_client + + for id, client in pairs(clients) do + if config.is_client_blacklisted(id) then + goto continue + else + if client.server_capabilities.hoverProvider then + used_client = client + break + end + end + ::continue:: + end + + if not used_client then + on_info(nil, { + contents = { + kind = 'markdown', + content = { 'No extra information availaible!' }, + }, + }) + end + + used_client.request('textDocument/hover', params, on_info, bufnr) +end + +-- probably change this +function M.should_use_provider(bufnr) + local clients = vim.lsp.buf_get_clients(bufnr) + local ret = false + + for id, client in pairs(clients) do + if config.is_client_blacklisted(id) then + goto continue + else + if client.server_capabilities.documentSymbolProvider then + ret = true + break + end + end + ::continue:: + end + + return ret +end + +---@param on_symbols function +function M.request_symbols(on_symbols) + vim.lsp.buf_request_all( + 0, + 'textDocument/documentSymbol', + getParams(), + on_symbols + ) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/rename.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/rename.lua new file mode 100644 index 0000000..b9d413d --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/rename.lua @@ -0,0 +1,42 @@ +local so = require 'symbols-outline' + +local M = {} + +local function get_rename_params(node, winnr) + local bufnr = vim.api.nvim_win_get_buf(winnr) + local fn = 'file://' .. vim.api.nvim_buf_get_name(bufnr) + + return { + textDocument = { uri = fn }, + position = { line = node.line, character = node.character }, + bufnr = bufnr, + } +end + +function M.rename() + local current_line = vim.api.nvim_win_get_cursor(so.view.winnr)[1] + local node = so.state.flattened_outline_items[current_line] + + local params = get_rename_params(node, so.state.code_win) + + local new_name = vim.fn.input('New Name: ', node.name) + if not new_name or new_name == '' or new_name == node.name then + return + end + + params.newName = new_name + + vim.lsp.buf_request( + params.bufnr, + 'textDocument/rename', + params, + function(_, result, ctx) + if result ~= nil then + local client = vim.lsp.get_client_by_id(ctx.client_id) + vim.lsp.util.apply_workspace_edit(result, client.offset_encoding) + end + end + ) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/symbols.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/symbols.lua new file mode 100644 index 0000000..9778ce8 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/symbols.lua @@ -0,0 +1,50 @@ +local config = require 'symbols-outline.config' + +local M = {} + +M.kinds = { + 'File', + 'Module', + 'Namespace', + 'Package', + 'Class', + 'Method', + 'Property', + 'Field', + 'Constructor', + 'Enum', + 'Interface', + 'Function', + 'Variable', + 'Constant', + 'String', + 'Number', + 'Boolean', + 'Array', + 'Object', + 'Key', + 'Null', + 'EnumMember', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter', + 'Component', + 'Fragment', +} + +function M.icon_from_kind(kind) + local symbols = config.options.symbols + + if type(kind) == 'string' then + return symbols[kind].icon + end + + -- If the kind is higher than the available ones then default to 'Object' + if kind > #M.kinds then + kind = 19 + end + return symbols[M.kinds[kind]].icon +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/ui.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/ui.lua new file mode 100644 index 0000000..325f901 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/ui.lua @@ -0,0 +1,58 @@ +local M = {} + +M.markers = { + bottom = '└', + middle = '├', + vertical = '│', + horizontal = '─', +} + +M.hovered_hl_ns = vim.api.nvim_create_namespace 'hovered_item' + +function M.clear_hover_highlight(bufnr) + vim.api.nvim_buf_clear_namespace(bufnr, M.hovered_hl_ns, 0, -1) +end + +function M.add_hover_highlight(bufnr, line, col_start) + vim.api.nvim_buf_add_highlight( + bufnr, + M.hovered_hl_ns, + 'FocusedSymbol', + line, + col_start, + -1 + ) +end + +function M.setup_highlights() + -- Setup the FocusedSymbol highlight group if it hasn't been done already by + -- a theme or manually set + if vim.fn.hlexists 'FocusedSymbol' == 0 then + local cline_hl = vim.api.nvim_get_hl_by_name('CursorLine', true) + local string_hl = vim.api.nvim_get_hl_by_name('String', true) + + vim.api.nvim_set_hl( + 0, + 'FocusedSymbol', + { bg = cline_hl.background, fg = string_hl.foreground } + ) + end + + -- Some colorschemes do some funky things with the comment highlight, most + -- notably making them italic, which messes up the outline connector. Fix + -- this by copying the foreground color from the comment hl into a new + -- highlight. + local comment_fg_gui = vim.fn.synIDattr( + vim.fn.synIDtrans(vim.fn.hlID 'Comment'), + 'fg', + 'gui' + ) + + if vim.fn.hlexists 'SymbolsOutlineConnector' == 0 then + vim.cmd( + string.format('hi SymbolsOutlineConnector guifg=%s', comment_fg_gui) + ) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/init.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/init.lua new file mode 100644 index 0000000..1b00738 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/init.lua @@ -0,0 +1,113 @@ +local M = {} + +---maps the table|string of keys to the action +---@param keys table|string +---@param action function|string +function M.nmap(bufnr, keys, action) + if type(keys) == 'string' then + keys = { keys } + end + + for _, lhs in ipairs(keys) do + vim.keymap.set( + 'n', + lhs, + action, + { silent = true, noremap = true, buffer = bufnr } + ) + end +end + +--- @param f function +--- @param delay number +--- @return function +function M.debounce(f, delay) + local timer = vim.loop.new_timer() + + return function(...) + local args = { ... } + + timer:start( + delay, + 0, + vim.schedule_wrap(function() + timer:stop() + f(unpack(args)) + end) + ) + end +end + +function M.items_dfs(callback, children) + for _, val in ipairs(children) do + callback(val) + + if val.children then + M.items_dfs(callback, val.children) + end + end +end + +---Merges a symbol tree recursively, only replacing nodes +---which have changed. This will maintain the folding +---status of any unchanged nodes. +---@param new_node table New node +---@param old_node table Old node +---@param index? number Index of old_item in parent +---@param parent? table Parent of old_item +M.merge_items_rec = function(new_node, old_node, index, parent) + local failed = false + + if not new_node or not old_node then + failed = true + else + for key, _ in pairs(new_node) do + if + vim.tbl_contains({ + 'parent', + 'children', + 'folded', + 'hovered', + 'line_in_outline', + 'hierarchy', + }, key) + then + goto continue + end + + if key == 'name' then + -- in the case of a rename, just rename the existing node + old_node['name'] = new_node['name'] + else + if not vim.deep_equal(new_node[key], old_node[key]) then + failed = true + break + end + end + + ::continue:: + end + end + + if failed then + if parent and index then + parent[index] = new_node + end + else + local next_new_item = new_node.children or {} + + -- in case new children are created on a node which + -- previously had no children + if #next_new_item > 0 and not old_node.children then + old_node.children = {} + end + + local next_old_item = old_node.children or {} + + for i = 1, math.max(#next_new_item, #next_old_item) do + M.merge_items_rec(next_new_item[i], next_old_item[i], i, next_old_item) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/lsp_utils.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/lsp_utils.lua new file mode 100644 index 0000000..a0fe87c --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/lsp_utils.lua @@ -0,0 +1,12 @@ +local M = {} + +function M.is_buf_attached_to_lsp(bufnr) + local clients = vim.lsp.buf_get_clients(bufnr or 0) + return clients ~= nil and #clients > 0 +end + +function M.is_buf_markdown(bufnr) + return vim.api.nvim_buf_get_option(bufnr, 'ft') == 'markdown' +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/table.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/table.lua new file mode 100644 index 0000000..93247d1 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/utils/table.lua @@ -0,0 +1,31 @@ +local M = {} + +function M.table_to_str(t) + local ret = '' + for _, value in ipairs(t) do + ret = ret .. tostring(value) + end + return ret +end + +function M.str_to_table(str) + local t = {} + for i = 1, #str do + t[i] = str:sub(i, i) + end + return t +end + +--- Copies an array and returns it because lua usually does references +---@generic T +---@param t T[] +---@return T[] +function M.array_copy(t) + local ret = {} + for _, value in ipairs(t) do + table.insert(ret, value) + end + return ret +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/view.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/view.lua new file mode 100644 index 0000000..3ffd0f6 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/view.lua @@ -0,0 +1,65 @@ +local config = require 'symbols-outline.config' + +local View = {} + +function View:new() + return setmetatable({ bufnr = nil, winnr = nil }, { __index = View }) +end + +---creates the outline window and sets it up +function View:setup_view() + -- create a scratch unlisted buffer + self.bufnr = vim.api.nvim_create_buf(false, true) + + -- delete buffer when window is closed / buffer is hidden + vim.api.nvim_buf_set_option(self.bufnr, 'bufhidden', 'delete') + -- create a split + vim.cmd(config.get_split_command()) + -- resize to a % of the current window size + vim.cmd('vertical resize ' .. config.get_window_width()) + + -- get current (outline) window and attach our buffer to it + self.winnr = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(self.winnr, self.bufnr) + + -- window stuff + vim.api.nvim_win_set_option(self.winnr, 'number', false) + vim.api.nvim_win_set_option(self.winnr, 'relativenumber', false) + vim.api.nvim_win_set_option(self.winnr, 'winfixwidth', true) + vim.api.nvim_win_set_option(self.winnr, 'list', false) + vim.api.nvim_win_set_option(self.winnr, 'wrap', config.options.wrap) + vim.api.nvim_win_set_option(self.winnr, 'linebreak', true) -- only has effect when wrap=true + vim.api.nvim_win_set_option(self.winnr, 'breakindent', true) -- only has effect when wrap=true + -- Would be nice to use ui.markers.vertical as part of showbreak to keep + -- continuity of the tree UI, but there's currently no way to style the + -- color, apart from globally overriding hl-NonText, which will potentially + -- mess with other theme/user settings. So just use empty spaces for now. + vim.api.nvim_win_set_option(self.winnr, 'showbreak', ' ') -- only has effect when wrap=true. + -- buffer stuff + vim.api.nvim_buf_set_name(self.bufnr, 'OUTLINE') + vim.api.nvim_buf_set_option(self.bufnr, 'filetype', 'Outline') + vim.api.nvim_buf_set_option(self.bufnr, 'modifiable', false) + + if config.options.show_numbers or config.options.show_relative_numbers then + vim.api.nvim_win_set_option(self.winnr, 'nu', true) + end + + if config.options.show_relative_numbers then + vim.api.nvim_win_set_option(self.winnr, 'rnu', true) + end +end + +function View:close() + vim.api.nvim_win_close(self.winnr, true) + self.winnr = nil + self.bufnr = nil +end + +function View:is_open() + return self.winnr + and self.bufnr + and vim.api.nvim_buf_is_valid(self.bufnr) + and vim.api.nvim_win_is_valid(self.winnr) +end + +return View diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/writer.lua b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/writer.lua new file mode 100644 index 0000000..e119940 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/lua/symbols-outline/writer.lua @@ -0,0 +1,100 @@ +local parser = require 'symbols-outline.parser' +local config = require 'symbols-outline.config' +local ui = require 'symbols-outline.ui' + +local M = {} + +local function is_buffer_outline(bufnr) + local isValid = vim.api.nvim_buf_is_valid(bufnr) + local name = vim.api.nvim_buf_get_name(bufnr) + local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype') + return string.match(name, 'OUTLINE') ~= nil and ft == 'Outline' and isValid +end + +local hlns = vim.api.nvim_create_namespace 'symbols-outline-icon-highlight' + +function M.write_outline(bufnr, lines) + if not is_buffer_outline(bufnr) then + return + end + vim.api.nvim_buf_set_option(bufnr, 'modifiable', true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + vim.api.nvim_buf_set_option(bufnr, 'modifiable', false) +end + +function M.add_highlights(bufnr, hl_info, nodes) + for _, line_hl in ipairs(hl_info) do + local line, hl_start, hl_end, hl_type = unpack(line_hl) + vim.api.nvim_buf_add_highlight( + bufnr, + hlns, + hl_type, + line - 1, + hl_start, + hl_end + ) + end + + M.add_hover_highlights(bufnr, nodes) +end + +local ns = vim.api.nvim_create_namespace 'symbols-outline-virt-text' + +function M.write_details(bufnr, lines) + if not is_buffer_outline(bufnr) then + return + end + if not config.options.show_symbol_details then + return + end + + for index, value in ipairs(lines) do + vim.api.nvim_buf_set_extmark(bufnr, ns, index - 1, -1, { + virt_text = { { value, 'Comment' } }, + virt_text_pos = 'eol', + hl_mode = 'combine', + }) + end +end + +local function clear_virt_text(bufnr) + vim.api.nvim_buf_clear_namespace(bufnr, -1, 0, -1) +end + +M.add_hover_highlights = function(bufnr, nodes) + if not config.options.highlight_hovered_item then + return + end + + -- clear old highlight + ui.clear_hover_highlight(bufnr) + for _, node in ipairs(nodes) do + if not node.hovered then + goto continue + end + + local marker_fac = (config.options.fold_markers and 1) or 0 + if node.prefix_length then + ui.add_hover_highlight( + bufnr, + node.line_in_outline - 1, + node.prefix_length + ) + end + ::continue:: + end +end + +-- runs the whole writing routine where the text is cleared, new data is parsed +-- and then written +function M.parse_and_write(bufnr, flattened_outline_items) + local lines, hl_info = parser.get_lines(flattened_outline_items) + M.write_outline(bufnr, lines) + + clear_virt_text(bufnr) + local details = parser.get_details(flattened_outline_items) + M.add_highlights(bufnr, hl_info, flattened_outline_items) + M.write_details(bufnr, details) +end + +return M diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/plugin/symbols-outline.vim b/etc/soft/nvim/+plugins/symbols-outline.nvim/plugin/symbols-outline.vim new file mode 100644 index 0000000..be0f1b9 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/plugin/symbols-outline.vim @@ -0,0 +1,3 @@ +command! SymbolsOutline :lua require'symbols-outline'.toggle_outline() +command! SymbolsOutlineOpen :lua require'symbols-outline'.open_outline() +command! SymbolsOutlineClose :lua require'symbols-outline'.close_outline() diff --git a/etc/soft/nvim/+plugins/symbols-outline.nvim/stylua.toml b/etc/soft/nvim/+plugins/symbols-outline.nvim/stylua.toml new file mode 100644 index 0000000..63ea177 --- /dev/null +++ b/etc/soft/nvim/+plugins/symbols-outline.nvim/stylua.toml @@ -0,0 +1,6 @@ +column_width = 80 +line_endings = 'Unix' +indent_type = 'Spaces' +indent_width = 2 +quote_style = 'AutoPreferSingle' +call_parentheses = 'None' diff --git a/etc/soft/nvim/+plugins/telescope-coc.nvim/LICENSE b/etc/soft/nvim/+plugins/telescope-coc.nvim/LICENSE new file mode 100644 index 0000000..2f3d317 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope-coc.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Heyward Fann + +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. diff --git a/etc/soft/nvim/+plugins/telescope-coc.nvim/README.md b/etc/soft/nvim/+plugins/telescope-coc.nvim/README.md new file mode 100644 index 0000000..1b54458 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope-coc.nvim/README.md @@ -0,0 +1,59 @@ +# telescope-coc.nvim + + + +GitHub Sponsors +Patreon donate button +PayPal donate button + +An extension for [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) +that allows you to find/filter/preview/pick results from [coc.nvim](https://github.com/neoclide/coc.nvim). + + + + +## Get Started + +```viml +Plug 'nvim-telescope/telescope.nvim' +Plug 'fannheyward/telescope-coc.nvim' + +lua << EOF +require("telescope").setup({ + extensions = { + coc = { + theme = 'ivy', + prefer_locations = true, -- always use Telescope locations to preview definitions/declarations/implementations etc + } + }, +}) +require('telescope').load_extension('coc') +EOF +``` + +## Usage + +`:Telescope coc` to get subcommands + +`:Telescope coc X` + +- `mru` +- `links` +- `commands` +- `locations` +- `references` +- `definitions` +- `declarations` +- `implementations` +- `type_definitions` +- `diagnostics` +- `code_actions` +- `line_code_actions` +- `file_code_actions` +- `document_symbols` +- `workspace_symbols` +- `workspace_diagnostics` + +## License + +MIT diff --git a/etc/soft/nvim/+plugins/telescope-coc.nvim/lua/telescope/_extensions/coc.lua b/etc/soft/nvim/+plugins/telescope-coc.nvim/lua/telescope/_extensions/coc.lua new file mode 100644 index 0000000..4b60296 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope-coc.nvim/lua/telescope/_extensions/coc.lua @@ -0,0 +1,678 @@ +local actions = require('telescope.actions') +local action_state = require('telescope.actions.state') +local conf = require('telescope.config').values +local finders = require('telescope.finders') +local make_entry = require('telescope.make_entry') +local pickers = require('telescope.pickers') +local entry_display = require('telescope.pickers.entry_display') +local utils = require('telescope.utils') +local Path = require('plenary.path') + +local string = string +---@diagnostic disable-next-line: undefined-global +local vim = vim +---@diagnostic disable-next-line: undefined-global +local jit = jit + +local F = vim.F +local fn = vim.fn +local api = vim.api +local CocAction = fn.CocAction +local CocActionAsync = fn.CocActionAsync + +local config = {} + +local function is_ready(feature) + if vim.g.coc_service_initialized ~= 1 then + print('Coc is not ready!') + return + end + + if feature and not fn.CocHasProvider(feature) then + print('Coc: server does not support ' .. feature) + return + end + + return true +end + +local locations_to_items = function(locs) + if not locs then + return + end + local items = {} + for _, l in ipairs(locs) do + if l.targetUri and l.targetRange then + -- LocationLink + l.uri = l.targetUri + l.range = l.targetRange + end + local bufnr = vim.uri_to_bufnr(l.uri) + vim.fn.bufload(bufnr) + local filename = vim.uri_to_fname(l.uri) + local row = l.range.start.line + local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] + items[#items + 1] = { + filename = filename, + lnum = row + 1, + col = l.range.start.character + 1, + text = line, + } + end + + return items +end + +local mru = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready() then + return + end + + local home = vim.call('coc#util#get_data_home') + local data = Path:new(home .. Path.path.sep .. 'mru'):read() + if not data or #data == 0 then + return + end + + local results = {} + local exists = {} + local cwd = vim.loop.cwd() .. Path.path.sep + for _, val in ipairs(utils.max_split(data, '\n')) do + local p = Path:new(val) + local lowerPrefix = val:sub(1, #cwd):gsub(Path.path.sep, ''):lower() + local lowerCWD = cwd:gsub(Path.path.sep, ''):lower() + if lowerCWD == lowerPrefix and p:exists() and p:is_file() + and exists[val:sub(#cwd + 1)] == nil then + exists[val:sub(#cwd + 1)] = true + results[#results + 1] = val:sub(#cwd + 1) + end + end + + local make_display = function(text) + local display, hl_group = utils.transform_devicons(text, text) + + if hl_group then + return display, { { { 1, 3 }, hl_group } } + else + return display + end + end + + pickers.new(opts, { + prompt_title = 'Coc MRU', + sorter = conf.generic_sorter(opts), + previewer = conf.qflist_previewer(opts), + finder = finders.new_table({ + results = results, + entry_maker = function(line) + return { + valid = line ~= nil, + value = line, + ordinal = line, + display = make_display(line), + } + end, + }), + }):find() +end + +local links = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready('documentLink') then + return + end + + local res = CocAction('links') + if type(res) ~= 'table' then + return + end + + if vim.tbl_isempty(res) then + print('No links available') + return + end + + local results = {} + for _, l in ipairs(res) do + results[#results + 1] = { + lnum = l.range.start.line + 1, + col = l.range.start.character, + text = l.target, + } + if l.target:find('file://') then + results[#results].filename = vim.uri_to_fname(l.target) + end + end + + pickers.new(opts, { + prompt_title = 'Coc Document Links', + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + local text = selection.text + if text:find('https?://') then + local opener = (jit.os == 'OSX' and 'open') or 'xdg-open' + os.execute(opener .. ' ' .. text) + else + vim.fn.execute('edit ' .. selection.filename) + end + end) + + return true + end, + }):find() +end + +local handle_code_actions = function(opts, mode) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready('codeAction') then + return + end + + local results = CocAction('codeActions', mode) + if type(results) ~= 'table' then + return + end + + if vim.tbl_isempty(results) then + print('No available code actions') + return + end + + for i, x in ipairs(results) do + x.idx = i + end + + pickers.new(opts, { + prompt_title = 'Coc Code Actions', + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = function(line) + return { + valid = line ~= nil, + value = line, + ordinal = line.idx .. line.title, + display = line.idx .. ': ' .. line.title, + } + end, + }), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + CocAction('doCodeAction', selection.value) + end) + + return true + end, + }):find() +end + +-- TODO +-- range code action +local cursor_code_actions = function(opts) + handle_code_actions(opts, 'cursor') +end + +local line_code_actions = function(opts) + handle_code_actions(opts, 'line') +end + +local file_code_actions = function(opts) + handle_code_actions(opts, nil) +end + +local function list_or_jump(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready(opts.coc_provider) then + return + end + + local defs = CocAction(opts.coc_action) + if type(defs) ~= 'table' then + return + end + + if vim.tbl_isempty(defs) then + print(('No %s found'):format(opts.coc_action)) + elseif #defs == 1 and not config.prefer_locations then + CocActionAsync('runCommand', 'workspace.openLocation', nil, defs[1]) + else + local results = locations_to_items(defs) + if not results then + return + end + pickers.new(opts, { + prompt_title = opts.coc_title, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }), + }):find() + end +end + +local definitions = function(opts) + opts.coc_provider = 'definition' + opts.coc_action = 'definitions' + opts.coc_title = 'Coc Definitions' + list_or_jump(opts) +end + +local declarations = function(opts) + opts.coc_provider = 'declaration' + opts.coc_action = 'declarations' + opts.coc_title = 'Coc Declarations' + list_or_jump(opts) +end + +local implementations = function(opts) + opts.coc_provider = 'implementation' + opts.coc_action = 'implementations' + opts.coc_title = 'Coc Implementations' + list_or_jump(opts) +end + +local type_definitions = function(opts) + opts.coc_provider = 'typeDefinition' + opts.coc_action = 'typeDefinitions' + opts.coc_title = 'Coc TypeDefinitions' + list_or_jump(opts) +end + +local references = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready('reference') then + return + end + + local excludeDeclaration = opts.excludeDeclaration or false + local refs = CocAction('references', excludeDeclaration) + if type(refs) ~= 'table' or vim.tbl_isempty(refs) then + return + end + + local results = locations_to_items(refs) + if not results then + return + end + + local displayer = entry_display.create({ + separator = '▏', + items = { + { width = 6 }, + { width = 40 }, + { remaining = true }, + }, + }) + + local make_display = function(entry) + local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' } + local filename = utils.transform_path(opts, entry.filename) + + return displayer({ + line_info, + filename, + entry.text:gsub('.* | ', ''), + }) + end + + pickers.new(opts, { + prompt_title = 'Coc References', + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = function(entry) + return { + valid = true, + + value = entry, + ordinal = (not opts.ignore_filename and entry.filename or '') .. ' ' .. entry.text, + display = make_display, + + filename = entry.filename, + lnum = entry.lnum, + col = entry.col, + text = entry.text, + } + end, + }), + }):find() +end + +local references_used = function(opts) + opts.excludeDeclaration = true + references(opts) +end + +local locations = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + local refs = vim.g.coc_jump_locations + local results = locations_to_items(refs) + if not results then + return + end + pickers.new(opts, { + prompt_title = 'Coc Locations', + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }), + }):find() +end + +local document_symbols = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready('documentSymbol') then + return + end + + local current_buf = api.nvim_get_current_buf() + local symbols = CocAction('documentSymbols', current_buf) + if type(symbols) ~= 'table' or vim.tbl_isempty(symbols) then + return + end + + local results = {} + for _, s in ipairs(symbols) do + results[#results + 1] = { + filename = api.nvim_buf_get_name(current_buf), + lnum = s.lnum, + col = s.col, + kind = s.kind, + text = string.format('[%s] %s', s.kind, s.text), + } + end + + opts.ignore_filename = opts.ignore_filename or true + pickers.new(opts, { + prompt_title = 'Coc Document Symbols', + previewer = conf.qflist_previewer(opts), + finder = finders.new_table({ + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts), + }), + sorter = conf.prefilter_sorter({ + tag = 'symbol_type', + sorter = conf.generic_sorter(opts), + }), + }):find() +end + +local function get_workspace_symbols_requester() + return function(prompt) + local results = {} + local symbols = CocAction('getWorkspaceSymbols', prompt) + if type(symbols) ~= 'table' or vim.tbl_isempty(symbols) then + return results + end + for _, s in ipairs(symbols) do + local filename = vim.uri_to_fname(s.location.uri) + local kind = vim.lsp.protocol.SymbolKind[s.kind] or 'Unknown' + results[#results + 1] = { + filename = filename, + lnum = s.location.range.start.line + 1, + col = s.location.range.start.character + 1, + kind = kind, + text = string.format('[%s] %s', kind, s.name), + } + end + return results + end +end + +local workspace_symbols = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + pickers.new(opts, { + prompt_title = 'Coc Workspace Symbols', + finder = finders.new_dynamic({ + entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts), + fn = get_workspace_symbols_requester(), + }), + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(), + }):find() +end + +local diagnostics = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready() then + return + end + + local diagnosticsList = CocAction('diagnosticList') + if type(diagnosticsList) ~= 'table' or vim.tbl_isempty(diagnosticsList) then + return + end + + opts = opts or {} + local results = {} + local buf_names = {} + local current_buf = api.nvim_get_current_buf() + local current_filename = api.nvim_buf_get_name(current_buf) + if opts.get_all then + local bufs = api.nvim_list_bufs() + for _, bn in ipairs(bufs) do + buf_names[api.nvim_buf_get_name(bn)] = bn + end + end + for _, d in ipairs(diagnosticsList) do + if d.severity == 'Information' then + d.severity = 'Info' + elseif d.severity == 'Warning' then + d.severity = 'Warn' + end + if opts.get_all or (d.file == current_filename) then + results[#results + 1] = { + bufnr = buf_names[d.file] or current_buf, + filename = d.file, + lnum = d.lnum, + col = d.col, + start = d.location.range.start, + finish = d.location.range['end'], + text = vim.trim(d.message:gsub('[\n]', '')), + type = d.severity:upper(), + } + end + end + + opts.path_display = F.if_nil(opts.path_display, {'hidden'}) + pickers.new(opts, { + prompt_title = 'Coc Diagnostics', + previewer = conf.qflist_previewer(opts), + finder = finders.new_table({ + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_diagnostics(opts), + }), + sorter = conf.prefilter_sorter({ + tag = 'type', + sorter = conf.generic_sorter(opts), + }), + }):find() +end + +local workspace_diagnostics = function(opts) + opts = F.if_nil(opts, {}) + opts.path_display = F.if_nil(opts.path_display, {'shorten'}) + opts.prompt_title = 'Coc Workspace Diagnostics' + opts.get_all = true + diagnostics(opts) +end + +local commands = function(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + if not is_ready() then + return + end + + local cmds = CocAction('commands') + if type(cmds) ~= 'table' or vim.tbl_isempty(cmds) then + print('No commands available') + return + end + + local results = {} + + local ok, history = pcall(function() + return vim.split((Path:new(vim.fn['coc#util#get_data_home'](), 'commands'):read()), '\n') + end) + if ok then + local id2title = {} + for _, cmd in ipairs(cmds) do + id2title[cmd.id] = cmd.title + end + + local exists = {} + for _, id in ipairs(history) do + if not exists[id] then + local title = id2title[id] + if title then + table.insert(results, { id = id, title = title }) + exists[id] = true + end + end + end + + for _, cmd in pairs(cmds) do + if not exists[cmd.id] then + table.insert(results, cmd) + exists[cmd.id] = true + end + end + else + results = cmds + end + + local displayer = entry_display.create({ + separator = ' ', + items = { + { width = 40 }, + { remaining = true }, + }, + }) + local make_display = function(entry) + return displayer({ + { entry.value, 'TelescopeResultsFunction' }, + entry.description, + }) + end + + pickers.new(opts, { + prompt_title = 'Coc Commands', + sorter = conf.generic_sorter(opts), + finder = finders.new_table({ + results = results, + entry_maker = function(line) + return { + value = line.id, + valid = line.id ~= nil, + ordinal = line.id, + display = make_display, + description = line.title or '', + } + end, + }), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + actions.close(prompt_bufnr) + CocActionAsync('runCommand', selection.value) + end) + return true + end, + }):find() +end + +local function subcommands(opts) + if config.theme then + opts = vim.tbl_deep_extend("force", opts or {}, config.theme) + end + local cmds = require('telescope.command').get_extensions_subcommand().coc + cmds = vim.tbl_filter(function(v) + return v ~= 'coc' + end, cmds) + + pickers.new(opts, { + prompt_title = 'Telescope Coc', + finder = finders.new_table({ + results = cmds, + }), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + actions.close(prompt_bufnr) + vim.defer_fn(function() + require('telescope').extensions.coc[selection.value](opts) + end, 20) + end) + return true + end, + }):find() +end + +return require('telescope').register_extension({ + setup = function(ext_config) + if ext_config.theme then + config.theme = require('telescope.themes')['get_' .. ext_config.theme]() + end + if ext_config.prefer_locations then + config.prefer_locations = true + end + end, + exports = { + coc = subcommands, + mru = mru, + links = links, + commands = commands, + locations = locations, + references = references, + references_used = references_used, + diagnostics = diagnostics, + definitions = definitions, + declarations = declarations, + implementations = implementations, + type_definitions = type_definitions, + code_actions = cursor_code_actions, + line_code_actions = line_code_actions, + file_code_actions = file_code_actions, + document_symbols = document_symbols, + workspace_symbols = workspace_symbols, + workspace_diagnostics = workspace_diagnostics, + }, +}) + +-- vim: set sw=2 ts=2 sts=2 et diff --git a/etc/soft/nvim/+plugins/telescope-coc.nvim/stylua.toml b/etc/soft/nvim/+plugins/telescope-coc.nvim/stylua.toml new file mode 100644 index 0000000..69a69a3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope-coc.nvim/stylua.toml @@ -0,0 +1,5 @@ +column_width = 100 +indent_type = "Spaces" +quote_style = "AutoPreferSingle" +indent_width = 2 +# no_call_parentheses = true diff --git a/etc/soft/nvim/+plugins/telescope.nvim/CONTRIBUTING.md b/etc/soft/nvim/+plugins/telescope.nvim/CONTRIBUTING.md new file mode 100644 index 0000000..b335e2c --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing + +## Submitting a new feature + +Thanks for taking the time to submit code to Telescope if you're reading this! We love having new contributors and love seeing the Neovim community come around this plugin and keep making it better If you are submitting a new PR with a feature addition, please make sure that you add the appropriate documentation. For examples of how to document a new picker for instance, check the `lua/telescope/builtin/init.lua` file to see how we write function headers for all of the pickers there. To learn how we go about writing documentation for this project, keep reading below! + +## Documentation with treesitter + +We are generating docs based on the tree sitter syntax tree. TJ wrote a grammar that includes the documentation in this syntax tree so we can do take this function header documentation and transform it into vim documentation. All documentation that is part of the returning module will be exported. For example: + +```lua +local m = {} + +--- Test Header +--@return 1: Returns always 1 +function m.a() -- or m:a() + return 1 +end + +--- Documentation +function m.__b() -- or m:__b() + return 2 +end + +--- Documentation +local c = function() + return 2 +end + +return m +``` + +This will export function `a` with header documentation and the return value. Module function `b` and local function `c` will not be exported. + +For a more in-depth look at how to write documentation take a look at this guide: [how to](https://github.com/tjdevries/tree-sitter-lua/blob/master/HOWTO.md) +This guide contains all annotations and we will update it when we add new annotations. + +## What is missing? + +The docgen has some problems on which people can work. This would happen in [https://github.com/tjdevries/tree-sitter-lua](https://github.com/tjdevries/tree-sitter-lua) and documentation of some modules here. +I would suggest we are documenting lua/telescope/builtin/init.lua rather than the files itself. We can use that init.lua file as "header" file, so we are not cluttering the other files. +How to help out with documentation: + +## Auto-updates from CI + +The easy way would be: + +- write some docs +- commit, push and create draft PR +- wait a minute until the CI generates a new commit with the changes +- Look at this commit and the changes +- Modify documentation until its perfect. You can do `git commit --amend` and `git push --force` to remove the github ci commit again + +## Generate on your local machine + +The other option would be setting up https://github.com/tjdevries/tree-sitter-lua + +- Install Treesitter, either with package manager or with github release +- Install plugin as usual +- cd to plugin +- `mkdir -p build parser` sadly those don't exist +- `make build_parser` +- `ln -s ../build/parser.so parser/lua.so` We need the shared object in parser/ so it gets picked up by neovim. Either copy or symbolic link +- Make sure that nvim-treesitter lua parser is not installed and also delete the lua queries in that repository. `queries/lua/*`. If you are not doing that you will have a bad time! +- cd into this project +- Write doc +- Run `make docgen` +- Repeat last two steps diff --git a/etc/soft/nvim/+plugins/telescope.nvim/LICENSE b/etc/soft/nvim/+plugins/telescope.nvim/LICENSE new file mode 100644 index 0000000..9117664 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2021 nvim-telescope + +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. diff --git a/etc/soft/nvim/+plugins/telescope.nvim/Makefile b/etc/soft/nvim/+plugins/telescope.nvim/Makefile new file mode 100644 index 0000000..c3da51d --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/Makefile @@ -0,0 +1,8 @@ +test: + nvim --headless --noplugin -u scripts/minimal_init.vim -c "PlenaryBustedDirectory lua/tests/automated/ { minimal_init = './scripts/minimal_init.vim' }" + +lint: + luacheck lua/telescope + +docgen: + nvim --headless --noplugin -u scripts/minimal_init.vim -c "luafile ./scripts/gendocs.lua" -c 'qa' diff --git a/etc/soft/nvim/+plugins/telescope.nvim/README.md b/etc/soft/nvim/+plugins/telescope.nvim/README.md new file mode 100644 index 0000000..6a57965 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/README.md @@ -0,0 +1,588 @@ +# telescope.nvim + +[![Gitter](https://badges.gitter.im/nvim-telescope/community.svg)](https://gitter.im/nvim-telescope/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +Gaze deeply into unknown regions using the power of the moon. + +## What Is Telescope? + +`telescope.nvim` is a highly extendable fuzzy finder over lists. Built on the +latest awesome features from `neovim` core. Telescope is centered around +modularity, allowing for easy customization. + +Community driven builtin [pickers](#pickers), [sorters](#sorters) and +[previewers](#previewers). + +![Preview](https://i.imgur.com/TTTja6t.gif) +For more showcases of Telescope, please visit the [Showcase +section](https://github.com/nvim-telescope/telescope.nvim/wiki/Showcase) in the +Telescope Wiki + +## Telescope Table of Contents + +- [Getting Started](#getting-started) +- [Usage](#usage) +- [Customization](#customization) +- [Default Mappings](#default-mappings) +- [Pickers](#pickers) +- [Previewers](#previewers) +- [Sorters](#sorters) +- [Layout](#layout-display) +- [Themes](#themes) +- [Commands](#vim-commands) +- [Autocmds](#autocmds) +- [Extensions](#extensions) +- [API](#api) +- [Media](#media) +- [Contributing](#contributing) +- [Changelog](https://github.com/nvim-telescope/telescope.nvim/blob/master/doc/telescope_changelog.txt) + +## Getting Started + +This section should guide you to run your first builtin pickers. + +[Neovim (v0.7.0)](https://github.com/neovim/neovim/releases/tag/v0.7.0) or the +latest neovim nightly commit is required for `telescope.nvim` to work. + +### Required dependencies + +- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim) is required. + +### Suggested dependencies + +- [BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep) is required for + `live_grep` and `grep_string` and is the first priority for `find_files`. + +We also suggest you install one native telescope sorter to significantly improve +sorting performance. Take a look at either +[telescope-fzf-native.nvim](https://github.com/nvim-telescope/telescope-fzf-native.nvim) +or +[telescope-fzy-native.nvim](https://github.com/nvim-telescope/telescope-fzy-native.nvim). +For more information and a performance benchmark take a look at the +[Extensions](https://github.com/nvim-telescope/telescope.nvim/wiki/Extensions) +wiki. + +### Optional dependencies + +- [sharkdp/fd](https://github.com/sharkdp/fd) (finder) +- [nvim-treesitter/nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) (finder/preview) +- [neovim LSP]( https://neovim.io/doc/user/lsp.html) (picker) +- [devicons](https://github.com/kyazdani42/nvim-web-devicons) (icons) + +### Installation + +It is suggested to either use the latest release +[tag](https://github.com/nvim-telescope/telescope.nvim/tags) or our release +branch (which will get consistent updates) +[0.1.x](https://github.com/nvim-telescope/telescope.nvim/tree/0.1.x). + +It is not suggested to run latest master. + +Using [vim-plug](https://github.com/junegunn/vim-plug) + +```viml +Plug 'nvim-lua/plenary.nvim' +Plug 'nvim-telescope/telescope.nvim', { 'tag': '0.1.0' } +" or , { 'branch': '0.1.x' } +``` + +Using [dein](https://github.com/Shougo/dein.vim) + +```viml +call dein#add('nvim-lua/plenary.nvim') +call dein#add('nvim-telescope/telescope.nvim', { 'rev': '0.1.0' }) +" or , { 'rev': '0.1.x' }) +``` +Using [packer.nvim](https://github.com/wbthomason/packer.nvim) + +```lua +use { + 'nvim-telescope/telescope.nvim', tag = '0.1.0', +-- or , branch = '0.1.x', + requires = { {'nvim-lua/plenary.nvim'} } +} +``` + +### checkhealth + +Make sure you call `:checkhealth telescope` after installing telescope to ensure +everything is set up correctly. + +After this setup you can continue reading here or switch to `:help telescope` +to get an understanding of how to use Telescope and how to configure it. + +## Usage + +Try the command `:Telescope find_files` + to see if `telescope.nvim` is installed correctly. + +Using VimL: + +```viml +" Find files using Telescope command-line sugar. +nnoremap ff Telescope find_files +nnoremap fg Telescope live_grep +nnoremap fb Telescope buffers +nnoremap fh Telescope help_tags + +" Using Lua functions +nnoremap ff lua require('telescope.builtin').find_files() +nnoremap fg lua require('telescope.builtin').live_grep() +nnoremap fb lua require('telescope.builtin').buffers() +nnoremap fh lua require('telescope.builtin').help_tags() +``` + +Using Lua: + +```lua +local builtin = require('telescope.builtin') +vim.keymap.set('n', 'ff', builtin.find_files, {}) +vim.keymap.set('n', 'fg', builtin.live_grep, {}) +vim.keymap.set('n', 'fb', builtin.buffers, {}) +vim.keymap.set('n', 'fh', builtin.help_tags, {}) +``` + +See [builtin pickers](#pickers) for a list of all builtin functions. + +## Customization + +This section should help you explore available options to configure and +customize your `telescope.nvim`. + +Unlike most vim plugins, `telescope.nvim` can be customized by either applying +customizations globally, or individually per picker. + +- **Global Customization** affecting all pickers can be done through the main + `setup()` method (see defaults below) +- **Individual Customization** affecting a single picker by passing `opts` to + builtin pickers (e.g. `builtin.find_files(opts)`) see + [Configuration recipes](https://github.com/nvim-telescope/telescope.nvim/wiki/Configuration-Recipes) + wiki page for ideas. + +### Telescope setup structure + +```lua +require('telescope').setup{ + defaults = { + -- Default configuration for telescope goes here: + -- config_key = value, + mappings = { + i = { + -- map actions.which_key to (default: ) + -- actions.which_key shows the mappings for your picker, + -- e.g. git_{create, delete, ...}_branch for the git_branches picker + [""] = "which_key" + } + } + }, + pickers = { + -- Default configuration for builtin pickers goes here: + -- picker_name = { + -- picker_config_key = value, + -- ... + -- } + -- Now the picker_config_key will be applied every time you call this + -- builtin picker + }, + extensions = { + -- Your extension configuration goes here: + -- extension_name = { + -- extension_config_key = value, + -- } + -- please take a look at the readme of the extension you want to configure + } +} +``` + +To look at what default configuration options exist please read: `:help +telescope.setup()`. For picker specific `opts` please read: `:help +telescope.builtin`. + + +To embed the above code snippet in a `.vim` file + (for example in `after/plugin/telescope.nvim.vim`), + wrap it in `lua << EOF code-snippet EOF`: + +```lua +lua << EOF +require('telescope').setup{ + -- ... +} +EOF +``` + +## Default Mappings + +Mappings are fully customizable. +Many familiar mapping patterns are set up as defaults. + +| Mappings | Action | +|----------------|------------------------------------------------------| +| `/` | Next item | +| `/` | Previous item | +| `j/k` | Next/previous (in normal mode) | +| `H/M/L` | Select High/Middle/Low (in normal mode) | +| `gg/G` | Select the first/last item (in normal mode) | +| `` | Confirm selection | +| `` | Go to file selection as a split | +| `` | Go to file selection as a vsplit | +| `` | Go to a file in a new tab | +| `` | Scroll up in preview window | +| `` | Scroll down in preview window | +| `` | Show mappings for picker actions (insert mode) | +| `?` | Show mappings for picker actions (normal mode) | +| `` | Close telescope | +| `` | Close telescope (in normal mode) | +| `` | Toggle selection and move to next selection | +| `` | Toggle selection and move to prev selection | +| `` | Send all items not filtered to quickfixlist (qflist) | +| `` | Send all selected items to qflist | + + +To see the full list of mappings, check out `lua/telescope/mappings.lua` and the +`default_mappings` table. + +**Tip**: you can use `` and `?` in insert and normal mode, respectively, to show the actions mapped to your picker. + +Much like [builtin pickers](#pickers), there are a number of +[actions](https://github.com/nvim-telescope/telescope.nvim/blob/master/lua/telescope/actions/init.lua) +you can pick from to remap your telescope buffer mappings, or create a new +custom action: + +```lua +-- Built-in actions +local transform_mod = require('telescope.actions.mt').transform_mod + +-- or create your custom action +local my_cool_custom_action = transform_mod({ + x = function(prompt_bufnr) + print("This function ran after another action. Prompt_bufnr: " .. prompt_bufnr) + -- Enter your function logic here. You can take inspiration from lua/telescope/actions.lua + end, +}) +``` + +To remap telescope mappings, please read `:help telescope.defaults.mappings`. +To do picker specific mappings, its suggested to do this with the `pickers` +table in `telescope.setup`. Each picker accepts a `mappings` table like its +explained in `:help telescope.defaults.mappings`. + +## Pickers + +Built-in functions. Ready to be bound to any key you like. + +```vim +:lua require'telescope.builtin'.planets{} + +:nnoremap pp :lua require'telescope.builtin'.planets{} +``` + +### File Pickers + +| Functions | Description | +|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `builtin.find_files` | Lists files in your current working directory, respects .gitignore | +| `builtin.git_files` | Fuzzy search through the output of `git ls-files` command, respects .gitignore | +| `builtin.grep_string` | Searches for the string under your cursor in your current working directory | +| `builtin.live_grep` | Search for a string in your current working directory and get results live as you type, respects .gitignore | + +### Vim Pickers + +| Functions | Description | +|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `builtin.buffers` | Lists open buffers in current neovim instance | +| `builtin.oldfiles` | Lists previously open files | +| `builtin.commands` | Lists available plugin/user commands and runs them on `` | +| `builtin.tags` | Lists tags in current directory with tag location file preview (users are required to run ctags -R to generate tags or update when introducing new changes) | +| `builtin.command_history` | Lists commands that were executed recently, and reruns them on `` | +| `builtin.search_history` | Lists searches that were executed recently, and reruns them on `` | +| `builtin.help_tags` | Lists available help tags and opens a new window with the relevant help info on `` | +| `builtin.man_pages` | Lists manpage entries, opens them in a help window on `` | +| `builtin.marks` | Lists vim marks and their value | +| `builtin.colorscheme` | Lists available colorschemes and applies them on `` | +| `builtin.quickfix` | Lists items in the quickfix list | +| `builtin.quickfixhistory` | Lists all quickfix lists in your history and open them with `builtin.quickfix` | +| `builtin.loclist` | Lists items from the current window's location list | +| `builtin.jumplist` | Lists Jump List entries | +| `builtin.vim_options` | Lists vim options, allows you to edit the current value on `` | +| `builtin.registers` | Lists vim registers, pastes the contents of the register on `` | +| `builtin.autocommands` | Lists vim autocommands and goes to their declaration on `` | +| `builtin.spell_suggest` | Lists spelling suggestions for the current word under the cursor, replaces word with selected suggestion on `` | +| `builtin.keymaps` | Lists normal mode keymappings | +| `builtin.filetypes` | Lists all available filetypes | +| `builtin.highlights` | Lists all available highlights | +| `builtin.current_buffer_fuzzy_find` | Live fuzzy search inside of the currently open buffer | +| `builtin.current_buffer_tags` | Lists all of the tags for the currently open buffer, with a preview | +| `builtin.resume` | Lists the results incl. multi-selections of the previous picker | +| `builtin.pickers` | Lists the previous pickers incl. multi-selections (see `:h telescope.defaults.cache_picker`) | + +### Neovim LSP Pickers + +| Functions | Description | +|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| +| `builtin.lsp_references` | Lists LSP references for word under the cursor | +| `builtin.lsp_incoming_calls` | Lists LSP incoming calls for word under the cursor | +| `builtin.lsp_outgoing_calls` | Lists LSP outgoing calls for word under the cursor | +| `builtin.lsp_document_symbols` | Lists LSP document symbols in the current buffer | +| `builtin.lsp_workspace_symbols` | Lists LSP document symbols in the current workspace | +| `builtin.lsp_dynamic_workspace_symbols` | Dynamically Lists LSP for all workspace symbols | +| `builtin.diagnostics` | Lists Diagnostics for all open buffers or a specific buffer. Use option `bufnr=0` for current buffer. | +| `builtin.lsp_implementations` | Goto the implementation of the word under the cursor if there's only one, otherwise show all options in Telescope | +| `builtin.lsp_definitions` | Goto the definition of the word under the cursor, if there's only one, otherwise show all options in Telescope | +| `builtin.lsp_type_definitions` | Goto the definition of the type of the word under the cursor, if there's only one, otherwise show all options in Telescope| + + +### Git Pickers + +| Functions | Description | +|-------------------------------------|------------------------------------------------------------------------------------------------------------| +| `builtin.git_commits` | Lists git commits with diff preview, checkout action ``, reset mixed `m`, reset soft `s` and reset hard `h` | +| `builtin.git_bcommits` | Lists buffer's git commits with diff preview and checks them out on `` | +| `builtin.git_branches` | Lists all branches with log preview, checkout action ``, track action `` and rebase action`` | +| `builtin.git_status` | Lists current changes per file with diff preview and add action. (Multi-selection still WIP) | +| `builtin.git_stash` | Lists stash items in current repository with ability to apply them on `` | + +### Treesitter Picker + +| Functions | Description | +|-------------------------------------|---------------------------------------------------| +| `builtin.treesitter` | Lists Function names, variables, from Treesitter! | + +### Lists Picker + +| Functions | Description | +|-------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `builtin.planets` | Use the telescope... | +| `builtin.builtin` | Lists Built-in pickers and run them on ``. | +| `builtin.reloader` | Lists Lua modules and reload them on ``. | +| `builtin.symbols` | Lists symbols inside a file `data/telescope-sources/*.json` found in your rtp. More info and symbol sources can be found [here](https://github.com/nvim-telescope/telescope-symbols.nvim) | + +## Previewers + +| Previewers | Description | +|------------------------------------|-----------------------------------------------------------| +| `previewers.vim_buffer_cat.new` | Default previewer for files. Uses vim buffers | +| `previewers.vim_buffer_vimgrep.new`| Default previewer for grep and similar. Uses vim buffers | +| `previewers.vim_buffer_qflist.new` | Default previewer for qflist. Uses vim buffers | +| `previewers.cat.new` | Terminal previewer for files. Uses `cat`/`bat` | +| `previewers.vimgrep.new` | Terminal previewer for grep and similar. Uses `cat`/`bat` | +| `previewers.qflist.new` | Terminal previewer for qflist. Uses `cat`/`bat` | + +The default previewers are from now on `vim_buffer_` previewers. They use vim +buffers for displaying files and use tree-sitter or regex for file highlighting. + +These previewers are guessing the filetype of the selected file, so there might +be cases where they miss, leading to wrong highlights. This is because we can't +determine the filetype in the traditional way: We don't do `bufload` and instead +read the file asynchronously with `vim.loop.fs_` and attach only a highlighter; +otherwise the speed of the previewer would slow down considerably. If you want +to configure more filetypes, take a look at +[plenary wiki](https://github.com/nvim-lua/plenary.nvim#plenaryfiletype). + +If you want to configure the `vim_buffer_` previewer (e.g. you want the line to wrap), do this: + +```vim +autocmd User TelescopePreviewerLoaded setlocal wrap +``` + +## Sorters + +| Sorters | Description | +|------------------------------------|-----------------------------------------------------------------| +| `sorters.get_fuzzy_file` | Telescope's default sorter for files | +| `sorters.get_generic_fuzzy_sorter` | Telescope's default sorter for everything else | +| `sorters.get_levenshtein_sorter` | Using Levenshtein distance algorithm (don't use :D) | +| `sorters.get_fzy_sorter` | Using fzy algorithm | +| `sorters.fuzzy_with_index_bias` | Used to list stuff with consideration to when the item is added | + +A `Sorter` is called by the `Picker` on each item returned by the `Finder`. It +returns a number, which is equivalent to the "distance" between the current +`prompt` and the `entry` returned by a `finder`. + +## Layout (display) + +Layout can be configured by choosing a specific `layout_strategy` and +specifying a particular `layout_config` for that strategy. +For more details on available strategies and configuration options, +see `:help telescope.layout`. + +Some options for configuring sizes in layouts are "resolvable". This means that +they can take different forms, and will be interpreted differently according to +which form they take. +For example, if we wanted to set the `width` of a picker using the `vertical` +layout strategy to 50% of the screen width, we would specify that width +as `0.5`, but if we wanted to specify the `width` to be exactly 80 +characters wide, we would specify it as `80`. +For more details on resolving sizes, see `:help telescope.resolve`. + +As an example, if we wanted to specify the layout strategy and width, +but only for this instance, we could do something like: + +``` +:lua require('telescope.builtin').find_files({layout_strategy='vertical',layout_config={width=0.5}}) +``` + +If we wanted to change the width for every time we use the `vertical` +layout strategy, we could add the following to our `setup()` call: + +```lua +require('telescope').setup({ + defaults = { + layout_config = { + vertical = { width = 0.5 } + -- other layout configuration here + }, + -- other defaults configuration here + }, + -- other configuration values here +}) +``` + +## Themes + +Common groups of settings can be set up to allow for themes. +We have some built in themes but are looking for more cool options. + +![dropdown](https://i.imgur.com/SorAcXv.png) + +| Themes | Description | +|--------------------------|---------------------------------------------------------------------------------------------| +| `themes.get_dropdown` | A list like centered list. [dropdown](https://i.imgur.com/SorAcXv.png) | +| `themes.get_cursor` | [A cursor relative list.](https://github.com/nvim-telescope/telescope.nvim/pull/878) | +| `themes.get_ivy` | Bottom panel overlay. [Ivy #771](https://github.com/nvim-telescope/telescope.nvim/pull/771) | + +To use a theme, simply append it to a builtin function: + +```vim +nnoremap f :lua require'telescope.builtin'.find_files(require('telescope.themes').get_dropdown({})) +" Change an option +nnoremap f :lua require'telescope.builtin'.find_files(require('telescope.themes').get_dropdown({ winblend = 10 })) +``` + +Or use with a command: + +```vim +Telescope find_files theme=dropdown +``` + +Or you can configure it in the pickers table in `telescope.setup`: + +```lua +require('telescope').setup{ + defaults = { + -- ... + }, + pickers = { + find_files = { + theme = "dropdown", + } + }, + extensions = { + -- ... + } +} +``` + +Themes should work with every `telescope.builtin` function. If you wish to make +a theme, check out `lua/telescope/themes.lua`. + +## Vim Commands + +All `telescope.nvim` functions are wrapped in `vim` commands for easy access, +tab completions and setting options. + +```viml +" Show all builtin pickers +:Telescope + +" Tab completion +:Telescope | +:Telescope find_files + +" Setting options +:Telescope find_files prompt_prefix=🔍 + +" If the option accepts a Lua table as its value, you can use, to connect each +" command string, e.g.: find_command, vimgrep_arguments are both options that +" accept a Lua table as a value. So, you can configure them on the command line +"like so: +:Telescope find_files find_command=rg,--ignore,--hidden,--files prompt_prefix=🔍 +``` + +for more information and how to realize more complex commands please read +`:help telescope.command`. + +## Autocmds + +Telescope user autocmds: + +| Event | Description | +|---------------------------------|---------------------------------------------------------| +| `User TelescopeFindPre` | Do it before Telescope creates all the floating windows | +| `User TelescopePreviewerLoaded` | Do it after Telescope previewer window is created | + +## Extensions + +Telescope provides the capabilities to create & register extensions, which +improve telescope in a variety of ways. + +Some extensions provide integration with external tools, outside of the scope of +`builtins`. Others provide performance enhancements by using compiled C and +interfacing directly with Lua over LuaJIT's FFI library. + +A list of community extensions can be found in the +[Extensions](https://github.com/nvim-telescope/telescope.nvim/wiki/Extensions) +wiki. Always read the README of the extension you want to install, but here is a +general overview of how most extensions work. + +### Loading extensions + +To load an extension, use the `load_extension` function as shown in the example +below: + +```lua +-- This will load fzy_native and have it override the default file sorter +require('telescope').load_extension('fzy_native') +``` + +You may skip explicitly loading extensions (they will then be lazy-loaded), but +tab completions will not be available right away. + +### Accessing pickers from extensions + +Pickers from extensions are added to the `:Telescope` command under their +respective name. For example: + +```viml +" Run the `configurations` picker from nvim-dap +:Telescope dap configurations +``` + +They can also be called directly from Lua: + +```lua +-- Run the `configurations` picker from nvim-dap +require('telescope').extensions.dap.configurations() +``` + +## API + +For writing your own picker and for information about the API please read the +[Developers Documentation](developers.md). + +## Media + +- [What is Telescope? (Video)](https://www.twitch.tv/teej_dv/clip/RichDistinctPlumberPastaThat) +- [More advanced configuration (Video)](https://www.twitch.tv/videos/756229115) +- [Example video](https://www.youtube.com/watch?v=65AVwHZflsU) + +## Contributing + +All contributions are welcome! Just open a pull request. +Please read [CONTRIBUTING.md](./CONTRIBUTING.md) + +## Related Projects + +- [fzf.vim](https://github.com/junegunn/fzf.vim) +- [denite.nvim](https://github.com/Shougo/denite.nvim) +- [vim-clap](https://github.com/liuchengxu/vim-clap) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/autoload/health/telescope.vim b/etc/soft/nvim/+plugins/telescope.nvim/autoload/health/telescope.vim new file mode 100644 index 0000000..46cc928 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/autoload/health/telescope.vim @@ -0,0 +1,3 @@ +function! health#telescope#check() + lua require 'telescope.health'.check() +endfunction diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/earth b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/earth new file mode 100644 index 0000000..aa624e1 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/earth @@ -0,0 +1,36 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▒▒▒░  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒▓▓▓▓▓▒▒   ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▓▒▒░   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓ ░▓▓▓▒▒▒▒▒  ▒░ ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▒▓▓▓▓▓▓▒▒ ▒▓▒░░▒▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓░▓▓▓▒▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓░░▒▓▓▓▓▓▓▓▓▓   ▒░░  ░ ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▒▓▒▒▓▓▓▓▒▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▒▓▓▓▓▓▓▓▓ ▒░  ░▓  ░ ░ ▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▒▒▒▓▓▒░░▓▒▓▓▓▓▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓     ░▒▓      ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▒▒▓▒▒░░░▓▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░    ▓       ▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▒▒▒▒▒▒▒░▒▓▓▓▓▒▓▒▒▓▒▓▓▓▒▓▓▓▓▓▓▓▓▓▓▒▒▓▒▒░▓▓▓░▓▓▓▓▓▒▓    ░▒▓▒▓░       ▓▓▓▓▓▓ +▓▓▓▓▓▓ ▒▒▒▒░▒▒▒░░▒▓▓▒▓░▓▓▓░▒▓▓▓▓▓▓▓▓▒▓▓▒▒▒▒▒░░░▓▓ ▓▓░▓▓▓▒▒░  ▓ ░▒▓         ▓▓▓▓▓ +▓▓▓▓▓▓▒▒▒▒░▒▒▒░▒▓▒▒▓▓▓▓▓▓▒▓▓ ▓░▓▓▓▒▒▒▓▓▓▓▒░░░░▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒         ▓▓▓▓▓ +▓▓▓▓▓ ▒▒▒▓▓▒▒▒▒▒▓▓▒▒▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▒▒ ▓▓▒▓▒▒░▒ ▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒░▓   ░░   ▒ ▓▓▓▓▓ +▓▓▓▓▓▒▒▒▒▒▒▒▒▒▓▒▓▓▒▓▓▓▓▓▓▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▒▒▒▓▓▒▓▓▓▓▓▓▓▓ ░▓▓▓▓▒▒░   ▒ ░   ▒░ ▓▓▓▓ +▓▓▓▓▓▒▒▒▒▒▒▒▒▒▓▓▒▓▓▓▒▓▓▓▓▒▓▓▓▓▒▓▒░ ▓▒▓▒▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓  ▒ ▒░▒░░    ░ ▒░     ▓▓▓▓ +▓▓▓▓▓▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▓▒▒▓▓▓▓▓▓▓▒▓▓▓▒▓▓▓▓▓▒▒▓░▓▓▓▒░▒   ░▒  ░ ░    ▒    ░▒░    ▓▓▓▓ +▓▓▓▓▓▒▒▒▒▒░░▒▓▒▒▒▒▒▒▒▓ ▓▓▓▒▓▓▓▓▓▓ ▓▒░░▓▓░▒▓▓            ░   ░ ░▒ ░░░░▒░ ░░▒ ▓▓▓▓ +▓▓▓▓▓▒▒▒▒▓▒▒▒▒░▒░▒▓  ▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▒ ▓▓▓▓░          ▒▒▓▒░ ▒░░ ▒▓▓▓▒▒▒░▒ ▓ ▓▓▓▓ +▓▓▓▓▓ ▒░▒▓▒▒░░▒░░░▒▒▓ ░ ░▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▒ ▒▓   ▓▓  ░░░▒░▒   ▒▓▓▓▓▒░░    ▓▓▓▓▓ +▓▓▓▓▓▓▒▒░  ▒▓▒▒▒▒▓▒░ ▒ ░▒ ░▓▓▓▓▓▓▓▓▓▓▓▓░▒░░▓▓▓▓ ▓▓▓ ░▓▒░  ░ ░▒▒░▒▓▓░░░▒    ▓▓▓▓▓ +▓▓▓▓▓▓░▒▒▒▒ ▒  ▒▒░▓▒░▒ ▒░▒▓░▒▒▓▓▓▓▓▓▓▓▓▒▒░ ▒▓▓▒▓▓▓▓▓▓▓▓▓▒░▓▒ ▓▓▒▓▓▓▓▓▓▒░ ▒▓▓▓▓▓▓ +▓▓▓▓▓▓▓▒▒▒▒▒▒▒░▒▒▒░░▒▓░░▒▒░▒░▒░▒▓▓▓▒▒▒▒▓░ ░ ░       ▓▓▒▒▒░▓▓▓▓▓▓▓▓▓▓▓▓▒▒░ ▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▒▒░▒▒▒▒░▒▒░▒ ▒ ▓▓▓░▒▓░▒▓▓▒░░░░▒▒  ░░▓ ▒░ ▒░░▒░▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒ ▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▒░▒▒▒▒▒░▒▒▒░▒▒▒▒  ▒▒░ ▒▒▒▓░▒▒   ▒▓▓░▒      ▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒ ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒░▒▒░▒░▒▒▓░▒▓▓░▒▒░▒░▒▓▒▓▓▓▓▒▒▓░▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▒▒▒▒ ▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒░▒▒▒▒░▒░░▓▒▓▒▒▓░▓▓░▓▓ ▓▒▓ ▒  ░▓ ▒░▓░▓▓▓▓▓▓▓▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓ ▒ ▒▒▒▒▒▒▒░▒░▒░░░▒▒░░░░ ▒ ░░▓▒░▓▓▓▓▓▒▓▓▓ ▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒▒▒▒▒▒▓░▒▒░▒░▒▒░░▒▓▒▒▓▒▒▒▓▒░▓▒▒▓▓▓▓▓▓▓▓░░▓▓▓▓▓▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▒▒░░▒▓▓▓▒▓▓▓▒▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒ ▒▒▒▒░░▒▒▒▓▒▒▓▒░▒▒▒▒░▒▒▓▓▒▒▓▓▒▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒░▒▒▒▒▒▒▒▒▒▒▓▒▒▒▓▓▒▓▒▒▒▒▓▓▒▒▓▓▓▓▓▒▓░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▓▓▒▓▓▓▒░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▓▒░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/jupiter b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/jupiter new file mode 100644 index 0000000..dac1487 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/jupiter @@ -0,0 +1,36 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ░░░ ░    ░  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▒▒▒▒▒▒▒░░ ▒ ░▒░ ░░▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░▒▓▒▒░▒▒ ▒▒▒░▒▒░▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒ ▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▓▓▒▓▒▓▓▓▓▓▓▓▓▒▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒   ▓ ░   ░▒▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▓▒▒▓▓▒▓▓▓▒▒▒▒▓▒▒▒▒▒▒▓▒▓▓▒▓▒▒▒▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▒▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▒▒▒▒▓▒▒▓▓▓▒▒▒▒▒▒▒▒▓░░░▒▒▒▒░░░░░▒▒▒░░░░░▒▒▒░░░▒░▒▒▒▒▒▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▓▓▓▓▓▓▒▓▓▓▓▒▒▓▓▒▒▒▓▓▒▒▒▒▓▓▓▒▓▓▓▓▓▒▓▒▓▓▓▓▓▓▓▓ ▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓▓▓▒░▒▓▒▒▓▓▓▒▓▓▒▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▒░▓▒▓▓   ▒ ▓▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▒▒░▓▓░░░  ▒   ▒▒▓▓▒  ▓▓░▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▒▓▓▓▒▒▓▓░▒░ ▓▓▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓ ▓▓▓ +▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▓▓▓▓▒▓▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▒▓▓▓▓▓▒▒▓▓▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓░░▓▒▒▓▒▓ ▒▒▓▓▒▓▓▒▓▓▒▓▓▓▓▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▒▒▒▒▒▒▓▒▓▓▓▒▒▓▓▒▒    ░░▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▒▒▓▓▓▒▓▓▓▓▒      ░▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▓▓▓▒▓▒▓▓▓▓▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mars b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mars new file mode 100644 index 0000000..2eb8cfd --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mars @@ -0,0 +1,27 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒░▒   ▓▒ ░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░  ░░░▒▒ ░ ▒░  ░░░░░░░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ ░░  ░ ░░▒▓░░░░░░░ ░░ ░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░  ░░░░░░░░ ░▒░▒░░░▒▒░░░░░░░░░ ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░ ░░░░░░░░░░░░░░░▒▒▒▓▒▒▒▒▒▒▒░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ░░ ░  ░░░░░░░░░░ ░░░▒▒▒▒▒▒▒▓▒▒▒▒░▒▒░░░▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ░ ░░         ░  ░  ░ ░░▒▓▒▒▒▒▒▒▒▓▒▒░▒░▒▒░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ ░░                  ░ ░░▒▒▒▒▒▓▒▒▓▒▒░░ ▒▒░ ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ░                        ░░░▒▒▒▒▒▒▒▒▒░ ░░▒░░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ░▒░░▒▒▒▒▒▒░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                        ░░ ░░░░░▒▒░▒░▒▒▒░░░░░ ░ ░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                        ░ ░▒▒ ░ ░▒░░░░░░░░░  ░▒░░ ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓                        ░░  ░░ ░░ ░░░░░░░ ░░░░  ░░  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓        ░           ░░ ░              ░     ▒░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                      ░░░░     ░░     ░░ ░░░░░ ▒░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                       ░░░             ░ ░░▒░ ░▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                        ░░  ░  ░       ░░░  ░▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░                       ░ ▒        ░  ░░░▒▒░▒▒▒░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░        ░     ░ ░    ▒  ░░░ ░  ░░░░▒░░▓░▒░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░        ░          ░░ ▒▓▒░▒▒▒ ░░░░▒▒░▒▒ ░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░░ ░   ░░░          ░▒ ▒▒▒▓▒▒░░░░░░░░░▒░▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░ ░░░░░░░░░░ ░░░░ ▓▒▒▒▒▒▒▒▒▒▒░░░░░░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒░░░░░░▒▒▒▒▒░▒▒▒░░▒▒▓▓▓▓▒▓▒▒▒▒▒░▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░   ░▒▒░░░░░▒▒░░▒▒▒▓▓▒▒▒▒▒▒░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ░ ░ ░░░░░░░░░▒▒░░░░░░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░░░░░░░ ░ ░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mercury b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mercury new file mode 100644 index 0000000..fcad37d --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/mercury @@ -0,0 +1,36 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓            ░  ░░    ░   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓    ░░            ░░   ░░  ░░ ░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓      ░░            ░   ░ ░░ ░ ░     ░   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓    ░░ ░          ░ ░        ░░ ░  ░░░     ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                 ░░    ░          ░░  ░  ░ ░  ░ ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓             ░░     ░░░░      ░ ░  ░ ░░  ░ ░░░░ ░░░░  ▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓             ░░  ░     ░ ░░░ ░░ ░ ░░░    ░ ░░░░░░░ ░░░░░ ▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓ ░    ░   ░ ░░░  ░░░ ░░  ░░░░░░   ░░░░░  ░ ░ ░░ ░░░ ░░░░░░░ ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓░   ░░ ░░  ░  ░  ░░░  ░░░ ░░ ░░  ░  ░░░░░░ ░░░ ░░░   ░░ ░░░░░░░▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓    ░░           ░░ ░   ░ ░░  ░░░░░░ ░░░░░░ ░    ░ ░  ░  ░░░░░ ░ ▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓ ░   ░ ░░░    ░  ░░░░ ░ ░ ░░░  ░░░░░░░░░░░░░░░░░░░ ░ ░ ░ ░░░░░░ ░░░▓▓▓▓▓▓ +▓▓▓▓▓▓    ░ ░     ░     ░░░░      ░   ░░░   ░ ░░░░░░░░░░░ ░░  ░░░  ░  ░ ░░▓▓▓▓▓▓ +▓▓▓▓▓▓ ░   ░  ░░░ ░   ░░░░░     ░   ░░  ░░   ░░░   ░░░░░░░░  ░░░░░░░░░░░░ ░▓▓▓▓▓ +▓▓▓▓▓░ ░  ░░        ░ ░░░ ░ ░░ ░  ░░░░░░░░ ░  ░░░ ░ ░░░░░░    ░░░ ░░░░░░░░  ▓▓▓▓ +▓▓▓▓▓   ░░░ ░░░░░  ░░  ░░░░░   ░░░░ ░░  ░ ░░░ ░░░░░░ ░ ░░░░ ░  ░░░░░░   ░░ ░▓▓▓▓ +▓▓▓▓▓ ░░░░░░░ ░░░     ░░░░ ░░░░░░░░     ░░░ ░      ░   ░░░░ ░░░░░░░░░░  ░░░ ▓▓▓▓ +▓▓▓▓ ░ ░░░░░ ░░░   ░░░░░ ░ ░░░░░░░░ ░   ░  ░  ░ ░░ ░░  ░░░░░░░░░░░░░░░░░░░░░▓▓▓▓ +▓▓▓▓░░░░░░░░░░░░░░░░░   ░    ░░ ░░░░░░  ░      ░ ░░░     ░ ░░░░░░░░░░░░░░░░ ▓▓▓▓ +▓▓▓▓ ░  ░░ ░░  ░░░░░░  ░ ░░    ░ ░░░░░         ░░ ░░░░░░░░░ ░░░░░░░░░░░░░ ░ ▓▓▓▓ +▓▓▓▓▓ ░░░       ░░░░░░ ░  ░     ░  ░░░░░ ░░░ ░░░░ ░░░░░░░░ ░░░░░░░░░░░░░░ ░░▓▓▓▓ +▓▓▓▓▓ ░░░░░░ ░  ░░░ ░░ ░ ░░░  ░     ░░░░ ░ ░░░░  ░░░ ░  ░░░  ░░░░░░░░   ░░  ▓▓▓▓ +▓▓▓▓▓░░░  ░░ ░░  ░░░ ░░░░░░░░░░░   ░  ░░░ ░░ ░ ░░░░░    ░ ░░ ░░ ░░░ ░ ░░ ░░░▓▓▓▓ +▓▓▓▓▓▓░░  ░░ ░  ░░░░░░░░░░░░░░░░░░░░░░░ ░  ░░░░       ░░░  ░░░░░░░░ ░░░ ░░ ▓▓▓▓▓ +▓▓▓▓▓▓▓  ░░ ░░░░░░░ ░ ░░░░ ░░░░░ ░░░░░   ░░ ░░░    ░  ░░ ░░░ ░ ░  ░░░░  ░░▓▓▓▓▓▓ +▓▓▓▓▓▓▓  ░░░░░░░░░░░░░░░░░░░ ░   ░░░░░░ ░  ░░ ░    ░  ░     ░  ░░░░  ░░░░░▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓ ░░░░ ░░░░░      ░░░░░  ░░         ░ ░ ░                  ░ ░ ░ ░▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓ ░░░░  ░░░░░░░░░░░░░   ░           ░░             ░    ░░     ░▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓      ░░░░  ░  ░░                ░ ░ ░  ░░        ░      ░░ ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓    ░░░░░░░░        ░                                 ░░ ▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ░░░░░░░                     ░░░    ░              ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ░           ░         ░  ░     ░          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░                      ░  ░    ░          ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ░                      ░           ░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓    ░   ░        ░              ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                   ░  ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/moon b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/moon new file mode 100644 index 0000000..2943e28 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/moon @@ -0,0 +1,35 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/neptune b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/neptune new file mode 100644 index 0000000..9c2954e --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/neptune @@ -0,0 +1,36 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░▒▒▒░░░▒▒░░▒░▒░▒░░▒▒░░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░░░░░░░░░░░░▒░░░░▒░▒░░▒▒▒░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒░░░░   ░░░░░░░░░░░░▒░▒░▒░░░▒▒░░▒░▒░░  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░▒░▒░▒░░░  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░▒░▒░░░░▒▒▒░░░░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒░░░░░░░░░░░░░      ░░░░░░░░░░░░░░░░░▒░░▒▒░▒░░░░   ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▒░░░▒░▒░░░░░░                 ░ ░░░░░░░░░░░▒░▒░░░░░░░   ▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓░▒░▒░░░░░                           ░░░░░░▒░░▒▒░░░▒░░░░░  ▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓ ▒▒▒░░░░░░                              ░░░░▒░▒░▒▒░▒▒░░░░░   ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓░▒▒░░░░░░                                ░ ░░░░░░▒░░▒░░░░░░░   ▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓ ▒▒▒░░░░░                                   ░░░░░░▒░░▒░░▒▒░░░░   ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▒▒░░░░░░                                    ░░░░░▒░░░▒░░▒░▒░░░   ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓░░░▒░░░                                       ░░░░░▒░░▒░▒░▒░░░░░   ▓▓▓▓▓▓▓ +▓▓▓▓▓▓▒░▒▒░░                         ░ ░░░░░░░░░     ░░░░░░░▒▒░░░▒░░░    ▓▓▓▓▓▓▓ +▓▓▓▓▓░▒░▒░░░░                          ░░░░░░░░░░░░░░░░░░░▒░▒░▒░░░░░░░░  ▓▓▓▓▓▓▓ +▓▓▓▓▓▒▒▒░░░░░                              ░ ░░░░░░░░░░░ ░▒▒░▒▒▒░░░░░░   ▓▓▓▓▓▓▓ +▓▓▓▓▓▒▒▒▒░░░░                                     ░░░░░░▒░▒▒▒▒▒░░░░░░    ▓▓▓▓▓▓▓ +▓▓▓▓▓░░▒░░░░░░                                   ░░░░░░  ░▒░▒░░░▒░░░░░   ▓▓▓▓▓▓▓ +▓▓▓▓▓░░▒░░░░░░                                   ░░░░░▒▒░░░░░▒▒░▒░▒░░░   ▓▓▓▓▓▓▓ +▓▓▓▓▓ ▒▒▒▒░░░░░░                                ░░░░░░░░░░░▒░▒░░▒░░░░░   ▓▓▓▓▓▓▓ +▓▓▓▓▓▓░▒░░▒░░░░░                            ░ ░░░░░░░░░░░░▒░▒░░░░░░░░   ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓░▒▒░▒▒░░░░░ ░      ░     ░   ░   ░░░░░░░░░░░░▒░░░░░░▒░░░░░░░░     ▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓░▒▒░▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░▒░░▒░░░░░░░░░   ▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓ ▒▒▒░▒ ░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░▒░░░▒░░░▒░▒░▒░░░░░▒░░ ░    ▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░▒░░░▒░░░░░░░░░░░▒░░░░░░░░░░░░▒░▒░░░░░░░░░░    ▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓ ▒░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░░▒░▒░░▒░░░▒░▒░░░░░░░░░ ░░   ▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓░▒░░░░▒░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░▒░░░░░░░░░░░░░    ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓░░▒░░░▒░░░░░░▒░░░░░░░░░░▒░░░░░░░▒░░░░░▒░░░░░░ ░      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░░░░▒░░░▒░░░░░░▒▒░░░░▒░▒░░░▒▒▒░░░░░░░░░  ░     ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░░░▒░░░░░░░░░░░░░░░░░░░░░░▒░░░░░░░░░░      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░▒░░░░░░░░░▒░░░░░░▒░░▒░░▒░░░░░░░ ░    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ ░░░░░░░░░░░░░░░░░░░▒░░░░░░░       ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓     ░░░░░░░░░░░░░░░░        ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/pluto b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/pluto new file mode 100644 index 0000000..cfcde6f --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/pluto @@ -0,0 +1,39 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▒▒▒▒▒▒▒░░░░ ░░░▒░▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▒▒▒▒░░ ░░░░░░ ░░░▒▒▒░▒▒▒▒▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒  ░░  ░     ░░░ ░ ░▒▒▒▒▒▒▒▓▓▒▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▓▒▒▒░           ░        ░░▒▒▒▒▒▒▓▓▓▓▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░                  ░░░▒▒░▒▒▒▒▒▓▓▓▒▓▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒▒▒░░     ▒   ░       ░░░░░░▒▒▒▒▒▓▓▓▓▓▓▓▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▒▒▒░░░▒ ░░▒▒▒░░      ░░░░░░▒▒▒▒▒▓▓▓▓▓▓▓▒▒▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▒▒░▒▒▒▒▒▒▒▒▒▒░░░░░░▒▒░▒▒▒▒▒▒▒▒▓▓▒▓▓▓▓▓▓▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▒▒▓░▒▒▒▒▒▒▒▒░░▒▒▒▒▒░▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▒▓▓▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▒▒▒▒▓▒▒▒▒░░▒ ░░▒▒▒▒▒▓▓▒▓▒▒▒▒▒▒▓▓▓▓▓▓▓▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▒▓▓▓▓▒▓▒▒▒▒▒       ░▒▒▒▓▓▒▓▓▒▒▒▒▒▓▒▒▓▓▓▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓▓▓▓▓▒▓▓▒▒▒▒ ░         ░▒▓▒▒▒▒▒▒▒░▒▓▒▒▓▓▓▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▓▒▓▓▓▓▒▒▓▓▒░             ░▒░░░▒▒▒▒▒▒▒▓▓▓▒▒▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▒▓▓▒▒▓▓▓▓▓░░░░          ░░░░ ░▒▒▒▒░▓▒▒▓▓▓▓  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓    ░░▒ ▒▓▓▒▓▓▒░         ░░░░░░░▒▒▒▒▒▒▓▓▓▒▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒      ▒▒▒ ▒▒▒▒░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▓▓▓▒▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒          ▒▓▒▒▒░░░░░▒▒▒░▒▒▒▒▒▒▒▓▓▓▓▓▒▒  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓    ▒ ▒   ▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓░░▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒  ▒       ▓▓▓▓▒▒▒▒▒▒▒▓▓▓▓▓▓░      ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▒░▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒░▒░     ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▒▒▒▒▒    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓▓▒▒▓▒░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/saturn b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/saturn new file mode 100644 index 0000000..68a7ffa --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/saturn @@ -0,0 +1,36 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒  ▒ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒░▒▒▒▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒ ▒▒▓▒▓▓▒▓░▓░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒    ░▒▒▒ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░▒▒  ▓▓  ▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░▒ ▓▓▓▓▓▓  ▒▓▓▓▓▒▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒ ▓▓▓▓▓▓▓▒ ▒▒▓▓▒▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ ▒▒ ▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▒▒ ▒▓▓▒▒▒▓▒░▓▒▓▒▓▒▓░▒▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▒▒▒▒░▓▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▒▒▒▒▒░░▒▒▒▒▒▒▒▒▒▒▒░▒▓▓▓▓▓▓▓▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒  ▓▒▒▒ ▒░░░░░▒▒▒▒▒▒▒▒▒▒░░▓▓▓▓▓▓▓▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░  ▒▓▒▒▒▒░▒░░░░▒▒▒▒▒▒▒▒▒▒▒░▒▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ▒  ▓▓▒▒▒▒▒░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒     ▓▓▒▒▒░░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓▓▓▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▓  ░▒▒▓▒▒▒▓▒░░░░░▒▒▒▒▒▒▒▓▒▓▒▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▓▓▒  ▒▓▒▒▒▒▒░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓░░▒▒▓▒▒▒ ░▒▒▒▒░▒▒▒▒▒▓▒▒▒▒▒▒░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒░  ▓ ░▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▒▓   ▓▒▒▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓░▓░  ▓▓▓▓▓▒▒▒░▒▓▒▒▒▒▓▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒▒▒ ▒▓▒▒▒▓▒▒▒▒▒▓▒▒▒▒▒▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▒▒▒▒▒▓▒▓▓▒▒▒▒░░▓▓▓▓▓ ▓▓ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒▒▒▒▒▒▓▓▒▓▒ ▓▓▓▓   ▓▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▓      ▒▒░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒   ░▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▒▒▒▓▓▓▒▒░ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒░▒▒▒▒▒░▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▒▒░▒▒ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/uranus b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/uranus new file mode 100644 index 0000000..f5a8b36 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/uranus @@ -0,0 +1,39 @@ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░▒▓▓▓▓▓▓▒▒░     ░░▒▒▓▓▓▓░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░▒▓▓▓▓▓▓▒░              ▒▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▓▓▓▓▓▓▓▓░                 ░▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▓▒▓▓▓▒▒▓▓▓░                   ░▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▒▒▓▒▒▒▒▒▒▒▓ ▓▓▓▓▓▓▓▓▓           ▒▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▓▓▒▒▒▒▒▒▒░░░░░  ▓▓▓▓▓▓▓▓▓▓          ▒▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▒▒▒░▒░░░░░░    ▓▓▓▓▓▓▓▓▓▓          ▒▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▓▓▓▒▒▒░░░░░░░      ▓▓▓▓▓▓▓▓▓▓▓       ▒▓▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▓▓▒▒▒▒░░░░░░░         ▓▓▓▓▓▓▓▓▓      ▒▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▓▓▒▒▒░░ ░░                ▓▓▓▓    ░▒▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒▒▒▒▒▒░░░                    ░▒▒▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒░░▒▒▒░░░░░            ░░░░░░░▒▒▒▒▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒░░  ░▒▒░░░░░░            ░░░░░░░▒▒▒▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒░░   ░▒▒░░░░    ░    ░  ░░░░░░▒▒▒▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░     ░▒░░░░░░░ ░░░░░░░░░░░░▒▒▒▒▓▓▒▓▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒░      ░▒░░░░░░░░░░░░░░░░▒▒▒▒▒▓▓▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒░       ░░▒▒▒▒░▒░▒▒▒▒▒▒▒▒▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒░         ░▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▒▒░░         ░░▒▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░▒▒▒▒▒▒▒░░▒▒▒▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ +▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/venus b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/venus new file mode 100644 index 0000000..b95aff1 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/data/memes/planets/venus @@ -0,0 +1,35 @@ +                          ▓   ▓  ░░        ░░░░ ▓  ▓                             +                           ▒   ░        ░░░░░░  ░   ▒                            +                  ▓ ▓▓ ░  ░      ░░░     ░  ░░   ░ ░ ░  ▒    ▓                   +                ▓   ░░░  ░      ░     ░ ░             ░░   ░                     +                 ▓ ░░░   ░      ░  ░ ░ ░         ░ ░  ▒ ░ ░ ░░   ▓▓              +              ▓    ░  ░░ ░       ░░   ░                        ░                 +             ▓░  ░░ ░ ░ ░  ░░  ░  ░ ░░  ░    ░  ░     ░          ▒               +          ▓▓ ░   ░  ░    ░  ░ ░    ░      ░ ░ ░    ░     ░ ░░ ░                  +          ▓  ░  ░  ░░  ░ ▒  ░ ░░░░        ▒  ░░ ░  ░░  ░░   ░         ▓          +           ░ ░  ░ ░░░   ░░  ░  ▒▒    ░  ░ ░   ░             ▒ ░  ░  ░            +        ▓     ░ ░ ░    ░       ░░      ░░▒     ░░▒▒          ░ ░  ░    ▒         +        ▓░   ░    ░  ░     ░  ░   ░   ░             ░  ░░       ▒  ░   ▓         +         ░     ░ ░    ░ ░  ░            ░░      ░  ░░░░░         ░               +        ░  ░     ░  ░░    ░ ░░   ░    ░ ░     ░░  ░ ▒  ░ ░  ░ ░  ░░    ░         +    ▓                   ░   ░ ▓░     ░           ░ ░         ░     ░  ░░         +    ▓ ▓░          ░  ░ ░░    ░  ░ ░  ░░░        ░░░             ░░               +       ▒     ░░ ░ ░  ░░  ░ ░ ░░           ░        ░    ░           ░ ░  ▓       +                  ░   ░░░░ ░  ░ ░  ░               ░                ░░   ▓       +        ░ ░      ░     ░    ░           ░     ░         ░           ░░           +     ▓ ▓     ░  ░ ░         ░            ░          ░      ░ ░          ▓        +         ░        ░    ░        ░    ░   ░         ░  ░                ▒         +        ▓ ░ ░  ░░   ░  ░    ░   ░  ░░   ░       ░                      ▓         +        ▓    ░  ░          ░   ░░ ░  ░░  ░ ░   ░░ ░              ░               +              ░  ░░     ░ ░░          ░   ░   ░░                ░   ░            +             ░    ░  ░  ░ ░    ░    ░ ░ ░░░░ ░                                   +             ░ ░       ░ ░      ░ ░░  ░   ░ ░   ░       ░░        ░▓             +              ▒░░       ░           ░        ░   ░   ░          ░░  ▓            +                ░                    ░░        ░    ░░ ░       ░                 +                  ▒ ░         ░        ░   ░      ░       ░  ░                   +                    ▒  ░ ░        ░    ░  ░     ░           ▓                    +                    ▓  ░░  ░        ░░  ░     ░  ░  ░   ░                        +                     ▓▓    ▒░      ░           ░   ░░   ▓                        +                              ▓  ░░░░       ░░   ▓                               + + diff --git a/etc/soft/nvim/+plugins/telescope.nvim/developers.md b/etc/soft/nvim/+plugins/telescope.nvim/developers.md new file mode 100644 index 0000000..9edb6f5 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/developers.md @@ -0,0 +1,418 @@ +# Developers + +- [Introduction](#introduction) +- [Guide to your first Picker](#guide-to-your-first-picker) + - [Requires](#requires) + - [First Picker](#first-picker) + - [Replacing Actions](#replacing-actions) + - [Entry Maker](#entry-maker) + - [Oneshot job](#oneshot-job) + - [Previewer](#previewer) + - [More examples](#more-examples) + - [Bundling as Extension](#bundling-as-extension) +- [Technical](#technical) + - [picker](#picker) + - [finders](#finders) + - [actions](#actions) + - [previewers](#previewers) + +## Introduction + +So you want to develop your own picker and/or extension for telescope? Then you +are in the right place! This file will first present an introduction on how to +do this. After that, this document will present a technical explanation of +pickers, finders, actions and the previewer. Should you now yet have an idea of +the general telescope architecture and its components, it is first recommend to +familiarize yourself with the architectural flow-chart that is provided in +vim docs (`:h telescope.nvim`). You can find more information in specific help +pages and we will probably move some of the technical stuff to our vim help docs +in the future. + +This guide is mainly for telescope so it will assume that you already have some knowledge of the Lua +programming language. If not then you can find information for Lua here: +- [Lua 5.1 Manual](https://www.lua.org/manual/5.1/) +- [Getting started using Lua in Neovim](https://github.com/nanotee/nvim-lua-guide) + +## Guide to your first Picker + +To guide you along the way to your first picker we will open an empty lua +scratch file, in which we will develop the picker and run it each time using +`:luafile %`. Later we will bundle this file as an extension. + +### Requires + +The most important includes are the following modules: +```lua +local pickers = require "telescope.pickers" +local finders = require "telescope.finders" +local conf = require("telescope.config").values +``` + +- `pickers`: main module which is used to create a new picker. +- `finders`: provides interfaces to fill the picker with items. +- `config`: `values` table which holds the user's configuration. +So to make it easier we access this table directly in `conf`. + +### First Picker + +We will now make the simplest color picker. (We will approach this example step by step, +you will still need to have the previous requires section above this code.) + +```lua +-- our picker function: colors +local colors = function(opts) + opts = opts or {} + pickers.new(opts, { + prompt_title = "colors", + finder = finders.new_table { + results = { "red", "green", "blue" } + }, + sorter = conf.generic_sorter(opts), + }):find() +end + +-- to execute the function +colors() +``` + +Running this code with `:luafile %` should open a telescope picker with the entries `red`, +`green`, `blue`. Selecting a color and pressing enter will open a new file. In this case +it's not what we want, so we will address this after explaining this snippet. + +We will define a new function `colors` which accepts a table `opts`. This is good +practice because now the user can change how telescope behaves by passing in their +own `opts` table when calling `colors`. + +For example the user can pass in a configuration in `opts` which allows them to change +the theme used for the picker. To allow this, we make sure to pass the `opts` table +as the first argument to `pickers.new`. The second argument is a table +which defines the default behavior of the picker. + +We have defined a `prompt_title` but this isn't required. This will default to use +the text `Prompt` if not set. + +`finder` is a required field that needs to be set to the result of a `finders` +function. In this case we take `new_table` which allows us to define a static +set of values, `results`, which is an array of elements, in this case our colors +as strings. It doesn't have to be an array of strings, it can also be an array of +tables. More on this later. + +`sorter` on the other hand is not a required field but it's good practice to +define it, because the default value will set it to `empty()`, meaning no sorter +is attached and you can't filter the results. Good practice is to set the sorter +to either `conf.generic_sorter(opts)` or `conf.file_sorter(opts)`. + +Setting it to a value from `conf` will respect the user's configuration, so if a user has set-up +`fzf-native` as the sorter then this decision will be respected and the `fzf-native` sorter +will be attached. It's also suggested to pass in `opts` here because the sorter +could make use of it. As an example the fzf sorter can be configured to be case +sensitive or insensitive. A user can set-up a default behavior and then alter +this behavior with the `opts` table. + +After the picker is defined you need to call `find()` to actually start the +picker. + +### Replacing Actions + +Now calling `colors()` will result in the opening of telescope with the values: +`red`, `green` and `blue`. The default theme isn't optimal for this picker so we +want to change it and thanks to the acceptance of `opts` we can. We will replace +the last line with the following to open the picker with the `dropdown` theme. + +```lua +colors(require("telescope.themes").get_dropdown{}) +``` + +Now let's address the issue that selecting a color opens a new buffer. For that +we need to replace the default select action. The benefit of replacing rather than +mapping a new function to `` is that it will respect the user's configuration. So +if a user has remapped `select_default` to another key then this decision will +be respected and it works as expected for the user. + +To make this work we need more requires at the top of the file. + +```lua +local actions = require "telescope.actions" +local action_state = require "telescope.actions.state" +``` + +- `actions`: holds all actions that can be mapped by a user. We also need it to + access the default action so we can replace it. Also see `:help + telescope.actions` + +- `action_state`: gives us a few utility functions we can use to get the + current picker, current selection or current line. Also see `:help + telescope.actions.state` + +So let's replace the default action. For that we need to define a new key value +pair in our table that we pass into `pickers.new`, for example after `sorter`. + +```lua + attach_mappings = function(prompt_bufnr, map) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + -- print(vim.inspect(selection)) + vim.api.nvim_put({ selection[1] }, "", false, true) + end) + return true + end, +``` + +We do this by setting the `attach_mappings` key to a function. This function +needs to return either `true` or `false`. If it returns false it means that only +the actions defined in the function should be attached. In this case it would +remove the default actions to move the selected item in the picker, +`move_selection_{next,previous}`. So in most cases you'll want to return `true`. +If the function does not return anything then an error is thrown. + +The `attach_mappings` function has two parameters, `prompt_bufnr` is the buffer number +of the prompt buffer, which we can use to get the pickers object and `map` is a function +we can use to map actions or functions to arbitrary key sequences. + +Now we are replacing `select_default` the default action, which is mapped to `` +by default. To do this we need to call `actions.select_default:replace` and +pass in a new function. + +In this new function we first close the picker with `actions.close` and then +get the `selection` with `action_state`. It's important +to notice that you can still get the selection and current prompt input +(`action_state.get_current_line()`) with `action_state` even after the picker is +closed. + +You can look at the selection with `print(vim.inspect(selection))` and see that it differs from our input +(string), this is because internally we pack it into a table with different +keys. You can specify this behavior and we'll talk about that in the next +section. Now all that is left is to do something with the selection we have. In +this case we just put the text in the current buffer with `vim.api.nvim_put`. + +### Entry Maker + +Entry maker is a function used to transform an item from the finder to an +internal entry table, which has a few required keys. It allows us to display +one string but match something completly different. It also allows us to set +an absolute path when working with files (so the file will always be found) +and a relative file path for display and sorting. This means the relative file +path doesn't even need to be valid in the context of the current working directory. + +We will now try to define our entry maker for our example by providing an +`entry_maker` to `finders.new_table` and changing our table to be a little bit +more interesting. We will end up with the following new code for `finders.new_table`: + +```lua + finder = finders.new_table { + results = { + { "red", "#ff0000" }, + { "green", "#00ff00" }, + { "blue", "#0000ff" }, + }, + entry_maker = function(entry) + return { + value = entry, + display = entry[1], + ordinal = entry[1], + } + end + }, +``` + +With the new snippet, we no longer have an array of strings but an array of +tables. Each table has a color, name, and the color's hex value. + +`entry_maker` is a function that will receive each table and then we can set the +values we need. It's best practice to have a `value` reference to the +original entry, that way we will always have access to the complete table in our +action. + +The `display` key is required and is either a string or a `function(tbl)`, +where `tbl` is the table returned by `entry_maker`. So in this example `tbl` would +give our `display` function access to `value` and `ordinal`. + +If our picker will have a lot of values it's suggested to use a function for `display`, +especially if you are modifying the text to display. This way the function will only be executed +for the entries being displayed. For an example of an entry maker take a look at +`lua/telescope/make_entry.lua`. + +A good way to make your `display` more like a table is to use a `displayer` which can be found in +`lua/telescope/pickers/entry_display.lua`. A simpler example of `displayer` is the +function `gen_from_git_commits` in `make_entry.lua`. + +The `ordinal` is also required, which is used for sorting. As already mentioned +this allows us to have different display and sorting values. This allows `display` +to be more complex with icons and special indicators but `ordinal` could be a simpler +sorting key. + +There are other important keys which can be set, but do not make sense in the +current context as we are not dealing with files: +- `path`: to set the absolute path of the file to make sure it's always found +- `lnum`: to specify a line number in the file. This will allow the + `conf.grep_previewer` to show that line and the default action to jump to + that line. + +### Previewer + +We will not write a previewer for this picker because it isn't required for +basic colors and is a more advanced topic. It's already well documented in `:help +telescope.previewers` so you can read this section if you want to write your +own `previewer`. If you want a file previewer without columns you should +default to `conf.file_previewer` or `conf.grep_previewer`. + +### Oneshot Job + +The `oneshot_job` finder can be used to have an asynchronous external process which will +find results and call `entry_maker` for each entry. An example usage would be +`find`. + +```lua +finder = finders.new_oneshot_job({ "find" }, opts ), +``` + +### More examples + +A good way to find more examples is to look into the [lua/telescope/builtin](https://github.com/nvim-telescope/telescope.nvim/tree/master/lua/telescope/builtin) +directory which contains all of the builtin pickers. Another way to find more examples +is to take a look at the [extension wiki page](https://github.com/nvim-telescope/telescope.nvim/wiki/Extensions) +as this provides many extensions people have already written which use these concepts. + +If you still have any questions after reading this guide please feel free to ask us for +more information on [gitter](https://gitter.im/nvim-telescope/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +and we will happily answer your questions and hopefully allow us to improve this guide. You can also +help us to improve this guide by sending a PR. + +### Bundling as extension + +If you now want to bundle your picker as extension, so it is available as +picker via the `:Telescope` command, the following has to be done. + +Structure your plugin as follows, so it can be found by telescope: + +``` +. +└── lua + ├── plugin_name # Your actual plugin code + │ ├── init.lua + │ └── some_file.lua + └── telescope + └── _extensions # The underscore is significant + └─ plugin_name.lua # Init and register your extension +``` + +The `lua/telescope/_extensions/plugin_name.lua` file needs to return the +following: (see `:help telescope.register_extension`) + +```lua +return require("telescope").register_extension { + setup = function(ext_config, config) + -- access extension config and user config + end, + exports = { + stuff = require("plugin_name").stuff + }, +} +``` + +The setup function can be used to access the extension config and setup +extension specific global configuration. You also have access to the user +telescope default config, so you can override specific internal function. For +example sorters if you have an extension that provides a replacement sorter, +like +[telescope-fzf-native](https://github.com/nvim-telescope/telescope-fzf-native.nvim). + +The exports table declares the exported pickers that can then be accessed via +`Telescope plugin_name stuff`. If you only provide one export it is suggested +that you name the key like the plugin, so you can access it with `Telescope +plugin_name`. + + +## Technical + +### Picker + +This section is an overview of how custom pickers can be created and configured. + +```lua +-- lua/telescope/pickers.lua +Picker:new{ + prompt_title = "", + finder = FUNCTION, -- see lua/telescope/finder.lua + sorter = FUNCTION, -- see lua/telescope/sorter.lua + previewer = FUNCTION, -- see lua/telescope/previewer.lua + selection_strategy = "reset", -- follow, reset, row + border = {}, + borderchars = {"─", "│", "─", "│", "┌", "┐", "┘", "└"}, + default_selection_index = 1, -- Change the index of the initial selection row +} +``` + +### Finders + +```lua +-- lua/telescope/finders.lua +Finder:new{ + entry_maker = function(line) end, + fn_command = function() { command = "", args = { "ls-files" } } end, + static = false, + maximum_results = false +} +``` + +### Actions + +#### Overriding actions/action_set + +How to override what different functions / keys do. + +TODO: Talk about what actions vs actions sets are + +##### Relevant Files + +- `lua/telescope/actions/init.lua` + - The most "user-facing" of the files, which has the builtin actions that we provide +- `lua/telescope/actions/set.lua` + - The second most "user-facing" of the files. This provides actions that are consumed by several builtin actions, which allows for only overriding ONE item, instead of copying the same configuration / function several times. +- `lua/telescope/actions/state.lua` + - Provides APIs for interacting with the state of telescope from within actions. + - These are useful for writing your own actions and interacting with telescope +- `lua/telescope/actions/mt.lua` + - You probably don't need to look at this, but it defines the behavior of actions. + +##### `:replace(function)` + +Directly override an action with a new function + +```lua +local actions = require('telescope.actions') +actions.select_default:replace(git_checkout_function) +``` + +##### `:replace_if(conditional, function)` + +Override an action only when `conditional` returns true. + +```lua +local action_set = require('telescope.actions.set') +action_set.select:replace_if( + function() + return action_state.get_selected_entry().path:sub(-1) == os_sep + end, function(_, type) + -- type is { "default", "horizontal", "vertical", "tab" } + local path = actions.get_selected_entry().path + action_state.get_current_picker(prompt_bufnr):refresh(gen_new_finder(new_cwd), { reset_prompt = true}) + end +) +``` + +##### `:replace_map(configuration)` + +```lua +local action_set = require('telescope.actions.set') +-- Use functions as keys to map to which function to execute when called. +action_set.select:replace_map { + [function(e) return e > 0 end] = function(e) return (e / 10) end, + [function(e) return e == 0 end] = function(e) return (e + 10) end, +} +``` + +### Previewers + +See `:help telescope.previewers` diff --git a/etc/soft/nvim/+plugins/telescope.nvim/doc/secret.txt b/etc/soft/nvim/+plugins/telescope.nvim/doc/secret.txt new file mode 100644 index 0000000..e872ca4 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/doc/secret.txt @@ -0,0 +1,32 @@ +================================================================================ + *telescope.theprimeagen* + +To The Viewers: ~ + +Oh why hello, I didn't see you there. So nice of you to join us. The Primeagen +must have sent you here. + +The places you want to look for help are: (you can do `:help ` below) + - |telescope.nvim| + - |telescope.setup| + - |telescope.builtin| + - |telescope.layout| + - |telescope.actions| + +I hope you enjoy telescope & Neovim. May your programming always be fun and +your vimming be quick. + + + +To The Primeagen: ~ + +Cyrnfr ernq guvf uryc znahny orsber pnyyvat zr ng 3 NZ jvgu gryrfpbcr +rzretrapvrf. V xabj ynfg gvzr jr fnirq gur ragver fgernzvat vaqhfgel, ohg +V unir n lbhat fba jub xrrcf zr hc ng avtug nyy ol uvzfrys. OGJ, unir lbh +pbafvqrerq fraqvat culfvpny QIQf sbe znkvzhz dhnyvgl naq rneyl npprff gb arj +pbagrag? Vg frrzf yvxr vg pbhyq or n cerggl pbby vqrn. + +#FunzryrffFrysCebzbgvba: uggcf://tvguho.pbz/fcbafbef/gwqrievrf + + + vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope.txt b/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope.txt new file mode 100644 index 0000000..6bb4947 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope.txt @@ -0,0 +1,3682 @@ +================================================================================ +INTRODUCTION *telescope.nvim* + +Telescope.nvim is a plugin for fuzzy finding and neovim. It helps you search, +filter, find and pick things in Lua. + +Getting started with telescope: + 1. Run `:checkhealth telescope` to make sure everything is installed. + 2. Evaluate it working with `:Telescope find_files` or `:lua + require("telescope.builtin").find_files()` + 3. Put a `require("telescope").setup() call somewhere in your neovim config. + 4. Read |telescope.setup| to check what config keys are available and what + you can put inside the setup call + 5. Read |telescope.builtin| to check which builtin pickers are offered and + what options these implement + 6. Profit + +The below flow chart illustrates a simplified telescope architecture: +┌───────────────────────────────────────────────────────────┐ +│ ┌────────┐ │ +│ │ Multi │ ┌───────+ │ +│ │ Select │ ┌───────┐ │ Entry │ │ +│ └─────┬──* │ Entry │ ┌────────+ │ Maker │ │ +│ │ ┌───│Manager│────│ Sorter │┐ └───┬───* │ +│ ▼ ▼ └───────* └────────┘│ │ │ +│ 1────────┐ 2───┴──┐ │ │ +│ ┌─────│ Picker │ │Finder│◄────┘ │ +│ ▼ └───┬────┘ └──────* │ +│ ┌────────┐ │ 3────────+ ▲ │ +│ │Selected│ └───────│ Prompt │─────────┘ │ +│ │ Entry │ └───┬────┘ │ +│ └────────* ┌───┴────┐ ┌────────┐ ┌────────┐ │ +│ │ ▲ 4─────────┐│ Prompt │ │(Attach)│ │Actions │ │ +│ ▼ └──► │ Results ││ Buffer │◄─┤Mappings│◄─┤User Fn │ │ +│5─────────┐ └─────────┘└────────┘ └────────┘ └────────┘ │ +││Previewer│ │ +│└─────────┘ telescope.nvim architecture │ +└───────────────────────────────────────────────────────────┘ + + + The `Entry Maker` at least defines + - value: "raw" result of the finder + - ordinal: string to be sorted derived from value + - display: line representation of entry in results buffer + + * The finder, entry manager, selected entry, and multi selections + comprises `entries` constructed by the `Entry Maker` from + raw results of the finder (`value`s) + + Primary components: + 1 Picker: central UI dedicated to varying use cases + (finding files, grepping, diagnostics, etc.) + see :h telescope.builtin + 2 Finder: pipe or interactively generates results to pick over + 3 Prompt: user input that triggers the finder which sorts results + in order into the entry manager + 4 Results: listed entries scored by sorter from finder results + 5 Previewer: preview of context of selected entry + see :h telescope.previewers + +A practical introduction into telescope customization is our `developers.md` +(top-level of repo) and `:h telescope.actions` that showcase how to access +information about the state of the picker (current selection, etc.). +To find out more: +https://github.com/nvim-telescope/telescope.nvim + + :h telescope.setup + :h telescope.command + :h telescope.builtin + :h telescope.themes + :h telescope.layout + :h telescope.resolve + :h telescope.actions + :h telescope.actions.state + :h telescope.actions.set + :h telescope.actions.utils + :h telescope.actions.generate + :h telescope.actions.history + :h telescope.previewers + +telescope.setup({opts}) *telescope.setup()* + Setup function to be run by user. Configures the defaults, pickers and + extensions of telescope. + + Usage: + > + require('telescope').setup{ + defaults = { + -- Default configuration for telescope goes here: + -- config_key = value, + -- .. + }, + pickers = { + -- Default configuration for builtin pickers goes here: + -- picker_name = { + -- picker_config_key = value, + -- ... + -- } + -- Now the picker_config_key will be applied every time you call this + -- builtin picker + }, + extensions = { + -- Your extension configuration goes here: + -- extension_name = { + -- extension_config_key = value, + -- } + -- please take a look at the readme of the extension you want to configure + } + } +< + + + Valid keys for {opts.defaults} + + *telescope.defaults.sorting_strategy* + sorting_strategy: ~ + Determines the direction "better" results are sorted towards. + + Available options are: + - "descending" (default) + - "ascending" + + *telescope.defaults.selection_strategy* + selection_strategy: ~ + Determines how the cursor acts after each sort iteration. + + Available options are: + - "reset" (default) + - "follow" + - "row" + - "closest" + - "none" + + *telescope.defaults.scroll_strategy* + scroll_strategy: ~ + Determines what happens if you try to scroll past the view of the + picker. + + Available options are: + - "cycle" (default) + - "limit" + + *telescope.defaults.layout_strategy* + layout_strategy: ~ + Determines the default layout of Telescope pickers. + See |telescope.layout| for details of the available strategies. + + Default: 'horizontal' + + *telescope.defaults.layout_config* + layout_config: ~ + Determines the default configuration values for layout strategies. + See |telescope.layout| for details of the configurations options for + each strategy. + + Allows setting defaults for all strategies as top level options and + for overriding for specific options. + For example, the default values below set the default width to 80% of + the screen width for all strategies except 'center', which has width + of 50% of the screen width. + + Default: { + bottom_pane = { + height = 25, + preview_cutoff = 120, + prompt_position = "top" + }, + center = { + height = 0.4, + preview_cutoff = 40, + prompt_position = "top", + width = 0.5 + }, + cursor = { + height = 0.9, + preview_cutoff = 40, + width = 0.8 + }, + horizontal = { + height = 0.9, + preview_cutoff = 120, + prompt_position = "bottom", + width = 0.8 + }, + vertical = { + height = 0.9, + preview_cutoff = 40, + prompt_position = "bottom", + width = 0.8 + } + } + + + *telescope.defaults.cycle_layout_list* + cycle_layout_list: ~ + Determines the layouts to cycle through when using `actions.cycle_layout_next` + and `actions.cycle_layout_prev`. + Should be a list of "layout setups". + Each "layout setup" can take one of two forms: + 1. string
+ This is interpreted as the name of a `layout_strategy` + 2. table
+ A table with possible keys `layout_strategy`, `layout_config` and `previewer` + + Default: { "horizontal", "vertical" } + + + *telescope.defaults.winblend* + winblend: ~ + Configure winblend for telescope floating windows. See |winblend| for + more information. + + Default: 0 + + *telescope.defaults.wrap_results* + wrap_results: ~ + Word wrap the search results + + Default: false + + *telescope.defaults.prompt_prefix* + prompt_prefix: ~ + The character(s) that will be shown in front of Telescope's prompt. + + Default: '> ' + + *telescope.defaults.selection_caret* + selection_caret: ~ + The character(s) that will be shown in front of the current selection. + + + Default: '> ' + + *telescope.defaults.entry_prefix* + entry_prefix: ~ + Prefix in front of each result entry. Current selection not included. + + Default: ' ' + + *telescope.defaults.multi_icon* + multi_icon: ~ + Symbol to add in front of a multi-selected result entry. + Replaces final character of |telescope.defaults.selection_caret| and + |telescope.defaults.entry_prefix| as appropriate. + To have no icon, set to the empty string. + + Default: '+' + + *telescope.defaults.initial_mode* + initial_mode: ~ + Determines in which mode telescope starts. Valid Keys: + `insert` and `normal`. + + Default: "insert" + + *telescope.defaults.border* + border: ~ + Boolean defining if borders are added to Telescope windows. + + Default: true + + *telescope.defaults.path_display* + path_display: ~ + Determines how file paths are displayed + + path_display can be set to an array with a combination of: + - "hidden" hide file names + - "tail" only display the file name, and not the path + - "absolute" display absolute paths + - "smart" remove as much from the path as possible to only show + the difference between the displayed paths. + Warning: The nature of the algorithm might have a negative + performance impact! + - "shorten" only display the first character of each directory in + the path + - "truncate" truncates the start of the path when the whole path will + not fit. To increase the the gap between the path and the edge. + set truncate to number `truncate = 3` + + You can also specify the number of characters of each directory name + to keep by setting `path_display.shorten = num`. + e.g. for a path like + `alpha/beta/gamma/delta.txt` + setting `path_display.shorten = 1` will give a path like: + `a/b/g/delta.txt` + Similarly, `path_display.shorten = 2` will give a path like: + `al/be/ga/delta.txt` + + You can also further customise the shortening behaviour by + setting `path_display.shorten = { len = num, exclude = list }`, + where `len` acts as above, and `exclude` is a list of positions + that are not shortened. Negative numbers in the list are considered + relative to the end of the path. + e.g. for a path like + `alpha/beta/gamma/delta.txt` + setting `path_display.shorten = { len = 1, exclude = {1, -1} }` + will give a path like: + `alpha/b/g/delta.txt` + setting `path_display.shorten = { len = 2, exclude = {2, -2} }` + will give a path like: + `al/beta/gamma/de` + + path_display can also be set to 'hidden' string to hide file names + + path_display can also be set to a function for custom formatting of + the path display. Example: + + -- Format path as "file.txt (path\to\file\)" + path_display = function(opts, path) + local tail = require("telescope.utils").path_tail(path) + return string.format("%s (%s)", tail, path) + end, + + Default: {} + + *telescope.defaults.borderchars* + borderchars: ~ + Set the borderchars of telescope floating windows. It has to be a + table of 8 string values. + + Default: { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + + *telescope.defaults.get_status_text* + get_status_text: ~ + A function that determines what the virtual text looks like. + Signature: function(picker) -> str + + Default: function that shows current count / all + + *telescope.defaults.hl_result_eol* + hl_result_eol: ~ + Changes if the highlight for the selected item in the results + window is always the full width of the window + + Default: true + + *telescope.defaults.dynamic_preview_title* + dynamic_preview_title: ~ + Will change the title of the preview window dynamically, where it + is supported. For example, the preview window's title could show up as + the full filename. + + Default: false + + *telescope.defaults.results_title* + results_title: ~ + Defines the default title of the results window. A false value + can be used to hide the title altogether. + + Default: "Results" + + *telescope.defaults.prompt_title* + prompt_title: ~ + Defines the default title of the prompt window. A false value + can be used to hide the title altogether. Most of the times builtins + define a prompt_title which will be prefered over this default. + + Default: "Prompt" + + *telescope.defaults.mappings* + mappings: ~ + Your mappings to override telescope's default mappings. + + See: ~ + |telescope.mappings| + + + *telescope.defaults.default_mappings* + default_mappings: ~ + Not recommended to use except for advanced users. + + Will allow you to completely remove all of telescope's default maps + and use your own. + + + *telescope.defaults.history* + history: ~ + This field handles the configuration for prompt history. + By default it is a table, with default values (more below). + To disable history, set it to false. + + Currently mappings still need to be added, Example: + mappings = { + i = { + [""] = require('telescope.actions').cycle_history_next, + [""] = require('telescope.actions').cycle_history_prev, + }, + }, + + Fields: + - path: The path to the telescope history as string. + default: stdpath("data")/telescope_history + - limit: The amount of entries that will be written in the + history. + Warning: If limit is set to nil it will grow unbound. + default: 100 + - handler: A lua function that implements the history. + This is meant as a developer setting for extensions to + override the history handling, e.g., + https://github.com/nvim-telescope/telescope-smart-history.nvim, + which allows context sensitive (cwd + picker) history. + + Default: + require('telescope.actions.history').get_simple_history + + *telescope.defaults.cache_picker* + cache_picker: ~ + This field handles the configuration for picker caching. + By default it is a table, with default values (more below). + To disable caching, set it to false. + + Caching preserves all previous multi selections and results and + therefore may result in slowdown or increased RAM occupation + if too many pickers (`cache_picker.num_pickers`) or entries + ('cache_picker.limit_entries`) are cached. + + Fields: + - num_pickers: The number of pickers to be cached. + Set to -1 to preserve all pickers of your session. + If passed to a picker, the cached pickers with + indices larger than `cache_picker.num_pickers` will + be cleared. + Default: 1 + - limit_entries: The amount of entries that will be written in the + Default: 1000 + + + *telescope.defaults.preview* + preview: ~ + This field handles the global configuration for previewers. + By default it is a table, with default values (more below). + To disable previewing, set it to false. If you have disabled previewers + globally, but want to opt in to previewing for single pickers, you will have to + pass `preview = true` or `preview = {...}` (your config) to the `opts` of + your picker. + + Fields: + - check_mime_type: Use `file` if available to try to infer whether the + file to preview is a binary if plenary's + filetype detection fails. + Windows users get `file` from: + https://github.com/julian-r/file-windows + Set to false to attempt to preview any mime type. + Default: true for all OS excl. Windows + - filesize_limit: The maximum file size in MB attempted to be previewed. + Set to false to attempt to preview any file size. + Default: 25 + - timeout: Timeout the previewer if the preview did not + complete within `timeout` milliseconds. + Set to false to not timeout preview. + Default: 250 + - hook(s): Function(s) that takes `(filepath, bufnr, opts)`, where opts + exposes winid and ft (filetype). + Available hooks (in order of priority): + {filetype, mime, filesize, timeout}_hook + Important: the filetype_hook must return true or false + to indicate whether to continue (true) previewing or not (false), + respectively. + Two examples: + local putils = require("telescope.previewers.utils") + ... -- preview is called in telescope.setup { ... } + preview = { + -- 1) Do not show previewer for certain files + filetype_hook = function(filepath, bufnr, opts) + -- you could analogously check opts.ft for filetypes + local excluded = vim.tbl_filter(function(ending) + return filepath:match(ending) + end, { + ".*%.csv", + ".*%.toml", + }) + if not vim.tbl_isempty(excluded) then + putils.set_preview_message( + bufnr, + opts.winid, + string.format("I don't like %s files!", + excluded[1]:sub(5, -1)) + ) + return false + end + return true + end, + -- 2) Truncate lines to preview window for too large files + filesize_hook = function(filepath, bufnr, opts) + local path = require("plenary.path"):new(filepath) + -- opts exposes winid + local height = vim.api.nvim_win_get_height(opts.winid) + local lines = vim.split(path:head(height), "[\r]?\n") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + end, + } + The configuration recipes for relevant examples. + Note: if plenary does not recognize your filetype yet -- + 1) Please consider contributing to: + $PLENARY_REPO/data/plenary/filetypes/builtin.lua + 2) Register your filetype locally as per link + https://github.com/nvim-lua/plenary.nvim#plenaryfiletype + Default: nil + - treesitter: Determines whether the previewer performs treesitter + highlighting, which falls back to regex-based highlighting. + `true`: treesitter highlighting for all available filetypes + `false`: regex-based highlighting for all filetypes + `table`: following nvim-treesitters highlighting options: + It contains two keys: + - enable boolean|table: if boolean, enable all ts + highlighing with that flag, + disable still considered. + Containing a list of filetypes, + that are enabled, disabled + ignored because it doesnt make + any sense in this case. + - disable table: containing a list of filetypes + that are disabled + Default: true + - msg_bg_fillchar: Character to fill background of unpreviewable buffers with + Default: "╱" + - hide_on_startup: Hide previewer when picker starts. Previewer can be toggled + with actions.toggle_preview. + Default: false + + + *telescope.defaults.vimgrep_arguments* + vimgrep_arguments: ~ + Defines the command that will be used for `live_grep` and `grep_string` + pickers. + Hint: Make sure that color is currently set to `never` because we do + not yet interpret color codes + Hint 2: Make sure that these options are in your changes arguments: + "--no-heading", "--with-filename", "--line-number", "--column" + because we need them so the ripgrep output is in the correct format. + + Default: { + "rg", + "--color=never", + "--no-heading", + "--with-filename", + "--line-number", + "--column", + "--smart-case" + } + + *telescope.defaults.use_less* + use_less: ~ + Boolean if less should be enabled in term_previewer (deprecated and + currently no longer used in the builtin pickers). + + Default: true + + *telescope.defaults.set_env* + set_env: ~ + Set an environment for term_previewer. A table of key values: + Example: { COLORTERM = "truecolor", ... } + Hint: Empty table is not allowed. + + Default: nil + + *telescope.defaults.color_devicons* + color_devicons: ~ + Boolean if devicons should be enabled or not. If set to false, the + text highlight group is used. + Hint: Coloring only works if |termguicolors| is enabled. + + Default: true + + *telescope.defaults.file_sorter* + file_sorter: ~ + A function pointer that specifies the file_sorter. This sorter will + be used for find_files, git_files and similar. + Hint: If you load a native sorter, you dont need to change this value, + the native sorter will override it anyway. + + Default: require("telescope.sorters").get_fzy_sorter + + *telescope.defaults.generic_sorter* + generic_sorter: ~ + A function pointer to the generic sorter. The sorter that should be + used for everything that is not a file. + Hint: If you load a native sorter, you dont need to change this value, + the native sorter will override it anyway. + + Default: require("telescope.sorters").get_fzy_sorter + + *telescope.defaults.prefilter_sorter* + prefilter_sorter: ~ + This points to a wrapper sorter around the generic_sorter that is able + to do prefiltering. + Its usually used for lsp_*_symbols and lsp_*_diagnostics + + Default: require("telescope.sorters").prefilter + + *telescope.defaults.tiebreak* + tiebreak: ~ + A function that determines how to break a tie when two entries have + the same score. + Having a function that always returns false would keep the entries in + the order they are found, so existing_entry before current_entry. + Vice versa always returning true would place the current_entry + before the existing_entry. + + Signature: function(current_entry, existing_entry, prompt) -> boolean + + Default: function that breaks the tie based on the length of the + entry's ordinal + + *telescope.defaults.file_ignore_patterns* + file_ignore_patterns: ~ + A table of lua regex that define the files that should be ignored. + Example: { "^scratch/" } -- ignore all files in scratch directory + Example: { "%.npz" } -- ignore all npz files + See: https://www.lua.org/manual/5.1/manual.html#5.4.1 for more + information about lua regex + Note: `file_ignore_patterns` will be used in all pickers that have a + file associated. This might lead to the problem that lsp_ pickers + aren't displaying results because they might be ignored by + `file_ignore_patterns`. For example, setting up node_modules as ignored + will never show node_modules in any results, even if you are + interested in lsp_ results. + + If you only want `file_ignore_patterns` for `find_files` and + `grep_string`/`live_grep` it is suggested that you setup `gitignore` + and have fd and or ripgrep installed because both tools will not show + `gitignore`d files on default. + + Default: nil + + *telescope.defaults.get_selection_window* + get_selection_window: ~ + Function that takes function(picker, entry) and returns a window id. + The window ID will be used to decide what window the chosen file will + be opened in and the cursor placed in upon leaving the picker. + + Default: `function() return 0 end` + + + *telescope.defaults.file_previewer* + file_previewer: ~ + Function pointer to the default file_previewer. It is mostly used + for find_files, git_files and similar. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").cat.new + + Default: require("telescope.previewers").vim_buffer_cat.new + + *telescope.defaults.grep_previewer* + grep_previewer: ~ + Function pointer to the default vim_grep previewer. It is mostly + used for live_grep, grep_string and similar. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").vimgrep.new + + Default: require("telescope.previewers").vim_buffer_vimgrep.new + + *telescope.defaults.qflist_previewer* + qflist_previewer: ~ + Function pointer to the default qflist previewer. It is mostly + used for qflist, loclist and lsp. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").qflist.new + + Default: require("telescope.previewers").vim_buffer_qflist.new + + *telescope.defaults.buffer_previewer_maker* + buffer_previewer_maker: ~ + Developer option that defines the underlining functionality + of the buffer previewer. + For interesting configuration examples take a look at + https://github.com/nvim-telescope/telescope.nvim/wiki/Configuration-Recipes + + Default: require("telescope.previewers").buffer_previewer_maker + + Parameters: ~ + {opts} (table) Configuration opts. Keys: defaults, pickers, + extensions + + +telescope.load_extension({name}) *telescope.load_extension()* + Load an extension. + - Notes: + - Loading triggers ext setup via the config passed in |telescope.setup| + + + Parameters: ~ + {name} (string) Name of the extension + + +telescope.register_extension({mod}) *telescope.register_extension()* + Register an extension. To be used by plugin authors. + + + Parameters: ~ + {mod} (table) Module + + +telescope.extensions() *telescope.extensions()* + Use telescope.extensions to reference any extensions within your + configuration. + While the docs currently generate this as a function, it's actually a + table. Sorry. + + + + +================================================================================ +COMMAND *telescope.command* + +Telescope commands can be called through two apis, the lua api and the viml +api. + +The lua api is the more direct way to interact with Telescope, as you directly +call the lua functions that Telescope defines. It can be called in a lua file +using commands like: +`require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})` +If you want to use this api from a vim file you should prepend `lua` to the +command, as below: +`lua require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})` +If you want to use this api from a neovim command line you should prepend +`:lua` to the command, as below: +`:lua require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})` + +The viml api is more indirect, as first the command must be parsed to the +relevant lua equivalent, which brings some limitations. The viml api can be +called using commands like: +`:Telescope find_files hidden=true layout_config={"prompt_position":"top"}` +This involves setting options using an `=` and using viml syntax for lists and +dictionaries when the corresponding lua function requires a table. + +One limitation of the viml api is that there can be no spaces in any of the +options. For example, if you want to use the `cwd` option for `find_files` to +specify that you only want to search within the folder `/foo bar/subfolder/` +you could not do that using the viml api, as the path name contains a space. +Similarly, you could NOT set the `prompt_position` to `"top"` using the +following command: +`:Telescope find_files layout_config={ "prompt_position" : "top" }` +as there are spaces in the option. + + + +================================================================================ +BUILTIN *telescope.builtin* + +Telescope Builtins is a collection of community maintained pickers to support +common workflows. It can be used as reference when writing PRs, Telescope +extensions, your own custom pickers, or just as a discovery tool for all of the +amazing pickers already shipped with Telescope! + +Any of these functions can just be called directly by doing: + +:lua require('telescope.builtin').$NAME_OF_PICKER() + +To use any of Telescope's default options or any picker-specific options, call +your desired picker by passing a lua table to the picker with all of the +options you want to use. Here's an example with the live_grep picker: + +> + :lua require('telescope.builtin').live_grep({ + prompt_title = 'find string in open buffers...', + grep_open_files = true + }) + -- or with dropdown theme + :lua require('telescope.builtin').find_files(require('telescope.themes').get_dropdown{ + previewer = false + }) +< + +builtin.live_grep({opts}) *telescope.builtin.live_grep()* + Search for a string and get results live as you type, respects .gitignore + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) root dir to search from + (default: cwd, use + utils.buffer_dir() to search + relative to open buffer) + {grep_open_files} (boolean) if true, restrict search to + open files only, mutually + exclusive with `search_dirs` + {search_dirs} (table) directory/directories/files to + search, mutually exclusive + with `grep_open_files` + {glob_pattern} (string|table) argument to be used with + `--glob`, e.g. "*.toml", can + use the opposite "!*.toml" + {type_filter} (string) argument to be used with + `--type`, e.g. "rust", see `rg + --type-list` + {additional_args} (function|table) additional arguments to be + passed on. Can be fn(opts) -> + tbl + {max_results} (number) define a upper result value + {disable_coordinates} (boolean) don't show the line & row + numbers (default: false) + + +builtin.grep_string({opts}) *telescope.builtin.grep_string()* + Searches for the string under your cursor in your current working directory + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) root dir to search from + (default: cwd, use + utils.buffer_dir() to search + relative to open buffer) + {search} (string) the query to search + {grep_open_files} (boolean) if true, restrict search to + open files only, mutually + exclusive with `search_dirs` + {search_dirs} (table) directory/directories/files to + search, mutually exclusive + with `grep_open_files` + {use_regex} (boolean) if true, special characters + won't be escaped, allows for + using regex (default: false) + {word_match} (string) can be set to `-w` to enable + exact word matches + {additional_args} (function|table) additional arguments to be + passed on. Can be fn(opts) -> + tbl + {disable_coordinates} (boolean) don't show the line and row + numbers (default: false) + {only_sort_text} (boolean) only sort the text, not the + file, line or row (default: + false) + + +builtin.find_files({opts}) *telescope.builtin.find_files()* + Search for files (respecting .gitignore) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) root dir to search from (default: + cwd, use utils.buffer_dir() to + search relative to open buffer) + {find_command} (function|table) cmd to use for the search. Can be + a fn(opts) -> tbl (default: + autodetect) + {follow} (boolean) if true, follows symlinks (i.e. + uses `-L` flag for the `find` + command) + {hidden} (boolean) determines whether to show hidden + files or not (default: false) + {no_ignore} (boolean) show files ignored by .gitignore, + .ignore, etc. (default: false) + {no_ignore_parent} (boolean) show files ignored by .gitignore, + .ignore, etc. in parent dirs. + (default: false) + {search_dirs} (table) directory/directories/files to + search + {search_file} (string) specify a filename to search for + + +builtin.fd() *telescope.builtin.fd()* + This is an alias for the `find_files` picker + + + +builtin.treesitter() *telescope.builtin.treesitter()* + Lists function names, variables, and other symbols from treesitter queries + - Default keymaps: + - ``: show autocompletion menu to prefilter your query by kind of ts + node you want to see (i.e. `:var:`) + + + Options: ~ + {show_line} (boolean) if true, shows the row:column that the + result is found at (default: true) + {bufnr} (number) specify the buffer number where + treesitter should run. (default: + current buffer) + {symbol_highlights} (table) string -> string. Matches symbol with + hl_group + + +builtin.current_buffer_fuzzy_find({opts}) *telescope.builtin.current_buffer_fuzzy_find()* + Live fuzzy search inside of the currently open buffer + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {skip_empty_lines} (boolean) if true we dont display empty lines + (default: false) + + +builtin.tags({opts}) *telescope.builtin.tags()* + Lists tags in current directory with tag location file preview (users are + required to run ctags -R to generate tags or update when introducing new + changes) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) root dir to search from (default: cwd, use + utils.buffer_dir() to search relative to + open buffer) + {ctags_file} (string) specify a particular ctags file to use + {show_line} (boolean) if true, shows the content of the line the + tag is found on in the picker (default: + true) + {only_sort_tags} (boolean) if true we will only sort tags (default: + false) + {fname_width} (number) defines the width of the filename section + (default: 30) + + +builtin.current_buffer_tags({opts}) *telescope.builtin.current_buffer_tags()* + Lists all of the tags for the currently open buffer, with a preview + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) root dir to search from (default: cwd, use + utils.buffer_dir() to search relative to + open buffer) + {ctags_file} (string) specify a particular ctags file to use + {show_line} (boolean) if true, shows the content of the line the + tag is found on in the picker (default: + true) + {only_sort_tags} (boolean) if true we will only sort tags (default: + false) + {fname_width} (number) defines the width of the filename section + (default: 30) + + +builtin.git_files({opts}) *telescope.builtin.git_files()* + Fuzzy search for files tracked by Git. This command lists the output of the + `git ls-files` command, respects .gitignore + - Default keymaps: + - ``: opens the currently selected file + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or + the cwd (important for submodule) + (default: true) + {show_untracked} (boolean) if true, adds `--others` flag to + command and shows untracked files + (default: false) + {recurse_submodules} (boolean) if true, adds the + `--recurse-submodules` flag to command + (default: false) + {git_command} (table) command that will be exectued. + {"git","ls-files","--exclude-standard","--cached"} + + +builtin.git_commits({opts}) *telescope.builtin.git_commits()* + Lists commits for current directory with diff preview + - Default keymaps: + - ``: checks out the currently selected commit + - `m`: resets current branch to selected commit using mixed mode + - `s`: resets current branch to selected commit using soft mode + - `h`: resets current branch to selected commit using hard mode + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {git_command} (table) command that will be exectued. + {"git","log","--pretty=oneline","--abbrev-commit","--","."} + + +builtin.git_bcommits({opts}) *telescope.builtin.git_bcommits()* + Lists commits for current buffer with diff preview + - Default keymaps or your overriden `select_` keys: + - ``: checks out the currently selected commit + - ``: opens a diff in a vertical split + - ``: opens a diff in a horizontal split + - ``: opens a diff in a new tab + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {current_file} (string) specify the current file that should be used + for bcommits (default: current buffer) + {git_command} (table) command that will be exectued. + {"git","log","--pretty=oneline","--abbrev-commit"} + + +builtin.git_branches({opts}) *telescope.builtin.git_branches()* + List branches for current directory, with output from `git log --oneline` + shown in the preview window + - Default keymaps: + - ``: checks out the currently selected branch + - ``: tracks currently selected branch + - ``: rebases currently selected branch + - ``: creates a new branch, with confirmation prompt before creation + - ``: deletes the currently selected branch, with confirmation + prompt before deletion + - ``: merges the currently selected branch, with confirmation prompt + before deletion + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {pattern} (string) specify the pattern to match all refs + + +builtin.git_status({opts}) *telescope.builtin.git_status()* + Lists git status for current directory + - Default keymaps: + - ``: stages or unstages the currently selected file + - ``: opens the currently selected file + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {git_icons} (table) string -> string. Matches name with icon + (see source code, make_entry.lua + git_icon_defaults) + + +builtin.git_stash({opts}) *telescope.builtin.git_stash()* + Lists stash items in current repository + - Default keymaps: + - ``: runs `git apply` for currently selected stash + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {show_branch} (boolean) if we should display the branch name for git + stash entries (default: true) + + +builtin.builtin({opts}) *telescope.builtin.builtin()* + Lists all of the community maintained pickers built into Telescope + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {include_extensions} (boolean) if true will show the pickers of the + installed extensions (default: false) + {use_default_opts} (boolean) if the selected picker should use its + default options (default: false) + + +builtin.resume({opts}) *telescope.builtin.resume()* + Opens the previous picker in the identical state (incl. multi selections) + - Notes: + - Requires `cache_picker` in setup or when having invoked pickers, see + |telescope.defaults.cache_picker| + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cache_index} (number) what picker to resume, where 1 denotes most + recent (default: 1) + + +builtin.pickers({opts}) *telescope.builtin.pickers()* + Opens a picker over previously cached pickers in their preserved states + (incl. multi selections) + - Default keymaps: + - ``: delete the selected cached picker + - Notes: + - Requires `cache_picker` in setup or when having invoked pickers, see + |telescope.defaults.cache_picker| + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.planets({opts}) *telescope.builtin.planets()* + Use the telescope... + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_pluto} (boolean) we love pluto (default: false, because its a + hidden feature) + {show_moon} (boolean) we love the moon (default: false, because its + a hidden feature) + + +builtin.symbols({opts}) *telescope.builtin.symbols()* + Lists symbols inside of `data/telescope-sources/*.json` found in your + runtime path or found in `stdpath("data")/telescope/symbols/*.json`. The + second path can be customized. We provide a couple of default symbols which + can be found in https://github.com/nvim-telescope/telescope-symbols.nvim. + This repos README also provides more information about the format in which + the symbols have to be. + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {symbol_path} (string) specify the second path. Default: + `stdpath("data")/telescope/symbols/*.json` + {sources} (table) specify a table of sources you want to load + this time + + +builtin.commands({opts}) *telescope.builtin.commands()* + Lists available plugin/user commands and runs them on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_buf_command} (boolean) show buf local command (Default: true) + + +builtin.quickfix({opts}) *telescope.builtin.quickfix()* + Lists items in the quickfix list, jumps to location on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + {fname_width} (number) defines the width of the filename section + (default: 30) + {nr} (number) specify the quickfix list number + + +builtin.quickfixhistory({opts}) *telescope.builtin.quickfixhistory()* + Lists all quickfix lists in your history and open them with + `builtin.quickfix`. It seems that neovim only keeps the full history for 10 + lists + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.loclist({opts}) *telescope.builtin.loclist()* + Lists items from the current window's location list, jumps to location on + `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + {fname_width} (number) defines the width of the filename section + (default: 30) + + +builtin.oldfiles({opts}) *telescope.builtin.oldfiles()* + Lists previously open files, opens on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {only_cwd} (boolean) show only files in the cwd (default: false) + {cwd_only} (boolean) alias for only_cwd + + +builtin.command_history({opts}) *telescope.builtin.command_history()* + Lists commands that were executed recently, and reruns them on `` + - Default keymaps: + - ``: open the command line with the text of the currently selected + result populated in it + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {filter_fn} (function) filter fn(cmd:string). true if the history + command should be presented. + + +builtin.search_history({opts}) *telescope.builtin.search_history()* + Lists searches that were executed recently, and reruns them on `` + - Default keymaps: + - ``: open a search window with the text of the currently selected + search result populated in it + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.vim_options({opts}) *telescope.builtin.vim_options()* + Lists vim options, allows you to edit the current value on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.help_tags({opts}) *telescope.builtin.help_tags()* + Lists available help tags and opens a new window with the relevant help + info on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {lang} (string) specify language (default: vim.o.helplang) + {fallback} (boolean) fallback to en if language isn't installed + (default: true) + + +builtin.man_pages({opts}) *telescope.builtin.man_pages()* + Lists manpage entries, opens them in a help window on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {sections} (table) a list of sections to search, use `{ "ALL" }` + to search in all sections (default: { "1" }) + {man_cmd} (function) that returns the man command. (Default: + `apropos ""` on linux, `apropos " "` on macos) + + +builtin.reloader({opts}) *telescope.builtin.reloader()* + Lists lua modules and reloads them on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {column_len} (number) define the max column len for the module name + (default: dynamic, longest module name) + + +builtin.buffers({opts}) *telescope.builtin.buffers()* + Lists open buffers in current neovim instance, opens selected buffer on + `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_all_buffers} (boolean) if true, show all buffers, + including unloaded buffers + (default: true) + {ignore_current_buffer} (boolean) if true, don't show the current + buffer in the list (default: false) + {only_cwd} (boolean) if true, only show buffers in the + current working directory (default: + false) + {cwd_only} (boolean) alias for only_cwd + {sort_lastused} (boolean) Sorts current and last buffer to + the top and selects the lastused + (default: false) + {sort_mru} (boolean) Sorts all buffers after most recent + used. Not just the current and last + one (default: false) + {bufnr_width} (number) Defines the width of the buffer + numbers in front of the filenames + (default: dynamic) + + +builtin.colorscheme({opts}) *telescope.builtin.colorscheme()* + Lists available colorschemes and applies them on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {enable_preview} (boolean) if true, will preview the selected color + + +builtin.marks({opts}) *telescope.builtin.marks()* + Lists vim marks and their value, jumps to the mark on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.registers({opts}) *telescope.builtin.registers()* + Lists vim registers, pastes the contents of the register on `` + - Default keymaps: + - ``: edit the contents of the currently selected register + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.keymaps({opts}) *telescope.builtin.keymaps()* + Lists normal mode keymappings, runs the selected keymap on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {modes} (table) a list of short-named keymap modes to search + (default: { "n", "i", "c", "x" }) + {show_plug} (boolean) if true, the keymaps for which the lhs contains + "" are also shown (default: true) + + +builtin.filetypes({opts}) *telescope.builtin.filetypes()* + Lists all available filetypes, sets currently open buffer's filetype to + selected filetype in Telescope on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.highlights({opts}) *telescope.builtin.highlights()* + Lists all available highlights + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.autocommands({opts}) *telescope.builtin.autocommands()* + Lists vim autocommands and goes to their declaration on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.spell_suggest({opts}) *telescope.builtin.spell_suggest()* + Lists spelling suggestions for the current word under the cursor, replaces + word with selected suggestion on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + +builtin.tagstack({opts}) *telescope.builtin.tagstack()* + Lists the tag stack for the current window, jumps to tag on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + {fname_width} (number) defines the width of the filename section + (default: 30) + + +builtin.jumplist({opts}) *telescope.builtin.jumplist()* + Lists items from Vim's jumplist, jumps to location on `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + {fname_width} (number) defines the width of the filename section + (default: 30) + + +builtin.lsp_references({opts}) *telescope.builtin.lsp_references()* + Lists LSP references for word under the cursor, jumps to reference on + `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {include_declaration} (boolean) include symbol declaration in the + lsp references (default: true) + {include_current_line} (boolean) include current line (default: + false) + {jump_type} (string) how to goto reference if there is + only one, values: "tab", "split", + "vsplit", "never" + {fname_width} (number) defines the width of the filename + section (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_incoming_calls({opts}) *telescope.builtin.lsp_incoming_calls()* + Lists LSP incoming calls for word under the cursor, jumps to reference on + `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {fname_width} (number) defines the width of the filename section + (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_outgoing_calls({opts}) *telescope.builtin.lsp_outgoing_calls()* + Lists LSP outgoing calls for word under the cursor, jumps to reference on + `` + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {fname_width} (number) defines the width of the filename section + (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_definitions({opts}) *telescope.builtin.lsp_definitions()* + Goto the definition of the word under the cursor, if there's only one, + otherwise show all options in Telescope + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {jump_type} (string) how to goto definition if there is only one, + values: "tab", "split", "vsplit", "never" + {fname_width} (number) defines the width of the filename section + (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_type_definitions({opts}) *telescope.builtin.lsp_type_definitions()* + Goto the definition of the type of the word under the cursor, if there's + only one, otherwise show all options in Telescope + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {jump_type} (string) how to goto definition if there is only one, + values: "tab", "split", "vsplit", "never" + {fname_width} (number) defines the width of the filename section + (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_implementations({opts}) *telescope.builtin.lsp_implementations()* + Goto the implementation of the word under the cursor if there's only one, + otherwise show all options in Telescope + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {jump_type} (string) how to goto implementation if there is only + one, values: "tab", "split", "vsplit", + "never" + {fname_width} (number) defines the width of the filename section + (default: 30) + {show_line} (boolean) show results text (default: true) + {trim_text} (boolean) trim results text (default: false) + + +builtin.lsp_document_symbols({opts}) *telescope.builtin.lsp_document_symbols()* + Lists LSP document symbols in the current buffer + - Default keymaps: + - ``: show autocompletion menu to prefilter your query by type of + symbol you want to see (i.e. `:variable:`) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {fname_width} (number) defines the width of the filename + section (default: 30) + {show_line} (boolean) if true, shows the content of the + line the tag is found on (default: + false) + {symbols} (string|table) filter results by symbol kind(s) + {ignore_symbols} (string|table) list of symbols to ignore + {symbol_highlights} (table) string -> string. Matches symbol + with hl_group + + +builtin.lsp_workspace_symbols({opts}) *telescope.builtin.lsp_workspace_symbols()* + Lists LSP document symbols in the current workspace + - Default keymaps: + - ``: show autocompletion menu to prefilter your query by type of + symbol you want to see (i.e. `:variable:`) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {query} (string) for what to query the workspace + (default: "") + {fname_width} (number) defines the width of the filename + section (default: 30) + {show_line} (boolean) if true, shows the content of the + line the tag is found on (default: + false) + {symbols} (string|table) filter results by symbol kind(s) + {ignore_symbols} (string|table) list of symbols to ignore + {symbol_highlights} (table) string -> string. Matches symbol + with hl_group + + +builtin.lsp_dynamic_workspace_symbols({opts}) *telescope.builtin.lsp_dynamic_workspace_symbols()* + Dynamically lists LSP for all workspace symbols + - Default keymaps: + - ``: show autocompletion menu to prefilter your query by type of + symbol you want to see (i.e. `:variable:`) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {fname_width} (number) defines the width of the filename + section (default: 30) + {show_line} (boolean) if true, shows the content of the + line the symbol is found on + (default: false) + {symbols} (string|table) filter results by symbol kind(s) + {ignore_symbols} (string|table) list of symbols to ignore + {symbol_highlights} (table) string -> string. Matches symbol + with hl_group + + +builtin.diagnostics({opts}) *telescope.builtin.diagnostics()* + Lists diagnostics + - Fields: + - `All severity flags can be passed as `string` or `number` as per + `:vim.diagnostic.severity:` + - Default keymaps: + - ``: show autocompletion menu to prefilter your query with the + diagnostic you want to see (i.e. `:warning:`) + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {bufnr} (number|nil) Buffer number to get diagnostics + from. Use 0 for current buffer or + nil for all buffers + {severity} (string|number) filter diagnostics by severity name + (string) or id (number) + {severity_limit} (string|number) keep diagnostics equal or more + severe wrt severity name (string) + or id (number) + {severity_bound} (string|number) keep diagnostics equal or less + severe wrt severity name (string) + or id (number) + {root_dir} (string|boolean) if set to string, get diagnostics + only for buffers under this dir + otherwise cwd + {no_unlisted} (boolean) if true, get diagnostics only for + listed buffers + {no_sign} (boolean) hide DiagnosticSigns from Results + (default: false) + {line_width} (number) set length of diagnostic entry text + in Results + {namespace} (number) limit your diagnostics to a + specific namespace + + + +================================================================================ +THEMES *telescope.themes* + +Themes are ways to combine several elements of styling together. + +They are helpful for managing the several different UI aspects for telescope +and provide a simple interface for users to get a particular "style" of picker. + +themes.get_dropdown() *telescope.themes.get_dropdown()* + Dropdown style theme. + + Usage: + > + `local builtin = require('telescope.builtin')` + `local themes = require('telescope.themes')` + `builtin.find_files(themes.get_dropdown())` +< + + + +themes.get_cursor() *telescope.themes.get_cursor()* + Cursor style theme. + + Usage: + > + + `local builtin = require('telescope.builtin')` + `local themes = require('telescope.themes')` + `builtin.lsp_references(themes.get_cursor())` +< + + + +themes.get_ivy() *telescope.themes.get_ivy()* + Ivy style theme. + + Usage: + > + `local builtin = require('telescope.builtin')` + `local themes = require('telescope.themes')` + `builtin.find_files(themes.get_ivy())` +< + + + + +================================================================================ +MAPPINGS *telescope.mappings* + +|telescope.mappings| is used to configure the keybindings within a telescope +picker. These keybinds are only local to the picker window and will be cleared +once you exit the picker. + +We provide multiple different ways of configuring, as described below, to +provide an easy to use experience for changing the default behavior of +telescope or extending for your own purposes. + +To see many of the builtin actions that you can use as values for this table, +see |telescope.actions| + +Format is: +> + { + mode = { ..keys } + } +< + +where {mode} is the one character letter for a mode ('i' for insert, 'n' for +normal). + +For example: +> + mappings = { + i = { + [""] = require('telescope.actions').close, + }, + } +< + +To disable a keymap, put `[map] = false` +For example: +> + { + ..., + [""] = false, + ..., + } +< +Into your config. + +To override behavior of a key, simply set the value to be a function (either by +requiring an action or by writing your own function) +> + { + ..., + [""] = require('telescope.actions').select_default, + ..., + } +< + +If the function you want is part of `telescope.actions`, then you can simply +give a string. For example, the previous option is equivalent to: +> + { + ..., + [""] = "select_default", + ..., + } +< + +You can also add other mappings using tables with `type = "command"`. For +example: +> + { + ..., + ["jj"] = { "", type = "command" }, + ["kk"] = { "echo \"Hello, World!\"", type = "command" },) + ..., + } +< + +You can also add additional options for mappings of any type ("action" and +"command"). For example: +> + { + ..., + [""] = { + actions.move_selection_next, type = "action", + opts = { nowait = true, silent = true } + }, + ..., + } +< + +There are three main places you can configure |telescope.mappings|. These are +ordered from the lowest priority to the highest priority. + +1. |telescope.defaults.mappings| +2. In the |telescope.setup()| table, inside a picker with a given name, use the + `mappings` key +> + require("telescope").setup { + pickers = { + find_files = { + mappings = { + n = { + ["kj"] = "close", + }, + }, + }, + }, + } +< +3. `attach_mappings` function for a particular picker. +> + require("telescope.builtin").find_files { + attach_mappings = function(_, map) + map("i", "asdf", function(_prompt_bufnr) + print "You typed asdf" + end) + + map({"i", "n"}, "", function(_prompt_bufnr) + print "You typed " + end) + + -- needs to return true if you want to map default_mappings and + -- false if not + return true + end, + } +< + + +================================================================================ +LAYOUT *telescope.layout* + +The layout of telescope pickers can be adjusted using the +|telescope.defaults.layout_strategy| and |telescope.defaults.layout_config| +options. For example, the following configuration changes the default layout +strategy and the default size of the picker: +> + require('telescope').setup{ + defaults = { + layout_strategy = 'vertical', + layout_config = { height = 0.95 }, + }, + } +< + + +──────────────────────────────────────────────────────────────────────────────── + +Layout strategies are different functions to position telescope. + +All layout strategies are functions with the following signature: + +> + function(picker, columns, lines, layout_config) + -- Do some calculations here... + return { + preview = preview_configuration + results = results_configuration, + prompt = prompt_configuration, + } + end +< + + Parameters: ~ + - picker : A Picker object. (docs coming soon) + - columns : (number) Columns in the vim window + - lines : (number) Lines in the vim window + - layout_config : (table) The configuration values specific to the picker. + + +This means you can create your own layout strategy if you want! Just be aware +for now that we may change some APIs or interfaces, so they may break if you +create your own. + +A good method for creating your own would be to copy one of the strategies that +most resembles what you want from +"./lua/telescope/pickers/layout_strategies.lua" in the telescope repo. + + +layout_strategies.horizontal() *telescope.layout.horizontal()* + Horizontal layout has two columns, one for the preview and one for the + prompt and results. + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ┌───────────────────┐┌───────────────────┐ │ + │ │ ││ │ │ + │ │ ││ │ │ + │ │ ││ │ │ + │ │ Results ││ │ │ + │ │ ││ Preview │ │ + │ │ ││ │ │ + │ │ ││ │ │ + │ └───────────────────┘│ │ │ + │ ┌───────────────────┐│ │ │ + │ │ Prompt ││ │ │ + │ └───────────────────┘└───────────────────┘ │ + │ │ + └──────────────────────────────────────────────────┘ + + `picker.layout_config` shared options: + - anchor: + - Which edge/corner to pin the picker to + - See |resolver.resolve_anchor_pos()| + - height: + - How tall to make Telescope's entire layout + - See |resolver.resolve_height()| + - mirror: Flip the location of the results/prompt and preview windows + - prompt_position: + - Where to place prompt window. + - Available Values: 'bottom', 'top' + - scroll_speed: The number of lines to scroll through the previewer + - width: + - How wide to make Telescope's entire layout + - See |resolver.resolve_width()| + + `picker.layout_config` unique options: + - preview_cutoff: When columns are less than this value, the preview will be disabled + - preview_width: + - Change the width of Telescope's preview window + - See |resolver.resolve_width()| + + +layout_strategies.center() *telescope.layout.center()* + Centered layout with a combined block of the prompt and results aligned to + the middle of the screen. The preview window is then placed in the + remaining space above or below, according to `anchor` or `mirror`. + Particularly useful for creating dropdown menus (see |telescope.themes| and + |themes.get_dropdown()|). + + Note that vertical anchoring, i.e. `anchor` containing `"N"` or `"S"`, will + override `mirror` config. For `"N"` anchoring preview will be placed below + prompt/result block. For `"S"` anchoring preview will be placed above + prompt/result block. For horizontal only anchoring preview will be placed + according to `mirror` config, default is above the prompt/result block. + + ┌──────────────────────────────────────────────────┐ + │ ┌────────────────────────────────────────┐ │ + │ | Preview | │ + │ | Preview | │ + │ └────────────────────────────────────────┘ │ + │ ┌────────────────────────────────────────┐ │ + │ | Prompt | │ + │ ├────────────────────────────────────────┤ │ + │ | Result | │ + │ | Result | │ + │ └────────────────────────────────────────┘ │ + │ │ + │ │ + │ │ + │ │ + └──────────────────────────────────────────────────┘ + + `picker.layout_config` shared options: + - anchor: + - Which edge/corner to pin the picker to + - See |resolver.resolve_anchor_pos()| + - height: + - How tall to make Telescope's entire layout + - See |resolver.resolve_height()| + - mirror: Flip the location of the results/prompt and preview windows + - prompt_position: + - Where to place prompt window. + - Available Values: 'bottom', 'top' + - scroll_speed: The number of lines to scroll through the previewer + - width: + - How wide to make Telescope's entire layout + - See |resolver.resolve_width()| + + `picker.layout_config` unique options: + - preview_cutoff: When lines are less than this value, the preview will be disabled + + +layout_strategies.cursor() *telescope.layout.cursor()* + Cursor layout dynamically positioned below the cursor if possible. If there + is no place below the cursor it will be placed above. + + ┌──────────────────────────────────────────────────┐ + │ │ + │ █ │ + │ ┌──────────────┐┌─────────────────────┐ │ + │ │ Prompt ││ Preview │ │ + │ ├──────────────┤│ Preview │ │ + │ │ Result ││ Preview │ │ + │ │ Result ││ Preview │ │ + │ └──────────────┘└─────────────────────┘ │ + │ █ │ + │ │ + │ │ + │ │ + │ │ + │ │ + └──────────────────────────────────────────────────┘ + + + +layout_strategies.vertical() *telescope.layout.vertical()* + Vertical layout stacks the items on top of each other. Particularly useful + with thinner windows. + + ┌──────────────────────────────────────────────────┐ + │ │ + │ ┌────────────────────────────────────────┐ │ + │ | Preview | │ + │ | Preview | │ + │ | Preview | │ + │ └────────────────────────────────────────┘ │ + │ ┌────────────────────────────────────────┐ │ + │ | Result | │ + │ | Result | │ + │ └────────────────────────────────────────┘ │ + │ ┌────────────────────────────────────────┐ │ + │ | Prompt | │ + │ └────────────────────────────────────────┘ │ + │ │ + └──────────────────────────────────────────────────┘ + + `picker.layout_config` shared options: + - anchor: + - Which edge/corner to pin the picker to + - See |resolver.resolve_anchor_pos()| + - height: + - How tall to make Telescope's entire layout + - See |resolver.resolve_height()| + - mirror: Flip the location of the results/prompt and preview windows + - prompt_position: + - Where to place prompt window. + - Available Values: 'bottom', 'top' + - scroll_speed: The number of lines to scroll through the previewer + - width: + - How wide to make Telescope's entire layout + - See |resolver.resolve_width()| + + `picker.layout_config` unique options: + - preview_cutoff: When lines are less than this value, the preview will be disabled + - preview_height: + - Change the height of Telescope's preview window + - See |resolver.resolve_height()| + + +layout_strategies.flex() *telescope.layout.flex()* + Flex layout swaps between `horizontal` and `vertical` strategies based on + the window width + - Supports |layout_strategies.vertical| or |layout_strategies.horizontal| + features + + + `picker.layout_config` shared options: + - anchor: + - Which edge/corner to pin the picker to + - See |resolver.resolve_anchor_pos()| + - height: + - How tall to make Telescope's entire layout + - See |resolver.resolve_height()| + - mirror: Flip the location of the results/prompt and preview windows + - prompt_position: + - Where to place prompt window. + - Available Values: 'bottom', 'top' + - scroll_speed: The number of lines to scroll through the previewer + - width: + - How wide to make Telescope's entire layout + - See |resolver.resolve_width()| + + `picker.layout_config` unique options: + - flip_columns: The number of columns required to move to horizontal mode + - flip_lines: The number of lines required to move to horizontal mode + - horizontal: Options to pass when switching to horizontal layout + - vertical: Options to pass when switching to vertical layout + + +layout_strategies.bottom_pane() *telescope.layout.bottom_pane()* + Bottom pane can be used to create layouts similar to "ivy". + + For an easy ivy configuration, see |themes.get_ivy()| + + + + +================================================================================ +RESOLVE *telescope.resolve* + +Provides "resolver functions" to allow more customisable inputs for options. + +resolver.resolve_height() *telescope.resolve.resolve_height()* + Converts input to a function that returns the height. The input must take + one of five forms: + 1. 0 <= number < 1 + This means total height as a percentage. + 2. 1 <= number + This means total height as a fixed number. + 3. function + Must have signature: function(self, max_columns, max_lines): number + 4. table of the form: { val, max = ..., min = ... } + val has to be in the first form 0 <= val < 1 and only one is given, + `min` or `max` as fixed number + 5. table of the form: {padding = `foo`} + where `foo` has one of the previous three forms. + The height is then set to be the remaining space after padding. For + example, if the window has height 50, and the input is {padding = 5}, + the height returned will be `40 = 50 - 2*5` + + The returned function will have signature: function(self, max_columns, + max_lines): number + + + +resolver.resolve_width() *telescope.resolve.resolve_width()* + Converts input to a function that returns the width. The input must take + one of five forms: + 1. 0 <= number < 1 + This means total width as a percentage. + 2. 1 <= number + This means total width as a fixed number. + 3. function + Must have signature: function(self, max_columns, max_lines): number + 4. table of the form: { val, max = ..., min = ... } + val has to be in the first form 0 <= val < 1 and only one is given, + `min` or `max` as fixed number + 5. table of the form: {padding = `foo`} + where `foo` has one of the previous three forms. + The width is then set to be the remaining space after padding. For + example, if the window has width 100, and the input is {padding = 5}, + the width returned will be `90 = 100 - 2*5` + + The returned function will have signature: function(self, max_columns, + max_lines): number + + + +resolver.resolve_anchor_pos() *telescope.resolve.resolve_anchor_pos()* + Calculates the adjustment required to move the picker from the middle of + the screen to an edge or corner. + The `anchor` can be any of the following strings: + - "", "CENTER", "NW", "N", "NE", "E", "SE", "S", "SW", "W" The anchors + have the following meanings: + - "" or "CENTER": + the picker will remain in the middle of the screen. + - Compass directions: + the picker will move to the corresponding edge/corner e.g. "NW" -> "top + left corner", "E" -> "right edge", "S" -> "bottom edge" + + + + +================================================================================ +MAKE_ENTRY *telescope.make_entry* + +Each picker has a finder made up of two parts, the results which are the data +to be displayed, and the entry_maker. These entry_makers are functions returned +from make_entry functions. These will be referrd to as entry_makers in the +following documentation. + +Every entry maker returns a function which accepts the data to be used for an +entry. This function will return an entry table (or nil, meaning skip this +entry) which contains of the - following important keys: +- value any: value key can be anything but still required +- valid bool: is an optional key because it defaults to true but if the key is + set to false it will not be displayed by the picker. (optional) +- ordinal string: is the text that is used for filtering (required) +- display string|function: is either a string of the text that is being + displayed or a function receiving the entry at a later stage, when the entry + is actually being displayed. A function can be useful here if complex + calculation have to be done. `make_entry` can also return a second value a + highlight array which will then apply to the line. Highlight entry in this + array has the following signature `{ { start_col, end_col }, hl_group }` + (required). +- filename string: will be interpreted by the default `` action as open + this file (optional) +- bufnr number: will be interpreted by the default `` action as open this + buffer (optional) +- lnum number: lnum value which will be interpreted by the default `` + action as a jump to this line (optional) +- col number: col value which will be interpreted by the default `` action + as a jump to this column (optional) + +More information on easier displaying, see |telescope.pickers.entry_display| + +TODO: Document something we call `entry_index` + + +================================================================================ +ENTRY_DISPLAY *telescope.pickers.entry_display* + +Entry Display is used to format each entry shown in the result panel. + +Entry Display create() will give us a function based on the configuration of +column widths we pass into it. We then can use this function n times to return +a string based on structured input. + +Note that if you call `create()` inside `make_display` it will be called for +every single entry. So it is suggested to do this outside of `make_display` for +the best performance. + +The create function will use the column widths passed to it in +configaration.items. Each item in that table is the number of characters in the +column. It's also possible for the final column to not have a fixed width, this +will be shown in the configuartion as 'remaining = true'. + +An example of this configuration is shown for the buffers picker +> +local displayer = entry_display.create { + separator = " ", + items = { + { width = opts.bufnr_width }, + { width = 4 }, + { width = icon_width }, + { remaining = true }, + }, +} +< + +This shows 4 columns, the first is defined in the opts as the width we'll use +when display_string the number of the buffer. The second has a fixed width of 4 +and the 3rd column's widht will be decided by the width of the icons we use. +The fourth column will use the remaining space. Finally we have also defined +the seperator between each column will be the space " ". + +An example of how the display reference will be used is shown, again for the +buffers picker: +> +return displayer { + { entry.bufnr, "TelescopeResultsNumber" }, + { entry.indicator, "TelescopeResultsComment" }, + { icon, hl_group }, + display_bufname .. ":" .. entry.lnum, +} +< + +There are two types of values each column can have. Either a simple String or a +table containing the String as well as the hl_group. + +The displayer can return values, string and an optional highlights. String is +all the text to be displayed for this entry as a single string. If parts of the +string are to be highlighted they will be described in the highlights table. + +For better understanding of how create() and displayer are used it's best to +look at the code in make_entry.lua. + + +================================================================================ +UTILS *telescope.utils* + +Utilities for writing telescope pickers + +utils.transform_path({opts}, {path}) *telescope.utils.transform_path()* + Transform path is a util function that formats a path based on path_display + found in `opts` or the default value from config. It is meant to be used in + make_entry to have a uniform interface for builtins as well as extensions + utilizing the same user configuration Note: It is only supported inside + `make_entry`/`make_display` the use of this function outside of telescope + might yield to undefined behavior and will not be addressed by us + + + Parameters: ~ + {opts} (table) The opts the users passed into the picker. Might + contains a path_display key + {path} (string) The path that should be formated + + Return: ~ + string: The transformed path ready to be displayed + + +utils.notify({funname}, {opts}) *telescope.utils.notify()* + Telescope Wrapper around vim.notify + + + Parameters: ~ + {funname} (string) name of the function that will be + {opts} (table) opts.level string, opts.msg string, opts.once bool + + + +================================================================================ +ACTIONS *telescope.actions* + +Actions functions that are useful for people creating their own mappings. + +Actions can be either normal functions that expect the prompt_bufnr as first +argument (1) or they can be a custom telescope type called "action" (2). + +(1) The `prompt_bufnr` of a normal function denotes the identifier of your +picker which can be used to access the picker state. In practice, users most +commonly access from both picker and global state via the following: +> + -- for utility functions + local action_state = require "telescope.actions.state" + + local actions = {} + actions.do_stuff = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) -- picker state + local entry = action_state.get_selected_entry() + end +< + +See |telescope.actions.state| for more information. + +(2) To transform a module of functions into a module of "action"s, you need to +do the following: +> + local transform_mod = require("telescope.actions.mt").transform_mod + + local mod = {} + mod.a1 = function(prompt_bufnr) + -- your code goes here + -- You can access the picker/global state as described above in (1). + end + + mod.a2 = function(prompt_bufnr) + -- your code goes here + end + mod = transform_mod(mod) + + -- Now the following is possible. This means that actions a2 will be executed + -- after action a1. You can chain as many actions as you want. + local action = mod.a1 + mod.a2 + action(bufnr) +< + +Another interesing thing to do is that these actions now have functions you can +call. These functions include `:replace(f)`, `:replace_if(f, c)`, +`replace_map(tbl)` and `enhance(tbl)`. More information on these functions can +be found in the `developers.md` and `lua/tests/automated/action_spec.lua` file. + +actions.move_selection_next({prompt_bufnr}) *telescope.actions.move_selection_next()* + Move the selection to the next entry + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_selection_previous({prompt_bufnr}) *telescope.actions.move_selection_previous()* + Move the selection to the previous entry + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_selection_worse({prompt_bufnr}) *telescope.actions.move_selection_worse()* + Move the selection to the entry that has a worse score + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_selection_better({prompt_bufnr}) *telescope.actions.move_selection_better()* + Move the selection to the entry that has a better score + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_to_top({prompt_bufnr}) *telescope.actions.move_to_top()* + Move to the top of the picker + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_to_middle({prompt_bufnr}) *telescope.actions.move_to_middle()* + Move to the middle of the picker + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.move_to_bottom({prompt_bufnr}) *telescope.actions.move_to_bottom()* + Move to the bottom of the picker + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.add_selection({prompt_bufnr}) *telescope.actions.add_selection()* + Add current entry to multi select + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.remove_selection({prompt_bufnr}) *telescope.actions.remove_selection()* + Remove current entry from multi select + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.toggle_selection({prompt_bufnr}) *telescope.actions.toggle_selection()* + Toggle current entry status for multi select + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_all({prompt_bufnr}) *telescope.actions.select_all()* + Multi select all entries. + - Note: selected entries may include results not visible in the results + popup. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.drop_all({prompt_bufnr}) *telescope.actions.drop_all()* + Drop all entries from the current multi selection. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.toggle_all({prompt_bufnr}) *telescope.actions.toggle_all()* + Toggle multi selection for all entries. + - Note: toggled entries may include results not visible in the results + popup. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.preview_scrolling_up({prompt_bufnr}) *telescope.actions.preview_scrolling_up()* + Scroll the preview window up + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.preview_scrolling_down({prompt_bufnr}) *telescope.actions.preview_scrolling_down()* + Scroll the preview window down + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.results_scrolling_up({prompt_bufnr}) *telescope.actions.results_scrolling_up()* + Scroll the results window up + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.results_scrolling_down({prompt_bufnr}) *telescope.actions.results_scrolling_down()* + Scroll the results window down + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.center({prompt_bufnr}) *telescope.actions.center()* + Center the cursor in the window, can be used after selecting a file to edit + You can just map `actions.select_default + actions.center` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_default({prompt_bufnr}) *telescope.actions.select_default()* + Perform default action on selection, usually something like + `:edit ` + + i.e. open the selection in the current buffer + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_horizontal({prompt_bufnr}) *telescope.actions.select_horizontal()* + Perform 'horizontal' action on selection, usually something like + `:new ` + + i.e. open the selection in a new horizontal split + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_vertical({prompt_bufnr}) *telescope.actions.select_vertical()* + Perform 'vertical' action on selection, usually something like + `:vnew ` + + i.e. open the selection in a new vertical split + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_tab({prompt_bufnr}) *telescope.actions.select_tab()* + Perform 'tab' action on selection, usually something like + `:tabedit ` + + i.e. open the selection in a new tab + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_drop({prompt_bufnr}) *telescope.actions.select_drop()* + Perform 'drop' action on selection, usually something like + `:drop ` + + i.e. open the selection in a window + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.select_tab_drop({prompt_bufnr}) *telescope.actions.select_tab_drop()* + Perform 'tab drop' action on selection, usually something like + `:tab drop ` + + i.e. open the selection in a new tab + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.file_edit({prompt_bufnr}) *telescope.actions.file_edit()* + Perform file edit on selection, usually something like + `:edit ` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.file_split({prompt_bufnr}) *telescope.actions.file_split()* + Perform file split on selection, usually something like + `:new ` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.file_vsplit({prompt_bufnr}) *telescope.actions.file_vsplit()* + Perform file vsplit on selection, usually something like + `:vnew ` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.file_tab({prompt_bufnr}) *telescope.actions.file_tab()* + Perform file tab on selection, usually something like + `:tabedit ` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.close({prompt_bufnr}) *telescope.actions.close()* + Close the Telescope window, usually used within an action + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions._close({prompt_bufnr}) *telescope.actions._close()* + Close the Telescope window, usually used within an action + Deprecated and no longer needed, does the same as + |telescope.actions.close|. Might be removed in the future + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.edit_command_line({prompt_bufnr}) *telescope.actions.edit_command_line()* + Set a value in the command line and dont run it, making it editable. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.set_command_line({prompt_bufnr}) *telescope.actions.set_command_line()* + Set a value in the command line and run it + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.edit_search_line({prompt_bufnr}) *telescope.actions.edit_search_line()* + Set a value in the search line and dont search for it, making it editable. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.set_search_line({prompt_bufnr}) *telescope.actions.set_search_line()* + Set a value in the search line and search for it + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.edit_register({prompt_bufnr}) *telescope.actions.edit_register()* + Edit a register + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.paste_register({prompt_bufnr}) *telescope.actions.paste_register()* + Paste the selected register into the buffer + + Note: only meant to be used inside builtin.registers + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.insert_symbol({prompt_bufnr}) *telescope.actions.insert_symbol()* + Insert a symbol into the current buffer (while switching to normal mode) + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.insert_symbol_i({prompt_bufnr}) *telescope.actions.insert_symbol_i()* + Insert a symbol into the current buffer and keeping the insert mode. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_create_branch({prompt_bufnr}) *telescope.actions.git_create_branch()* + Create and checkout a new git branch if it doesn't already exist + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_apply_stash({prompt_bufnr}) *telescope.actions.git_apply_stash()* + Applies an existing git stash + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_checkout({prompt_bufnr}) *telescope.actions.git_checkout()* + Checkout an existing git branch + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_switch_branch({prompt_bufnr}) *telescope.actions.git_switch_branch()* + Switch to git branch. + If the branch already exists in local, switch to that. If the branch is + only in remote, create new branch tracking remote and switch to new one. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_track_branch({prompt_bufnr}) *telescope.actions.git_track_branch()* + Tell git to track the currently selected remote branch in Telescope + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_delete_branch({prompt_bufnr}) *telescope.actions.git_delete_branch()* + Delete the currently selected branch + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_merge_branch({prompt_bufnr}) *telescope.actions.git_merge_branch()* + Merge the currently selected branch + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_rebase_branch({prompt_bufnr}) *telescope.actions.git_rebase_branch()* + Rebase to selected git branch + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_reset_mixed({prompt_bufnr}) *telescope.actions.git_reset_mixed()* + Reset to selected git commit using mixed mode + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_reset_soft({prompt_bufnr}) *telescope.actions.git_reset_soft()* + Reset to selected git commit using soft mode + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_reset_hard({prompt_bufnr}) *telescope.actions.git_reset_hard()* + Reset to selected git commit using hard mode + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_checkout_current_buffer({prompt_bufnr}) *telescope.actions.git_checkout_current_buffer()* + Checkout a specific file for a given sha + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.git_staging_toggle({prompt_bufnr}) *telescope.actions.git_staging_toggle()* + Stage/unstage selected file + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.send_selected_to_qflist({prompt_bufnr}) *telescope.actions.send_selected_to_qflist()* + Sends the selected entries to the quickfix list, replacing the previous + entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.add_selected_to_qflist({prompt_bufnr}) *telescope.actions.add_selected_to_qflist()* + Adds the selected entries to the quickfix list, keeping the previous + entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.send_to_qflist({prompt_bufnr}) *telescope.actions.send_to_qflist()* + Sends all entries to the quickfix list, replacing the previous entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.add_to_qflist({prompt_bufnr}) *telescope.actions.add_to_qflist()* + Adds all entries to the quickfix list, keeping the previous entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.send_selected_to_loclist({prompt_bufnr}) *telescope.actions.send_selected_to_loclist()* + Sends the selected entries to the location list, replacing the previous + entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.add_selected_to_loclist({prompt_bufnr}) *telescope.actions.add_selected_to_loclist()* + Adds the selected entries to the location list, keeping the previous + entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.send_to_loclist({prompt_bufnr}) *telescope.actions.send_to_loclist()* + Sends all entries to the location list, replacing the previous entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.add_to_loclist({prompt_bufnr}) *telescope.actions.add_to_loclist()* + Adds all entries to the location list, keeping the previous entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.smart_send_to_qflist({prompt_bufnr}) *telescope.actions.smart_send_to_qflist()* + Sends the selected entries to the quickfix list, replacing the previous + entries. If no entry was selected, sends all entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.smart_add_to_qflist({prompt_bufnr}) *telescope.actions.smart_add_to_qflist()* + Adds the selected entries to the quickfix list, keeping the previous + entries. If no entry was selected, adds all entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.smart_send_to_loclist({prompt_bufnr}) *telescope.actions.smart_send_to_loclist()* + Sends the selected entries to the location list, replacing the previous + entries. If no entry was selected, sends all entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.smart_add_to_loclist({prompt_bufnr}) *telescope.actions.smart_add_to_loclist()* + Adds the selected entries to the location list, keeping the previous + entries. If no entry was selected, adds all entries. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.complete_tag({prompt_bufnr}) *telescope.actions.complete_tag()* + Open completion menu containing the tags which can be used to filter the + results in a faster way + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.cycle_history_next({prompt_bufnr}) *telescope.actions.cycle_history_next()* + Cycle to the next search prompt in the history + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.cycle_history_prev({prompt_bufnr}) *telescope.actions.cycle_history_prev()* + Cycle to the previous search prompt in the history + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.open_qflist({prompt_bufnr}) *telescope.actions.open_qflist()* + Open the quickfix list. It makes sense to use this in combination with one + of the send_to_qflist actions `actions.smart_send_to_qflist + + actions.open_qflist` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.open_loclist({prompt_bufnr}) *telescope.actions.open_loclist()* + Open the location list. It makes sense to use this in combination with one + of the send_to_loclist actions `actions.smart_send_to_qflist + + actions.open_qflist` + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.delete_buffer({prompt_bufnr}) *telescope.actions.delete_buffer()* + Delete the selected buffer or all the buffers selected using multi + selection. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.cycle_previewers_next({prompt_bufnr}) *telescope.actions.cycle_previewers_next()* + Cycle to the next previewer if there is one available. + This action is not mapped on default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.cycle_previewers_prev({prompt_bufnr}) *telescope.actions.cycle_previewers_prev()* + Cycle to the previous previewer if there is one available. + This action is not mapped on default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.remove_selected_picker({prompt_bufnr}) *telescope.actions.remove_selected_picker()* + Removes the selected picker in |builtin.pickers|. + This action is not mapped by default and only intended for + |builtin.pickers|. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.which_key({prompt_bufnr}) *telescope.actions.which_key()* + Display the keymaps of registered actions similar to which-key.nvim. + + - Notes: + - The defaults can be overridden via |action_generate.which_key|. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +actions.to_fuzzy_refine({prompt_bufnr}) *telescope.actions.to_fuzzy_refine()* + Move from a none fuzzy search to a fuzzy one + This action is meant to be used in live_grep and + lsp_dynamic_workspace_symbols + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + + +================================================================================ +ACTIONS_STATE *telescope.actions.state* + +Functions to be used to determine the current state of telescope. + +Generally used from within other |telescope.actions| + +action_state.get_selected_entry() *telescope.actions.state.get_selected_entry()* + Get the current entry + + + +action_state.get_current_line() *telescope.actions.state.get_current_line()* + Gets the current line + + + +action_state.get_current_picker({prompt_bufnr}) *telescope.actions.state.get_current_picker()* + Gets the current picker + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + + +================================================================================ +ACTIONS_SET *telescope.actions.set* + +Telescope action sets are used to provide an interface for managing actions +that all primarily do the same thing, but with slight tweaks. + +For example, when editing files you may want it in the current split, a +vertical split, etc. Instead of making users have to overwrite EACH of those +every time they want to change this behavior, they can instead replace the +`set` itself and then it will work great and they're done. + +action_set.shift_selection({prompt_bufnr}, {change}) *telescope.actions.set.shift_selection()* + Move the current selection of a picker {change} rows. Handles not + overflowing / underflowing the list. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {change} (number) The amount to shift the selection by + + +action_set.select({prompt_bufnr}, {type}) *telescope.actions.set.select()* + Select the current entry. This is the action set to overwrite common + actions by the user. + + By default maps to editing a file. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {type} (string) The type of selection to make + + +action_set.edit({prompt_bufnr}, {command}) *telescope.actions.set.edit()* + Edit a file based on the current selection. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {command} (string) The command to use to open the file. + + +action_set.scroll_previewer({prompt_bufnr}, {direction}) *telescope.actions.set.scroll_previewer()* + Scrolls the previewer up or down. Defaults to a half page scroll, but can + be overridden using the `scroll_speed` option in `layout_config`. See + |telescope.layout| for more details. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {direction} (number) The direction of the scrolling + + +action_set.scroll_results({prompt_bufnr}, {direction}) *telescope.actions.set.scroll_results()* + Scrolls the results up or down. Defaults to a half page scroll, but can be + overridden using the `scroll_speed` option in `layout_config`. See + |telescope.layout| for more details. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {direction} (number) The direction of the scrolling + + + +================================================================================ +ACTIONS_LAYOUT *telescope.actions.layout* + +The layout actions are actions to be used to change the layout of a picker. + +action_layout.toggle_preview({prompt_bufnr}) *telescope.actions.layout.toggle_preview()* + Toggle preview window. + - Note: preview window can be toggled even if preview is set to false. + + This action is not mapped by default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +action_layout.toggle_prompt_position({prompt_bufnr}) *telescope.actions.layout.toggle_prompt_position()* + Toggles the `prompt_position` option between "top" and "bottom". Checks if + `prompt_position` is an option for the current layout. + + This action is not mapped by default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +action_layout.toggle_mirror({prompt_bufnr}) *telescope.actions.layout.toggle_mirror()* + Toggles the `mirror` option between `true` and `false`. Checks if `mirror` + is an option for the current layout. + + This action is not mapped by default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +action_layout.cycle_layout_next({prompt_bufnr}) *telescope.actions.layout.cycle_layout_next()* + Cycles to the next layout in `cycle_layout_list`. + + This action is not mapped by default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + +action_layout.cycle_layout_prev({prompt_bufnr}) *telescope.actions.layout.cycle_layout_prev()* + Cycles to the previous layout in `cycle_layout_list`. + + This action is not mapped by default. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + + +================================================================================ +ACTIONS_UTILS *telescope.actions.utils* + +Utilities to wrap functions around picker selections and entries. + +Generally used from within other |telescope.actions| + +utils.map_entries({prompt_bufnr}, {f}) *telescope.actions.utils.map_entries()* + Apply `f` to the entries of the current picker. + - Notes: + - Mapped entries include all currently filtered results, not just the + visible onces. + - Indices are 1-indexed, whereas rows are 0-indexed. + - Warning: `map_entries` has no return value. + - The below example showcases how to collect results + + Usage: + > + local action_state = require "telescope.actions.state" + local action_utils = require "telescope.actions.utils" + function entry_value_by_row() + local prompt_bufnr = vim.api.nvim_get_current_buf() + local current_picker = action_state.get_current_picker(prompt_bufnr) + local results = {} + action_utils.map_entries(prompt_bufnr, function(entry, index, row) + results[row] = entry.value + end) + return results + end +< + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {f} (function) Function to map onto entries of picker that + takes (entry, index, row) as viable + arguments + + +utils.map_selections({prompt_bufnr}, {f}) *telescope.actions.utils.map_selections()* + Apply `f` to the multi selections of the current picker and return a table + of mapped selections. + - Notes: + - Mapped selections may include results not visible in the results popup. + - Selected entries are returned in order of their selection. + - Warning: `map_selections` has no return value. + - The below example showcases how to collect results + + Usage: + > + local action_state = require "telescope.actions.state" + local action_utils = require "telescope.actions.utils" + function selection_by_index() + local prompt_bufnr = vim.api.nvim_get_current_buf() + local current_picker = action_state.get_current_picker(prompt_bufnr) + local results = {} + action_utils.map_selections(prompt_bufnr, function(entry, index) + results[index] = entry.value + end) + return results + end +< + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + {f} (function) Function to map onto selection of picker + that takes (selection) as a viable argument + + +utils.get_registered_mappings({prompt_bufnr}) *telescope.actions.utils.get_registered_mappings()* + Utility to collect mappings of prompt buffer in array of `{mode, keybind, + name}`. + + + Parameters: ~ + {prompt_bufnr} (number) The prompt bufnr + + + +================================================================================ +ACTIONS_GENERATE *telescope.actions.generate* + +Module for convenience to override defaults of corresponding +|telescope.actions| at |telescope.setup()|. + +General usage: +> + require("telescope").setup { + defaults = { + mappings = { + n = { + ["?"] = action_generate.which_key { + name_width = 20, -- typically leads to smaller floats + max_height = 0.5, -- increase potential maximum height + separator = " > ", -- change sep between mode, keybind, and name + close_with_action = false, -- do not close float on action + }, + }, + }, + }, + } +< + +action_generate.which_key({opts}) *telescope.actions.generate.which_key()* + Display the keymaps of registered actions similar to which-key.nvim. + + - Floating window: + - Appears on the opposite side of the prompt. + - Resolves to minimum required number of lines to show hints with `opts` + or truncates entries at `max_height`. + - Closes automatically on action call and can be disabled with by setting + `close_with_action` to false. + + + Parameters: ~ + {opts} (table) options to pass to toggling registered actions + + Fields: ~ + {max_height} (number) % of max. height or no. of rows + for hints (default: 0.4), see + |resolver.resolve_height()| + {only_show_current_mode} (boolean) only show keymaps for the current + mode (default: true) + {mode_width} (number) fixed width of mode to be shown + (default: 1) + {keybind_width} (number) fixed width of keybind to be shown + (default: 7) + {name_width} (number) fixed width of action name to be + shown (default: 30) + {column_padding} (string) string to split; can be used for + vertical separator (default: " ") + {mode_hl} (string) hl group of mode (default: + TelescopeResultsConstant) + {keybind_hl} (string) hl group of keybind (default: + TelescopeResultsVariable) + {name_hl} (string) hl group of action name (default: + TelescopeResultsFunction) + {column_indent} (number) number of left-most spaces before + keybinds are shown (default: 4) + {line_padding} (number) row padding in top and bottom of + float (default: 1) + {separator} (string) separator string between mode, key + bindings, and action (default: " + -> ") + {close_with_action} (boolean) registered action will close + keymap float (default: true) + {normal_hl} (string) winhl of "Normal" for keymap hints + floating window (default: + "TelescopePrompt") + {border_hl} (string) winhl of "Normal" for keymap + borders (default: + "TelescopePromptBorder") + {winblend} (number) pseudo-transparency of keymap + hints floating window + + + +================================================================================ +PREVIEWERS *telescope.previewers* + +Provides a Previewer table that has to be implemented by each previewer. To +achieve this, this module also provides two wrappers that abstract most of the +work and make it really easy create new previewers. + - `previewers.new_termopen_previewer` + - `previewers.new_buffer_previewer` + +Furthermore, there are a collection of previewers already defined which can be +used for every picker, as long as the entries of the picker provide the +necessary fields. The more important ones are + - `previewers.cat` + - `previewers.vimgrep` + - `previewers.qflist` + - `previewers.vim_buffer_cat` + - `previewers.vim_buffer_vimgrep` + - `previewers.vim_buffer_qflist` + +Previewers can be disabled for any builtin or custom picker by doing :Telescope +find_files previewer=false + +previewers.Previewer() *telescope.previewers.Previewer()* + This is the base table all previewers have to implement. It's possible to + write a wrapper for this because most previewers need to have the same keys + set. Examples of wrappers are: + - `new_buffer_previewer` + - `new_termopen_previewer` + + To create a new table do following: + - `local new_previewer = Previewer:new(opts)` + + What `:new` expects is listed below + + The interface provides following set of functions. All of them, besides + `new`, will be handled by telescope pickers. + - `:new(opts)` + - `:preview(entry, status)` + - `:teardown()` + - `:send_input(input)` + - `:scroll_fn(direction)` + + `Previewer:new()` expects a table as input with following keys: + - `setup` function(self): Will be called the first time preview will be + called. + - `teardown` function(self): Will be called on cleanup. + - `preview_fn` function(self, entry, status): Will be called each time a + new entry was selected. + - `title` function(self): Will return the static title of the previewer. + - `dynamic_title` function(self, entry): Will return the dynamic title of + the previewer. Will only be called when config value + dynamic_preview_title is true. + - `send_input` function(self, input): This is meant for + `termopen_previewer` and it can be used to send input to the terminal + application, like less. + - `scroll_fn` function(self, direction): Used to make scrolling work. + + + +previewers.new() *telescope.previewers.new()* + A shorthand for creating a new Previewer. The provided table will be + forwarded to `Previewer:new(...)` + + + +previewers.new_termopen_previewer() *telescope.previewers.new_termopen_previewer()* + Is a wrapper around Previewer and helps with creating a new + `termopen_previewer`. + + It requires you to specify one table entry `get_command(entry, status)`. + This `get_command` function has to return the terminal command that will be + executed for each entry. Example: + > + get_command = function(entry, status) + return { 'bat', entry.path } + end +< + + Additionally you can define: + - `title` a static title for example "File Preview" + - `dyn_title(self, entry)` a dynamic title function which gets called when + config value `dynamic_preview_title = true` + + It's an easy way to get your first previewer going and it integrates well + with `bat` and `less`. Providing out of the box scrolling if the command + uses less. + + Furthermore, it will forward all `config.set_env` environment variables to + that terminal process. + + + +previewers.cat() *telescope.previewers.cat()* + Provides a `termopen_previewer` which has the ability to display files. It + will always show the top of the file and has support for `bat`(prioritized) + and `cat`. Each entry has to provide either the field `path` or `filename` + in order to make this previewer work. + + The preferred way of using this previewer is like this + `require('telescope.config').values.cat_previewer` This will respect user + configuration and will use `buffer_previewers` in case it's configured that + way. + + + +previewers.vimgrep() *telescope.previewers.vimgrep()* + Provides a `termopen_previewer` which has the ability to display files at + the provided line. It has support for `bat`(prioritized) and `cat`. Each + entry has to provide either the field `path` or `filename` and a `lnum` + field in order to make this previewer work. + + The preferred way of using this previewer is like this + `require('telescope.config').values.grep_previewer` This will respect user + configuration and will use `buffer_previewers` in case it's configured that + way. + + + +previewers.qflist() *telescope.previewers.qflist()* + Provides a `termopen_previewer` which has the ability to display files at + the provided line or range. It has support for `bat`(prioritized) and + `cat`. Each entry has to provide either the field `path` or `filename`, + `lnum` and a `start` and `finish` range in order to make this previewer + work. + + The preferred way of using this previewer is like this + `require('telescope.config').values.qflist_previewer` This will respect + user configuration and will use buffer previewers in case it's configured + that way. + + + +previewers.new_buffer_previewer() *telescope.previewers.new_buffer_previewer()* + An interface to instantiate a new `buffer_previewer`. That means that the + content actually lives inside a vim buffer which enables us more control + over the actual content. For example, we can use `vim.fn.search` to jump to + a specific line or reuse buffers/already opened files more easily. This + interface is more complex than `termopen_previewer` but offers more + flexibility over your content. It was designed to display files but was + extended to also display the output of terminal commands. + + In the following options, state table and general tips are mentioned to + make your experience with this previewer more seamless. + + + options: + - `define_preview = function(self, entry, status)` (required) Is called + for each selected entry, after each selection_move (up or down) and is + meant to handle things like reading file, jump to line or attach a + highlighter. + - `setup = function(self)` (optional) Is called once at the beginning, + before the preview for the first entry is displayed. You can return a + table of vars that will be available in `self.state` in each + `define_preview` call. + - `teardown = function(self)` (optional) Will be called at the end, when + the picker is being closed and is meant to cleanup everything that was + allocated by the previewer. The `buffer_previewer` will automatically + cleanup all created buffers. So you only need to handle things that + were introduced by you. + - `keep_last_buf = true` (optional) Will not delete the last selected + buffer. This would allow you to reuse that buffer in the select action. + For example, that buffer can be opened in a new split, rather than + recreating that buffer in an action. To access the last buffer number: + `require('telescope.state').get_global_key("last_preview_bufnr")` + - `get_buffer_by_name = function(self, entry)` Allows you to set a unique + name for each buffer. This is used for caching purpose. + `self.state.bufname` will be nil if the entry was never loaded or the + unique name when it was loaded once. For example, useful if you have + one file but multiple entries. This happens for grep and lsp builtins. + So to make the cache work only load content if `self.state.bufname ~= + entry.your_unique_key` + - `title` a static title for example "File Preview" + - `dyn_title(self, entry)` a dynamic title function which gets called + when config value `dynamic_preview_title = true` + + `self.state` table: + - `self.state.bufnr` Is the current buffer number, in which you have to + write the loaded content. Don't create a buffer yourself, otherwise + it's not managed by the buffer_previewer interface and you will + probably be better off writing your own interface. + - self.state.winid Current window id. Useful if you want to set the + cursor to a provided line number. + - self.state.bufname Will return the current buffer name, if + `get_buffer_by_name` is defined. nil will be returned if the entry was + never loaded or when `get_buffer_by_name` is not set. + + Tips: + - If you want to display content of a terminal job, use: + `require('telescope.previewers.utils').job_maker(cmd, bufnr, opts)` + - `cmd` table: for example { 'git', 'diff', entry.value } + - `bufnr` number: in which the content will be written + - `opts` table: with following keys + - `bufname` string: used for cache + - `value` string: used for cache + - `mode` string: either "insert" or "append". "insert" is default + - `env` table: define environment variables. Example: + - `{ ['PAGER'] = '', ['MANWIDTH'] = 50 }` + - `cwd` string: define current working directory for job + - `callback` function(bufnr, content): will be called when job is + done. Content will be nil if job is already loaded. So you can do + highlighting only the first time the previewer is created for + that entry. Use the returned `bufnr` and not `self.state.bufnr` + in callback, because state can already be changed at this point + in time. + - If you want to attach a highlighter use: + - `require('telescope.previewers.utils').highlighter(bufnr, ft)` + - This will prioritize tree sitter highlighting if available for + environment and language. + - `require('telescope.previewers.utils').regex_highlighter(bufnr, ft)` + - `require('telescope.previewers.utils').ts_highlighter(bufnr, ft)` + - If you want to use `vim.fn.search` or similar you need to run it in + that specific buffer context. Do + > + vim.api.nvim_buf_call(bufnr, function() + -- for example `search` and `matchadd` + end) + to achieve that. +< + - If you want to read a file into the buffer it's best to use + `buffer_previewer_maker`. But access this function with + `require('telescope.config').values.buffer_previewer_maker` because it + can be redefined by users. + + + +previewers.buffer_previewer_maker({filepath}, {bufnr}, {opts}) *telescope.previewers.buffer_previewer_maker()* + A universal way of reading a file into a buffer previewer. It handles async + reading, cache, highlighting, displaying directories and provides a + callback which can be used, to jump to a line in the buffer. + + + Parameters: ~ + {filepath} (string) String to the filepath, will be expanded + {bufnr} (number) Where the content will be written + {opts} (table) keys: `use_ft_detect`, `bufname` and `callback` + + +previewers.vim_buffer_cat() *telescope.previewers.vim_buffer_cat()* + A previewer that is used to display a file. It uses the `buffer_previewer` + interface and won't jump to the line. To integrate this one into your own + picker make sure that the field `path` or `filename` is set for each entry. + The preferred way of using this previewer is like this + `require('telescope.config').values.file_previewer` This will respect user + configuration and will use `termopen_previewer` in case it's configured + that way. + + + +previewers.vim_buffer_vimgrep() *telescope.previewers.vim_buffer_vimgrep()* + A previewer that is used to display a file and jump to the provided line. + It uses the `buffer_previewer` interface. To integrate this one into your + own picker make sure that the field `path` or `filename` and `lnum` is set + in each entry. If the latter is not present, it will default to the first + line. The preferred way of using this previewer is like this + `require('telescope.config').values.grep_previewer` This will respect user + configuration and will use `termopen_previewer` in case it's configured + that way. + + + +previewers.vim_buffer_qflist() *telescope.previewers.vim_buffer_qflist()* + Is the same as `vim_buffer_vimgrep` and only exist for consistency with + `term_previewers`. + + The preferred way of using this previewer is like this + `require('telescope.config').values.qflist_previewer` This will respect + user configuration and will use `termopen_previewer` in case it's + configured that way. + + + +previewers.git_branch_log() *telescope.previewers.git_branch_log()* + A previewer that shows a log of a branch as graph + + + +previewers.git_stash_diff() *telescope.previewers.git_stash_diff()* + A previewer that shows a diff of a stash + + + +previewers.git_commit_diff_to_parent() *telescope.previewers.git_commit_diff_to_parent()* + A previewer that shows a diff of a commit to a parent commit. + The run command is `git --no-pager diff SHA^! -- $CURRENT_FILE` + + The current file part is optional. So is only uses it with bcommits. + + + +previewers.git_commit_diff_to_head() *telescope.previewers.git_commit_diff_to_head()* + A previewer that shows a diff of a commit to head. + The run command is `git --no-pager diff --cached $SHA -- $CURRENT_FILE` + + The current file part is optional. So is only uses it with bcommits. + + + +previewers.git_commit_diff_as_was() *telescope.previewers.git_commit_diff_as_was()* + A previewer that shows a diff of a commit as it was. + The run command is `git --no-pager show $SHA:$CURRENT_FILE` or `git + --no-pager show $SHA` + + + +previewers.git_commit_message() *telescope.previewers.git_commit_message()* + A previewer that shows the commit message of a diff. + The run command is `git --no-pager log -n 1 $SHA` + + + +previewers.git_file_diff() *telescope.previewers.git_file_diff()* + A previewer that shows the current diff of a file. Used in git_status. + The run command is `git --no-pager diff $FILE` + + + +previewers.display_content() *telescope.previewers.display_content()* + A deprecated way of displaying content more easily. Was written at a time, + where the buffer_previewer interface wasn't present. Nowadays it's easier + to just use this. We will keep it around for backwards compatibility + because some extensions use it. It doesn't use cache or some other clever + tricks. + + + + +================================================================================ +HISTORY *telescope.actions.history* + +A base implementation of a prompt history that provides a simple history and +can be replaced with a custom implementation. + +For example: We provide a extension for a smart history that uses sql.nvim to +map histories to metadata, like the calling picker or cwd. + +So you have a history for: +- find_files project_1 +- grep_string project_1 +- live_grep project_1 +- find_files project_2 +- grep_string project_2 +- live_grep project_2 +- etc + +See https://github.com/nvim-telescope/telescope-smart-history.nvim + +histories.History() *telescope.actions.history.History()* + Manages prompt history + + + Fields: ~ + {enabled} (boolean) Will indicate if History is enabled or disabled + {path} (string) Will point to the location of the history file + {limit} (string) Will have the limit of the history. Can be nil, + if limit is disabled. + {content} (table) History table. Needs to be filled by your own + History implementation + {index} (number) Used to keep track of the next or previous index. + Default is #content + 1 + + +histories.History:new({opts}) *telescope.actions.history.History:new()* + Create a new History + + + Parameters: ~ + {opts} (table) Defines the behavior of History + + Fields: ~ + {init} (function) Will be called after handling configuration + (required) + {append} (function) How to append a new prompt item (required) + {reset} (function) What happens on reset. Will be called when + telescope closes (required) + {pre_get} (function) Will be called before a next or previous item + will be returned (optional) + + +histories.new() *telescope.actions.history.new()* + Shorthand to create a new history + + + +histories.History:reset() *telescope.actions.history.History:reset()* + Will reset the history index to the default initial state. Will happen + after the picker closed + + + +histories.History:append({line}, {picker}, {no_reset}) *telescope.actions.history.History:append()* + Append a new line to the history + + + Parameters: ~ + {line} (string) current line that will be appended + {picker} (table) the current picker object + {no_reset} (boolean) On default it will reset the state at the end. + If you don't want to do this set to true + + +histories.History:get_next({line}, {picker}) *telescope.actions.history.History:get_next()* + Will return the next history item. Can be nil if there are no next items + + + Parameters: ~ + {line} (string) the current line + {picker} (table) the current picker object + + Return: ~ + string: the next history item + + +histories.History:get_prev({line}, {picker}) *telescope.actions.history.History:get_prev()* + Will return the previous history item. Can be nil if there are no previous + items + + + Parameters: ~ + {line} (string) the current line + {picker} (table) the current picker object + + Return: ~ + string: the previous history item + + +histories.get_simple_history() *telescope.actions.history.get_simple_history()* + A simple implementation of history. + + It will keep one unified history across all pickers. + + + + + vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope_changelog.txt b/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope_changelog.txt new file mode 100644 index 0000000..fbbb471 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/doc/telescope_changelog.txt @@ -0,0 +1,258 @@ +================================================================================ + *telescope.changelog* + +# Changelog + + *telescope.changelog-922* + +Date: May 17, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/922 + +This is one of our largest breaking changes thus far, so I (TJ) am adding some +information here so that you can more easily update (without having to track +down the commit, etc.). + +The goal of these breaking changes is to greatly simplify the way +configuration for layouts happen. This should make it much easier to configure +each picker, layout_strategy, and more. Please report any bugs or behavior +that is broken / confusing upstream and we can try and make the configuration +better. + +|telescope.setup()| has changed `layout_defaults` -> `layout_config`. + This makes it so that the setup and the pickers share the same key, + otherwise it is too confusing which key is for which. + + +`picker:find()` now has different values available for configuring the UI. + All configuration for the layout must be passed in the key: + `layout_config`. + + Previously, these keys were passed via `picker:find(opts)`, but should be + passed via `opts.layout_config` now. + - {height} + - {width} + - {prompt_position} + - {preview_cutoff} + + These keys are removed: + - {results_height}: This key is no longer valid. Instead, use `height` + and the corresponding `preview_*` options for the layout strategy to + get the correct results height. This simplifies the configuration + for many of the existing strategies. + + - {results_width}: This key actually never did anything. It was + leftover from some hacking that I had attempted before. Instead you + should be using something like the `preview_width` configuration + option for |layout_strategies.horizontal()| + + You should get error messages when you try and use any of the above keys now. + + *telescope.changelog-839* + +Date: July 7, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/839 + +Small breaking change regarding `shorten_path` and `hide_filename`. +This allows to configure path displays on a global level and offers a way for +extension developers to make use of the same configuration, offering a better +overall experience. + +The new way to configure to configure path displays is with: + `path_display`: It is a table and accepts multiple values: + - "hidden" hide file names + - "tail" only display the file name, and not the path + - "absolute" display absolute paths + - "shorten" only display the first character of each directory in + the path + see |telescope.defaults.path_display| + +Example would be for a global configuration: + require("telescope").setup{ + defaults = { + path_display = { + "shorten", + "absolute", + }, + } + } + +You can also still pass this to a single builtin call: + require("telescope.builtin").find_files { + path_display = { "shorten" } + } + +For extension developers there is a new util function that can be used to +display a path: + local filename = utils.transform_path(opts, entry.filename) + + *telescope.changelog-473* + +Date: July 14, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/473 + +Deprecation of telescope.path + +Extension developers need to move to plenary.path, because we will remove the +telescope.path module soon. + +Guide to switch over to plenary.path + - separator + before: require("telescope.path").separator + now: require("plenary.path").path.sep + - home + before: require("telescope.path").home + now: require("plenary.path").path.home + - make_relative + before: require("telescope.path").make_relative(filepath, cwd) + now: require("plenary.path"):new(filepath):make_relative(cwd) + - shorten + before: require("telescope.path").shorten(filepath) + now: require("plenary.path"):new(filepath):shorten() + with optional len, default is 1 + - normalize + before: require("telescope.path").normalize(filepath, cwd) + now: require("plenary.path"):new(filepath):normalize(cwd) + - read_file + before: require("telescope.path").read_file(filepath) + now: require("plenary.path"):new(filepath):read() + - read_file_async + before: require("telescope.path").read_file_async(filepath, callback) + now: require("plenary.path"):new(filepath):read(callback) + + *telescope.changelog-1406* + +Date: November 4, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1406 + +Telescope requires Neovim release 0.5.1 or a recent nightly + +Due to making use of newly implemented extmark features, Telescope now +requires users to be on Neovim 0.5.1 (the most recent stable version) or on +the LATEST version of Neovim nightly. + + + *telescope.changelog-1549* + +Date: December 10, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1549 + +Telescope requires now Neovim release 0.6.0 or a more recent nightly. +If you are running neovim nightly, you need to make sure that you are on the +LATEST version. Every other commit is not supported. So make sure you build +the newest nightly before reporting issues. + + + *telescope.changelog-1553* + +Date: December 10, 2021 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1553 + +Move from `vim.lsp.diagnostic` to `vim.diagnostic`. + +Because the newly added `vim.diagnostic` has no longer anything to do with lsp +we also decided to rename our diagnostic functions: + Telescope lsp_document_diagnostics -> Telescope diagnostics bufnr=0 + Telescope lsp_workspace_diagnostics -> Telescope diagnostics +Because of that the `lsp_*_diagnostics` inside Telescope will be deprecated +and removed soon. The new `diagnostics` works almost identical to the previous +functions. Note that there is no longer a workspace diagnostics. You can only +get all diagnostics for all open buffers. + + + *telescope.changelog-1851* + +Date: April 22, 2022 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1851 + +Telescope requires now Neovim release 0.7.0 or a more recent nightly. +If you are running Neovim nightly, you need to make sure that you are on the +LATEST version. Every other commit is not supported. So make sure you build +the newest nightly before reporting issues. +In the future, we will adopt a different release strategy. This release +strategy follows the approach that the latest telescope.nvim master will only +work with latest Neovim nightly and we will provide tags for specific Neovim +versions. You can read more about this strategy here: +https://github.com/nvim-telescope/telescope.nvim/issues/1772 + + + *telescope.changelog-1866* + +Date: April 25, 2022 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1866 + +We decided to remove both `lsp_code_actions` and `lsp_range_code_actions`. +Currently, both functions are highly duplicated code from neovim, with fewer +features, because it's out of date. So rather that we copy over the required +changes to fix some bugs or implement client side code actions, we decided to +remove both of them and suggest you use `vim.lsp.buf.code_action` and +`vim.lsp.buf.range_code_action`. The transition to it is easy thanks to +`vim.ui.select` which allows you to override the select UI. We provide a small +extension for quite some time that make it easy to use telescope for +`vim.ui.select`. You can found the code here +https://github.com/nvim-telescope/telescope-ui-select.nvim. It offers the same +displaying as the current version of `lsp_code_actions`. An alternative is +https://github.com/stevearc/dressing.nvim which has support for multiple +different backends including telescope. + + + *telescope.changelog-1945* + +Date: July 01, 2022 +PR: https://github.com/nvim-telescope/telescope.nvim/pull/1945 + +This is our dev branch which contains a lot of PRs, a lot of fixes, +refactoring and general quality of life improvements. It also contains new +features, the most noteworthy are the following (mostly developed by the +community): +- feat: none strategy & control attachment (#1867) +- feat: force buffer delete for terminal and improvements for + Picker:delete_selection (#1943) +- feat(tags): process tagfiles on the fly (#1989) +- feat(builtin.lsp): implement builtin handlers for + lsp.(incoming|outgoing)_calls (#1484) +- feat: clear previewer if no item is selected (#2004) +- feat: add min max boundary to width, height resolver (#2002) +- feat: Add entry_index for entry_makers (#1850) +- feat: refine with new_table (#1115) + +The last one is one of the most exciting new features, because it allows you +to go from live_grep into a fuzzy environment with the following mapping +``. It's a general interface we now implemented for `live_grep` and +`lsp_dynamic_workspace_symbols` but it could also be easily implemented for +other builtins, by us or the user. It's now available for extension developers. +We will add documentation in the next couple of days and improve it by adding +more options to configure it after the initial 0.1 release. + +But as with all longer development phases, there are also some breaking +changes. This is the main reason we moved development to a separate branch, for +the past two months. We can't promise that there won't be more breaking +changes, but it is the plan that this is the last set of breaking changes prior +to the 0.1 release on July, 12. We are deeply sorry for the inconvenience. The +following breaking changes are included in this PR: +- break(git_files): change `show_untracked` default to false. Can be changed + back with `:Telescope git_files show_untracked=true` +- break: deprecate `utils.get_default` `utils.if_nil`, will be removed prior + to 0.1, so if you use it in your config, please move to `vim.F.if_nil` +- break: drops `ignore_filename` option, use `path_display= { "hidden" }` + instead +- break: prefix internal interfaces with __ so + `require("telescope.builtin.files").find_files` will show a notify error but + still works for now. The error will be removed prior to 0.1! You should use + `require("telescope.builtin").find_files` because we wrap all the functions + that are exposed in this module. +- break: defaults.preview.treesitter rework that allows you to either enable a + list of languages, or enable all and disable some. Please read + `:help telescope.defaults.preview` for more information. + Something like this is now possible: + > + treesitter = { + enable = false, + -- or + enable = { "c" }, + -- disable can be set if enable isn't set + disable = { "perl", "javascript" }, + }, +< + + + vim:tw=78:ts=8:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopePrompt.lua b/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopePrompt.lua new file mode 100644 index 0000000..8888a88 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopePrompt.lua @@ -0,0 +1,12 @@ +-- Don't wrap textwidth things +vim.opt_local.formatoptions:remove "t" +vim.opt_local.formatoptions:remove "c" + +-- Don't include `showbreak` when calculating strdisplaywidth +vim.opt_local.wrap = false + +-- There's also no reason to enable textwidth here anyway +vim.opt_local.textwidth = 0 +vim.opt_local.scrollbind = false + +vim.opt_local.signcolumn = "no" diff --git a/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopeResults.lua b/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopeResults.lua new file mode 100644 index 0000000..08e9dcc --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/ftplugin/TelescopeResults.lua @@ -0,0 +1,5 @@ +-- Don't have scrolloff, it makes things weird. +vim.opt_local.scrolloff = 0 +vim.opt_local.scrollbind = false + +vim.opt_local.signcolumn = "no" diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_.lua new file mode 100644 index 0000000..6800c9e --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_.lua @@ -0,0 +1,323 @@ +local uv = vim.loop + +local Object = require "plenary.class" +local log = require "plenary.log" + +local async = require "plenary.async" +local channel = require("plenary.async").control.channel + +local M = {} + +local AsyncJob = {} +AsyncJob.__index = AsyncJob + +function AsyncJob.new(opts) + local self = setmetatable({}, AsyncJob) + + self.command, self.uv_opts = M.convert_opts(opts) + + self.stdin = opts.stdin or M.NullPipe() + self.stdout = opts.stdout or M.NullPipe() + self.stderr = opts.stderr or M.NullPipe() + + if opts.cwd and opts.cwd ~= "" then + self.uv_opts.cwd = vim.fn.expand(opts.cwd) + -- this is a "illegal" hack for windows. E.g. If the git command returns `/` rather than `\` as delimiter, + -- vim.fn.expand might just end up returning an empty string. Weird + -- Because empty string is not allowed in libuv the job will not spawn. Solution is we just set it to opts.cwd + if self.uv_opts.cwd == "" then + self.uv_opts.cwd = opts.cwd + end + end + + self.uv_opts.stdio = { + self.stdin.handle, + self.stdout.handle, + self.stderr.handle, + } + + return self +end + +function AsyncJob:_for_each_pipe(f, ...) + for _, pipe in ipairs { self.stdin, self.stdout, self.stderr } do + f(pipe, ...) + end +end + +function AsyncJob:close(force) + if force == nil then + force = true + end + + self:_for_each_pipe(function(p) + p:close(force) + end) + + uv.process_kill(self.handle, "SIGTERM") + + log.debug "[async_job] closed" +end + +M.spawn = function(opts) + local self = AsyncJob.new(opts) + self.handle, self.pid = uv.spawn( + self.command, + self.uv_opts, + async.void(function() + self:close(false) + if not self.handle:is_closing() then + self.handle:close() + end + end) + ) + + if not self.handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(self))) + end + + return self +end + +---@class uv_pipe_t +--- A pipe handle from libuv +---@field read_start function: Start reading +---@field read_stop function: Stop reading +---@field close function: Close the handle +---@field is_closing function: Whether handle is currently closing +---@field is_active function: Whether the handle is currently reading + +---@class BasePipe +---@field super Object: Always available +---@field handle uv_pipe_t: A pipe handle +---@field extend function: Extend +local BasePipe = Object:extend() + +function BasePipe:new() + self.eof_tx, self.eof_rx = channel.oneshot() +end + +function BasePipe:close(force) + if force == nil then + force = true + end + + assert(self.handle, "Must have a pipe to close. Otherwise it's weird!") + + if self.handle:is_closing() then + return + end + + -- If we're not forcing the stop, allow waiting for eof + -- This ensures that we don't end up with weird race conditions + if not force then + self.eof_rx() + end + + self.handle:read_stop() + if not self.handle:is_closing() then + self.handle:close() + end + + self._closed = true +end + +---@class LinesPipe : BasePipe +local LinesPipe = BasePipe:extend() + +function LinesPipe:new() + LinesPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function LinesPipe:read() + local read_tx, read_rx = channel.oneshot() + + self.handle:read_start(function(err, data) + assert(not err, err) + self.handle:read_stop() + + read_tx(data) + if data == nil then + self.eof_tx() + end + end) + + return read_rx() +end + +function LinesPipe:iter(schedule) + if schedule == nil then + schedule = true + end + + local text = nil + local index = nil + + local get_next_text = function(previous) + index = nil + + local read = self:read() + if previous == nil and read == nil then + return + end + + read = string.gsub(read or "", "\r", "") + return (previous or "") .. read + end + + local next_value = nil + next_value = function() + if schedule then + async.util.scheduler() + end + + if text == nil or (text == "" and index == nil) then + return nil + end + + local start = index + index = string.find(text, "\n", index, true) + + if index == nil then + text = get_next_text(string.sub(text, start or 1)) + return next_value() + end + + index = index + 1 + + return string.sub(text, start or 1, index - 2) + end + + text = get_next_text() + + return function() + return next_value() + end +end + +---@class NullPipe : BasePipe +local NullPipe = BasePipe:extend() + +function NullPipe:new() + NullPipe.super.new(self) + self.start = function() end + self.read_start = function() end + self.close = function() end + + -- This always has eof tx done, so can just call it now + self.eof_tx() +end + +---@class ChunkPipe : BasePipe +local ChunkPipe = BasePipe:extend() + +function ChunkPipe:new() + ChunkPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function ChunkPipe:read() + local read_tx, read_rx = channel.oneshot() + + self.handle:read_start(function(err, data) + assert(not err, err) + self.handle:read_stop() + + read_tx(data) + if data == nil then + self.eof_tx() + end + end) + + return read_rx() +end + +function ChunkPipe:iter() + return function() + if self._closed then + return nil + end + + return self:read() + end +end + +---@class ErrorPipe : BasePipe +local ErrorPipe = BasePipe:extend() + +function ErrorPipe:new() + ErrorPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function ErrorPipe:start() + self.handle:read_start(function(err, data) + if not err and not data then + return + end + + self.handle:read_stop() + self.handle:close() + + error(string.format("Err: %s, Data: '%s'", err, data)) + end) +end + +M.NullPipe = NullPipe +M.LinesPipe = LinesPipe +M.ChunkPipe = ChunkPipe +M.ErrorPipe = ErrorPipe + +M.convert_opts = function(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.args = args + + if o.env then + if type(o.env) ~= "table" then + error(debug.traceback "'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 + + return command, obj +end + +return M diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_extensions/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_extensions/init.lua new file mode 100644 index 0000000..c31205a --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/_extensions/init.lua @@ -0,0 +1,73 @@ +local extensions = {} + +extensions._loaded = {} +extensions._config = {} +extensions._health = {} + +local load_extension = function(name) + local ok, ext = pcall(require, "telescope._extensions." .. name) + if not ok then + error(string.format("'%s' extension doesn't exist or isn't installed: %s", name, ext)) + end + return ext +end + +extensions.manager = setmetatable({}, { + __index = function(t, k) + local ext = load_extension(k) + t[k] = ext.exports or {} + if ext.setup then + ext.setup(extensions._config[k] or {}, require("telescope.config").values) + end + extensions._health[k] = ext.health + + return t[k] + end, +}) + +--- Register an extension module. +--- +--- Extensions have several important keys. +--- - setup: +--- function(ext_config, config) -> nil +--- +--- Called when first loading the extension. +--- The first parameter is the config passed by the user +--- in telescope setup. The second parameter is the resulting +--- config.values after applying the users setup defaults. +--- +--- It is acceptable for a plugin to override values in config, +--- as some plugins will be installed simply to manage some setup, +--- install some sorter, etc. +--- +--- - exports: +--- table +--- +--- Only the items in `exports` will be exposed on the resulting +--- module that users can access via require('telescope').extensions.foo +--- Also, any top-level key-value pairs in exports where the value is a function and the +--- key doesn't start with an underscore will be included when calling the `builtin` picker +--- with the `include_extensions` option enabled. +--- +--- Other things in the module will not be accessible. This is the public API +--- for your extension. Consider not breaking it a lot :laugh: +--- +--- TODO: +--- - actions +extensions.register = function(mod) + return mod +end + +extensions.load = function(name) + local ext = load_extension(name) + if ext.setup then + ext.setup(extensions._config[name] or {}, require("telescope.config").values) + end + return extensions.manager[name] +end + +extensions.set_config = function(extensions_config) + extensions._config = extensions_config or {} +end + +return extensions diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/generate.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/generate.lua new file mode 100644 index 0000000..4d33f9f --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/generate.lua @@ -0,0 +1,117 @@ +---@tag telescope.actions.generate +---@config { ["module"] = "telescope.actions.generate", ["name"] = "ACTIONS_GENERATE" } + +---@brief [[ +--- Module for convenience to override defaults of corresponding |telescope.actions| at |telescope.setup()|. +--- +--- General usage: +--- +--- require("telescope").setup { +--- defaults = { +--- mappings = { +--- n = { +--- ["?"] = action_generate.which_key { +--- name_width = 20, -- typically leads to smaller floats +--- max_height = 0.5, -- increase potential maximum height +--- separator = " > ", -- change sep between mode, keybind, and name +--- close_with_action = false, -- do not close float on action +--- }, +--- }, +--- }, +--- }, +--- } +--- +---@brief ]] + +local actions = require "telescope.actions" +local config = require "telescope.config" +local action_state = require "telescope.actions.state" +local finders = require "telescope.finders" + +local action_generate = {} + +--- Display the keymaps of registered actions similar to which-key.nvim.
+--- - Floating window: +--- - Appears on the opposite side of the prompt. +--- - Resolves to minimum required number of lines to show hints with `opts` or truncates entries at `max_height`. +--- - Closes automatically on action call and can be disabled with by setting `close_with_action` to false. +---@param opts table: options to pass to toggling registered actions +---@field max_height number: % of max. height or no. of rows for hints (default: 0.4), see |resolver.resolve_height()| +---@field only_show_current_mode boolean: only show keymaps for the current mode (default: true) +---@field mode_width number: fixed width of mode to be shown (default: 1) +---@field keybind_width number: fixed width of keybind to be shown (default: 7) +---@field name_width number: fixed width of action name to be shown (default: 30) +---@field column_padding string: string to split; can be used for vertical separator (default: " ") +---@field mode_hl string: hl group of mode (default: TelescopeResultsConstant) +---@field keybind_hl string: hl group of keybind (default: TelescopeResultsVariable) +---@field name_hl string: hl group of action name (default: TelescopeResultsFunction) +---@field column_indent number: number of left-most spaces before keybinds are shown (default: 4) +---@field line_padding number: row padding in top and bottom of float (default: 1) +---@field separator string: separator string between mode, key bindings, and action (default: " -> ") +---@field close_with_action boolean: registered action will close keymap float (default: true) +---@field normal_hl string: winhl of "Normal" for keymap hints floating window (default: "TelescopePrompt") +---@field border_hl string: winhl of "Normal" for keymap borders (default: "TelescopePromptBorder") +---@field winblend number: pseudo-transparency of keymap hints floating window +action_generate.which_key = function(opts) + return function(prompt_bufnr) + actions.which_key(prompt_bufnr, opts) + end +end + +action_generate.refine = function(prompt_bufnr, opts) + opts = opts or {} + opts.prompt_to_prefix = vim.F.if_nil(opts.prompt_to_prefix, false) + opts.prefix_hl_group = vim.F.if_nil(opts.prompt_hl_group, "TelescopePromptPrefix") + opts.prompt_prefix = vim.F.if_nil(opts.promt_prefix, config.values.prompt_prefix) + opts.reset_multi_selection = vim.F.if_nil(opts.reset_multi_selection, false) + opts.reset_prompt = vim.F.if_nil(opts.reset_prompt, true) + opts.sorter = vim.F.if_nil(opts.sorter, config.values.generic_sorter {}) + local push_history = vim.F.if_nil(opts.push_history, true) + + local current_picker = action_state.get_current_picker(prompt_bufnr) + local current_line = action_state.get_current_line() + if push_history then + action_state.get_current_history():append(current_line, current_picker) + end + + -- title + if opts.prompt_title and current_picker.prompt_border then + current_picker.prompt_border:change_title(opts.prompt_title) + end + + if opts.results_title and current_picker.results_border then + current_picker.results_border:change_title(opts.results_title) + end + + local results = {} + for entry in current_picker.manager:iter() do + table.insert(results, entry) + end + + -- if opts.sorter == false, keep older sorter + if opts.sorter then + current_picker.sorter:_destroy() + current_picker.sorter = opts.sorter + current_picker.sorter:_init() + end + + local new_finder = finders.new_table { + results = results, + entry_maker = function(x) + return x + end, + } + + if not opts.reset_multi_selection and current_line ~= "" then + opts.multi = current_picker._multi + end + + if opts.prompt_to_prefix then + local current_prefix = current_picker.prompt_prefix + local suffix = current_prefix ~= opts.prompt_prefix and current_prefix or "" + opts.new_prefix = suffix .. current_line .. " " .. opts.prompt_prefix + end + current_picker:refresh(new_finder, opts) +end + +return action_generate diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/history.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/history.lua new file mode 100644 index 0000000..264a3e6 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/history.lua @@ -0,0 +1,207 @@ +local conf = require("telescope.config").values +local Path = require "plenary.path" +local utils = require "telescope.utils" + +local uv = vim.loop + +---@tag telescope.actions.history +---@config { ["module"] = "telescope.actions.history" } + +---@brief [[ +--- A base implementation of a prompt history that provides a simple history +--- and can be replaced with a custom implementation. +--- +--- For example: We provide a extension for a smart history that uses sql.nvim +--- to map histories to metadata, like the calling picker or cwd. +--- +--- So you have a history for: +--- - find_files project_1 +--- - grep_string project_1 +--- - live_grep project_1 +--- - find_files project_2 +--- - grep_string project_2 +--- - live_grep project_2 +--- - etc +--- +--- See https://github.com/nvim-telescope/telescope-smart-history.nvim +---@brief ]] + +-- TODO(conni2461): currently not present in plenary path only sync. +-- But sync is just unnecessary here +local write_async = function(path, txt, flag) + uv.fs_open(path, flag, 438, function(open_err, fd) + assert(not open_err, open_err) + uv.fs_write(fd, txt, -1, function(write_err) + assert(not write_err, write_err) + uv.fs_close(fd, function(close_err) + assert(not close_err, close_err) + end) + end) + end) +end + +local append_async = function(path, txt) + write_async(path, txt, "a") +end + +local histories = {} + +--- Manages prompt history +---@class History @Manages prompt history +---@field enabled boolean: Will indicate if History is enabled or disabled +---@field path string: Will point to the location of the history file +---@field limit string: Will have the limit of the history. Can be nil, if limit is disabled. +---@field content table: History table. Needs to be filled by your own History implementation +---@field index number: Used to keep track of the next or previous index. Default is #content + 1 +histories.History = {} +histories.History.__index = histories.History + +--- Create a new History +---@param opts table: Defines the behavior of History +---@field init function: Will be called after handling configuration (required) +---@field append function: How to append a new prompt item (required) +---@field reset function: What happens on reset. Will be called when telescope closes (required) +---@field pre_get function: Will be called before a next or previous item will be returned (optional) +function histories.History:new(opts) + local obj = {} + if conf.history == false or type(conf.history) ~= "table" then + obj.enabled = false + return setmetatable(obj, self) + end + obj.enabled = true + if conf.history.limit then + obj.limit = conf.history.limit + end + obj.path = vim.fn.expand(conf.history.path) + obj.content = {} + obj.index = 1 + + opts.init(obj) + obj._reset = opts.reset + obj._append = opts.append + obj._pre_get = opts.pre_get + + return setmetatable(obj, self) +end + +--- Shorthand to create a new history +function histories.new(...) + return histories.History:new(...) +end + +--- Will reset the history index to the default initial state. Will happen after the picker closed +function histories.History:reset() + if not self.enabled then + return + end + self._reset(self) +end + +--- Append a new line to the history +---@param line string: current line that will be appended +---@param picker table: the current picker object +---@param no_reset boolean: On default it will reset the state at the end. If you don't want to do this set to true +function histories.History:append(line, picker, no_reset) + if not self.enabled then + return + end + self._append(self, line, picker, no_reset) +end + +--- Will return the next history item. Can be nil if there are no next items +---@param line string: the current line +---@param picker table: the current picker object +---@return string: the next history item +function histories.History:get_next(line, picker) + if not self.enabled then + utils.notify("History:get_next", { + msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'", + level = "WARN", + }) + return false + end + if self._pre_get then + self._pre_get(self, line, picker) + end + + local next_idx = self.index + 1 + if next_idx <= #self.content then + self.index = next_idx + return self.content[next_idx] + end + self.index = #self.content + 1 + return nil +end + +--- Will return the previous history item. Can be nil if there are no previous items +---@param line string: the current line +---@param picker table: the current picker object +---@return string: the previous history item +function histories.History:get_prev(line, picker) + if not self.enabled then + utils.notify("History:get_prev", { + msg = "You are cycling to next the history item but history is disabled. Read ':help telescope.defaults.history'", + level = "WARN", + }) + return false + end + if self._pre_get then + self._pre_get(self, line, picker) + end + + local next_idx = self.index - 1 + if self.index == #self.content + 1 then + if line ~= "" then + self:append(line, picker, true) + end + end + if next_idx >= 1 then + self.index = next_idx + return self.content[next_idx] + end + return nil +end + +--- A simple implementation of history. +--- +--- It will keep one unified history across all pickers. +histories.get_simple_history = function() + return histories.new { + init = function(obj) + local p = Path:new(obj.path) + if not p:exists() then + p:touch { parents = true } + end + + obj.content = Path:new(obj.path):readlines() + obj.index = #obj.content + table.remove(obj.content, obj.index) + end, + reset = function(self) + self.index = #self.content + 1 + end, + append = function(self, line, _, no_reset) + if line ~= "" then + if self.content[#self.content] ~= line then + table.insert(self.content, line) + + local len = #self.content + if self.limit and len > self.limit then + local diff = len - self.limit + for i = diff, 1, -1 do + table.remove(self.content, i) + end + write_async(self.path, table.concat(self.content, "\n") .. "\n", "w") + else + append_async(self.path, line .. "\n") + end + end + end + if not no_reset then + self:reset() + end + end, + } +end + +return histories diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/init.lua new file mode 100644 index 0000000..3a6ccc7 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/init.lua @@ -0,0 +1,1332 @@ +---@tag telescope.actions +---@config { ["module"] = "telescope.actions" } + +---@brief [[ +--- Actions functions that are useful for people creating their own mappings. +--- +--- Actions can be either normal functions that expect the prompt_bufnr as +--- first argument (1) or they can be a custom telescope type called "action" (2). +--- +--- (1) The `prompt_bufnr` of a normal function denotes the identifier of your +--- picker which can be used to access the picker state. In practice, users +--- most commonly access from both picker and global state via the following: +--- +--- -- for utility functions +--- local action_state = require "telescope.actions.state" +--- +--- local actions = {} +--- actions.do_stuff = function(prompt_bufnr) +--- local current_picker = action_state.get_current_picker(prompt_bufnr) -- picker state +--- local entry = action_state.get_selected_entry() +--- end +--- +--- +--- See |telescope.actions.state| for more information. +--- +--- (2) To transform a module of functions into a module of "action"s, you need +--- to do the following: +--- +--- local transform_mod = require("telescope.actions.mt").transform_mod +--- +--- local mod = {} +--- mod.a1 = function(prompt_bufnr) +--- -- your code goes here +--- -- You can access the picker/global state as described above in (1). +--- end +--- +--- mod.a2 = function(prompt_bufnr) +--- -- your code goes here +--- end +--- mod = transform_mod(mod) +--- +--- -- Now the following is possible. This means that actions a2 will be executed +--- -- after action a1. You can chain as many actions as you want. +--- local action = mod.a1 + mod.a2 +--- action(bufnr) +--- +--- +--- Another interesing thing to do is that these actions now have functions you +--- can call. These functions include `:replace(f)`, `:replace_if(f, c)`, +--- `replace_map(tbl)` and `enhance(tbl)`. More information on these functions +--- can be found in the `developers.md` and `lua/tests/automated/action_spec.lua` +--- file. +---@brief ]] + +local a = vim.api + +local conf = require("telescope.config").values +local state = require "telescope.state" +local utils = require "telescope.utils" +local popup = require "plenary.popup" +local p_scroller = require "telescope.pickers.scroller" + +local action_state = require "telescope.actions.state" +local action_utils = require "telescope.actions.utils" +local action_set = require "telescope.actions.set" +local entry_display = require "telescope.pickers.entry_display" +local from_entry = require "telescope.from_entry" + +local transform_mod = require("telescope.actions.mt").transform_mod +local resolver = require "telescope.config.resolve" + +local actions = setmetatable({}, { + __index = function(_, k) + error("Key does not exist for 'telescope.actions': " .. tostring(k)) + end, +}) + +--- Move the selection to the next entry +---@param prompt_bufnr number: The prompt bufnr +actions.move_selection_next = function(prompt_bufnr) + action_set.shift_selection(prompt_bufnr, 1) +end + +--- Move the selection to the previous entry +---@param prompt_bufnr number: The prompt bufnr +actions.move_selection_previous = function(prompt_bufnr) + action_set.shift_selection(prompt_bufnr, -1) +end + +--- Move the selection to the entry that has a worse score +---@param prompt_bufnr number: The prompt bufnr +actions.move_selection_worse = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + action_set.shift_selection(prompt_bufnr, p_scroller.worse(picker.sorting_strategy)) +end + +--- Move the selection to the entry that has a better score +---@param prompt_bufnr number: The prompt bufnr +actions.move_selection_better = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + action_set.shift_selection(prompt_bufnr, p_scroller.better(picker.sorting_strategy)) +end + +--- Move to the top of the picker +---@param prompt_bufnr number: The prompt bufnr +actions.move_to_top = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:set_selection( + p_scroller.top(current_picker.sorting_strategy, current_picker.max_results, current_picker.manager:num_results()) + ) +end + +--- Move to the middle of the picker +---@param prompt_bufnr number: The prompt bufnr +actions.move_to_middle = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:set_selection( + p_scroller.middle(current_picker.sorting_strategy, current_picker.max_results, current_picker.manager:num_results()) + ) +end + +--- Move to the bottom of the picker +---@param prompt_bufnr number: The prompt bufnr +actions.move_to_bottom = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:set_selection( + p_scroller.bottom(current_picker.sorting_strategy, current_picker.max_results, current_picker.manager:num_results()) + ) +end + +--- Add current entry to multi select +---@param prompt_bufnr number: The prompt bufnr +actions.add_selection = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:add_selection(current_picker:get_selection_row()) +end + +--- Remove current entry from multi select +---@param prompt_bufnr number: The prompt bufnr +actions.remove_selection = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:remove_selection(current_picker:get_selection_row()) +end + +--- Toggle current entry status for multi select +---@param prompt_bufnr number: The prompt bufnr +actions.toggle_selection = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:toggle_selection(current_picker:get_selection_row()) +end + +--- Multi select all entries. +--- - Note: selected entries may include results not visible in the results popup. +---@param prompt_bufnr number: The prompt bufnr +actions.select_all = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + action_utils.map_entries(prompt_bufnr, function(entry, _, row) + if not current_picker._multi:is_selected(entry) then + current_picker._multi:add(entry) + if current_picker:can_select_row(row) then + local caret = current_picker:update_prefix(entry, row) + if current_picker._selection_entry == entry and current_picker._selection_row == row then + current_picker.highlighter:hi_selection(row, caret:match "(.*%S)") + end + current_picker.highlighter:hi_multiselect(row, current_picker._multi:is_selected(entry)) + end + end + end) + current_picker:get_status_updater(current_picker.prompt_win, current_picker.prompt_bufnr)() +end + +--- Drop all entries from the current multi selection. +---@param prompt_bufnr number: The prompt bufnr +actions.drop_all = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + action_utils.map_entries(prompt_bufnr, function(entry, _, row) + current_picker._multi:drop(entry) + if current_picker:can_select_row(row) then + local caret = current_picker:update_prefix(entry, row) + if current_picker._selection_entry == entry and current_picker._selection_row == row then + current_picker.highlighter:hi_selection(row, caret:match "(.*%S)") + end + current_picker.highlighter:hi_multiselect(row, current_picker._multi:is_selected(entry)) + end + end) + current_picker:get_status_updater(current_picker.prompt_win, current_picker.prompt_bufnr)() +end + +--- Toggle multi selection for all entries. +--- - Note: toggled entries may include results not visible in the results popup. +---@param prompt_bufnr number: The prompt bufnr +actions.toggle_all = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + action_utils.map_entries(prompt_bufnr, function(entry, _, row) + current_picker._multi:toggle(entry) + if current_picker:can_select_row(row) then + local caret = current_picker:update_prefix(entry, row) + if current_picker._selection_entry == entry and current_picker._selection_row == row then + current_picker.highlighter:hi_selection(row, caret:match "(.*%S)") + end + current_picker.highlighter:hi_multiselect(row, current_picker._multi:is_selected(entry)) + end + end) + current_picker:get_status_updater(current_picker.prompt_win, current_picker.prompt_bufnr)() +end + +--- Scroll the preview window up +---@param prompt_bufnr number: The prompt bufnr +actions.preview_scrolling_up = function(prompt_bufnr) + action_set.scroll_previewer(prompt_bufnr, -1) +end + +--- Scroll the preview window down +---@param prompt_bufnr number: The prompt bufnr +actions.preview_scrolling_down = function(prompt_bufnr) + action_set.scroll_previewer(prompt_bufnr, 1) +end + +--- Scroll the results window up +---@param prompt_bufnr number: The prompt bufnr +actions.results_scrolling_up = function(prompt_bufnr) + action_set.scroll_results(prompt_bufnr, -1) +end + +--- Scroll the results window down +---@param prompt_bufnr number: The prompt bufnr +actions.results_scrolling_down = function(prompt_bufnr) + action_set.scroll_results(prompt_bufnr, 1) +end + +--- Center the cursor in the window, can be used after selecting a file to edit +--- You can just map `actions.select_default + actions.center` +---@param prompt_bufnr number: The prompt bufnr +actions.center = function(prompt_bufnr) + vim.cmd ":normal! zz" +end + +--- Perform default action on selection, usually something like
+--- `:edit ` +--- +--- i.e. open the selection in the current buffer +---@param prompt_bufnr number: The prompt bufnr +actions.select_default = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "default") + end, +} + +--- Perform 'horizontal' action on selection, usually something like
+---`:new ` +--- +--- i.e. open the selection in a new horizontal split +---@param prompt_bufnr number: The prompt bufnr +actions.select_horizontal = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "horizontal") + end, +} + +--- Perform 'vertical' action on selection, usually something like
+---`:vnew ` +--- +--- i.e. open the selection in a new vertical split +---@param prompt_bufnr number: The prompt bufnr +actions.select_vertical = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "vertical") + end, +} + +--- Perform 'tab' action on selection, usually something like
+---`:tabedit ` +--- +--- i.e. open the selection in a new tab +---@param prompt_bufnr number: The prompt bufnr +actions.select_tab = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "tab") + end, +} + +--- Perform 'drop' action on selection, usually something like
+---`:drop ` +--- +--- i.e. open the selection in a window +---@param prompt_bufnr number: The prompt bufnr +actions.select_drop = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "drop") + end, +} + +--- Perform 'tab drop' action on selection, usually something like
+---`:tab drop ` +--- +--- i.e. open the selection in a new tab +---@param prompt_bufnr number: The prompt bufnr +actions.select_tab_drop = { + pre = function(prompt_bufnr) + action_state + .get_current_history() + :append(action_state.get_current_line(), action_state.get_current_picker(prompt_bufnr)) + end, + action = function(prompt_bufnr) + return action_set.select(prompt_bufnr, "tab drop") + end, +} + +-- TODO: consider adding float! +-- https://github.com/nvim-telescope/telescope.nvim/issues/365 + +--- Perform file edit on selection, usually something like
+--- `:edit ` +---@param prompt_bufnr number: The prompt bufnr +actions.file_edit = function(prompt_bufnr) + return action_set.edit(prompt_bufnr, "edit") +end + +--- Perform file split on selection, usually something like
+--- `:new ` +---@param prompt_bufnr number: The prompt bufnr +actions.file_split = function(prompt_bufnr) + return action_set.edit(prompt_bufnr, "new") +end + +--- Perform file vsplit on selection, usually something like
+--- `:vnew ` +---@param prompt_bufnr number: The prompt bufnr +actions.file_vsplit = function(prompt_bufnr) + return action_set.edit(prompt_bufnr, "vnew") +end + +--- Perform file tab on selection, usually something like
+--- `:tabedit ` +---@param prompt_bufnr number: The prompt bufnr +actions.file_tab = function(prompt_bufnr) + return action_set.edit(prompt_bufnr, "tabedit") +end + +actions.close_pum = function(_) + if 0 ~= vim.fn.pumvisible() then + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, true, true), "n", true) + end +end + +--- Close the Telescope window, usually used within an action +---@param prompt_bufnr number: The prompt bufnr +actions.close = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + local original_win_id = picker.original_win_id + local cursor_valid, original_cursor = pcall(a.nvim_win_get_cursor, original_win_id) + + actions.close_pum(prompt_bufnr) + + require("telescope.pickers").on_close_prompt(prompt_bufnr) + pcall(a.nvim_set_current_win, original_win_id) + if cursor_valid and a.nvim_get_mode().mode == "i" and picker._original_mode ~= "i" then + pcall(a.nvim_win_set_cursor, original_win_id, { original_cursor[1], original_cursor[2] + 1 }) + end +end + +--- Close the Telescope window, usually used within an action
+--- Deprecated and no longer needed, does the same as |telescope.actions.close|. Might be removed in the future +---@deprecated +---@param prompt_bufnr number: The prompt bufnr +actions._close = function(prompt_bufnr) + actions.close(prompt_bufnr) +end + +local set_edit_line = function(prompt_bufnr, fname, prefix, postfix) + postfix = vim.F.if_nil(postfix, "") + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection(fname) + return + end + actions.close(prompt_bufnr) + a.nvim_feedkeys(a.nvim_replace_termcodes(prefix .. selection.value .. postfix, true, false, true), "t", true) +end + +--- Set a value in the command line and dont run it, making it editable. +---@param prompt_bufnr number: The prompt bufnr +actions.edit_command_line = function(prompt_bufnr) + set_edit_line(prompt_bufnr, "actions.edit_command_line", ":") +end + +--- Set a value in the command line and run it +---@param prompt_bufnr number: The prompt bufnr +actions.set_command_line = function(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.set_command_line" + return + end + actions.close(prompt_bufnr) + vim.fn.histadd("cmd", selection.value) + vim.cmd(selection.value) +end + +--- Set a value in the search line and dont search for it, making it editable. +---@param prompt_bufnr number: The prompt bufnr +actions.edit_search_line = function(prompt_bufnr) + set_edit_line(prompt_bufnr, "actions.edit_search_line", "/") +end + +--- Set a value in the search line and search for it +---@param prompt_bufnr number: The prompt bufnr +actions.set_search_line = function(prompt_bufnr) + set_edit_line(prompt_bufnr, "actions.set_search_line", "/", "") +end + +--- Edit a register +---@param prompt_bufnr number: The prompt bufnr +actions.edit_register = function(prompt_bufnr) + local selection = action_state.get_selected_entry() + local picker = action_state.get_current_picker(prompt_bufnr) + + vim.fn.inputsave() + local updated_value = vim.fn.input("Edit [" .. selection.value .. "] ❯ ", selection.content) + vim.fn.inputrestore() + if updated_value ~= selection.content then + vim.fn.setreg(selection.value, updated_value) + selection.content = updated_value + end + + -- update entry in results table + -- TODO: find way to redraw finder content + for _, v in pairs(picker.finder.results) do + if v == selection then + v.content = updated_value + end + end + -- print(vim.inspect(picker.finder.results)) +end + +--- Paste the selected register into the buffer +--- +--- Note: only meant to be used inside builtin.registers +---@param prompt_bufnr number: The prompt bufnr +actions.paste_register = function(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.paste_register" + return + end + + actions.close(prompt_bufnr) + + -- ensure that the buffer can be written to + if vim.api.nvim_buf_get_option(vim.api.nvim_get_current_buf(), "modifiable") then + vim.api.nvim_paste(selection.content, true, -1) + end +end + +--- Insert a symbol into the current buffer (while switching to normal mode) +---@param prompt_bufnr number: The prompt bufnr +actions.insert_symbol = function(prompt_bufnr) + local symbol = action_state.get_selected_entry().value[1] + actions.close(prompt_bufnr) + vim.api.nvim_put({ symbol }, "", true, true) +end + +--- Insert a symbol into the current buffer and keeping the insert mode. +---@param prompt_bufnr number: The prompt bufnr +actions.insert_symbol_i = function(prompt_bufnr) + local symbol = action_state.get_selected_entry().value[1] + actions.close(prompt_bufnr) + vim.schedule(function() + vim.cmd [[startinsert]] + vim.api.nvim_put({ symbol }, "", true, true) + end) +end + +-- TODO: Think about how to do this. +actions.insert_value = function(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.insert_value" + return + end + + vim.schedule(function() + actions.close(prompt_bufnr) + end) + + return selection.value +end + +--- Create and checkout a new git branch if it doesn't already exist +---@param prompt_bufnr number: The prompt bufnr +actions.git_create_branch = function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local new_branch = action_state.get_current_line() + + if new_branch == "" then + utils.notify("actions.git_create_branch", { + msg = "Missing the new branch name", + level = "ERROR", + }) + else + local confirmation = vim.fn.input(string.format("Create new branch '%s'? [y/n]: ", new_branch)) + if string.len(confirmation) == 0 or string.sub(string.lower(confirmation), 0, 1) ~= "y" then + utils.notify("actions.git_create_branch", { + msg = string.format("fail to create branch: '%s'", new_branch), + level = "ERROR", + }) + return + end + + actions.close(prompt_bufnr) + + local _, ret, stderr = utils.get_os_command_output({ "git", "checkout", "-b", new_branch }, cwd) + if ret == 0 then + utils.notify("actions.git_create_branch", { + msg = string.format("Switched to a new branch: %s", new_branch), + level = "INFO", + }) + else + utils.notify("actions.git_create_branch", { + msg = string.format( + "Error when creating new branch: '%s' Git returned '%s'", + new_branch, + table.concat(stderr, " ") + ), + level = "INFO", + }) + end + end +end + +--- Applies an existing git stash +---@param prompt_bufnr number: The prompt bufnr +actions.git_apply_stash = function(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_apply_stash" + return + end + actions.close(prompt_bufnr) + local _, ret, stderr = utils.get_os_command_output { "git", "stash", "apply", "--index", selection.value } + if ret == 0 then + utils.notify("actions.git_apply_stash", { + msg = string.format("applied: '%s' ", selection.value), + level = "INFO", + }) + else + utils.notify("actions.git_apply_stash", { + msg = string.format("Error when applying: %s. Git returned: '%s'", selection.value, table.concat(stderr, " ")), + level = "ERROR", + }) + end +end + +--- Checkout an existing git branch +---@param prompt_bufnr number: The prompt bufnr +actions.git_checkout = function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_checkout" + return + end + actions.close(prompt_bufnr) + local _, ret, stderr = utils.get_os_command_output({ "git", "checkout", selection.value }, cwd) + if ret == 0 then + utils.notify("actions.git_checkout", { + msg = string.format("Checked out: %s", selection.value), + level = "INFO", + }) + vim.cmd "checktime" + else + utils.notify("actions.git_checkout", { + msg = string.format( + "Error when checking out: %s. Git returned: '%s'", + selection.value, + table.concat(stderr, " ") + ), + level = "ERROR", + }) + end +end + +--- Switch to git branch.
+--- If the branch already exists in local, switch to that. +--- If the branch is only in remote, create new branch tracking remote and switch to new one. +---@param prompt_bufnr number: The prompt bufnr +actions.git_switch_branch = function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_switch_branch" + return + end + actions.close(prompt_bufnr) + local pattern = "^refs/remotes/%w+/" + local branch = selection.value + if string.match(selection.refname, pattern) then + branch = string.gsub(selection.refname, pattern, "") + end + local _, ret, stderr = utils.get_os_command_output({ "git", "switch", branch }, cwd) + if ret == 0 then + utils.notify("actions.git_switch_branch", { + msg = string.format("Switched to: '%s'", branch), + level = "INFO", + }) + else + utils.notify("actions.git_switch_branch", { + msg = string.format( + "Error when switching to: %s. Git returned: '%s'", + selection.value, + table.concat(stderr, " ") + ), + level = "ERROR", + }) + end +end + +local function make_git_branch_action(opts) + return function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection(opts.action_name) + return + end + + local should_confirm = opts.should_confirm + if should_confirm then + local confirmation = vim.fn.input(string.format(opts.confirmation_question, selection.value)) + if confirmation ~= "" and string.lower(confirmation) ~= "y" then + return + end + end + + actions.close(prompt_bufnr) + local _, ret, stderr = utils.get_os_command_output(opts.command(selection.value), cwd) + if ret == 0 then + utils.notify(opts.action_name, { + msg = string.format(opts.success_message, selection.value), + level = "INFO", + }) + else + utils.notify(opts.action_name, { + msg = string.format(opts.error_message, selection.value, table.concat(stderr, " ")), + level = "ERROR", + }) + end + end +end + +--- Tell git to track the currently selected remote branch in Telescope +---@param prompt_bufnr number: The prompt bufnr +actions.git_track_branch = make_git_branch_action { + should_confirm = false, + action_name = "actions.git_track_branch", + success_message = "Tracking branch: %s", + error_message = "Error when tracking branch: %s. Git returned: '%s'", + command = function(branch_name) + return { "git", "checkout", "--track", branch_name } + end, +} + +--- Delete the currently selected branch +---@param prompt_bufnr number: The prompt bufnr +actions.git_delete_branch = make_git_branch_action { + should_confirm = true, + action_name = "actions.git_delete_branch", + confirmation_question = "Do you really wanna delete branch %s? [Y/n] ", + success_message = "Deleted branch: %s", + error_message = "Error when deleting branch: %s. Git returned: '%s'", + command = function(branch_name) + return { "git", "branch", "-D", branch_name } + end, +} + +--- Merge the currently selected branch +---@param prompt_bufnr number: The prompt bufnr +actions.git_merge_branch = make_git_branch_action { + should_confirm = true, + action_name = "actions.git_merge_branch", + confirmation_question = "Do you really wanna merge branch %s? [Y/n] ", + success_message = "Merged branch: %s", + error_message = "Error when merging branch: %s. Git returned: '%s'", + command = function(branch_name) + return { "git", "merge", branch_name } + end, +} + +--- Rebase to selected git branch +---@param prompt_bufnr number: The prompt bufnr +actions.git_rebase_branch = make_git_branch_action { + should_confirm = true, + action_name = "actions.git_rebase_branch", + confirmation_question = "Do you really wanna rebase branch %s? [Y/n] ", + success_message = "Rebased branch: %s", + error_message = "Error when rebasing branch: %s. Git returned: '%s'", + command = function(branch_name) + return { "git", "rebase", branch_name } + end, +} + +local git_reset_branch = function(prompt_bufnr, mode) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_reset_branch" + return + end + + local confirmation = vim.fn.input("Do you really wanna " .. mode .. " reset to " .. selection.value .. "? [Y/n] ") + if confirmation ~= "" and string.lower(confirmation) ~= "y" then + return + end + + actions.close(prompt_bufnr) + local _, ret, stderr = utils.get_os_command_output({ "git", "reset", mode, selection.value }, cwd) + if ret == 0 then + utils.notify("actions.git_rebase_branch", { + msg = string.format("Reset to: '%s'", selection.value), + level = "INFO", + }) + else + utils.notify("actions.git_rebase_branch", { + msg = string.format("Rest to: %s. Git returned: '%s'", selection.value, table.concat(stderr, " ")), + level = "ERROR", + }) + end +end + +--- Reset to selected git commit using mixed mode +---@param prompt_bufnr number: The prompt bufnr +actions.git_reset_mixed = function(prompt_bufnr) + git_reset_branch(prompt_bufnr, "--mixed") +end + +--- Reset to selected git commit using soft mode +---@param prompt_bufnr number: The prompt bufnr +actions.git_reset_soft = function(prompt_bufnr) + git_reset_branch(prompt_bufnr, "--soft") +end + +--- Reset to selected git commit using hard mode +---@param prompt_bufnr number: The prompt bufnr +actions.git_reset_hard = function(prompt_bufnr) + git_reset_branch(prompt_bufnr, "--hard") +end + +--- Checkout a specific file for a given sha +---@param prompt_bufnr number: The prompt bufnr +actions.git_checkout_current_buffer = function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_checkout_current_buffer" + + return + end + actions.close(prompt_bufnr) + utils.get_os_command_output({ "git", "checkout", selection.value, "--", selection.file }, cwd) + vim.cmd "checktime" +end + +--- Stage/unstage selected file +---@param prompt_bufnr number: The prompt bufnr +actions.git_staging_toggle = function(prompt_bufnr) + local cwd = action_state.get_current_picker(prompt_bufnr).cwd + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "actions.git_staging_toggle" + return + end + if selection.status:sub(2) == " " then + utils.get_os_command_output({ "git", "restore", "--staged", selection.value }, cwd) + else + utils.get_os_command_output({ "git", "add", selection.value }, cwd) + end +end + +local entry_to_qf = function(entry) + local text = entry.text + + if not text then + if type(entry.value) == "table" then + text = entry.value.text + else + text = entry.value + end + end + + return { + bufnr = entry.bufnr, + filename = from_entry.path(entry, false, false), + lnum = vim.F.if_nil(entry.lnum, 1), + col = vim.F.if_nil(entry.col, 1), + text = text, + } +end + +local send_selected_to_qf = function(prompt_bufnr, mode, target) + local picker = action_state.get_current_picker(prompt_bufnr) + + local qf_entries = {} + for _, entry in ipairs(picker:get_multi_selection()) do + table.insert(qf_entries, entry_to_qf(entry)) + end + + local prompt = picker:_get_prompt() + actions.close(prompt_bufnr) + + if target == "loclist" then + vim.fn.setloclist(picker.original_win_id, qf_entries, mode) + else + local qf_title = string.format([[%s (%s)]], picker.prompt_title, prompt) + vim.fn.setqflist(qf_entries, mode) + vim.fn.setqflist({}, "a", { title = qf_title }) + end +end + +local send_all_to_qf = function(prompt_bufnr, mode, target) + local picker = action_state.get_current_picker(prompt_bufnr) + local manager = picker.manager + + local qf_entries = {} + for entry in manager:iter() do + table.insert(qf_entries, entry_to_qf(entry)) + end + + local prompt = picker:_get_prompt() + actions.close(prompt_bufnr) + + if target == "loclist" then + vim.fn.setloclist(picker.original_win_id, qf_entries, mode) + else + vim.fn.setqflist(qf_entries, mode) + local qf_title = string.format([[%s (%s)]], picker.prompt_title, prompt) + vim.fn.setqflist({}, "a", { title = qf_title }) + end +end + +--- Sends the selected entries to the quickfix list, replacing the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.send_selected_to_qflist = function(prompt_bufnr) + send_selected_to_qf(prompt_bufnr, " ") +end + +--- Adds the selected entries to the quickfix list, keeping the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.add_selected_to_qflist = function(prompt_bufnr) + send_selected_to_qf(prompt_bufnr, "a") +end + +--- Sends all entries to the quickfix list, replacing the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.send_to_qflist = function(prompt_bufnr) + send_all_to_qf(prompt_bufnr, " ") +end + +--- Adds all entries to the quickfix list, keeping the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.add_to_qflist = function(prompt_bufnr) + send_all_to_qf(prompt_bufnr, "a") +end + +--- Sends the selected entries to the location list, replacing the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.send_selected_to_loclist = function(prompt_bufnr) + send_selected_to_qf(prompt_bufnr, " ", "loclist") +end + +--- Adds the selected entries to the location list, keeping the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.add_selected_to_loclist = function(prompt_bufnr) + send_selected_to_qf(prompt_bufnr, "a", "loclist") +end + +--- Sends all entries to the location list, replacing the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.send_to_loclist = function(prompt_bufnr) + send_all_to_qf(prompt_bufnr, " ", "loclist") +end + +--- Adds all entries to the location list, keeping the previous entries. +---@param prompt_bufnr number: The prompt bufnr +actions.add_to_loclist = function(prompt_bufnr) + send_all_to_qf(prompt_bufnr, "a", "loclist") +end + +local smart_send = function(prompt_bufnr, mode, target) + local picker = action_state.get_current_picker(prompt_bufnr) + if #picker:get_multi_selection() > 0 then + send_selected_to_qf(prompt_bufnr, mode, target) + else + send_all_to_qf(prompt_bufnr, mode, target) + end +end + +--- Sends the selected entries to the quickfix list, replacing the previous entries. +--- If no entry was selected, sends all entries. +---@param prompt_bufnr number: The prompt bufnr +actions.smart_send_to_qflist = function(prompt_bufnr) + smart_send(prompt_bufnr, " ") +end + +--- Adds the selected entries to the quickfix list, keeping the previous entries. +--- If no entry was selected, adds all entries. +---@param prompt_bufnr number: The prompt bufnr +actions.smart_add_to_qflist = function(prompt_bufnr) + smart_send(prompt_bufnr, "a") +end + +--- Sends the selected entries to the location list, replacing the previous entries. +--- If no entry was selected, sends all entries. +---@param prompt_bufnr number: The prompt bufnr +actions.smart_send_to_loclist = function(prompt_bufnr) + smart_send(prompt_bufnr, " ", "loclist") +end + +--- Adds the selected entries to the location list, keeping the previous entries. +--- If no entry was selected, adds all entries. +---@param prompt_bufnr number: The prompt bufnr +actions.smart_add_to_loclist = function(prompt_bufnr) + smart_send(prompt_bufnr, "a", "loclist") +end + +--- Open completion menu containing the tags which can be used to filter the results in a faster way +---@param prompt_bufnr number: The prompt bufnr +actions.complete_tag = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + local tags = current_picker.sorter.tags + local delimiter = current_picker.sorter._delimiter + + if not tags then + utils.notify("actions.complete_tag", { + msg = "No tag pre-filtering set for this picker", + level = "ERROR", + }) + + return + end + + -- format tags to match filter_function + local prefilter_tags = {} + for tag, _ in pairs(tags) do + table.insert(prefilter_tags, string.format("%s%s%s ", delimiter, tag:lower(), delimiter)) + end + + local line = action_state.get_current_line() + local filtered_tags = {} + -- retrigger completion with already selected tag anew + -- trim and add space since we can match [[:pattern: ]] with or without space at the end + if vim.tbl_contains(prefilter_tags, vim.trim(line) .. " ") then + filtered_tags = prefilter_tags + else + -- match tag by substring + for _, tag in pairs(prefilter_tags) do + local start, _ = tag:find(line) + if start then + table.insert(filtered_tags, tag) + end + end + end + + if vim.tbl_isempty(filtered_tags) then + utils.notify("complete_tag", { + msg = "No matches found", + level = "INFO", + }) + return + end + + -- incremental completion by substituting string starting from col - #line byte offset + local col = vim.api.nvim_win_get_cursor(0)[2] + 1 + vim.fn.complete(col - #line, filtered_tags) +end + +--- Cycle to the next search prompt in the history +---@param prompt_bufnr number: The prompt bufnr +actions.cycle_history_next = function(prompt_bufnr) + local history = action_state.get_current_history() + local current_picker = action_state.get_current_picker(prompt_bufnr) + local line = action_state.get_current_line() + + local entry = history:get_next(line, current_picker) + if entry == false then + return + end + + current_picker:reset_prompt() + if entry ~= nil then + current_picker:set_prompt(entry) + end +end + +--- Cycle to the previous search prompt in the history +---@param prompt_bufnr number: The prompt bufnr +actions.cycle_history_prev = function(prompt_bufnr) + local history = action_state.get_current_history() + local current_picker = action_state.get_current_picker(prompt_bufnr) + local line = action_state.get_current_line() + + local entry = history:get_prev(line, current_picker) + if entry == false then + return + end + if entry ~= nil then + current_picker:reset_prompt() + current_picker:set_prompt(entry) + end +end + +--- Open the quickfix list. It makes sense to use this in combination with one of the send_to_qflist actions +--- `actions.smart_send_to_qflist + actions.open_qflist` +---@param prompt_bufnr number: The prompt bufnr +actions.open_qflist = function(prompt_bufnr) + vim.cmd [[copen]] +end + +--- Open the location list. It makes sense to use this in combination with one of the send_to_loclist actions +--- `actions.smart_send_to_qflist + actions.open_qflist` +---@param prompt_bufnr number: The prompt bufnr +actions.open_loclist = function(prompt_bufnr) + vim.cmd [[lopen]] +end + +--- Delete the selected buffer or all the buffers selected using multi selection. +---@param prompt_bufnr number: The prompt bufnr +actions.delete_buffer = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + current_picker:delete_selection(function(selection) + local force = vim.api.nvim_buf_get_option(selection.bufnr, "buftype") == "terminal" + local ok = pcall(vim.api.nvim_buf_delete, selection.bufnr, { force = force }) + return ok + end) +end + +--- Cycle to the next previewer if there is one available.
+--- This action is not mapped on default. +---@param prompt_bufnr number: The prompt bufnr +actions.cycle_previewers_next = function(prompt_bufnr) + action_state.get_current_picker(prompt_bufnr):cycle_previewers(1) +end + +--- Cycle to the previous previewer if there is one available.
+--- This action is not mapped on default. +---@param prompt_bufnr number: The prompt bufnr +actions.cycle_previewers_prev = function(prompt_bufnr) + action_state.get_current_picker(prompt_bufnr):cycle_previewers(-1) +end + +--- Removes the selected picker in |builtin.pickers|.
+--- This action is not mapped by default and only intended for |builtin.pickers|. +---@param prompt_bufnr number: The prompt bufnr +actions.remove_selected_picker = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + local selection_index = current_picker:get_index(current_picker:get_selection_row()) + local cached_pickers = state.get_global_key "cached_pickers" + current_picker:delete_selection(function() + table.remove(cached_pickers, selection_index) + end) + if #cached_pickers == 0 then + actions.close(prompt_bufnr) + end +end + +--- Display the keymaps of registered actions similar to which-key.nvim.
+--- - Notes: +--- - The defaults can be overridden via |action_generate.which_key|. +---@param prompt_bufnr number: The prompt bufnr +actions.which_key = function(prompt_bufnr, opts) + opts = opts or {} + opts.max_height = vim.F.if_nil(opts.max_height, 0.4) + opts.only_show_current_mode = vim.F.if_nil(opts.only_show_current_mode, true) + opts.mode_width = vim.F.if_nil(opts.mode_width, 1) + opts.keybind_width = vim.F.if_nil(opts.keybind_width, 7) + opts.name_width = vim.F.if_nil(opts.name_width, 30) + opts.line_padding = vim.F.if_nil(opts.line_padding, 1) + opts.separator = vim.F.if_nil(opts.separator, " -> ") + opts.close_with_action = vim.F.if_nil(opts.close_with_action, true) + opts.normal_hl = vim.F.if_nil(opts.normal_hl, "TelescopePrompt") + opts.border_hl = vim.F.if_nil(opts.border_hl, "TelescopePromptBorder") + opts.winblend = vim.F.if_nil(opts.winblend, conf.winblend) + opts.column_padding = vim.F.if_nil(opts.column_padding, " ") + + -- Assigning into 'opts.column_indent' would override a number with a string and + -- cause issues with subsequent calls, keep a local copy of the string instead + local column_indent = table.concat(utils.repeated_table(vim.F.if_nil(opts.column_indent, 4), " ")) + + -- close on repeated keypress + local km_bufs = (function() + local ret = {} + local bufs = a.nvim_list_bufs() + for _, buf in ipairs(bufs) do + for _, bufname in ipairs { "_TelescopeWhichKey", "_TelescopeWhichKeyBorder" } do + if string.find(a.nvim_buf_get_name(buf), bufname) then + table.insert(ret, buf) + end + end + end + return ret + end)() + if not vim.tbl_isempty(km_bufs) then + for _, buf in ipairs(km_bufs) do + utils.buf_delete(buf) + local win_ids = vim.fn.win_findbuf(buf) + for _, win_id in ipairs(win_ids) do + pcall(a.nvim_win_close, win_id, true) + end + end + return + end + + local displayer = entry_display.create { + separator = opts.separator, + items = { + { width = opts.mode_width }, + { width = opts.keybind_width }, + { width = opts.name_width }, + }, + } + + local make_display = function(mapping) + return displayer { + { mapping.mode, vim.F.if_nil(opts.mode_hl, "TelescopeResultsConstant") }, + { mapping.keybind, vim.F.if_nil(opts.keybind_hl, "TelescopeResultsVariable") }, + { mapping.name, vim.F.if_nil(opts.name_hl, "TelescopeResultsFunction") }, + } + end + + local mappings = {} + local mode = a.nvim_get_mode().mode + for _, v in pairs(action_utils.get_registered_mappings(prompt_bufnr)) do + -- holds true for registered keymaps + if type(v.func) == "table" then + local name = "" + for _, action in ipairs(v.func) do + if type(action) == "string" then + name = name == "" and action or name .. " + " .. action + end + end + if name and name ~= "which_key" and name ~= "nop" then + if not opts.only_show_current_mode or mode == v.mode then + table.insert(mappings, { mode = v.mode, keybind = v.keybind, name = name }) + end + end + elseif type(v.func) == "function" then + if not opts.only_show_current_mode or mode == v.mode then + local fname = action_utils._get_anon_function_name(v.func) + -- telescope.setup mappings might result in function names that reflect the keys + fname = fname:lower() == v.keybind:lower() and "" or fname + table.insert(mappings, { mode = v.mode, keybind = v.keybind, name = fname }) + if fname == "" then + utils.notify("actions.which_key", { + msg = "No name available for anonymous functions.", + level = "INFO", + once = true, + }) + end + end + end + end + + table.sort(mappings, function(x, y) + if x.name < y.name then + return true + elseif x.name == y.name then + -- show normal mode as the standard mode first + if x.mode > y.mode then + return true + else + return false + end + else + return false + end + end) + + local entry_width = #opts.column_padding + + opts.mode_width + + opts.keybind_width + + opts.name_width + + (3 * #opts.separator) + local num_total_columns = math.floor((vim.o.columns - #column_indent) / entry_width) + opts.num_rows = + math.min(math.ceil(#mappings / num_total_columns), resolver.resolve_height(opts.max_height)(_, _, vim.o.lines)) + local total_available_entries = opts.num_rows * num_total_columns + local winheight = opts.num_rows + 2 * opts.line_padding + + -- place hints at top or bottom relative to prompt + local win_central_row = function(win_nr) + return a.nvim_win_get_position(win_nr)[1] + 0.5 * a.nvim_win_get_height(win_nr) + end + -- TODO(fdschmidt93|l-kershaw): better generalization of where to put which key float + local picker = action_state.get_current_picker(prompt_bufnr) + local prompt_row = win_central_row(picker.prompt_win) + local results_row = win_central_row(picker.results_win) + local preview_row = picker.preview_win and win_central_row(picker.preview_win) or results_row + local prompt_pos = prompt_row < 0.4 * vim.o.lines + or prompt_row < 0.6 * vim.o.lines and results_row + preview_row < vim.o.lines + + local modes = { n = "Normal", i = "Insert" } + local title_mode = opts.only_show_current_mode and modes[mode] .. " Mode " or "" + local title_text = title_mode .. "Keymaps" + local popup_opts = { + relative = "editor", + enter = false, + minwidth = vim.o.columns, + maxwidth = vim.o.columns, + minheight = winheight, + maxheight = winheight, + line = prompt_pos == true and vim.o.lines - winheight + 1 or 1, + col = 0, + border = { prompt_pos and 1 or 0, 0, not prompt_pos and 1 or 0, 0 }, + borderchars = { prompt_pos and "─" or " ", "", not prompt_pos and "─" or " ", "", "", "", "", "" }, + noautocmd = true, + title = { { text = title_text, pos = prompt_pos and "N" or "S" } }, + } + local km_win_id, km_opts = popup.create("", popup_opts) + local km_buf = a.nvim_win_get_buf(km_win_id) + a.nvim_buf_set_name(km_buf, "_TelescopeWhichKey") + a.nvim_buf_set_name(km_opts.border.bufnr, "_TelescopeTelescopeWhichKeyBorder") + a.nvim_win_set_option(km_win_id, "winhl", "Normal:" .. opts.normal_hl) + a.nvim_win_set_option(km_opts.border.win_id, "winhl", "Normal:" .. opts.border_hl) + a.nvim_win_set_option(km_win_id, "winblend", opts.winblend) + a.nvim_win_set_option(km_win_id, "foldenable", false) + + vim.api.nvim_create_autocmd("BufLeave", { + buffer = km_buf, + once = true, + callback = function() + pcall(vim.api.nvim_win_close, km_win_id, true) + pcall(vim.api.nvim_win_close, km_opts.border.win_id, true) + require("telescope.utils").buf_delete(km_buf) + end, + }) + + a.nvim_buf_set_lines(km_buf, 0, -1, false, utils.repeated_table(opts.num_rows + 2 * opts.line_padding, column_indent)) + + local keymap_highlights = a.nvim_create_namespace "telescope_whichkey" + local highlights = {} + for index, mapping in ipairs(mappings) do + local row = utils.cycle(index, opts.num_rows) - 1 + opts.line_padding + local prev_line = a.nvim_buf_get_lines(km_buf, row, row + 1, false)[1] + if index == total_available_entries and total_available_entries > #mappings then + local new_line = prev_line .. "..." + a.nvim_buf_set_lines(km_buf, row, row + 1, false, { new_line }) + break + end + local display, display_hl = make_display(mapping) + local new_line = prev_line .. display .. opts.column_padding -- incl. padding + a.nvim_buf_set_lines(km_buf, row, row + 1, false, { new_line }) + table.insert(highlights, { hl = display_hl, row = row, col = #prev_line }) + end + + -- highlighting only after line setting as vim.api.nvim_buf_set_lines removes hl otherwise + for _, highlight_tbl in pairs(highlights) do + local highlight = highlight_tbl.hl + local row_ = highlight_tbl.row + local col = highlight_tbl.col + for _, hl_block in ipairs(highlight) do + a.nvim_buf_add_highlight(km_buf, keymap_highlights, hl_block[2], row_, col + hl_block[1][1], col + hl_block[1][2]) + end + end + + -- only set up autocommand after showing preview completed + if opts.close_with_action then + vim.schedule(function() + vim.api.nvim_create_autocmd("User TelescopeKeymap", { + once = true, + callback = function() + pcall(vim.api.nvim_win_close, km_win_id, true) + pcall(vim.api.nvim_win_close, km_opts.border.win_id, true) + require("telescope.utils").buf_delete(km_buf) + end, + }) + end) + end +end + +--- Move from a none fuzzy search to a fuzzy one
+--- This action is meant to be used in live_grep and lsp_dynamic_workspace_symbols +---@param prompt_bufnr number: The prompt bufnr +actions.to_fuzzy_refine = function(prompt_bufnr) + local line = action_state.get_current_line() + local prefix = (function() + local title = action_state.get_current_picker(prompt_bufnr).prompt_title + if title == "Live Grep" then + return "Find Word" + elseif title == "LSP Dynamic Workspace Symbols" then + return "LSP Workspace Symbols" + else + return "Fuzzy over" + end + end)() + + require("telescope.actions.generate").refine(prompt_bufnr, { + prompt_title = string.format("%s (%s)", prefix, line), + sorter = conf.generic_sorter {}, + }) +end + +actions.nop = function(_) end + +-- ================================================== +-- Transforms modules and sets the correct metatables. +-- ================================================== +actions = transform_mod(actions) +return actions diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/layout.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/layout.lua new file mode 100644 index 0000000..0e8b27a --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/layout.lua @@ -0,0 +1,148 @@ +---@tag telescope.actions.layout +---@config { ["module"] = "telescope.actions.layout", ["name"] = "ACTIONS_LAYOUT" } + +---@brief [[ +--- The layout actions are actions to be used to change the layout of a picker. +---@brief ]] + +local action_state = require "telescope.actions.state" +local state = require "telescope.state" +local layout_strats = require "telescope.pickers.layout_strategies" + +local transform_mod = require("telescope.actions.mt").transform_mod + +local action_layout = setmetatable({}, { + __index = function(_, k) + error("'telescope.actions.layout' does not have a value: " .. tostring(k)) + end, +}) + +--- Toggle preview window. +--- - Note: preview window can be toggled even if preview is set to false. +--- +--- This action is not mapped by default. +---@param prompt_bufnr number: The prompt bufnr +action_layout.toggle_preview = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + local status = state.get_status(picker.prompt_bufnr) + + if picker.previewer and status.preview_win then + picker.hidden_previewer = picker.previewer + picker.previewer = nil + elseif picker.hidden_previewer and not status.preview_win then + picker.previewer = picker.hidden_previewer + picker.hidden_previewer = nil + else + return + end + picker:full_layout_update() +end + +-- TODO IMPLEMENT (mentored project available, contact @l-kershaw) +action_layout.toggle_padding = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + -- if padding ~= 0 + -- 1. Save `height` and `width` of picker + -- 2. Set both to `{padding = 0}` + -- else + -- 1. Lookup previous `height` and `width` of picker + -- 2. Set both to previous values + picker:full_layout_update() +end + +--- Toggles the `prompt_position` option between "top" and "bottom". +--- Checks if `prompt_position` is an option for the current layout. +--- +--- This action is not mapped by default. +---@param prompt_bufnr number: The prompt bufnr +action_layout.toggle_prompt_position = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + picker.layout_config = picker.layout_config or {} + picker.layout_config[picker.layout_strategy] = picker.layout_config[picker.layout_strategy] or {} + -- flex layout is weird and needs handling separately + if picker.layout_strategy == "flex" then + picker.layout_config.flex.horizontal = picker.layout_config.flex.horizontal or {} + picker.layout_config.flex.vertical = picker.layout_config.flex.vertical or {} + local old_pos = picker.layout_config.flex[picker.__flex_strategy].prompt_position + local new_pos = old_pos == "top" and "bottom" or "top" + picker.layout_config[picker.__flex_strategy].prompt_position = new_pos + picker.layout_config.flex[picker.__flex_strategy].prompt_position = new_pos + picker:full_layout_update() + elseif layout_strats._configurations[picker.layout_strategy].prompt_position then + if picker.layout_config.prompt_position == "top" then + picker.layout_config.prompt_position = "bottom" + picker.layout_config[picker.layout_strategy].prompt_position = "bottom" + else + picker.layout_config.prompt_position = "top" + picker.layout_config[picker.layout_strategy].prompt_position = "top" + end + picker:full_layout_update() + end +end + +--- Toggles the `mirror` option between `true` and `false`. +--- Checks if `mirror` is an option for the current layout. +--- +--- This action is not mapped by default. +---@param prompt_bufnr number: The prompt bufnr +action_layout.toggle_mirror = function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + -- flex layout is weird and needs handling separately + if picker.layout_strategy == "flex" then + picker.layout_config.flex.horizontal = picker.layout_config.flex.horizontal or {} + picker.layout_config.flex.vertical = picker.layout_config.flex.vertical or {} + local new_mirror = not picker.layout_config.flex[picker.__flex_strategy].mirror + picker.layout_config[picker.__flex_strategy].mirror = new_mirror + picker.layout_config.flex[picker.__flex_strategy].mirror = new_mirror + picker:full_layout_update() + elseif layout_strats._configurations[picker.layout_strategy].mirror then + picker.layout_config = picker.layout_config or {} + local new_mirror = not picker.layout_config.mirror + picker.layout_config.mirror = new_mirror + picker.layout_config[picker.layout_strategy] = picker.layout_config[picker.layout_strategy] or {} + picker.layout_config[picker.layout_strategy].mirror = new_mirror + picker:full_layout_update() + end +end + +-- Helper function for `cycle_layout_next` and `cycle_layout_prev`. +local get_cycle_layout = function(dir) + return function(prompt_bufnr) + local picker = action_state.get_current_picker(prompt_bufnr) + if picker.__layout_index then + picker.__layout_index = ((picker.__layout_index + dir - 1) % #picker.__cycle_layout_list) + 1 + else + picker.__layout_index = 1 + end + local new_layout = picker.__cycle_layout_list[picker.__layout_index] + if type(new_layout) == "string" then + picker.layout_strategy = new_layout + picker.layout_config = {} + picker.previewer = picker.all_previewers and picker.all_previewers[1] or nil + elseif type(new_layout) == "table" then + picker.layout_strategy = new_layout.layout_strategy + picker.layout_config = new_layout.layout_config or {} + picker.previewer = (new_layout.previewer == nil and picker.all_previewers[picker.current_previewer_index]) + or new_layout.previewer + else + error("Not a valid layout setup: " .. vim.inspect(new_layout) .. "\nShould be a string or a table") + end + + picker:full_layout_update() + end +end + +--- Cycles to the next layout in `cycle_layout_list`. +--- +--- This action is not mapped by default. +---@param prompt_bufnr number: The prompt bufnr +action_layout.cycle_layout_next = get_cycle_layout(1) + +--- Cycles to the previous layout in `cycle_layout_list`. +--- +--- This action is not mapped by default. +---@param prompt_bufnr number: The prompt bufnr +action_layout.cycle_layout_prev = get_cycle_layout(-1) + +action_layout = transform_mod(action_layout) +return action_layout diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/mt.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/mt.lua new file mode 100644 index 0000000..07b1e42 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/mt.lua @@ -0,0 +1,210 @@ +local action_mt = {} + +--- Checks all replacement combinations to determine which function to run. +--- If no replacement can be found, then it will run the original function +local run_replace_or_original = function(replacements, original_func, ...) + for _, replacement_map in ipairs(replacements or {}) do + for condition, replacement in pairs(replacement_map) do + if condition == true or condition(...) then + return replacement(...) + end + end + end + + return original_func(...) +end + +local append_action_copy = function(new, v, old) + table.insert(new, v) + new._func[v] = old._func[v] + new._static_pre[v] = old._static_pre[v] + new._pre[v] = old._pre[v] + new._replacements[v] = old._replacements[v] + new._static_post[v] = old._static_post[v] + new._post[v] = old._post[v] +end + +-- TODO(conni2461): Not a fan of this solution/hack. Needs to be addressed +local all_mts = {} + +--TODO(conni2461): It gets worse. This is so bad but because we have now n mts for n actions +-- We have to check all actions for relevant mts to set replace and before, after +-- Its not bad for performance because its being called on startup when we attach mappings. +-- Its just a bad solution +local find_all_relevant_mts = function(action_name, f) + for _, mt in ipairs(all_mts) do + for fun, _ in pairs(mt._func) do + if fun == action_name then + f(mt) + end + end + end +end + +--- an action is metatable which allows replacement(prepend or append) of the function +---@class Action +---@field _func table: the original action function +---@field _static_pre table: will allways run before the function even if its replaced +---@field _pre table: the functions that will run before the action +---@field _replacements table: the function that replaces this action +---@field _static_post table: will allways run after the function even if its replaced +---@field _post table: the functions that will run after the action +action_mt.create = function() + local mt = { + __call = function(t, ...) + local values = {} + for _, action_name in ipairs(t) do + if t._static_pre[action_name] then + t._static_pre[action_name](...) + end + if vim.tbl_isempty(t._replacements) and t._pre[action_name] then + t._pre[action_name](...) + end + + local result = { + run_replace_or_original(t._replacements[action_name], t._func[action_name], ...), + } + for _, res in ipairs(result) do + table.insert(values, res) + end + + if t._static_post[action_name] then + t._static_post[action_name](...) + end + if vim.tbl_isempty(t._replacements) and t._post[action_name] then + t._post[action_name](...) + end + end + + return unpack(values) + end, + + __add = function(lhs, rhs) + local new_action = setmetatable({}, action_mt.create()) + for _, v in ipairs(lhs) do + append_action_copy(new_action, v, lhs) + end + + for _, v in ipairs(rhs) do + append_action_copy(new_action, v, rhs) + end + new_action.clear = function() + lhs.clear() + rhs.clear() + end + + return new_action + end, + + _func = {}, + _static_pre = {}, + _pre = {}, + _replacements = {}, + _static_post = {}, + _post = {}, + } + + mt.__index = mt + + mt.clear = function() + mt._pre = {} + mt._replacements = {} + mt._post = {} + end + + --- Replace the reference to the function with a new one temporarily + function mt:replace(v) + assert(#self == 1, "Cannot replace an already combined action") + + return self:replace_map { [true] = v } + end + + function mt:replace_if(condition, replacement) + assert(#self == 1, "Cannot replace an already combined action") + + return self:replace_map { [condition] = replacement } + end + + --- Replace table with + -- Example: + -- + -- actions.select:replace_map { + -- [function() return filetype == 'lua' end] = actions.file_split, + -- [function() return filetype == 'other' end] = actions.file_split_edit, + -- } + function mt:replace_map(tbl) + assert(#self == 1, "Cannot replace an already combined action") + + local action_name = self[1] + find_all_relevant_mts(action_name, function(another) + if not another._replacements[action_name] then + another._replacements[action_name] = {} + end + + table.insert(another._replacements[action_name], 1, tbl) + end) + + return self + end + + function mt:enhance(opts) + assert(#self == 1, "Cannot enhance already combined actions") + + local action_name = self[1] + find_all_relevant_mts(action_name, function(another) + if opts.pre then + another._pre[action_name] = opts.pre + end + + if opts.post then + another._post[action_name] = opts.post + end + end) + + return self + end + + table.insert(all_mts, mt) + return mt +end + +action_mt.transform = function(k, mt, _, v) + local res = setmetatable({ k }, mt) + if type(v) == "table" then + res._static_pre[k] = v.pre + res._static_post[k] = v.post + res._func[k] = v.action + else + res._func[k] = v + end + return res +end + +action_mt.transform_mod = function(mod) + -- Pass the metatable of the module if applicable. + -- This allows for custom errors, lookups, etc. + local redirect = setmetatable({}, getmetatable(mod) or {}) + + for k, v in pairs(mod) do + local mt = action_mt.create() + redirect[k] = action_mt.transform(k, mt, _, v) + end + + redirect._clear = function() + for k, v in pairs(redirect) do + if k ~= "_clear" then + pcall(v.clear) + end + end + end + + return redirect +end + +action_mt.clear_all = function() + for _, v in ipairs(all_mts) do + pcall(v.clear) + end +end + +return action_mt diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/set.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/set.lua new file mode 100644 index 0000000..11d379b --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/set.lua @@ -0,0 +1,225 @@ +---@tag telescope.actions.set +---@config { ["module"] = "telescope.actions.set", ["name"] = "ACTIONS_SET" } + +---@brief [[ +--- Telescope action sets are used to provide an interface for managing +--- actions that all primarily do the same thing, but with slight tweaks. +--- +--- For example, when editing files you may want it in the current split, +--- a vertical split, etc. Instead of making users have to overwrite EACH +--- of those every time they want to change this behavior, they can instead +--- replace the `set` itself and then it will work great and they're done. +---@brief ]] + +local a = vim.api + +local log = require "telescope.log" +local Path = require "plenary.path" +local state = require "telescope.state" +local utils = require "telescope.utils" + +local action_state = require "telescope.actions.state" + +local transform_mod = require("telescope.actions.mt").transform_mod + +local action_set = setmetatable({}, { + __index = function(_, k) + error("'telescope.actions.set' does not have a value: " .. tostring(k)) + end, +}) + +--- Move the current selection of a picker {change} rows. +--- Handles not overflowing / underflowing the list. +---@param prompt_bufnr number: The prompt bufnr +---@param change number: The amount to shift the selection by +action_set.shift_selection = function(prompt_bufnr, change) + local count = vim.v.count + count = count == 0 and 1 or count + count = a.nvim_get_mode().mode == "n" and count or 1 + action_state.get_current_picker(prompt_bufnr):move_selection(change * count) +end + +--- Select the current entry. This is the action set to overwrite common +--- actions by the user. +--- +--- By default maps to editing a file. +---@param prompt_bufnr number: The prompt bufnr +---@param type string: The type of selection to make +-- Valid types include: "default", "horizontal", "vertical", "tabedit" +action_set.select = function(prompt_bufnr, type) + return action_set.edit(prompt_bufnr, action_state.select_key_to_edit_key(type)) +end + +-- goal: currently we have a workaround in actions/init.lua where we do this for all files +-- action_set.select = { +-- -- Will not be called if `select_default` is replaced rather than `action_set.select` because we never get here +-- pre = function(prompt_bufnr) +-- action_state.get_current_history():append( +-- action_state.get_current_line(), +-- action_state.get_current_picker(prompt_bufnr) +-- ) +-- end, +-- action = function(prompt_bufnr, type) +-- return action_set.edit(prompt_bufnr, action_state.select_key_to_edit_key(type)) +-- end +-- } + +local edit_buffer +do + local map = { + drop = "drop", + ["tab drop"] = "tab drop", + edit = "buffer", + new = "sbuffer", + vnew = "vert sbuffer", + tabedit = "tab sb", + } + + edit_buffer = function(command, bufnr) + command = map[command] + if command == nil then + error "There was no associated buffer command" + end + if command ~= "drop" and command ~= "tab drop" then + vim.cmd(string.format("%s %d", command, bufnr)) + else + vim.cmd(string.format("%s %s", command, vim.api.nvim_buf_get_name(bufnr))) + end + end +end + +--- Edit a file based on the current selection. +---@param prompt_bufnr number: The prompt bufnr +---@param command string: The command to use to open the file. +-- Valid commands include: "edit", "new", "vedit", "tabedit" +action_set.edit = function(prompt_bufnr, command) + local entry = action_state.get_selected_entry() + + if not entry then + utils.notify("actions.set.edit", { + msg = "Nothing currently selected", + level = "WARN", + }) + return + end + + local filename, row, col + + if entry.path or entry.filename then + filename = entry.path or entry.filename + + -- TODO: Check for off-by-one + row = entry.row or entry.lnum + col = vim.F.if_nil(entry.col, 1) + elseif not entry.bufnr then + -- TODO: Might want to remove this and force people + -- to put stuff into `filename` + local value = entry.value + if not value then + utils.notify("actions.set.edit", { + msg = "Could not do anything with blank line...", + level = "WARN", + }) + return + end + + if type(value) == "table" then + value = entry.display + end + + local sections = vim.split(value, ":") + + filename = sections[1] + row = tonumber(sections[2]) + col = tonumber(sections[3]) + end + + local entry_bufnr = entry.bufnr + + local picker = action_state.get_current_picker(prompt_bufnr) + require("telescope.pickers").on_close_prompt(prompt_bufnr) + pcall(vim.api.nvim_set_current_win, picker.original_win_id) + local win_id = picker.get_selection_window(picker, entry) + + if picker.push_cursor_on_edit then + vim.cmd "normal! m'" + end + + if picker.push_tagstack_on_edit then + local from = { vim.fn.bufnr "%", vim.fn.line ".", vim.fn.col ".", 0 } + local items = { { tagname = vim.fn.expand "", from = from } } + vim.fn.settagstack(vim.fn.win_getid(), { items = items }, "t") + end + + if win_id ~= 0 and a.nvim_get_current_win() ~= win_id then + vim.api.nvim_set_current_win(win_id) + end + + if entry_bufnr then + if not vim.api.nvim_buf_get_option(entry_bufnr, "buflisted") then + vim.api.nvim_buf_set_option(entry_bufnr, "buflisted", true) + end + edit_buffer(command, entry_bufnr) + else + -- check if we didn't pick a different buffer + -- prevents restarting lsp server + if vim.api.nvim_buf_get_name(0) ~= filename or command ~= "edit" then + filename = Path:new(vim.fn.fnameescape(filename)):normalize(vim.loop.cwd()) + pcall(vim.cmd, string.format("%s %s", command, filename)) + end + end + + if row and col then + local ok, err_msg = pcall(a.nvim_win_set_cursor, 0, { row, col }) + if not ok then + log.debug("Failed to move to cursor:", err_msg, row, col) + end + end +end + +--- Scrolls the previewer up or down. +--- Defaults to a half page scroll, but can be overridden using the `scroll_speed` +--- option in `layout_config`. See |telescope.layout| for more details. +---@param prompt_bufnr number: The prompt bufnr +---@param direction number: The direction of the scrolling +-- Valid directions include: "1", "-1" +action_set.scroll_previewer = function(prompt_bufnr, direction) + local previewer = action_state.get_current_picker(prompt_bufnr).previewer + local status = state.get_status(prompt_bufnr) + + -- Check if we actually have a previewer and a preview window + if type(previewer) ~= "table" or previewer.scroll_fn == nil or status.preview_win == nil then + return + end + + local default_speed = vim.api.nvim_win_get_height(status.preview_win) / 2 + local speed = status.picker.layout_config.scroll_speed or default_speed + + previewer:scroll_fn(math.floor(speed * direction)) +end + +--- Scrolls the results up or down. +--- Defaults to a half page scroll, but can be overridden using the `scroll_speed` +--- option in `layout_config`. See |telescope.layout| for more details. +---@param prompt_bufnr number: The prompt bufnr +---@param direction number: The direction of the scrolling +-- Valid directions include: "1", "-1" +action_set.scroll_results = function(prompt_bufnr, direction) + local status = state.get_status(prompt_bufnr) + local default_speed = vim.api.nvim_win_get_height(status.results_win) / 2 + local speed = status.picker.layout_config.scroll_speed or default_speed + + local input = direction > 0 and [[]] or [[]] + + vim.api.nvim_win_call(status.results_win, function() + vim.cmd([[normal! ]] .. math.floor(speed) .. input) + end) + + action_set.shift_selection(prompt_bufnr, math.floor(speed) * direction) +end + +-- ================================================== +-- Transforms modules and sets the corect metatables. +-- ================================================== +action_set = transform_mod(action_set) +return action_set diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/state.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/state.lua new file mode 100644 index 0000000..d5109ed --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/state.lua @@ -0,0 +1,58 @@ +---@tag telescope.actions.state +---@config { ["module"] = "telescope.actions.state", ["name"] = "ACTIONS_STATE" } + +---@brief [[ +--- Functions to be used to determine the current state of telescope. +--- +--- Generally used from within other |telescope.actions| +---@brief ]] + +local global_state = require "telescope.state" +local conf = require("telescope.config").values + +local action_state = {} + +--- Get the current entry +function action_state.get_selected_entry() + return global_state.get_global_key "selected_entry" +end + +--- Gets the current line +function action_state.get_current_line() + return global_state.get_global_key "current_line" or "" +end + +--- Gets the current picker +---@param prompt_bufnr number: The prompt bufnr +function action_state.get_current_picker(prompt_bufnr) + return global_state.get_status(prompt_bufnr).picker +end + +local select_to_edit_map = { + default = "edit", + horizontal = "new", + vertical = "vnew", + tab = "tabedit", + drop = "drop", + ["tab drop"] = "tab drop", +} +function action_state.select_key_to_edit_key(type) + return select_to_edit_map[type] +end + +function action_state.get_current_history() + local history = global_state.get_global_key "history" + if not history then + if conf.history == false or type(conf.history) ~= "table" then + history = require("telescope.actions.history").get_simple_history() + global_state.set_global_key("history", history) + else + history = conf.history.handler() + global_state.set_global_key("history", history) + end + end + + return history +end + +return action_state diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/utils.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/utils.lua new file mode 100644 index 0000000..8018d64 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/actions/utils.lua @@ -0,0 +1,151 @@ +---@tag telescope.actions.utils +---@config { ["module"] = "telescope.actions.utils", ["name"] = "ACTIONS_UTILS" } + +---@brief [[ +--- Utilities to wrap functions around picker selections and entries. +--- +--- Generally used from within other |telescope.actions| +---@brief ]] + +local action_state = require "telescope.actions.state" + +local utils = {} + +--- Apply `f` to the entries of the current picker. +--- - Notes: +--- - Mapped entries include all currently filtered results, not just the visible onces. +--- - Indices are 1-indexed, whereas rows are 0-indexed. +--- - Warning: `map_entries` has no return value. +--- - The below example showcases how to collect results +--- +--- Usage: +--- +--- local action_state = require "telescope.actions.state" +--- local action_utils = require "telescope.actions.utils" +--- function entry_value_by_row() +--- local prompt_bufnr = vim.api.nvim_get_current_buf() +--- local current_picker = action_state.get_current_picker(prompt_bufnr) +--- local results = {} +--- action_utils.map_entries(prompt_bufnr, function(entry, index, row) +--- results[row] = entry.value +--- end) +--- return results +--- end +--- +---@param prompt_bufnr number: The prompt bufnr +---@param f function: Function to map onto entries of picker that takes (entry, index, row) as viable arguments +function utils.map_entries(prompt_bufnr, f) + vim.validate { + f = { f, "function" }, + } + local current_picker = action_state.get_current_picker(prompt_bufnr) + local index = 1 + -- indices are 1-indexed, rows are 0-indexed + for entry in current_picker.manager:iter() do + local row = current_picker:get_row(index) + f(entry, index, row) + index = index + 1 + end +end + +--- Apply `f` to the multi selections of the current picker and return a table of mapped selections. +--- - Notes: +--- - Mapped selections may include results not visible in the results popup. +--- - Selected entries are returned in order of their selection. +--- - Warning: `map_selections` has no return value. +--- - The below example showcases how to collect results +--- +--- Usage: +--- +--- local action_state = require "telescope.actions.state" +--- local action_utils = require "telescope.actions.utils" +--- function selection_by_index() +--- local prompt_bufnr = vim.api.nvim_get_current_buf() +--- local current_picker = action_state.get_current_picker(prompt_bufnr) +--- local results = {} +--- action_utils.map_selections(prompt_bufnr, function(entry, index) +--- results[index] = entry.value +--- end) +--- return results +--- end +--- +---@param prompt_bufnr number: The prompt bufnr +---@param f function: Function to map onto selection of picker that takes (selection) as a viable argument +function utils.map_selections(prompt_bufnr, f) + vim.validate { + f = { f, "function" }, + } + local current_picker = action_state.get_current_picker(prompt_bufnr) + for _, selection in ipairs(current_picker:get_multi_selection()) do + f(selection) + end +end + +local findnth = function(str, nth) + local array = {} + for i in string.gmatch(str, "%d+") do + table.insert(array, tonumber(i)) + end + return array[nth] +end + +--- Utility to collect mappings of prompt buffer in array of `{mode, keybind, name}`. +---@param prompt_bufnr number: The prompt bufnr +function utils.get_registered_mappings(prompt_bufnr) + local ret = {} + for _, mode in ipairs { "n", "i" } do + local mode_mappings = vim.api.nvim_buf_get_keymap(prompt_bufnr, mode) + for _, mapping in ipairs(mode_mappings) do + -- ensure only telescope mappings + if mapping.rhs and string.find(mapping.rhs, [[require%('telescope.mappings'%).execute_keymap]]) then + local funcid = findnth(mapping.rhs, 2) + table.insert(ret, { mode = mode, keybind = mapping.lhs, func = __TelescopeKeymapStore[prompt_bufnr][funcid] }) + end + end + end + return ret +end + +-- Best effort to infer function names for actions.which_key +function utils._get_anon_function_name(func_ref) + local Path = require "plenary.path" + local info = debug.getinfo(func_ref) + local fname + -- if fn defined in string (ie loadstring) source is string + -- if fn defined in file, source is file name prefixed with a `@´ + local path = Path:new((info.source:gsub("@", ""))) + if not path:exists() then + return "" + end + for i, line in ipairs(path:readlines()) do + if i == info.linedefined then + fname = line + break + end + end + + -- test if assignment or named function, otherwise anon + if (fname:match "=" == nil) and (fname:match "function %S+%(" == nil) then + return "" + else + local patterns = { + { "function", "" }, -- remove function + { "local", "" }, -- remove local + { "[%s=]", "" }, -- remove whitespace and = + { [=[%[["']]=], "" }, -- remove left-hand bracket of table assignment + { [=[["']%]]=], "" }, -- remove right-ahnd bracket of table assignment + { "%((.+)%)", "" }, -- remove function arguments + { "(.+)%.", "" }, -- remove TABLE. prefix if available + } + for _, tbl in ipairs(patterns) do + fname = (fname:gsub(tbl[1], tbl[2])) -- make sure only string is returned + end + -- not sure if this can happen, catch all just in case + if fname == nil or fname == "" then + return "" + end + return fname + end +end + +return utils diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/fzy.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/fzy.lua new file mode 100644 index 0000000..bf322ab --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/fzy.lua @@ -0,0 +1,197 @@ +-- The fzy matching algorithm +-- +-- by Seth Warn +-- a lua port of John Hawthorn's fzy +-- +-- > fzy tries to find the result the user intended. It does this by favouring +-- > matches on consecutive letters and starts of words. This allows matching +-- > using acronyms or different parts of the path." - J Hawthorn + +local has_path, Path = pcall(require, "plenary.path") +if not has_path then + Path = { + path = { + separator = "/", + }, + } +end + +local SCORE_GAP_LEADING = -0.005 +local SCORE_GAP_TRAILING = -0.005 +local SCORE_GAP_INNER = -0.01 +local SCORE_MATCH_CONSECUTIVE = 1.0 +local SCORE_MATCH_SLASH = 0.9 +local SCORE_MATCH_WORD = 0.8 +local SCORE_MATCH_CAPITAL = 0.7 +local SCORE_MATCH_DOT = 0.6 +local SCORE_MAX = math.huge +local SCORE_MIN = -math.huge +local MATCH_MAX_LENGTH = 1024 + +local fzy = {} + +function fzy.has_match(needle, haystack) + needle = string.lower(needle) + haystack = string.lower(haystack) + + local j = 1 + for i = 1, string.len(needle) do + j = string.find(haystack, needle:sub(i, i), j, true) + if not j then + return false + else + j = j + 1 + end + end + + return true +end + +local function is_lower(c) + return c:match "%l" +end + +local function is_upper(c) + return c:match "%u" +end + +local function precompute_bonus(haystack) + local match_bonus = {} + + local last_char = Path.path.sep + for i = 1, string.len(haystack) do + local this_char = haystack:sub(i, i) + if last_char == Path.path.sep then + match_bonus[i] = SCORE_MATCH_SLASH + elseif last_char == "-" or last_char == "_" or last_char == " " then + match_bonus[i] = SCORE_MATCH_WORD + elseif last_char == "." then + match_bonus[i] = SCORE_MATCH_DOT + elseif is_lower(last_char) and is_upper(this_char) then + match_bonus[i] = SCORE_MATCH_CAPITAL + else + match_bonus[i] = 0 + end + + last_char = this_char + end + + return match_bonus +end + +local function compute(needle, haystack, D, M) + local match_bonus = precompute_bonus(haystack) + local n = string.len(needle) + local m = string.len(haystack) + local lower_needle = string.lower(needle) + local lower_haystack = string.lower(haystack) + + -- Because lua only grants access to chars through substring extraction, + -- get all the characters from the haystack once now, to reuse below. + local haystack_chars = {} + for i = 1, m do + haystack_chars[i] = lower_haystack:sub(i, i) + end + + for i = 1, n do + D[i] = {} + M[i] = {} + + local prev_score = SCORE_MIN + local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER + local needle_char = lower_needle:sub(i, i) + + for j = 1, m do + if needle_char == haystack_chars[j] then + local score = SCORE_MIN + if i == 1 then + score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j] + elseif j > 1 then + local a = M[i - 1][j - 1] + match_bonus[j] + local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE + score = math.max(a, b) + end + D[i][j] = score + prev_score = math.max(score, prev_score + gap_score) + M[i][j] = prev_score + else + D[i][j] = SCORE_MIN + prev_score = prev_score + gap_score + M[i][j] = prev_score + end + end + end +end + +function fzy.score(needle, haystack) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then + return SCORE_MIN + elseif n == m then + return SCORE_MAX + else + local D = {} + local M = {} + compute(needle, haystack, D, M) + return M[n][m] + end +end + +function fzy.positions(needle, haystack) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then + return {} + elseif n == m then + local consecutive = {} + for i = 1, n do + consecutive[i] = i + end + return consecutive + end + + local D = {} + local M = {} + compute(needle, haystack, D, M) + + local positions = {} + local match_required = false + local j = m + for i = n, 1, -1 do + while j >= 1 do + if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == M[i][j]) then + match_required = (i ~= 1) and (j ~= 1) and (M[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE) + positions[i] = j + j = j - 1 + break + else + j = j - 1 + end + end + end + + return positions +end + +-- If strings a or b are empty or too long, `fzy.score(a, b) == fzy.get_score_min()`. +function fzy.get_score_min() + return SCORE_MIN +end + +-- For exact matches, `fzy.score(s, s) == fzy.get_score_max()`. +function fzy.get_score_max() + return SCORE_MAX +end + +-- For all strings a and b that +-- - are not covered by either `fzy.get_score_min()` or fzy.get_score_max()`, and +-- - are matched, such that `fzy.has_match(a, b) == true`, +-- then `fzy.score(a, b) > fzy.get_score_floor()` will be true. +function fzy.get_score_floor() + return (MATCH_MAX_LENGTH + 1) * SCORE_GAP_INNER +end + +return fzy diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/linked_list.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/linked_list.lua new file mode 100644 index 0000000..2da6a6e --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/linked_list.lua @@ -0,0 +1,255 @@ +local LinkedList = {} +LinkedList.__index = LinkedList + +function LinkedList:new(opts) + opts = opts or {} + local track_at = opts.track_at + + return setmetatable({ + size = 0, + head = false, + tail = false, + + -- track_at: Track at can track a particular node + -- Use to keep a node tracked at a particular index + -- This greatly decreases looping for checking values at this location. + track_at = track_at, + _tracked_node = nil, + tracked = nil, + }, self) +end + +function LinkedList:_increment() + self.size = self.size + 1 + return self.size +end + +local create_node = function(item) + return { + item = item, + } +end + +function LinkedList:append(item) + local final_size = self:_increment() + + local node = create_node(item) + + if not self.head then + self.head = node + end + + if self.tail then + self.tail.next = node + node.prev = self.tail + end + + self.tail = node + + if self.track_at then + if final_size == self.track_at then + self.tracked = item + self._tracked_node = node + end + end +end + +function LinkedList:prepend(item) + local final_size = self:_increment() + local node = create_node(item) + + if not self.tail then + self.tail = node + end + + if self.head then + self.head.prev = node + node.next = self.head + end + + self.head = node + + if self.track_at then + if final_size == self.track_at then + self._tracked_node = self.tail + elseif final_size > self.track_at then + self._tracked_node = self._tracked_node.prev + else + return + end + + self.tracked = self._tracked_node.item + end +end + +-- [a, b, c] +-- b.prev = a +-- b.next = c +-- +-- a.next = b +-- c.prev = c +-- +-- insert d after b +-- [a, b, d, c] +-- +-- b.next = d +-- b.prev = a +-- +-- Place "item" after "node" (which is at index `index`) +function LinkedList:place_after(index, node, item) + local new_node = create_node(item) + + assert(node.prev ~= node) + assert(node.next ~= node) + local final_size = self:_increment() + + -- Update tail to be the next node. + if self.tail == node then + self.tail = new_node + end + + new_node.prev = node + new_node.next = node.next + + node.next = new_node + + if new_node.prev then + new_node.prev.next = new_node + end + + if new_node.next then + new_node.next.prev = new_node + end + + if self.track_at then + if index == self.track_at then + self._tracked_node = new_node + elseif index < self.track_at then + if final_size == self.track_at then + self._tracked_node = self.tail + elseif final_size > self.track_at then + self._tracked_node = self._tracked_node.prev + else + return + end + end + + self.tracked = self._tracked_node.item + end +end + +function LinkedList:place_before(index, node, item) + local new_node = create_node(item) + + assert(node.prev ~= node) + assert(node.next ~= node) + local final_size = self:_increment() + + -- Update head to be the node we are inserting. + if self.head == node then + self.head = new_node + end + + new_node.prev = node.prev + new_node.next = node + + node.prev = new_node + -- node.next = node.next + + if new_node.prev then + new_node.prev.next = new_node + end + + if new_node.next then + new_node.next.prev = new_node + end + + if self.track_at then + if index == self.track_at - 1 then + self._tracked_node = node + elseif index < self.track_at then + if final_size == self.track_at then + self._tracked_node = self.tail + elseif final_size > self.track_at then + self._tracked_node = self._tracked_node.prev + else + return + end + end + + self.tracked = self._tracked_node.item + end +end + +-- Do you even do this in linked lists...? +-- function LinkedList:remove(item) +-- end + +function LinkedList:iter() + local current_node = self.head + + return function() + local node = current_node + if not node then + return nil + end + + current_node = current_node.next + return node.item + end +end + +function LinkedList:ipairs() + local index = 0 + local current_node = self.head + + return function() + local node = current_node + if not node then + return nil + end + + current_node = current_node.next + index = index + 1 + return index, node.item, node + end +end + +function LinkedList:truncate(max_results) + if max_results >= self.size then + return + end + + local current_node + if max_results < self.size - max_results then + local index = 1 + current_node = self.head + while index < max_results do + local node = current_node + if not node.next then + break + end + current_node = current_node.next + index = index + 1 + end + self.size = max_results + else + current_node = self.tail + while self.size > max_results do + if current_node.prev == nil then + break + end + current_node = current_node.prev + self.size = self.size - 1 + end + end + self.tail = current_node + self.tail.next = nil + if max_results < self.track_at then + self.track_at = max_results + self.tracked = current_node.item + self._tracked_node = current_node + end +end + +return LinkedList diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/string_distance.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/string_distance.lua new file mode 100644 index 0000000..c2c5ead --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/algos/string_distance.lua @@ -0,0 +1,52 @@ +local function min(a, b, c) + local min_val = a + + if b < min_val then + min_val = b + end + if c < min_val then + min_val = c + end + + return min_val +end + +---------------------------------- +--- Levenshtein distance function. +-- @tparam string s1 +-- @tparam string s2 +-- @treturn number the levenshtein distance +-- @within Metrics +return function(s1, s2) + if s1 == s2 then + return 0 + end + if s1:len() == 0 then + return s2:len() + end + if s2:len() == 0 then + return s1:len() + end + if s1:len() < s2:len() then + s1, s2 = s2, s1 + end + + local t = {} + for i = 1, #s1 + 1 do + t[i] = { i - 1 } + end + + for i = 1, #s2 + 1 do + t[1][i] = i - 1 + end + + local cost + for i = 2, #s1 + 1 do + for j = 2, #s2 + 1 do + cost = (s1:sub(i - 1, i - 1) == s2:sub(j - 1, j - 1) and 0) or 1 + t[i][j] = min(t[i - 1][j] + 1, t[i][j - 1] + 1, t[i - 1][j - 1] + cost) + end + end + + return t[#s1 + 1][#s2 + 1] +end diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__diagnostics.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__diagnostics.lua new file mode 100644 index 0000000..ad50cfa --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__diagnostics.lua @@ -0,0 +1,152 @@ +local conf = require("telescope.config").values +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local pickers = require "telescope.pickers" +local utils = require "telescope.utils" + +local diagnostics = {} + +local convert_diagnostic_type = function(severities, severity) + -- convert from string to int + if type(severity) == "string" then + -- make sure that e.g. error is uppercased to ERROR + return severities[severity:upper()] + end + -- otherwise keep original value, incl. nil + return severity +end + +local diagnostics_to_tbl = function(opts) + opts = vim.F.if_nil(opts, {}) + local items = {} + local severities = vim.diagnostic.severity + local current_buf = vim.api.nvim_get_current_buf() + + opts.severity = convert_diagnostic_type(severities, opts.severity) + opts.severity_limit = convert_diagnostic_type(severities, opts.severity_limit) + opts.severity_bound = convert_diagnostic_type(severities, opts.severity_bound) + + local diagnosis_opts = { severity = {}, namespace = opts.namespace } + if opts.severity ~= nil then + if opts.severity_limit ~= nil or opts.severity_bound ~= nil then + utils.notify("builtin.diagnostics", { + msg = "Invalid severity parameters. Both a specific severity and a limit/bound is not allowed", + level = "ERROR", + }) + return {} + end + diagnosis_opts.severity = opts.severity + else + if opts.severity_limit ~= nil then + diagnosis_opts.severity["min"] = opts.severity_limit + end + if opts.severity_bound ~= nil then + diagnosis_opts.severity["max"] = opts.severity_bound + end + end + + opts.root_dir = opts.root_dir == true and vim.loop.cwd() or opts.root_dir + + local bufnr_name_map = {} + local filter_diag = function(diagnostic) + if bufnr_name_map[diagnostic.bufnr] == nil then + bufnr_name_map[diagnostic.bufnr] = vim.api.nvim_buf_get_name(diagnostic.bufnr) + end + + local root_dir_test = not opts.root_dir + or string.sub(bufnr_name_map[diagnostic.bufnr], 1, #opts.root_dir) == opts.root_dir + local listed_test = not opts.no_unlisted or vim.api.nvim_buf_get_option(diagnostic.bufnr, "buflisted") + + return root_dir_test and listed_test + end + + local preprocess_diag = function(diagnostic) + return { + bufnr = diagnostic.bufnr, + filename = bufnr_name_map[diagnostic.bufnr], + lnum = diagnostic.lnum + 1, + col = diagnostic.col + 1, + text = vim.trim(diagnostic.message:gsub("[\n]", "")), + type = severities[diagnostic.severity] or severities[1], + } + end + + for _, d in ipairs(vim.diagnostic.get(opts.bufnr, diagnosis_opts)) do + if filter_diag(d) then + table.insert(items, preprocess_diag(d)) + end + end + + -- sort results by bufnr (prioritize cur buf), severity, lnum + table.sort(items, function(a, b) + if a.bufnr == b.bufnr then + if a.type == b.type then + return a.lnum < b.lnum + else + return a.type < b.type + end + else + -- prioritize for current bufnr + if a.bufnr == current_buf then + return true + end + if b.bufnr == current_buf then + return false + end + return a.bufnr < b.bufnr + end + end) + + return items +end + +diagnostics.get = function(opts) + if opts.bufnr ~= 0 then + opts.bufnr = nil + end + if opts.bufnr == nil then + opts.path_display = vim.F.if_nil(opts.path_display, {}) + end + if type(opts.bufnr) == "string" then + opts.bufnr = tonumber(opts.bufnr) + end + + local locations = diagnostics_to_tbl(opts) + + if vim.tbl_isempty(locations) then + utils.notify("builtin.diagnostics", { + msg = "No diagnostics found", + level = "INFO", + }) + return + end + + opts.path_display = vim.F.if_nil(opts.path_display, "hidden") + pickers + .new(opts, { + prompt_title = opts.bufnr == nil and "Workspace Diagnostics" or "Document Diagnostics", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_diagnostics(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.prefilter_sorter { + tag = "type", + sorter = conf.generic_sorter(opts), + }, + }) + :find() +end + +local function apply_checks(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + opts = opts or {} + v(opts) + end + end + + return mod +end + +return apply_checks(diagnostics) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__files.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__files.lua new file mode 100644 index 0000000..dd54125 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__files.lua @@ -0,0 +1,572 @@ +local action_state = require "telescope.actions.state" +local action_set = require "telescope.actions.set" +local actions = require "telescope.actions" +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local pickers = require "telescope.pickers" +local previewers = require "telescope.previewers" +local sorters = require "telescope.sorters" +local utils = require "telescope.utils" +local conf = require("telescope.config").values +local log = require "telescope.log" + +local Path = require "plenary.path" + +local flatten = vim.tbl_flatten +local filter = vim.tbl_filter + +local files = {} + +local escape_chars = function(string) + return string.gsub(string, "[%(|%)|\\|%[|%]|%-|%{%}|%?|%+|%*|%^|%$|%.]", { + ["\\"] = "\\\\", + ["-"] = "\\-", + ["("] = "\\(", + [")"] = "\\)", + ["["] = "\\[", + ["]"] = "\\]", + ["{"] = "\\{", + ["}"] = "\\}", + ["?"] = "\\?", + ["+"] = "\\+", + ["*"] = "\\*", + ["^"] = "\\^", + ["$"] = "\\$", + ["."] = "\\.", + }) +end + +local get_open_filelist = function(grep_open_files, cwd) + if not grep_open_files then + return nil + end + + local bufnrs = filter(function(b) + if 1 ~= vim.fn.buflisted(b) then + return false + end + return true + end, vim.api.nvim_list_bufs()) + if not next(bufnrs) then + return + end + + local filelist = {} + for _, bufnr in ipairs(bufnrs) do + local file = vim.api.nvim_buf_get_name(bufnr) + table.insert(filelist, Path:new(file):make_relative(cwd)) + end + return filelist +end + +local opts_contain_invert = function(args) + local invert = false + local files_with_matches = false + + for _, v in ipairs(args) do + if v == "--invert-match" then + invert = true + elseif v == "--files-with-matches" or v == "--files-without-match" then + files_with_matches = true + end + + if #v >= 2 and v:sub(1, 1) == "-" and v:sub(2, 2) ~= "-" then + for i = 2, #v do + local vi = v:sub(i, i) + if vi == "v" then + invert = true + elseif vi == "l" then + files_with_matches = true + end + end + end + end + return invert, files_with_matches +end + +-- Special keys: +-- opts.search_dirs -- list of directory to search in +-- opts.grep_open_files -- boolean to restrict search to open files +files.live_grep = function(opts) + local vimgrep_arguments = opts.vimgrep_arguments or conf.vimgrep_arguments + local search_dirs = opts.search_dirs + local grep_open_files = opts.grep_open_files + opts.cwd = opts.cwd and vim.fn.expand(opts.cwd) or vim.loop.cwd() + + local filelist = get_open_filelist(grep_open_files, opts.cwd) + if search_dirs then + for i, path in ipairs(search_dirs) do + search_dirs[i] = vim.fn.expand(path) + end + end + + local additional_args = {} + if opts.additional_args ~= nil then + if type(opts.additional_args) == "function" then + additional_args = opts.additional_args(opts) + elseif type(opts.additional_args) == "table" then + additional_args = opts.additional_args + end + end + + if opts.type_filter then + additional_args[#additional_args + 1] = "--type=" .. opts.type_filter + end + + if type(opts.glob_pattern) == "string" then + additional_args[#additional_args + 1] = "--glob=" .. opts.glob_pattern + elseif type(opts.glob_pattern) == "table" then + for i = 1, #opts.glob_pattern do + additional_args[#additional_args + 1] = "--glob=" .. opts.glob_pattern[i] + end + end + + local args = flatten { vimgrep_arguments, additional_args } + opts.__inverted, opts.__matches = opts_contain_invert(args) + + local live_grepper = finders.new_job(function(prompt) + if not prompt or prompt == "" then + return nil + end + + local search_list = {} + + if grep_open_files then + search_list = filelist + elseif search_dirs then + search_list = search_dirs + end + + return flatten { args, "--", prompt, search_list } + end, opts.entry_maker or make_entry.gen_from_vimgrep(opts), opts.max_results, opts.cwd) + + pickers + .new(opts, { + prompt_title = "Live Grep", + finder = live_grepper, + previewer = conf.grep_previewer(opts), + -- TODO: It would be cool to use `--json` output for this + -- and then we could get the highlight positions directly. + sorter = sorters.highlighter_only(opts), + attach_mappings = function(_, map) + map("i", "", actions.to_fuzzy_refine) + return true + end, + }) + :find() +end + +files.grep_string = function(opts) + -- TODO: This should probably check your visual selection as well, if you've got one + opts.cwd = opts.cwd and vim.fn.expand(opts.cwd) or vim.loop.cwd() + local vimgrep_arguments = vim.F.if_nil(opts.vimgrep_arguments, conf.vimgrep_arguments) + local word = vim.F.if_nil(opts.search, vim.fn.expand "") + local search = opts.use_regex and word or escape_chars(word) + + local additional_args = {} + if opts.additional_args ~= nil then + if type(opts.additional_args) == "function" then + additional_args = opts.additional_args(opts) + elseif type(opts.additional_args) == "table" then + additional_args = opts.additional_args + end + end + + if search == "" then + search = { "-v", "--", "^[[:space:]]*$" } + else + search = { "--", search } + end + + local args = flatten { + vimgrep_arguments, + additional_args, + opts.word_match, + search, + } + opts.__inverted, opts.__matches = opts_contain_invert(args) + + if opts.grep_open_files then + for _, file in ipairs(get_open_filelist(opts.grep_open_files, opts.cwd)) do + table.insert(args, file) + end + elseif opts.search_dirs then + for _, path in ipairs(opts.search_dirs) do + table.insert(args, vim.fn.expand(path)) + end + end + + opts.entry_maker = opts.entry_maker or make_entry.gen_from_vimgrep(opts) + pickers + .new(opts, { + prompt_title = "Find Word (" .. word:gsub("\n", "\\n") .. ")", + finder = finders.new_oneshot_job(args, opts), + previewer = conf.grep_previewer(opts), + sorter = conf.generic_sorter(opts), + }) + :find() +end + +files.find_files = function(opts) + local find_command = (function() + if opts.find_command then + if type(opts.find_command) == "function" then + return opts.find_command(opts) + end + return opts.find_command + elseif 1 == vim.fn.executable "rg" then + return { "rg", "--files", "--color", "never" } + elseif 1 == vim.fn.executable "fd" then + return { "fd", "--type", "f", "--color", "never" } + elseif 1 == vim.fn.executable "fdfind" then + return { "fdfind", "--type", "f", "--color", "never" } + elseif 1 == vim.fn.executable "find" and vim.fn.has "win32" == 0 then + return { "find", ".", "-type", "f" } + elseif 1 == vim.fn.executable "where" then + return { "where", "/r", ".", "*" } + end + end)() + + if not find_command then + utils.notify("builtin.find_files", { + msg = "You need to install either find, fd, or rg", + level = "ERROR", + }) + return + end + + local command = find_command[1] + local hidden = opts.hidden + local no_ignore = opts.no_ignore + local no_ignore_parent = opts.no_ignore_parent + local follow = opts.follow + local search_dirs = opts.search_dirs + local search_file = opts.search_file + + if search_dirs then + for k, v in pairs(search_dirs) do + search_dirs[k] = vim.fn.expand(v) + end + end + + if command == "fd" or command == "fdfind" or command == "rg" then + if hidden then + find_command[#find_command + 1] = "--hidden" + end + if no_ignore then + find_command[#find_command + 1] = "--no-ignore" + end + if no_ignore_parent then + find_command[#find_command + 1] = "--no-ignore-parent" + end + if follow then + find_command[#find_command + 1] = "-L" + end + if search_file then + if command == "rg" then + find_command[#find_command + 1] = "-g" + find_command[#find_command + 1] = "*" .. search_file .. "*" + else + find_command[#find_command + 1] = search_file + end + end + if search_dirs then + if command ~= "rg" and not search_file then + find_command[#find_command + 1] = "." + end + vim.list_extend(find_command, search_dirs) + end + elseif command == "find" then + if not hidden then + table.insert(find_command, { "-not", "-path", "*/.*" }) + find_command = flatten(find_command) + end + if no_ignore ~= nil then + log.warn "The `no_ignore` key is not available for the `find` command in `find_files`." + end + if no_ignore_parent ~= nil then + log.warn "The `no_ignore_parent` key is not available for the `find` command in `find_files`." + end + if follow then + table.insert(find_command, 2, "-L") + end + if search_file then + table.insert(find_command, "-name") + table.insert(find_command, "*" .. search_file .. "*") + end + if search_dirs then + table.remove(find_command, 2) + for _, v in pairs(search_dirs) do + table.insert(find_command, 2, v) + end + end + elseif command == "where" then + if hidden ~= nil then + log.warn "The `hidden` key is not available for the Windows `where` command in `find_files`." + end + if no_ignore ~= nil then + log.warn "The `no_ignore` key is not available for the Windows `where` command in `find_files`." + end + if no_ignore_parent ~= nil then + log.warn "The `no_ignore_parent` key is not available for the Windows `where` command in `find_files`." + end + if follow ~= nil then + log.warn "The `follow` key is not available for the Windows `where` command in `find_files`." + end + if search_dirs ~= nil then + log.warn "The `search_dirs` key is not available for the Windows `where` command in `find_files`." + end + if search_file ~= nil then + log.warn "The `search_file` key is not available for the Windows `where` command in `find_files`." + end + end + + if opts.cwd then + opts.cwd = vim.fn.expand(opts.cwd) + end + + opts.entry_maker = opts.entry_maker or make_entry.gen_from_file(opts) + + pickers + .new(opts, { + prompt_title = "Find Files", + finder = finders.new_oneshot_job(find_command, opts), + previewer = conf.file_previewer(opts), + sorter = conf.file_sorter(opts), + }) + :find() +end + +local function prepare_match(entry, kind) + local entries = {} + + if entry.node then + table.insert(entries, entry) + else + for name, item in pairs(entry) do + vim.list_extend(entries, prepare_match(item, name)) + end + end + + return entries +end + +-- TODO: finish docs for opts.show_line +files.treesitter = function(opts) + opts.show_line = vim.F.if_nil(opts.show_line, true) + + local has_nvim_treesitter, _ = pcall(require, "nvim-treesitter") + if not has_nvim_treesitter then + utils.notify("builtin.treesitter", { + msg = "User need to install nvim-treesitter needs to be installed", + level = "ERROR", + }) + return + end + + local parsers = require "nvim-treesitter.parsers" + if not parsers.has_parser(parsers.get_buf_lang(opts.bufnr)) then + utils.notify("builtin.treesitter", { + msg = "No parser for the current buffer", + level = "ERROR", + }) + return + end + + local ts_locals = require "nvim-treesitter.locals" + local results = {} + for _, definition in ipairs(ts_locals.get_definitions(opts.bufnr)) do + local entries = prepare_match(ts_locals.get_local_nodes(definition)) + for _, entry in ipairs(entries) do + entry.kind = vim.F.if_nil(entry.kind, "") + table.insert(results, entry) + end + end + + if vim.tbl_isempty(results) then + return + end + + pickers + .new(opts, { + prompt_title = "Treesitter Symbols", + finder = finders.new_table { + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_treesitter(opts), + }, + previewer = conf.grep_previewer(opts), + sorter = conf.prefilter_sorter { + tag = "kind", + sorter = conf.generic_sorter(opts), + }, + }) + :find() +end + +files.current_buffer_fuzzy_find = function(opts) + -- All actions are on the current buffer + local filename = vim.fn.expand(vim.api.nvim_buf_get_name(opts.bufnr)) + local filetype = vim.api.nvim_buf_get_option(opts.bufnr, "filetype") + + local lines = vim.api.nvim_buf_get_lines(opts.bufnr, 0, -1, false) + local lines_with_numbers = {} + + for lnum, line in ipairs(lines) do + table.insert(lines_with_numbers, { + lnum = lnum, + bufnr = opts.bufnr, + filename = filename, + text = line, + }) + end + + local ts_ok, ts_parsers = pcall(require, "nvim-treesitter.parsers") + if ts_ok then + filetype = ts_parsers.ft_to_lang(filetype) + end + local _, ts_configs = pcall(require, "nvim-treesitter.configs") + + local parser_ok, parser = pcall(vim.treesitter.get_parser, opts.bufnr, filetype) + local query_ok, query = pcall(vim.treesitter.get_query, filetype, "highlights") + if parser_ok and query_ok and ts_ok and ts_configs.is_enabled("highlight", filetype, opts.bufnr) then + local root = parser:parse()[1]:root() + + local highlighter = vim.treesitter.highlighter.new(parser) + local highlighter_query = highlighter:get_query(filetype) + + local line_highlights = setmetatable({}, { + __index = function(t, k) + local obj = {} + rawset(t, k, obj) + return obj + end, + }) + + -- update to changes on Neovim master, see https://github.com/neovim/neovim/pull/19931 + -- TODO(clason): remove when dropping support for Neovim 0.7 + local on_nvim_master = vim.fn.has "nvim-0.8" == 1 + for id, node in query:iter_captures(root, opts.bufnr, 0, -1) do + local hl = on_nvim_master and query.captures[id] or highlighter_query:_get_hl_from_capture(id) + if hl and type(hl) ~= "number" then + local row1, col1, row2, col2 = node:range() + + if row1 == row2 then + local row = row1 + 1 + + for index = col1, col2 do + line_highlights[row][index] = hl + end + else + local row = row1 + 1 + for index = col1, #lines[row] do + line_highlights[row][index] = hl + end + + while row < row2 + 1 do + row = row + 1 + + for index = 0, #(lines[row] or {}) do + line_highlights[row][index] = hl + end + end + end + end + end + + opts.line_highlights = line_highlights + end + + pickers + .new(opts, { + prompt_title = "Current Buffer Fuzzy", + finder = finders.new_table { + results = lines_with_numbers, + entry_maker = opts.entry_maker or make_entry.gen_from_buffer_lines(opts), + }, + sorter = conf.generic_sorter(opts), + previewer = conf.grep_previewer(opts), + attach_mappings = function() + action_set.select:enhance { + post = function() + local selection = action_state.get_selected_entry() + vim.api.nvim_win_set_cursor(0, { selection.lnum, 0 }) + end, + } + + return true + end, + }) + :find() +end + +files.tags = function(opts) + local tagfiles = opts.ctags_file and { opts.ctags_file } or vim.fn.tagfiles() + for i, ctags_file in ipairs(tagfiles) do + tagfiles[i] = vim.fn.expand(ctags_file, true) + end + if vim.tbl_isempty(tagfiles) then + utils.notify("builtin.tags", { + msg = "No tags file found. Create one with ctags -R", + level = "ERROR", + }) + return + end + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_ctags(opts)) + + pickers + .new(opts, { + prompt_title = "Tags", + finder = finders.new_oneshot_job(flatten { "cat", tagfiles }, opts), + previewer = previewers.ctags.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function() + action_set.select:enhance { + post = function() + local selection = action_state.get_selected_entry() + if not selection then + return + end + + if selection.scode then + -- un-escape / then escape required + -- special chars for vim.fn.search() + -- ] ~ * + local scode = selection.scode:gsub([[\/]], "/"):gsub("[%]~*]", function(x) + return "\\" .. x + end) + + vim.cmd "norm! gg" + vim.fn.search(scode) + vim.cmd "norm! zz" + else + vim.api.nvim_win_set_cursor(0, { selection.lnum, 0 }) + end + end, + } + return true + end, + }) + :find() +end + +files.current_buffer_tags = function(opts) + return files.tags(vim.tbl_extend("force", { + prompt_title = "Current Buffer Tags", + only_current_file = true, + path_display = "hidden", + }, opts)) +end + +local function apply_checks(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + opts = opts or {} + + v(opts) + end + end + + return mod +end + +return apply_checks(files) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__git.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__git.lua new file mode 100644 index 0000000..1b9d0fa --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__git.lua @@ -0,0 +1,406 @@ +local actions = require "telescope.actions" +local action_state = require "telescope.actions.state" +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local pickers = require "telescope.pickers" +local previewers = require "telescope.previewers" +local utils = require "telescope.utils" +local entry_display = require "telescope.pickers.entry_display" +local strings = require "plenary.strings" +local Path = require "plenary.path" + +local conf = require("telescope.config").values + +local git = {} + +git.files = function(opts) + if opts.is_bare then + utils.notify("builtin.git_files", { + msg = "This operation must be run in a work tree", + level = "ERROR", + }) + return + end + + local show_untracked = vim.F.if_nil(opts.show_untracked, false) + local recurse_submodules = vim.F.if_nil(opts.recurse_submodules, false) + if show_untracked and recurse_submodules then + utils.notify("builtin.git_files", { + msg = "Git does not support both --others and --recurse-submodules", + level = "ERROR", + }) + return + end + + -- By creating the entry maker after the cwd options, + -- we ensure the maker uses the cwd options when being created. + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_file(opts)) + local git_command = vim.F.if_nil(opts.git_command, { "git", "ls-files", "--exclude-standard", "--cached" }) + + pickers + .new(opts, { + prompt_title = "Git Files", + finder = finders.new_oneshot_job( + vim.tbl_flatten { + git_command, + show_untracked and "--others" or nil, + recurse_submodules and "--recurse-submodules" or nil, + }, + opts + ), + previewer = conf.file_previewer(opts), + sorter = conf.file_sorter(opts), + }) + :find() +end + +git.commits = function(opts) + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_commits(opts)) + local git_command = vim.F.if_nil(opts.git_command, { "git", "log", "--pretty=oneline", "--abbrev-commit", "--", "." }) + + pickers + .new(opts, { + prompt_title = "Git Commits", + finder = finders.new_oneshot_job(git_command, opts), + previewer = { + previewers.git_commit_diff_to_parent.new(opts), + previewers.git_commit_diff_to_head.new(opts), + previewers.git_commit_diff_as_was.new(opts), + previewers.git_commit_message.new(opts), + }, + sorter = conf.file_sorter(opts), + attach_mappings = function(_, map) + actions.select_default:replace(actions.git_checkout) + map({ "i", "n" }, "m", actions.git_reset_mixed) + map({ "i", "n" }, "s", actions.git_reset_soft) + map({ "i", "n" }, "h", actions.git_reset_hard) + return true + end, + }) + :find() +end + +git.stash = function(opts) + opts.show_branch = vim.F.if_nil(opts.show_branch, true) + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_stash(opts)) + + pickers + .new(opts, { + prompt_title = "Git Stash", + finder = finders.new_oneshot_job( + vim.tbl_flatten { + "git", + "--no-pager", + "stash", + "list", + }, + opts + ), + previewer = previewers.git_stash_diff.new(opts), + sorter = conf.file_sorter(opts), + attach_mappings = function() + actions.select_default:replace(actions.git_apply_stash) + return true + end, + }) + :find() +end + +local get_current_buf_line = function(winnr) + local lnum = vim.api.nvim_win_get_cursor(winnr)[1] + return vim.trim(vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(winnr), lnum - 1, lnum, false)[1]) +end + +git.bcommits = function(opts) + opts.current_line = (opts.current_file == nil) and get_current_buf_line(opts.winnr) or nil + opts.current_file = vim.F.if_nil(opts.current_file, vim.api.nvim_buf_get_name(opts.bufnr)) + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_commits(opts)) + local git_command = + vim.F.if_nil(opts.git_command, { "git", "log", "--pretty=oneline", "--abbrev-commit", "--follow" }) + + pickers + .new(opts, { + prompt_title = "Git BCommits", + finder = finders.new_oneshot_job( + vim.tbl_flatten { + git_command, + opts.current_file, + }, + opts + ), + previewer = { + previewers.git_commit_diff_to_parent.new(opts), + previewers.git_commit_diff_to_head.new(opts), + previewers.git_commit_diff_as_was.new(opts), + previewers.git_commit_message.new(opts), + }, + sorter = conf.file_sorter(opts), + attach_mappings = function() + actions.select_default:replace(actions.git_checkout_current_buffer) + local transfrom_file = function() + return opts.current_file and Path:new(opts.current_file):make_relative(opts.cwd) or "" + end + + local get_buffer_of_orig = function(selection) + local value = selection.value .. ":" .. transfrom_file() + local content = utils.get_os_command_output({ "git", "--no-pager", "show", value }, opts.cwd) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) + vim.api.nvim_buf_set_name(bufnr, "Original") + return bufnr + end + + local vimdiff = function(selection, command) + local ft = vim.bo.filetype + vim.cmd "diffthis" + + local bufnr = get_buffer_of_orig(selection) + vim.cmd(string.format("%s %s", command, bufnr)) + vim.bo.filetype = ft + vim.cmd "diffthis" + + vim.api.nvim_create_autocmd("WinClosed", { + buffer = bufnr, + nested = true, + once = true, + callback = function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end, + }) + end + + actions.select_vertical:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vimdiff(selection, "leftabove vert sbuffer") + end) + + actions.select_horizontal:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vimdiff(selection, "belowright sbuffer") + end) + + actions.select_tab:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vim.cmd("tabedit " .. transfrom_file()) + vimdiff(selection, "leftabove vert sbuffer") + end) + return true + end, + }) + :find() +end + +git.branches = function(opts) + local format = "%(HEAD)" + .. "%(refname)" + .. "%(authorname)" + .. "%(upstream:lstrip=2)" + .. "%(committerdate:format-local:%Y/%m/%d %H:%M:%S)" + local output = utils.get_os_command_output( + { "git", "for-each-ref", "--perl", "--format", format, "--sort", "-authordate", opts.pattern }, + opts.cwd + ) + + local results = {} + local widths = { + name = 0, + authorname = 0, + upstream = 0, + committerdate = 0, + } + local unescape_single_quote = function(v) + return string.gsub(v, "\\([\\'])", "%1") + end + local parse_line = function(line) + local fields = vim.split(string.sub(line, 2, -2), "''", true) + local entry = { + head = fields[1], + refname = unescape_single_quote(fields[2]), + authorname = unescape_single_quote(fields[3]), + upstream = unescape_single_quote(fields[4]), + committerdate = fields[5], + } + local prefix + if vim.startswith(entry.refname, "refs/remotes/") then + prefix = "refs/remotes/" + elseif vim.startswith(entry.refname, "refs/heads/") then + prefix = "refs/heads/" + else + return + end + local index = 1 + if entry.head ~= "*" then + index = #results + 1 + end + + entry.name = string.sub(entry.refname, string.len(prefix) + 1) + for key, value in pairs(widths) do + widths[key] = math.max(value, strings.strdisplaywidth(entry[key] or "")) + end + if string.len(entry.upstream) > 0 then + widths.upstream_indicator = 2 + end + table.insert(results, index, entry) + end + for _, line in ipairs(output) do + parse_line(line) + end + if #results == 0 then + return + end + + local displayer = entry_display.create { + separator = " ", + items = { + { width = 1 }, + { width = widths.name }, + { width = widths.authorname }, + { width = widths.upstream_indicator }, + { width = widths.upstream }, + { width = widths.committerdate }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.head }, + { entry.name, "TelescopeResultsIdentifier" }, + { entry.authorname }, + { string.len(entry.upstream) > 0 and "=>" or "" }, + { entry.upstream, "TelescopeResultsIdentifier" }, + { entry.committerdate }, + } + end + + pickers + .new(opts, { + prompt_title = "Git Branches", + finder = finders.new_table { + results = results, + entry_maker = function(entry) + entry.value = entry.name + entry.ordinal = entry.name + entry.display = make_display + return make_entry.set_default_entry_mt(entry, opts) + end, + }, + previewer = previewers.git_branch_log.new(opts), + sorter = conf.file_sorter(opts), + attach_mappings = function(_, map) + actions.select_default:replace(actions.git_checkout) + map({ "i", "n" }, "", actions.git_track_branch) + map({ "i", "n" }, "", actions.git_rebase_branch) + map({ "i", "n" }, "", actions.git_create_branch) + map({ "i", "n" }, "", actions.git_switch_branch) + map({ "i", "n" }, "", actions.git_delete_branch) + map({ "i", "n" }, "", actions.git_merge_branch) + return true + end, + }) + :find() +end + +git.status = function(opts) + if opts.is_bare then + utils.notify("builtin.git_status", { + msg = "This operation must be run in a work tree", + level = "ERROR", + }) + return + end + + local gen_new_finder = function() + local expand_dir = vim.F.if_nil(opts.expand_dir, true) + local git_cmd = { "git", "status", "-s", "--", "." } + + if expand_dir then + table.insert(git_cmd, #git_cmd - 1, "-u") + end + + local output = utils.get_os_command_output(git_cmd, opts.cwd) + + if #output == 0 then + print "No changes found" + utils.notify("builtin.git_status", { + msg = "No changes found", + level = "WARN", + }) + return + end + + return finders.new_table { + results = output, + entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_status(opts)), + } + end + + local initial_finder = gen_new_finder() + if not initial_finder then + return + end + + pickers + .new(opts, { + prompt_title = "Git Status", + finder = initial_finder, + previewer = previewers.git_file_diff.new(opts), + sorter = conf.file_sorter(opts), + attach_mappings = function(prompt_bufnr, map) + actions.git_staging_toggle:enhance { + post = function() + action_state.get_current_picker(prompt_bufnr):refresh(gen_new_finder(), { reset_prompt = true }) + end, + } + + map({ "i", "n" }, "", actions.git_staging_toggle) + return true + end, + }) + :find() +end + +local set_opts_cwd = function(opts) + if opts.cwd then + opts.cwd = vim.fn.expand(opts.cwd) + else + opts.cwd = vim.loop.cwd() + end + + -- Find root of git directory and remove trailing newline characters + local git_root, ret = utils.get_os_command_output({ "git", "rev-parse", "--show-toplevel" }, opts.cwd) + local use_git_root = vim.F.if_nil(opts.use_git_root, true) + + if ret ~= 0 then + local in_worktree = utils.get_os_command_output({ "git", "rev-parse", "--is-inside-work-tree" }, opts.cwd) + local in_bare = utils.get_os_command_output({ "git", "rev-parse", "--is-bare-repository" }, opts.cwd) + + if in_worktree[1] ~= "true" and in_bare[1] ~= "true" then + error(opts.cwd .. " is not a git directory") + elseif in_worktree[1] ~= "true" and in_bare[1] == "true" then + opts.is_bare = true + end + else + if use_git_root then + opts.cwd = git_root[1] + end + end +end + +local function apply_checks(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + opts = vim.F.if_nil(opts, {}) + + set_opts_cwd(opts) + v(opts) + end + end + + return mod +end + +return apply_checks(git) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__internal.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__internal.lua new file mode 100644 index 0000000..d0f32a3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__internal.lua @@ -0,0 +1,1374 @@ +local actions = require "telescope.actions" +local action_set = require "telescope.actions.set" +local action_state = require "telescope.actions.state" +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local Path = require "plenary.path" +local pickers = require "telescope.pickers" +local previewers = require "telescope.previewers" +local p_window = require "telescope.pickers.window" +local state = require "telescope.state" +local utils = require "telescope.utils" + +local conf = require("telescope.config").values + +local filter = vim.tbl_filter + +-- Makes sure aliased options are set correctly +local function apply_cwd_only_aliases(opts) + local has_cwd_only = opts.cwd_only ~= nil + local has_only_cwd = opts.only_cwd ~= nil + + if has_only_cwd and not has_cwd_only then + -- Internally, use cwd_only + opts.cwd_only = opts.only_cwd + opts.only_cwd = nil + end + + return opts +end + +local internal = {} + +internal.builtin = function(opts) + opts.include_extensions = vim.F.if_nil(opts.include_extensions, false) + opts.use_default_opts = vim.F.if_nil(opts.use_default_opts, false) + + local objs = {} + + for k, v in pairs(require "telescope.builtin") do + local debug_info = debug.getinfo(v) + table.insert(objs, { + filename = string.sub(debug_info.source, 2), + text = k, + }) + end + + local title = "Telescope Builtin" + + if opts.include_extensions then + title = "Telescope Pickers" + for ext, funcs in pairs(require("telescope").extensions) do + for func_name, func_obj in pairs(funcs) do + -- Only include exported functions whose name doesn't begin with an underscore + if type(func_obj) == "function" and string.sub(func_name, 0, 1) ~= "_" then + local debug_info = debug.getinfo(func_obj) + table.insert(objs, { + filename = string.sub(debug_info.source, 2), + text = string.format("%s : %s", ext, func_name), + }) + end + end + end + end + + opts.bufnr = vim.api.nvim_get_current_buf() + opts.winnr = vim.api.nvim_get_current_win() + pickers + .new(opts, { + prompt_title = title, + finder = finders.new_table { + results = objs, + entry_maker = function(entry) + return make_entry.set_default_entry_mt({ + value = entry, + text = entry.text, + display = entry.text, + ordinal = entry.text, + filename = entry.filename, + }, opts) + end, + }, + previewer = previewers.builtin.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function(_) + actions.select_default:replace(function(prompt_bufnr) + local selection = action_state.get_selected_entry() + if not selection then + utils.__warn_no_selection "builtin.builtin" + return + end + + -- we do this to avoid any surprises + opts.include_extensions = nil + + local picker_opts + if not opts.use_default_opts then + picker_opts = opts + end + + actions.close(prompt_bufnr) + if string.match(selection.text, " : ") then + -- Call appropriate function from extensions + local split_string = vim.split(selection.text, " : ") + local ext = split_string[1] + local func = split_string[2] + require("telescope").extensions[ext][func](picker_opts) + else + -- Call appropriate telescope builtin + require("telescope.builtin")[selection.text](picker_opts) + end + end) + return true + end, + }) + :find() +end + +internal.resume = function(opts) + opts = opts or {} + opts.cache_index = vim.F.if_nil(opts.cache_index, 1) + + local cached_pickers = state.get_global_key "cached_pickers" + if cached_pickers == nil or vim.tbl_isempty(cached_pickers) then + utils.notify("builtin.resume", { + msg = "No cached picker(s).", + level = "INFO", + }) + return + end + local picker = cached_pickers[opts.cache_index] + if picker == nil then + utils.notify("builtin.resume", { + msg = string.format("Index too large as there are only '%s' pickers cached", #cached_pickers), + level = "ERROR", + }) + return + end + -- reset layout strategy and get_window_options if default as only one is valid + -- and otherwise unclear which was actually set + if picker.layout_strategy == conf.layout_strategy then + picker.layout_strategy = nil + end + if picker.get_window_options == p_window.get_window_options then + picker.get_window_options = nil + end + picker.cache_picker.index = opts.cache_index + + -- avoid partial `opts.cache_picker` at picker creation + if opts.cache_picker ~= false then + picker.cache_picker = vim.tbl_extend("keep", opts.cache_picker or {}, picker.cache_picker) + else + picker.cache_picker.disabled = true + end + opts.cache_picker = nil + picker.previewer = picker.all_previewers + if picker.hidden_previewer then + picker.hidden_previewer = nil + opts.previewer = vim.F.if_nil(opts.previewer, false) + end + pickers.new(opts, picker):find() +end + +internal.pickers = function(opts) + local cached_pickers = state.get_global_key "cached_pickers" + if cached_pickers == nil or vim.tbl_isempty(cached_pickers) then + utils.notify("builtin.pickers", { + msg = "No cached picker(s).", + level = "INFO", + }) + return + end + + opts = opts or {} + + -- clear cache picker for immediate pickers.new and pass option to resumed picker + if opts.cache_picker ~= nil then + opts._cache_picker = opts.cache_picker + opts.cache_picker = nil + end + + pickers + .new(opts, { + prompt_title = "Pickers", + finder = finders.new_table { + results = cached_pickers, + entry_maker = make_entry.gen_from_picker(opts), + }, + previewer = previewers.pickers.new(opts), + sorter = conf.generic_sorter(opts), + cache_picker = false, + attach_mappings = function(_, map) + actions.select_default:replace(function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + local selection_index = current_picker:get_index(current_picker:get_selection_row()) + actions.close(prompt_bufnr) + opts.cache_picker = opts._cache_picker + opts["cache_index"] = selection_index + opts["initial_mode"] = cached_pickers[selection_index].initial_mode + internal.resume(opts) + end) + map({ "i", "n" }, "", actions.remove_selected_picker) + return true + end, + }) + :find() +end + +internal.planets = function(opts) + local show_pluto = opts.show_pluto or false + local show_moon = opts.show_moon or false + + local sourced_file = require("plenary.debug_utils").sourced_filepath() + local base_directory = vim.fn.fnamemodify(sourced_file, ":h:h:h:h") + + local globbed_files = vim.fn.globpath(base_directory .. "/data/memes/planets/", "*", true, true) + local acceptable_files = {} + for _, v in ipairs(globbed_files) do + if (show_pluto or not v:find "pluto") and (show_moon or not v:find "moon") then + table.insert(acceptable_files, vim.fn.fnamemodify(v, ":t")) + end + end + + pickers + .new(opts, { + prompt_title = "Planets", + finder = finders.new_table { + results = acceptable_files, + entry_maker = function(line) + return make_entry.set_default_entry_mt({ + ordinal = line, + display = line, + filename = base_directory .. "/data/memes/planets/" .. line, + }, opts) + end, + }, + previewer = previewers.cat.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.planets" + return + end + + actions.close(prompt_bufnr) + print("Enjoy astronomy! You viewed:", selection.display) + end) + + return true + end, + }) + :find() +end + +internal.symbols = function(opts) + local initial_mode = vim.fn.mode() + local files = vim.api.nvim_get_runtime_file("data/telescope-sources/*.json", true) + local data_path = (function() + if not opts.symbol_path then + return Path:new { vim.fn.stdpath "data", "telescope", "symbols" } + else + return Path:new { opts.symbol_path } + end + end)() + if data_path:exists() then + for _, v in ipairs(require("plenary.scandir").scan_dir(data_path:absolute(), { search_pattern = "%.json$" })) do + table.insert(files, v) + end + end + + if #files == 0 then + utils.notify("builtin.symbols", { + msg = "No sources found! Check out https://github.com/nvim-telescope/telescope-symbols.nvim " + .. "for some prebuild symbols or how to create you own symbol source.", + level = "ERROR", + }) + return + end + + local sources = {} + if opts.sources then + for _, v in ipairs(files) do + for _, s in ipairs(opts.sources) do + if v:find(s) then + table.insert(sources, v) + end + end + end + else + sources = files + end + + local results = {} + for _, source in ipairs(sources) do + local data = vim.json.decode(Path:new(source):read()) + for _, entry in ipairs(data) do + table.insert(results, entry) + end + end + + pickers + .new(opts, { + prompt_title = "Symbols", + finder = finders.new_table { + results = results, + entry_maker = function(entry) + return make_entry.set_default_entry_mt({ + value = entry, + ordinal = entry[1] .. " " .. entry[2], + display = entry[1] .. " " .. entry[2], + }, opts) + end, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_) + if initial_mode == "i" then + actions.select_default:replace(actions.insert_symbol_i) + else + actions.select_default:replace(actions.insert_symbol) + end + return true + end, + }) + :find() +end + +internal.commands = function(opts) + pickers + .new(opts, { + prompt_title = "Commands", + finder = finders.new_table { + results = (function() + local command_iter = vim.api.nvim_get_commands {} + local commands = {} + + for _, cmd in pairs(command_iter) do + table.insert(commands, cmd) + end + + local need_buf_command = vim.F.if_nil(opts.show_buf_command, true) + + if need_buf_command then + local buf_command_iter = vim.api.nvim_buf_get_commands(0, {}) + buf_command_iter[true] = nil -- remove the redundant entry + for _, cmd in pairs(buf_command_iter) do + table.insert(commands, cmd) + end + end + return commands + end)(), + + entry_maker = opts.entry_maker or make_entry.gen_from_commands(opts), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.commands" + return + end + + actions.close(prompt_bufnr) + local val = selection.value + local cmd = string.format([[:%s ]], val.name) + + if val.nargs == "0" then + vim.cmd(cmd) + else + vim.cmd [[stopinsert]] + vim.fn.feedkeys(cmd, "n") + end + end) + + return true + end, + }) + :find() +end + +internal.quickfix = function(opts) + local qf_identifier = opts.id or vim.F.if_nil(opts.nr, "$") + local locations = vim.fn.getqflist({ [opts.id and "id" or "nr"] = qf_identifier, items = true }).items + + if vim.tbl_isempty(locations) then + return + end + + pickers + .new(opts, { + prompt_title = "Quickfix", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + }) + :find() +end + +internal.quickfixhistory = function(opts) + local qflists = {} + for i = 1, 10 do -- (n)vim keeps at most 10 quickfix lists in full + -- qf weirdness: id = 0 gets id of quickfix list nr + local qflist = vim.fn.getqflist { nr = i, id = 0, title = true, items = true } + if not vim.tbl_isempty(qflist.items) then + table.insert(qflists, qflist) + end + end + local entry_maker = opts.make_entry + or function(entry) + return make_entry.set_default_entry_mt({ + value = entry.title or "Untitled", + ordinal = entry.title or "Untitled", + display = entry.title or "Untitled", + nr = entry.nr, + id = entry.id, + items = entry.items, + }, opts) + end + local qf_entry_maker = make_entry.gen_from_quickfix(opts) + pickers + .new(opts, { + prompt_title = "Quickfix History", + finder = finders.new_table { + results = qflists, + entry_maker = entry_maker, + }, + previewer = previewers.new_buffer_previewer { + title = "Quickfix List Preview", + dyn_title = function(_, entry) + return entry.title + end, + + get_buffer_by_name = function(_, entry) + return "quickfixlist_" .. tostring(entry.nr) + end, + + define_preview = function(self, entry) + if self.state.bufname then + return + end + local entries = vim.tbl_map(function(i) + return qf_entry_maker(i):display() + end, entry.items) + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, entries) + end, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_, _) + action_set.select:replace(function(prompt_bufnr) + local nr = action_state.get_selected_entry().nr + actions.close(prompt_bufnr) + internal.quickfix { nr = nr } + end) + return true + end, + }) + :find() +end + +internal.loclist = function(opts) + local locations = vim.fn.getloclist(0) + local filenames = {} + for _, value in pairs(locations) do + local bufnr = value.bufnr + if filenames[bufnr] == nil then + filenames[bufnr] = vim.api.nvim_buf_get_name(bufnr) + end + value.filename = filenames[bufnr] + end + + if vim.tbl_isempty(locations) then + return + end + + pickers + .new(opts, { + prompt_title = "Loclist", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + }) + :find() +end + +internal.oldfiles = function(opts) + opts = apply_cwd_only_aliases(opts) + opts.include_current_session = vim.F.if_nil(opts.include_current_session, true) + + local current_buffer = vim.api.nvim_get_current_buf() + local current_file = vim.api.nvim_buf_get_name(current_buffer) + local results = {} + + if opts.include_current_session then + for _, buffer in ipairs(vim.split(vim.fn.execute ":buffers! t", "\n")) do + local match = tonumber(string.match(buffer, "%s*(%d+)")) + local open_by_lsp = string.match(buffer, "line 0$") + if match and not open_by_lsp then + local file = vim.api.nvim_buf_get_name(match) + if vim.loop.fs_stat(file) and match ~= current_buffer then + table.insert(results, file) + end + end + end + end + + for _, file in ipairs(vim.v.oldfiles) do + if vim.loop.fs_stat(file) and not vim.tbl_contains(results, file) and file ~= current_file then + table.insert(results, file) + end + end + + if opts.cwd_only then + local cwd = vim.loop.cwd() + cwd = cwd:gsub([[\]], [[\\]]) + results = vim.tbl_filter(function(file) + return vim.fn.matchstrpos(file, cwd)[2] ~= -1 + end, results) + end + + pickers + .new(opts, { + prompt_title = "Oldfiles", + finder = finders.new_table { + results = results, + entry_maker = opts.entry_maker or make_entry.gen_from_file(opts), + }, + sorter = conf.file_sorter(opts), + previewer = conf.file_previewer(opts), + }) + :find() +end + +internal.command_history = function(opts) + local history_string = vim.fn.execute "history cmd" + local history_list = vim.split(history_string, "\n") + + local results = {} + local filter_fn = opts.filter_fn + + for i = #history_list, 3, -1 do + local item = history_list[i] + local _, finish = string.find(item, "%d+ +") + local cmd = string.sub(item, finish + 1) + + if filter_fn then + if filter_fn(cmd) then + table.insert(results, cmd) + end + else + table.insert(results, cmd) + end + end + + pickers + .new(opts, { + prompt_title = "Command History", + finder = finders.new_table(results), + sorter = conf.generic_sorter(opts), + + attach_mappings = function(_, map) + map({ "i", "n" }, "", actions.set_command_line) + map({ "i", "n" }, "", actions.edit_command_line) + + -- TODO: Find a way to insert the text... it seems hard. + -- map('i', '', actions.insert_value, { expr = true }) + + return true + end, + }) + :find() +end + +internal.search_history = function(opts) + local search_string = vim.fn.execute "history search" + local search_list = vim.split(search_string, "\n") + + local results = {} + for i = #search_list, 3, -1 do + local item = search_list[i] + local _, finish = string.find(item, "%d+ +") + table.insert(results, string.sub(item, finish + 1)) + end + + pickers + .new(opts, { + prompt_title = "Search History", + finder = finders.new_table(results), + sorter = conf.generic_sorter(opts), + + attach_mappings = function(_, map) + map({ "i", "n" }, "", actions.set_search_line) + map({ "i", "n" }, "", actions.edit_search_line) + + -- TODO: Find a way to insert the text... it seems hard. + -- map('i', '', actions.insert_value, { expr = true }) + + return true + end, + }) + :find() +end + +internal.vim_options = function(opts) + local res = {} + for _, v in pairs(vim.api.nvim_get_all_options_info()) do + table.insert(res, v) + end + table.sort(res, function(left, right) + return left.name < right.name + end) + + pickers + .new(opts, { + prompt_title = "options", + finder = finders.new_table { + results = res, + entry_maker = opts.entry_maker or make_entry.gen_from_vimoptions(opts), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function() + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.vim_options" + return + end + + local esc = "" + if vim.fn.mode() == "i" then + esc = vim.api.nvim_replace_termcodes("", true, false, true) + end + + vim.api.nvim_feedkeys( + string.format("%s:set %s=%s", esc, selection.value.name, selection.value.value), + "m", + true + ) + end) + + return true + end, + }) + :find() +end + +internal.help_tags = function(opts) + opts.lang = vim.F.if_nil(opts.lang, vim.o.helplang) + opts.fallback = vim.F.if_nil(opts.fallback, true) + opts.file_ignore_patterns = {} + + local langs = vim.split(opts.lang, ",", true) + if opts.fallback and not vim.tbl_contains(langs, "en") then + table.insert(langs, "en") + end + local langs_map = {} + for _, lang in ipairs(langs) do + langs_map[lang] = true + end + + local tag_files = {} + local function add_tag_file(lang, file) + if langs_map[lang] then + if tag_files[lang] then + table.insert(tag_files[lang], file) + else + tag_files[lang] = { file } + end + end + end + + local help_files = {} + local all_files = vim.api.nvim_get_runtime_file("doc/*", true) + for _, fullpath in ipairs(all_files) do + local file = utils.path_tail(fullpath) + if file == "tags" then + add_tag_file("en", fullpath) + elseif file:match "^tags%-..$" then + local lang = file:sub(-2) + add_tag_file(lang, fullpath) + else + help_files[file] = fullpath + end + end + + local tags = {} + local tags_map = {} + local delimiter = string.char(9) + for _, lang in ipairs(langs) do + for _, file in ipairs(tag_files[lang] or {}) do + local lines = vim.split(Path:new(file):read(), "\n", true) + for _, line in ipairs(lines) do + -- TODO: also ignore tagComment starting with ';' + if not line:match "^!_TAG_" then + local fields = vim.split(line, delimiter, true) + if #fields == 3 and not tags_map[fields[1]] then + if fields[1] ~= "help-tags" or fields[2] ~= "tags" then + table.insert(tags, { + name = fields[1], + filename = help_files[fields[2]], + cmd = fields[3], + lang = lang, + }) + tags_map[fields[1]] = true + end + end + end + end + end + end + + pickers + .new(opts, { + prompt_title = "Help", + finder = finders.new_table { + results = tags, + entry_maker = function(entry) + return make_entry.set_default_entry_mt({ + value = entry.name .. "@" .. entry.lang, + display = entry.name, + ordinal = entry.name, + filename = entry.filename, + cmd = entry.cmd, + }, opts) + end, + }, + previewer = previewers.help.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + action_set.select:replace(function(_, cmd) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.help_tags" + return + end + + actions.close(prompt_bufnr) + if cmd == "default" or cmd == "horizontal" then + vim.cmd("help " .. selection.value) + elseif cmd == "vertical" then + vim.cmd("vert help " .. selection.value) + elseif cmd == "tab" then + vim.cmd("tab help " .. selection.value) + end + end) + + return true + end, + }) + :find() +end + +internal.man_pages = function(opts) + opts.sections = vim.F.if_nil(opts.sections, { "1" }) + assert(vim.tbl_islist(opts.sections), "sections should be a list") + opts.man_cmd = utils.get_lazy_default(opts.man_cmd, function() + local is_darwin = vim.loop.os_uname().sysname == "Darwin" + return is_darwin and { "apropos", " " } or { "apropos", "" } + end) + opts.entry_maker = opts.entry_maker or make_entry.gen_from_apropos(opts) + opts.env = { PATH = vim.env.PATH, MANPATH = vim.env.MANPATH } + + pickers + .new(opts, { + prompt_title = "Man", + finder = finders.new_oneshot_job(opts.man_cmd, opts), + previewer = previewers.man.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + action_set.select:replace(function(_, cmd) + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.man_pages" + return + end + + local args = selection.section .. " " .. selection.value + actions.close(prompt_bufnr) + if cmd == "default" or cmd == "horizontal" then + vim.cmd("Man " .. args) + elseif cmd == "vertical" then + vim.cmd("vert Man " .. args) + elseif cmd == "tab" then + vim.cmd("tab Man " .. args) + end + end) + + return true + end, + }) + :find() +end + +internal.reloader = function(opts) + local package_list = vim.tbl_keys(package.loaded) + + -- filter out packages we don't want and track the longest package name + local column_len = 0 + for index, module_name in pairs(package_list) do + if + type(require(module_name)) ~= "table" + or module_name:sub(1, 1) == "_" + or package.searchpath(module_name, package.path) == nil + then + table.remove(package_list, index) + elseif #module_name > column_len then + column_len = #module_name + end + end + opts.column_len = vim.F.if_nil(opts.column_len, column_len) + + pickers + .new(opts, { + prompt_title = "Packages", + finder = finders.new_table { + results = package_list, + entry_maker = opts.entry_maker or make_entry.gen_from_packages(opts), + }, + -- previewer = previewers.vim_buffer.new(opts), + sorter = conf.generic_sorter(opts), + + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.reloader" + return + end + + actions.close(prompt_bufnr) + require("plenary.reload").reload_module(selection.value) + utils.notify("builtin.reloader", { + msg = string.format("[%s] - module reloaded", selection.value), + level = "INFO", + }) + end) + + return true + end, + }) + :find() +end + +internal.buffers = function(opts) + opts = apply_cwd_only_aliases(opts) + local bufnrs = filter(function(b) + if 1 ~= vim.fn.buflisted(b) then + return false + end + -- only hide unloaded buffers if opts.show_all_buffers is false, keep them listed if true or nil + if opts.show_all_buffers == false and not vim.api.nvim_buf_is_loaded(b) then + return false + end + if opts.ignore_current_buffer and b == vim.api.nvim_get_current_buf() then + return false + end + if opts.cwd_only and not string.find(vim.api.nvim_buf_get_name(b), vim.loop.cwd(), 1, true) then + return false + end + return true + end, vim.api.nvim_list_bufs()) + if not next(bufnrs) then + return + end + if opts.sort_mru then + table.sort(bufnrs, function(a, b) + return vim.fn.getbufinfo(a)[1].lastused > vim.fn.getbufinfo(b)[1].lastused + end) + end + + local buffers = {} + local default_selection_idx = 1 + for _, bufnr in ipairs(bufnrs) do + local flag = bufnr == vim.fn.bufnr "" and "%" or (bufnr == vim.fn.bufnr "#" and "#" or " ") + + if opts.sort_lastused and not opts.ignore_current_buffer and flag == "#" then + default_selection_idx = 2 + end + + local element = { + bufnr = bufnr, + flag = flag, + info = vim.fn.getbufinfo(bufnr)[1], + } + + if opts.sort_lastused and (flag == "#" or flag == "%") then + local idx = ((buffers[1] ~= nil and buffers[1].flag == "%") and 2 or 1) + table.insert(buffers, idx, element) + else + table.insert(buffers, element) + end + end + + if not opts.bufnr_width then + local max_bufnr = math.max(unpack(bufnrs)) + opts.bufnr_width = #tostring(max_bufnr) + end + + pickers + .new(opts, { + prompt_title = "Buffers", + finder = finders.new_table { + results = buffers, + entry_maker = opts.entry_maker or make_entry.gen_from_buffer(opts), + }, + previewer = conf.grep_previewer(opts), + sorter = conf.generic_sorter(opts), + default_selection_index = default_selection_idx, + }) + :find() +end + +internal.colorscheme = function(opts) + local before_background = vim.o.background + local before_color = vim.api.nvim_exec("colorscheme", true) + local need_restore = true + + local colors = opts.colors or { before_color } + if not vim.tbl_contains(colors, before_color) then + table.insert(colors, 1, before_color) + end + + colors = vim.list_extend( + colors, + vim.tbl_filter(function(color) + return color ~= before_color + end, vim.fn.getcompletion("", "color")) + ) + + local previewer + if opts.enable_preview then + -- define previewer + local bufnr = vim.api.nvim_get_current_buf() + local p = vim.api.nvim_buf_get_name(bufnr) + + -- don't need previewer + if vim.fn.buflisted(bufnr) ~= 1 then + local deleted = false + local function del_win(win_id) + if win_id and vim.api.nvim_win_is_valid(win_id) then + utils.buf_delete(vim.api.nvim_win_get_buf(win_id)) + pcall(vim.api.nvim_win_close, win_id, true) + end + end + + previewer = previewers.new { + preview_fn = function(_, entry, status) + if not deleted then + deleted = true + del_win(status.preview_win) + del_win(status.preview_border_win) + end + vim.cmd("colorscheme " .. entry.value) + end, + } + else + -- show current buffer content in previewer + previewer = previewers.new_buffer_previewer { + get_buffer_by_name = function() + return p + end, + define_preview = function(self, entry) + if vim.loop.fs_stat(p) then + conf.buffer_previewer_maker(p, self.state.bufnr, { bufname = self.state.bufname }) + else + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines) + end + vim.cmd("colorscheme " .. entry.value) + end, + } + end + end + + local picker = pickers.new(opts, { + prompt_title = "Change Colorscheme", + finder = finders.new_table { + results = colors, + }, + sorter = conf.generic_sorter(opts), + previewer = previewer, + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.colorscheme" + return + end + + actions.close(prompt_bufnr) + need_restore = false + vim.cmd("colorscheme " .. selection.value) + end) + + return true + end, + }) + + if opts.enable_preview then + -- rewrite picker.close_windows. restore color if needed + local close_windows = picker.close_windows + picker.close_windows = function(status) + close_windows(status) + if need_restore then + vim.o.background = before_background + vim.cmd("colorscheme " .. before_color) + end + end + end + + picker:find() +end + +internal.marks = function(opts) + local local_marks = { + items = vim.fn.getmarklist(opts.bufnr), + name_func = function(_, line) + return vim.api.nvim_buf_get_lines(opts.bufnr, line - 1, line, false)[1] + end, + } + local global_marks = { + items = vim.fn.getmarklist(), + name_func = function(mark, _) + -- get buffer name if it is opened, otherwise get file name + return vim.api.nvim_get_mark(mark, {})[4] + end, + } + local marks_table = {} + local marks_others = {} + local bufname = vim.api.nvim_buf_get_name(opts.bufnr) + for _, cnf in ipairs { local_marks, global_marks } do + for _, v in ipairs(cnf.items) do + -- strip the first single quote character + local mark = string.sub(v.mark, 2, 3) + local _, lnum, col, _ = unpack(v.pos) + local name = cnf.name_func(mark, lnum) + -- same format to :marks command + local line = string.format("%s %6d %4d %s", mark, lnum, col - 1, name) + local row = { + line = line, + lnum = lnum, + col = col, + filename = v.file or bufname, + } + -- non alphanumeric marks goes to last + if mark:match "%w" then + table.insert(marks_table, row) + else + table.insert(marks_others, row) + end + end + end + marks_table = vim.fn.extend(marks_table, marks_others) + + pickers + .new(opts, { + prompt_title = "Marks", + finder = finders.new_table { + results = marks_table, + entry_maker = opts.entry_maker or make_entry.gen_from_marks(opts), + }, + previewer = conf.grep_previewer(opts), + sorter = conf.generic_sorter(opts), + push_cursor_on_edit = true, + push_tagstack_on_edit = true, + }) + :find() +end + +internal.registers = function(opts) + local registers_table = { '"', "_", "#", "=", "_", "/", "*", "+", ":", ".", "%" } + + -- named + for i = 0, 9 do + table.insert(registers_table, tostring(i)) + end + + -- alphabetical + for i = 65, 90 do + table.insert(registers_table, string.char(i)) + end + + pickers + .new(opts, { + prompt_title = "Registers", + finder = finders.new_table { + results = registers_table, + entry_maker = opts.entry_maker or make_entry.gen_from_registers(opts), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_, map) + actions.select_default:replace(actions.paste_register) + map({ "i", "n" }, "", actions.edit_register) + + return true + end, + }) + :find() +end + +-- TODO: make filtering include the mapping and the action +internal.keymaps = function(opts) + opts.modes = vim.F.if_nil(opts.modes, { "n", "i", "c", "x" }) + opts.show_plug = vim.F.if_nil(opts.show_plug, true) + + local keymap_encountered = {} -- used to make sure no duplicates are inserted into keymaps_table + local keymaps_table = {} + local max_len_lhs = 0 + + -- helper function to populate keymaps_table and determine max_len_lhs + local function extract_keymaps(keymaps) + for _, keymap in pairs(keymaps) do + local keymap_key = keymap.buffer .. keymap.mode .. keymap.lhs -- should be distinct for every keymap + if not keymap_encountered[keymap_key] then + keymap_encountered[keymap_key] = true + if opts.show_plug or not string.find(keymap.lhs, "") then + table.insert(keymaps_table, keymap) + max_len_lhs = math.max(max_len_lhs, #utils.display_termcodes(keymap.lhs)) + end + end + end + end + + for _, mode in pairs(opts.modes) do + local global = vim.api.nvim_get_keymap(mode) + local buf_local = vim.api.nvim_buf_get_keymap(0, mode) + extract_keymaps(global) + extract_keymaps(buf_local) + end + opts.width_lhs = max_len_lhs + 1 + + pickers + .new(opts, { + prompt_title = "Key Maps", + finder = finders.new_table { + results = keymaps_table, + entry_maker = opts.entry_maker or make_entry.gen_from_keymaps(opts), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.keymaps" + return + end + + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(selection.value.lhs, true, false, true), "t", true) + return actions.close(prompt_bufnr) + end) + return true + end, + }) + :find() +end + +internal.filetypes = function(opts) + local filetypes = vim.fn.getcompletion("", "filetype") + + pickers + .new(opts, { + prompt_title = "Filetypes", + finder = finders.new_table { + results = filetypes, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + print "[telescope] Nothing currently selected" + return + end + + actions.close(prompt_bufnr) + vim.cmd("setfiletype " .. selection[1]) + end) + return true + end, + }) + :find() +end + +internal.highlights = function(opts) + local highlights = vim.fn.getcompletion("", "highlight") + + pickers + .new(opts, { + prompt_title = "Highlights", + finder = finders.new_table { + results = highlights, + entry_maker = opts.entry_maker or make_entry.gen_from_highlights(opts), + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.highlights" + return + end + + actions.close(prompt_bufnr) + vim.cmd("hi " .. selection.value) + end) + return true + end, + previewer = previewers.highlights.new(opts), + }) + :find() +end + +internal.autocommands = function(opts) + local autocmds = vim.api.nvim_get_autocmds {} + table.sort(autocmds, function(lhs, rhs) + return lhs.event < rhs.event + end) + pickers + .new(opts, { + prompt_title = "autocommands", + finder = finders.new_table { + results = autocmds, + entry_maker = opts.entry_maker or make_entry.gen_from_autocommands(opts), + }, + previewer = previewers.autocommands.new(opts), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + action_set.select:replace_if(function() + local selection = action_state.get_selected_entry() + if selection == nil then + return false + end + local val = selection.value + local group_name = val.group_name ~= "" and val.group_name or "" + local output = + vim.fn.execute("verb autocmd " .. group_name .. " " .. val.event .. " " .. val.pattern, "silent") + for line in output:gmatch "[^\r\n]+" do + local source_file = line:match "Last set from (.*) line %d*$" or line:match "Last set from (.*)$" + if source_file and source_file ~= "Lua" then + selection.filename = source_file + local source_lnum = line:match "line (%d*)$" or "1" + selection.lnum = tonumber(source_lnum) + selection.col = 1 + return false + end + end + return true + end, function() + local selection = action_state.get_selected_entry() + actions.close(prompt_bufnr) + print("You selected autocmd: " .. vim.inspect(selection.value)) + end) + + return true + end, + }) + :find() +end + +internal.spell_suggest = function(opts) + local cursor_word = vim.fn.expand "" + local suggestions = vim.fn.spellsuggest(cursor_word) + + pickers + .new(opts, { + prompt_title = "Spelling Suggestions", + finder = finders.new_table { + results = suggestions, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + if selection == nil then + utils.__warn_no_selection "builtin.spell_suggest" + return + end + + action_state.get_current_picker(prompt_bufnr)._original_mode = "i" + actions.close(prompt_bufnr) + vim.cmd("normal! ciw" .. selection[1]) + vim.cmd "stopinsert" + end) + return true + end, + }) + :find() +end + +internal.tagstack = function(opts) + opts = opts or {} + local tagstack = vim.fn.gettagstack().items + + local tags = {} + for i = #tagstack, 1, -1 do + local tag = tagstack[i] + tag.bufnr = tag.from[1] + if vim.api.nvim_buf_is_valid(tag.bufnr) then + tags[#tags + 1] = tag + tag.filename = vim.fn.bufname(tag.bufnr) + tag.lnum = tag.from[2] + tag.col = tag.from[3] + + tag.text = vim.api.nvim_buf_get_lines(tag.bufnr, tag.lnum - 1, tag.lnum, false)[1] or "" + end + end + + if vim.tbl_isempty(tags) then + utils.notify("builtin.tagstack", { + msg = "No tagstack available", + level = "WARN", + }) + return + end + + pickers + .new(opts, { + prompt_title = "TagStack", + finder = finders.new_table { + results = tags, + entry_maker = make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + }) + :find() +end + +internal.jumplist = function(opts) + opts = opts or {} + local jumplist = vim.fn.getjumplist()[1] + + -- reverse the list + local sorted_jumplist = {} + for i = #jumplist, 1, -1 do + if vim.api.nvim_buf_is_valid(jumplist[i].bufnr) then + jumplist[i].text = vim.api.nvim_buf_get_lines(jumplist[i].bufnr, jumplist[i].lnum, jumplist[i].lnum + 1, false)[1] + or "" + table.insert(sorted_jumplist, jumplist[i]) + end + end + + pickers + .new(opts, { + prompt_title = "Jumplist", + finder = finders.new_table { + results = sorted_jumplist, + entry_maker = make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + }) + :find() +end + +local function apply_checks(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + opts = opts or {} + + v(opts) + end + end + + return mod +end + +return apply_checks(internal) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__lsp.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__lsp.lua new file mode 100644 index 0000000..c23f25c --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/__lsp.lua @@ -0,0 +1,414 @@ +local channel = require("plenary.async.control").channel +local actions = require "telescope.actions" +local sorters = require "telescope.sorters" +local conf = require("telescope.config").values +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local pickers = require "telescope.pickers" +local utils = require "telescope.utils" + +local lsp = {} + +lsp.references = function(opts) + local filepath = vim.api.nvim_buf_get_name(opts.bufnr) + local lnum = vim.api.nvim_win_get_cursor(opts.winnr)[1] + local params = vim.lsp.util.make_position_params(opts.winnr) + local include_current_line = vim.F.if_nil(opts.include_current_line, false) + params.context = { includeDeclaration = vim.F.if_nil(opts.include_declaration, true) } + + vim.lsp.buf_request(opts.bufnr, "textDocument/references", params, function(err, result, ctx, _) + if err then + vim.api.nvim_err_writeln("Error when finding references: " .. err.message) + return + end + + local locations = {} + if result then + local results = vim.lsp.util.locations_to_items(result, vim.lsp.get_client_by_id(ctx.client_id).offset_encoding) + if not include_current_line then + locations = vim.tbl_filter(function(v) + -- Remove current line from result + return not (v.filename == filepath and v.lnum == lnum) + end, vim.F.if_nil(results, {})) + else + locations = vim.F.if_nil(results, {}) + end + end + + if vim.tbl_isempty(locations) then + return + end + + if #locations == 1 and opts.jump_type ~= "never" then + if opts.jump_type == "tab" then + vim.cmd "tabedit" + elseif opts.jump_type == "split" then + vim.cmd "new" + elseif opts.jump_type == "vsplit" then + vim.cmd "vnew" + end + -- jump to location + local location = locations[1] + local bufnr = opts.bufnr + if location.filename then + bufnr = vim.uri_to_bufnr(vim.uri_from_fname(location.filename)) + end + vim.api.nvim_win_set_buf(0, bufnr) + vim.api.nvim_win_set_cursor(0, { location.lnum, location.col - 1 }) + return + end + + pickers + .new(opts, { + prompt_title = "LSP References", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + push_cursor_on_edit = true, + push_tagstack_on_edit = true, + }) + :find() + end) +end + +local function call_hierarchy(opts, method, title, direction, item) + vim.lsp.buf_request(opts.bufnr, method, { item = item }, function(err, result) + if err then + vim.api.nvim_err_writeln("Error handling " .. title .. ": " .. err.message) + return + end + + if not result or vim.tbl_isempty(result) then + return + end + + local locations = {} + for _, ch_call in pairs(result) do + local ch_item = ch_call[direction] + for _, range in pairs(ch_call.fromRanges) do + table.insert(locations, { + filename = vim.uri_to_fname(ch_item.uri), + text = ch_item.name, + lnum = range.start.line + 1, + col = range.start.character + 1, + }) + end + end + + pickers + .new(opts, { + prompt_title = title, + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + push_cursor_on_edit = true, + push_tagstack_on_edit = true, + }) + :find() + end) +end + +local function pick_call_hierarchy_item(call_hierarchy_items) + if not call_hierarchy_items then + return + end + if #call_hierarchy_items == 1 then + return call_hierarchy_items[1] + end + local items = {} + for i, item in pairs(call_hierarchy_items) do + local entry = item.detail or item.name + table.insert(items, string.format("%d. %s", i, entry)) + end + local choice = vim.fn.inputlist(items) + if choice < 1 or choice > #items then + return + end + return choice +end + +local function calls(opts, direction) + local params = vim.lsp.util.make_position_params() + vim.lsp.buf_request(opts.bufnr, "textDocument/prepareCallHierarchy", params, function(err, result) + if err then + vim.api.nvim_err_writeln("Error when preparing call hierarchy: " .. err) + return + end + + local call_hierarchy_item = pick_call_hierarchy_item(result) + if not call_hierarchy_item then + return + end + + if direction == "from" then + call_hierarchy(opts, "callHierarchy/incomingCalls", "LSP Incoming Calls", direction, call_hierarchy_item) + else + call_hierarchy(opts, "callHierarchy/outgoingCalls", "LSP Outgoing Calls", direction, call_hierarchy_item) + end + end) +end + +lsp.incoming_calls = function(opts) + calls(opts, "from") +end + +lsp.outgoing_calls = function(opts) + calls(opts, "to") +end + +local function list_or_jump(action, title, opts) + local params = vim.lsp.util.make_position_params(opts.winnr) + vim.lsp.buf_request(opts.bufnr, action, params, function(err, result, ctx, _) + if err then + vim.api.nvim_err_writeln("Error when executing " .. action .. " : " .. err.message) + return + end + local flattened_results = {} + if result then + -- textDocument/definition can return Location or Location[] + if not vim.tbl_islist(result) then + flattened_results = { result } + end + + vim.list_extend(flattened_results, result) + end + + local offset_encoding = vim.lsp.get_client_by_id(ctx.client_id).offset_encoding + + if #flattened_results == 0 then + return + elseif #flattened_results == 1 and opts.jump_type ~= "never" then + if opts.jump_type == "tab" then + vim.cmd "tabedit" + elseif opts.jump_type == "split" then + vim.cmd "new" + elseif opts.jump_type == "vsplit" then + vim.cmd "vnew" + end + vim.lsp.util.jump_to_location(flattened_results[1], offset_encoding) + else + local locations = vim.lsp.util.locations_to_items(flattened_results, offset_encoding) + pickers + .new(opts, { + prompt_title = title, + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.generic_sorter(opts), + push_cursor_on_edit = true, + push_tagstack_on_edit = true, + }) + :find() + end + end) +end + +lsp.definitions = function(opts) + return list_or_jump("textDocument/definition", "LSP Definitions", opts) +end + +lsp.type_definitions = function(opts) + return list_or_jump("textDocument/typeDefinition", "LSP Type Definitions", opts) +end + +lsp.implementations = function(opts) + return list_or_jump("textDocument/implementation", "LSP Implementations", opts) +end + +lsp.document_symbols = function(opts) + local params = vim.lsp.util.make_position_params(opts.winnr) + vim.lsp.buf_request(opts.bufnr, "textDocument/documentSymbol", params, function(err, result, _, _) + if err then + vim.api.nvim_err_writeln("Error when finding document symbols: " .. err.message) + return + end + + if not result or vim.tbl_isempty(result) then + utils.notify("builtin.lsp_document_symbols", { + msg = "No results from textDocument/documentSymbol", + level = "INFO", + }) + return + end + + local locations = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr) or {} + locations = utils.filter_symbols(locations, opts) + if locations == nil then + -- error message already printed in `utils.filter_symbols` + return + end + + if vim.tbl_isempty(locations) then + utils.notify("builtin.lsp_document_symbols", { + msg = "No document_symbol locations found", + level = "INFO", + }) + return + end + + opts.path_display = { "hidden" } + pickers + .new(opts, { + prompt_title = "LSP Document Symbols", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.prefilter_sorter { + tag = "symbol_type", + sorter = conf.generic_sorter(opts), + }, + push_cursor_on_edit = true, + push_tagstack_on_edit = true, + }) + :find() + end) +end + +lsp.workspace_symbols = function(opts) + local params = { query = opts.query or "" } + vim.lsp.buf_request(opts.bufnr, "workspace/symbol", params, function(err, server_result, _, _) + if err then + vim.api.nvim_err_writeln("Error when finding workspace symbols: " .. err.message) + return + end + + local locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr) or {} + locations = utils.filter_symbols(locations, opts) + if locations == nil then + -- error message already printed in `utils.filter_symbols` + return + end + + if vim.tbl_isempty(locations) then + utils.notify("builtin.lsp_workspace_symbols", { + msg = "No results from workspace/symbol. Maybe try a different query: " + .. "'Telescope lsp_workspace_symbols query=example'", + level = "INFO", + }) + return + end + + opts.ignore_filename = vim.F.if_nil(opts.ignore_filename, false) + + pickers + .new(opts, { + prompt_title = "LSP Workspace Symbols", + finder = finders.new_table { + results = locations, + entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = conf.prefilter_sorter { + tag = "symbol_type", + sorter = conf.generic_sorter(opts), + }, + }) + :find() + end) +end + +local function get_workspace_symbols_requester(bufnr, opts) + local cancel = function() end + + return function(prompt) + local tx, rx = channel.oneshot() + cancel() + _, cancel = vim.lsp.buf_request(bufnr, "workspace/symbol", { query = prompt }, tx) + + -- Handle 0.5 / 0.5.1 handler situation + local err, res = rx() + assert(not err, err) + + local locations = vim.lsp.util.symbols_to_items(res or {}, bufnr) or {} + if not vim.tbl_isempty(locations) then + locations = utils.filter_symbols(locations, opts) or {} + end + return locations + end +end + +lsp.dynamic_workspace_symbols = function(opts) + pickers + .new(opts, { + prompt_title = "LSP Dynamic Workspace Symbols", + finder = finders.new_dynamic { + entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts), + fn = get_workspace_symbols_requester(opts.bufnr, opts), + }, + previewer = conf.qflist_previewer(opts), + sorter = sorters.highlighter_only(opts), + attach_mappings = function(_, map) + map("i", "", actions.to_fuzzy_refine) + return true + end, + }) + :find() +end + +local function check_capabilities(feature, bufnr) + local clients = vim.lsp.buf_get_clients(bufnr) + + local supported_client = false + for _, client in pairs(clients) do + supported_client = client.server_capabilities[feature] + if supported_client then + break + end + end + + if supported_client then + return true + else + if #clients == 0 then + utils.notify("builtin.lsp_*", { + msg = "no client attached", + level = "INFO", + }) + else + utils.notify("builtin.lsp_*", { + msg = "server does not support " .. feature, + level = "INFO", + }) + end + return false + end +end + +local feature_map = { + ["document_symbols"] = "documentSymbolProvider", + ["references"] = "referencesProvider", + ["definitions"] = "definitionProvider", + ["type_definitions"] = "typeDefinitionProvider", + ["implementations"] = "implementationProvider", + ["workspace_symbols"] = "workspaceSymbolProvider", + ["incoming_calls"] = "callHierarchyProvider", + ["outgoing_calls"] = "callHierarchyProvider", +} + +local function apply_checks(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + opts = opts or {} + + local feature_name = feature_map[k] + if feature_name and not check_capabilities(feature_name, opts.bufnr) then + return + end + v(opts) + end + end + + return mod +end + +return apply_checks(lsp) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/init.lua new file mode 100644 index 0000000..4c069f0 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/builtin/init.lua @@ -0,0 +1,522 @@ +---@tag telescope.builtin + +---@config { ['field_heading'] = "Options", ["module"] = "telescope.builtin" } + +---@brief [[ +--- Telescope Builtins is a collection of community maintained pickers to support common workflows. It can be used as +--- reference when writing PRs, Telescope extensions, your own custom pickers, or just as a discovery tool for all of +--- the amazing pickers already shipped with Telescope! +--- +--- Any of these functions can just be called directly by doing: +--- +--- :lua require('telescope.builtin').$NAME_OF_PICKER() +--- +--- To use any of Telescope's default options or any picker-specific options, call your desired picker by passing a lua +--- table to the picker with all of the options you want to use. Here's an example with the live_grep picker: +--- +--- +--- :lua require('telescope.builtin').live_grep({ +--- prompt_title = 'find string in open buffers...', +--- grep_open_files = true +--- }) +--- -- or with dropdown theme +--- :lua require('telescope.builtin').find_files(require('telescope.themes').get_dropdown{ +--- previewer = false +--- }) +--- +---@brief ]] + +local builtin = {} + +-- Ref: https://github.com/tjdevries/lazy.nvim +local function require_on_exported_call(mod) + return setmetatable({}, { + __index = function(_, picker) + return function(...) + return require(mod)[picker](...) + end + end, + }) +end + +-- +-- +-- File-related Pickers +-- +-- + +--- Search for a string and get results live as you type, respects .gitignore +---@param opts table: options to pass to the picker +---@field cwd string: root dir to search from (default: cwd, use utils.buffer_dir() to search relative to open buffer) +---@field grep_open_files boolean: if true, restrict search to open files only, mutually exclusive with `search_dirs` +---@field search_dirs table: directory/directories/files to search, mutually exclusive with `grep_open_files` +---@field glob_pattern string|table: argument to be used with `--glob`, e.g. "*.toml", can use the opposite "!*.toml" +---@field type_filter string: argument to be used with `--type`, e.g. "rust", see `rg --type-list` +---@field additional_args function|table: additional arguments to be passed on. Can be fn(opts) -> tbl +---@field max_results number: define a upper result value +---@field disable_coordinates boolean: don't show the line & row numbers (default: false) +builtin.live_grep = require_on_exported_call("telescope.builtin.__files").live_grep + +--- Searches for the string under your cursor in your current working directory +---@param opts table: options to pass to the picker +---@field cwd string: root dir to search from (default: cwd, use utils.buffer_dir() to search relative to open buffer) +---@field search string: the query to search +---@field grep_open_files boolean: if true, restrict search to open files only, mutually exclusive with `search_dirs` +---@field search_dirs table: directory/directories/files to search, mutually exclusive with `grep_open_files` +---@field use_regex boolean: if true, special characters won't be escaped, allows for using regex (default: false) +---@field word_match string: can be set to `-w` to enable exact word matches +---@field additional_args function|table: additional arguments to be passed on. Can be fn(opts) -> tbl +---@field disable_coordinates boolean: don't show the line and row numbers (default: false) +---@field only_sort_text boolean: only sort the text, not the file, line or row (default: false) +builtin.grep_string = require_on_exported_call("telescope.builtin.__files").grep_string + +--- Search for files (respecting .gitignore) +---@param opts table: options to pass to the picker +---@field cwd string: root dir to search from (default: cwd, use utils.buffer_dir() to search relative to open buffer) +---@field find_command function|table: cmd to use for the search. Can be a fn(opts) -> tbl (default: autodetect) +---@field follow boolean: if true, follows symlinks (i.e. uses `-L` flag for the `find` command) +---@field hidden boolean: determines whether to show hidden files or not (default: false) +---@field no_ignore boolean: show files ignored by .gitignore, .ignore, etc. (default: false) +---@field no_ignore_parent boolean: show files ignored by .gitignore, .ignore, etc. in parent dirs. (default: false) +---@field search_dirs table: directory/directories/files to search +---@field search_file string: specify a filename to search for +builtin.find_files = require_on_exported_call("telescope.builtin.__files").find_files + +--- This is an alias for the `find_files` picker +builtin.fd = builtin.find_files + +--- Lists function names, variables, and other symbols from treesitter queries +--- - Default keymaps: +--- - ``: show autocompletion menu to prefilter your query by kind of ts node you want to see (i.e. `:var:`) +---@field show_line boolean: if true, shows the row:column that the result is found at (default: true) +---@field bufnr number: specify the buffer number where treesitter should run. (default: current buffer) +---@field symbol_highlights table: string -> string. Matches symbol with hl_group +builtin.treesitter = require_on_exported_call("telescope.builtin.__files").treesitter + +--- Live fuzzy search inside of the currently open buffer +---@param opts table: options to pass to the picker +---@field skip_empty_lines boolean: if true we dont display empty lines (default: false) +builtin.current_buffer_fuzzy_find = require_on_exported_call("telescope.builtin.__files").current_buffer_fuzzy_find + +--- Lists tags in current directory with tag location file preview (users are required to run ctags -R to generate tags +--- or update when introducing new changes) +---@param opts table: options to pass to the picker +---@field cwd string: root dir to search from (default: cwd, use utils.buffer_dir() to search relative to open buffer) +---@field ctags_file string: specify a particular ctags file to use +---@field show_line boolean: if true, shows the content of the line the tag is found on in the picker (default: true) +---@field only_sort_tags boolean: if true we will only sort tags (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +builtin.tags = require_on_exported_call("telescope.builtin.__files").tags + +--- Lists all of the tags for the currently open buffer, with a preview +---@param opts table: options to pass to the picker +---@field cwd string: root dir to search from (default: cwd, use utils.buffer_dir() to search relative to open buffer) +---@field ctags_file string: specify a particular ctags file to use +---@field show_line boolean: if true, shows the content of the line the tag is found on in the picker (default: true) +---@field only_sort_tags boolean: if true we will only sort tags (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +builtin.current_buffer_tags = require_on_exported_call("telescope.builtin.__files").current_buffer_tags + +-- +-- +-- Git-related Pickers +-- +-- + +--- Fuzzy search for files tracked by Git. This command lists the output of the `git ls-files` command, +--- respects .gitignore +--- - Default keymaps: +--- - ``: opens the currently selected file +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field show_untracked boolean: if true, adds `--others` flag to command and shows untracked files (default: false) +---@field recurse_submodules boolean: if true, adds the `--recurse-submodules` flag to command (default: false) +---@field git_command table: command that will be exectued. {"git","ls-files","--exclude-standard","--cached"} +builtin.git_files = require_on_exported_call("telescope.builtin.__git").files + +--- Lists commits for current directory with diff preview +--- - Default keymaps: +--- - ``: checks out the currently selected commit +--- - `m`: resets current branch to selected commit using mixed mode +--- - `s`: resets current branch to selected commit using soft mode +--- - `h`: resets current branch to selected commit using hard mode +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field git_command table: command that will be exectued. {"git","log","--pretty=oneline","--abbrev-commit","--","."} +builtin.git_commits = require_on_exported_call("telescope.builtin.__git").commits + +--- Lists commits for current buffer with diff preview +--- - Default keymaps or your overriden `select_` keys: +--- - ``: checks out the currently selected commit +--- - ``: opens a diff in a vertical split +--- - ``: opens a diff in a horizontal split +--- - ``: opens a diff in a new tab +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field current_file string: specify the current file that should be used for bcommits (default: current buffer) +---@field git_command table: command that will be exectued. {"git","log","--pretty=oneline","--abbrev-commit"} +builtin.git_bcommits = require_on_exported_call("telescope.builtin.__git").bcommits + +--- List branches for current directory, with output from `git log --oneline` shown in the preview window +--- - Default keymaps: +--- - ``: checks out the currently selected branch +--- - ``: tracks currently selected branch +--- - ``: rebases currently selected branch +--- - ``: creates a new branch, with confirmation prompt before creation +--- - ``: deletes the currently selected branch, with confirmation prompt before deletion +--- - ``: merges the currently selected branch, with confirmation prompt before deletion +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field pattern string: specify the pattern to match all refs +builtin.git_branches = require_on_exported_call("telescope.builtin.__git").branches + +--- Lists git status for current directory +--- - Default keymaps: +--- - ``: stages or unstages the currently selected file +--- - ``: opens the currently selected file +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field git_icons table: string -> string. Matches name with icon (see source code, make_entry.lua git_icon_defaults) +builtin.git_status = require_on_exported_call("telescope.builtin.__git").status + +--- Lists stash items in current repository +--- - Default keymaps: +--- - ``: runs `git apply` for currently selected stash +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field show_branch boolean: if we should display the branch name for git stash entries (default: true) +builtin.git_stash = require_on_exported_call("telescope.builtin.__git").stash + +-- +-- +-- Internal and Vim-related Pickers +-- +-- + +--- Lists all of the community maintained pickers built into Telescope +---@param opts table: options to pass to the picker +---@field include_extensions boolean: if true will show the pickers of the installed extensions (default: false) +---@field use_default_opts boolean: if the selected picker should use its default options (default: false) +builtin.builtin = require_on_exported_call("telescope.builtin.__internal").builtin + +--- Opens the previous picker in the identical state (incl. multi selections) +--- - Notes: +--- - Requires `cache_picker` in setup or when having invoked pickers, see |telescope.defaults.cache_picker| +---@param opts table: options to pass to the picker +---@field cache_index number: what picker to resume, where 1 denotes most recent (default: 1) +builtin.resume = require_on_exported_call("telescope.builtin.__internal").resume + +--- Opens a picker over previously cached pickers in their preserved states (incl. multi selections) +--- - Default keymaps: +--- - ``: delete the selected cached picker +--- - Notes: +--- - Requires `cache_picker` in setup or when having invoked pickers, see |telescope.defaults.cache_picker| +---@param opts table: options to pass to the picker +builtin.pickers = require_on_exported_call("telescope.builtin.__internal").pickers + +--- Use the telescope... +---@param opts table: options to pass to the picker +---@field show_pluto boolean: we love pluto (default: false, because its a hidden feature) +---@field show_moon boolean: we love the moon (default: false, because its a hidden feature) +builtin.planets = require_on_exported_call("telescope.builtin.__internal").planets + +--- Lists symbols inside of `data/telescope-sources/*.json` found in your runtime path +--- or found in `stdpath("data")/telescope/symbols/*.json`. The second path can be customized. +--- We provide a couple of default symbols which can be found in +--- https://github.com/nvim-telescope/telescope-symbols.nvim. This repos README also provides more +--- information about the format in which the symbols have to be. +---@param opts table: options to pass to the picker +---@field symbol_path string: specify the second path. Default: `stdpath("data")/telescope/symbols/*.json` +---@field sources table: specify a table of sources you want to load this time +builtin.symbols = require_on_exported_call("telescope.builtin.__internal").symbols + +--- Lists available plugin/user commands and runs them on `` +---@param opts table: options to pass to the picker +---@field show_buf_command boolean: show buf local command (Default: true) +builtin.commands = require_on_exported_call("telescope.builtin.__internal").commands + +--- Lists items in the quickfix list, jumps to location on `` +---@param opts table: options to pass to the picker +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +---@field nr number: specify the quickfix list number +builtin.quickfix = require_on_exported_call("telescope.builtin.__internal").quickfix + +--- Lists all quickfix lists in your history and open them with `builtin.quickfix`. It seems that neovim +--- only keeps the full history for 10 lists +---@param opts table: options to pass to the picker +builtin.quickfixhistory = require_on_exported_call("telescope.builtin.__internal").quickfixhistory + +--- Lists items from the current window's location list, jumps to location on `` +---@param opts table: options to pass to the picker +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +builtin.loclist = require_on_exported_call("telescope.builtin.__internal").loclist + +--- Lists previously open files, opens on `` +---@param opts table: options to pass to the picker +---@field only_cwd boolean: show only files in the cwd (default: false) +---@field cwd_only boolean: alias for only_cwd +builtin.oldfiles = require_on_exported_call("telescope.builtin.__internal").oldfiles + +--- Lists commands that were executed recently, and reruns them on `` +--- - Default keymaps: +--- - ``: open the command line with the text of the currently selected result populated in it +---@param opts table: options to pass to the picker +---@field filter_fn function: filter fn(cmd:string). true if the history command should be presented. +builtin.command_history = require_on_exported_call("telescope.builtin.__internal").command_history + +--- Lists searches that were executed recently, and reruns them on `` +--- - Default keymaps: +--- - ``: open a search window with the text of the currently selected search result populated in it +---@param opts table: options to pass to the picker +builtin.search_history = require_on_exported_call("telescope.builtin.__internal").search_history + +--- Lists vim options, allows you to edit the current value on `` +---@param opts table: options to pass to the picker +builtin.vim_options = require_on_exported_call("telescope.builtin.__internal").vim_options + +--- Lists available help tags and opens a new window with the relevant help info on `` +---@param opts table: options to pass to the picker +---@field lang string: specify language (default: vim.o.helplang) +---@field fallback boolean: fallback to en if language isn't installed (default: true) +builtin.help_tags = require_on_exported_call("telescope.builtin.__internal").help_tags + +--- Lists manpage entries, opens them in a help window on `` +---@param opts table: options to pass to the picker +---@field sections table: a list of sections to search, use `{ "ALL" }` to search in all sections (default: { "1" }) +---@field man_cmd function: that returns the man command. (Default: `apropos ""` on linux, `apropos " "` on macos) +builtin.man_pages = require_on_exported_call("telescope.builtin.__internal").man_pages + +--- Lists lua modules and reloads them on `` +---@param opts table: options to pass to the picker +---@field column_len number: define the max column len for the module name (default: dynamic, longest module name) +builtin.reloader = require_on_exported_call("telescope.builtin.__internal").reloader + +--- Lists open buffers in current neovim instance, opens selected buffer on `` +---@param opts table: options to pass to the picker +---@field show_all_buffers boolean: if true, show all buffers, including unloaded buffers (default: true) +---@field ignore_current_buffer boolean: if true, don't show the current buffer in the list (default: false) +---@field only_cwd boolean: if true, only show buffers in the current working directory (default: false) +---@field cwd_only boolean: alias for only_cwd +---@field sort_lastused boolean: Sorts current and last buffer to the top and selects the lastused (default: false) +---@field sort_mru boolean: Sorts all buffers after most recent used. Not just the current and last one (default: false) +---@field bufnr_width number: Defines the width of the buffer numbers in front of the filenames (default: dynamic) +builtin.buffers = require_on_exported_call("telescope.builtin.__internal").buffers + +--- Lists available colorschemes and applies them on `` +---@param opts table: options to pass to the picker +---@field enable_preview boolean: if true, will preview the selected color +builtin.colorscheme = require_on_exported_call("telescope.builtin.__internal").colorscheme + +--- Lists vim marks and their value, jumps to the mark on `` +---@param opts table: options to pass to the picker +builtin.marks = require_on_exported_call("telescope.builtin.__internal").marks + +--- Lists vim registers, pastes the contents of the register on `` +--- - Default keymaps: +--- - ``: edit the contents of the currently selected register +---@param opts table: options to pass to the picker +builtin.registers = require_on_exported_call("telescope.builtin.__internal").registers + +--- Lists normal mode keymappings, runs the selected keymap on `` +---@param opts table: options to pass to the picker +---@field modes table: a list of short-named keymap modes to search (default: { "n", "i", "c", "x" }) +---@field show_plug boolean: if true, the keymaps for which the lhs contains "" are also shown (default: true) +builtin.keymaps = require_on_exported_call("telescope.builtin.__internal").keymaps + +--- Lists all available filetypes, sets currently open buffer's filetype to selected filetype in Telescope on `` +---@param opts table: options to pass to the picker +builtin.filetypes = require_on_exported_call("telescope.builtin.__internal").filetypes + +--- Lists all available highlights +---@param opts table: options to pass to the picker +builtin.highlights = require_on_exported_call("telescope.builtin.__internal").highlights + +--- Lists vim autocommands and goes to their declaration on `` +---@param opts table: options to pass to the picker +builtin.autocommands = require_on_exported_call("telescope.builtin.__internal").autocommands + +--- Lists spelling suggestions for the current word under the cursor, replaces word with selected suggestion on `` +---@param opts table: options to pass to the picker +builtin.spell_suggest = require_on_exported_call("telescope.builtin.__internal").spell_suggest + +--- Lists the tag stack for the current window, jumps to tag on `` +---@param opts table: options to pass to the picker +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +builtin.tagstack = require_on_exported_call("telescope.builtin.__internal").tagstack + +--- Lists items from Vim's jumplist, jumps to location on `` +---@param opts table: options to pass to the picker +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +---@field fname_width number: defines the width of the filename section (default: 30) +builtin.jumplist = require_on_exported_call("telescope.builtin.__internal").jumplist + +-- +-- +-- LSP-related Pickers +-- +-- + +--- Lists LSP references for word under the cursor, jumps to reference on `` +---@param opts table: options to pass to the picker +---@field include_declaration boolean: include symbol declaration in the lsp references (default: true) +---@field include_current_line boolean: include current line (default: false) +---@field jump_type string: how to goto reference if there is only one, values: "tab", "split", "vsplit", "never" +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_references = require_on_exported_call("telescope.builtin.__lsp").references + +--- Lists LSP incoming calls for word under the cursor, jumps to reference on `` +---@param opts table: options to pass to the picker +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_incoming_calls = require_on_exported_call("telescope.builtin.__lsp").incoming_calls + +--- Lists LSP outgoing calls for word under the cursor, jumps to reference on `` +---@param opts table: options to pass to the picker +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_outgoing_calls = require_on_exported_call("telescope.builtin.__lsp").outgoing_calls + +--- Goto the definition of the word under the cursor, if there's only one, otherwise show all options in Telescope +---@param opts table: options to pass to the picker +---@field jump_type string: how to goto definition if there is only one, values: "tab", "split", "vsplit", "never" +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_definitions = require_on_exported_call("telescope.builtin.__lsp").definitions + +--- Goto the definition of the type of the word under the cursor, if there's only one, +--- otherwise show all options in Telescope +---@param opts table: options to pass to the picker +---@field jump_type string: how to goto definition if there is only one, values: "tab", "split", "vsplit", "never" +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_type_definitions = require("telescope.builtin.__lsp").type_definitions + +--- Goto the implementation of the word under the cursor if there's only one, otherwise show all options in Telescope +---@param opts table: options to pass to the picker +---@field jump_type string: how to goto implementation if there is only one, values: "tab", "split", "vsplit", "never" +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: show results text (default: true) +---@field trim_text boolean: trim results text (default: false) +builtin.lsp_implementations = require_on_exported_call("telescope.builtin.__lsp").implementations + +--- Lists LSP document symbols in the current buffer +--- - Default keymaps: +--- - ``: show autocompletion menu to prefilter your query by type of symbol you want to see (i.e. `:variable:`) +---@param opts table: options to pass to the picker +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: if true, shows the content of the line the tag is found on (default: false) +---@field symbols string|table: filter results by symbol kind(s) +---@field ignore_symbols string|table: list of symbols to ignore +---@field symbol_highlights table: string -> string. Matches symbol with hl_group +builtin.lsp_document_symbols = require_on_exported_call("telescope.builtin.__lsp").document_symbols + +--- Lists LSP document symbols in the current workspace +--- - Default keymaps: +--- - ``: show autocompletion menu to prefilter your query by type of symbol you want to see (i.e. `:variable:`) +---@param opts table: options to pass to the picker +---@field query string: for what to query the workspace (default: "") +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: if true, shows the content of the line the tag is found on (default: false) +---@field symbols string|table: filter results by symbol kind(s) +---@field ignore_symbols string|table: list of symbols to ignore +---@field symbol_highlights table: string -> string. Matches symbol with hl_group +builtin.lsp_workspace_symbols = require_on_exported_call("telescope.builtin.__lsp").workspace_symbols + +--- Dynamically lists LSP for all workspace symbols +--- - Default keymaps: +--- - ``: show autocompletion menu to prefilter your query by type of symbol you want to see (i.e. `:variable:`) +---@param opts table: options to pass to the picker +---@field fname_width number: defines the width of the filename section (default: 30) +---@field show_line boolean: if true, shows the content of the line the symbol is found on (default: false) +---@field symbols string|table: filter results by symbol kind(s) +---@field ignore_symbols string|table: list of symbols to ignore +---@field symbol_highlights table: string -> string. Matches symbol with hl_group +builtin.lsp_dynamic_workspace_symbols = require_on_exported_call("telescope.builtin.__lsp").dynamic_workspace_symbols + +-- +-- +-- Diagnostics Pickers +-- +-- + +--- Lists diagnostics +--- - Fields: +--- - `All severity flags can be passed as `string` or `number` as per `:vim.diagnostic.severity:` +--- - Default keymaps: +--- - ``: show autocompletion menu to prefilter your query with the diagnostic you want to see (i.e. `:warning:`) +---@param opts table: options to pass to the picker +---@field bufnr number|nil: Buffer number to get diagnostics from. Use 0 for current buffer or nil for all buffers +---@field severity string|number: filter diagnostics by severity name (string) or id (number) +---@field severity_limit string|number: keep diagnostics equal or more severe wrt severity name (string) or id (number) +---@field severity_bound string|number: keep diagnostics equal or less severe wrt severity name (string) or id (number) +---@field root_dir string|boolean: if set to string, get diagnostics only for buffers under this dir otherwise cwd +---@field no_unlisted boolean: if true, get diagnostics only for listed buffers +---@field no_sign boolean: hide DiagnosticSigns from Results (default: false) +---@field line_width number: set length of diagnostic entry text in Results +---@field namespace number: limit your diagnostics to a specific namespace +builtin.diagnostics = require_on_exported_call("telescope.builtin.__diagnostics").get + +local apply_config = function(mod) + for k, v in pairs(mod) do + mod[k] = function(opts) + local pickers_conf = require("telescope.config").pickers + + opts = opts or {} + opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf() + opts.winnr = opts.winnr or vim.api.nvim_get_current_win() + local pconf = pickers_conf[k] or {} + local defaults = (function() + if pconf.theme then + return require("telescope.themes")["get_" .. pconf.theme](pconf) + end + return vim.deepcopy(pconf) + end)() + + if pconf.mappings then + defaults.attach_mappings = function(_, map) + for mode, tbl in pairs(pconf.mappings) do + for key, action in pairs(tbl) do + map(mode, key, action) + end + end + return true + end + end + + if pconf.attach_mappings and opts.attach_mappings then + local opts_attach = opts.attach_mappings + opts.attach_mappings = function(prompt_bufnr, map) + pconf.attach_mappings(prompt_bufnr, map) + return opts_attach(prompt_bufnr, map) + end + end + + v(vim.tbl_extend("force", defaults, opts)) + end + end + + return mod +end + +-- We can't do this in one statement because tree-sitter-lua docgen gets confused if we do +builtin = apply_config(builtin) +return builtin diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/command.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/command.lua new file mode 100644 index 0000000..051d955 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/command.lua @@ -0,0 +1,256 @@ +---@tag telescope.command +---@config { ["module"] = "telescope.command" } + +---@brief [[ +--- +--- Telescope commands can be called through two apis, +--- the lua api and the viml api. +--- +--- The lua api is the more direct way to interact with Telescope, as you directly call the +--- lua functions that Telescope defines. +--- It can be called in a lua file using commands like: +---
+--- `require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})`
+--- 
+--- If you want to use this api from a vim file you should prepend `lua` to the command, as below: +---
+--- `lua require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})`
+--- 
+--- If you want to use this api from a neovim command line you should prepend `:lua` to +--- the command, as below: +---
+--- `:lua require("telescope.builtin").find_files({hidden=true, layout_config={prompt_position="top"}})`
+--- 
+--- +--- The viml api is more indirect, as first the command must be parsed to the relevant lua +--- equivalent, which brings some limitations. +--- The viml api can be called using commands like: +---
+--- `:Telescope find_files hidden=true layout_config={"prompt_position":"top"}`
+--- 
+--- This involves setting options using an `=` and using viml syntax for lists and +--- dictionaries when the corresponding lua function requires a table. +--- +--- One limitation of the viml api is that there can be no spaces in any of the options. +--- For example, if you want to use the `cwd` option for `find_files` to specify that you +--- only want to search within the folder `/foo bar/subfolder/` you could not do that using the +--- viml api, as the path name contains a space. +--- Similarly, you could NOT set the `prompt_position` to `"top"` using the following command: +---
+--- `:Telescope find_files layout_config={ "prompt_position" : "top" }`
+--- 
+--- as there are spaces in the option. +--- +---@brief ]] +local themes = require "telescope.themes" +local builtin = require "telescope.builtin" +local extensions = require("telescope._extensions").manager +local config = require "telescope.config" +local utils = require "telescope.utils" +local command = {} + +local arg_value = { + ["nil"] = nil, + ['""'] = "", + ['"'] = "", +} + +local bool_type = { + ["false"] = false, + ["true"] = true, +} + +local split_keywords = { + ["find_command"] = true, + ["vimgrep_arguments"] = true, + ["sections"] = true, + ["search_dirs"] = true, + ["symbols"] = true, + ["ignore_symbols"] = true, +} + +-- convert command line string arguments to +-- lua number boolean type and nil value +command.convert_user_opts = function(user_opts) + local default_opts = config.values + + local _switch = { + ["boolean"] = function(key, val) + if val == "false" then + user_opts[key] = false + return + end + user_opts[key] = true + end, + ["number"] = function(key, val) + user_opts[key] = tonumber(val) + end, + ["string"] = function(key, val) + if arg_value[val] ~= nil then + user_opts[key] = arg_value[val] + return + end + + if bool_type[val] ~= nil then + user_opts[key] = bool_type[val] + end + end, + ["table"] = function(key, val) + local ok, eval = pcall(vim.fn.eval, val) + if ok then + user_opts[key] = eval + else + local err + eval, err = loadstring("return " .. val) + if err ~= nil then + -- discard invalid lua expression + user_opts[key] = nil + elseif eval ~= nil then + ok, eval = pcall(eval) + if ok and type(eval) == "table" then + -- allow if return a table only + user_opts[key] = eval + else + -- otherwise return nil (allows split check later) + user_opts[key] = nil + end + end + end + end, + } + + local _switch_metatable = { + __index = function(_, k) + utils.notify("command", { + msg = string.format("Type of '%s' does not match", k), + level = "WARN", + }) + end, + } + + setmetatable(_switch, _switch_metatable) + + for key, val in pairs(user_opts) do + if split_keywords[key] then + _switch["table"](key, val) + if user_opts[key] == nil then + user_opts[key] = vim.split(val, ",") + end + elseif default_opts[key] ~= nil then + _switch[type(default_opts[key])](key, val) + elseif tonumber(val) ~= nil then + _switch["number"](key, val) + else + _switch["string"](key, val) + end + end +end + +-- receive the viml command args +-- it should be a table value like +-- { +-- cmd = 'find_files', +-- theme = 'dropdown', +-- extension_type = 'command' +-- opts = { +-- cwd = '***', +-- } +local function run_command(args) + local user_opts = args or {} + if next(user_opts) == nil and not user_opts.cmd then + utils.notify("command", { + msg = "Command missing arguments", + level = "ERROR", + }) + return + end + + local cmd = user_opts.cmd + local opts = user_opts.opts or {} + local extension_type = user_opts.extension_type or "" + local theme = user_opts.theme or "" + + if next(opts) ~= nil then + command.convert_user_opts(opts) + end + + if string.len(theme) > 0 then + local func = themes[theme] or themes["get_" .. theme] + opts = func(opts) + end + + if string.len(extension_type) > 0 and extension_type ~= '"' then + extensions[cmd][extension_type](opts) + return + end + + if builtin[cmd] then + builtin[cmd](opts) + return + end + + if rawget(extensions, cmd) then + extensions[cmd][cmd](opts) + return + end + + utils.notify("run_command", { + msg = "Unknown command", + level = "ERROR", + }) +end + +-- @Summary get extensions sub command +-- register extensions dap gh etc. +-- input in command line `Telescope gh ` +-- Returns a list for each extension. +function command.get_extensions_subcommand() + local exts = require("telescope._extensions").manager + local complete_ext_table = {} + for cmd, value in pairs(exts) do + if type(value) == "table" then + local subcmds = {} + for key, _ in pairs(value) do + table.insert(subcmds, key) + end + complete_ext_table[cmd] = subcmds + end + end + return complete_ext_table +end + +function command.register_keyword(keyword) + split_keywords[keyword] = true +end + +function command.load_command(cmd, ...) + local args = { ... } + if cmd == nil then + run_command { cmd = "builtin" } + return + end + + local user_opts = { + cmd = cmd, + opts = {}, + } + + for _, arg in ipairs(args) do + if arg:find("=", 1) == nil then + user_opts["extension_type"] = arg + else + local param = vim.split(arg, "=") + local key = table.remove(param, 1) + param = table.concat(param, "=") + if key == "theme" then + user_opts["theme"] = param + else + user_opts.opts[key] = param + end + end + end + + run_command(user_opts) +end + +return command diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config.lua new file mode 100644 index 0000000..8eeb60e --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config.lua @@ -0,0 +1,884 @@ +local strings = require "plenary.strings" +local deprecated = require "telescope.deprecated" +local sorters = require "telescope.sorters" +local os_sep = require("plenary.path").path.sep +local has_win = vim.fn.has "win32" == 1 + +-- Keep the values around between reloads +_TelescopeConfigurationValues = _TelescopeConfigurationValues or {} +_TelescopeConfigurationPickers = _TelescopeConfigurationPickers or {} + +local function first_non_null(...) + local n = select("#", ...) + for i = 1, n do + local value = select(i, ...) + + if value ~= nil then + return value + end + end +end + +-- A function that creates an amended copy of the `base` table, +-- by replacing keys at "level 2" that match keys in "level 1" in `priority`, +-- and then performs a deep_extend. +-- May give unexpected results if used with tables of "depth" +-- greater than 2. +local smarter_depth_2_extend = function(priority, base) + local result = {} + for key, val in pairs(base) do + if type(val) ~= "table" then + result[key] = first_non_null(priority[key], val) + else + result[key] = {} + for k, v in pairs(val) do + result[key][k] = first_non_null(priority[k], v) + end + end + end + for key, val in pairs(priority) do + if type(val) ~= "table" then + result[key] = first_non_null(val, result[key]) + else + result[key] = vim.tbl_extend("keep", val, result[key] or {}) + end + end + return result +end + +local resolve_table_opts = function(priority, base) + if priority == false or (priority == nil and base == false) then + return false + end + if priority == nil and type(base) == "table" then + return base + end + return smarter_depth_2_extend(priority, base) +end + +-- TODO: Add other major configuration points here. +-- selection_strategy + +local config = {} +config.smarter_depth_2_extend = smarter_depth_2_extend +config.resolve_table_opts = resolve_table_opts + +config.values = _TelescopeConfigurationValues +config.descriptions = {} +config.pickers = _TelescopeConfigurationPickers + +function config.set_pickers(pickers) + pickers = vim.F.if_nil(pickers, {}) + + for k, v in pairs(pickers) do + config.pickers[k] = v + end +end + +local layout_config_defaults = { + + horizontal = { + width = 0.8, + height = 0.9, + prompt_position = "bottom", + preview_cutoff = 120, + }, + + vertical = { + width = 0.8, + height = 0.9, + prompt_position = "bottom", + preview_cutoff = 40, + }, + + center = { + width = 0.5, + height = 0.4, + preview_cutoff = 40, + prompt_position = "top", + }, + + cursor = { + width = 0.8, + height = 0.9, + preview_cutoff = 40, + }, + + bottom_pane = { + height = 25, + prompt_position = "top", + preview_cutoff = 120, + }, +} + +local layout_config_description = string.format( + [[ + Determines the default configuration values for layout strategies. + See |telescope.layout| for details of the configurations options for + each strategy. + + Allows setting defaults for all strategies as top level options and + for overriding for specific options. + For example, the default values below set the default width to 80%% of + the screen width for all strategies except 'center', which has width + of 50%% of the screen width. + + Default: %s +]], + vim.inspect(layout_config_defaults, { newline = "\n ", indent = " " }) +) + +-- A table of all the usual defaults for telescope. +-- Keys will be the name of the default, +-- values will be a list where: +-- - first entry is the value +-- - second entry is the description of the option + +local telescope_defaults = {} +config.descriptions_order = {} +local append = function(name, val, doc) + telescope_defaults[name] = { val, doc } + table.insert(config.descriptions_order, name) +end + +append( + "sorting_strategy", + "descending", + [[ + Determines the direction "better" results are sorted towards. + + Available options are: + - "descending" (default) + - "ascending"]] +) + +append( + "selection_strategy", + "reset", + [[ + Determines how the cursor acts after each sort iteration. + + Available options are: + - "reset" (default) + - "follow" + - "row" + - "closest" + - "none"]] +) + +append( + "scroll_strategy", + "cycle", + [[ + Determines what happens if you try to scroll past the view of the + picker. + + Available options are: + - "cycle" (default) + - "limit"]] +) + +append( + "layout_strategy", + "horizontal", + [[ + Determines the default layout of Telescope pickers. + See |telescope.layout| for details of the available strategies. + + Default: 'horizontal']] +) + +append("layout_config", layout_config_defaults, layout_config_description) + +append( + "cycle_layout_list", + { "horizontal", "vertical" }, + [[ + Determines the layouts to cycle through when using `actions.cycle_layout_next` + and `actions.cycle_layout_prev`. + Should be a list of "layout setups". + Each "layout setup" can take one of two forms: + 1. string
+ This is interpreted as the name of a `layout_strategy` + 2. table
+ A table with possible keys `layout_strategy`, `layout_config` and `previewer` + + Default: { "horizontal", "vertical" } + ]] +) + +append( + "winblend", + 0, + [[ + Configure winblend for telescope floating windows. See |winblend| for + more information. + + Default: 0]] +) + +append( + "wrap_results", + false, + [[ + Word wrap the search results + + Default: false]] +) + +append( + "prompt_prefix", + "> ", + [[ + The character(s) that will be shown in front of Telescope's prompt. + + Default: '> ']] +) + +append( + "selection_caret", + "> ", + [[ + The character(s) that will be shown in front of the current selection. + + + Default: '> ']] +) + +append( + "entry_prefix", + " ", + [[ + Prefix in front of each result entry. Current selection not included. + + Default: ' ']] +) + +append( + "multi_icon", + "+", + [[ + Symbol to add in front of a multi-selected result entry. + Replaces final character of |telescope.defaults.selection_caret| and + |telescope.defaults.entry_prefix| as appropriate. + To have no icon, set to the empty string. + + Default: '+']] +) + +append( + "initial_mode", + "insert", + [[ + Determines in which mode telescope starts. Valid Keys: + `insert` and `normal`. + + Default: "insert"]] +) + +append( + "border", + true, + [[ + Boolean defining if borders are added to Telescope windows. + + Default: true]] +) + +append( + "path_display", + {}, + [[ + Determines how file paths are displayed + + path_display can be set to an array with a combination of: + - "hidden" hide file names + - "tail" only display the file name, and not the path + - "absolute" display absolute paths + - "smart" remove as much from the path as possible to only show + the difference between the displayed paths. + Warning: The nature of the algorithm might have a negative + performance impact! + - "shorten" only display the first character of each directory in + the path + - "truncate" truncates the start of the path when the whole path will + not fit. To increase the the gap between the path and the edge. + set truncate to number `truncate = 3` + + You can also specify the number of characters of each directory name + to keep by setting `path_display.shorten = num`. + e.g. for a path like + `alpha/beta/gamma/delta.txt` + setting `path_display.shorten = 1` will give a path like: + `a/b/g/delta.txt` + Similarly, `path_display.shorten = 2` will give a path like: + `al/be/ga/delta.txt` + + You can also further customise the shortening behaviour by + setting `path_display.shorten = { len = num, exclude = list }`, + where `len` acts as above, and `exclude` is a list of positions + that are not shortened. Negative numbers in the list are considered + relative to the end of the path. + e.g. for a path like + `alpha/beta/gamma/delta.txt` + setting `path_display.shorten = { len = 1, exclude = {1, -1} }` + will give a path like: + `alpha/b/g/delta.txt` + setting `path_display.shorten = { len = 2, exclude = {2, -2} }` + will give a path like: + `al/beta/gamma/de` + + path_display can also be set to 'hidden' string to hide file names + + path_display can also be set to a function for custom formatting of + the path display. Example: + + -- Format path as "file.txt (path\to\file\)" + path_display = function(opts, path) + local tail = require("telescope.utils").path_tail(path) + return string.format("%s (%s)", tail, path) + end, + + Default: {}]] +) + +append( + "borderchars", + { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + [[ + Set the borderchars of telescope floating windows. It has to be a + table of 8 string values. + + Default: { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }]] +) + +append( + "get_status_text", + function(self) + local ww = #(self:get_multi_selection()) + local xx = (self.stats.processed or 0) - (self.stats.filtered or 0) + local yy = self.stats.processed or 0 + if xx == 0 and yy == 0 then + return "" + end + + -- local status_icon + -- if opts.completed then + -- status_icon = "✔️" + -- else + -- status_icon = "*" + -- end + if ww == 0 then + return string.format("%s / %s", xx, yy) + else + return string.format("%s / %s / %s", ww, xx, yy) + end + end, + [[ + A function that determines what the virtual text looks like. + Signature: function(picker) -> str + + Default: function that shows current count / all]] +) + +append( + "hl_result_eol", + true, + [[ + Changes if the highlight for the selected item in the results + window is always the full width of the window + + Default: true]] +) + +append( + "dynamic_preview_title", + false, + [[ + Will change the title of the preview window dynamically, where it + is supported. For example, the preview window's title could show up as + the full filename. + + Default: false]] +) + +append( + "results_title", + "Results", + [[ + Defines the default title of the results window. A false value + can be used to hide the title altogether. + + Default: "Results"]] +) + +append( + "prompt_title", + "Prompt", + [[ + Defines the default title of the prompt window. A false value + can be used to hide the title altogether. Most of the times builtins + define a prompt_title which will be prefered over this default. + + Default: "Prompt"]] +) + +append( + "mappings", + {}, + [[ + Your mappings to override telescope's default mappings. + + See: ~ + |telescope.mappings| + ]] +) + +append( + "default_mappings", + nil, + [[ + Not recommended to use except for advanced users. + + Will allow you to completely remove all of telescope's default maps + and use your own. + ]] +) + +append( + "history", + { + path = vim.fn.stdpath "data" .. os_sep .. "telescope_history", + limit = 100, + handler = function(...) + return require("telescope.actions.history").get_simple_history(...) + end, + }, + [[ + This field handles the configuration for prompt history. + By default it is a table, with default values (more below). + To disable history, set it to false. + + Currently mappings still need to be added, Example: + mappings = { + i = { + [""] = require('telescope.actions').cycle_history_next, + [""] = require('telescope.actions').cycle_history_prev, + }, + }, + + Fields: + - path: The path to the telescope history as string. + default: stdpath("data")/telescope_history + - limit: The amount of entries that will be written in the + history. + Warning: If limit is set to nil it will grow unbound. + default: 100 + - handler: A lua function that implements the history. + This is meant as a developer setting for extensions to + override the history handling, e.g., + https://github.com/nvim-telescope/telescope-smart-history.nvim, + which allows context sensitive (cwd + picker) history. + + Default: + require('telescope.actions.history').get_simple_history]] +) + +append( + "cache_picker", + { + num_pickers = 1, + limit_entries = 1000, + }, + [[ + This field handles the configuration for picker caching. + By default it is a table, with default values (more below). + To disable caching, set it to false. + + Caching preserves all previous multi selections and results and + therefore may result in slowdown or increased RAM occupation + if too many pickers (`cache_picker.num_pickers`) or entries + ('cache_picker.limit_entries`) are cached. + + Fields: + - num_pickers: The number of pickers to be cached. + Set to -1 to preserve all pickers of your session. + If passed to a picker, the cached pickers with + indices larger than `cache_picker.num_pickers` will + be cleared. + Default: 1 + - limit_entries: The amount of entries that will be written in the + Default: 1000 + ]] +) + +append( + "preview", + { + check_mime_type = not has_win, + filesize_limit = 25, + timeout = 250, + treesitter = true, + msg_bg_fillchar = "╱", + hide_on_startup = false, + }, + [[ + This field handles the global configuration for previewers. + By default it is a table, with default values (more below). + To disable previewing, set it to false. If you have disabled previewers + globally, but want to opt in to previewing for single pickers, you will have to + pass `preview = true` or `preview = {...}` (your config) to the `opts` of + your picker. + + Fields: + - check_mime_type: Use `file` if available to try to infer whether the + file to preview is a binary if plenary's + filetype detection fails. + Windows users get `file` from: + https://github.com/julian-r/file-windows + Set to false to attempt to preview any mime type. + Default: true for all OS excl. Windows + - filesize_limit: The maximum file size in MB attempted to be previewed. + Set to false to attempt to preview any file size. + Default: 25 + - timeout: Timeout the previewer if the preview did not + complete within `timeout` milliseconds. + Set to false to not timeout preview. + Default: 250 + - hook(s): Function(s) that takes `(filepath, bufnr, opts)`, where opts + exposes winid and ft (filetype). + Available hooks (in order of priority): + {filetype, mime, filesize, timeout}_hook + Important: the filetype_hook must return true or false + to indicate whether to continue (true) previewing or not (false), + respectively. + Two examples: + local putils = require("telescope.previewers.utils") + ... -- preview is called in telescope.setup { ... } + preview = { + -- 1) Do not show previewer for certain files + filetype_hook = function(filepath, bufnr, opts) + -- you could analogously check opts.ft for filetypes + local excluded = vim.tbl_filter(function(ending) + return filepath:match(ending) + end, { + ".*%.csv", + ".*%.toml", + }) + if not vim.tbl_isempty(excluded) then + putils.set_preview_message( + bufnr, + opts.winid, + string.format("I don't like %s files!", + excluded[1]:sub(5, -1)) + ) + return false + end + return true + end, + -- 2) Truncate lines to preview window for too large files + filesize_hook = function(filepath, bufnr, opts) + local path = require("plenary.path"):new(filepath) + -- opts exposes winid + local height = vim.api.nvim_win_get_height(opts.winid) + local lines = vim.split(path:head(height), "[\r]?\n") + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + end, + } + The configuration recipes for relevant examples. + Note: if plenary does not recognize your filetype yet -- + 1) Please consider contributing to: + $PLENARY_REPO/data/plenary/filetypes/builtin.lua + 2) Register your filetype locally as per link + https://github.com/nvim-lua/plenary.nvim#plenaryfiletype + Default: nil + - treesitter: Determines whether the previewer performs treesitter + highlighting, which falls back to regex-based highlighting. + `true`: treesitter highlighting for all available filetypes + `false`: regex-based highlighting for all filetypes + `table`: following nvim-treesitters highlighting options: + It contains two keys: + - enable boolean|table: if boolean, enable all ts + highlighing with that flag, + disable still considered. + Containing a list of filetypes, + that are enabled, disabled + ignored because it doesnt make + any sense in this case. + - disable table: containing a list of filetypes + that are disabled + Default: true + - msg_bg_fillchar: Character to fill background of unpreviewable buffers with + Default: "╱" + - hide_on_startup: Hide previewer when picker starts. Previewer can be toggled + with actions.toggle_preview. + Default: false + ]] +) + +append( + "vimgrep_arguments", + { "rg", "--color=never", "--no-heading", "--with-filename", "--line-number", "--column", "--smart-case" }, + [[ + Defines the command that will be used for `live_grep` and `grep_string` + pickers. + Hint: Make sure that color is currently set to `never` because we do + not yet interpret color codes + Hint 2: Make sure that these options are in your changes arguments: + "--no-heading", "--with-filename", "--line-number", "--column" + because we need them so the ripgrep output is in the correct format. + + Default: { + "rg", + "--color=never", + "--no-heading", + "--with-filename", + "--line-number", + "--column", + "--smart-case" + }]] +) + +append( + "use_less", + true, + [[ + Boolean if less should be enabled in term_previewer (deprecated and + currently no longer used in the builtin pickers). + + Default: true]] +) + +append( + "set_env", + nil, + [[ + Set an environment for term_previewer. A table of key values: + Example: { COLORTERM = "truecolor", ... } + Hint: Empty table is not allowed. + + Default: nil]] +) + +append( + "color_devicons", + true, + [[ + Boolean if devicons should be enabled or not. If set to false, the + text highlight group is used. + Hint: Coloring only works if |termguicolors| is enabled. + + Default: true]] +) + +append( + "file_sorter", + sorters.get_fzy_sorter, + [[ + A function pointer that specifies the file_sorter. This sorter will + be used for find_files, git_files and similar. + Hint: If you load a native sorter, you dont need to change this value, + the native sorter will override it anyway. + + Default: require("telescope.sorters").get_fzy_sorter]] +) + +append( + "generic_sorter", + sorters.get_fzy_sorter, + [[ + A function pointer to the generic sorter. The sorter that should be + used for everything that is not a file. + Hint: If you load a native sorter, you dont need to change this value, + the native sorter will override it anyway. + + Default: require("telescope.sorters").get_fzy_sorter]] +) + +--TODO(conni2461): Why is this even configurable??? +append( + "prefilter_sorter", + sorters.prefilter, + [[ + This points to a wrapper sorter around the generic_sorter that is able + to do prefiltering. + Its usually used for lsp_*_symbols and lsp_*_diagnostics + + Default: require("telescope.sorters").prefilter]] +) + +append( + "tiebreak", + function(current_entry, existing_entry, _) + return #current_entry.ordinal < #existing_entry.ordinal + end, + [[ + A function that determines how to break a tie when two entries have + the same score. + Having a function that always returns false would keep the entries in + the order they are found, so existing_entry before current_entry. + Vice versa always returning true would place the current_entry + before the existing_entry. + + Signature: function(current_entry, existing_entry, prompt) -> boolean + + Default: function that breaks the tie based on the length of the + entry's ordinal]] +) + +append( + "file_ignore_patterns", + nil, + [[ + A table of lua regex that define the files that should be ignored. + Example: { "^scratch/" } -- ignore all files in scratch directory + Example: { "%.npz" } -- ignore all npz files + See: https://www.lua.org/manual/5.1/manual.html#5.4.1 for more + information about lua regex + Note: `file_ignore_patterns` will be used in all pickers that have a + file associated. This might lead to the problem that lsp_ pickers + aren't displaying results because they might be ignored by + `file_ignore_patterns`. For example, setting up node_modules as ignored + will never show node_modules in any results, even if you are + interested in lsp_ results. + + If you only want `file_ignore_patterns` for `find_files` and + `grep_string`/`live_grep` it is suggested that you setup `gitignore` + and have fd and or ripgrep installed because both tools will not show + `gitignore`d files on default. + + Default: nil]] +) + +append( + "get_selection_window", + function() + return 0 + end, + [[ + Function that takes function(picker, entry) and returns a window id. + The window ID will be used to decide what window the chosen file will + be opened in and the cursor placed in upon leaving the picker. + + Default: `function() return 0 end` + ]] +) + +append( + "file_previewer", + function(...) + return require("telescope.previewers").vim_buffer_cat.new(...) + end, + [[ + Function pointer to the default file_previewer. It is mostly used + for find_files, git_files and similar. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").cat.new + + Default: require("telescope.previewers").vim_buffer_cat.new]] +) + +append( + "grep_previewer", + function(...) + return require("telescope.previewers").vim_buffer_vimgrep.new(...) + end, + [[ + Function pointer to the default vim_grep previewer. It is mostly + used for live_grep, grep_string and similar. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").vimgrep.new + + Default: require("telescope.previewers").vim_buffer_vimgrep.new]] +) + +append( + "qflist_previewer", + function(...) + return require("telescope.previewers").vim_buffer_qflist.new(...) + end, + [[ + Function pointer to the default qflist previewer. It is mostly + used for qflist, loclist and lsp. + You can change this function pointer to either use your own + previewer or use the command-line program bat as the previewer: + require("telescope.previewers").qflist.new + + Default: require("telescope.previewers").vim_buffer_qflist.new]] +) + +append( + "buffer_previewer_maker", + function(...) + return require("telescope.previewers").buffer_previewer_maker(...) + end, + [[ + Developer option that defines the underlining functionality + of the buffer previewer. + For interesting configuration examples take a look at + https://github.com/nvim-telescope/telescope.nvim/wiki/Configuration-Recipes + + Default: require("telescope.previewers").buffer_previewer_maker]] +) + +-- @param user_defaults table: a table where keys are the names of options, +-- and values are the ones the user wants +-- @param tele_defaults table: (optional) a table containing all of the defaults +-- for telescope [defaults to `telescope_defaults`] +function config.set_defaults(user_defaults, tele_defaults) + user_defaults = vim.F.if_nil(user_defaults, {}) + tele_defaults = vim.F.if_nil(tele_defaults, telescope_defaults) + + -- Check if using layout keywords outside of `layout_config` + deprecated.options(user_defaults) + + local function get(name, default_val) + if name == "layout_config" then + return smarter_depth_2_extend( + vim.F.if_nil(user_defaults[name], {}), + vim.tbl_deep_extend("keep", vim.F.if_nil(config.values[name], {}), vim.F.if_nil(default_val, {})) + ) + end + if name == "history" or name == "cache_picker" or name == "preview" then + if user_defaults[name] == false or config.values[name] == false then + return false + end + if user_defaults[name] == true then + return vim.F.if_nil(config.values[name], {}) + end + + return smarter_depth_2_extend( + vim.F.if_nil(user_defaults[name], {}), + vim.tbl_deep_extend("keep", vim.F.if_nil(config.values[name], {}), vim.F.if_nil(default_val, {})) + ) + end + return first_non_null(user_defaults[name], config.values[name], default_val) + end + + local function set(name, default_val, description) + assert(description, "Config values must always have a description") + + config.values[name] = get(name, default_val) + config.descriptions[name] = strings.dedent(description) + end + + for key, info in pairs(tele_defaults) do + set(key, info[1], info[2]) + end + + local M = {} + M.get = get + return M +end + +function config.clear_defaults() + for k, _ in pairs(config.values) do + config.values[k] = nil + end +end + +config.set_defaults() + +return config diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config/resolve.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config/resolve.lua new file mode 100644 index 0000000..9b951d3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/config/resolve.lua @@ -0,0 +1,317 @@ +---@tag telescope.resolve +---@config { ["module"] = "telescope.resolve" } + +---@brief [[ +--- Provides "resolver functions" to allow more customisable inputs for options. +---@brief ]] + +--[[ + +Ultimately boils down to getting `height` and `width` for: +- prompt +- preview +- results + +No matter what you do, I will not make prompt have more than one line (atm) + +Result of `resolve` should be a table with: + +{ + preview = { + get_width = function(self, max_columns, max_lines) end + get_height = function(self, max_columns, max_lines) end + }, + + result = { + get_width = function(self, max_columns, max_lines) end + get_height = function(self, max_columns, max_lines) end + }, + + prompt = { + get_width = function(self, max_columns, max_lines) end + get_height = function(self, max_columns, max_lines) end + }, + + total ? +} + +!!NOT IMPLEMENTED YET!! + +height = + 1. 0 <= number < 1 + This means total height as a percentage + + 2. 1 <= number + This means total height as a fixed number + + 3. function(picker, columns, lines) + -> returns one of the above options + return math.min(110, max_rows * .5) + + if columns > 120 then + return 110 + else + return 0.6 + end + + 3. { + previewer = x, + results = x, + prompt = x, + }, this means I do my best guess I can for these, given your options + +width = + exactly the same, but switch to width + + +{ + height = 0.5, + width = { + previewer = 0.25, + results = 30, + } +} + +https://github.com/nvim-lua/telescope.nvim/pull/43 + +After we get layout, we should try and make top-down sorting work. +That's the next step to scrolling. + +{ + vertical = { + }, + horizontal = { + }, + + height = ... + width = ... +} + + + +--]] + +local resolver = {} +local _resolve_map = {} + +local throw_invalid_config_option = function(key, value) + error(string.format("Invalid configuration option for '%s': '%s'", key, tostring(value)), 2) +end + +-- Booleans +_resolve_map[function(val) + return val == false +end] = function(_, val) + return function(...) + return val + end +end + +-- Percentages +_resolve_map[function(val) + return type(val) == "number" and val >= 0 and val < 1 +end] = function(selector, val) + return function(...) + local selected = select(selector, ...) + return math.floor(val * selected) + end +end + +-- Numbers +_resolve_map[function(val) + return type(val) == "number" and val >= 1 +end] = function(selector, val) + return function(...) + local selected = select(selector, ...) + return math.min(val, selected) + end +end + +-- function: +-- Function must have same signature as get_window_layout +-- function(self, max_columns, max_lines): number +-- +-- Resulting number is used for this configuration value. +_resolve_map[function(val) + return type(val) == "function" +end] = function(_, val) + return val +end + +_resolve_map[function(val) + return type(val) == "table" and val["max"] ~= nil and val[1] ~= nil and val[1] >= 0 and val[1] < 1 +end] = + function(selector, val) + return function(...) + local selected = select(selector, ...) + return math.min(math.floor(val[1] * selected), val["max"]) + end + end + +_resolve_map[function(val) + return type(val) == "table" and val["min"] ~= nil and val[1] ~= nil and val[1] >= 0 and val[1] < 1 +end] = + function(selector, val) + return function(...) + local selected = select(selector, ...) + return math.max(math.floor(val[1] * selected), val["min"]) + end + end + +-- Add padding option +_resolve_map[function(val) + return type(val) == "table" and val["padding"] ~= nil +end] = function(selector, val) + local resolve_pad = function(value) + for k, v in pairs(_resolve_map) do + if k(value) then + return v(selector, value) + end + end + throw_invalid_config_option("padding", value) + end + + return function(...) + local selected = select(selector, ...) + local padding = resolve_pad(val["padding"]) + return math.floor(selected - 2 * padding(...)) + end +end + +--- Converts input to a function that returns the height. +--- The input must take one of five forms: +--- 1. 0 <= number < 1
+--- This means total height as a percentage. +--- 2. 1 <= number
+--- This means total height as a fixed number. +--- 3. function
+--- Must have signature: +--- function(self, max_columns, max_lines): number +--- 4. table of the form: { val, max = ..., min = ... }
+--- val has to be in the first form 0 <= val < 1 and only one is given, +--- `min` or `max` as fixed number +--- 5. table of the form: {padding = `foo`}
+--- where `foo` has one of the previous three forms.
+--- The height is then set to be the remaining space after padding. +--- For example, if the window has height 50, and the input is {padding = 5}, +--- the height returned will be `40 = 50 - 2*5` +--- +--- The returned function will have signature: +--- function(self, max_columns, max_lines): number +resolver.resolve_height = function(val) + for k, v in pairs(_resolve_map) do + if k(val) then + return v(3, val) + end + end + throw_invalid_config_option("height", val) +end + +--- Converts input to a function that returns the width. +--- The input must take one of five forms: +--- 1. 0 <= number < 1
+--- This means total width as a percentage. +--- 2. 1 <= number
+--- This means total width as a fixed number. +--- 3. function
+--- Must have signature: +--- function(self, max_columns, max_lines): number +--- 4. table of the form: { val, max = ..., min = ... }
+--- val has to be in the first form 0 <= val < 1 and only one is given, +--- `min` or `max` as fixed number +--- 5. table of the form: {padding = `foo`}
+--- where `foo` has one of the previous three forms.
+--- The width is then set to be the remaining space after padding. +--- For example, if the window has width 100, and the input is {padding = 5}, +--- the width returned will be `90 = 100 - 2*5` +--- +--- The returned function will have signature: +--- function(self, max_columns, max_lines): number +resolver.resolve_width = function(val) + for k, v in pairs(_resolve_map) do + if k(val) then + return v(2, val) + end + end + + throw_invalid_config_option("width", val) +end + +--- Calculates the adjustment required to move the picker from the middle of the screen to +--- an edge or corner.
+--- The `anchor` can be any of the following strings: +--- - "", "CENTER", "NW", "N", "NE", "E", "SE", "S", "SW", "W" +--- The anchors have the following meanings: +--- - "" or "CENTER":
+--- the picker will remain in the middle of the screen. +--- - Compass directions:
+--- the picker will move to the corresponding edge/corner +--- e.g. "NW" -> "top left corner", "E" -> "right edge", "S" -> "bottom edge" +resolver.resolve_anchor_pos = function(anchor, p_width, p_height, max_columns, max_lines) + anchor = anchor:upper() + local pos = { 0, 0 } + if anchor == "CENTER" then + return pos + end + if anchor:find "W" then + pos[1] = math.ceil((p_width - max_columns) / 2) + 1 + elseif anchor:find "E" then + pos[1] = math.ceil((max_columns - p_width) / 2) - 1 + end + if anchor:find "N" then + pos[2] = math.ceil((p_height - max_lines) / 2) + 1 + elseif anchor:find "S" then + pos[2] = math.ceil((max_lines - p_height) / 2) - 1 + end + return pos +end + +-- Win option always returns a table with preview, results, and prompt. +-- It handles many different ways. Some examples are as follows: +-- +-- -- Disable +-- borderchars = false +-- +-- -- All three windows share the same +-- borderchars = { '─', '│', '─', '│', '┌', '┐', '┘', '└'}, +-- +-- -- Each window gets it's own configuration +-- borderchars = { +-- preview = {...}, +-- results = {...}, +-- prompt = {...}, +-- } +-- +-- -- Default to [1] but override with specific items +-- borderchars = { +-- {...} +-- prompt = {...}, +-- } +resolver.win_option = function(val, default) + if type(val) ~= "table" or vim.tbl_islist(val) then + if val == nil then + val = default + end + + return { + preview = val, + results = val, + prompt = val, + } + elseif type(val) == "table" then + assert(not vim.tbl_islist(val)) + + local val_to_set = val[1] + if val_to_set == nil then + val_to_set = default + end + + return { + preview = vim.F.if_nil(val.preview, val_to_set), + results = vim.F.if_nil(val.results, val_to_set), + prompt = vim.F.if_nil(val.prompt, val_to_set), + } + end +end + +return resolver diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/debounce.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/debounce.lua new file mode 100644 index 0000000..6e230b5 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/debounce.lua @@ -0,0 +1,179 @@ +-- Credit: https://gist.github.com/runiq/31aa5c4bf00f8e0843cd267880117201 +-- + +local M = {} + +---Validates args for `throttle()` and `debounce()`. +local function td_validate(fn, ms) + vim.validate { + fn = { fn, "f" }, + ms = { + ms, + function(v) + return type(v) == "number" and v > 0 + end, + "number > 0", + }, + } +end + +--- Throttles a function on the leading edge. Automatically `schedule_wrap()`s. +--- +--@param fn (function) Function to throttle +--@param timeout (number) Timeout in ms +--@returns (function, timer) throttled function and timer. Remember to call +---`timer:close()` at the end or you will leak memory! +function M.throttle_leading(fn, ms) + td_validate(fn, ms) + local timer = vim.loop.new_timer() + local running = false + + local function wrapped_fn(...) + if not running then + timer:start(ms, 0, function() + running = false + end) + running = true + pcall(vim.schedule_wrap(fn), select(1, ...)) + end + end + return wrapped_fn, timer +end + +--- Throttles a function on the trailing edge. Automatically +--- `schedule_wrap()`s. +--- +--@param fn (function) Function to throttle +--@param timeout (number) Timeout in ms +--@param last (boolean, optional) Whether to use the arguments of the last +---call to `fn` within the timeframe. Default: Use arguments of the first call. +--@returns (function, timer) Throttled function and timer. Remember to call +---`timer:close()` at the end or you will leak memory! +function M.throttle_trailing(fn, ms, last) + td_validate(fn, ms) + local timer = vim.loop.new_timer() + local running = false + + local wrapped_fn + if not last then + function wrapped_fn(...) + if not running then + local argv = { ... } + local argc = select("#", ...) + + timer:start(ms, 0, function() + running = false + pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) + end) + running = true + end + end + else + local argv, argc + function wrapped_fn(...) + argv = { ... } + argc = select("#", ...) + + if not running then + timer:start(ms, 0, function() + running = false + pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) + end) + running = true + end + end + end + return wrapped_fn, timer +end + +--- Debounces a function on the leading edge. Automatically `schedule_wrap()`s. +--- +--@param fn (function) Function to debounce +--@param timeout (number) Timeout in ms +--@returns (function, timer) Debounced function and timer. Remember to call +---`timer:close()` at the end or you will leak memory! +function M.debounce_leading(fn, ms) + td_validate(fn, ms) + local timer = vim.loop.new_timer() + local running = false + + local function wrapped_fn(...) + timer:start(ms, 0, function() + running = false + end) + + if not running then + running = true + pcall(vim.schedule_wrap(fn), select(1, ...)) + end + end + return wrapped_fn, timer +end + +--- Debounces a function on the trailing edge. Automatically +--- `schedule_wrap()`s. +--- +--@param fn (function) Function to debounce +--@param timeout (number) Timeout in ms +--@param first (boolean, optional) Whether to use the arguments of the first +---call to `fn` within the timeframe. Default: Use arguments of the last call. +--@returns (function, timer) Debounced function and timer. Remember to call +---`timer:close()` at the end or you will leak memory! +function M.debounce_trailing(fn, ms, first) + td_validate(fn, ms) + local timer = vim.loop.new_timer() + local wrapped_fn + + if not first then + function wrapped_fn(...) + local argv = { ... } + local argc = select("#", ...) + + timer:start(ms, 0, function() + pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) + end) + end + else + local argv, argc + function wrapped_fn(...) + argv = argv or { ... } + argc = argc or select("#", ...) + + timer:start(ms, 0, function() + pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) + end) + end + end + return wrapped_fn, timer +end + +--- Test deferment methods (`{throttle,debounce}_{leading,trailing}()`). +--- +--@param bouncer (string) Bouncer function to test +--@param ms (number, optional) Timeout in ms, default 2000. +--@param firstlast (bool, optional) Whether to use the 'other' fn call +---strategy. +function M.test_defer(bouncer, ms, firstlast) + local bouncers = { + tl = M.throttle_leading, + tt = M.throttle_trailing, + dl = M.debounce_leading, + dt = M.debounce_trailing, + } + + local timeout = ms or 2000 + + local bounced = bouncers[bouncer](function(i) + vim.cmd('echom "' .. bouncer .. ": " .. i .. '"') + end, timeout, firstlast) + + for i, _ in ipairs { 1, 2, 3, 4, 5 } do + bounced(i) + vim.schedule(function() + vim.cmd("echom " .. i) + end) + vim.fn.call("wait", { 1000, "v:false" }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/deprecated.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/deprecated.lua new file mode 100644 index 0000000..b1cbf2d --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/deprecated.lua @@ -0,0 +1,12 @@ +local deprecated = {} + +deprecated.options = function(opts) + local messages = {} + + if #messages > 0 then + table.insert(messages, 1, "Deprecated options. Please see ':help telescope.changelog'") + vim.api.nvim_err_write(table.concat(messages, "\n \n ") .. "\n \nPress to continue\n") + end +end + +return deprecated diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/entry_manager.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/entry_manager.lua new file mode 100644 index 0000000..a8331e4 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/entry_manager.lua @@ -0,0 +1,168 @@ +local log = require "telescope.log" + +local LinkedList = require "telescope.algos.linked_list" + +local EntryManager = {} +EntryManager.__index = EntryManager + +function EntryManager:new(max_results, set_entry, info) + log.trace "Creating entry_manager..." + + info = info or {} + info.looped = 0 + info.inserted = 0 + info.find_loop = 0 + + -- state contains list of + -- { entry, score } + -- Stored directly in a table, accessed as [1], [2] + set_entry = set_entry or function() end + + return setmetatable({ + linked_states = LinkedList:new { track_at = max_results }, + info = info, + max_results = max_results, + set_entry = set_entry, + worst_acceptable_score = math.huge, + }, self) +end + +function EntryManager:num_results() + return self.linked_states.size +end + +function EntryManager:get_container(index) + local count = 0 + for val in self.linked_states:iter() do + count = count + 1 + + if count == index then + return val + end + end + + return {} +end + +function EntryManager:get_entry(index) + return self:get_container(index)[1] +end + +function EntryManager:get_score(index) + return self:get_container(index)[2] +end + +function EntryManager:get_ordinal(index) + return self:get_entry(index).ordinal +end + +function EntryManager:find_entry(entry) + local info = self.info + + local count = 0 + for container in self.linked_states:iter() do + count = count + 1 + + if container[1] == entry then + info.find_loop = info.find_loop + count + + return count + end + end + + info.find_loop = info.find_loop + count + return nil +end + +function EntryManager:_update_score_from_tracked() + local linked = self.linked_states + + if linked.tracked then + self.worst_acceptable_score = math.min(self.worst_acceptable_score, linked.tracked[2]) + end +end + +function EntryManager:_insert_container_before(picker, index, linked_node, new_container) + self.linked_states:place_before(index, linked_node, new_container) + self.set_entry(picker, index, new_container[1], new_container[2], true) + + self:_update_score_from_tracked() +end + +function EntryManager:_insert_container_after(picker, index, linked_node, new_container) + self.linked_states:place_after(index, linked_node, new_container) + self.set_entry(picker, index, new_container[1], new_container[2], true) + + self:_update_score_from_tracked() +end + +function EntryManager:_append_container(picker, new_container, should_update) + self.linked_states:append(new_container) + self.worst_acceptable_score = math.min(self.worst_acceptable_score, new_container[2]) + + if should_update then + self.set_entry(picker, self.linked_states.size, new_container[1], new_container[2]) + end +end + +function EntryManager:add_entry(picker, score, entry, prompt) + score = score or 0 + + local max_res = self.max_results + local worst_score = self.worst_acceptable_score + local size = self.linked_states.size + + local info = self.info + info.maxed = info.maxed or 0 + + local new_container = { entry, score } + + -- Short circuit for bad scores -- they never need to be displayed. + -- Just save them and we'll deal with them later. + if score >= worst_score then + return self.linked_states:append(new_container) + end + + -- Short circuit for first entry. + if size == 0 then + self.linked_states:prepend(new_container) + self.set_entry(picker, 1, entry, score) + return + end + + for index, container, node in self.linked_states:ipairs() do + info.looped = info.looped + 1 + + if container[2] > score then + return self:_insert_container_before(picker, index, node, new_container) + end + + if score < 1 and container[2] == score and picker.tiebreak(entry, container[1], prompt) then + return self:_insert_container_before(picker, index, node, new_container) + end + + -- Don't add results that are too bad. + if index >= max_res then + info.maxed = info.maxed + 1 + return self:_append_container(picker, new_container, false) + end + end + + if self.linked_states.size >= max_res then + self.worst_acceptable_score = math.min(self.worst_acceptable_score, score) + end + + return self:_insert_container_after(picker, size + 1, self.linked_states.tail, new_container) +end + +function EntryManager:iter() + local iterator = self.linked_states:iter() + return function() + local val = iterator() + if val then + return val[1] + end + end +end + +return EntryManager diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders.lua new file mode 100644 index 0000000..fe1af96 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders.lua @@ -0,0 +1,211 @@ +local Job = require "plenary.job" + +local make_entry = require "telescope.make_entry" +local log = require "telescope.log" + +local async_static_finder = require "telescope.finders.async_static_finder" +local async_oneshot_finder = require "telescope.finders.async_oneshot_finder" +local async_job_finder = require "telescope.finders.async_job_finder" + +local finders = {} + +local _callable_obj = function() + local obj = {} + + obj.__index = obj + obj.__call = function(t, ...) + return t:_find(...) + end + + obj.close = function() end + + return obj +end + +--[[ ============================================================= + + JobFinder + +Uses an external Job to get results. Processes results as they arrive. + +For more information about how Jobs are implemented, checkout 'plenary.job' + +-- ============================================================= ]] +local JobFinder = _callable_obj() + +--- Create a new finder command +--- +---@param opts table Keys: +-- fn_command function The function to call +function JobFinder:new(opts) + opts = opts or {} + + assert(not opts.results, "`results` should be used with finder.new_table") + assert(not opts.static, "`static` should be used with finder.new_oneshot_job") + + local obj = setmetatable({ + entry_maker = opts.entry_maker or make_entry.gen_from_string(opts), + fn_command = opts.fn_command, + cwd = opts.cwd, + writer = opts.writer, + + -- Maximum number of results to process. + -- Particularly useful for live updating large queries. + maximum_results = opts.maximum_results, + }, self) + + return obj +end + +function JobFinder:_find(prompt, process_result, process_complete) + log.trace "Finding..." + + if self.job and not self.job.is_shutdown then + log.debug "Shutting down old job" + self.job:shutdown() + end + + local on_output = function(_, line, _) + if not line or line == "" then + return + end + + if self.entry_maker then + line = self.entry_maker(line) + end + + process_result(line) + end + + local opts = self:fn_command(prompt) + if not opts then + return + end + + local writer = nil + if opts.writer and Job.is_job(opts.writer) then + writer = opts.writer + elseif opts.writer then + writer = Job:new(opts.writer) + end + + self.job = Job:new { + command = opts.command, + args = opts.args, + cwd = opts.cwd or self.cwd, + + maximum_results = self.maximum_results, + + writer = writer, + + enable_recording = false, + + on_stdout = on_output, + -- on_stderr = on_output, + + on_exit = function() + process_complete() + end, + } + + self.job:start() +end + +local DynamicFinder = _callable_obj() + +function DynamicFinder:new(opts) + opts = opts or {} + + assert(not opts.results, "`results` should be used with finder.new_table") + assert(not opts.static, "`static` should be used with finder.new_oneshot_job") + + local obj = setmetatable({ + curr_buf = opts.curr_buf, + fn = opts.fn, + entry_maker = opts.entry_maker or make_entry.gen_from_string(opts), + }, self) + + return obj +end + +function DynamicFinder:_find(prompt, process_result, process_complete) + local results = self.fn(prompt) + + for _, result in ipairs(results) do + if process_result(self.entry_maker(result)) then + return + end + end + + process_complete() +end + +--- Return a new Finder +-- +-- Use at your own risk. +-- This opts dictionary is likely to change, but you are welcome to use it right now. +-- I will try not to change it needlessly, but I will change it sometimes and I won't feel bad. +finders._new = function(opts) + assert(not opts.results, "finder.new is deprecated with `results`. You should use `finder.new_table`") + return JobFinder:new(opts) +end + +finders.new_async_job = function(opts) + if opts.writer then + return finders._new(opts) + end + + return async_job_finder(opts) +end + +finders.new_job = function(command_generator, entry_maker, _, cwd) + return async_job_finder { + command_generator = command_generator, + entry_maker = entry_maker, + cwd = cwd, + } +end + +--- One shot job +---@param command_list string[]: Command list to execute. +---@param opts table: stuff +-- @key entry_maker function Optional: function(line: string) => table +-- @key cwd string +finders.new_oneshot_job = function(command_list, opts) + opts = opts or {} + + assert(not opts.results, "`results` should be used with finder.new_table") + + command_list = vim.deepcopy(command_list) + local command = table.remove(command_list, 1) + + return async_oneshot_finder { + entry_maker = opts.entry_maker or make_entry.gen_from_string(opts), + + cwd = opts.cwd, + maximum_results = opts.maximum_results, + + fn_command = function() + return { + command = command, + args = command_list, + } + end, + } +end + +--- Used to create a finder for a Lua table. +-- If you only pass a table of results, then it will use that as the entries. +-- +-- If you pass a table, and then a function, it's used as: +-- results table, the results to run on +-- entry_maker function, the function to convert results to entries. +finders.new_table = function(t) + return async_static_finder(t) +end + +finders.new_dynamic = function(t) + return DynamicFinder:new(t) +end + +return finders diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_job_finder.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_job_finder.lua new file mode 100644 index 0000000..20af604 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_job_finder.lua @@ -0,0 +1,77 @@ +local async_job = require "telescope._" +local LinesPipe = require("telescope._").LinesPipe + +local make_entry = require "telescope.make_entry" +local log = require "telescope.log" + +return function(opts) + log.trace("Creating async_job:", opts) + local entry_maker = opts.entry_maker or make_entry.gen_from_string(opts) + + local fn_command = function(prompt) + local command_list = opts.command_generator(prompt) + if command_list == nil then + return nil + end + + local command = table.remove(command_list, 1) + + local res = { + command = command, + args = command_list, + } + + return res + end + + local job + + local callable = function(_, prompt, process_result, process_complete) + if job then + job:close(true) + end + + local job_opts = fn_command(prompt) + if not job_opts then + return + end + + local writer = nil + -- if job_opts.writer and Job.is_job(job_opts.writer) then + -- writer = job_opts.writer + if opts.writer then + error "async_job_finder.writer is not yet implemented" + writer = async_job.writer(opts.writer) + end + + local stdout = LinesPipe() + + job = async_job.spawn { + command = job_opts.command, + args = job_opts.args, + cwd = job_opts.cwd or opts.cwd, + env = job_opts.env or opts.env, + writer = writer, + + stdout = stdout, + } + + for line in stdout:iter(true) do + if process_result(entry_maker(line)) then + return + end + end + + process_complete() + end + + return setmetatable({ + close = function() + if job then + job:close(true) + end + end, + }, { + __call = callable, + }) +end diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_oneshot_finder.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_oneshot_finder.lua new file mode 100644 index 0000000..bafb134 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_oneshot_finder.lua @@ -0,0 +1,101 @@ +local async = require "plenary.async" +local async_job = require "telescope._" +local LinesPipe = require("telescope._").LinesPipe + +local make_entry = require "telescope.make_entry" + +local await_count = 1000 + +return function(opts) + opts = opts or {} + + local entry_maker = opts.entry_maker or make_entry.gen_from_string(opts) + local cwd = opts.cwd + local env = opts.env + local fn_command = assert(opts.fn_command, "Must pass `fn_command`") + + local results = vim.F.if_nil(opts.results, {}) + local num_results = #results + + local job_started = false + local job_completed = false + local stdout = nil + + local job + + return setmetatable({ + close = function() + if job then + job:close() + end + end, + results = results, + entry_maker = entry_maker, + }, { + __call = function(_, prompt, process_result, process_complete) + if not job_started then + local job_opts = fn_command() + + -- TODO: Handle writers. + -- local writer + -- if job_opts.writer and Job.is_job(job_opts.writer) then + -- writer = job_opts.writer + -- elseif job_opts.writer then + -- writer = Job:new(job_opts.writer) + -- end + + stdout = LinesPipe() + job = async_job.spawn { + command = job_opts.command, + args = job_opts.args, + cwd = cwd, + env = env, + + stdout = stdout, + } + + job_started = true + end + + if not job_completed then + if not vim.tbl_isempty(results) then + for _, v in ipairs(results) do + process_result(v) + end + end + for line in stdout:iter(false) do + num_results = num_results + 1 + + if num_results % await_count then + async.util.scheduler() + end + + local v = entry_maker(line) + results[num_results] = v + process_result(v) + end + + process_complete() + job_completed = true + + return + end + + local current_count = num_results + for index = 1, current_count do + -- TODO: Figure out scheduling... + if index % await_count then + async.util.scheduler() + end + + if process_result(results[index]) then + break + end + end + + if job_completed then + process_complete() + end + end, + }) +end diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_static_finder.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_static_finder.lua new file mode 100644 index 0000000..140d1c8 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/finders/async_static_finder.lua @@ -0,0 +1,44 @@ +local scheduler = require("plenary.async").util.scheduler + +local make_entry = require "telescope.make_entry" + +return function(opts) + local input_results + if vim.tbl_islist(opts) then + input_results = opts + else + input_results = opts.results + end + + local entry_maker = opts.entry_maker or make_entry.gen_from_string(opts) + + local results = {} + for k, v in ipairs(input_results) do + local entry = entry_maker(v) + + if entry then + entry.index = k + table.insert(results, entry) + end + end + + return setmetatable({ + results = results, + entry_maker = entry_maker, + close = function() end, + }, { + __call = function(_, _, process_result, process_complete) + for i, v in ipairs(results) do + if process_result(v) then + break + end + + if i % 1000 == 0 then + scheduler() + end + end + + process_complete() + end, + }) +end diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/from_entry.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/from_entry.lua new file mode 100644 index 0000000..486b57c --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/from_entry.lua @@ -0,0 +1,44 @@ +--[[ ============================================================================= + +Get metadata from entries. + +This file is still WIP, so expect some changes if you're trying to consume these APIs. + +This will provide standard mechanism for accessing information from an entry. + +--============================================================================= ]] + +local from_entry = {} + +function from_entry.path(entry, validate, escape) + escape = vim.F.if_nil(escape, true) + local path = entry.path + if path == nil then + path = entry.filename + end + if path == nil then + path = entry.value + end + if path == nil then + require("telescope.log").error(string.format("Invalid Entry: '%s'", vim.inspect(entry))) + return + end + + -- only 0 if neither filereadable nor directory + if validate then + -- We need to expand for filereadable and isdirectory + -- TODO(conni2461): we are not going to return the expanded path because + -- this would lead to cache misses in the perviewer. + -- Requires overall refactoring in previewer interface + local expanded = vim.fn.expand(path) + if (vim.fn.filereadable(expanded) + vim.fn.isdirectory(expanded)) == 0 then + return + end + end + if escape then + return vim.fn.fnameescape(path) + end + return path +end + +return from_entry diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/health.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/health.lua new file mode 100644 index 0000000..d33a966 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/health.lua @@ -0,0 +1,124 @@ +local health = vim.health or require "health" +local extension_module = require "telescope._extensions" +local extension_info = require("telescope").extensions +local is_win = vim.api.nvim_call_function("has", { "win32" }) == 1 + +local optional_dependencies = { + { + finder_name = "live-grep", + package = { + { + name = "rg", + url = "[BurntSushi/ripgrep](https://github.com/BurntSushi/ripgrep)", + optional = false, + }, + }, + }, + { + finder_name = "find-files", + package = { + { + name = "fd", + binaries = { "fdfind", "fd" }, + url = "[sharkdp/fd](https://github.com/sharkdp/fd)", + optional = true, + }, + }, + }, +} + +local required_plugins = { + { lib = "plenary", optional = false }, + { + lib = "nvim-treesitter", + optional = true, + info = "", + }, +} + +local check_binary_installed = function(package) + local binaries = package.binaries or { package.name } + for _, binary in ipairs(binaries) do + if is_win then + binary = binary .. ".exe" + end + if vim.fn.executable(binary) == 1 then + local handle = io.popen(binary .. " --version") + local binary_version = handle:read "*a" + handle:close() + return true, binary_version + end + end +end + +local function lualib_installed(lib_name) + local res, _ = pcall(require, lib_name) + return res +end + +local M = {} + +M.check = function() + -- Required lua libs + health.report_start "Checking for required plugins" + for _, plugin in ipairs(required_plugins) do + if lualib_installed(plugin.lib) then + health.report_ok(plugin.lib .. " installed.") + else + local lib_not_installed = plugin.lib .. " not found." + if plugin.optional then + health.report_warn(("%s %s"):format(lib_not_installed, plugin.info)) + else + health.report_error(lib_not_installed) + end + end + end + + -- external dependencies + -- TODO: only perform checks if user has enabled dependency in their config + health.report_start "Checking external dependencies" + + for _, opt_dep in pairs(optional_dependencies) do + for _, package in ipairs(opt_dep.package) do + local installed, version = check_binary_installed(package) + if not installed then + local err_msg = ("%s: not found."):format(package.name) + if package.optional then + health.report_warn(("%s %s"):format(err_msg, ("Install %s for extended capabilities"):format(package.url))) + else + health.report_error( + ("%s %s"):format( + err_msg, + ("`%s` finder will not function without %s installed."):format(opt_dep.finder_name, package.url) + ) + ) + end + else + local eol = version:find "\n" + health.report_ok(("%s: found %s"):format(package.name, version:sub(0, eol - 1) or "(unknown version)")) + end + end + end + + -- Extensions + health.report_start "===== Installed extensions =====" + + local installed = {} + for extension_name, _ in pairs(extension_info) do + installed[#installed + 1] = extension_name + end + table.sort(installed) + + for _, installed_ext in ipairs(installed) do + local extension_healthcheck = extension_module._health[installed_ext] + + health.report_start(string.format("Telescope Extension: `%s`", installed_ext)) + if extension_healthcheck then + extension_healthcheck() + else + health.report_info "No healthcheck provided" + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/init.lua new file mode 100644 index 0000000..c267f06 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/init.lua @@ -0,0 +1,176 @@ +local _extensions = require "telescope._extensions" + +local telescope = {} + +-- TODO(conni2461): also table of contents for tree-sitter-lua +-- TODO: Add pre to the works +-- ---@pre [[ +-- ---@pre ]] + +---@brief [[ +--- Telescope.nvim is a plugin for fuzzy finding and neovim. It helps you search, +--- filter, find and pick things in Lua. +--- +--- Getting started with telescope: +--- 1. Run `:checkhealth telescope` to make sure everything is installed. +--- 2. Evaluate it working with +--- `:Telescope find_files` or +--- `:lua require("telescope.builtin").find_files()` +--- 3. Put a `require("telescope").setup() call somewhere in your neovim config. +--- 4. Read |telescope.setup| to check what config keys are available and what you can put inside the setup call +--- 5. Read |telescope.builtin| to check which builtin pickers are offered and what options these implement +--- 6. Profit +--- +--- The below flow chart illustrates a simplified telescope architecture: +---
+--- ┌───────────────────────────────────────────────────────────┐
+--- │      ┌────────┐                                           │
+--- │      │ Multi  │                                ┌───────+  │
+--- │      │ Select │    ┌───────┐                   │ Entry │  │
+--- │      └─────┬──*    │ Entry │    ┌────────+     │ Maker │  │
+--- │            │   ┌───│Manager│────│ Sorter │┐    └───┬───*  │
+--- │            ▼   ▼   └───────*    └────────┘│        │      │
+--- │            1────────┐                 2───┴──┐     │      │
+--- │      ┌─────│ Picker │                 │Finder│◄────┘      │
+--- │      ▼     └───┬────┘                 └──────*            │
+--- │ ┌────────┐     │       3────────+         ▲               │
+--- │ │Selected│     └───────│ Prompt │─────────┘               │
+--- │ │ Entry  │             └───┬────┘                         │
+--- │ └────────*             ┌───┴────┐  ┌────────┐  ┌────────┐ │
+--- │     │  ▲    4─────────┐│ Prompt │  │(Attach)│  │Actions │ │
+--- │     ▼  └──► │ Results ││ Buffer │◄─┤Mappings│◄─┤User Fn │ │
+--- │5─────────┐  └─────────┘└────────┘  └────────┘  └────────┘ │
+--- ││Previewer│                                                │
+--- │└─────────┘                   telescope.nvim architecture  │
+--- └───────────────────────────────────────────────────────────┘
+---
+---   + The `Entry Maker` at least defines
+---     - value: "raw" result of the finder
+---     - ordinal: string to be sorted derived from value
+---     - display: line representation of entry in results buffer
+---
+---   * The finder, entry manager, selected entry, and multi selections
+---     comprises `entries` constructed by the `Entry Maker` from
+---     raw results of the finder (`value`s)
+---
+---  Primary components:
+---   1 Picker: central UI dedicated to varying use cases
+---             (finding files, grepping, diagnostics, etc.)
+---             see :h telescope.builtin
+---   2 Finder: pipe or interactively generates results to pick over
+---   3 Prompt: user input that triggers the finder which sorts results
+---             in order into the entry manager
+---   4 Results: listed entries scored by sorter from finder results
+---   5 Previewer: preview of context of selected entry
+---                see :h telescope.previewers
+--- 
+--- +--- A practical introduction into telescope customization is our +--- `developers.md` (top-level of repo) and `:h telescope.actions` that +--- showcase how to access information about the state of the picker (current +--- selection, etc.). +---
+--- To find out more:
+--- https://github.com/nvim-telescope/telescope.nvim
+---
+---   :h telescope.setup
+---   :h telescope.command
+---   :h telescope.builtin
+---   :h telescope.themes
+---   :h telescope.layout
+---   :h telescope.resolve
+---   :h telescope.actions
+---   :h telescope.actions.state
+---   :h telescope.actions.set
+---   :h telescope.actions.utils
+---   :h telescope.actions.generate
+---   :h telescope.actions.history
+---   :h telescope.previewers
+--- 
+---@brief ]] + +---@tag telescope.nvim +---@config { ["name"] = "INTRODUCTION" } + +--- Setup function to be run by user. Configures the defaults, pickers and +--- extensions of telescope. +--- +--- Usage: +--- +--- require('telescope').setup{ +--- defaults = { +--- -- Default configuration for telescope goes here: +--- -- config_key = value, +--- -- .. +--- }, +--- pickers = { +--- -- Default configuration for builtin pickers goes here: +--- -- picker_name = { +--- -- picker_config_key = value, +--- -- ... +--- -- } +--- -- Now the picker_config_key will be applied every time you call this +--- -- builtin picker +--- }, +--- extensions = { +--- -- Your extension configuration goes here: +--- -- extension_name = { +--- -- extension_config_key = value, +--- -- } +--- -- please take a look at the readme of the extension you want to configure +--- } +--- } +--- +---@param opts table: Configuration opts. Keys: defaults, pickers, extensions +---@eval { ["description"] = require('telescope').__format_setup_keys() } +function telescope.setup(opts) + opts = opts or {} + + if opts.default then + error "'default' is not a valid value for setup. See 'defaults'" + end + + require("telescope.config").set_defaults(opts.defaults) + require("telescope.config").set_pickers(opts.pickers) + _extensions.set_config(opts.extensions) +end + +--- Load an extension. +--- - Notes: +--- - Loading triggers ext setup via the config passed in |telescope.setup| +---@param name string: Name of the extension +function telescope.load_extension(name) + return _extensions.load(name) +end + +--- Register an extension. To be used by plugin authors. +---@param mod table: Module +function telescope.register_extension(mod) + return _extensions.register(mod) +end + +--- Use telescope.extensions to reference any extensions within your configuration.
+--- While the docs currently generate this as a function, it's actually a table. Sorry. +telescope.extensions = require("telescope._extensions").manager + +telescope.__format_setup_keys = function() + local names = require("telescope.config").descriptions_order + local descriptions = require("telescope.config").descriptions + + local result = { "
", "", "Valid keys for {opts.defaults}" }
+  for _, name in ipairs(names) do
+    local desc = descriptions[name]
+
+    table.insert(result, "")
+    table.insert(result, string.format("%s*telescope.defaults.%s*", string.rep(" ", 70 - 20 - #name), name))
+    table.insert(result, string.format("%s: ~", name))
+    for _, line in ipairs(vim.split(desc, "\n")) do
+      table.insert(result, string.format("    %s", line))
+    end
+  end
+
+  table.insert(result, "
") + return result +end + +return telescope diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/log.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/log.lua new file mode 100644 index 0000000..c74378f --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/log.lua @@ -0,0 +1,4 @@ +return require("plenary.log").new { + plugin = "telescope", + level = "info", +} diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/make_entry.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/make_entry.lua new file mode 100644 index 0000000..2a17f80 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/make_entry.lua @@ -0,0 +1,1327 @@ +---@tag telescope.make_entry + +---@brief [[ +--- +--- Each picker has a finder made up of two parts, the results which are the +--- data to be displayed, and the entry_maker. These entry_makers are functions +--- returned from make_entry functions. These will be referrd to as +--- entry_makers in the following documentation. +--- +--- Every entry maker returns a function which accepts the data to be used for +--- an entry. This function will return an entry table (or nil, meaning skip +--- this entry) which contains of the - following important keys: +--- - value any: value key can be anything but still required +--- - valid bool: is an optional key because it defaults to true but if the key +--- is set to false it will not be displayed by the picker. (optional) +--- - ordinal string: is the text that is used for filtering (required) +--- - display string|function: is either a string of the text that is being +--- displayed or a function receiving the entry at a later stage, when the entry +--- is actually being displayed. A function can be useful here if complex +--- calculation have to be done. `make_entry` can also return a second value +--- a highlight array which will then apply to the line. Highlight entry in +--- this array has the following signature `{ { start_col, end_col }, hl_group }` +--- (required). +--- - filename string: will be interpreted by the default `` action as +--- open this file (optional) +--- - bufnr number: will be interpreted by the default `` action as open +--- this buffer (optional) +--- - lnum number: lnum value which will be interpreted by the default `` +--- action as a jump to this line (optional) +--- - col number: col value which will be interpreted by the default `` +--- action as a jump to this column (optional) +--- +--- More information on easier displaying, see |telescope.pickers.entry_display| +--- +--- TODO: Document something we call `entry_index` +---@brief ]] + +local entry_display = require "telescope.pickers.entry_display" +local utils = require "telescope.utils" +local strings = require "plenary.strings" +local Path = require "plenary.path" + +local treesitter_type_highlight = { + ["associated"] = "TSConstant", + ["constant"] = "TSConstant", + ["field"] = "TSField", + ["function"] = "TSFunction", + ["method"] = "TSMethod", + ["parameter"] = "TSParameter", + ["property"] = "TSProperty", + ["struct"] = "Struct", + ["var"] = "TSVariableBuiltin", +} + +local lsp_type_highlight = { + ["Class"] = "TelescopeResultsClass", + ["Constant"] = "TelescopeResultsConstant", + ["Field"] = "TelescopeResultsField", + ["Function"] = "TelescopeResultsFunction", + ["Method"] = "TelescopeResultsMethod", + ["Property"] = "TelescopeResultsOperator", + ["Struct"] = "TelescopeResultsStruct", + ["Variable"] = "TelescopeResultsVariable", +} + +local get_filename_fn = function() + local bufnr_name_cache = {} + return function(bufnr) + bufnr = vim.F.if_nil(bufnr, 0) + local c = bufnr_name_cache[bufnr] + if c then + return c + end + + local n = vim.api.nvim_buf_get_name(bufnr) + bufnr_name_cache[bufnr] = n + return n + end +end + +local handle_entry_index = function(opts, t, k) + local override = ((opts or {}).entry_index or {})[k] + if not override then + return + end + + local val, save = override(t, opts) + if save then + rawset(t, k, val) + end + return val +end + +local make_entry = {} + +make_entry.set_default_entry_mt = function(tbl, opts) + return setmetatable({}, { + __index = function(t, k) + local override = handle_entry_index(opts, t, k) + if override then + return override + end + + -- Only hit tbl once + local val = tbl[k] + if val then + rawset(t, k, val) + end + + return val + end, + }) +end + +do + local lookup_keys = { + display = 1, + ordinal = 1, + value = 1, + } + + function make_entry.gen_from_string(opts) + local mt_string_entry = { + __index = function(t, k) + local override = handle_entry_index(opts, t, k) + if override then + return override + end + + return rawget(t, rawget(lookup_keys, k)) + end, + } + + return function(line) + return setmetatable({ + line, + }, mt_string_entry) + end + end +end + +do + local lookup_keys = { + ordinal = 1, + value = 1, + filename = 1, + cwd = 2, + } + + function make_entry.gen_from_file(opts) + opts = opts or {} + + local cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()) + + local disable_devicons = opts.disable_devicons + + local mt_file_entry = {} + + mt_file_entry.cwd = cwd + mt_file_entry.display = function(entry) + local hl_group + local display = utils.transform_path(opts, entry.value) + + display, hl_group = utils.transform_devicons(entry.value, display, disable_devicons) + + if hl_group then + return display, { { { 1, 3 }, hl_group } } + else + return display + end + end + + mt_file_entry.__index = function(t, k) + local override = handle_entry_index(opts, t, k) + if override then + return override + end + + local raw = rawget(mt_file_entry, k) + if raw then + return raw + end + + if k == "path" then + local retpath = Path:new({ t.cwd, t.value }):absolute() + if not vim.loop.fs_access(retpath, "R", nil) then + retpath = t.value + end + return retpath + end + + return rawget(t, rawget(lookup_keys, k)) + end + + return function(line) + return setmetatable({ line }, mt_file_entry) + end + end +end + +do + local lookup_keys = { + value = 1, + ordinal = 1, + } + + -- Gets called only once to parse everything out for the vimgrep, after that looks up directly. + local parse_with_col = function(t) + local _, _, filename, lnum, col, text = string.find(t.value, [[(..-):(%d+):(%d+):(.*)]]) + + local ok + ok, lnum = pcall(tonumber, lnum) + if not ok then + lnum = nil + end + + ok, col = pcall(tonumber, col) + if not ok then + col = nil + end + + t.filename = filename + t.lnum = lnum + t.col = col + t.text = text + + return { filename, lnum, col, text } + end + + local parse_without_col = function(t) + local _, _, filename, lnum, text = string.find(t.value, [[(..-):(%d+):(.*)]]) + + local ok + ok, lnum = pcall(tonumber, lnum) + if not ok then + lnum = nil + end + + t.filename = filename + t.lnum = lnum + t.col = nil + t.text = text + + return { filename, lnum, nil, text } + end + + local parse_only_filename = function(t) + t.filename = t.value + t.lnum = nil + t.col = nil + t.text = "" + + return { t.filename, nil, nil, "" } + end + + function make_entry.gen_from_vimgrep(opts) + opts = opts or {} + + local mt_vimgrep_entry + local parse = parse_with_col + if opts.__matches == true then + parse = parse_only_filename + elseif opts.__inverted == true then + parse = parse_without_col + end + + local disable_devicons = opts.disable_devicons + local disable_coordinates = opts.disable_coordinates + local only_sort_text = opts.only_sort_text + + local execute_keys = { + path = function(t) + if Path:new(t.filename):is_absolute() then + return t.filename, false + else + return Path:new({ t.cwd, t.filename }):absolute(), false + end + end, + + filename = function(t) + return parse(t)[1], true + end, + + lnum = function(t) + return parse(t)[2], true + end, + + col = function(t) + return parse(t)[3], true + end, + + text = function(t) + return parse(t)[4], true + end, + } + + -- For text search only, the ordinal value is actually the text. + if only_sort_text then + execute_keys.ordinal = function(t) + return t.text + end + end + + local display_string = "%s%s%s" + + mt_vimgrep_entry = { + cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()), + + display = function(entry) + local display_filename = utils.transform_path(opts, entry.filename) + + local coordinates = "" + if not disable_coordinates then + if entry.lnum then + if entry.col then + coordinates = string.format(":%s:%s:", entry.lnum, entry.col) + else + coordinates = string.format(":%s:", entry.lnum) + end + end + end + + local display, hl_group = utils.transform_devicons( + entry.filename, + string.format(display_string, display_filename, coordinates, entry.text), + disable_devicons + ) + + if hl_group then + return display, { { { 1, 3 }, hl_group } } + else + return display + end + end, + + __index = function(t, k) + local override = handle_entry_index(opts, t, k) + if override then + return override + end + + local raw = rawget(mt_vimgrep_entry, k) + if raw then + return raw + end + + local executor = rawget(execute_keys, k) + if executor then + local val, save = executor(t) + if save then + rawset(t, k, val) + end + return val + end + + return rawget(t, rawget(lookup_keys, k)) + end, + } + + return function(line) + return setmetatable({ line }, mt_vimgrep_entry) + end + end +end + +function make_entry.gen_from_git_stash(opts) + local displayer = entry_display.create { + separator = " ", + items = { + { width = 10 }, + opts.show_branch and { width = 15 } or "", + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.value, "TelescopeResultsLineNr" }, + opts.show_branch and { entry.branch_name, "TelescopeResultsIdentifier" } or "", + entry.commit_info, + } + end + + return function(entry) + if entry == "" then + return nil + end + + local splitted = utils.max_split(entry, ": ", 2) + local stash_idx = splitted[1] + local _, branch_name = string.match(splitted[2], "^([WIP on|On]+) (.+)") + local commit_info = splitted[3] + + return make_entry.set_default_entry_mt({ + value = stash_idx, + ordinal = commit_info, + branch_name = branch_name, + commit_info = commit_info, + display = make_display, + }, opts) + end +end + +function make_entry.gen_from_git_commits(opts) + opts = opts or {} + + local displayer = entry_display.create { + separator = " ", + items = { + { width = 8 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.value, "TelescopeResultsIdentifier" }, + entry.msg, + } + end + + return function(entry) + if entry == "" then + return nil + end + + local sha, msg = string.match(entry, "([^ ]+) (.+)") + + if not msg then + sha = entry + msg = "" + end + + return make_entry.set_default_entry_mt({ + value = sha, + ordinal = sha .. " " .. msg, + msg = msg, + display = make_display, + current_file = opts.current_file, + }, opts) + end +end + +function make_entry.gen_from_quickfix(opts) + opts = opts or {} + local show_line = vim.F.if_nil(opts.show_line, true) + + local hidden = utils.is_path_hidden(opts) + local items = { + { width = vim.F.if_nil(opts.fname_width, 30) }, + { remaining = true }, + } + if hidden then + items[1] = { width = 8 } + end + if not show_line then + table.remove(items, 1) + end + + local displayer = entry_display.create { separator = "▏", items = items } + + local make_display = function(entry) + local input = {} + if not hidden then + table.insert(input, string.format("%s:%d:%d", utils.transform_path(opts, entry.filename), entry.lnum, entry.col)) + else + table.insert(input, string.format("%4d:%2d", entry.lnum, entry.col)) + end + + if show_line then + local text = entry.text + if opts.trim_text then + text = text:gsub("^%s*(.-)%s*$", "%1") + end + text = text:gsub(".* | ", "") + table.insert(input, text) + end + + return displayer(input) + end + + local get_filename = get_filename_fn() + return function(entry) + local filename = vim.F.if_nil(entry.filename, get_filename(entry.bufnr)) + + return make_entry.set_default_entry_mt({ + value = entry, + ordinal = (not hidden and filename or "") .. " " .. entry.text, + display = make_display, + + bufnr = entry.bufnr, + filename = filename, + lnum = entry.lnum, + col = entry.col, + text = entry.text, + start = entry.start, + finish = entry.finish, + }, opts) + end +end + +function make_entry.gen_from_lsp_symbols(opts) + opts = opts or {} + + local bufnr = opts.bufnr or vim.api.nvim_get_current_buf() + + -- Default we have two columns, symbol and type(unbound) + -- If path is not hidden then its, filepath, symbol and type(still unbound) + -- If show_line is also set, type is bound to len 8 + local display_items = { + { width = opts.symbol_width or 25 }, + { remaining = true }, + } + + local hidden = utils.is_path_hidden(opts) + if not hidden then + table.insert(display_items, 1, { width = vim.F.if_nil(opts.fname_width, 30) }) + end + + if opts.show_line then + -- bound type to len 8 or custom + table.insert(display_items, #display_items, { width = opts.symbol_type_width or 8 }) + end + + local displayer = entry_display.create { + separator = " ", + hl_chars = { ["["] = "TelescopeBorder", ["]"] = "TelescopeBorder" }, + items = display_items, + } + local type_highlight = vim.F.if_nil(opts.symbol_highlights or lsp_type_highlight) + + local make_display = function(entry) + local msg + + if opts.show_line then + msg = vim.trim(vim.F.if_nil(vim.api.nvim_buf_get_lines(bufnr, entry.lnum - 1, entry.lnum, false)[1], "")) + end + + if hidden then + return displayer { + entry.symbol_name, + { entry.symbol_type:lower(), type_highlight[entry.symbol_type] }, + msg, + } + else + return displayer { + utils.transform_path(opts, entry.filename), + entry.symbol_name, + { entry.symbol_type:lower(), type_highlight[entry.symbol_type] }, + msg, + } + end + end + + local get_filename = get_filename_fn() + return function(entry) + local filename = vim.F.if_nil(entry.filename, get_filename(entry.bufnr)) + local symbol_msg = entry.text + local symbol_type, symbol_name = symbol_msg:match "%[(.+)%]%s+(.*)" + local ordinal = "" + if not hidden and filename then + ordinal = filename .. " " + end + ordinal = ordinal .. symbol_name .. " " .. (symbol_type or "unknown") + return make_entry.set_default_entry_mt({ + value = entry, + ordinal = ordinal, + display = make_display, + + filename = filename, + lnum = entry.lnum, + col = entry.col, + symbol_name = symbol_name, + symbol_type = symbol_type, + start = entry.start, + finish = entry.finish, + }, opts) + end +end + +function make_entry.gen_from_buffer(opts) + opts = opts or {} + + local disable_devicons = opts.disable_devicons + + local icon_width = 0 + if not disable_devicons then + local icon, _ = utils.get_devicons("fname", disable_devicons) + icon_width = strings.strdisplaywidth(icon) + end + + local displayer = entry_display.create { + separator = " ", + items = { + { width = opts.bufnr_width }, + { width = 4 }, + { width = icon_width }, + { remaining = true }, + }, + } + + local cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()) + + local make_display = function(entry) + -- bufnr_width + modes + icon + 3 spaces + : + lnum + opts.__prefix = opts.bufnr_width + 4 + icon_width + 3 + 1 + #tostring(entry.lnum) + local display_bufname = utils.transform_path(opts, entry.filename) + local icon, hl_group = utils.get_devicons(entry.filename, disable_devicons) + + return displayer { + { entry.bufnr, "TelescopeResultsNumber" }, + { entry.indicator, "TelescopeResultsComment" }, + { icon, hl_group }, + display_bufname .. ":" .. entry.lnum, + } + end + + return function(entry) + local bufname = entry.info.name ~= "" and entry.info.name or "[No Name]" + -- if bufname is inside the cwd, trim that part of the string + bufname = Path:new(bufname):normalize(cwd) + + local hidden = entry.info.hidden == 1 and "h" or "a" + local readonly = vim.api.nvim_buf_get_option(entry.bufnr, "readonly") and "=" or " " + local changed = entry.info.changed == 1 and "+" or " " + local indicator = entry.flag .. hidden .. readonly .. changed + local line_count = vim.api.nvim_buf_line_count(entry.bufnr) + + return make_entry.set_default_entry_mt({ + value = bufname, + ordinal = entry.bufnr .. " : " .. bufname, + display = make_display, + + bufnr = entry.bufnr, + filename = bufname, + -- account for potentially stale lnum as getbufinfo might not be updated or from resuming buffers picker + lnum = entry.info.lnum ~= 0 and math.max(math.min(entry.info.lnum, line_count), 1) or 1, + indicator = indicator, + }, opts) + end +end + +function make_entry.gen_from_treesitter(opts) + opts = opts or {} + + local bufnr = opts.bufnr or vim.api.nvim_get_current_buf() + + local display_items = { + { width = 25 }, + { width = 10 }, + { remaining = true }, + } + + if opts.show_line then + table.insert(display_items, 2, { width = 6 }) + end + + local displayer = entry_display.create { + separator = " ", + items = display_items, + } + + local type_highlight = opts.symbol_highlights or treesitter_type_highlight + + local make_display = function(entry) + local msg = vim.api.nvim_buf_get_lines(bufnr, entry.lnum, entry.lnum, false)[1] or "" + msg = vim.trim(msg) + + local display_columns = { + entry.text, + { entry.kind, type_highlight[entry.kind], type_highlight[entry.kind] }, + msg, + } + if opts.show_line then + table.insert(display_columns, 2, { entry.lnum .. ":" .. entry.col, "TelescopeResultsLineNr" }) + end + + return displayer(display_columns) + end + + local get_filename = get_filename_fn() + return function(entry) + local ts_utils = require "nvim-treesitter.ts_utils" + local start_row, start_col, end_row, _ = ts_utils.get_node_range(entry.node) + local node_text = vim.treesitter.get_node_text(entry.node, bufnr) + return make_entry.set_default_entry_mt({ + value = entry.node, + kind = entry.kind, + ordinal = node_text .. " " .. (entry.kind or "unknown"), + display = make_display, + + node_text = node_text, + + filename = get_filename(bufnr), + -- need to add one since the previewer substacts one + lnum = start_row + 1, + col = start_col, + text = node_text, + start = start_row, + finish = end_row, + }, opts) + end +end + +function make_entry.gen_from_packages(opts) + opts = opts or {} + + local make_display = function(module_name) + local p_path = package.searchpath(module_name, package.path) or "" + local display = string.format("%-" .. opts.column_len .. "s : %s", module_name, vim.fn.fnamemodify(p_path, ":~:.")) + + return display + end + + return function(module_name) + return make_entry.set_default_entry_mt({ + valid = module_name ~= "", + value = module_name, + ordinal = module_name, + display = make_display(module_name), + }, opts) + end +end + +function make_entry.gen_from_apropos(opts) + local sections = {} + if #opts.sections == 1 and opts.sections[1] == "ALL" then + setmetatable(sections, { + __index = function() + return true + end, + }) + else + for _, section in ipairs(opts.sections) do + sections[section] = true + end + end + + local displayer = entry_display.create { + separator = " ", + items = { + { width = 30 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.keyword, "TelescopeResultsFunction" }, + entry.description, + } + end + + return function(line) + local keyword, cmd, section, desc = line:match "^((.-)%s*%(([^)]+)%).-)%s+%-%s+(.*)$" + -- apropos might return alternatives for the cmd which are split on `,` and breaks everything else + -- for example on void linux it will return `alacritty, Alacritty` which will later result in + -- `man 1 alacritty, Alacritty`. So we just take the first one. + -- doing this outside of regex because of obvious reasons + cmd = vim.split(cmd, ",")[1] + return keyword + and sections[section] + and make_entry.set_default_entry_mt({ + value = cmd, + description = desc, + ordinal = cmd, + display = make_display, + section = section, + keyword = keyword, + }, opts) + or nil + end +end + +function make_entry.gen_from_marks(opts) + return function(item) + return make_entry.set_default_entry_mt({ + value = item.line, + ordinal = item.line, + display = item.line, + lnum = item.lnum, + col = item.col, + start = item.lnum, + filename = item.filename, + }, opts) + end +end + +function make_entry.gen_from_registers(opts) + local displayer = entry_display.create { + separator = " ", + hl_chars = { ["["] = "TelescopeBorder", ["]"] = "TelescopeBorder" }, + items = { + { width = 3 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + local content = entry.content + return displayer { + { "[" .. entry.value .. "]", "TelescopeResultsNumber" }, + type(content) == "string" and content:gsub("\n", "\\n") or content, + } + end + + return function(entry) + local contents = vim.fn.getreg(entry) + return make_entry.set_default_entry_mt({ + value = entry, + ordinal = string.format("%s %s", entry, contents), + content = contents, + display = make_display, + }, opts) + end +end + +function make_entry.gen_from_keymaps(opts) + local function get_desc(entry) + if entry.callback and not entry.desc then + return require("telescope.actions.utils")._get_anon_function_name(entry.callback) + end + return vim.F.if_nil(entry.desc, entry.rhs) + end + + local function get_lhs(entry) + return utils.display_termcodes(entry.lhs) + end + + local displayer = require("telescope.pickers.entry_display").create { + separator = "▏", + items = { + { width = 2 }, + { width = opts.width_lhs }, + { remaining = true }, + }, + } + local make_display = function(entry) + return displayer { + entry.mode, + get_lhs(entry), + get_desc(entry), + } + end + + return function(entry) + return make_entry.set_default_entry_mt({ + mode = entry.mode, + lhs = get_lhs(entry), + desc = get_desc(entry), + -- + valid = entry ~= "", + value = entry, + ordinal = entry.mode .. " " .. get_lhs(entry) .. " " .. get_desc(entry), + display = make_display, + }, opts) + end +end + +function make_entry.gen_from_highlights(opts) + local make_display = function(entry) + local display = entry.value + return display, { { { 0, #display }, display } } + end + + return function(entry) + return make_entry.set_default_entry_mt({ + value = entry, + display = make_display, + ordinal = entry, + }, opts) + end +end + +function make_entry.gen_from_picker(opts) + local displayer = entry_display.create { + separator = " │ ", + items = { + { width = 0.5 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + entry.value.prompt_title, + entry.value.default_text, + } + end + + return function(entry) + return make_entry.set_default_entry_mt({ + value = entry, + text = entry.prompt_title, + ordinal = string.format("%s %s", entry.prompt_title, vim.F.if_nil(entry.default_text, "")), + display = make_display, + }, opts) + end +end + +function make_entry.gen_from_buffer_lines(opts) + local displayer = entry_display.create { + separator = " │ ", + items = { + { width = 5 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.lnum, opts.lnum_highlight_group or "TelescopeResultsSpecialComment" }, + { + entry.text, + function() + if not opts.line_highlights then + return {} + end + + local line_hl = opts.line_highlights[entry.lnum] or {} + -- TODO: We could probably squash these together if the are the same... + -- But I don't think that it's worth it at the moment. + local result = {} + + for col, hl in pairs(line_hl) do + table.insert(result, { { col, col + 1 }, hl }) + end + + return result + end, + }, + } + end + + return function(entry) + if opts.skip_empty_lines and string.match(entry.text, "^$") then + return + end + + return make_entry.set_default_entry_mt({ + ordinal = entry.text, + display = make_display, + filename = entry.filename, + lnum = entry.lnum, + text = entry.text, + }, opts) + end +end + +function make_entry.gen_from_vimoptions(opts) + local displayer = entry_display.create { + separator = "", + hl_chars = { ["["] = "TelescopeBorder", ["]"] = "TelescopeBorder" }, + items = { + { width = 25 }, + { width = 12 }, + { width = 11 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.value.name, "Keyword" }, + { "[" .. entry.value.type .. "]", "Type" }, + { "[" .. entry.value.scope .. "]", "Identifier" }, + utils.display_termcodes(tostring(entry.value.value)), + } + end + + return function(o) + local entry = { + display = make_display, + value = { + name = o.name, + value = o.default, + type = o.type, + scope = o.scope, + }, + ordinal = string.format("%s %s %s", o.name, o.type, o.scope), + } + + local ok, value = pcall(vim.api.nvim_get_option, o.name) + if ok then + entry.value.value = value + entry.ordinal = entry.ordinal .. " " .. utils.display_termcodes(tostring(value)) + else + entry.ordinal = entry.ordinal .. " " .. utils.display_termcodes(tostring(o.default)) + end + + return make_entry.set_default_entry_mt(entry, opts) + end +end + +function make_entry.gen_from_ctags(opts) + opts = opts or {} + + local cwd = vim.fn.expand(opts.cwd or vim.loop.cwd()) + local current_file = Path:new(vim.api.nvim_buf_get_name(opts.bufnr)):normalize(cwd) + + local display_items = { + { remaining = true }, + } + + local idx = 1 + local hidden = utils.is_path_hidden(opts) + if not hidden then + table.insert(display_items, idx, { width = vim.F.if_nil(opts.fname_width, 30) }) + idx = idx + 1 + end + + if opts.show_line then + table.insert(display_items, idx, { width = 30 }) + end + + local displayer = entry_display.create { + separator = " │ ", + items = display_items, + } + + local make_display = function(entry) + local filename = utils.transform_path(opts, entry.filename) + + local scode + if opts.show_line then + scode = entry.scode + end + + if hidden then + return displayer { + entry.tag, + scode, + } + else + return displayer { + filename, + entry.tag, + scode, + } + end + end + + local mt = {} + mt.__index = function(t, k) + local override = handle_entry_index(opts, t, k) + if override then + return override + end + + if k == "path" then + local retpath = Path:new({ t.filename }):absolute() + if not vim.loop.fs_access(retpath, "R", nil) then + retpath = t.filename + end + return retpath + end + end + + local current_file_cache = {} + return function(line) + if line == "" or line:sub(1, 1) == "!" then + return nil + end + + local tag, file, scode, lnum + -- ctags gives us: 'tags\tfile\tsource' + tag, file, scode = string.match(line, '([^\t]+)\t([^\t]+)\t/^?\t?(.*)/;"\t+.*') + if not tag then + -- hasktags gives us: 'tags\tfile\tlnum' + tag, file, lnum = string.match(line, "([^\t]+)\t([^\t]+)\t(%d+).*") + end + + if Path.path.sep == "\\" then + file = string.gsub(file, "/", "\\") + end + + if opts.only_current_file then + if current_file_cache[file] == nil then + current_file_cache[file] = Path:new(file):normalize(cwd) == current_file + end + + if current_file_cache[file] == false then + return nil + end + end + + local tag_entry = {} + if opts.only_sort_tags then + tag_entry.ordinal = tag + else + tag_entry.ordinal = file .. ": " .. tag + end + + tag_entry.display = make_display + tag_entry.scode = scode + tag_entry.tag = tag + tag_entry.filename = file + tag_entry.col = 1 + tag_entry.lnum = lnum and tonumber(lnum) or 1 + + return setmetatable(tag_entry, mt) + end +end + +function make_entry.gen_from_diagnostics(opts) + opts = opts or {} + + local signs = (function() + if opts.no_sign then + return + end + local signs = {} + local type_diagnostic = vim.diagnostic.severity + for _, severity in ipairs(type_diagnostic) do + local status, sign = pcall(function() + -- only the first char is upper all others are lowercalse + return vim.trim(vim.fn.sign_getdefined("DiagnosticSign" .. severity:lower():gsub("^%l", string.upper))[1].text) + end) + if not status then + sign = severity:sub(1, 1) + end + signs[severity] = sign + end + return signs + end)() + + local display_items = { + { width = signs ~= nil and 10 or 8 }, + { remaining = true }, + } + local line_width = vim.F.if_nil(opts.line_width, 0.5) + local hidden = utils.is_path_hidden(opts) + if not hidden then + table.insert(display_items, 2, { width = line_width }) + end + local displayer = entry_display.create { + separator = "▏", + items = display_items, + } + + local make_display = function(entry) + local filename = utils.transform_path(opts, entry.filename) + + -- add styling of entries + local pos = string.format("%4d:%2d", entry.lnum, entry.col) + local line_info = { + (signs and signs[entry.type] .. " " or "") .. pos, + "DiagnosticSign" .. entry.type, + } + + return displayer { + line_info, + entry.text, + filename, + } + end + + return function(entry) + return make_entry.set_default_entry_mt({ + value = entry, + ordinal = ("%s %s"):format(not hidden and entry.filename or "", entry.text), + display = make_display, + filename = entry.filename, + type = entry.type, + lnum = entry.lnum, + col = entry.col, + text = entry.text, + }, opts) + end +end + +function make_entry.gen_from_autocommands(opts) + local displayer = entry_display.create { + separator = "▏", + items = { + { width = 14 }, + { width = 18 }, + { width = 16 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + { entry.value.event, "vimAutoEvent" }, + { entry.value.group_name, "vimAugroup" }, + { entry.value.pattern, "vimAutoCmdSfxList" }, + entry.value.command, + } + end + + return function(entry) + local group_name = vim.F.if_nil(entry.group_name, "") + local command = entry.command + if entry.desc and (entry.callback or vim.startswith(command, "%s]?(.+)") + + return setmetatable({ + value = file, + status = mod, + ordinal = entry, + display = make_display, + path = Path:new({ opts.cwd, file }):absolute(), + }, opts) + end +end + +return make_entry diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/mappings.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/mappings.lua new file mode 100644 index 0000000..45fad81 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/mappings.lua @@ -0,0 +1,360 @@ +---@tag telescope.mappings + +---@brief [[ +--- |telescope.mappings| is used to configure the keybindings within +--- a telescope picker. These keybinds are only local to the picker window +--- and will be cleared once you exit the picker. +--- +--- We provide multiple different ways of configuring, as described below, +--- to provide an easy to use experience for changing the default behavior +--- of telescope or extending for your own purposes. +--- +--- To see many of the builtin actions that you can use as values for this +--- table, see |telescope.actions| +--- +--- Format is: +--- +--- { +--- mode = { ..keys } +--- } +--- +--- +--- where {mode} is the one character letter for a mode ('i' for insert, 'n' for normal). +--- +--- For example: +--- +--- mappings = { +--- i = { +--- [""] = require('telescope.actions').close, +--- }, +--- } +--- +--- +--- To disable a keymap, put `[map] = false`
+--- For example: +--- +--- { +--- ..., +--- [""] = false, +--- ..., +--- } +--- +--- Into your config. +--- +--- To override behavior of a key, simply set the value +--- to be a function (either by requiring an action or by writing +--- your own function) +--- +--- { +--- ..., +--- [""] = require('telescope.actions').select_default, +--- ..., +--- } +--- +--- +--- If the function you want is part of `telescope.actions`, then you can +--- simply give a string. +--- For example, the previous option is equivalent to: +--- +--- { +--- ..., +--- [""] = "select_default", +--- ..., +--- } +--- +--- +--- You can also add other mappings using tables with `type = "command"`. +--- For example: +--- +--- { +--- ..., +--- ["jj"] = { "", type = "command" }, +--- ["kk"] = { "echo \"Hello, World!\"", type = "command" },) +--- ..., +--- } +--- +--- +--- You can also add additional options for mappings of any type ("action" and "command"). +--- For example: +--- +--- { +--- ..., +--- [""] = { +--- actions.move_selection_next, type = "action", +--- opts = { nowait = true, silent = true } +--- }, +--- ..., +--- } +--- +--- +--- There are three main places you can configure |telescope.mappings|. These are +--- ordered from the lowest priority to the highest priority. +--- +--- 1. |telescope.defaults.mappings| +--- 2. In the |telescope.setup()| table, inside a picker with a given name, use the `mappings` key +--- +--- require("telescope").setup { +--- pickers = { +--- find_files = { +--- mappings = { +--- n = { +--- ["kj"] = "close", +--- }, +--- }, +--- }, +--- }, +--- } +--- +--- 3. `attach_mappings` function for a particular picker. +--- +--- require("telescope.builtin").find_files { +--- attach_mappings = function(_, map) +--- map("i", "asdf", function(_prompt_bufnr) +--- print "You typed asdf" +--- end) +--- +--- map({"i", "n"}, "", function(_prompt_bufnr) +--- print "You typed " +--- end) +--- +--- -- needs to return true if you want to map default_mappings and +--- -- false if not +--- return true +--- end, +--- } +--- +---@brief ]] + +local a = vim.api + +local actions = require "telescope.actions" +local config = require "telescope.config" + +local mappings = {} + +mappings.default_mappings = config.values.default_mappings + or { + i = { + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + + [""] = actions.close, + + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + + [""] = actions.select_default, + [""] = actions.select_horizontal, + [""] = actions.select_vertical, + [""] = actions.select_tab, + + [""] = actions.preview_scrolling_up, + [""] = actions.preview_scrolling_down, + + [""] = actions.results_scrolling_up, + [""] = actions.results_scrolling_down, + + [""] = actions.toggle_selection + actions.move_selection_worse, + [""] = actions.toggle_selection + actions.move_selection_better, + [""] = actions.send_to_qflist + actions.open_qflist, + [""] = actions.send_selected_to_qflist + actions.open_qflist, + [""] = actions.complete_tag, + [""] = actions.which_key, + [""] = actions.which_key, -- keys from pressing + [""] = { "", type = "command" }, + + -- disable c-j because we dont want to allow new lines #2123 + [""] = actions.nop, + }, + + n = { + [""] = actions.close, + [""] = actions.select_default, + [""] = actions.select_horizontal, + [""] = actions.select_vertical, + [""] = actions.select_tab, + + [""] = actions.toggle_selection + actions.move_selection_worse, + [""] = actions.toggle_selection + actions.move_selection_better, + [""] = actions.send_to_qflist + actions.open_qflist, + [""] = actions.send_selected_to_qflist + actions.open_qflist, + + -- TODO: This would be weird if we switch the ordering. + ["j"] = actions.move_selection_next, + ["k"] = actions.move_selection_previous, + ["H"] = actions.move_to_top, + ["M"] = actions.move_to_middle, + ["L"] = actions.move_to_bottom, + + [""] = actions.move_selection_next, + [""] = actions.move_selection_previous, + ["gg"] = actions.move_to_top, + ["G"] = actions.move_to_bottom, + + [""] = actions.preview_scrolling_up, + [""] = actions.preview_scrolling_down, + + [""] = actions.results_scrolling_up, + [""] = actions.results_scrolling_down, + + ["?"] = actions.which_key, + }, + } + +__TelescopeKeymapStore = __TelescopeKeymapStore + or setmetatable({}, { + __index = function(t, k) + rawset(t, k, {}) + + return rawget(t, k) + end, + }) +local keymap_store = __TelescopeKeymapStore + +local _mapping_key_id = 0 +local get_next_id = function() + _mapping_key_id = _mapping_key_id + 1 + return _mapping_key_id +end + +local assign_function = function(prompt_bufnr, func) + local func_id = get_next_id() + + keymap_store[prompt_bufnr][func_id] = func + + return func_id +end + +local telescope_map = function(prompt_bufnr, mode, key_bind, key_func, opts) + if not key_func then + return + end + + opts = opts or {} + if opts.noremap == nil then + opts.noremap = true + end + if opts.silent == nil then + opts.silent = true + end + + if type(key_func) == "string" then + key_func = actions[key_func] + elseif type(key_func) == "table" then + if key_func.type == "command" then + a.nvim_buf_set_keymap(prompt_bufnr, mode, key_bind, key_func[1], opts or { + silent = true, + }) + return + elseif key_func.type == "action_key" then + key_func = actions[key_func[1]] + elseif key_func.type == "action" then + key_func = key_func[1] + end + end + + local key_id = assign_function(prompt_bufnr, key_func) + local prefix + + local map_string + if opts.expr then + map_string = + string.format([[luaeval("require('telescope.mappings').execute_keymap(%s, %s)")]], prompt_bufnr, key_id) + else + if mode == "i" and not opts.expr then + prefix = "" + elseif mode == "n" then + prefix = ":" + else + prefix = ":" + end + + map_string = + string.format("%slua require('telescope.mappings').execute_keymap(%s, %s)", prefix, prompt_bufnr, key_id) + end + + a.nvim_buf_set_keymap(prompt_bufnr, mode, key_bind, map_string, opts) +end + +local extract_keymap_opts = function(key_func) + if type(key_func) == "table" and key_func.opts ~= nil then + -- we can't clear this because key_func could be a table from the config. + -- If we clear it the table ref would lose opts after the first bind + -- We need to copy it so noremap and silent won't be part of the table ref after the first bind + return vim.deepcopy(key_func.opts) + end + return {} +end + +mappings.apply_keymap = function(prompt_bufnr, attach_mappings, buffer_keymap) + local applied_mappings = { n = {}, i = {} } + + local map = function(modes, key_bind, key_func, opts) + if type(modes) == "string" then + modes = { modes } + end + + for _, mode in pairs(modes) do + mode = string.lower(mode) + local key_bind_internal = a.nvim_replace_termcodes(key_bind, true, true, true) + applied_mappings[mode][key_bind_internal] = true + + telescope_map(prompt_bufnr, mode, key_bind, key_func, opts) + end + end + + if attach_mappings then + local attach_results = attach_mappings(prompt_bufnr, map) + + if attach_results == nil then + error( + "Attach mappings must always return a value. `true` means use default mappings, " + .. "`false` means only use attached mappings" + ) + end + + if not attach_results then + return + end + end + + for mode, mode_map in pairs(buffer_keymap or {}) do + mode = string.lower(mode) + + for key_bind, key_func in pairs(mode_map) do + local key_bind_internal = a.nvim_replace_termcodes(key_bind, true, true, true) + if not applied_mappings[mode][key_bind_internal] then + applied_mappings[mode][key_bind_internal] = true + telescope_map(prompt_bufnr, mode, key_bind, key_func, extract_keymap_opts(key_func)) + end + end + end + + -- TODO: Probably should not overwrite any keymaps + for mode, mode_map in pairs(mappings.default_mappings) do + mode = string.lower(mode) + + for key_bind, key_func in pairs(mode_map) do + local key_bind_internal = a.nvim_replace_termcodes(key_bind, true, true, true) + if not applied_mappings[mode][key_bind_internal] then + applied_mappings[mode][key_bind_internal] = true + telescope_map(prompt_bufnr, mode, key_bind, key_func, extract_keymap_opts(key_func)) + end + end + end +end + +mappings.execute_keymap = function(prompt_bufnr, keymap_identifier) + local key_func = keymap_store[prompt_bufnr][keymap_identifier] + + assert(key_func, string.format("Unsure of how we got this failure: %s %s", prompt_bufnr, keymap_identifier)) + + key_func(prompt_bufnr) + vim.api.nvim_exec_autocmds("User TelescopeKeymap", {}) +end + +mappings.clear = function(prompt_bufnr) + keymap_store[prompt_bufnr] = nil +end + +return mappings diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers.lua new file mode 100644 index 0000000..5019abf --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers.lua @@ -0,0 +1,1566 @@ +require "telescope" + +local a = vim.api + +local async = require "plenary.async" +local await_schedule = async.util.scheduler +local channel = require("plenary.async.control").channel +local popup = require "plenary.popup" + +local actions = require "telescope.actions" +local config = require "telescope.config" +local debounce = require "telescope.debounce" +local deprecated = require "telescope.deprecated" +local log = require "telescope.log" +local mappings = require "telescope.mappings" +local state = require "telescope.state" +local utils = require "telescope.utils" + +local entry_display = require "telescope.pickers.entry_display" +local p_highlighter = require "telescope.pickers.highlights" +local p_scroller = require "telescope.pickers.scroller" +local p_window = require "telescope.pickers.window" + +local EntryManager = require "telescope.entry_manager" +local MultiSelect = require "telescope.pickers.multi" + +local truncate = require("plenary.strings").truncate +local strdisplaywidth = require("plenary.strings").strdisplaywidth + +local ns_telescope_matching = a.nvim_create_namespace "telescope_matching" +local ns_telescope_prompt = a.nvim_create_namespace "telescope_prompt" +local ns_telescope_prompt_prefix = a.nvim_create_namespace "telescope_prompt_prefix" + +local pickers = {} + +-- TODO: Add overscroll option for results buffer + +---@class Picker +--- Picker is the main UI that shows up to interact w/ your results. +-- Takes a filter & a previewer +local Picker = {} +Picker.__index = Picker + +--- Create new picker +function Picker:new(opts) + opts = opts or {} + + if opts.layout_strategy and opts.get_window_options then + error "layout_strategy and get_window_options are not compatible keys" + end + + if vim.fn.win_gettype() == "command" then + error "Can't open telescope from command-line window. See E11" + end + + deprecated.options(opts) + + -- We need to clear at the beginning not on close because after close we can still have select:post + -- etc ... + require("telescope.actions.mt").clear_all() + -- TODO(conni2461): This seems like the better solution but it won't clear actions that were never mapped + -- for _, v in ipairs(keymap_store[prompt_bufnr]) do + -- pcall(v.clear) + -- end + + local layout_strategy = vim.F.if_nil(opts.layout_strategy, config.values.layout_strategy) + + local obj = setmetatable({ + prompt_title = vim.F.if_nil(opts.prompt_title, config.values.prompt_title), + results_title = vim.F.if_nil(opts.results_title, config.values.results_title), + -- either whats passed in by the user or whats defined by the previewer + preview_title = opts.preview_title, + + prompt_prefix = vim.F.if_nil(opts.prompt_prefix, config.values.prompt_prefix), + wrap_results = vim.F.if_nil(opts.wrap_results, config.values.wrap_results), + selection_caret = vim.F.if_nil(opts.selection_caret, config.values.selection_caret), + entry_prefix = vim.F.if_nil(opts.entry_prefix, config.values.entry_prefix), + multi_icon = vim.F.if_nil(opts.multi_icon, config.values.multi_icon), + + initial_mode = vim.F.if_nil(opts.initial_mode, config.values.initial_mode), + _original_mode = vim.api.nvim_get_mode().mode, + debounce = vim.F.if_nil(tonumber(opts.debounce), nil), + + _finder_attached = true, + default_text = opts.default_text, + get_status_text = vim.F.if_nil(opts.get_status_text, config.values.get_status_text), + _on_input_filter_cb = opts.on_input_filter_cb or function() end, + + finder = assert(opts.finder, "Finder is required."), + sorter = opts.sorter or require("telescope.sorters").empty(), + + all_previewers = opts.previewer, + current_previewer_index = 1, + + default_selection_index = opts.default_selection_index, + + get_selection_window = vim.F.if_nil(opts.get_selection_window, config.values.get_selection_window), + + cwd = opts.cwd, + + _find_id = 0, + _completion_callbacks = type(opts._completion_callbacks) == "table" and opts._completion_callbacks or {}, + manager = (type(opts.manager) == "table" and getmetatable(opts.manager) == EntryManager) and opts.manager, + _multi = (type(opts._multi) == "table" and getmetatable(opts._multi) == getmetatable(MultiSelect:new())) + and opts._multi + or MultiSelect:new(), + + track = vim.F.if_nil(opts.track, false), + stats = {}, + + attach_mappings = opts.attach_mappings, + file_ignore_patterns = vim.F.if_nil(opts.file_ignore_patterns, config.values.file_ignore_patterns), + + scroll_strategy = vim.F.if_nil(opts.scroll_strategy, config.values.scroll_strategy), + sorting_strategy = vim.F.if_nil(opts.sorting_strategy, config.values.sorting_strategy), + tiebreak = vim.F.if_nil(opts.tiebreak, config.values.tiebreak), + selection_strategy = vim.F.if_nil(opts.selection_strategy, config.values.selection_strategy), + + push_cursor_on_edit = vim.F.if_nil(opts.push_cursor_on_edit, false), + push_tagstack_on_edit = vim.F.if_nil(opts.push_tagstack_on_edit, false), + + layout_strategy = layout_strategy, + layout_config = config.smarter_depth_2_extend(opts.layout_config or {}, config.values.layout_config or {}), + + __cycle_layout_list = vim.F.if_nil(opts.cycle_layout_list, config.values.cycle_layout_list), + + window = { + winblend = vim.F.if_nil( + opts.winblend, + type(opts.window) == "table" and opts.window.winblend or config.values.winblend + ), + border = vim.F.if_nil(opts.border, type(opts.window) == "table" and opts.window.border or config.values.border), + borderchars = vim.F.if_nil( + opts.borderchars, + type(opts.window) == "table" and opts.window.borderchars or config.values.borderchars + ), + }, + + cache_picker = config.resolve_table_opts(opts.cache_picker, vim.deepcopy(config.values.cache_picker)), + + __scrolling_limit = tonumber(vim.F.if_nil(opts.temp__scrolling_limit, 250)), + }, self) + + obj.get_window_options = opts.get_window_options or p_window.get_window_options + + if obj.all_previewers ~= nil and obj.all_previewers ~= false then + if obj.all_previewers[1] == nil then + obj.all_previewers = { obj.all_previewers } + end + obj.previewer = obj.all_previewers[1] + if obj.preview_title == nil then + obj.preview_title = obj.previewer:title(nil, config.values.dynamic_preview_title) + else + obj.fix_preview_title = true + end + else + obj.previewer = false + end + + local __hide_previewer = opts.__hide_previewer + if __hide_previewer then + obj.hidden_previewer = obj.previewer + obj.previewer = nil + else + obj.hidden_previewer = nil + end + + -- TODO: It's annoying that this is create and everything else is "new" + obj.scroller = p_scroller.create(obj.scroll_strategy, obj.sorting_strategy) + + obj.highlighter = p_highlighter.new(obj) + + if opts.on_complete then + for _, on_complete_item in ipairs(opts.on_complete) do + obj:register_completion_callback(on_complete_item) + end + end + + return obj +end + +--- Take an index and get a row. +---@note: Rows are 0-indexed, and `index` is 1 indexed (table index) +---@param index number: the index in line_manager +---@return number: the row for the picker to display in +function Picker:get_row(index) + if self.sorting_strategy == "ascending" then + return index - 1 + else + return self.max_results - index + end +end + +--- Take a row and get an index +---@note: Rows are 0-indexed, and `index` is 1 indexed (table index) +---@param row number: The row being displayed +---@return number: The index in line_manager +function Picker:get_index(row) + if self.sorting_strategy == "ascending" then + return row + 1 + else + return self.max_results - row + end +end + +--- Get the row number of the "best" entry +---@return number: the number of the "reset" row +function Picker:get_reset_row() + if self.sorting_strategy == "ascending" then + return 0 + else + return self.max_results - 1 + end +end + +--- Check if the picker is no longer in use +---@return boolean|nil: `true` if picker is closed, `nil` otherwise +function Picker:is_done() + if not self.manager then + return true + end +end + +--- Clear rows that are after the final remaining entry +---@note: useful when number of remaining results is narrowed down +---@param results_bufnr number: the buffer number of the results buffer +function Picker:clear_extra_rows(results_bufnr) + if self:is_done() then + log.trace "Not clearing due to being already complete" + return + end + + if not vim.api.nvim_buf_is_valid(results_bufnr) then + log.debug("Invalid results_bufnr for clearing:", results_bufnr) + return + end + + local worst_line, ok, msg + if self.sorting_strategy == "ascending" then + local num_results = self.manager:num_results() + worst_line = self.max_results - num_results + + if worst_line <= 0 then + return + end + + ok, msg = pcall(vim.api.nvim_buf_set_lines, results_bufnr, num_results, -1, false, {}) + else + worst_line = self:get_row(self.manager:num_results()) + if worst_line <= 0 then + return + end + + local empty_lines = utils.repeated_table(worst_line, "") + ok, msg = pcall(vim.api.nvim_buf_set_lines, results_bufnr, 0, worst_line, false, empty_lines) + end + + if not ok then + log.debug("Failed to set lines:", msg) + end + + log.trace("Clearing:", worst_line) +end + +--- Highlight the entry corresponding to the given row +---@param results_bufnr number: the buffer number of the results buffer +---@param prompt table: table with information about the prompt buffer +---@param display string: the text corresponding to the given row +---@param row number: the number of the chosen row +function Picker:highlight_one_row(results_bufnr, prompt, display, row) + if not self.sorter.highlighter then + return + end + + local highlights = self.sorter:highlighter(prompt, display) + + if highlights then + for _, hl in ipairs(highlights) do + local highlight, start, finish + if type(hl) == "table" then + highlight = hl.highlight or "TelescopeMatching" + start = hl.start + finish = hl.finish or hl.start + elseif type(hl) == "number" then + highlight = "TelescopeMatching" + start = hl + finish = hl + else + error "Invalid higlighter fn" + end + + self:_increment "highlights" + + vim.api.nvim_buf_add_highlight(results_bufnr, ns_telescope_matching, highlight, row, start - 1, finish) + end + end + + local entry = self.manager:get_entry(self:get_index(row)) + self.highlighter:hi_multiselect(row, self:is_multi_selected(entry)) +end + +--- Check if the given row number can be selected +---@param row number: the number of the chosen row in the results buffer +---@return boolean +function Picker:can_select_row(row) + if self.sorting_strategy == "ascending" then + return row <= self.manager:num_results() and row < self.max_results + else + return row >= 0 and row <= self.max_results and row >= self.max_results - self.manager:num_results() + end +end + +--TODO: document what `find_id` is for +function Picker:_next_find_id() + local find_id = self._find_id + 1 + self._find_id = find_id + + return find_id +end + +--- A helper function for creating each of the windows in a picker +---@param bufnr number: the buffer number to be used in the window +---@param popup_opts table: options to pass to `popup.create` +---@param nowrap boolean: is |'wrap'| disabled in the created window +function Picker:_create_window(bufnr, popup_opts, nowrap) + local what = bufnr or "" + local win, opts = popup.create(what, popup_opts) + + a.nvim_win_set_option(win, "winblend", self.window.winblend) + a.nvim_win_set_option(win, "wrap", not nowrap) + local border_win = opts and opts.border and opts.border.win_id + if border_win then + a.nvim_win_set_option(border_win, "winblend", self.window.winblend) + end + return win, opts, border_win +end + +--- Opens the given picker for the user to interact with +---@note: this is the main function for pickers, as it actually creates the interface for users +function Picker:find() + self:close_existing_pickers() + self:reset_selection() + + self.original_win_id = a.nvim_get_current_win() + + -- User autocmd run it before create Telescope window + vim.api.nvim_exec_autocmds("User TelescopeFindPre", {}) + + -- Create three windows: + -- 1. Prompt window + -- 2. Options window + -- 3. Preview window + + local line_count = vim.o.lines - vim.o.cmdheight + if vim.o.laststatus ~= 0 then + line_count = line_count - 1 + end + + local popup_opts = self:get_window_options(vim.o.columns, line_count) + + -- `popup.nvim` massaging so people don't have to remember minheight shenanigans + popup_opts.results.minheight = popup_opts.results.height + popup_opts.results.highlight = "TelescopeResultsNormal" + popup_opts.results.borderhighlight = "TelescopeResultsBorder" + popup_opts.results.titlehighlight = "TelescopeResultsTitle" + popup_opts.prompt.minheight = popup_opts.prompt.height + popup_opts.prompt.highlight = "TelescopePromptNormal" + popup_opts.prompt.borderhighlight = "TelescopePromptBorder" + popup_opts.prompt.titlehighlight = "TelescopePromptTitle" + if popup_opts.preview then + popup_opts.preview.minheight = popup_opts.preview.height + popup_opts.preview.highlight = "TelescopePreviewNormal" + popup_opts.preview.borderhighlight = "TelescopePreviewBorder" + popup_opts.preview.titlehighlight = "TelescopePreviewTitle" + end + + local results_win, results_opts, results_border_win = + self:_create_window("", popup_opts.results, not self.wrap_results) + + local results_bufnr = a.nvim_win_get_buf(results_win) + pcall(a.nvim_buf_set_option, results_bufnr, "tabstop", 1) -- #1834 + + self.results_bufnr = results_bufnr + self.results_win = results_win + self.results_border = results_opts and results_opts.border + + local preview_win, preview_opts, preview_bufnr, preview_border_win + if popup_opts.preview then + preview_win, preview_opts, preview_border_win = self:_create_window("", popup_opts.preview) + preview_bufnr = a.nvim_win_get_buf(preview_win) + end + -- This is needed for updating the title + local preview_border = preview_opts and preview_opts.border + self.preview_win = preview_win + self.preview_border = preview_border + + local prompt_win, prompt_opts, prompt_border_win = self:_create_window("", popup_opts.prompt) + local prompt_bufnr = a.nvim_win_get_buf(prompt_win) + pcall(a.nvim_buf_set_option, prompt_bufnr, "tabstop", 1) -- #1834 + + self.prompt_bufnr = prompt_bufnr + self.prompt_win = prompt_win + self.prompt_border = prompt_opts and prompt_opts.border + + -- Prompt prefix + local prompt_prefix = self.prompt_prefix + a.nvim_buf_set_option(prompt_bufnr, "buftype", "prompt") + vim.fn.prompt_setprompt(prompt_bufnr, prompt_prefix) + self.prompt_prefix = prompt_prefix + self:_reset_prefix_color() + + -- TODO: This could be configurable in the future, but I don't know why you would + -- want to scroll through more than 10,000 items. + -- + -- This just lets us stop doing stuff after tons of things. + self.max_results = self.__scrolling_limit + + vim.api.nvim_buf_set_lines(results_bufnr, 0, self.max_results, false, utils.repeated_table(self.max_results, "")) + + local status_updater = self:get_status_updater(prompt_win, prompt_bufnr) + local debounced_status = debounce.throttle_leading(status_updater, 50) + + local tx, rx = channel.mpsc() + self._on_lines = tx.send + + local find_id = self:_next_find_id() + + if self.default_text then + self:set_prompt(self.default_text) + end + + if vim.tbl_contains({ "insert", "normal" }, self.initial_mode) then + local mode = vim.fn.mode() + local keys + if self.initial_mode == "normal" then + -- n: A makes sure cursor is at always at end of prompt w/o default_text + keys = mode ~= "n" and "A" or "A" + else + -- always fully retrigger insert mode: required for going from one picker to next + keys = mode ~= "n" and "A" or "A" + end + a.nvim_feedkeys(a.nvim_replace_termcodes(keys, true, false, true), "n", true) + else + utils.notify( + "pickers.find", + { msg = "`initial_mode` should be one of ['normal', 'insert'] but passed " .. self.initial_mode, level = "ERROR" } + ) + end + + local main_loop = async.void(function() + self.sorter:_init() + + -- Do filetype last, so that users can register at the last second. + pcall(a.nvim_buf_set_option, prompt_bufnr, "filetype", "TelescopePrompt") + pcall(a.nvim_buf_set_option, results_bufnr, "filetype", "TelescopeResults") + + await_schedule() + + while true do + -- Wait for the next input + rx.last() + await_schedule() + + self:_reset_track() + + if not vim.api.nvim_buf_is_valid(prompt_bufnr) then + log.debug("ON_LINES: Invalid prompt_bufnr", prompt_bufnr) + return + end + + local start_time = vim.loop.hrtime() + + local prompt = self:_get_next_filtered_prompt() + + -- TODO: Entry manager should have a "bulk" setter. This can prevent a lot of redraws from display + if self.cache_picker == false or self.cache_picker.is_cached ~= true then + self.sorter:_start(prompt) + self.manager = EntryManager:new(self.max_results, self.entry_adder, self.stats) + + self:_reset_highlights() + local process_result = self:get_result_processor(find_id, prompt, debounced_status) + local process_complete = self:get_result_completor(self.results_bufnr, find_id, prompt, status_updater) + + local ok, msg = pcall(function() + self.finder(prompt, process_result, process_complete) + end) + + if not ok then + log.warn("Finder failed with msg: ", msg) + end + + local diff_time = (vim.loop.hrtime() - start_time) / 1e6 + if self.debounce and diff_time < self.debounce then + async.util.sleep(self.debounce - diff_time) + end + else + -- TODO(scroll): This can only happen once, I don't like where it is. + self:_resume_picker() + end + end + end) + + -- Register attach + vim.api.nvim_buf_attach(prompt_bufnr, false, { + on_lines = function(...) + if self._finder_attached then + find_id = self:_next_find_id() + + status_updater { completed = false } + self._on_lines(...) + end + end, + + on_detach = function() + self:_detach() + end, + }) + + vim.api.nvim_create_augroup("PickerInsert", {}) + -- TODO: Use WinLeave as well? + vim.api.nvim_create_autocmd("BufLeave", { + buffer = prompt_bufnr, + group = "PickerInsert", + nested = true, + once = true, + callback = function() + require("telescope.pickers").on_close_prompt(prompt_bufnr) + end, + }) + vim.api.nvim_create_autocmd("VimResized", { + buffer = prompt_bufnr, + group = "PickerInsert", + nested = true, + callback = function() + require("telescope.pickers").on_resize_window(prompt_bufnr) + end, + }) + + self.prompt_bufnr = prompt_bufnr + + state.set_status( + prompt_bufnr, + setmetatable({ + prompt_bufnr = prompt_bufnr, + prompt_win = prompt_win, + prompt_border_win = prompt_border_win, + + results_bufnr = results_bufnr, + results_win = results_win, + results_border_win = results_border_win, + + preview_bufnr = preview_bufnr, + preview_win = preview_win, + preview_border_win = preview_border_win, + picker = self, + }, { + __mode = "kv", + }) + ) + + mappings.apply_keymap(prompt_bufnr, self.attach_mappings, config.values.mappings) + + tx.send() + main_loop() +end + +--- A helper function to update picker windows when layout options are changed +function Picker:recalculate_layout() + local line_count = vim.o.lines - vim.o.cmdheight + if vim.o.laststatus ~= 0 then + line_count = line_count - 1 + end + + local popup_opts = self:get_window_options(vim.o.columns, line_count) + -- `popup.nvim` massaging so people don't have to remember minheight shenanigans + popup_opts.results.minheight = popup_opts.results.height + popup_opts.prompt.minheight = popup_opts.prompt.height + if popup_opts.preview then + popup_opts.preview.minheight = popup_opts.preview.height + end + + local status = state.get_status(self.prompt_bufnr) + + local prompt_win = status.prompt_win + local results_win = status.results_win + local preview_win = status.preview_win + + local preview_opts, preview_border_win + if popup_opts.preview then + if preview_win ~= nil then + -- Move all popups at the same time + popup.move(prompt_win, popup_opts.prompt) + popup.move(results_win, popup_opts.results) + popup.move(preview_win, popup_opts.preview) + else + popup_opts.preview.highlight = "TelescopePreviewNormal" + popup_opts.preview.borderhighlight = "TelescopePreviewBorder" + popup_opts.preview.titlehighlight = "TelescopePreviewTitle" + local preview_bufnr = status.preview_bufnr ~= nil + and vim.api.nvim_buf_is_valid(status.preview_bufnr) + and status.preview_bufnr + or "" + preview_win, preview_opts, preview_border_win = self:_create_window(preview_bufnr, popup_opts.preview) + if preview_bufnr == "" then + preview_bufnr = a.nvim_win_get_buf(preview_win) + end + status.preview_win = preview_win + status.preview_bufnr = preview_bufnr + status.preview_border_win = preview_border_win + state.set_status(prompt_win, status) + self.preview_win = preview_win + self.preview_border_win = preview_border_win + self.preview_border = preview_opts and preview_opts.border + if self.previewer and self.previewer.state and self.previewer.state.winid then + self.previewer.state.winid = preview_win + end + + -- Move prompt and results after preview created + vim.defer_fn(function() + popup.move(prompt_win, popup_opts.prompt) + popup.move(results_win, popup_opts.results) + end, 0) + end + elseif preview_win ~= nil then + popup.move(prompt_win, popup_opts.prompt) + popup.move(results_win, popup_opts.results) + + -- Remove preview after the prompt and results are moved + vim.defer_fn(function() + utils.win_delete("preview_win", preview_win, true) + utils.win_delete("preview_win", status.preview_border_win, true) + status.preview_win = nil + status.preview_border_win = nil + state.set_status(prompt_win, status) + self.preview_win = nil + self.preview_border_win = nil + self.preview_border = nil + end, 0) + else + popup.move(prompt_win, popup_opts.prompt) + popup.move(results_win, popup_opts.results) + end + + -- Temporarily disabled: Draw the screen ASAP. This makes things feel speedier. + -- vim.cmd [[redraw]] + + -- self.max_results = popup_opts.results.height +end + +local update_scroll = function(win, oldinfo, oldcursor, strategy, buf_maxline) + if strategy == "ascending" then + vim.api.nvim_win_set_cursor(win, { buf_maxline, 0 }) + vim.api.nvim_win_set_cursor(win, { oldinfo.topline, 0 }) + vim.api.nvim_win_set_cursor(win, oldcursor) + elseif strategy == "descending" then + vim.api.nvim_win_set_cursor(win, { 1, 0 }) + vim.api.nvim_win_set_cursor(win, { oldinfo.botline, 0 }) + vim.api.nvim_win_set_cursor(win, oldcursor) + else + error(debug.traceback("Unknown sorting strategy: " .. (strategy or ""))) + end +end + +--- A wrapper for `Picker:recalculate_layout()` that also handles maintaining cursor position +function Picker:full_layout_update() + local oldinfo = vim.fn.getwininfo(self.results_win)[1] + local oldcursor = vim.api.nvim_win_get_cursor(self.results_win) + self:recalculate_layout() + self:refresh_previewer() + + -- update scrolled position + local buf_maxline = #vim.api.nvim_buf_get_lines(self.results_bufnr, 0, -1, false) + update_scroll(self.results_win, oldinfo, oldcursor, self.sorting_strategy, buf_maxline) +end + +-- TODO: update multi-select with the correct tag name when available +--- A simple interface to remove an entry from the results window without +--- closing telescope. This either deletes the current selection or all the +--- selections made using multi-select. It can be used to define actions +--- such as deleting buffers or files. +--- +--- Example usage: +--- +--- actions.delete_something = function(prompt_bufnr) +--- local current_picker = action_state.get_current_picker(prompt_bufnr) +--- current_picker:delete_selection(function(selection) +--- -- delete the selection outside of telescope +--- end) +--- end +--- +--- +--- Example usage in telescope: +--- - `actions.delete_buffer()` +---@param delete_cb function: called for each selection fn(s) -> bool|nil (true|nil removes the entry from the results) +function Picker:delete_selection(delete_cb) + vim.validate { delete_cb = { delete_cb, "f" } } + local original_selection_strategy = self.selection_strategy + self.selection_strategy = "row" + + local delete_selections = self._multi:get() + local used_multi_select = true + if vim.tbl_isempty(delete_selections) then + table.insert(delete_selections, self:get_selection()) + used_multi_select = false + end + + local selection_index = {} + for result_index, result_entry in ipairs(self.finder.results) do + if vim.tbl_contains(delete_selections, result_entry) then + table.insert(selection_index, result_index) + end + end + + -- Sort in reverse order as removing an entry from the table shifts down the + -- other elements to close the hole. + table.sort(selection_index, function(x, y) + return x > y + end) + for _, index in ipairs(selection_index) do + local delete_cb_return = delete_cb(self.finder.results[index]) + if delete_cb_return == nil or delete_cb_return == true then + table.remove(self.finder.results, index) + end + end + + if used_multi_select then + self._multi = MultiSelect:new() + end + + self:refresh() + vim.defer_fn(function() + self.selection_strategy = original_selection_strategy + end, 50) +end + +function Picker:set_prompt(text) + self:reset_prompt(text) +end + +--- Closes the windows for the prompt, results and preview +---@param status table: table containing information on the picker +--- and associated windows. Generally obtained from `state.get_status` +function Picker.close_windows(status) + utils.win_delete("results_win", status.results_win, true, true) + utils.win_delete("preview_win", status.preview_win, true, true) + + utils.win_delete("prompt_border_win", status.prompt_border_win, true, true) + utils.win_delete("results_border_win", status.results_border_win, true, true) + utils.win_delete("preview_border_win", status.preview_border_win, true, true) + + -- we cant use win_delete. We first need to close and then delete the buffer + vim.api.nvim_win_close(status.prompt_win, true) + utils.buf_delete(status.prompt_bufnr) + + state.clear_status(status.prompt_bufnr) +end + +--- Get the entry table of the current selection +---@return table +function Picker:get_selection() + return self._selection_entry +end + +--- Get the row number of the current selection +---@return number +function Picker:get_selection_row() + if self._selection_row then + -- If the current row is no longer selectable than reduce it to num_results - 1, so the next selectable row. + -- This makes selection_strategy `row` work much better if the selected row is no longer part of the output. + --TODO(conni2461): Maybe this can be moved to scroller. (currently in a hotfix so not viable) + if self.selection_strategy == "row" then + local num_results = self.manager:num_results() + if self.sorting_strategy == "ascending" then + if self._selection_row >= num_results then + return num_results - 1 + end + else + local max = self.max_results - num_results + if self._selection_row < max then + return self.max_results - num_results + end + end + end + return self._selection_row + end + return self.max_results +end + +--- Move the current selection by `change` steps +---@param change number +function Picker:move_selection(change) + self:set_selection(self:get_selection_row() + change) +end + +--- Add the entry of the given row to the multi-select object +---@param row number: the number of the chosen row +function Picker:add_selection(row) + local entry = self.manager:get_entry(self:get_index(row)) + self._multi:add(entry) + + self:update_prefix(entry, row) + self:get_status_updater(self.prompt_win, self.prompt_bufnr)() + self.highlighter:hi_multiselect(row, true) +end + +--- Remove the entry of the given row to the multi-select object +---@param row number: the number of the chosen row +function Picker:remove_selection(row) + local entry = self.manager:get_entry(self:get_index(row)) + self._multi:drop(entry) + + self:update_prefix(entry, row) + self:get_status_updater(self.prompt_win, self.prompt_bufnr)() + self.highlighter:hi_multiselect(row, false) +end + +--- Check if the given row is in the multi-select object +---@param entry table: table with information about the chosen entry +---@return number: the "count" associated to the entry in the multi-select +--- object (if present), `nil` otherwise +function Picker:is_multi_selected(entry) + return self._multi:is_selected(entry) +end + +--- Get a table containing all of the currently selected entries +---@return table: an integer indexed table of selected entries +function Picker:get_multi_selection() + return self._multi:get() +end + +--- Toggle the given row in and out of the multi-select object. +--- Also updates the highlighting for the given entry +---@param row number: the number of the chosen row +function Picker:toggle_selection(row) + local entry = self.manager:get_entry(self:get_index(row)) + if entry == nil then + return + end + self._multi:toggle(entry) + + self:update_prefix(entry, row) + self:get_status_updater(self.prompt_win, self.prompt_bufnr)() + self.highlighter:hi_multiselect(row, self._multi:is_selected(entry)) +end + +--- Set the current selection to `nil` +---@note: generally used when a picker is first activated with `find()` +function Picker:reset_selection() + self._selection_entry = nil + self._selection_row = nil +end + +function Picker:_reset_prefix_color(hl_group) + self._current_prefix_hl_group = hl_group or nil + + if self.prompt_prefix ~= "" then + vim.api.nvim_buf_add_highlight( + self.prompt_bufnr, + ns_telescope_prompt_prefix, + self._current_prefix_hl_group or "TelescopePromptPrefix", + 0, + 0, + #self.prompt_prefix + ) + end +end + +-- TODO(conni2461): Maybe _ prefix these next two functions +-- TODO(conni2461): Next two functions only work together otherwise color doesn't work +-- Probably a issue with prompt buffers +--- Change the prefix in the prompt to be `new_prefix` and apply `hl_group` +---@param new_prefix string: the string to be used as the new prefix +---@param hl_group string: the name of the chosen highlight +function Picker:change_prompt_prefix(new_prefix, hl_group) + if not new_prefix then + return + end + + if new_prefix ~= "" then + vim.fn.prompt_setprompt(self.prompt_bufnr, new_prefix) + else + vim.api.nvim_buf_set_text(self.prompt_bufnr, 0, 0, 0, #self.prompt_prefix, {}) + vim.api.nvim_buf_set_option(self.prompt_bufnr, "buftype", "") + end + self.prompt_prefix = new_prefix + self:_reset_prefix_color(hl_group) +end + +--- Reset the prompt to the provided `text` +---@param text string +function Picker:reset_prompt(text) + local prompt_text = self.prompt_prefix .. (text or "") + vim.api.nvim_buf_set_lines(self.prompt_bufnr, 0, -1, false, { prompt_text }) + self:_reset_prefix_color(self._current_prefix_hl_group) + + if text then + vim.api.nvim_win_set_cursor(self.prompt_win, { 1, #prompt_text }) + end +end + +---@param finder finder: telescope finder (see telescope/finders.lua) +---@param opts table: options to pass when refreshing the picker +---@field new_prefix string|table: either as string or { new_string, hl_group } +---@field reset_prompt bool: whether to reset the prompt +---@field multi MultiSelect: multi-selection to persist upon renewing finder (see telescope/pickers/multi.lua) +function Picker:refresh(finder, opts) + opts = opts or {} + if opts.new_prefix then + local handle = type(opts.new_prefix) == "table" and unpack or function(x) + return x + end + self:change_prompt_prefix(handle(opts.new_prefix), opts.prefix_hl_group) + end + + if finder then + self.finder:close() + self.finder = finder + self._multi = vim.F.if_nil(opts.multi, MultiSelect:new()) + end + + -- reset already triggers finder loop + if opts.reset_prompt then + self:reset_prompt() + else + self._on_lines(nil, nil, nil, 0, 1) + end +end + +---Set the selection to the provided `row` +---@param row number +function Picker:set_selection(row) + if not self.manager then + return + end + + row = self.scroller(self.max_results, self.manager:num_results(), row) + + if not self:can_select_row(row) then + -- If the current selected row exceeds number of currently displayed + -- elements we have to reset it. Affects sorting_strategy = 'row'. + if not self:can_select_row(self:get_selection_row()) then + row = self:get_row(self.manager:num_results()) + else + log.trace("Cannot select row:", row, self.manager:num_results(), self.max_results) + return + end + end + + local results_bufnr = self.results_bufnr + if not a.nvim_buf_is_valid(results_bufnr) then + return + end + + if row > a.nvim_buf_line_count(results_bufnr) then + log.debug( + string.format("Should not be possible to get row this large %s %s", row, a.nvim_buf_line_count(results_bufnr)) + ) + + return + end + + local entry = self.manager:get_entry(self:get_index(row)) + state.set_global_key("selected_entry", entry) + + if not entry then + -- also refresh previewer when there is no entry selected, so the preview window is cleared + self._selection_entry = entry + self:refresh_previewer() + return + end + + local old_entry + + -- TODO: Probably should figure out what the rows are that made this happen... + -- Probably something with setting a row that's too high for this? + -- Not sure. + local set_ok, set_errmsg = pcall(function() + local prompt = self:_get_prompt() + + -- Check if previous selection is still visible + if self._selection_entry and self.manager:find_entry(self._selection_entry) then + -- Find old selection, and update prefix and highlights + old_entry = self._selection_entry + local old_row = self:get_row(self.manager:find_entry(old_entry)) + + self._selection_entry = entry + + if old_row >= 0 then + self:update_prefix(old_entry, old_row) + self.highlighter:hi_multiselect(old_row, self:is_multi_selected(old_entry)) + end + else + self._selection_entry = entry + end + + local caret = self:update_prefix(entry, row) + + local display, _ = entry_display.resolve(self, entry) + display = caret .. display + + -- TODO: You should go back and redraw the highlights for this line from the sorter. + -- That's the only smart thing to do. + if not a.nvim_buf_is_valid(results_bufnr) then + log.debug "Invalid buf somehow..." + return + end + + -- don't highlight any whitespace at the end of caret + self.highlighter:hi_selection(row, caret:match "(.*%S)") + self.highlighter:hi_sorter(row, prompt, display) + + self.highlighter:hi_multiselect(row, self:is_multi_selected(entry)) + end) + + if not set_ok then + log.debug(set_errmsg) + return + end + + if old_entry == entry and self._selection_row == row then + return + end + + -- TODO: Get row & text in the same obj + self._selection_entry = entry + self._selection_row = row + + self:refresh_previewer() + + vim.api.nvim_win_set_cursor(self.results_win, { row + 1, 0 }) +end + +--- Update prefix for entry on a given row +function Picker:update_prefix(entry, row) + local prefix = function(sel, multi) + local t + if sel then + t = self.selection_caret + else + t = self.entry_prefix + end + if multi and type(self.multi_icon) == "string" then + t = truncate(t, strdisplaywidth(t) - strdisplaywidth(self.multi_icon), "") .. self.multi_icon + end + return t + end + + local line = vim.api.nvim_buf_get_lines(self.results_bufnr, row, row + 1, false)[1] + if not line then + log.trace(string.format("no line found at row %d in buffer %d", row, self.results_bufnr)) + return + end + + local old_caret = string.sub(line, 0, #prefix(true)) == prefix(true) and prefix(true) + or string.sub(line, 0, #prefix(true, true)) == prefix(true, true) and prefix(true, true) + or string.sub(line, 0, #prefix(false)) == prefix(false) and prefix(false) + or string.sub(line, 0, #prefix(false, true)) == prefix(false, true) and prefix(false, true) + if old_caret == false then + log.warn(string.format("can't identify old caret in line: %s", line)) + return + end + + local pre = prefix(entry == self._selection_entry, self:is_multi_selected(entry)) + -- Only change the first couple characters, nvim_buf_set_text leaves the existing highlights + a.nvim_buf_set_text(self.results_bufnr, row, 0, row, #old_caret, { pre }) + return pre +end + +--- Refresh the previewer based on the current `status` of the picker +function Picker:refresh_previewer() + local status = state.get_status(self.prompt_bufnr) + if self.previewer and status.preview_win and a.nvim_win_is_valid(status.preview_win) then + self:_increment "previewed" + + self.previewer:preview(self._selection_entry, status) + if self.preview_border then + if self.fix_preview_title then + return + end + + local new_title = self.previewer:title(self._selection_entry, config.values.dynamic_preview_title) + if new_title ~= nil and new_title ~= self.preview_title then + self.preview_title = new_title + self.preview_border:change_title(new_title) + end + end + end +end + +function Picker:cycle_previewers(next) + local size = #self.all_previewers + if size == 1 then + return + end + + self.current_previewer_index = self.current_previewer_index + next + if self.current_previewer_index > size then + self.current_previewer_index = 1 + elseif self.current_previewer_index < 1 then + self.current_previewer_index = size + end + + if self.previewer then + self.previewer = self.all_previewers[self.current_previewer_index] + self:refresh_previewer() + elseif self.hidden_previewer then + self.hidden_previewer = self.all_previewers[self.current_previewer_index] + end +end + +--- Handler for when entries are added by `self.manager` +---@param index number: the index to add the entry at +---@param entry table: the entry that has been added to the manager +---@param insert boolean: whether the entry has been "inserted" or not +function Picker:entry_adder(index, entry, _, insert) + if not entry then + return + end + + local row = self:get_row(index) + + -- If it's less than 0, then we don't need to show it at all. + if row < 0 then + log.debug("ON_ENTRY: Weird row", row) + return + end + + local display, display_highlights = entry_display.resolve(self, entry) + if not display then + log.info("Weird entry", entry) + return + end + + -- This is the two spaces to manage the '> ' stuff. + -- Maybe someday we can use extmarks or floaty text or something to draw this and not insert here. + -- until then, insert two spaces + local prefix = self.entry_prefix + display = prefix .. display + + self:_increment "displayed" + + local offset = insert and 0 or 1 + if not vim.api.nvim_buf_is_valid(self.results_bufnr) then + log.debug "ON_ENTRY: Invalid buffer" + return + end + + -- TODO: Does this every get called? + -- local line_count = vim.api.nvim_win_get_height(self.results_win) + local line_count = vim.api.nvim_buf_line_count(self.results_bufnr) + if row > line_count then + return + end + + if insert then + if self.sorting_strategy == "descending" then + vim.api.nvim_buf_set_lines(self.results_bufnr, 0, 1, false, {}) + end + end + + local set_ok, msg = pcall(vim.api.nvim_buf_set_lines, self.results_bufnr, row, row + offset, false, { display }) + if set_ok then + if display_highlights then + self.highlighter:hi_display(row, prefix, display_highlights) + end + self:update_prefix(entry, row) + self:highlight_one_row(self.results_bufnr, self:_get_prompt(), display, row) + end + + if not set_ok then + log.debug("Failed to set lines...", msg) + end + + -- This pretty much only fails when people leave newlines in their results. + -- So we'll clean it up for them if it fails. + if not set_ok and display:find "\n" then + display = display:gsub("\n", " | ") + vim.api.nvim_buf_set_lines(self.results_bufnr, row, row + 1, false, { display }) + end +end + +--- Reset tracked information for this picker +function Picker:_reset_track() + self.stats.processed = 0 + self.stats.displayed = 0 + self.stats.display_fn = 0 + self.stats.previewed = 0 + self.stats.status = 0 + + self.stats.filtered = 0 + self.stats.highlights = 0 +end + +--- Increment the count of the tracked info at `self.stats[key]` +---@param key string +function Picker:_increment(key) + self.stats[key] = (self.stats[key] or 0) + 1 +end + +--- Decrement the count of the tracked info at `self.stats[key]` +---@param key string +function Picker:_decrement(key) + self.stats[key] = (self.stats[key] or 0) - 1 +end + +-- TODO: Decide how much we want to use this. +-- Would allow for better debugging of items. +function Picker:register_completion_callback(cb) + table.insert(self._completion_callbacks, cb) +end + +function Picker:clear_completion_callbacks() + self._completion_callbacks = {} +end + +function Picker:_on_complete() + for _, v in ipairs(self._completion_callbacks) do + pcall(v, self) + end +end + +--- Close all open Telescope pickers +function Picker:close_existing_pickers() + for _, prompt_bufnr in ipairs(state.get_existing_prompts()) do + pcall(actions.close, prompt_bufnr) + end +end + +--- Returns a function that sets virtual text for the count indicator +--- e.g. "10/50" as "filtered"/"processed" +---@param prompt_win number +---@param prompt_bufnr number +---@return function +function Picker:get_status_updater(prompt_win, prompt_bufnr) + return function(opts) + if self.closed or not vim.api.nvim_buf_is_valid(prompt_bufnr) then + return + end + + local current_prompt = self:_get_prompt() + if not current_prompt then + return + end + + if not vim.api.nvim_win_is_valid(prompt_win) then + return + end + + local text = self:get_status_text(opts) + vim.api.nvim_buf_clear_namespace(prompt_bufnr, ns_telescope_prompt, 0, -1) + vim.api.nvim_buf_set_extmark(prompt_bufnr, ns_telescope_prompt, 0, 0, { + virt_text = { { text, "TelescopePromptCounter" } }, + virt_text_pos = "right_align", + }) + + self:_increment "status" + end +end + +--- Returns a function that will process an element. +--- Returned function handles updating the "filtered" and "processed" counts +--- as appropriate and runs the sorters score function +---@param find_id number +---@param prompt string +---@param status_updater function +---@return function +function Picker:get_result_processor(find_id, prompt, status_updater) + local count = 0 + + local cb_add = function(score, entry) + -- may need the prompt for tiebreak + self.manager:add_entry(self, score, entry, prompt) + status_updater { completed = false } + end + + local cb_filter = function(_) + self:_increment "filtered" + end + + return function(entry) + if find_id ~= self._find_id then + return true + end + + if not entry or entry.valid == false then + return + end + + self:_increment "processed" + + count = count + 1 + + -- TODO: Probably should asyncify this / cache this / do something because this probably takes + -- a ton of time on large results. + log.trace("Processing result... ", entry) + for _, v in ipairs(self.file_ignore_patterns or {}) do + local file = vim.F.if_nil(entry.filename, type(entry.value) == "string" and entry.value) -- false if none is true + if file then + if string.find(file, v) then + log.trace("SKIPPING", entry.value, "because", v) + self:_decrement "processed" + return + end + end + end + + self.sorter:score(prompt, entry, cb_add, cb_filter) + end +end + +--- Handles updating the picker after all the entries are scored/processed. +---@param results_bufnr number +---@param find_id number +---@param prompt string +---@param status_updater function +function Picker:get_result_completor(results_bufnr, find_id, prompt, status_updater) + return vim.schedule_wrap(function() + if self.closed == true or self:is_done() then + return + end + + self:_do_selection(prompt) + + state.set_global_key("current_line", self:_get_prompt()) + status_updater { completed = true } + + self:clear_extra_rows(results_bufnr) + self.sorter:_finish(prompt) + + self:_on_complete() + end) +end + +function Picker:_do_selection(prompt) + local selection_strategy = self.selection_strategy or "reset" + -- TODO: Either: always leave one result or make sure we actually clean up the results when nothing matches + if selection_strategy == "row" then + if self._selection_row == nil and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + self:set_selection(self:get_selection_row()) + end + elseif selection_strategy == "follow" then + if self._selection_row == nil and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + local index = self.manager:find_entry(self:get_selection()) + + if index then + local follow_row = self:get_row(index) + self:set_selection(follow_row) + else + self:set_selection(self:get_reset_row()) + end + end + elseif selection_strategy == "reset" then + if self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + self:set_selection(self:get_reset_row()) + end + elseif selection_strategy == "closest" then + if prompt == "" and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + self:set_selection(self:get_reset_row()) + end + elseif selection_strategy == "none" then + if self._selection_entry then + local old_entry, old_row = self._selection_entry, self._selection_row + self:reset_selection() -- required to reset selection before updating prefix + if old_row >= 0 then + self:update_prefix(old_entry, old_row) + self.highlighter:hi_multiselect(old_row, self:is_multi_selected(old_entry)) + end + end + return + else + error("Unknown selection strategy: " .. selection_strategy) + end +end + +--- Wrapper function for `Picker:new` that incorporates user provided `opts` +--- with the telescope `defaults` +---@param opts table +---@param defaults table +---@return Picker +pickers.new = function(opts, defaults) + opts = opts or {} + defaults = defaults or {} + local result = {} + + for k, v in pairs(opts) do + assert(type(k) == "string" or type(k) == "number", "Should be string or number, found: " .. type(k)) + result[k] = v + end + + for k, v in pairs(defaults) do + if result[k] == nil then + assert(type(k) == "string", "Should be string, defaults") + result[k] = v + else + -- For attach mappings, we want people to be able to pass in another function + -- and apply their mappings after we've applied our defaults. + if k == "attach_mappings" then + local opt_value = result[k] + result[k] = function(...) + v(...) + return opt_value(...) + end + end + end + end + + if result["previewer"] == false then + result["previewer"] = defaults["previewer"] + result["__hide_previewer"] = true + elseif type(opts["preview"]) == "table" and opts["preview"]["hide_on_startup"] then + result["__hide_previewer"] = true + end + + return Picker:new(result) +end + +--- Close the picker which has prompt with buffer number `prompt_bufnr` +---@param prompt_bufnr number +function pickers.on_close_prompt(prompt_bufnr) + local status = state.get_status(prompt_bufnr) + local picker = status.picker + require("telescope.actions.state").get_current_history():reset() + + if type(picker.cache_picker) == "table" then + local cached_pickers = state.get_global_key "cached_pickers" or {} + + if type(picker.cache_picker.index) == "number" then + if not vim.tbl_isempty(cached_pickers) then + table.remove(cached_pickers, picker.cache_picker.index) + end + end + + -- if picker was disabled post-hoc (e.g. `cache_picker = false` conclude after deletion) + if picker.cache_picker.disabled ~= true then + if picker.cache_picker.limit_entries > 0 then + -- edge case: starting in normal mode and not having run a search means having no manager instantiated + if picker.manager then + picker.manager.linked_states:truncate(picker.cache_picker.limit_entries) + else + picker.manager = EntryManager:new(picker.max_results, picker.entry_adder, picker.stats) + end + end + picker.default_text = picker:_get_prompt() + picker.cache_picker.selection_row = picker._selection_row + picker.cache_picker.cached_prompt = picker:_get_prompt() + picker.cache_picker.is_cached = true + table.insert(cached_pickers, 1, picker) + + -- release pickers + if picker.cache_picker.num_pickers > 0 then + while #cached_pickers > picker.cache_picker.num_pickers do + table.remove(cached_pickers, #cached_pickers) + end + end + state.set_global_key("cached_pickers", cached_pickers) + end + end + + if picker.sorter then + picker.sorter:_destroy() + end + + if picker.all_previewers then + for _, v in ipairs(picker.all_previewers) do + v:teardown() + end + end + + if picker.finder then + picker.finder:close() + end + + -- so we dont call close_windows multiple times we clear that autocmd + vim.api.nvim_clear_autocmds { + group = "PickerInsert", + event = "BufLeave", + buffer = prompt_bufnr, + } + picker.close_windows(status) + mappings.clear(prompt_bufnr) +end + +function pickers.on_resize_window(prompt_bufnr) + local status = state.get_status(prompt_bufnr) + local picker = status.picker + + picker:full_layout_update() +end + +--- Get the prompt text without the prompt prefix. +function Picker:_get_prompt() + return vim.api.nvim_buf_get_lines(self.prompt_bufnr, 0, 1, false)[1]:sub(#self.prompt_prefix + 1) +end + +function Picker:_reset_highlights() + self.highlighter:clear_display() + vim.api.nvim_buf_clear_namespace(self.results_bufnr, ns_telescope_matching, 0, -1) +end + +-- Toggles whether finder is attached to prompt buffer input +function Picker:_toggle_finder_attach() + self._finder_attached = not self._finder_attached +end + +function Picker:_detach() + self.finder:close() + + -- TODO: Can we add a "cleanup" / "teardown" function that completely removes these. + -- self.finder = nil + -- self.previewer = nil + -- self.sorter = nil + -- self.manager = nil + + self.closed = true +end + +function Picker:_get_next_filtered_prompt() + local prompt = self:_get_prompt() + local on_input_result = self._on_input_filter_cb(prompt) or {} + + local new_prompt = on_input_result.prompt + if new_prompt then + prompt = new_prompt + end + + local new_finder = on_input_result.updated_finder + if new_finder then + self.finder:close() + self.finder = new_finder + end + + return prompt +end + +function Picker:_resume_picker() + -- resume previous picker + local index = 1 + for entry in self.manager:iter() do + self:entry_adder(index, entry, _, true) + index = index + 1 + end + self.cache_picker.is_cached = false + -- if text changed, required to set anew to restart finder; otherwise hl and selection + if self.cache_picker.cached_prompt ~= self.default_text then + self:set_prompt(self.default_text) + else + -- scheduling required to apply highlighting and selection appropriately + await_schedule(function() + if self.cache_picker.selection_row ~= nil then + self:set_selection(self.cache_picker.selection_row) + end + end) + end +end + +pickers._Picker = Picker + +return pickers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/_test.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/_test.lua new file mode 100644 index 0000000..a8680d0 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/_test.lua @@ -0,0 +1,244 @@ +local assert = require "luassert" +local builtin = require "telescope.builtin" +local log = require "telescope.log" + +local Job = require "plenary.job" +local Path = require "plenary.path" + +local tester = {} + +tester.debug = false + +local replace_terms = function(input) + return vim.api.nvim_replace_termcodes(input, true, false, true) +end + +local nvim_feed = function(text, feed_opts) + feed_opts = feed_opts or "m" + + vim.api.nvim_feedkeys(text, feed_opts, true) +end + +local writer = function(val) + if type(val) == "table" then + val = vim.json.encode(val) .. "\n" + end + + if tester.debug then + print(val) + else + io.stderr:write(val) + end +end + +local execute_test_case = function(location, key, spec) + local ok, actual = pcall(spec[2]) + + if not ok then + writer { + location = "Error: " .. location, + case = key, + expected = "To succeed and return: " .. tostring(spec[1]), + actual = actual, + + _type = spec._type, + } + else + writer { + location = location, + case = key, + expected = spec[1], + actual = actual, + + _type = spec._type, + } + end +end + +local end_test_cases = function() + vim.cmd [[qa!]] +end + +local invalid_test_case = function(k) + writer { case = k, expected = "", actual = k } + + end_test_cases() +end + +tester.picker_feed = function(input, test_cases) + input = replace_terms(input) + + return coroutine.wrap(function() + for i = 1, #input do + local char = input:sub(i, i) + nvim_feed(char, "") + + -- TODO: I'm not 100% sure this is a hack or not... + -- it's possible these characters could still have an on_complete... but i'm not sure. + if string.match(char, "%g") then + coroutine.yield() + end + + if tester.debug then + vim.wait(200) + end + end + + vim.wait(10) + + if tester.debug then + coroutine.yield() + end + + vim.defer_fn(function() + if test_cases.post_typed then + for k, v in ipairs(test_cases.post_typed) do + execute_test_case("post_typed", k, v) + end + end + + nvim_feed(replace_terms "", "") + end, 20) + + vim.defer_fn(function() + if test_cases.post_close then + for k, v in ipairs(test_cases.post_close) do + execute_test_case("post_close", k, v) + end + end + + if tester.debug then + return + end + + vim.defer_fn(end_test_cases, 20) + end, 40) + + coroutine.yield() + end) +end + +local _VALID_KEYS = { + post_typed = true, + post_close = true, +} + +tester.builtin_picker = function(builtin_key, input, test_cases, opts) + opts = opts or {} + tester.debug = opts.debug or false + + for k, _ in pairs(test_cases) do + if not _VALID_KEYS[k] then + return invalid_test_case(k) + end + end + + opts.on_complete = { + tester.picker_feed(input, test_cases), + } + + builtin[builtin_key](opts) +end + +local get_results_from_file = function(file) + local j = Job:new { + command = "nvim", + args = { + "--noplugin", + "-u", + "scripts/minimal_init.vim", + "-c", + string.format([[lua require("telescope.pickers._test")._execute("%s")]], file), + }, + } + + j:sync(10000) + + local results = j:stderr_result() + local result_table = {} + for _, v in ipairs(results) do + table.insert(result_table, vim.json.decode(v)) + end + + return result_table +end + +local asserters = { + _default = assert.are.same, + + are = assert.are.same, + are_not = assert.are_not.same, +} + +local check_results = function(results) + -- TODO: We should get all the test cases here that fail, not just the first one. + for _, v in ipairs(results) do + local assertion = asserters[v._type or "default"] + + assertion(v.expected, v.actual, string.format("Test Case: %s // %s", v.location, v.case)) + end +end + +tester.run_string = function(contents) + local tempname = vim.fn.tempname() + + contents = [[ + local tester = require('telescope.pickers._test') + local helper = require('telescope.pickers._test_helpers') + + helper.make_globals() + ]] .. contents + + vim.fn.writefile(vim.split(contents, "\n"), tempname) + local result_table = get_results_from_file(tempname) + vim.fn.delete(tempname) + + check_results(result_table) +end + +tester.run_file = function(filename) + local file = "./lua/tests/pickers/" .. filename .. ".lua" + + if not Path:new(file):exists() then + assert.are.same("", file) + end + + local result_table = get_results_from_file(file) + + check_results(result_table) +end + +tester.not_ = function(val) + val._type = "are_not" + return val +end + +tester._execute = function(filename) + -- Important so that the outputs don't get mixed + log.use_console = false + + vim.cmd(string.format("luafile %s", filename)) + + local f = loadfile(filename) + if not f then + writer { + location = "Error: " .. filename, + case = filename, + expected = "To succeed", + actual = nil, + } + end + + local ok, msg = pcall(f) + if not ok then + writer { + location = "Error: " .. msg, + case = msg, + expected = msg, + } + end + + end_test_cases() +end + +return tester diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/entry_display.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/entry_display.lua new file mode 100644 index 0000000..6e520f4 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/entry_display.lua @@ -0,0 +1,169 @@ +---@tag telescope.pickers.entry_display + +---@brief [[ +--- Entry Display is used to format each entry shown in the result panel. +--- +--- Entry Display create() will give us a function based on the configuration +--- of column widths we pass into it. We then can use this function n times to +--- return a string based on structured input. +--- +--- Note that if you call `create()` inside `make_display` it will be called for +--- every single entry. So it is suggested to do this outside of `make_display` +--- for the best performance. +--- +--- The create function will use the column widths passed to it in +--- configaration.items. Each item in that table is the number of characters in +--- the column. It's also possible for the final column to not have a fixed +--- width, this will be shown in the configuartion as 'remaining = true'. +--- +--- An example of this configuration is shown for the buffers picker +--- +--- local displayer = entry_display.create { +--- separator = " ", +--- items = { +--- { width = opts.bufnr_width }, +--- { width = 4 }, +--- { width = icon_width }, +--- { remaining = true }, +--- }, +--- } +--- +--- +--- This shows 4 columns, the first is defined in the opts as the width we'll +--- use when display_string the number of the buffer. The second has a fixed +--- width of 4 and the 3rd column's widht will be decided by the width of the +--- icons we use. The fourth column will use the remaining space. Finally we +--- have also defined the seperator between each column will be the space " ". +--- +--- An example of how the display reference will be used is shown, again for +--- the buffers picker: +--- +--- return displayer { +--- { entry.bufnr, "TelescopeResultsNumber" }, +--- { entry.indicator, "TelescopeResultsComment" }, +--- { icon, hl_group }, +--- display_bufname .. ":" .. entry.lnum, +--- } +--- +--- +--- There are two types of values each column can have. Either a simple String +--- or a table containing the String as well as the hl_group. +--- +--- The displayer can return values, string and an optional highlights. +--- String is all the text to be displayed for this entry as a single string. If +--- parts of the string are to be highlighted they will be described in the +--- highlights table. +--- +--- For better understanding of how create() and displayer are used it's best to look +--- at the code in make_entry.lua. +---@brief ]] + +local strings = require "plenary.strings" +local state = require "telescope.state" +local resolve = require "telescope.config.resolve" + +local entry_display = {} +entry_display.truncate = strings.truncate + +entry_display.create = function(configuration) + local generator = {} + for _, v in ipairs(configuration.items) do + if v.width then + local justify = v.right_justify + local width + table.insert(generator, function(item) + if width == nil then + local status = state.get_status(vim.F.if_nil(configuration.prompt_bufnr, vim.api.nvim_get_current_buf())) + local s = {} + s[1] = vim.api.nvim_win_get_width(status.results_win) - #status.picker.selection_caret + s[2] = vim.api.nvim_win_get_height(status.results_win) + width = resolve.resolve_width(v.width)(nil, s[1], s[2]) + end + if type(item) == "table" then + return strings.align_str(entry_display.truncate(item[1], width), width, justify), item[2] + else + return strings.align_str(entry_display.truncate(item, width), width, justify) + end + end) + else + table.insert(generator, function(item) + if type(item) == "table" then + return item[1], item[2] + else + return item + end + end) + end + end + + return function(self, picker) + local results = {} + local highlights = {} + for i = 1, #generator do + if self[i] ~= nil then + local str, hl = generator[i](self[i], picker) + if hl then + local hl_start = 0 + for j = 1, (i - 1) do + hl_start = hl_start + #results[j] + (#configuration.separator or 1) + end + local hl_end = hl_start + #str:gsub("%s*$", "") + + if type(hl) == "function" then + for _, hl_res in ipairs(hl()) do + table.insert(highlights, { { hl_res[1][1] + hl_start, hl_res[1][2] + hl_start }, hl_res[2] }) + end + else + table.insert(highlights, { { hl_start, hl_end }, hl }) + end + end + + table.insert(results, str) + end + end + + if configuration.separator_hl then + local width = #configuration.separator or 1 + + local hl_start, hl_end + for _, v in ipairs(results) do + hl_start = (hl_end or 0) + #tostring(v) + hl_end = hl_start + width + table.insert(highlights, { { hl_start, hl_end }, configuration.separator_hl }) + end + end + + local final_str = table.concat(results, configuration.separator or "│") + if configuration.hl_chars then + for i = 1, #final_str do + local c = final_str:sub(i, i) + local hl = configuration.hl_chars[c] + if hl then + table.insert(highlights, { { i - 1, i }, hl }) + end + end + end + + return final_str, highlights + end +end + +entry_display.resolve = function(self, entry) + local display, display_highlights + if type(entry.display) == "function" then + self:_increment "display_fn" + display, display_highlights = entry:display(self) + + if type(display) == "string" then + return display, display_highlights + end + else + display = entry.display + end + + if type(display) == "string" then + return display, display_highlights + end +end + +return entry_display diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/highlights.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/highlights.lua new file mode 100644 index 0000000..be693a7 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/highlights.lua @@ -0,0 +1,125 @@ +local a = vim.api +local log = require "telescope.log" +local conf = require("telescope.config").values + +local highlights = {} + +local ns_telescope_selection = a.nvim_create_namespace "telescope_selection" +local ns_telescope_multiselection = a.nvim_create_namespace "telescope_multiselection" +local ns_telescope_entry = a.nvim_create_namespace "telescope_entry" + +local Highlighter = {} +Highlighter.__index = Highlighter + +function Highlighter:new(picker) + return setmetatable({ + picker = picker, + }, self) +end + +function Highlighter:hi_display(row, prefix, display_highlights) + -- This is the bug that made my highlight fixes not work. + -- We will leave the solution commented, so the test fails. + if not display_highlights or vim.tbl_isempty(display_highlights) then + return + end + + local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr") + + a.nvim_buf_clear_namespace(results_bufnr, ns_telescope_entry, row, row + 1) + local len_prefix = #prefix + + for _, hl_block in ipairs(display_highlights) do + a.nvim_buf_add_highlight( + results_bufnr, + ns_telescope_entry, + hl_block[2], + row, + len_prefix + hl_block[1][1], + len_prefix + hl_block[1][2] + ) + end +end + +function Highlighter:clear_display() + if + not self + or not self.picker + or not self.picker.results_bufnr + or not vim.api.nvim_buf_is_valid(self.picker.results_bufnr) + then + return + end + + a.nvim_buf_clear_namespace(self.picker.results_bufnr, ns_telescope_entry, 0, -1) +end + +function Highlighter:hi_sorter(row, prompt, display) + local picker = self.picker + if not picker.sorter or not picker.sorter.highlighter then + return + end + + local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr") + picker:highlight_one_row(results_bufnr, prompt, display, row) +end + +function Highlighter:hi_selection(row, caret) + caret = vim.F.if_nil(caret, "") + local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr") + + a.nvim_buf_clear_namespace(results_bufnr, ns_telescope_selection, 0, -1) + a.nvim_buf_add_highlight(results_bufnr, ns_telescope_selection, "TelescopeSelectionCaret", row, 0, #caret) + + a.nvim_buf_set_extmark( + results_bufnr, + ns_telescope_selection, + row, + #caret, + { end_line = row + 1, hl_eol = conf.hl_result_eol, hl_group = "TelescopeSelection" } + ) +end + +function Highlighter:hi_multiselect(row, is_selected) + local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr") + + if is_selected then + vim.api.nvim_buf_add_highlight(results_bufnr, ns_telescope_multiselection, "TelescopeMultiSelection", row, 0, -1) + if self.picker.multi_icon then + local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] + local pos = line:find(self.picker.multi_icon) + if pos and pos <= math.max(#self.picker.selection_caret, #self.picker.entry_prefix) then + vim.api.nvim_buf_add_highlight( + results_bufnr, + ns_telescope_multiselection, + "TelescopeMultiIcon", + row, + pos - 1, + pos - 1 + #self.picker.multi_icon + ) + end + end + else + local existing_marks = vim.api.nvim_buf_get_extmarks( + results_bufnr, + ns_telescope_multiselection, + { row, 0 }, + { row, -1 }, + {} + ) + + -- This is still kind of weird to me, since it seems like I'm erasing stuff + -- when I shouldn't... Perhaps it's about the gravity of the extmark? + if #existing_marks > 0 then + log.trace("Clearing highlight multi select row: ", row) + + vim.api.nvim_buf_clear_namespace(results_bufnr, ns_telescope_multiselection, row, row + 1) + end + end +end + +highlights.new = function(...) + return Highlighter:new(...) +end + +return highlights diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/layout_strategies.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/layout_strategies.lua new file mode 100644 index 0000000..7af7761 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/layout_strategies.lua @@ -0,0 +1,941 @@ +---@tag telescope.layout +---@config { ["module"] = "telescope.layout" } + +---@brief [[ +--- The layout of telescope pickers can be adjusted using the +--- |telescope.defaults.layout_strategy| and |telescope.defaults.layout_config| options. +--- For example, the following configuration changes the default layout strategy and the +--- default size of the picker: +--- +--- require('telescope').setup{ +--- defaults = { +--- layout_strategy = 'vertical', +--- layout_config = { height = 0.95 }, +--- }, +--- } +--- +--- +--- ──────────────────────────────────────────────────────────────────────────────── +--- +--- Layout strategies are different functions to position telescope. +--- +--- All layout strategies are functions with the following signature: +--- +--- +--- function(picker, columns, lines, layout_config) +--- -- Do some calculations here... +--- return { +--- preview = preview_configuration +--- results = results_configuration, +--- prompt = prompt_configuration, +--- } +--- end +--- +--- +---
+---   Parameters: ~
+---     - picker        : A Picker object. (docs coming soon)
+---     - columns       : (number) Columns in the vim window
+---     - lines         : (number) Lines in the vim window
+---     - layout_config : (table) The configuration values specific to the picker.
+---
+--- 
+--- +--- This means you can create your own layout strategy if you want! Just be aware +--- for now that we may change some APIs or interfaces, so they may break if you create +--- your own. +--- +--- A good method for creating your own would be to copy one of the strategies that most +--- resembles what you want from "./lua/telescope/pickers/layout_strategies.lua" in the +--- telescope repo. +--- +---@brief ]] + +local resolve = require "telescope.config.resolve" +local p_window = require "telescope.pickers.window" + +local get_border_size = function(opts) + if opts.window.border == false then + return 0 + end + + return 1 +end + +local calc_tabline = function(max_lines) + local tbln = (vim.o.showtabline == 2) or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1) + if tbln then + max_lines = max_lines - 1 + end + return max_lines, tbln +end + +-- Helper function for capping over/undersized width/height, and calculating spacing +--@param cur_size number: size to be capped +--@param max_size any: the maximum size, e.g. max_lines or max_columns +--@param bs number: the size of the border +--@param w_num number: the maximum number of windows of the picker in the given direction +--@param b_num number: the number of border rows/column in the given direction (when border enabled) +--@param s_num number: the number of gaps in the given direction (when border disabled) +local calc_size_and_spacing = function(cur_size, max_size, bs, w_num, b_num, s_num) + local spacing = s_num * (1 - bs) + b_num * bs + cur_size = math.min(cur_size, max_size) + cur_size = math.max(cur_size, w_num + spacing) + return cur_size, spacing +end + +local layout_strategies = {} +layout_strategies._configurations = {} + +--@param strategy_config table: table with keys for each option for a strategy +--@return table: table with keys for each option (for this strategy) and with keys for each layout_strategy +local get_valid_configuration_keys = function(strategy_config) + local valid_configuration_keys = { + -- TEMP: There are a few keys we should say are valid to start with. + preview_cutoff = true, + prompt_position = true, + } + + for key in pairs(strategy_config) do + valid_configuration_keys[key] = true + end + + for name in pairs(layout_strategies) do + valid_configuration_keys[name] = true + end + + return valid_configuration_keys +end + +local adjust_pos = function(pos, ...) + for _, opts in ipairs { ... } do + opts.col = opts.col and opts.col + pos[1] + opts.line = opts.line and opts.line + pos[2] + end +end + +--@param strategy_name string: the name of the layout_strategy we are validating for +--@param configuration table: table with keys for each option available +--@param values table: table containing all of the non-default options we want to set +--@param default_layout_config table: table with the default values to configure layouts +--@return table: table containing the combined options (defaults and non-defaults) +local function validate_layout_config(strategy_name, configuration, values, default_layout_config) + assert(strategy_name, "It is required to have a strategy name for validation.") + local valid_configuration_keys = get_valid_configuration_keys(configuration) + + -- If no default_layout_config provided, check Telescope's config values + default_layout_config = vim.F.if_nil(default_layout_config, require("telescope.config").values.layout_config) + + local result = {} + local get_value = function(k) + -- skip "private" items + if string.sub(k, 1, 1) == "_" then + return + end + + local val + -- Prioritise options that are specific to this strategy + if values[strategy_name] ~= nil and values[strategy_name][k] ~= nil then + val = values[strategy_name][k] + end + + -- Handle nested layout config values + if layout_strategies[k] and strategy_name ~= k and type(val) == "table" then + val = vim.tbl_deep_extend("force", default_layout_config[k], val) + end + + if val == nil and values[k] ~= nil then + val = values[k] + end + + if val == nil then + if default_layout_config[strategy_name] ~= nil and default_layout_config[strategy_name][k] ~= nil then + val = default_layout_config[strategy_name][k] + else + val = default_layout_config[k] + end + end + + return val + end + + -- Always set the values passed first. + for k in pairs(values) do + if not valid_configuration_keys[k] then + -- TODO: At some point we'll move to error here, + -- but it's a bit annoying to just straight up crash everyone's stuff. + vim.api.nvim_err_writeln( + string.format( + "Unsupported layout_config key for the %s strategy: %s\n%s", + strategy_name, + k, + vim.inspect(values) + ) + ) + end + + result[k] = get_value(k) + end + + -- And then set other valid keys via "inheritance" style extension + for k in pairs(valid_configuration_keys) do + if result[k] == nil then + result[k] = get_value(k) + end + end + + return result +end + +-- List of options that are shared by more than one layout. +local shared_options = { + width = { "How wide to make Telescope's entire layout", "See |resolver.resolve_width()|" }, + height = { "How tall to make Telescope's entire layout", "See |resolver.resolve_height()|" }, + mirror = "Flip the location of the results/prompt and preview windows", + scroll_speed = "The number of lines to scroll through the previewer", + prompt_position = { "Where to place prompt window.", "Available Values: 'bottom', 'top'" }, + anchor = { "Which edge/corner to pin the picker to", "See |resolver.resolve_anchor_pos()|" }, +} + +-- Used for generating vim help documentation. +layout_strategies._format = function(name) + local strategy_config = layout_strategies._configurations[name] + if vim.tbl_isempty(strategy_config) then + return {} + end + + local results = { "
", "`picker.layout_config` shared options:" }
+
+  local strategy_keys = vim.tbl_keys(strategy_config)
+  table.sort(strategy_keys, function(a, b)
+    return a < b
+  end)
+
+  local add_value = function(k, val)
+    if type(val) == "string" then
+      table.insert(results, string.format("  - %s: %s", k, val))
+    elseif type(val) == "table" then
+      table.insert(results, string.format("  - %s:", k))
+      for _, line in ipairs(val) do
+        table.insert(results, string.format("    - %s", line))
+      end
+    else
+      error(string.format("expected string or table but found '%s'", type(val)))
+    end
+  end
+
+  for _, k in ipairs(strategy_keys) do
+    if shared_options[k] then
+      add_value(k, strategy_config[k])
+    end
+  end
+
+  table.insert(results, "")
+  table.insert(results, "`picker.layout_config` unique options:")
+
+  for _, k in ipairs(strategy_keys) do
+    if not shared_options[k] then
+      add_value(k, strategy_config[k])
+    end
+  end
+
+  table.insert(results, "
") + return results +end + +--@param name string: the name to be assigned to the layout +--@param layout_config table: table where keys are the available options for the layout +--@param layout function: function with signature +-- function(self, max_columns, max_lines, layout_config): table +-- the returned table is the sizing and location information for the parts of the picker +--@retun function: wrapped function that inputs a validated layout_config into the `layout` function +local function make_documented_layout(name, layout_config, layout) + -- Save configuration data to be used by documentation + layout_strategies._configurations[name] = layout_config + + -- Return new function that always validates configuration + return function(self, max_columns, max_lines, override_layout) + return layout( + self, + max_columns, + max_lines, + validate_layout_config( + name, + layout_config, + vim.tbl_deep_extend("keep", vim.F.if_nil(override_layout, {}), vim.F.if_nil(self.layout_config, {})) + ) + ) + end +end + +--- Horizontal layout has two columns, one for the preview +--- and one for the prompt and results. +--- +---
+--- ┌──────────────────────────────────────────────────┐
+--- │                                                  │
+--- │    ┌───────────────────┐┌───────────────────┐    │
+--- │    │                   ││                   │    │
+--- │    │                   ││                   │    │
+--- │    │                   ││                   │    │
+--- │    │      Results      ││                   │    │
+--- │    │                   ││      Preview      │    │
+--- │    │                   ││                   │    │
+--- │    │                   ││                   │    │
+--- │    └───────────────────┘│                   │    │
+--- │    ┌───────────────────┐│                   │    │
+--- │    │      Prompt       ││                   │    │
+--- │    └───────────────────┘└───────────────────┘    │
+--- │                                                  │
+--- └──────────────────────────────────────────────────┘
+--- 
+---@eval { ["description"] = require('telescope.pickers.layout_strategies')._format("horizontal") } +--- +layout_strategies.horizontal = make_documented_layout( + "horizontal", + vim.tbl_extend("error", shared_options, { + preview_width = { "Change the width of Telescope's preview window", "See |resolver.resolve_width()|" }, + preview_cutoff = "When columns are less than this value, the preview will be disabled", + }), + function(self, max_columns, max_lines, layout_config) + local initial_options = p_window.get_initial_window_options(self) + local preview = initial_options.preview + local results = initial_options.results + local prompt = initial_options.prompt + + local tbln + max_lines, tbln = calc_tabline(max_lines) + + local width_opt = layout_config.width + local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) + + local height_opt = layout_config.height + local height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) + + local bs = get_border_size(self) + + local w_space + if self.previewer and max_columns >= layout_config.preview_cutoff then + -- Cap over/undersized width (with previewer) + width, w_space = calc_size_and_spacing(width, max_columns, bs, 2, 4, 1) + + preview.width = resolve.resolve_width(vim.F.if_nil(layout_config.preview_width, function(_, cols) + if cols < 150 then + return math.floor(cols * 0.4) + elseif cols < 200 then + return 80 + else + return 120 + end + end))(self, width, max_lines) + + results.width = width - preview.width - w_space + prompt.width = results.width + else + -- Cap over/undersized width (without previewer) + width, w_space = calc_size_and_spacing(width, max_columns, bs, 1, 2, 0) + + preview.width = 0 + results.width = width - preview.width - w_space + prompt.width = results.width + end + + local h_space + -- Cap over/undersized height + height, h_space = calc_size_and_spacing(height, max_lines, bs, 2, 4, 1) + + prompt.height = 1 + results.height = height - prompt.height - h_space + + if self.previewer then + preview.height = height - 2 * bs + else + preview.height = 0 + end + + local width_padding = math.floor((max_columns - width) / 2) + -- Default value is false, to use the normal horizontal layout + if not layout_config.mirror then + results.col = width_padding + bs + 1 + prompt.col = results.col + preview.col = results.col + results.width + 1 + bs + else + preview.col = width_padding + bs + 1 + prompt.col = preview.col + preview.width + 1 + bs + results.col = preview.col + preview.width + 1 + bs + end + + preview.line = math.floor((max_lines - height) / 2) + bs + 1 + if layout_config.prompt_position == "top" then + prompt.line = preview.line + results.line = prompt.line + prompt.height + 1 + bs + elseif layout_config.prompt_position == "bottom" then + results.line = preview.line + prompt.line = results.line + results.height + 1 + bs + else + error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config))) + end + + local anchor_pos = resolve.resolve_anchor_pos(layout_config.anchor or "", width, height, max_columns, max_lines) + adjust_pos(anchor_pos, prompt, results, preview) + + if tbln then + prompt.line = prompt.line + 1 + results.line = results.line + 1 + preview.line = preview.line + 1 + end + + return { + preview = self.previewer and preview.width > 0 and preview, + results = results, + prompt = prompt, + } + end +) + +--- Centered layout with a combined block of the prompt +--- and results aligned to the middle of the screen. +--- The preview window is then placed in the remaining +--- space above or below, according to `anchor` or `mirror`. +--- Particularly useful for creating dropdown menus +--- (see |telescope.themes| and |themes.get_dropdown()|). +--- +--- Note that vertical anchoring, i.e. `anchor` containing +--- `"N"` or `"S"`, will override `mirror` config. For `"N"` +--- anchoring preview will be placed below prompt/result +--- block. For `"S"` anchoring preview will be placed above +--- prompt/result block. For horizontal only anchoring preview +--- will be placed according to `mirror` config, default is +--- above the prompt/result block. +--- +---
+--- ┌──────────────────────────────────────────────────┐
+--- │    ┌────────────────────────────────────────┐    │
+--- │    |                 Preview                |    │
+--- │    |                 Preview                |    │
+--- │    └────────────────────────────────────────┘    │
+--- │    ┌────────────────────────────────────────┐    │
+--- │    |                 Prompt                 |    │
+--- │    ├────────────────────────────────────────┤    │
+--- │    |                 Result                 |    │
+--- │    |                 Result                 |    │
+--- │    └────────────────────────────────────────┘    │
+--- │                                                  │
+--- │                                                  │
+--- │                                                  │
+--- │                                                  │
+--- └──────────────────────────────────────────────────┘
+--- 
+---@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("center") } +--- +layout_strategies.center = make_documented_layout( + "center", + vim.tbl_extend("error", shared_options, { + preview_cutoff = "When lines are less than this value, the preview will be disabled", + }), + function(self, max_columns, max_lines, layout_config) + local initial_options = p_window.get_initial_window_options(self) + local preview = initial_options.preview + local results = initial_options.results + local prompt = initial_options.prompt + + local tbln + max_lines, tbln = calc_tabline(max_lines) + + -- This sets the width for the whole layout + local width_opt = layout_config.width + local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) + + -- This sets the height for the whole layout + local height_opt = layout_config.height + local height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) + + local bs = get_border_size(self) + + local w_space + -- Cap over/undersized width + width, w_space = calc_size_and_spacing(width, max_columns, bs, 1, 2, 0) + + prompt.width = width - w_space + results.width = width - w_space + preview.width = width - w_space + + local h_space + -- Cap over/undersized height + height, h_space = calc_size_and_spacing(height, max_lines, bs, 2, 3, 0) + + prompt.height = 1 + results.height = height - prompt.height - h_space + + local topline = math.floor((max_lines / 2) - ((results.height + (2 * bs)) / 2) + 1) + -- Align the prompt and results so halfway up the screen is + -- in the middle of this combined block + if layout_config.prompt_position == "top" then + prompt.line = topline + results.line = prompt.line + 1 + bs + elseif layout_config.prompt_position == "bottom" then + results.line = topline + prompt.line = results.line + results.height + bs + if type(prompt.title) == "string" then + prompt.title = { { pos = "S", text = prompt.title } } + end + else + error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config))) + end + + local width_padding = math.floor((max_columns - width) / 2) + bs + 1 + results.col, preview.col, prompt.col = width_padding, width_padding, width_padding + + local anchor = layout_config.anchor or "" + local anchor_pos = resolve.resolve_anchor_pos(anchor, width, height, max_columns, max_lines) + adjust_pos(anchor_pos, prompt, results, preview) + + -- Vertical anchoring (S or N variations) ignores layout_config.mirror + anchor = anchor:upper() + local mirror + if anchor:find "S" then + mirror = false + elseif anchor:find "N" then + mirror = true + else + mirror = layout_config.mirror + end + + -- Set preview position + local block_line = math.min(results.line, prompt.line) + if not mirror then -- Preview at top + preview.line = 1 + bs + preview.height = block_line - (2 + 2 * bs) + else -- Preview at bottom + preview.line = block_line + results.height + 2 + 2 * bs + preview.height = max_lines - preview.line - bs + 1 + end + + if not (self.previewer and max_lines >= layout_config.preview_cutoff) then + preview.height = 0 + end + + if tbln then + prompt.line = prompt.line + 1 + results.line = results.line + 1 + preview.line = preview.line + 1 + end + + return { + preview = self.previewer and preview.height > 0 and preview, + results = results, + prompt = prompt, + } + end +) + +--- Cursor layout dynamically positioned below the cursor if possible. +--- If there is no place below the cursor it will be placed above. +--- +---
+--- ┌──────────────────────────────────────────────────┐
+--- │                                                  │
+--- │   █                                              │
+--- │   ┌──────────────┐┌─────────────────────┐        │
+--- │   │    Prompt    ││      Preview        │        │
+--- │   ├──────────────┤│      Preview        │        │
+--- │   │    Result    ││      Preview        │        │
+--- │   │    Result    ││      Preview        │        │
+--- │   └──────────────┘└─────────────────────┘        │
+--- │                                         █        │
+--- │                                                  │
+--- │                                                  │
+--- │                                                  │
+--- │                                                  │
+--- │                                                  │
+--- └──────────────────────────────────────────────────┘
+--- 
+layout_strategies.cursor = make_documented_layout( + "cursor", + vim.tbl_extend("error", { + width = shared_options.width, + height = shared_options.height, + scroll_speed = shared_options.scroll_speed, + }, { + preview_width = { "Change the width of Telescope's preview window", "See |resolver.resolve_width()|" }, + preview_cutoff = "When columns are less than this value, the preview will be disabled", + }), + function(self, max_columns, max_lines, layout_config) + local initial_options = p_window.get_initial_window_options(self) + local preview = initial_options.preview + local results = initial_options.results + local prompt = initial_options.prompt + + local height_opt = layout_config.height + local height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) + + local width_opt = layout_config.width + local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) + + local bs = get_border_size(self) + + local h_space + -- Cap over/undersized height + height, h_space = calc_size_and_spacing(height, max_lines, bs, 2, 3, 0) + + prompt.height = 1 + results.height = height - prompt.height - h_space + preview.height = height - 2 * bs + + local w_space + if self.previewer and max_columns >= layout_config.preview_cutoff then + -- Cap over/undersized width (with preview) + width, w_space = calc_size_and_spacing(width, max_columns, bs, 2, 4, 0) + + preview.width = resolve.resolve_width(vim.F.if_nil(layout_config.preview_width, 2 / 3))(self, width, max_lines) + prompt.width = width - preview.width - w_space + results.width = prompt.width + else + -- Cap over/undersized width (without preview) + width, w_space = calc_size_and_spacing(width, max_columns, bs, 1, 2, 0) + + preview.width = 0 + prompt.width = width - w_space + results.width = prompt.width + end + + local position = vim.api.nvim_win_get_position(0) + local winbar = (function() + if vim.fn.exists "&winbar" == 1 then + return vim.o.winbar == "" and 0 or 1 + end + return 0 + end)() + local top_left = { + line = vim.fn.winline() + position[1] + bs + winbar, + col = vim.fn.wincol() + position[2], + } + local bot_right = { + line = top_left.line + height - 1, + col = top_left.col + width - 1, + } + + if bot_right.line > max_lines then + -- position above current line + top_left.line = top_left.line - height - 1 + end + if bot_right.col >= max_columns then + -- cap to the right of the screen + top_left.col = max_columns - width + end + + prompt.line = top_left.line + 1 + results.line = prompt.line + bs + 1 + preview.line = prompt.line + + prompt.col = top_left.col + 1 + results.col = prompt.col + preview.col = results.col + (bs * 2) + results.width + + return { + preview = self.previewer and preview.width > 0 and preview, + results = results, + prompt = prompt, + } + end +) + +--- Vertical layout stacks the items on top of each other. +--- Particularly useful with thinner windows. +--- +---
+--- ┌──────────────────────────────────────────────────┐
+--- │                                                  │
+--- │    ┌────────────────────────────────────────┐    │
+--- │    |                 Preview                |    │
+--- │    |                 Preview                |    │
+--- │    |                 Preview                |    │
+--- │    └────────────────────────────────────────┘    │
+--- │    ┌────────────────────────────────────────┐    │
+--- │    |                 Result                 |    │
+--- │    |                 Result                 |    │
+--- │    └────────────────────────────────────────┘    │
+--- │    ┌────────────────────────────────────────┐    │
+--- │    |                 Prompt                 |    │
+--- │    └────────────────────────────────────────┘    │
+--- │                                                  │
+--- └──────────────────────────────────────────────────┘
+--- 
+---@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("vertical") } +--- +layout_strategies.vertical = make_documented_layout( + "vertical", + vim.tbl_extend("error", shared_options, { + preview_cutoff = "When lines are less than this value, the preview will be disabled", + preview_height = { "Change the height of Telescope's preview window", "See |resolver.resolve_height()|" }, + }), + function(self, max_columns, max_lines, layout_config) + local initial_options = p_window.get_initial_window_options(self) + local preview = initial_options.preview + local results = initial_options.results + local prompt = initial_options.prompt + + local tbln + max_lines, tbln = calc_tabline(max_lines) + + local width_opt = layout_config.width + local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) + + local height_opt = layout_config.height + local height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) + + local bs = get_border_size(self) + + local w_space + -- Cap over/undersized width + width, w_space = calc_size_and_spacing(width, max_columns, bs, 1, 2, 0) + + prompt.width = width - w_space + results.width = prompt.width + preview.width = prompt.width + + local h_space + if self.previewer and max_lines >= layout_config.preview_cutoff then + -- Cap over/undersized height (with previewer) + height, h_space = calc_size_and_spacing(height, max_lines, bs, 3, 6, 2) + + preview.height = + resolve.resolve_height(vim.F.if_nil(layout_config.preview_height, 0.5))(self, max_columns, height) + else + -- Cap over/undersized height (without previewer) + height, h_space = calc_size_and_spacing(height, max_lines, bs, 2, 4, 1) + + preview.height = 0 + end + prompt.height = 1 + results.height = height - preview.height - prompt.height - h_space + + local width_padding = math.floor((max_columns - width) / 2) + bs + 1 + results.col, preview.col, prompt.col = width_padding, width_padding, width_padding + + local height_padding = math.floor((max_lines - height) / 2) + if not layout_config.mirror then + preview.line = height_padding + (1 + bs) + if layout_config.prompt_position == "top" then + prompt.line = (preview.height == 0) and preview.line or preview.line + preview.height + (1 + bs) + results.line = prompt.line + prompt.height + (1 + bs) + elseif layout_config.prompt_position == "bottom" then + results.line = (preview.height == 0) and preview.line or preview.line + preview.height + (1 + bs) + prompt.line = results.line + results.height + (1 + bs) + else + error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config))) + end + else + if layout_config.prompt_position == "top" then + prompt.line = height_padding + (1 + bs) + results.line = prompt.line + prompt.height + (1 + bs) + preview.line = results.line + results.height + (1 + bs) + elseif layout_config.prompt_position == "bottom" then + results.line = height_padding + (1 + bs) + prompt.line = results.line + results.height + (1 + bs) + preview.line = prompt.line + prompt.height + (1 + bs) + else + error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config))) + end + end + + local anchor_pos = resolve.resolve_anchor_pos(layout_config.anchor or "", width, height, max_columns, max_lines) + adjust_pos(anchor_pos, prompt, results, preview) + + if tbln then + prompt.line = prompt.line + 1 + results.line = results.line + 1 + preview.line = preview.line + 1 + end + + return { + preview = self.previewer and preview.height > 0 and preview, + results = results, + prompt = prompt, + } + end +) + +--- Flex layout swaps between `horizontal` and `vertical` strategies based on the window width +--- - Supports |layout_strategies.vertical| or |layout_strategies.horizontal| features +--- +---@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("flex") } +--- +layout_strategies.flex = make_documented_layout( + "flex", + vim.tbl_extend("error", shared_options, { + flip_columns = "The number of columns required to move to horizontal mode", + flip_lines = "The number of lines required to move to horizontal mode", + vertical = "Options to pass when switching to vertical layout", + horizontal = "Options to pass when switching to horizontal layout", + }), + function(self, max_columns, max_lines, layout_config) + local flip_columns = vim.F.if_nil(layout_config.flip_columns, 100) + local flip_lines = vim.F.if_nil(layout_config.flip_lines, 20) + + if max_columns < flip_columns and max_lines > flip_lines then + self.__flex_strategy = "vertical" + return layout_strategies.vertical(self, max_columns, max_lines, layout_config.vertical) + else + self.__flex_strategy = "horizontal" + return layout_strategies.horizontal(self, max_columns, max_lines, layout_config.horizontal) + end + end +) + +layout_strategies.current_buffer = make_documented_layout("current_buffer", { + -- No custom options. + -- height, width ignored +}, function(self, _, _, _) + local initial_options = p_window.get_initial_window_options(self) + + local window_width = vim.api.nvim_win_get_width(0) + local window_height = vim.api.nvim_win_get_height(0) + + local preview = initial_options.preview + local results = initial_options.results + local prompt = initial_options.prompt + + local bs = get_border_size(self) + + -- Width + local width_padding = (1 + bs) -- TODO(l-kershaw): make this configurable + + prompt.width = window_width - 2 * width_padding + results.width = prompt.width + preview.width = prompt.width + + -- Height + local height_padding = (1 + bs) -- TODO(l-kershaw): make this configurable + + prompt.height = 1 + if self.previewer then + results.height = 10 -- TODO(l-kershaw): make this configurable + preview.height = window_height - results.height - prompt.height - 2 * (1 + bs) - 2 * height_padding + else + results.height = window_height - prompt.height - (1 + bs) - 2 * height_padding + preview.height = 0 + end + + local win_position = vim.api.nvim_win_get_position(0) + + local line = win_position[1] + if self.previewer then + preview.line = height_padding + line + 1 + results.line = preview.line + preview.height + (1 + bs) + prompt.line = results.line + results.height + (1 + bs) + else + results.line = height_padding + line + 1 + prompt.line = results.line + results.height + (1 + bs) + end + + local col = win_position[2] + width_padding + 1 + preview.col, results.col, prompt.col = col, col, col + + return { + preview = preview.height > 0 and preview, + results = results, + prompt = prompt, + } +end) + +--- Bottom pane can be used to create layouts similar to "ivy". +--- +--- For an easy ivy configuration, see |themes.get_ivy()| +layout_strategies.bottom_pane = make_documented_layout( + "bottom_pane", + vim.tbl_extend("error", shared_options, { + preview_width = { "Change the width of Telescope's preview window", "See |resolver.resolve_width()|" }, + preview_cutoff = "When columns are less than this value, the preview will be disabled", + }), + function(self, max_columns, max_lines, layout_config) + local initial_options = p_window.get_initial_window_options(self) + local results = initial_options.results + local prompt = initial_options.prompt + local preview = initial_options.preview + + local tbln + max_lines, tbln = calc_tabline(max_lines) + + local height = vim.F.if_nil(resolve.resolve_height(layout_config.height)(self, max_columns, max_lines), 25) + if type(layout_config.height) == "table" and type(layout_config.height.padding) == "number" then + -- Since bottom_pane only has padding at the top, we only need half as much padding in total + -- This doesn't match the vim help for `resolve.resolve_height`, but it matches expectations + height = math.floor((max_lines + height) / 2) + end + + local bs = get_border_size(self) + + -- Cap over/undersized height + height, _ = calc_size_and_spacing(height, max_lines, bs, 2, 3, 0) + + -- Height + prompt.height = 1 + results.height = height - prompt.height - (2 * bs) + preview.height = results.height - bs + + -- Width + prompt.width = max_columns - 2 * bs + if self.previewer and max_columns >= layout_config.preview_cutoff then + -- Cap over/undersized width (with preview) + local width, w_space = calc_size_and_spacing(max_columns, max_columns, bs, 2, 4, 0) + + preview.width = resolve.resolve_width(vim.F.if_nil(layout_config.preview_width, 0.5))(self, width, max_lines) + results.width = width - preview.width - w_space + else + results.width = prompt.width + preview.width = 0 + end + + -- Line + if layout_config.prompt_position == "top" then + prompt.line = max_lines - results.height - (1 + bs) + 1 + results.line = prompt.line + 1 + preview.line = results.line + bs + if results.border == true then + results.border = { 0, 1, 1, 1 } + end + if type(results.title) == "string" then + results.title = { { pos = "S", text = results.title } } + end + elseif layout_config.prompt_position == "bottom" then + results.line = max_lines - results.height - (1 + bs) + 1 + preview.line = results.line + prompt.line = max_lines - bs + if type(prompt.title) == "string" then + prompt.title = { { pos = "S", text = prompt.title } } + end + if results.border == true then + results.border = { 1, 1, 0, 1 } + end + else + error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config))) + end + + -- Col + prompt.col = 0 -- centered + if layout_config.mirror and preview.width > 0 then + results.col = preview.width + (3 * bs) + 1 + preview.col = bs + 1 + else + results.col = bs + 1 + preview.col = results.width + (3 * bs) + 1 + end + + if tbln then + prompt.line = prompt.line + 1 + results.line = results.line + 1 + preview.line = preview.line + 1 + end + + return { + preview = self.previewer and preview.width > 0 and preview, + prompt = prompt, + results = results, + } + end +) + +layout_strategies._validate_layout_config = validate_layout_config + +return layout_strategies diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/multi.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/multi.lua new file mode 100644 index 0000000..50aa051 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/multi.lua @@ -0,0 +1,50 @@ +local MultiSelect = {} +MultiSelect.__index = MultiSelect + +function MultiSelect:new() + return setmetatable({ + _entries = {}, + }, MultiSelect) +end + +function MultiSelect:get() + local marked_entries = {} + for entry, count in pairs(self._entries) do + table.insert(marked_entries, { count, entry }) + end + + table.sort(marked_entries, function(left, right) + return left[1] < right[1] + end) + + local selections = {} + for _, entry in ipairs(marked_entries) do + table.insert(selections, entry[2]) + end + + return selections +end + +function MultiSelect:is_selected(entry) + return self._entries[entry] +end + +local multi_select_count = 0 +function MultiSelect:add(entry) + multi_select_count = multi_select_count + 1 + self._entries[entry] = multi_select_count +end + +function MultiSelect:drop(entry) + self._entries[entry] = nil +end + +function MultiSelect:toggle(entry) + if self:is_selected(entry) then + self:drop(entry) + else + self:add(entry) + end +end + +return MultiSelect diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/scroller.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/scroller.lua new file mode 100644 index 0000000..a658f68 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/scroller.lua @@ -0,0 +1,124 @@ +local scroller = {} + +local range_calculators = { + ascending = function(max_results, num_results) + return 0, math.min(max_results, num_results) + end, + + descending = function(max_results, num_results) + return math.max(max_results - num_results, 0), max_results + end, +} + +local scroll_calculators = { + cycle = function(range_fn) + return function(max_results, num_results, row) + local start, finish = range_fn(max_results, num_results) + + if row >= finish then + return start + elseif row < start then + return (finish - 1 < 0) and finish or finish - 1 + end + + return row + end + end, + + limit = function(range_fn) + return function(max_results, num_results, row) + local start, finish = range_fn(max_results, num_results) + + if row >= finish and finish > 0 then + return finish - 1 + elseif row < start then + return start + end + + return row + end + end, +} + +scroller.create = function(scroll_strategy, sorting_strategy) + local range_fn = range_calculators[sorting_strategy] + if not range_fn then + error(debug.traceback("Unknown sorting strategy: " .. sorting_strategy)) + end + + local scroll_fn = scroll_calculators[scroll_strategy] + if not scroll_fn then + error(debug.traceback("Unknown scroll strategy: " .. (scroll_strategy or ""))) + end + + local calculator = scroll_fn(range_fn) + return function(max_results, num_results, row) + local result = calculator(max_results, num_results, row) + + if result < 0 then + error( + string.format( + "Must never return a negative row: { result = %s, args = { %s %s %s } }", + result, + max_results, + num_results, + row + ) + ) + end + + if result > max_results then + error( + string.format( + "Must never exceed max results: { result = %s, args = { %s %s %s } }", + result, + max_results, + num_results, + row + ) + ) + end + + return result + end +end + +scroller.top = function(sorting_strategy, max_results, num_results) + if sorting_strategy == "ascending" then + return 0 + end + return (num_results > max_results) and 0 or (max_results - num_results) +end + +scroller.middle = function(sorting_strategy, max_results, num_results) + local mid_pos + + if sorting_strategy == "ascending" then + mid_pos = math.floor(num_results / 2) + else + mid_pos = math.floor(max_results - num_results / 2) + end + + return (num_results < max_results) and mid_pos or math.floor(max_results / 2) +end + +scroller.bottom = function(sorting_strategy, max_results, num_results) + if sorting_strategy == "ascending" then + return math.min(max_results, num_results) - 1 + end + return max_results - 1 +end + +scroller.better = function(sorting_strategy) + if sorting_strategy == "ascending" then + return -1 + else + return 1 + end +end + +scroller.worse = function(sorting_strategy) + return -(scroller.better(sorting_strategy)) +end + +return scroller diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/window.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/window.lua new file mode 100644 index 0000000..945a7e3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/pickers/window.lua @@ -0,0 +1,49 @@ +local resolve = require "telescope.config.resolve" + +local p_window = {} + +function p_window.get_window_options(picker, max_columns, max_lines) + local layout_strategy = picker.layout_strategy + local getter = require("telescope.pickers.layout_strategies")[layout_strategy] + + if not getter then + error(string.format("'%s' is not a valid layout strategy", layout_strategy)) + end + + return getter(picker, max_columns, max_lines) +end + +function p_window.get_initial_window_options(picker) + local popup_border = resolve.win_option(picker.window.border) + local popup_borderchars = resolve.win_option(picker.window.borderchars) + + local preview = { + title = picker.preview_title, + border = popup_border.preview, + borderchars = popup_borderchars.preview, + enter = false, + highlight = false, + } + + local results = { + title = picker.results_title, + border = popup_border.results, + borderchars = popup_borderchars.results, + enter = false, + } + + local prompt = { + title = picker.prompt_title, + border = popup_border.prompt, + borderchars = popup_borderchars.prompt, + enter = true, + } + + return { + preview = preview, + results = results, + prompt = prompt, + } +end + +return p_window diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/buffer_previewer.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/buffer_previewer.lua new file mode 100644 index 0000000..cd795f1 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/buffer_previewer.lua @@ -0,0 +1,1117 @@ +local from_entry = require "telescope.from_entry" +local Path = require "plenary.path" +local utils = require "telescope.utils" +local putils = require "telescope.previewers.utils" +local Previewer = require "telescope.previewers.previewer" +local conf = require("telescope.config").values + +local pfiletype = require "plenary.filetype" +local pscan = require "plenary.scandir" + +local buf_delete = utils.buf_delete + +local previewers = {} + +local ns_previewer = vim.api.nvim_create_namespace "telescope.previewers" + +local has_file = 1 == vim.fn.executable "file" + +-- TODO(fdschmidt93) switch to Job once file_maker callbacks get cleaned up with plenary async +-- avoids SIGABRT from utils.get_os_command_output due to vim.time in fs_stat cb +local function capture(cmd, raw) + local f = assert(io.popen(cmd, "r")) + local s = assert(f:read "*a") + f:close() + if raw then + return s + end + s = string.gsub(s, "^%s+", "") + s = string.gsub(s, "%s+$", "") + s = string.gsub(s, "[\n\r]+", " ") + return s +end + +local function defaulter(f, default_opts) + default_opts = default_opts or {} + return { + new = function(opts) + if conf.preview == false and not opts.preview then + return false + end + opts.preview = type(opts.preview) ~= "table" and {} or opts.preview + if type(conf.preview) == "table" then + for k, v in pairs(conf.preview) do + opts.preview[k] = vim.F.if_nil(opts.preview[k], v) + end + end + return f(opts) + end, + __call = function() + local ok, err = pcall(f(default_opts)) + if not ok then + error(debug.traceback(err)) + end + end, + } +end + +-- modified vim.split to incorporate a timer +local function split(s, sep, plain, opts) + opts = opts or {} + local t = {} + for c in vim.gsplit(s, sep, plain) do + table.insert(t, c) + if opts.preview.timeout then + local diff_time = (vim.loop.hrtime() - opts.start_time) / 1e6 + if diff_time > opts.preview.timeout then + return + end + end + end + return t +end +local bytes_to_megabytes = math.pow(1024, 2) + +local color_hash = { + ["p"] = "TelescopePreviewPipe", + ["c"] = "TelescopePreviewCharDev", + ["d"] = "TelescopePreviewDirectory", + ["b"] = "TelescopePreviewBlock", + ["l"] = "TelescopePreviewLink", + ["s"] = "TelescopePreviewSocket", + ["."] = "TelescopePreviewNormal", + ["r"] = "TelescopePreviewRead", + ["w"] = "TelescopePreviewWrite", + ["x"] = "TelescopePreviewExecute", + ["-"] = "TelescopePreviewHyphen", + ["T"] = "TelescopePreviewSticky", + ["S"] = "TelescopePreviewSticky", + [2] = "TelescopePreviewSize", + [3] = "TelescopePreviewUser", + [4] = "TelescopePreviewGroup", + [5] = "TelescopePreviewDate", +} +color_hash[6] = function(line) + return color_hash[line:sub(1, 1)] +end + +local colorize_ls = function(bufnr, data, sections) + local windows_add = Path.path.sep == "\\" and 2 or 0 + for lnum, line in ipairs(data) do + local section = sections[lnum] + for i = 1, section[1].end_index - 1 do -- Highlight permissions + local c = line:sub(i, i) + vim.api.nvim_buf_add_highlight(bufnr, ns_previewer, color_hash[c], lnum - 1, i - 1, i) + end + for i = 2, #section do -- highlights size, (user, group), date and name + local hl_group = color_hash[i + (i ~= 2 and windows_add or 0)] + vim.api.nvim_buf_add_highlight( + bufnr, + ns_previewer, + type(hl_group) == "function" and hl_group(line) or hl_group, + lnum - 1, + section[i].start_index - 1, + section[i].end_index - 1 + ) + end + end +end + +local search_cb_jump = function(self, bufnr, query) + if not query then + return + end + vim.api.nvim_buf_call(bufnr, function() + pcall(vim.fn.matchdelete, self.state.hl_id, self.state.winid) + vim.cmd "norm! gg" + vim.fn.search(query, "W") + vim.cmd "norm! zz" + + self.state.hl_id = vim.fn.matchadd("TelescopePreviewMatch", query) + end) +end + +local search_teardown = function(self) + if self.state and self.state.hl_id then + pcall(vim.fn.matchdelete, self.state.hl_id, self.state.hl_win) + self.state.hl_id = nil + end +end + +local scroll_fn = function(self, direction) + if not self.state then + return + end + + local input = direction > 0 and [[]] or [[]] + local count = math.abs(direction) + + vim.api.nvim_win_call(self.state.winid, function() + vim.cmd([[normal! ]] .. count .. input) + end) +end + +previewers.file_maker = function(filepath, bufnr, opts) + opts = vim.F.if_nil(opts, {}) + -- TODO(conni2461): here shouldn't be any hardcoded magic numbers ... + opts.preview = vim.F.if_nil(opts.preview, {}) + opts.preview.timeout = vim.F.if_nil(opts.preview.timeout, 250) -- in ms + opts.preview.filesize_limit = vim.F.if_nil(opts.preview.filesize_limit, 25) -- in mb + opts.preview.msg_bg_fillchar = vim.F.if_nil(opts.preview.msg_bg_fillchar, "╱") -- in mb + opts.preview.treesitter = vim.F.if_nil(opts.preview.treesitter, true) + if opts.use_ft_detect == nil then + opts.use_ft_detect = true + end + opts.ft = opts.use_ft_detect and pfiletype.detect(filepath) + if opts.bufname ~= filepath then + if not vim.in_fast_event() then + filepath = vim.fn.expand(filepath) + end + if type(opts.preview.filetype_hook) == "function" then + if not opts.preview.filetype_hook(filepath, bufnr, opts) then + return + end + end + vim.loop.fs_stat(filepath, function(_, stat) + if not stat then + return + end + if stat.type == "directory" then + pscan.ls_async(filepath, { + hidden = true, + group_directories_first = true, + on_exit = vim.schedule_wrap(function(data, sections) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, data) + colorize_ls(bufnr, data, sections) + if opts.callback then + opts.callback(bufnr) + end + end), + }) + else + if opts.preview.check_mime_type == true and has_file and opts.ft == "" then + -- avoid SIGABRT in buffer previewer happening with utils.get_os_command_output + local output = capture(string.format([[file --mime-type -b "%s"]], filepath)) + local mime_type = vim.split(output, "/")[1] + if mime_type ~= "text" and mime_type ~= "inode" then + if type(opts.preview.mime_hook) == "function" then + vim.schedule_wrap(opts.preview.mime_hook)(filepath, bufnr, opts) + else + vim.schedule_wrap(putils.set_preview_message)( + bufnr, + opts.winid, + "Binary cannot be previewed", + opts.preview.msg_bg_fillchar + ) + end + return + end + end + + if opts.preview.filesize_limit then + local mb_filesize = math.floor(stat.size / bytes_to_megabytes) + if mb_filesize > opts.preview.filesize_limit then + if type(opts.preview.filesize_hook) == "function" then + vim.schedule_wrap(opts.preview.filesize_hook)(filepath, bufnr, opts) + else + vim.schedule_wrap(putils.set_preview_message)( + bufnr, + opts.winid, + "File exceeds preview size limit", + opts.preview.msg_bg_fillchar + ) + end + return + end + end + + opts.start_time = vim.loop.hrtime() + Path:new(filepath):_read_async(vim.schedule_wrap(function(data) + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + local processed_data = split(data, "[\r]?\n", _, opts) + + if processed_data then + local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, processed_data) + if not ok then + return + end + + if opts.callback then + opts.callback(bufnr) + end + putils.highlighter(bufnr, opts.ft, opts) + else + if type(opts.preview.timeout_hook) == "function" then + vim.schedule_wrap(opts.preview.timeout_hook)(filepath, bufnr, opts) + else + vim.schedule_wrap(putils.set_preview_message)( + bufnr, + opts.winid, + "Previewer timed out", + opts.preview.msg_bg_fillchar + ) + end + return + end + end)) + end + end) + else + if opts.callback then + if vim.in_fast_event() then + vim.schedule(function() + opts.callback(bufnr) + end) + else + opts.callback(bufnr) + end + end + end +end + +previewers.new_buffer_previewer = function(opts) + opts = opts or {} + + assert(opts.define_preview, "define_preview is a required function") + assert(not opts.preview_fn, "preview_fn not allowed") + + local opt_setup = opts.setup + local opt_teardown = opts.teardown + + local old_bufs = {} + local bufname_table = {} + + local global_state = require "telescope.state" + local preview_window_id + + local function get_bufnr(self) + if not self.state then + return nil + end + return self.state.bufnr + end + + local function set_bufnr(self, value) + if self.state then + self.state.bufnr = value + table.insert(old_bufs, value) + end + end + + local function get_bufnr_by_bufname(self, value) + if not self.state then + return nil + end + return bufname_table[value] + end + + local function set_bufname(self, value) + if self.state then + self.state.bufname = value + if value then + bufname_table[value] = get_bufnr(self) + end + end + end + + function opts.setup(self) + local state = {} + if opt_setup then + vim.tbl_deep_extend("force", state, opt_setup(self)) + end + return state + end + + function opts.teardown(self) + if opt_teardown then + opt_teardown(self) + end + + local last_nr + if opts.keep_last_buf then + last_nr = global_state.get_global_key "last_preview_bufnr" + -- Push in another buffer so the last one will not be cleaned up + if preview_window_id then + local bufnr = vim.api.nvim_create_buf(false, true) + utils.win_set_buf_noautocmd(preview_window_id, bufnr) + end + end + + set_bufnr(self, nil) + set_bufname(self, nil) + + for _, bufnr in ipairs(old_bufs) do + if bufnr ~= last_nr then + buf_delete(bufnr) + end + end + -- enable resuming picker with existing previewer to avoid lookup of deleted bufs + bufname_table = {} + end + + function opts.preview_fn(self, entry, status) + if get_bufnr(self) == nil then + set_bufnr(self, vim.api.nvim_win_get_buf(status.preview_win)) + preview_window_id = status.preview_win + end + + if opts.get_buffer_by_name and get_bufnr_by_bufname(self, opts.get_buffer_by_name(self, entry)) then + self.state.bufname = opts.get_buffer_by_name(self, entry) + self.state.bufnr = get_bufnr_by_bufname(self, self.state.bufname) + utils.win_set_buf_noautocmd(status.preview_win, self.state.bufnr) + else + local bufnr = vim.api.nvim_create_buf(false, true) + set_bufnr(self, bufnr) + + vim.schedule(function() + if vim.api.nvim_buf_is_valid(bufnr) then + utils.win_set_buf_noautocmd(status.preview_win, bufnr) + end + end) + + vim.api.nvim_win_set_option(status.preview_win, "winhl", "Normal:TelescopePreviewNormal") + vim.api.nvim_win_set_option(status.preview_win, "signcolumn", "no") + vim.api.nvim_win_set_option(status.preview_win, "foldlevel", 100) + vim.api.nvim_win_set_option(status.preview_win, "wrap", false) + vim.api.nvim_win_set_option(status.preview_win, "scrollbind", false) + + self.state.winid = status.preview_win + self.state.bufname = nil + end + + if opts.keep_last_buf then + global_state.set_global_key("last_preview_bufnr", self.state.bufnr) + end + + opts.define_preview(self, entry, status) + + vim.schedule(function() + if not self or not self.state or not self.state.bufnr then + return + end + + if vim.api.nvim_buf_is_valid(self.state.bufnr) then + vim.api.nvim_buf_call(self.state.bufnr, function() + vim.cmd "do User TelescopePreviewerLoaded" + end) + end + end) + + if opts.get_buffer_by_name then + set_bufname(self, opts.get_buffer_by_name(self, entry)) + end + end + + if not opts.scroll_fn then + opts.scroll_fn = scroll_fn + end + + return Previewer:new(opts) +end + +previewers.cat = defaulter(function(opts) + opts = opts or {} + local cwd = opts.cwd or vim.loop.cwd() + return previewers.new_buffer_previewer { + title = "File Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, false, false)):normalize(cwd) + end, + + get_buffer_by_name = function(_, entry) + return from_entry.path(entry, false) + end, + + define_preview = function(self, entry, status) + local p = from_entry.path(entry, true) + if p == nil or p == "" then + return + end + conf.buffer_previewer_maker(p, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + preview = opts.preview, + }) + end, + } +end, {}) + +previewers.vimgrep = defaulter(function(opts) + opts = opts or {} + local cwd = opts.cwd or vim.loop.cwd() + + local jump_to_line = function(self, bufnr, lnum) + pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_previewer, 0, -1) + if lnum and lnum > 0 then + pcall(vim.api.nvim_buf_add_highlight, bufnr, ns_previewer, "TelescopePreviewLine", lnum - 1, 0, -1) + pcall(vim.api.nvim_win_set_cursor, self.state.winid, { lnum, 0 }) + vim.api.nvim_buf_call(bufnr, function() + vim.cmd "norm! zz" + end) + end + end + + return previewers.new_buffer_previewer { + title = "Grep Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, false, false)):normalize(cwd) + end, + + get_buffer_by_name = function(_, entry) + return from_entry.path(entry, false) + end, + + define_preview = function(self, entry, status) + -- builtin.buffers: bypass path validation for terminal buffers that don't have appropriate path + local has_buftype = entry.bufnr and vim.api.nvim_buf_get_option(entry.bufnr, "buftype") ~= "" or false + local p + if not has_buftype then + p = from_entry.path(entry, true) + if p == nil or p == "" then + return + end + end + + -- Workaround for unnamed buffer when using builtin.buffer + if entry.bufnr and (p == "[No Name]" or has_buftype) then + local lines = vim.api.nvim_buf_get_lines(entry.bufnr, 0, -1, false) + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, lines) + jump_to_line(self, self.state.bufnr, entry.lnum) + else + conf.buffer_previewer_maker(p, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + preview = opts.preview, + callback = function(bufnr) + jump_to_line(self, bufnr, entry.lnum) + end, + }) + end + end, + } +end, {}) + +previewers.qflist = previewers.vimgrep + +previewers.ctags = defaulter(function(_) + local determine_jump = function(entry) + if entry.scode then + return function(self) + -- un-escape / then escape required + -- special chars for vim.fn.search() + -- ] ~ * + local scode = entry.scode:gsub([[\/]], "/"):gsub("[%]~*]", function(x) + return "\\" .. x + end) + + pcall(vim.fn.matchdelete, self.state.hl_id, self.state.winid) + vim.cmd "norm! gg" + vim.fn.search(scode, "W") + vim.cmd "norm! zz" + + self.state.hl_id = vim.fn.matchadd("TelescopePreviewMatch", scode) + end + else + return function(self, bufnr) + if self.state.last_set_bufnr then + pcall(vim.api.nvim_buf_clear_namespace, self.state.last_set_bufnr, ns_previewer, 0, -1) + end + pcall(vim.api.nvim_buf_add_highlight, bufnr, ns_previewer, "TelescopePreviewMatch", entry.lnum - 1, 0, -1) + pcall(vim.api.nvim_win_set_cursor, self.state.winid, { entry.lnum, 0 }) + self.state.last_set_bufnr = bufnr + end + end + end + + return previewers.new_buffer_previewer { + title = "Tags Preview", + teardown = function(self) + if self.state and self.state.hl_id then + pcall(vim.fn.matchdelete, self.state.hl_id, self.state.hl_win) + self.state.hl_id = nil + elseif self.state and self.state.last_set_bufnr and vim.api.nvim_buf_is_valid(self.state.last_set_bufnr) then + vim.api.nvim_buf_clear_namespace(self.state.last_set_bufnr, ns_previewer, 0, -1) + end + end, + + get_buffer_by_name = function(_, entry) + return entry.filename + end, + + define_preview = function(self, entry, status) + conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + callback = function(bufnr) + pcall(vim.api.nvim_buf_call, bufnr, function() + determine_jump(entry)(self, bufnr) + end) + end, + }) + end, + } +end, {}) + +previewers.builtin = defaulter(function(_) + return previewers.new_buffer_previewer { + title = "Grep Preview", + teardown = search_teardown, + + get_buffer_by_name = function(_, entry) + return entry.filename + end, + + define_preview = function(self, entry, status) + local module_name = vim.fn.fnamemodify(vim.fn.fnamemodify(entry.filename, ":h"), ":t") + local text + if entry.text:sub(1, #module_name) ~= module_name then + text = module_name .. "." .. entry.text + else + text = entry.text:gsub("_", ".", 1) + end + + conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + callback = function(bufnr) + search_cb_jump(self, bufnr, text) + end, + }) + end, + } +end, {}) + +previewers.help = defaulter(function(_) + return previewers.new_buffer_previewer { + title = "Help Preview", + teardown = search_teardown, + + get_buffer_by_name = function(_, entry) + return entry.filename + end, + + define_preview = function(self, entry, status) + local query = entry.cmd + query = query:sub(2) + query = [[\V]] .. query + + conf.buffer_previewer_maker(entry.filename, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + callback = function(bufnr) + putils.regex_highlighter(bufnr, "help") + search_cb_jump(self, bufnr, query) + end, + }) + end, + } +end, {}) + +previewers.man = defaulter(function(opts) + local pager = utils.get_lazy_default(opts.PAGER, function() + return vim.fn.executable "col" == 1 and { "col", "-bx" } or { "cat" } + end) + return previewers.new_buffer_previewer { + title = "Man Preview", + get_buffer_by_name = function(_, entry) + return entry.value .. "/" .. entry.section + end, + + define_preview = function(self, entry, status) + local win_width = vim.api.nvim_win_get_width(self.state.winid) + putils.job_maker(vim.deepcopy(pager), self.state.bufnr, { + writer = { "man", entry.section, entry.value }, + env = { ["MANWIDTH"] = win_width, PATH = vim.env.PATH, MANPATH = vim.env.MANPATH }, + value = entry.value .. "/" .. entry.section, + bufname = self.state.bufname, + }) + putils.regex_highlighter(self.state.bufnr, "man") + end, + } +end) + +previewers.git_branch_log = defaulter(function(opts) + local highlight_buffer = function(bufnr, content) + for i = 1, #content do + local line = content[i] + local _, hstart = line:find "[%*%s|]*" + if hstart then + local hend = hstart + 7 + if hend < #line then + pcall( + vim.api.nvim_buf_add_highlight, + bufnr, + ns_previewer, + "TelescopeResultsIdentifier", + i - 1, + hstart - 1, + hend + ) + end + end + local _, cstart = line:find "- %(" + if cstart then + local cend = string.find(line, "%) ") + if cend then + pcall( + vim.api.nvim_buf_add_highlight, + bufnr, + ns_previewer, + "TelescopeResultsConstant", + i - 1, + cstart - 1, + cend + ) + end + end + local dstart, _ = line:find " %(%d" + if dstart then + pcall( + vim.api.nvim_buf_add_highlight, + bufnr, + ns_previewer, + "TelescopeResultsSpecialComment", + i - 1, + dstart, + #line + ) + end + end + end + + return previewers.new_buffer_previewer { + title = "Git Branch Preview", + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + local cmd = { + "git", + "--no-pager", + "log", + "--graph", + "--pretty=format:%h -%d %s (%cr)", + "--abbrev-commit", + "--date=relative", + entry.value, + } + + putils.job_maker(cmd, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr, content) + if not content then + return + end + highlight_buffer(bufnr, content) + end, + }) + end, + } +end, {}) + +previewers.git_stash_diff = defaulter(function(opts) + return previewers.new_buffer_previewer { + title = "Git Stash Preview", + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, _) + putils.job_maker({ "git", "--no-pager", "stash", "show", "-p", entry.value }, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr) + if vim.api.nvim_buf_is_valid(bufnr) then + putils.regex_highlighter(bufnr, "diff") + end + end, + }) + end, + } +end, {}) + +previewers.git_commit_diff_to_parent = defaulter(function(opts) + return previewers.new_buffer_previewer { + title = "Git Diff to Parent Preview", + teardown = search_teardown, + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + local cmd = { "git", "--no-pager", "diff", entry.value .. "^!" } + if opts.current_file then + table.insert(cmd, "--") + table.insert(cmd, opts.current_file) + end + + putils.job_maker(cmd, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr) + if vim.api.nvim_buf_is_valid(bufnr) then + search_cb_jump(self, bufnr, opts.current_line) + putils.regex_highlighter(bufnr, "diff") + end + end, + }) + end, + } +end, {}) + +previewers.git_commit_diff_to_head = defaulter(function(opts) + return previewers.new_buffer_previewer { + title = "Git Diff to Head Preview", + teardown = search_teardown, + + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + local cmd = { "git", "--no-pager", "diff", "--cached", entry.value } + if opts.current_file then + table.insert(cmd, "--") + table.insert(cmd, opts.current_file) + end + + putils.job_maker(cmd, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr) + if vim.api.nvim_buf_is_valid(bufnr) then + search_cb_jump(self, bufnr, opts.current_line) + putils.regex_highlighter(bufnr, "diff") + end + end, + }) + end, + } +end, {}) + +previewers.git_commit_diff_as_was = defaulter(function(opts) + return previewers.new_buffer_previewer { + title = "Git Show Preview", + teardown = search_teardown, + + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + local cmd = { "git", "--no-pager", "show" } + local cf = opts.current_file and Path:new(opts.current_file):make_relative(opts.cwd) + local value = cf and (entry.value .. ":" .. cf) or entry.value + local ft = cf and pfiletype.detect(value) or "diff" + table.insert(cmd, value) + + putils.job_maker(cmd, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr) + if vim.api.nvim_buf_is_valid(bufnr) then + search_cb_jump(self, bufnr, opts.current_line) + putils.regex_highlighter(bufnr, ft) + end + end, + }) + end, + } +end, {}) + +previewers.git_commit_message = defaulter(function(opts) + local hl_map = { + "TelescopeResultsIdentifier", + "TelescopePreviewUser", + "TelescopePreviewDate", + } + return previewers.new_buffer_previewer { + title = "Git Message", + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + local cmd = { "git", "--no-pager", "log", "-n 1", entry.value } + + putils.job_maker(cmd, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr, content) + if not content then + return + end + for k, v in ipairs(hl_map) do + local _, s = content[k]:find "%s" + if s then + vim.api.nvim_buf_add_highlight(bufnr, ns_previewer, v, k - 1, s, #content[k]) + end + end + end, + }) + end, + } +end, {}) + +previewers.git_file_diff = defaulter(function(opts) + return previewers.new_buffer_previewer { + title = "Git File Diff Preview", + get_buffer_by_name = function(_, entry) + return entry.value + end, + + define_preview = function(self, entry, status) + if entry.status and (entry.status == "??" or entry.status == "A ") then + local p = from_entry.path(entry, true) + if p == nil or p == "" then + return + end + conf.buffer_previewer_maker(p, self.state.bufnr, { + bufname = self.state.bufname, + winid = self.state.winid, + }) + else + putils.job_maker({ "git", "--no-pager", "diff", entry.value }, self.state.bufnr, { + value = entry.value, + bufname = self.state.bufname, + cwd = opts.cwd, + callback = function(bufnr) + if vim.api.nvim_buf_is_valid(bufnr) then + putils.regex_highlighter(bufnr, "diff") + end + end, + }) + end + end, + } +end, {}) + +previewers.autocommands = defaulter(function(_) + return previewers.new_buffer_previewer { + title = "Autocommands Preview", + teardown = function(self) + if self.state and self.state.last_set_bufnr and vim.api.nvim_buf_is_valid(self.state.last_set_bufnr) then + pcall(vim.api.nvim_buf_clear_namespace, self.state.last_set_bufnr, ns_previewer, 0, -1) + end + end, + + get_buffer_by_name = function(_, entry) + return entry.value.group_name + end, + + define_preview = function(self, entry, status) + local results = vim.tbl_filter(function(x) + return x.value.group_name == entry.value.group_name + end, status.picker.finder.results) + + if self.state.last_set_bufnr then + pcall(vim.api.nvim_buf_clear_namespace, self.state.last_set_bufnr, ns_previewer, 0, -1) + end + + local selected_row = 0 + if self.state.bufname ~= entry.value.group_name then + local display = {} + table.insert(display, string.format(" augroup: %s - [ %d entries ]", entry.value.group_name, #results)) + -- TODO: calculate banner width/string in setup() + -- TODO: get column characters to be the same HL group as border + table.insert(display, string.rep("─", vim.fn.getwininfo(status.preview_win)[1].width)) + + for idx, item in ipairs(results) do + if item == entry then + selected_row = idx + end + table.insert( + display, + string.format(" %-14s▏%-08s %s", item.value.event, item.value.pattern, item.value.command) + ) + end + + vim.api.nvim_buf_set_option(self.state.bufnr, "filetype", "vim") + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, display) + vim.api.nvim_buf_add_highlight(self.state.bufnr, 0, "TelescopeBorder", 1, 0, -1) + else + for idx, item in ipairs(results) do + if item == entry then + selected_row = idx + break + end + end + end + + vim.api.nvim_buf_add_highlight(self.state.bufnr, ns_previewer, "TelescopePreviewLine", selected_row + 1, 0, -1) + -- set the cursor position after self.state.bufnr is connected to the + -- preview window (which is scheduled in new_buffer_previewer) + vim.schedule(function() + pcall(vim.api.nvim_win_set_cursor, status.preview_win, { selected_row, 0 }) + end) + + self.state.last_set_bufnr = self.state.bufnr + end, + } +end, {}) + +previewers.highlights = defaulter(function(_) + return previewers.new_buffer_previewer { + title = "Highlights Preview", + teardown = function(self) + if self.state and self.state.last_set_bufnr and vim.api.nvim_buf_is_valid(self.state.last_set_bufnr) then + vim.api.nvim_buf_clear_namespace(self.state.last_set_bufnr, ns_previewer, 0, -1) + end + end, + + get_buffer_by_name = function() + return "highlights" + end, + + define_preview = function(self, entry, status) + putils.with_preview_window(status, nil, function() + if not self.state.bufname then + local output = vim.split(vim.fn.execute "highlight", "\n") + local hl_groups = {} + for _, v in ipairs(output) do + if v ~= "" then + if v:sub(1, 1) == " " then + local part_of_old = v:match "%s+(.*)" + hl_groups[#hl_groups] = hl_groups[#hl_groups] .. part_of_old + else + table.insert(hl_groups, v) + end + end + end + + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, hl_groups) + for k, v in ipairs(hl_groups) do + local startPos = string.find(v, "xxx", 1, true) - 1 + local endPos = startPos + 3 + local hlgroup = string.match(v, "([^ ]*)%s+.*") + pcall(vim.api.nvim_buf_add_highlight, self.state.bufnr, 0, hlgroup, k - 1, startPos, endPos) + end + end + + pcall(vim.api.nvim_buf_clear_namespace, self.state.bufnr, ns_previewer, 0, -1) + vim.cmd "norm! gg" + vim.fn.search(entry.value .. " ") + local lnum = vim.fn.line "." + -- That one is actually a match but its better to use it like that then matchadd + vim.api.nvim_buf_add_highlight( + self.state.bufnr, + ns_previewer, + "TelescopePreviewMatch", + lnum - 1, + 0, + #entry.value + ) + end) + end, + } +end, {}) + +previewers.pickers = defaulter(function(_) + local ns_telescope_multiselection = vim.api.nvim_create_namespace "telescope_mulitselection" + local get_row = function(picker, preview_height, index) + if picker.sorting_strategy == "ascending" then + return index - 1 + else + return preview_height - index + end + end + return previewers.new_buffer_previewer { + + dyn_title = function(_, entry) + if entry.value.default_text and entry.value.default_text ~= "" then + return string.format("%s ─ %s", entry.value.prompt_title, entry.value.default_text) + end + return entry.value.prompt_title + end, + + get_buffer_by_name = function(_, entry) + return tostring(entry.value.prompt_bufnr) + end, + + teardown = function(self) + if self.state and self.state.last_set_bufnr and vim.api.nvim_buf_is_valid(self.state.last_set_bufnr) then + vim.api.nvim_buf_clear_namespace(self.state.last_set_bufnr, ns_telescope_multiselection, 0, -1) + end + end, + + define_preview = function(self, entry, status) + putils.with_preview_window(status, nil, function() + local ns_telescope_entry = vim.api.nvim_create_namespace "telescope_entry" + local preview_height = vim.api.nvim_win_get_height(status.preview_win) + + if self.state.bufname then + return + end + + local picker = entry.value + -- prefill buffer to be able to set lines individually + local placeholder = utils.repeated_table(preview_height, "") + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, placeholder) + + for index = 1, math.min(preview_height, picker.manager:num_results()) do + local row = get_row(picker, preview_height, index) + local e = picker.manager:get_entry(index) + + local display, display_highlight + -- if-clause as otherwise function return values improperly unpacked + if type(e.display) == "function" then + display, display_highlight = e:display() + else + display = e.display + end + + vim.api.nvim_buf_set_lines(self.state.bufnr, row, row + 1, false, { display }) + + if display_highlight ~= nil then + for _, hl_block in ipairs(display_highlight) do + vim.api.nvim_buf_add_highlight( + self.state.bufnr, + ns_telescope_entry, + hl_block[2], + row, + hl_block[1][1], + hl_block[1][2] + ) + end + end + if picker._multi:is_selected(e) then + vim.api.nvim_buf_add_highlight( + self.state.bufnr, + ns_telescope_multiselection, + "TelescopeMultiSelection", + row, + 0, + -1 + ) + end + end + end) + end, + } +end, {}) + +previewers.display_content = defaulter(function(_) + return previewers.new_buffer_previewer { + define_preview = function(self, entry, status) + putils.with_preview_window(status, nil, function() + assert( + type(entry.preview_command) == "function", + "entry must provide a preview_command function which will put the content into the buffer" + ) + entry.preview_command(entry, self.state.bufnr) + end) + end, + } +end, {}) + +return previewers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/init.lua new file mode 100644 index 0000000..4b20f06 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/init.lua @@ -0,0 +1,315 @@ +---@tag telescope.previewers +---@config { ["module"] = "telescope.previewers" } + +---@brief [[ +--- Provides a Previewer table that has to be implemented by each previewer. +--- To achieve this, this module also provides two wrappers that abstract most +--- of the work and make it really easy create new previewers. +--- - `previewers.new_termopen_previewer` +--- - `previewers.new_buffer_previewer` +--- +--- Furthermore, there are a collection of previewers already defined which +--- can be used for every picker, as long as the entries of the picker provide +--- the necessary fields. The more important ones are +--- - `previewers.cat` +--- - `previewers.vimgrep` +--- - `previewers.qflist` +--- - `previewers.vim_buffer_cat` +--- - `previewers.vim_buffer_vimgrep` +--- - `previewers.vim_buffer_qflist` +--- +--- Previewers can be disabled for any builtin or custom picker by doing +--- :Telescope find_files previewer=false +---@brief ]] + +local Previewer = require "telescope.previewers.previewer" +local term_previewer = require "telescope.previewers.term_previewer" +local buffer_previewer = require "telescope.previewers.buffer_previewer" + +local previewers = {} + +--- This is the base table all previewers have to implement. It's possible to +--- write a wrapper for this because most previewers need to have the same +--- keys set. +--- Examples of wrappers are: +--- - `new_buffer_previewer` +--- - `new_termopen_previewer` +--- +--- To create a new table do following: +--- - `local new_previewer = Previewer:new(opts)` +--- +--- What `:new` expects is listed below +--- +--- The interface provides following set of functions. All of them, besides +--- `new`, will be handled by telescope pickers. +--- - `:new(opts)` +--- - `:preview(entry, status)` +--- - `:teardown()` +--- - `:send_input(input)` +--- - `:scroll_fn(direction)` +--- +--- `Previewer:new()` expects a table as input with following keys: +--- - `setup` function(self): Will be called the first time preview will be +--- called. +--- - `teardown` function(self): Will be called on cleanup. +--- - `preview_fn` function(self, entry, status): Will be called each time +--- a new entry was selected. +--- - `title` function(self): Will return the static title of the previewer. +--- - `dynamic_title` function(self, entry): Will return the dynamic title of +--- the previewer. Will only be called +--- when config value dynamic_preview_title +--- is true. +--- - `send_input` function(self, input): This is meant for +--- `termopen_previewer` and it can be +--- used to send input to the terminal +--- application, like less. +--- - `scroll_fn` function(self, direction): Used to make scrolling work. +previewers.Previewer = Previewer + +--- A shorthand for creating a new Previewer. +--- The provided table will be forwarded to `Previewer:new(...)` +previewers.new = function(...) + return Previewer:new(...) +end + +--- Is a wrapper around Previewer and helps with creating a new +--- `termopen_previewer`. +--- +--- It requires you to specify one table entry `get_command(entry, status)`. +--- This `get_command` function has to return the terminal command that will be +--- executed for each entry. Example: +--- +--- get_command = function(entry, status) +--- return { 'bat', entry.path } +--- end +--- +--- +--- Additionally you can define: +--- - `title` a static title for example "File Preview" +--- - `dyn_title(self, entry)` a dynamic title function which gets called +--- when config value `dynamic_preview_title = true` +--- +--- It's an easy way to get your first previewer going and it integrates well +--- with `bat` and `less`. Providing out of the box scrolling if the command +--- uses less. +--- +--- Furthermore, it will forward all `config.set_env` environment variables to +--- that terminal process. +previewers.new_termopen_previewer = term_previewer.new_termopen_previewer + +--- Provides a `termopen_previewer` which has the ability to display files. +--- It will always show the top of the file and has support for +--- `bat`(prioritized) and `cat`. Each entry has to provide either the field +--- `path` or `filename` in order to make this previewer work. +--- +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.cat_previewer` +--- This will respect user configuration and will use `buffer_previewers` in +--- case it's configured that way. +previewers.cat = term_previewer.cat + +--- Provides a `termopen_previewer` which has the ability to display files at +--- the provided line. It has support for `bat`(prioritized) and `cat`. +--- Each entry has to provide either the field `path` or `filename` and +--- a `lnum` field in order to make this previewer work. +--- +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.grep_previewer` +--- This will respect user configuration and will use `buffer_previewers` in +--- case it's configured that way. +previewers.vimgrep = term_previewer.vimgrep + +--- Provides a `termopen_previewer` which has the ability to display files at +--- the provided line or range. It has support for `bat`(prioritized) and +--- `cat`. Each entry has to provide either the field `path` or `filename`, +--- `lnum` and a `start` and `finish` range in order to make this previewer +--- work. +--- +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.qflist_previewer` +--- This will respect user configuration and will use buffer previewers in +--- case it's configured that way. +previewers.qflist = term_previewer.qflist + +--- An interface to instantiate a new `buffer_previewer`. +--- That means that the content actually lives inside a vim buffer which +--- enables us more control over the actual content. For example, we can +--- use `vim.fn.search` to jump to a specific line or reuse buffers/already +--- opened files more easily. +--- This interface is more complex than `termopen_previewer` but offers more +--- flexibility over your content. +--- It was designed to display files but was extended to also display the +--- output of terminal commands. +--- +--- In the following options, state table and general tips are mentioned to +--- make your experience with this previewer more seamless. +--- +--- +--- options: +--- - `define_preview = function(self, entry, status)` (required) +--- Is called for each selected entry, after each selection_move +--- (up or down) and is meant to handle things like reading file, +--- jump to line or attach a highlighter. +--- - `setup = function(self)` (optional) +--- Is called once at the beginning, before the preview for the first +--- entry is displayed. You can return a table of vars that will be +--- available in `self.state` in each `define_preview` call. +--- - `teardown = function(self)` (optional) +--- Will be called at the end, when the picker is being closed and is +--- meant to cleanup everything that was allocated by the previewer. +--- The `buffer_previewer` will automatically cleanup all created buffers. +--- So you only need to handle things that were introduced by you. +--- - `keep_last_buf = true` (optional) +--- Will not delete the last selected buffer. This would allow you to +--- reuse that buffer in the select action. For example, that buffer can +--- be opened in a new split, rather than recreating that buffer in +--- an action. To access the last buffer number: +--- `require('telescope.state').get_global_key("last_preview_bufnr")` +--- - `get_buffer_by_name = function(self, entry)` +--- Allows you to set a unique name for each buffer. This is used for +--- caching purpose. `self.state.bufname` will be nil if the entry was +--- never loaded or the unique name when it was loaded once. For example, +--- useful if you have one file but multiple entries. This happens for grep +--- and lsp builtins. So to make the cache work only load content if +--- `self.state.bufname ~= entry.your_unique_key` +--- - `title` a static title for example "File Preview" +--- - `dyn_title(self, entry)` a dynamic title function which gets called +--- when config value `dynamic_preview_title = true` +--- +--- `self.state` table: +--- - `self.state.bufnr` +--- Is the current buffer number, in which you have to write the loaded +--- content. +--- Don't create a buffer yourself, otherwise it's not managed by the +--- buffer_previewer interface and you will probably be better off +--- writing your own interface. +--- - self.state.winid +--- Current window id. Useful if you want to set the cursor to a provided +--- line number. +--- - self.state.bufname +--- Will return the current buffer name, if `get_buffer_by_name` is +--- defined. nil will be returned if the entry was never loaded or when +--- `get_buffer_by_name` is not set. +--- +--- Tips: +--- - If you want to display content of a terminal job, use: +--- `require('telescope.previewers.utils').job_maker(cmd, bufnr, opts)` +--- - `cmd` table: for example { 'git', 'diff', entry.value } +--- - `bufnr` number: in which the content will be written +--- - `opts` table: with following keys +--- - `bufname` string: used for cache +--- - `value` string: used for cache +--- - `mode` string: either "insert" or "append". "insert" is default +--- - `env` table: define environment variables. Example: +--- - `{ ['PAGER'] = '', ['MANWIDTH'] = 50 }` +--- - `cwd` string: define current working directory for job +--- - `callback` function(bufnr, content): will be called when job +--- is done. Content will be nil if job is already loaded. +--- So you can do highlighting only the first time the previewer +--- is created for that entry. +--- Use the returned `bufnr` and not `self.state.bufnr` in callback, +--- because state can already be changed at this point in time. +--- - If you want to attach a highlighter use: +--- - `require('telescope.previewers.utils').highlighter(bufnr, ft)` +--- - This will prioritize tree sitter highlighting if available for +--- environment and language. +--- - `require('telescope.previewers.utils').regex_highlighter(bufnr, ft)` +--- - `require('telescope.previewers.utils').ts_highlighter(bufnr, ft)` +--- - If you want to use `vim.fn.search` or similar you need to run it in +--- that specific buffer context. Do +--- +--- vim.api.nvim_buf_call(bufnr, function() +--- -- for example `search` and `matchadd` +--- end) +--- to achieve that. +--- +--- - If you want to read a file into the buffer it's best to use +--- `buffer_previewer_maker`. But access this function with +--- `require('telescope.config').values.buffer_previewer_maker` +--- because it can be redefined by users. +previewers.new_buffer_previewer = buffer_previewer.new_buffer_previewer + +--- A universal way of reading a file into a buffer previewer. +--- It handles async reading, cache, highlighting, displaying directories +--- and provides a callback which can be used, to jump to a line in the buffer. +---@param filepath string: String to the filepath, will be expanded +---@param bufnr number: Where the content will be written +---@param opts table: keys: `use_ft_detect`, `bufname` and `callback` +previewers.buffer_previewer_maker = buffer_previewer.file_maker + +--- A previewer that is used to display a file. It uses the `buffer_previewer` +--- interface and won't jump to the line. To integrate this one into your +--- own picker make sure that the field `path` or `filename` is set for +--- each entry. +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.file_previewer` +--- This will respect user configuration and will use `termopen_previewer` in +--- case it's configured that way. +previewers.vim_buffer_cat = buffer_previewer.cat + +--- A previewer that is used to display a file and jump to the provided line. +--- It uses the `buffer_previewer` interface. To integrate this one into your +--- own picker make sure that the field `path` or `filename` and `lnum` is set +--- in each entry. If the latter is not present, it will default to the first +--- line. +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.grep_previewer` +--- This will respect user configuration and will use `termopen_previewer` in +--- case it's configured that way. +previewers.vim_buffer_vimgrep = buffer_previewer.vimgrep + +--- Is the same as `vim_buffer_vimgrep` and only exist for consistency with +--- `term_previewers`. +--- +--- The preferred way of using this previewer is like this +--- `require('telescope.config').values.qflist_previewer` +--- This will respect user configuration and will use `termopen_previewer` in +--- case it's configured that way. +previewers.vim_buffer_qflist = buffer_previewer.qflist + +--- A previewer that shows a log of a branch as graph +previewers.git_branch_log = buffer_previewer.git_branch_log + +--- A previewer that shows a diff of a stash +previewers.git_stash_diff = buffer_previewer.git_stash_diff + +--- A previewer that shows a diff of a commit to a parent commit.
+--- The run command is `git --no-pager diff SHA^! -- $CURRENT_FILE` +--- +--- The current file part is optional. So is only uses it with bcommits. +previewers.git_commit_diff_to_parent = buffer_previewer.git_commit_diff_to_parent + +--- A previewer that shows a diff of a commit to head.
+--- The run command is `git --no-pager diff --cached $SHA -- $CURRENT_FILE` +--- +--- The current file part is optional. So is only uses it with bcommits. +previewers.git_commit_diff_to_head = buffer_previewer.git_commit_diff_to_head + +--- A previewer that shows a diff of a commit as it was.
+--- The run command is `git --no-pager show $SHA:$CURRENT_FILE` or `git --no-pager show $SHA` +previewers.git_commit_diff_as_was = buffer_previewer.git_commit_diff_as_was + +--- A previewer that shows the commit message of a diff.
+--- The run command is `git --no-pager log -n 1 $SHA` +previewers.git_commit_message = buffer_previewer.git_commit_message + +--- A previewer that shows the current diff of a file. Used in git_status.
+--- The run command is `git --no-pager diff $FILE` +previewers.git_file_diff = buffer_previewer.git_file_diff + +previewers.ctags = buffer_previewer.ctags +previewers.builtin = buffer_previewer.builtin +previewers.help = buffer_previewer.help +previewers.man = buffer_previewer.man +previewers.autocommands = buffer_previewer.autocommands +previewers.highlights = buffer_previewer.highlights +previewers.pickers = buffer_previewer.pickers + +--- A deprecated way of displaying content more easily. Was written at a time, +--- where the buffer_previewer interface wasn't present. Nowadays it's easier +--- to just use this. We will keep it around for backwards compatibility +--- because some extensions use it. +--- It doesn't use cache or some other clever tricks. +previewers.display_content = buffer_previewer.display_content + +return previewers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/previewer.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/previewer.lua new file mode 100644 index 0000000..f986dac --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/previewer.lua @@ -0,0 +1,98 @@ +local utils = require "telescope.utils" + +local Previewer = {} +Previewer.__index = Previewer + +local force_function_wrap = function(value) + if value ~= nil then + if type(value) ~= "function" then + return function() + return tostring(value) + end + else + return value + end + end +end + +function Previewer:new(opts) + opts = opts or {} + + return setmetatable({ + state = nil, + _title_fn = force_function_wrap(opts.title), + _dyn_title_fn = force_function_wrap(opts.dyn_title), + _setup_func = opts.setup, + _teardown_func = opts.teardown, + _send_input = opts.send_input, + _scroll_fn = opts.scroll_fn, + preview_fn = opts.preview_fn, + _empty_bufnr = nil, + }, Previewer) +end + +function Previewer:preview(entry, status) + if not entry then + if not self._empty_bufnr then + self._empty_bufnr = vim.api.nvim_create_buf(false, true) + end + + if vim.api.nvim_buf_is_valid(self._empty_bufnr) then + vim.api.nvim_win_set_buf(status.preview_win, self._empty_bufnr) + end + return + end + + if not self.state then + if self._setup_func then + self.state = self:_setup_func(status) + else + self.state = {} + end + end + + return self:preview_fn(entry, status) +end + +function Previewer:title(entry, dynamic) + if dynamic == true and self._dyn_title_fn ~= nil then + if entry == nil then + if self._title_fn ~= nil then + return self:_title_fn() + else + return "" + end + end + return self:_dyn_title_fn(entry) + end + if self._title_fn ~= nil then + return self:_title_fn() + end +end + +function Previewer:teardown() + if self._empty_bufnr then + utils.buf_delete(self._empty_bufnr) + end + if self._teardown_func then + self:_teardown_func() + end +end + +function Previewer:send_input(input) + if self._send_input then + self:_send_input(input) + else + vim.api.nvim_err_writeln "send_input is not defined for this previewer" + end +end + +function Previewer:scroll_fn(direction) + if self._scroll_fn then + self:_scroll_fn(direction) + else + vim.api.nvim_err_writeln "scroll_fn is not defined for this previewer" + end +end + +return Previewer diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/term_previewer.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/term_previewer.lua new file mode 100644 index 0000000..368f6a9 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/term_previewer.lua @@ -0,0 +1,339 @@ +local conf = require("telescope.config").values +local utils = require "telescope.utils" +local Path = require "plenary.path" +local from_entry = require "telescope.from_entry" +local Previewer = require "telescope.previewers.previewer" + +local defaulter = utils.make_default_callable + +local previewers = {} + +-- TODO: Should play with these some more, ty @clason +local bat_options = { "--style=plain", "--color=always", "--paging=always" } +local has_less = (vim.fn.executable "less" == 1) and conf.use_less + +local get_file_stat = function(filename) + return vim.loop.fs_stat(vim.fn.expand(filename)) or {} +end + +local list_dir = (function() + if vim.fn.has "win32" == 1 then + return function(dirname) + return { "cmd.exe", "/c", "dir", vim.fn.expand(dirname) } + end + else + return function(dirname) + return { "ls", "-la", vim.fn.expand(dirname) } + end + end +end)() + +local bat_maker = function(filename, lnum, start, finish) + if get_file_stat(filename).type == "directory" then + return list_dir(filename) + end + + local command = { "bat" } + + if lnum then + table.insert(command, { "--highlight-line", lnum }) + end + + if has_less then + if start then + table.insert(command, { "--pager", string.format("less -RS +%s", start) }) + else + table.insert(command, { "--pager", "less -RS" }) + end + else + if start and finish then + table.insert(command, { "-r", string.format("%s:%s", start, finish) }) + end + end + + return vim.tbl_flatten { + command, + bat_options, + "--", + vim.fn.expand(filename), + } +end + +local cat_maker = function(filename, _, start, _) + if get_file_stat(filename).type == "directory" then + return list_dir(filename) + end + + if 1 == vim.fn.executable "file" then + local output = utils.get_os_command_output { "file", "--mime-type", "-b", filename } + local mime_type = vim.split(output[1], "/")[1] + if mime_type ~= "text" then + return { "echo", "Binary file found. These files cannot be displayed!" } + end + end + + if has_less then + if start then + return { "less", "-RS", string.format("+%s", start), vim.fn.expand(filename) } + else + return { "less", "-RS", vim.fn.expand(filename) } + end + else + return { + "cat", + "--", + vim.fn.expand(filename), + } + end +end + +local get_maker = function(opts) + local maker = opts.maker + if not maker and 1 == vim.fn.executable "bat" then + maker = bat_maker + elseif not maker and 1 == vim.fn.executable "cat" then + maker = cat_maker + end + + if not maker then + error "Needs maker" + end + + return maker +end + +-- TODO: We shoudl make sure that all our terminals close all the way. +-- Otherwise it could be bad if they're just sitting around, waiting to be closed. +-- I don't think that's the problem, but it could be? +previewers.new_termopen_previewer = function(opts) + opts = opts or {} + + assert(opts.get_command, "get_command is a required function") + assert(not opts.preview_fn, "preview_fn not allowed") + + local opt_setup = opts.setup + local opt_teardown = opts.teardown + + local old_bufs = {} + local bufentry_table = {} + local term_ids = {} + + local function get_term_id(self) + if self.state then + return self.state.termopen_id + end + end + + local function get_bufnr(self) + if self.state then + return self.state.termopen_bufnr + end + end + + local function set_term_id(self, value) + if self.state and term_ids[self.state.termopen_bufnr] == nil then + term_ids[self.state.termopen_bufnr] = value + self.state.termopen_id = value + end + end + + local function set_bufnr(self, value) + if get_bufnr(self) then + table.insert(old_bufs, get_bufnr(self)) + end + if self.state then + self.state.termopen_bufnr = value + end + end + + local function get_bufnr_by_bufentry(self, value) + if self.state then + return bufentry_table[value] + end + end + + local function set_bufentry(self, value) + if self.state and value then + bufentry_table[value] = get_bufnr(self) + end + end + + function opts.setup(self) + local state = {} + if opt_setup then + vim.tbl_deep_extend("force", state, opt_setup(self)) + end + return state + end + + function opts.teardown(self) + if opt_teardown then + opt_teardown(self) + end + + set_bufnr(self, nil) + set_bufentry(self, nil) + + for _, bufnr in ipairs(old_bufs) do + local term_id = term_ids[bufnr] + if term_id and utils.job_is_running(term_id) then + vim.fn.jobstop(term_id) + end + utils.buf_delete(bufnr) + end + bufentry_table = {} + end + + function opts.preview_fn(self, entry, status) + if get_bufnr(self) == nil then + set_bufnr(self, vim.api.nvim_win_get_buf(status.preview_win)) + end + + local prev_bufnr = get_bufnr_by_bufentry(self, entry) + if prev_bufnr then + self.state.termopen_bufnr = prev_bufnr + utils.win_set_buf_noautocmd(status.preview_win, self.state.termopen_bufnr) + self.state.termopen_id = term_ids[self.state.termopen_bufnr] + else + local bufnr = vim.api.nvim_create_buf(false, true) + set_bufnr(self, bufnr) + utils.win_set_buf_noautocmd(status.preview_win, bufnr) + + local term_opts = { + cwd = opts.cwd or vim.loop.cwd(), + env = conf.set_env, + } + + local cmd = opts.get_command(entry, status) + if cmd then + vim.api.nvim_buf_call(bufnr, function() + set_term_id(self, vim.fn.termopen(cmd, term_opts)) + end) + end + set_bufentry(self, entry) + end + end + + if not opts.send_input then + function opts.send_input(self, input) + local termcode = vim.api.nvim_replace_termcodes(input, true, false, true) + + local term_id = get_term_id(self) + if term_id then + vim.fn.chansend(term_id, termcode) + end + end + end + + if not opts.scroll_fn then + function opts.scroll_fn(self, direction) + if not self.state then + return + end + + local input = direction > 0 and "d" or "u" + local count = math.abs(direction) + + self:send_input(count .. input) + end + end + + return Previewer:new(opts) +end + +previewers.cat = defaulter(function(opts) + opts = opts or {} + + local maker = get_maker(opts) + local cwd = opts.cwd or vim.loop.cwd() + + return previewers.new_termopen_previewer { + title = "File Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, false, false)):normalize(cwd) + end, + + get_command = function(entry) + local p = from_entry.path(entry, true, false) + if p == nil or p == "" then + return + end + + return maker(p) + end, + } +end, {}) + +previewers.vimgrep = defaulter(function(opts) + opts = opts or {} + + local maker = get_maker(opts) + local cwd = opts.cwd or vim.loop.cwd() + + return previewers.new_termopen_previewer { + title = "Grep Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, false, false)):normalize(cwd) + end, + + get_command = function(entry, status) + local win_id = status.preview_win + local height = vim.api.nvim_win_get_height(win_id) + + local p = from_entry.path(entry, true, false) + if p == nil or p == "" then + return + end + if entry.bufnr and (p == "[No Name]" or vim.api.nvim_buf_get_option(entry.bufnr, "buftype") ~= "") then + return + end + + local lnum = entry.lnum or 0 + + local context = math.floor(height / 2) + local start = math.max(0, lnum - context) + local finish = lnum + context + + return maker(p, lnum, start, finish) + end, + } +end, {}) + +previewers.qflist = defaulter(function(opts) + opts = opts or {} + + local maker = get_maker(opts) + local cwd = opts.cwd or vim.loop.cwd() + + return previewers.new_termopen_previewer { + title = "Grep Preview", + dyn_title = function(_, entry) + return Path:new(from_entry.path(entry, false, false)):normalize(cwd) + end, + + get_command = function(entry, status) + local win_id = status.preview_win + local height = vim.api.nvim_win_get_height(win_id) + + local p = from_entry.path(entry, true, false) + if p == nil or p == "" then + return + end + local lnum = entry.lnum + + local start, finish + if entry.start and entry.finish then + start = entry.start + finish = entry.finish + else + local context = math.floor(height / 2) + start = math.max(0, lnum - context) + finish = lnum + context + end + + return maker(p, lnum, start, finish) + end, + } +end, {}) + +return previewers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/utils.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/utils.lua new file mode 100644 index 0000000..bd8c300 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/previewers/utils.lua @@ -0,0 +1,206 @@ +local context_manager = require "plenary.context_manager" +local ts_utils = require "telescope.utils" +local strings = require "plenary.strings" +local conf = require("telescope.config").values + +local has_ts, _ = pcall(require, "nvim-treesitter") +local _, ts_configs = pcall(require, "nvim-treesitter.configs") +local _, ts_parsers = pcall(require, "nvim-treesitter.parsers") + +local Job = require "plenary.job" + +local utils = {} + +utils.with_preview_window = function(status, bufnr, callable) + if bufnr and vim.api.nvim_buf_call and false then + vim.api.nvim_buf_call(bufnr, callable) + else + return context_manager.with(function() + vim.cmd(string.format("noautocmd call nvim_set_current_win(%s)", status.preview_win)) + coroutine.yield() + vim.cmd(string.format("noautocmd call nvim_set_current_win(%s)", status.prompt_win)) + end, callable) + end +end + +-- API helper functions for buffer previewer +--- Job maker for buffer previewer +utils.job_maker = function(cmd, bufnr, opts) + opts = opts or {} + opts.mode = opts.mode or "insert" + -- bufname and value are optional + -- if passed, they will be use as the cache key + -- if any of them are missing, cache will be skipped + if opts.bufname ~= opts.value or not opts.bufname or not opts.value then + local command = table.remove(cmd, 1) + local writer = (function() + if opts.writer ~= nil then + local wcommand = table.remove(opts.writer, 1) + return Job:new { + command = wcommand, + args = opts.writer, + env = opts.env, + cwd = opts.cwd, + } + end + end)() + + Job:new({ + command = command, + args = cmd, + env = opts.env, + cwd = opts.cwd, + writer = writer, + on_exit = vim.schedule_wrap(function(j) + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + if opts.mode == "append" then + vim.api.nvim_buf_set_lines(bufnr, -1, -1, false, j:result()) + elseif opts.mode == "insert" then + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, j:result()) + end + if opts.callback then + opts.callback(bufnr, j:result()) + end + end), + }):start() + else + if opts.callback then + opts.callback(bufnr) + end + end +end + +local function has_filetype(ft) + return ft and ft ~= "" +end + +--- Attach default highlighter which will choose between regex and ts +utils.highlighter = function(bufnr, ft, opts) + opts = vim.F.if_nil(opts, {}) + opts.preview = vim.F.if_nil(opts.preview, {}) + opts.preview.treesitter = (function() + if type(opts.preview) == "table" and opts.preview.treesitter then + return opts.preview.treesitter + end + if type(conf.preview) == "table" and conf.preview.treesitter then + return conf.preview.treesitter + end + if type(conf.preview) == "boolean" then + return conf.preview + end + -- We should never get here + return false + end)() + + if type(opts.preview.treesitter) == "boolean" then + local temp = { enable = opts.preview.treesitter } + opts.preview.treesitter = temp + end + + local ts_highlighting = (function() + if type(opts.preview.treesitter.enable) == "table" then + if vim.tbl_contains(opts.preview.treesitter.enable, ft) then + return true + end + return false + end + + if vim.tbl_contains(vim.F.if_nil(opts.preview.treesitter.disable, {}), ft) then + return false + end + + return opts.preview.treesitter.enable == nil or opts.preview.treesitter.enable == true + end)() + + local ts_success + if ts_highlighting then + ts_success = utils.ts_highlighter(bufnr, ft) + end + if not ts_highlighting or ts_success == false then + utils.regex_highlighter(bufnr, ft) + end +end + +--- Attach regex highlighter +utils.regex_highlighter = function(bufnr, ft) + if has_filetype(ft) then + vim.api.nvim_buf_set_option(bufnr, "syntax", ft) + return true + end + return false +end + +local treesitter_attach = function(bufnr, ft) + local lang = ts_parsers.ft_to_lang(ft) + if not ts_configs.is_enabled("highlight", lang, bufnr) then + return false + end + + local config = ts_configs.get_module "highlight" + vim.treesitter.highlighter.new(ts_parsers.get_parser(bufnr, lang)) + local is_table = type(config.additional_vim_regex_highlighting) == "table" + if + config.additional_vim_regex_highlighting + and (not is_table or vim.tbl_contains(config.additional_vim_regex_highlighting, lang)) + then + vim.api.nvim_buf_set_option(bufnr, "syntax", ft) + end + return true +end + +-- Attach ts highlighter +utils.ts_highlighter = function(bufnr, ft) + if not has_ts then + has_ts, _ = pcall(require, "nvim-treesitter") + if has_ts then + _, ts_configs = pcall(require, "nvim-treesitter.configs") + _, ts_parsers = pcall(require, "nvim-treesitter.parsers") + end + end + + if has_ts and has_filetype(ft) then + return treesitter_attach(bufnr, ft) + end + return false +end + +utils.set_preview_message = function(bufnr, winid, message, fillchar) + fillchar = vim.F.if_nil(fillchar, "╱") + local height = vim.api.nvim_win_get_height(winid) + local width = vim.api.nvim_win_get_width(winid) + vim.api.nvim_buf_set_lines( + bufnr, + 0, + -1, + false, + ts_utils.repeated_table(height, table.concat(ts_utils.repeated_table(width, fillchar), "")) + ) + local anon_ns = vim.api.nvim_create_namespace "" + local padding = table.concat(ts_utils.repeated_table(#message + 4, " "), "") + local lines = { + padding, + " " .. message .. " ", + padding, + } + vim.api.nvim_buf_set_extmark( + bufnr, + anon_ns, + 0, + 0, + { end_line = height, hl_group = "TelescopePreviewMessageFillchar" } + ) + local col = math.floor((width - strings.strdisplaywidth(lines[2])) / 2) + for i, line in ipairs(lines) do + vim.api.nvim_buf_set_extmark( + bufnr, + anon_ns, + math.floor(height / 2) - 1 + i, + 0, + { virt_text = { { line, "TelescopePreviewMessage" } }, virt_text_pos = "overlay", virt_text_win_col = col } + ) + end +end + +return utils diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/sorters.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/sorters.lua new file mode 100644 index 0000000..6817c97 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/sorters.lua @@ -0,0 +1,616 @@ +local log = require "telescope.log" +local util = require "telescope.utils" + +local sorters = {} + +local ngram_highlighter = function(ngram_len, prompt, display) + local highlights = {} + display = display:lower() + + for disp_index = 1, #display do + local char = display:sub(disp_index, disp_index + ngram_len - 1) + if prompt:find(char, 1, true) then + table.insert(highlights, { + start = disp_index, + finish = disp_index + ngram_len - 1, + }) + end + end + + return highlights +end + +local FILTERED = -1 + +local Sorter = {} +Sorter.__index = Sorter + +---@class Sorter +--- Sorter sorts a list of results by return a single integer for a line, +--- given a prompt +--- +--- Lower number is better (because it's like a closer match) +--- But, any number below 0 means you want that line filtered out. +---@field scoring_function function: Function that has the interface: (sorter, prompt, line): number +---@field tags table: Unique tags collected at filtering for tag completion +---@field filter_function function: Function that can filter results +---@field highlighter function: Highlights results to display them pretty +---@field discard boolean: Whether this is a discardable style sorter or not. +---@field score function: Override the score function if desired. +---@field init function: Function to run when creating sorter +---@field start function: Function to run on every new prompt +---@field finish function: Function to run after every new prompt +---@field destroy function: Functo to run when destroying sorter +function Sorter:new(opts) + opts = opts or {} + + return setmetatable({ + score = opts.score, + state = {}, + tags = opts.tags, + + -- State management + init = opts.init, + start = opts.start, + finish = opts.finish, + destroy = opts.destroy, + _status = nil, + + filter_function = opts.filter_function, + scoring_function = opts.scoring_function, + highlighter = opts.highlighter, + discard = opts.discard, + _discard_state = { + filtered = {}, + prompt = "", + }, + }, Sorter) +end + +function Sorter:_init() + self._status = "init" + if self.init then + self:init() + end +end + +function Sorter:_destroy() + self._status = "destroy" + if self.destroy then + self:destroy() + end +end + +-- TODO: We could make this a bit smarter and cache results "as we go" and where they got filtered. +-- Then when we hit backspace, we don't have to re-caculate everything. +-- Prime did a lot of the hard work already, but I don't want to copy as much memory around +-- as he did in his example. +-- Example can be found in ./scratch/prime_prompt_cache.lua +function Sorter:_start(prompt) + self._status = "start" + if self.start then + self:start(prompt) + end + + if not self.discard then + return + end + + local previous = self._discard_state.prompt + local len_previous = #previous + + if #prompt < len_previous then + log.trace "Reset discard because shorter prompt" + self._discard_state.filtered = {} + elseif string.sub(prompt, 1, len_previous) ~= previous then + log.trace "Reset discard no match" + self._discard_state.filtered = {} + end + + self._discard_state.prompt = prompt +end + +function Sorter:_finish(prompt) + self._status = "finish" + if self.finish then + self:finish(prompt) + end +end + +-- TODO: Consider doing something that makes it so we can skip the filter checks +-- if we're not discarding. Also, that means we don't have to check otherwise as well :) +function Sorter:score(prompt, entry, cb_add, cb_filter) + if not entry or not entry.ordinal then + return + end + + if self._status and self._status ~= "start" then + return + end + + local ordinal = entry.ordinal + if self:_was_discarded(prompt, ordinal) then + return cb_filter(entry) + end + + local filter_score + if self.filter_function ~= nil then + if self.tags then + self.tags:insert(entry) + end + filter_score, prompt = self:filter_function(prompt, entry, cb_add, cb_filter) + end + + if filter_score == FILTERED then + return cb_filter(entry) + end + + local score = self:scoring_function(prompt or "", ordinal, entry, cb_add, cb_filter) + if score == FILTERED then + self:_mark_discarded(prompt, ordinal) + return cb_filter(entry) + end + + if cb_add then + return cb_add(score, entry) + else + return score + end +end + +function Sorter:_was_discarded(prompt, ordinal) + return self.discard and self._discard_state.filtered[ordinal] +end + +function Sorter:_mark_discarded(prompt, ordinal) + if not self.discard then + return + end + + self._discard_state.filtered[ordinal] = true +end + +function sorters.new(...) + return Sorter:new(...) +end + +sorters.Sorter = Sorter + +local make_cached_tail = function() + local os_sep = util.get_separator() + local match_string = "[^" .. os_sep .. "]*$" + return setmetatable({}, { + __index = function(t, k) + local tail = string.match(k, match_string) + + rawset(t, k, tail) + return tail + end, + }) +end + +local make_cached_uppers = function() + return setmetatable({}, { + __index = function(t, k) + local obj = {} + for i = 1, #k do + local s_byte = k:byte(i, i) + if s_byte <= 90 and s_byte >= 65 then + obj[s_byte] = true + end + end + + rawset(t, k, obj) + return obj + end, + }) +end + +-- TODO: Match on upper case words +-- TODO: Match on last match +sorters.get_fuzzy_file = function(opts) + opts = opts or {} + + local ngram_len = opts.ngram_len or 2 + + local cached_ngrams = {} + + local function overlapping_ngrams(s, n) + if cached_ngrams[s] and cached_ngrams[s][n] then + return cached_ngrams[s][n] + end + + local R = {} + for i = 1, s:len() - n + 1 do + R[#R + 1] = s:sub(i, i + n - 1) + end + + if not cached_ngrams[s] then + cached_ngrams[s] = {} + end + + cached_ngrams[s][n] = R + + return R + end + + local cached_tails = make_cached_tail() + local cached_uppers = make_cached_uppers() + + return Sorter:new { + scoring_function = function(_, prompt, line) + local N = #prompt + + if N == 0 or N < ngram_len then + -- TODO: If the character is in the line, + -- then it should get a point or somethin. + return 1 + end + + local prompt_lower = prompt:lower() + local line_lower = line:lower() + + local prompt_lower_ngrams = overlapping_ngrams(prompt_lower, ngram_len) + + -- Contains the original string + local contains_string = line_lower:find(prompt_lower, 1, true) + + local prompt_uppers = cached_uppers[prompt] + local line_uppers = cached_uppers[line] + + local uppers_matching = 0 + for k, _ in pairs(prompt_uppers) do + if line_uppers[k] then + uppers_matching = uppers_matching + 1 + end + end + + -- TODO: Consider case senstivity + local tail = cached_tails[line_lower] + local contains_tail = tail:find(prompt, 1, true) + + local consecutive_matches = 0 + local previous_match_index = 0 + local match_count = 0 + + for i = 1, #prompt_lower_ngrams do + local match_start = line_lower:find(prompt_lower_ngrams[i], 1, true) + if match_start then + match_count = match_count + 1 + if match_start > previous_match_index then + consecutive_matches = consecutive_matches + 1 + end + + previous_match_index = match_start + end + end + + local tail_modifier = 1 + if contains_tail then + tail_modifier = 2 + end + + local denominator = ( + (10 * match_count / #prompt_lower_ngrams) + -- biases for shorter strings + + 3 * match_count * ngram_len / #line + + consecutive_matches + + N / (contains_string or (2 * #line)) + -- + 30/(c1 or 2*N) + -- TODO: It might be possible that this too strongly correlates, + -- but it's unlikely for people to type capital letters without actually + -- wanting to do something with a capital letter in it. + + uppers_matching + ) * tail_modifier + + if denominator == 0 or denominator ~= denominator then + return -1 + end + + if #prompt > 2 and denominator < 0.5 then + return -1 + end + + return 1 / denominator + end, + + highlighter = opts.highlighter or function(_, prompt, display) + return ngram_highlighter(ngram_len, prompt, display) + end, + } +end + +sorters.get_generic_fuzzy_sorter = function(opts) + opts = opts or {} + + local ngram_len = opts.ngram_len or 2 + + local cached_ngrams = {} + local function overlapping_ngrams(s, n) + if cached_ngrams[s] and cached_ngrams[s][n] then + return cached_ngrams[s][n] + end + + local R = {} + for i = 1, s:len() - n + 1 do + R[#R + 1] = s:sub(i, i + n - 1) + end + + if not cached_ngrams[s] then + cached_ngrams[s] = {} + end + + cached_ngrams[s][n] = R + + return R + end + + return Sorter:new { + -- self + -- prompt (which is the text on the line) + -- line (entry.ordinal) + -- entry (the whole entry) + scoring_function = function(_, prompt, line, _) + if prompt == 0 or #prompt < ngram_len then + return 1 + end + + local prompt_lower = prompt:lower() + local line_lower = line:lower() + + local prompt_ngrams = overlapping_ngrams(prompt_lower, ngram_len) + + local N = #prompt + + local contains_string = line_lower:find(prompt_lower, 1, true) + + local consecutive_matches = 0 + local previous_match_index = 0 + local match_count = 0 + + for i = 1, #prompt_ngrams do + local match_start = line_lower:find(prompt_ngrams[i], 1, true) + if match_start then + match_count = match_count + 1 + if match_start > previous_match_index then + consecutive_matches = consecutive_matches + 1 + end + + previous_match_index = match_start + end + end + + -- TODO: Copied from ashkan. + local denominator = ( + (10 * match_count / #prompt_ngrams) + -- biases for shorter strings + -- TODO(ashkan): this can bias towards repeated finds of the same + -- subpattern with overlapping_ngrams + + 3 * match_count * ngram_len / #line + + consecutive_matches + + N / (contains_string or (2 * #line)) -- + 30/(c1 or 2*N) + + ) + + if denominator == 0 or denominator ~= denominator then + return -1 + end + + if #prompt > 2 and denominator < 0.5 then + return -1 + end + + return 1 / denominator + end, + + highlighter = opts.highlighter or function(_, prompt, display) + return ngram_highlighter(ngram_len, prompt, display) + end, + } +end + +sorters.fuzzy_with_index_bias = function(opts) + opts = opts or {} + opts.ngram_len = 2 + + -- TODO: Probably could use a better sorter here. + local fuzzy_sorter = sorters.get_generic_fuzzy_sorter(opts) + + return Sorter:new { + scoring_function = function(_, prompt, line, entry, cb_add, cb_filter) + local base_score = fuzzy_sorter:scoring_function(prompt, line, cb_add, cb_filter) + + if base_score == FILTERED then + return FILTERED + end + + if not base_score or base_score == 0 then + return entry.index + else + return math.min(math.pow(entry.index, 0.25), 2) * base_score + end + end, + highlighter = fuzzy_sorter.highlighter, + } +end + +-- Sorter using the fzy algorithm +sorters.get_fzy_sorter = function(opts) + opts = opts or {} + local fzy = opts.fzy_mod or require "telescope.algos.fzy" + local OFFSET = -fzy.get_score_floor() + + return sorters.Sorter:new { + discard = true, + + scoring_function = function(_, prompt, line) + -- Check for actual matches before running the scoring alogrithm. + if not fzy.has_match(prompt, line) then + return -1 + end + + local fzy_score = fzy.score(prompt, line) + + -- The fzy score is -inf for empty queries and overlong strings. Since + -- this function converts all scores into the range (0, 1), we can + -- convert these to 1 as a suitable "worst score" value. + if fzy_score == fzy.get_score_min() then + return 1 + end + + -- Poor non-empty matches can also have negative values. Offset the score + -- so that all values are positive, then invert to match the + -- telescope.Sorter "smaller is better" convention. Note that for exact + -- matches, fzy returns +inf, which when inverted becomes 0. + return 1 / (fzy_score + OFFSET) + end, + + -- The fzy.positions function, which returns an array of string indices, is + -- compatible with telescope's conventions. It's moderately wasteful to + -- call call fzy.score(x,y) followed by fzy.positions(x,y): both call the + -- fzy.compute function, which does all the work. But, this doesn't affect + -- perceived performance. + highlighter = function(_, prompt, display) + return fzy.positions(prompt, display) + end, + } +end + +-- TODO: Could probably do something nice where we check their conf +-- and choose their default for this. +-- But I think `fzy` is good default for now. +sorters.highlighter_only = function(opts) + opts = opts or {} + local fzy = opts.fzy_mod or require "telescope.algos.fzy" + + return Sorter:new { + scoring_function = function() + return 1 + end, + + highlighter = function(_, prompt, display) + return fzy.positions(prompt, display) + end, + } +end + +sorters.empty = function() + return Sorter:new { + scoring_function = function() + return 1 + end, + } +end + +-- Bad & Dumb Sorter +sorters.get_levenshtein_sorter = function() + return Sorter:new { + scoring_function = function(_, prompt, line) + return require "telescope.algos.string_distance"(prompt, line) + end, + } +end + +local substr_highlighter = function(_, prompt, display) + local highlights = {} + display = display:lower() + + local search_terms = util.max_split(prompt, "%s") + local hl_start, hl_end + + for _, word in pairs(search_terms) do + hl_start, hl_end = display:find(word, 1, true) + if hl_start then + table.insert(highlights, { start = hl_start, finish = hl_end }) + end + end + + return highlights +end + +sorters.get_substr_matcher = function() + return Sorter:new { + highlighter = substr_highlighter, + scoring_function = function(_, prompt, _, entry) + if #prompt == 0 then + return 1 + end + + local display = entry.ordinal:lower() + + local search_terms = util.max_split(prompt, "%s") + local matched = 0 + local total_search_terms = 0 + for _, word in pairs(search_terms) do + total_search_terms = total_search_terms + 1 + if display:find(word, 1, true) then + matched = matched + 1 + end + end + + return matched == total_search_terms and entry.index or -1 + end, + } +end + +local substr_matcher = function(_, prompt, line, _) + local display = line:lower() + local search_terms = util.max_split(prompt:lower(), "%s") + local matched = 0 + local total_search_terms = 0 + for _, word in pairs(search_terms) do + total_search_terms = total_search_terms + 1 + if display:find(word, 1, true) then + matched = matched + 1 + end + end + + return matched == total_search_terms and 0 or FILTERED +end + +local filter_function = function(opts) + local scoring_function = vim.F.if_nil(opts.filter_function, substr_matcher) + local tag = vim.F.if_nil(opts.tag, "ordinal") + + return function(_, prompt, entry) + local filter = "^(" .. opts.delimiter .. "(%S+)" .. "[" .. opts.delimiter .. "%s]" .. ")" + local matched = prompt:match(filter) + + if matched == nil then + return 0, prompt + end + -- clear prompt of tag + prompt = prompt:sub(#matched + 1, -1) + local query = vim.trim(matched:gsub(opts.delimiter, "")) + return scoring_function(_, query, entry[tag], _), prompt + end +end + +local function create_tag_set(tag) + tag = vim.F.if_nil(tag, "ordinal") + local set = {} + return setmetatable(set, { + __index = { + insert = function(set_, entry) + local value = entry[tag] + if not set_[value] then + set_[value] = true + end + end, + }, + }) +end + +sorters.prefilter = function(opts) + local sorter = opts.sorter + opts.delimiter = vim.F.if_nil(opts.delimiter, ":") + sorter._delimiter = opts.delimiter + sorter.tags = create_tag_set(opts.tag) + sorter.filter_function = filter_function(opts) + sorter._was_discarded = function() + return false + end + return sorter +end + +return sorters diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/state.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/state.lua new file mode 100644 index 0000000..6a06eb1 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/state.lua @@ -0,0 +1,31 @@ +local state = {} + +TelescopeGlobalState = TelescopeGlobalState or {} +TelescopeGlobalState.global = TelescopeGlobalState.global or {} + +--- Set the status for a particular prompt bufnr +function state.set_status(prompt_bufnr, status) + TelescopeGlobalState[prompt_bufnr] = status +end + +function state.set_global_key(key, value) + TelescopeGlobalState.global[key] = value +end + +function state.get_global_key(key) + return TelescopeGlobalState.global[key] +end + +function state.get_status(prompt_bufnr) + return TelescopeGlobalState[prompt_bufnr] or {} +end + +function state.clear_status(prompt_bufnr) + state.set_status(prompt_bufnr, nil) +end + +function state.get_existing_prompts() + return vim.tbl_keys(TelescopeGlobalState) +end + +return state diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/helpers.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/helpers.lua new file mode 100644 index 0000000..5296c45 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/helpers.lua @@ -0,0 +1,56 @@ +local test_helpers = {} + +test_helpers.get_picker = function() + local state = require "telescope.state" + return state.get_status(vim.api.nvim_get_current_buf()).picker +end + +test_helpers.get_results_bufnr = function() + local state = require "telescope.state" + return state.get_status(vim.api.nvim_get_current_buf()).results_bufnr +end + +test_helpers.get_file = function() + return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t") +end + +test_helpers.get_prompt = function() + return vim.api.nvim_buf_get_lines(0, 0, -1, false)[1] +end + +test_helpers.get_results = function() + return vim.api.nvim_buf_get_lines(test_helpers.get_results_bufnr(), 0, -1, false) +end + +test_helpers.get_best_result = function() + local results = test_helpers.get_results() + local picker = test_helpers.get_picker() + + if picker.sorting_strategy == "ascending" then + return results[1] + else + return results[#results] + end +end + +test_helpers.get_selection = function() + local state = require "telescope.state" + return state.get_global_key "selected_entry" +end + +test_helpers.get_selection_value = function() + return test_helpers.get_selection().value +end + +test_helpers.make_globals = function() + GetFile = test_helpers.get_file -- luacheck: globals GetFile + GetPrompt = test_helpers.get_prompt -- luacheck: globals GetPrompt + + GetResults = test_helpers.get_results -- luacheck: globals GetResults + GetBestResult = test_helpers.get_best_result -- luacheck: globals GetBestResult + + GetSelection = test_helpers.get_selection -- luacheck: globals GetSelection + GetSelectionValue = test_helpers.get_selection_value -- luacheck: globals GetSelectionValue +end + +return test_helpers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/init.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/init.lua new file mode 100644 index 0000000..9a8e707 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/init.lua @@ -0,0 +1,112 @@ +local assert = require "luassert" + +local Path = require "plenary.path" + +local tester = {} +tester.debug = false + +local get_results_from_contents = function(content) + local nvim = vim.fn.jobstart( + { "nvim", "--noplugin", "-u", "scripts/minimal_init.vim", "--headless", "--embed" }, + { rpc = true } + ) + + local result = vim.fn.rpcrequest(nvim, "nvim_exec_lua", content, {}) + assert.are.same(true, result[1], vim.inspect(result)) + + local count = 0 + while + vim.fn.rpcrequest(nvim, "nvim_exec_lua", "return require('telescope.testharness.runner').state.done", {}) ~= true + do + count = count + 1 + vim.wait(100) + + -- TODO: Could maybe wait longer, but it's annoying to wait if the test is going to timeout. + if count > 100 then + break + end + end + + local state = vim.fn.rpcrequest(nvim, "nvim_exec_lua", "return require('telescope.testharness.runner').state", {}) + vim.fn.jobstop(nvim) + + assert.are.same(true, state.done, vim.inspect(state)) + + local result_table = {} + for _, v in ipairs(state.results) do + table.insert(result_table, v) + end + + return result_table, state +end + +local check_results = function(results, state) + assert(state, "Must pass state") + + for _, v in ipairs(results) do + local assertion + if not v._type or v._type == "are" or v._type == "_default" then + assertion = assert.are.same + else + assertion = assert.are_not.same + end + + -- TODO: I think it would be nice to be able to see the state, + -- but it clutters up the test output so much here. + -- + -- So we would have to consider how to do that I think. + assertion(v.expected, v.actual, string.format("Test Case: %s // %s", v.location, v.case)) + end +end + +tester.run_string = function(contents) + contents = [[ + return (function() + local tester = require('telescope.testharness') + local runner = require('telescope.testharness.runner') + local helper = require('telescope.testharness.helpers') + helper.make_globals() + local ok, msg = pcall(function() + runner.log("Loading Test") + ]] .. contents .. [[ + end) + return {ok, msg or runner.state} + end)() + ]] + + check_results(get_results_from_contents(contents)) +end + +tester.run_file = function(filename) + local file = "./lua/tests/pickers/" .. filename .. ".lua" + local path = Path:new(file) + + if not path:exists() then + assert.are.same("", file) + end + + local contents = string.format( + [[ + return (function() + local runner = require('telescope.testharness.runner') + local helper = require('telescope.testharness.helpers') + helper.make_globals() + local ok, msg = pcall(function() + runner.log("Loading Test") + return loadfile("%s")() + end) + return {ok, msg or runner.state} + end)() + ]], + path:absolute() + ) + + check_results(get_results_from_contents(contents)) +end + +tester.not_ = function(val) + val._type = "are_not" + return val +end + +return tester diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/runner.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/runner.lua new file mode 100644 index 0000000..af1bf30 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/testharness/runner.lua @@ -0,0 +1,156 @@ +local builtin = require "telescope.builtin" + +local DELAY = vim.g.telescope_test_delay or 50 +local runner = {} + +-- State is test variable +runner.state = { + done = false, + results = {}, + msgs = {}, +} + +local writer = function(val) + table.insert(runner.state.results, val) +end + +local invalid_test_case = function(k) + error { case = k, expected = "
", actual = k } +end + +local _VALID_KEYS = { + post_typed = true, + post_close = true, +} + +local replace_terms = function(input) + return vim.api.nvim_replace_termcodes(input, true, false, true) +end + +runner.nvim_feed = function(text, feed_opts) + feed_opts = feed_opts or "m" + + vim.api.nvim_feedkeys(text, feed_opts, true) +end + +local end_test_cases = function() + runner.state.done = true +end + +local execute_test_case = function(location, key, spec) + local ok, actual = pcall(spec[2]) + + if not ok then + writer { + location = "Error: " .. location, + case = key, + expected = "To succeed and return: " .. tostring(spec[1]), + actual = actual, + + _type = spec._type, + } + + end_test_cases() + else + writer { + location = location, + case = key, + expected = spec[1], + actual = actual, + + _type = spec._type, + } + end + + return ok +end + +runner.log = function(msg) + table.insert(runner.state.msgs, msg) +end + +runner.picker = function(picker_name, input, test_cases, opts) + opts = opts or {} + + for k, _ in pairs(test_cases) do + if not _VALID_KEYS[k] then + return invalid_test_case(k) + end + end + + opts.on_complete = { + runner.create_on_complete(input, test_cases), + } + + opts._on_error = function(self, msg) + runner.state.done = true + writer { + location = "Error while running on complete", + expected = "To Work", + actual = msg, + } + end + + runner.log "Starting picker" + builtin[picker_name](opts) + runner.log "Called picker" +end + +runner.create_on_complete = function(input, test_cases) + input = replace_terms(input) + + local actions = {} + for i = 1, #input do + local char = input:sub(i, i) + table.insert(actions, { + cb = function() + runner.log("Inserting char: " .. char) + runner.nvim_feed(char, "") + end, + char = char, + }) + end + + return function() + local action + + repeat + action = table.remove(actions, 1) + if action then + action.cb() + end + until not action or string.match(action.char, "%g") + + if #actions > 0 then + return + end + + vim.defer_fn(function() + if test_cases.post_typed then + for k, v in ipairs(test_cases.post_typed) do + if not execute_test_case("post_typed", k, v) then + return + end + end + end + + vim.defer_fn(function() + runner.nvim_feed(replace_terms "", "") + + vim.defer_fn(function() + if test_cases.post_close then + for k, v in ipairs(test_cases.post_close) do + if not execute_test_case("post_close", k, v) then + return + end + end + end + + vim.defer_fn(end_test_cases, DELAY) + end, DELAY) + end, DELAY) + end, DELAY) + end +end + +return runner diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/themes.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/themes.lua new file mode 100644 index 0000000..0fe5d99 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/themes.lua @@ -0,0 +1,137 @@ +-- Prototype Theme System (WIP) +-- Currently certain designs need a number of parameters. +-- +-- local opts = themes.get_dropdown { winblend = 3 } + +---@tag telescope.themes +---@config { ["module"] = "telescope.themes" } + +---@brief [[ +--- Themes are ways to combine several elements of styling together. +--- +--- They are helpful for managing the several different UI aspects for telescope and provide +--- a simple interface for users to get a particular "style" of picker. +---@brief ]] + +local themes = {} + +--- Dropdown style theme. +--- +--- Usage: +--- +--- `local builtin = require('telescope.builtin')` +--- `local themes = require('telescope.themes')` +--- `builtin.find_files(themes.get_dropdown())` +--- +function themes.get_dropdown(opts) + opts = opts or {} + + local theme_opts = { + theme = "dropdown", + + results_title = false, + + sorting_strategy = "ascending", + layout_strategy = "center", + layout_config = { + preview_cutoff = 1, -- Preview should always show (unless previewer = false) + + width = function(_, max_columns, _) + return math.min(max_columns, 80) + end, + + height = function(_, _, max_lines) + return math.min(max_lines, 15) + end, + }, + + border = true, + borderchars = { + prompt = { "─", "│", " ", "│", "╭", "╮", "│", "│" }, + results = { "─", "│", "─", "│", "├", "┤", "╯", "╰" }, + preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + }, + } + if opts.layout_config and opts.layout_config.prompt_position == "bottom" then + theme_opts.borderchars = { + prompt = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + results = { "─", "│", "─", "│", "╭", "╮", "┤", "├" }, + preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + } + end + + return vim.tbl_deep_extend("force", theme_opts, opts) +end + +--- Cursor style theme. +--- +--- Usage: +--- +--- +--- `local builtin = require('telescope.builtin')` +--- `local themes = require('telescope.themes')` +--- `builtin.lsp_references(themes.get_cursor())` +--- +function themes.get_cursor(opts) + opts = opts or {} + + local theme_opts = { + theme = "cursor", + + sorting_strategy = "ascending", + results_title = false, + layout_strategy = "cursor", + layout_config = { + width = 80, + height = 9, + }, + borderchars = { + prompt = { "─", "│", " ", "│", "╭", "╮", "│", "│" }, + results = { "─", "│", "─", "│", "├", "┤", "╯", "╰" }, + preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + }, + } + + return vim.tbl_deep_extend("force", theme_opts, opts) +end + +--- Ivy style theme. +--- +--- Usage: +--- +--- `local builtin = require('telescope.builtin')` +--- `local themes = require('telescope.themes')` +--- `builtin.find_files(themes.get_ivy())` +--- +function themes.get_ivy(opts) + opts = opts or {} + + local theme_opts = { + theme = "ivy", + + sorting_strategy = "ascending", + + layout_strategy = "bottom_pane", + layout_config = { + height = 25, + }, + + border = true, + borderchars = { + prompt = { "─", " ", " ", " ", "─", "─", " ", " " }, + results = { " " }, + preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + }, + } + if opts.layout_config and opts.layout_config.prompt_position == "bottom" then + theme_opts.borderchars = { + prompt = { " ", " ", "─", " ", " ", " ", "─", "─" }, + results = { "─", " ", " ", " ", "─", "─", " ", " " }, + preview = { "─", " ", "─", "│", "┬", "─", "─", "╰" }, + } + end + + return vim.tbl_deep_extend("force", theme_opts, opts) +end + +return themes diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/utils.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/utils.lua new file mode 100644 index 0000000..dcc3276 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/telescope/utils.lua @@ -0,0 +1,515 @@ +---@tag telescope.utils +---@config { ["module"] = "telescope.utils" } + +---@brief [[ +--- Utilities for writing telescope pickers +---@brief ]] + +local Path = require "plenary.path" +local Job = require "plenary.job" + +local log = require "telescope.log" + +local truncate = require("plenary.strings").truncate +local get_status = require("telescope.state").get_status + +local utils = {} + +utils.get_separator = function() + return Path.path.sep +end + +utils.cycle = function(i, n) + return i % n == 0 and n or i % n +end + +utils.get_lazy_default = function(x, defaulter, ...) + if x == nil then + return defaulter(...) + else + return x + end +end + +utils.repeated_table = function(n, val) + local empty_lines = {} + for _ = 1, n do + table.insert(empty_lines, val) + end + return empty_lines +end + +utils.filter_symbols = function(results, opts) + local has_ignore = opts.ignore_symbols ~= nil + local has_symbols = opts.symbols ~= nil + local filtered_symbols + + if has_symbols and has_ignore then + utils.notify("filter_symbols", { + msg = "Either opts.symbols or opts.ignore_symbols, can't process opposing options at the same time!", + level = "ERROR", + }) + return + elseif not (has_ignore or has_symbols) then + return results + elseif has_ignore then + if type(opts.ignore_symbols) == "string" then + opts.ignore_symbols = { opts.ignore_symbols } + end + if type(opts.ignore_symbols) ~= "table" then + utils.notify("filter_symbols", { + msg = "Please pass ignore_symbols as either a string or a list of strings", + level = "ERROR", + }) + return + end + + opts.ignore_symbols = vim.tbl_map(string.lower, opts.ignore_symbols) + filtered_symbols = vim.tbl_filter(function(item) + return not vim.tbl_contains(opts.ignore_symbols, string.lower(item.kind)) + end, results) + elseif has_symbols then + if type(opts.symbols) == "string" then + opts.symbols = { opts.symbols } + end + if type(opts.symbols) ~= "table" then + utils.notify("filter_symbols", { + msg = "Please pass filtering symbols as either a string or a list of strings", + level = "ERROR", + }) + return + end + + opts.symbols = vim.tbl_map(string.lower, opts.symbols) + filtered_symbols = vim.tbl_filter(function(item) + return vim.tbl_contains(opts.symbols, string.lower(item.kind)) + end, results) + end + + -- TODO(conni2461): If you understand this correctly then we sort the results table based on the bufnr + -- If you ask me this should be its own function, that happens after the filtering part and should be + -- called in the lsp function directly + local current_buf = vim.api.nvim_get_current_buf() + if not vim.tbl_isempty(filtered_symbols) then + -- filter adequately for workspace symbols + local filename_to_bufnr = {} + for _, symbol in ipairs(filtered_symbols) do + if filename_to_bufnr[symbol.filename] == nil then + filename_to_bufnr[symbol.filename] = vim.uri_to_bufnr(vim.uri_from_fname(symbol.filename)) + end + symbol["bufnr"] = filename_to_bufnr[symbol.filename] + end + table.sort(filtered_symbols, function(a, b) + if a.bufnr == b.bufnr then + return a.lnum < b.lnum + end + if a.bufnr == current_buf then + return true + end + if b.bufnr == current_buf then + return false + end + return a.bufnr < b.bufnr + end) + return filtered_symbols + end + + -- print message that filtered_symbols is now empty + if has_symbols then + local symbols = table.concat(opts.symbols, ", ") + utils.notify("filter_symbols", { + msg = string.format("%s symbol(s) were not part of the query results", symbols), + level = "WARN", + }) + elseif has_ignore then + local symbols = table.concat(opts.ignore_symbols, ", ") + utils.notify("filter_symbols", { + msg = string.format("%s ignore_symbol(s) have removed everything from the query result", symbols), + level = "WARN", + }) + end +end + +utils.path_smart = (function() + local paths = {} + return function(filepath) + local final = filepath + if #paths ~= 0 then + local dirs = vim.split(filepath, "/") + local max = 1 + for _, p in pairs(paths) do + if #p > 0 and p ~= filepath then + local _dirs = vim.split(p, "/") + for i = 1, math.min(#dirs, #_dirs) do + if (dirs[i] ~= _dirs[i]) and i > max then + max = i + break + end + end + end + end + if #dirs ~= 0 then + if max == 1 and #dirs >= 2 then + max = #dirs - 2 + end + final = "" + for k, v in pairs(dirs) do + if k >= max - 1 then + final = final .. (#final > 0 and "/" or "") .. v + end + end + end + end + if not paths[filepath] then + paths[filepath] = "" + table.insert(paths, filepath) + end + if final and final ~= filepath then + return "../" .. final + else + return filepath + end + end +end)() + +utils.path_tail = (function() + local os_sep = utils.get_separator() + + return function(path) + for i = #path, 1, -1 do + if path:sub(i, i) == os_sep then + return path:sub(i + 1, -1) + end + end + return path + end +end)() + +utils.is_path_hidden = function(opts, path_display) + path_display = path_display or vim.F.if_nil(opts.path_display, require("telescope.config").values.path_display) + + return path_display == nil + or path_display == "hidden" + or type(path_display) == "table" and (vim.tbl_contains(path_display, "hidden") or path_display.hidden) +end + +local is_uri = function(filename) + return string.match(filename, "^%w+://") ~= nil +end + +local calc_result_length = function(truncate_len) + local status = get_status(vim.api.nvim_get_current_buf()) + local len = vim.api.nvim_win_get_width(status.results_win) - status.picker.selection_caret:len() - 2 + return type(truncate_len) == "number" and len - truncate_len or len +end + +--- Transform path is a util function that formats a path based on path_display +--- found in `opts` or the default value from config. +--- It is meant to be used in make_entry to have a uniform interface for +--- builtins as well as extensions utilizing the same user configuration +--- Note: It is only supported inside `make_entry`/`make_display` the use of +--- this function outside of telescope might yield to undefined behavior and will +--- not be addressed by us +---@param opts table: The opts the users passed into the picker. Might contains a path_display key +---@param path string: The path that should be formated +---@return string: The transformed path ready to be displayed +utils.transform_path = function(opts, path) + if path == nil then + return + end + if is_uri(path) then + return path + end + + local path_display = vim.F.if_nil(opts.path_display, require("telescope.config").values.path_display) + + local transformed_path = path + + if type(path_display) == "function" then + return path_display(opts, transformed_path) + elseif utils.is_path_hidden(nil, path_display) then + return "" + elseif type(path_display) == "table" then + if vim.tbl_contains(path_display, "tail") or path_display.tail then + transformed_path = utils.path_tail(transformed_path) + elseif vim.tbl_contains(path_display, "smart") or path_display.smart then + transformed_path = utils.path_smart(transformed_path) + else + if not vim.tbl_contains(path_display, "absolute") or path_display.absolute == false then + local cwd + if opts.cwd then + cwd = opts.cwd + if not vim.in_fast_event() then + cwd = vim.fn.expand(opts.cwd) + end + else + cwd = vim.loop.cwd() + end + transformed_path = Path:new(transformed_path):make_relative(cwd) + end + + if vim.tbl_contains(path_display, "shorten") or path_display["shorten"] ~= nil then + if type(path_display["shorten"]) == "table" then + local shorten = path_display["shorten"] + transformed_path = Path:new(transformed_path):shorten(shorten.len, shorten.exclude) + else + transformed_path = Path:new(transformed_path):shorten(path_display["shorten"]) + end + end + if vim.tbl_contains(path_display, "truncate") or path_display.truncate then + if opts.__length == nil then + opts.__length = calc_result_length(path_display.truncate) + end + if opts.__prefix == nil then + opts.__prefix = 0 + end + transformed_path = truncate(transformed_path, opts.__length - opts.__prefix, nil, -1) + end + end + + return transformed_path + else + log.warn("`path_display` must be either a function or a table.", "See `:help telescope.defaults.path_display.") + return transformed_path + end +end + +-- local x = utils.make_default_callable(function(opts) +-- return function() +-- print(opts.example, opts.another) +-- end +-- end, { example = 7, another = 5 }) + +-- x() +-- x.new { example = 3 }() +function utils.make_default_callable(f, default_opts) + default_opts = default_opts or {} + + return setmetatable({ + new = function(opts) + opts = vim.tbl_extend("keep", opts, default_opts) + return f(opts) + end, + }, { + __call = function() + local ok, err = pcall(f(default_opts)) + if not ok then + error(debug.traceback(err)) + end + end, + }) +end + +function utils.job_is_running(job_id) + if job_id == nil then + return false + end + return vim.fn.jobwait({ job_id }, 0)[1] == -1 +end + +function utils.buf_delete(bufnr) + if bufnr == nil then + return + end + + -- Suppress the buffer deleted message for those with &report<2 + local start_report = vim.o.report + if start_report < 2 then + vim.o.report = 2 + end + + if vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + + if start_report < 2 then + vim.o.report = start_report + end +end + +function utils.win_delete(name, win_id, force, bdelete) + if win_id == nil or not vim.api.nvim_win_is_valid(win_id) then + return + end + + local bufnr = vim.api.nvim_win_get_buf(win_id) + if bdelete then + utils.buf_delete(bufnr) + end + + if not vim.api.nvim_win_is_valid(win_id) then + return + end + + if not pcall(vim.api.nvim_win_close, win_id, force) then + log.trace("Unable to close window: ", name, "/", win_id) + end +end + +function utils.max_split(s, pattern, maxsplit) + pattern = pattern or " " + maxsplit = maxsplit or -1 + + local t = {} + + local curpos = 0 + while maxsplit ~= 0 and curpos < #s do + local found, final = string.find(s, pattern, curpos, false) + if found ~= nil then + local val = string.sub(s, curpos, found - 1) + + if #val > 0 then + maxsplit = maxsplit - 1 + table.insert(t, val) + end + + curpos = final + 1 + else + table.insert(t, string.sub(s, curpos)) + break + -- curpos = curpos + 1 + end + + if maxsplit == 0 then + table.insert(t, string.sub(s, curpos)) + end + end + + return t +end + +function utils.data_directory() + local sourced_file = require("plenary.debug_utils").sourced_filepath() + local base_directory = vim.fn.fnamemodify(sourced_file, ":h:h:h") + + return Path:new({ base_directory, "data" }):absolute() .. Path.path.sep +end + +function utils.buffer_dir() + return vim.fn.expand "%:p:h" +end + +function utils.display_termcodes(str) + return str:gsub(string.char(9), ""):gsub("", ""):gsub(" ", "") +end + +function utils.get_os_command_output(cmd, cwd) + if type(cmd) ~= "table" then + utils.notify("get_os_command_output", { + msg = "cmd has to be a table", + level = "ERROR", + }) + return {} + end + local command = table.remove(cmd, 1) + local stderr = {} + local stdout, ret = Job:new({ + command = command, + args = cmd, + cwd = cwd, + on_stderr = function(_, data) + table.insert(stderr, data) + end, + }):sync() + return stdout, ret, stderr +end + +function utils.win_set_buf_noautocmd(win, buf) + local save_ei = vim.o.eventignore + vim.o.eventignore = "all" + vim.api.nvim_win_set_buf(win, buf) + vim.o.eventignore = save_ei +end + +local load_once = function(f) + local resolved = nil + return function(...) + if resolved == nil then + resolved = f() + end + + return resolved(...) + end +end + +utils.transform_devicons = load_once(function() + local has_devicons, devicons = pcall(require, "nvim-web-devicons") + + if has_devicons then + if not devicons.has_loaded() then + devicons.setup() + end + + return function(filename, display, disable_devicons) + local conf = require("telescope.config").values + if disable_devicons or not filename then + return display + end + + local icon, icon_highlight = devicons.get_icon(utils.path_tail(filename), nil, { default = true }) + local icon_display = (icon or " ") .. " " .. (display or "") + + if conf.color_devicons then + return icon_display, icon_highlight + else + return icon_display, nil + end + end + else + return function(_, display, _) + return display + end + end +end) + +utils.get_devicons = load_once(function() + local has_devicons, devicons = pcall(require, "nvim-web-devicons") + + if has_devicons then + if not devicons.has_loaded() then + devicons.setup() + end + + return function(filename, disable_devicons) + local conf = require("telescope.config").values + if disable_devicons or not filename then + return "" + end + + local icon, icon_highlight = devicons.get_icon(utils.path_tail(filename), nil, { default = true }) + if conf.color_devicons then + return icon, icon_highlight + else + return icon, nil + end + end + else + return function(_, _) + return "" + end + end +end) + +--- Telescope Wrapper around vim.notify +---@param funname string: name of the function that will be +---@param opts table: opts.level string, opts.msg string, opts.once bool +utils.notify = function(funname, opts) + opts.once = vim.F.if_nil(opts.once, false) + local level = vim.log.levels[opts.level] + if not level then + error("Invalid error level", 2) + end + local notify_fn = opts.once and vim.notify_once or vim.notify + notify_fn(string.format("[telescope.%s]: %s", funname, opts.msg), level, { + title = "telescope.nvim", + }) +end + +utils.__warn_no_selection = function(name) + utils.notify(name, { + msg = "Nothing currently selected", + level = "WARN", + }) +end + +return utils diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/action_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/action_spec.lua new file mode 100644 index 0000000..3db014f --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/action_spec.lua @@ -0,0 +1,510 @@ +local actions = require "telescope.actions" +local action_set = require "telescope.actions.set" + +local transform_mod = require("telescope.actions.mt").transform_mod + +local eq = assert.are.same + +describe("actions", function() + it("should allow creating custom actions", function() + local a = transform_mod { + x = function() + return 5 + end, + } + + eq(5, a.x()) + end) + + it("allows adding actions", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local x_plus_y = a.x + a.y + + eq({ "x", "y" }, { x_plus_y() }) + end) + + it("ignores nils from added actions", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + nil_maker = function() + return nil + end, + } + + local x_plus_y = a.x + a.nil_maker + a.y + + eq({ "x", "y" }, { x_plus_y() }) + end) + + it("allows overriding an action", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + -- actions.file_goto_selection_edit:replace(...) + a.x:replace(function() + return "foo" + end) + eq("foo", a.x()) + + a._clear() + eq("x", a.x()) + end) + + it("allows overriding an action only in specific cases with if", function() + local a = transform_mod { + x = function(e) + return e * 10 + end, + y = function() + return "y" + end, + } + + -- actions.file_goto_selection_edit:replace(...) + a.x:replace_if(function(e) + return e > 0 + end, function(e) + return (e / 10) + end) + eq(-100, a.x(-10)) + eq(10, a.x(100)) + eq(1, a.x(10)) + + a._clear() + eq(100, a.x(10)) + end) + + it("allows overriding an action only in specific cases with mod", function() + local a = transform_mod { + x = function(e) + return e * 10 + end, + y = function() + return "y" + end, + } + + -- actions.file_goto_selection_edit:replace(...) + a.x:replace_map { + [function(e) + return e > 0 + end] = function(e) + return (e / 10) + end, + [function(e) + return e == 0 + end] = function(e) + return (e + 10) + end, + } + + eq(-100, a.x(-10)) + eq(10, a.x(100)) + eq(1, a.x(10)) + eq(10, a.x(0)) + + a._clear() + eq(100, a.x(10)) + end) + + it("continuous replacement", function() + local a = transform_mod { + x = function() + return "cleared" + end, + y = function() + return "y" + end, + } + + -- Replace original, which becomes new fallback + a.x:replace(function() + return "negative" + end) + + -- actions.file_goto_selection_edit:replace(...) + a.x:replace_map { + [function(e) + return e > 0 + end] = function(e) + return "positive" + end, + [function(e) + return e == 0 + end] = function(e) + return "zero" + end, + } + + eq("positive", a.x(10)) + eq("zero", a.x(0)) + eq("negative", a.x(-10)) + + a._clear() + eq("cleared", a.x(10)) + end) + + it("enhance.pre", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_pre = false + + a.y:enhance { + pre = function() + called_pre = true + end, + } + eq("y", a.y()) + eq(true, called_pre) + end) + + it("enhance.post", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_post = false + + a.y:enhance { + post = function() + called_post = true + end, + } + eq("y", a.y()) + eq(true, called_post) + end) + + it("static_pre static_post", function() + local called_pre = false + local called_post = false + local static_post = 0 + local a = transform_mod { + x = { + pre = function() + called_pre = true + end, + action = function() + return "x" + end, + post = function() + called_post = true + end, + }, + } + + eq("x", a.x()) + eq(true, called_pre) + eq(true, called_post) + end) + + it("can call both", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_count = 0 + local count_inc = function() + called_count = called_count + 1 + end + + a.y:enhance { + pre = count_inc, + post = count_inc, + } + + eq("y", a.y()) + eq(2, called_count) + end) + + it("can call both even when combined", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_count = 0 + local count_inc = function() + called_count = called_count + 1 + end + + a.y:enhance { + pre = count_inc, + post = count_inc, + } + + a.x:enhance { + post = count_inc, + } + + local x_plus_y = a.x + a.y + x_plus_y() + + eq(3, called_count) + end) + + it( + "can call replace fn even when combined before replace registered the fn (because that happens with mappings)", + function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_count = 0 + local count_inc = function() + called_count = called_count + 1 + end + + local x_plus_y = a.x + a.y + a.x:replace(function() + count_inc() + end) + a.y:replace(function() + count_inc() + end) + + x_plus_y() + + eq(2, called_count) + end + ) + + it( + "can call enhance fn even when combined before enhance registed fns (because that happens with mappings)", + function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_count = 0 + local count_inc = function() + called_count = called_count + 1 + end + + local x_plus_y = a.x + a.y + a.y:enhance { + pre = count_inc, + post = count_inc, + } + + a.x:enhance { + post = count_inc, + } + + x_plus_y() + + eq(3, called_count) + end + ) + + it("clears enhance", function() + local a = transform_mod { + x = function() + return "x" + end, + y = function() + return "y" + end, + } + + local called_post = false + + a.y:enhance { + post = function() + called_post = true + end, + } + + a._clear() + + eq("y", a.y()) + eq(false, called_post) + end) + + it("handles passing arguments", function() + local a = transform_mod { + x = function(bufnr) + return string.format "bufnr: %s" + end, + } + + a.x:replace(function(bufnr) + return string.format("modified: %s", bufnr) + end) + eq("modified: 5", a.x(5)) + end) + + it("handles add with two different tables", function() + local count_a = 0 + local count_b = 0 + local a = transform_mod { + x = function() + count_a = count_a + 1 + end, + } + local b = transform_mod { + y = function() + count_b = count_b + 1 + end, + } + + local called_count = 0 + local count_inc = function() + called_count = called_count + 1 + end + + a.x:enhance { + post = count_inc, + } + b.y:enhance { + post = count_inc, + } + + local x_plus_y = a.x + b.y + x_plus_y() + + eq(2, called_count) + eq(1, count_a) + eq(1, count_b) + end) + + it("handles tripple concat with static pre post", function() + local count_a = 0 + local count_b = 0 + local count_c = 0 + local static_pre = 0 + local static_post = 0 + local a = transform_mod { + x = { + pre = function() + static_pre = static_pre + 1 + end, + action = function() + count_a = count_a + 1 + end, + post = function() + static_post = static_post + 1 + end, + }, + } + local b = transform_mod { + y = { + pre = function() + static_pre = static_pre + 1 + end, + action = function() + count_b = count_b + 1 + end, + post = function() + static_post = static_post + 1 + end, + }, + } + local c = transform_mod { + z = { + pre = function() + static_pre = static_pre + 1 + end, + action = function() + count_c = count_c + 1 + end, + post = function() + static_post = static_post + 1 + end, + }, + } + + local replace_count = 0 + a.x:replace(function() + replace_count = replace_count + 1 + end) + + local x_plus_y_plus_z = a.x + b.y + c.z + x_plus_y_plus_z() + + eq(0, count_a) + eq(1, count_b) + eq(1, count_c) + eq(1, replace_count) + eq(3, static_pre) + eq(3, static_post) + end) + + describe("action_set", function() + it("can replace `action_set.edit`", function() + action_set.edit:replace(function(_, arg) + return "replaced:" .. arg + end) + eq("replaced:edit", actions.file_edit()) + eq("replaced:vnew", actions.file_vsplit()) + end) + + pending("handles backwards compat with select and edit files", function() + -- Reproduce steps: + -- In config, we have { [""] = actions.select, ... } + -- In caller, we have actions._goto:replace(...) + -- Person calls `select`, does not see update + action_set.edit:replace(function(_, arg) + return "default_to_edit:" .. arg + end) + eq("default_to_edit:edit", actions.select_default()) + + action_set.select:replace(function(_, arg) + return "override_with_select:" .. arg + end) + eq("override_with_select:default", actions.select_default()) + + -- Sometimes you might want to change the default selection... + -- but you don't want to prohibit the ability to edit the code... + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/command_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/command_spec.lua new file mode 100644 index 0000000..9212815 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/command_spec.lua @@ -0,0 +1,102 @@ +local command = require "telescope.command" + +local eq = assert.are.same + +describe("command_parser", function() + local test_parse = function(should, input, output) + it(should, function() + command.convert_user_opts(input) + eq(output, input) + end) + end + + -- Strings + test_parse("should handle cwd", { cwd = "string" }, { cwd = "string" }) + + -- Find commands + test_parse( + "should handle find_command 1", + { find_command = "rg,--ignore,--hidden,files" }, + { find_command = { "rg", "--ignore", "--hidden", "files" } } + ) + test_parse( + "should handle find_command 2", + { find_command = "fd,-t,f,-H" }, + { find_command = { "fd", "-t", "f", "-H" } } + ) + test_parse( + "should handle find_command 3", + { find_command = "fdfind,--type,f,--no-ignore" }, + { find_command = { "fdfind", "--type", "f", "--no-ignore" } } + ) + + -- Dictionaries/tables + test_parse( + "should handle layout_config viml 1", + { layout_config = "{'prompt_position':'top'}" }, + { layout_config = { prompt_position = "top" } } + ) + test_parse( + "should handle layout_config viml 2", + { layout_config = "#{prompt_position:'bottom'}" }, + { layout_config = { prompt_position = "bottom" } } + ) + test_parse( + "should handle layout_config viml 3", + { layout_config = "{'mirror':v:true}" }, + { layout_config = { mirror = true } } + ) + test_parse( + "should handle layout_config viml 4", + { layout_config = "#{mirror:v:true}" }, + { layout_config = { mirror = true } } + ) + test_parse( + "should handle layout_config lua 1", + { layout_config = "{prompt_position='bottom'}" }, + { layout_config = { prompt_position = "bottom" } } + ) + test_parse( + "should handle layout_config lua 2", + { layout_config = "{mirror=true}" }, + { layout_config = { mirror = true } } + ) + + -- Lists/tables + test_parse( + "should handle symbols commas list", + { symbols = "alpha,beta,gamma" }, + { symbols = { "alpha", "beta", "gamma" } } + ) + test_parse( + "should handle symbols viml list", + { symbols = "['alpha','beta','gamma']" }, + { symbols = { "alpha", "beta", "gamma" } } + ) + test_parse( + "should handle symbols lua list", + { symbols = "{'alpha','beta','gamma'}" }, + { symbols = { "alpha", "beta", "gamma" } } + ) + + -- Booleans + test_parse("should handle booleans 1", { hidden = "true" }, { hidden = true }) + test_parse("should handle booleans 2", { no_ignore = "false" }, { no_ignore = false }) + + -- Numbers + test_parse("should handle numbers 1", { depth = "2" }, { depth = 2 }) + test_parse("should handle numbers 2", { bufnr_width = "4" }, { bufnr_width = 4 }) + test_parse("should handle numbers 3", { severity = "27" }, { severity = 27 }) + + -- Multiple options + test_parse( + "should handle multiple options 1", + { layout_config = '{prompt_position="top"}', cwd = "/foobar", severity = "27" }, + { layout_config = { prompt_position = "top" }, cwd = "/foobar", severity = 27 } + ) + test_parse( + "should handle multiple options 2", + { symbols = "['alef','bet','gimel']", depth = "2", find_command = "rg,--ignore,files" }, + { symbols = { "alef", "bet", "gimel" }, depth = 2, find_command = { "rg", "--ignore", "files" } } + ) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_display_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_display_spec.lua new file mode 100644 index 0000000..a09ccae --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_display_spec.lua @@ -0,0 +1,34 @@ +local entry_display = require "telescope.pickers.entry_display" + +describe("truncate", function() + for _, ambiwidth in ipairs { "single", "double" } do + for _, case in ipairs { + { args = { "abcde", 6 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 5 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 4 }, expected = { single = "abc…", double = "ab…" } }, + { args = { "アイウエオ", 11 }, expected = { single = "アイウエオ", double = "アイウエオ" } }, + { args = { "アイウエオ", 10 }, expected = { single = "アイウエオ", double = "アイウエオ" } }, + { args = { "アイウエオ", 9 }, expected = { single = "アイウエ…", double = "アイウ…" } }, + { args = { "アイウエオ", 8 }, expected = { single = "アイウ…", double = "アイウ…" } }, + { args = { "├─┤", 7 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 6 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 5 }, expected = { single = "├─┤", double = "├…" } }, + { args = { "├─┤", 4 }, expected = { single = "├─┤", double = "├…" } }, + { args = { "├─┤", 3 }, expected = { single = "├─┤", double = "…" } }, + { args = { "├─┤", 2 }, expected = { single = "├…", double = "…" } }, + } do + local msg = ("can truncate: ambiwidth = %s, [%s, %d] -> %s"):format( + ambiwidth, + case.args[1], + case.args[2], + case.expected[ambiwidth] + ) + it(msg, function() + local original = vim.o.ambiwidth + vim.o.ambiwidth = ambiwidth + assert.are.same(case.expected[ambiwidth], entry_display.truncate(case.args[1], case.args[2])) + vim.o.ambiwidth = original + end) + end + end +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_manager_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_manager_spec.lua new file mode 100644 index 0000000..6d2b5d3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/entry_manager_spec.lua @@ -0,0 +1,189 @@ +local EntryManager = require "telescope.entry_manager" + +local eq = assert.are.same + +describe("process_result", function() + it("works with one entry", function() + local manager = EntryManager:new(5, nil) + + manager:add_entry(nil, 1, "hello", "") + + eq(1, manager:get_score(1)) + end) + + it("works with two entries", function() + local manager = EntryManager:new(5, nil) + + manager:add_entry(nil, 1, "hello", "") + manager:add_entry(nil, 2, "later", "") + + eq(2, manager.linked_states.size) + + eq("hello", manager:get_entry(1)) + eq("later", manager:get_entry(2)) + end) + + it("calls functions when inserting", function() + local called_count = 0 + local manager = EntryManager:new(5, function() + called_count = called_count + 1 + end) + + assert(called_count == 0) + manager:add_entry(nil, 1, "hello", "") + assert(called_count == 1) + end) + + it("calls functions when inserting twice", function() + local called_count = 0 + local manager = EntryManager:new(5, function() + called_count = called_count + 1 + end) + + assert(called_count == 0) + manager:add_entry(nil, 1, "hello", "") + manager:add_entry(nil, 2, "world", "") + assert(called_count == 2) + end) + + it("correctly sorts lower scores", function() + local called_count = 0 + local manager = EntryManager:new(5, function() + called_count = called_count + 1 + end) + manager:add_entry(nil, 5, "worse result", "") + manager:add_entry(nil, 2, "better result", "") + + eq("better result", manager:get_entry(1)) + eq("worse result", manager:get_entry(2)) + + eq(2, called_count) + end) + + it("respects max results", function() + local called_count = 0 + local manager = EntryManager:new(1, function() + called_count = called_count + 1 + end) + manager:add_entry(nil, 2, "better result", "") + manager:add_entry(nil, 5, "worse result", "") + + eq("better result", manager:get_entry(1)) + eq(1, called_count) + end) + + it("should allow simple entries", function() + local manager = EntryManager:new(5) + + local counts_executed = 0 + manager:add_entry( + nil, + 1, + setmetatable({}, { + __index = function(t, k) + local val = nil + if k == "ordinal" then + counts_executed = counts_executed + 1 + + -- This could be expensive, only call later + val = "wow" + end + + rawset(t, k, val) + return val + end, + }), + "" + ) + + eq("wow", manager:get_ordinal(1)) + eq("wow", manager:get_ordinal(1)) + eq("wow", manager:get_ordinal(1)) + + eq(1, counts_executed) + end) + + it("should not loop a bunch", function() + local info = {} + local manager = EntryManager:new(5, nil, info) + manager:add_entry(nil, 4, "better result", "") + manager:add_entry(nil, 3, "better result", "") + manager:add_entry(nil, 2, "better result", "") + + -- Loops once to find 3 < 4 + -- Loops again to find 2 < 3 + eq(2, info.looped) + end) + + it("should not loop a bunch, part 2", function() + local info = {} + local manager = EntryManager:new(5, nil, info) + manager:add_entry(nil, 4, "better result", "") + manager:add_entry(nil, 2, "better result", "") + manager:add_entry(nil, 3, "better result", "") + + -- Loops again to find 2 < 4 + -- Loops once to find 3 > 2 + -- but less than 4 + eq(3, info.looped) + end) + + it("should update worst score in all append case", function() + local manager = EntryManager:new(2, nil) + manager:add_entry(nil, 2, "result 2", "") + manager:add_entry(nil, 3, "result 3", "") + manager:add_entry(nil, 4, "result 4", "") + + eq(3, manager.worst_acceptable_score) + end) + + it("should update worst score in all prepend case", function() + local called_count = 0 + local manager = EntryManager:new(2, function() + called_count = called_count + 1 + end) + manager:add_entry(nil, 5, "worse result", "") + manager:add_entry(nil, 4, "less worse result", "") + manager:add_entry(nil, 2, "better result", "") + + -- Once for insert 5 + -- Once for prepend 4 + -- Once for prepend 2 + eq(3, called_count) + + eq("better result", manager:get_entry(1)) + eq(4, manager.worst_acceptable_score) + end) + + it("should call tiebreaker if score is the same, sort length", function() + local manager = EntryManager:new(5, nil) + local picker = { + tiebreak = function(curr, prev, prompt) + eq("asdf", prompt) + return #curr < #prev + end, + } + + manager:add_entry(picker, 0.5, "same same", "asdf") + manager:add_entry(picker, 0.5, "same", "asdf") + + eq("same", manager:get_entry(1)) + eq("same same", manager:get_entry(2)) + end) + + it("should call tiebreaker if score is the same, keep initial", function() + local manager = EntryManager:new(5, nil) + local picker = { + tiebreak = function(_, _, prompt) + eq("asdf", prompt) + return false + end, + } + + manager:add_entry(picker, 0.5, "same same", "asdf") + manager:add_entry(picker, 0.5, "same", "asdf") + + eq("same", manager:get_entry(2)) + eq("same same", manager:get_entry(1)) + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/layout_strategies_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/layout_strategies_spec.lua new file mode 100644 index 0000000..8b5af48 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/layout_strategies_spec.lua @@ -0,0 +1,162 @@ +-- local tester = require('telescope.pickers._test') +local config = require "telescope.config" +local resolve = require "telescope.config.resolve" +local layout_strats = require "telescope.pickers.layout_strategies" + +local validate_layout_config = layout_strats._validate_layout_config + +local eq = assert.are.same + +describe("layout_strategies", function() + it("should have validator", function() + assert(validate_layout_config, "Has validator") + end) + + local test_height = function(should, output, input, opts) + opts = opts or {} + + local max_columns, max_lines = opts.max_columns or 100, opts.max_lines or 100 + it(should, function() + local layout_config = validate_layout_config("horizontal", { height = true }, { height = input }) + + eq(output, resolve.resolve_height(layout_config.height)({}, max_columns, max_lines)) + end) + end + + test_height("should handle numbers", 10, 10) + + test_height("should handle percentage: 100", 10, 0.1, { max_lines = 100 }) + test_height("should handle percentage: 110", 11, 0.1, { max_lines = 110 }) + + test_height("should call functions: simple", 5, function() + return 5 + end) + test_height("should call functions: percentage", 15, function(_, _, lines) + return 0.1 * lines + end, { + max_lines = 150, + }) + + local test_defaults_key = function(should, key, strat, output, ours, theirs, override) + ours = ours or {} + theirs = theirs or {} + override = override or {} + + it(should, function() + config.clear_defaults() + config.set_defaults({ layout_config = theirs }, { layout_config = { ours, "description" } }) + local layout_config = validate_layout_config(strat, layout_strats._configurations[strat], override) + eq(output, layout_config[key]) + end) + end + + test_defaults_key( + "should use ours if theirs and override don't give the key", + "height", + "horizontal", + 50, + { height = 50 }, + { width = 100 }, + { width = 120 } + ) + + test_defaults_key( + "should use ours if theirs and override don't give the key for this strategy", + "height", + "horizontal", + 50, + { height = 50 }, + { vertical = { height = 100 } }, + { vertical = { height = 120 } } + ) + + test_defaults_key( + "should use theirs if override doesn't give the key", + "height", + "horizontal", + 100, + { height = 50 }, + { height = 100 }, + { width = 120 } + ) + + test_defaults_key( + "should use override if key given", + "height", + "horizontal", + 120, + { height = 50 }, + { height = 100 }, + { height = 120 } + ) + + test_defaults_key( + "should use override if key given for this strategy", + "height", + "horizontal", + 120, + { height = 50 }, + { height = 100 }, + { horizontal = { height = 120 } } + ) + + test_defaults_key( + "should use theirs if override doesn't give key (even if ours has strategy specific)", + "height", + "horizontal", + 100, + { horizontal = { height = 50 } }, + { height = 100 }, + { width = 120 } + ) + + test_defaults_key( + "should use override (even if ours has strategy specific)", + "height", + "horizontal", + 120, + { horizontal = { height = 50 } }, + { height = 100 }, + { height = 120 } + ) + + test_defaults_key( + "should use override (even if theirs has strategy specific)", + "height", + "horizontal", + 120, + { height = 50 }, + { horizontal = { height = 100 } }, + { height = 120 } + ) + + test_defaults_key( + "should use override (even if ours and theirs have strategy specific)", + "height", + "horizontal", + 120, + { horizontal = { height = 50 } }, + { horizontal = { height = 100 } }, + { height = 120 } + ) + + test_defaults_key( + "should handle user config overriding a table with a number", + "height", + "horizontal", + 120, + { height = { padding = 5 } }, + { height = 120 }, + {} + ) + + test_defaults_key( + "should handle user oneshot overriding a table with a number", + "height", + "horizontal", + 120, + {}, + { height = { padding = 5 } }, + { height = 120 } + ) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/linked_list_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/linked_list_spec.lua new file mode 100644 index 0000000..bc17ba1 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/linked_list_spec.lua @@ -0,0 +1,133 @@ +local LinkedList = require "telescope.algos.linked_list" + +describe("LinkedList", function() + it("can create a list", function() + local l = LinkedList:new() + + assert.are.same(0, l.size) + end) + + it("can add a single entry to the list", function() + local l = LinkedList:new() + l:append "hello" + + assert.are.same(1, l.size) + end) + + it("can iterate over one item", function() + local l = LinkedList:new() + l:append "hello" + + for val in l:iter() do + assert.are.same("hello", val) + end + end) + + it("iterates in order", function() + local l = LinkedList:new() + l:append "hello" + l:append "world" + + local x = {} + for val in l:iter() do + table.insert(x, val) + end + + assert.are.same({ "hello", "world" }, x) + end) + + it("iterates in order, for prepend", function() + local l = LinkedList:new() + l:prepend "world" + l:prepend "hello" + + local x = {} + for val in l:iter() do + table.insert(x, val) + end + + assert.are.same({ "hello", "world" }, x) + end) + + it("iterates in order, for combo", function() + local l = LinkedList:new() + l:prepend "world" + l:prepend "hello" + l:append "last" + l:prepend "first" + + local x = {} + for val in l:iter() do + table.insert(x, val) + end + + assert.are.same({ "first", "hello", "world", "last" }, x) + assert.are.same(#x, l.size) + end) + + it("has ipairs", function() + local l = LinkedList:new() + l:prepend "world" + l:prepend "hello" + l:append "last" + l:prepend "first" + + local x = {} + for v in l:iter() do + table.insert(x, v) + end + assert.are.same({ "first", "hello", "world", "last" }, x) + + local expected = {} + for i, v in ipairs(x) do + table.insert(expected, { i, v }) + end + + local actual = {} + for i, v in l:ipairs() do + table.insert(actual, { i, v }) + end + + assert.are.same(expected, actual) + end) + + describe("track_at", function() + it("should update tracked when only appending", function() + local l = LinkedList:new { track_at = 2 } + l:append "first" + l:append "second" + l:append "third" + + assert.are.same("second", l.tracked) + end) + + it("should update tracked when first some prepend and then append", function() + local l = LinkedList:new { track_at = 2 } + l:prepend "first" + l:append "second" + l:append "third" + + assert.are.same("second", l.tracked) + end) + + it("should update when only prepending", function() + local l = LinkedList:new { track_at = 2 } + l:prepend "third" + l:prepend "second" + l:prepend "first" + + assert.are.same("second", l.tracked) + end) + + it("should update when lots of prepend and append", function() + local l = LinkedList:new { track_at = 2 } + l:prepend "third" + l:prepend "second" + l:prepend "first" + l:append "fourth" + l:prepend "zeroth" + + assert.are.same("first", l.tracked) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/find_files_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/find_files_spec.lua new file mode 100644 index 0000000..7e1c027 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/find_files_spec.lua @@ -0,0 +1,139 @@ +-- Just skip on mac, it has flaky CI for some reason +if vim.fn.has "mac" == 1 then + return +end + +local tester = require "telescope.testharness" + +local disp = function(val) + return vim.inspect(val, { newline = " ", indent = "" }) +end + +describe("builtin.find_files", function() + it("should find the readme", function() + tester.run_file "find_files__readme" + end) + + for _, configuration in ipairs { + { sorting_strategy = "descending" }, + { sorting_strategy = "ascending" }, + } do + it("should not display devicons when disabled: " .. disp(configuration), function() + tester.run_string(string.format( + [[ + local max_results = 5 + + runner.picker('find_files', 'README.md', { + post_typed = { + { "> README.md", GetPrompt }, + { "> README.md", GetBestResult }, + }, + post_close = { + { 'README.md', GetFile }, + { 'README.md', GetFile }, + } + }, vim.tbl_extend("force", { + disable_devicons = true, + sorter = require('telescope.sorters').get_fzy_sorter(), + layout_strategy = 'center', + layout_config = { + height = max_results + 1, + width = 0.9, + }, + }, vim.json.decode([==[%s]==]))) + ]], + vim.json.encode(configuration) + )) + end) + + pending("use devicons, if it has it when enabled", function() + if not pcall(require, "nvim-web-devicons") then + return + end + + local md = require("nvim-web-devicons").get_icon "md" + tester.run_string(string.format( + [[ + runner.picker('find_files', 'README.md', { + post_typed = { + { "> README.md", GetPrompt }, + { "> %s README.md", GetBestResult } + }, + post_close = { + { 'README.md', GetFile }, + { 'README.md', GetFile }, + } + }, vim.tbl_extend("force", { + disable_devicons = false, + sorter = require('telescope.sorters').get_fzy_sorter(), + }, vim.json.decode([==[%s]==]))) + ]], + md, + vim.json.encode(configuration) + )) + end) + end + + it("should find the readme, using lowercase", function() + tester.run_string [[ + runner.picker('find_files', 'readme.md', { + post_close = { + { 'README.md', GetFile }, + } + }) + ]] + end) + + it("should find the pickers.lua, using lowercase", function() + tester.run_string [[ + runner.picker('find_files', 'pickers.lua', { + post_close = { + { 'pickers.lua', GetFile }, + } + }) + ]] + end) + + it("should find the pickers.lua", function() + tester.run_string [[ + runner.picker('find_files', 'pickers.lua', { + post_close = { + { 'pickers.lua', GetFile }, + { 'pickers.lua', GetFile }, + } + }) + ]] + end) + + it("should be able to c-n the items", function() + tester.run_string [[ + runner.picker('find_files', 'fixtures/file', { + post_typed = { + { + { + " lua/tests/fixtures/file_a.txt", + "> lua/tests/fixtures/file_abc.txt", + }, GetResults + }, + }, + post_close = { + { 'file_abc.txt', GetFile }, + }, + }, { + sorter = require('telescope.sorters').get_fzy_sorter(), + sorting_strategy = "ascending", + disable_devicons = true, + }) + ]] + end) + + it("should be able to get the current selection", function() + tester.run_string [[ + runner.picker('find_files', 'fixtures/file_abc', { + post_typed = { + { 'lua/tests/fixtures/file_abc.txt', GetSelectionValue }, + } + }) + ]] + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/scrolling_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/scrolling_spec.lua new file mode 100644 index 0000000..760d2b9 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/pickers/scrolling_spec.lua @@ -0,0 +1,12 @@ +require("plenary.reload").reload_module "telescope" + +local tester = require "telescope.pickers._test" + +local log = require "telescope.log" +log.use_console = false + +describe("scrolling strategies", function() + it("should handle cycling for full list", function() + tester.run_file [[find_files__scrolling_descending_cycle]] + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/resolver_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/resolver_spec.lua new file mode 100644 index 0000000..f30a323 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/resolver_spec.lua @@ -0,0 +1,208 @@ +local eq = function(a, b) + assert.are.same(a, b) +end + +local resolve = require "telescope.config.resolve" + +describe("telescope.config.resolve", function() + describe("win_option", function() + it("should resolve for percentages", function() + local height_config = 0.8 + local opt = resolve.win_option(height_config) + + eq(height_config, opt.preview) + eq(height_config, opt.prompt) + eq(height_config, opt.results) + end) + + it("should resolve for percentages with default", function() + local height_config = 0.8 + local opt = resolve.win_option(nil, height_config) + + eq(height_config, opt.preview) + eq(height_config, opt.prompt) + eq(height_config, opt.results) + end) + + it("should resolve table values", function() + local table_val = { "a" } + local opt = resolve.win_option(nil, table_val) + + eq(table_val, opt.preview) + eq(table_val, opt.prompt) + eq(table_val, opt.results) + end) + + it("should allow overrides for different wins", function() + local prompt_override = { "a", prompt = "b" } + local opt = resolve.win_option(prompt_override) + eq("a", opt.preview) + eq("a", opt.results) + eq("b", opt.prompt) + end) + + it("should allow overrides for all wins", function() + local all_specified = { preview = "a", prompt = "b", results = "c" } + local opt = resolve.win_option(all_specified) + eq("a", opt.preview) + eq("b", opt.prompt) + eq("c", opt.results) + end) + + it("should allow some specified with a simple default", function() + local some_specified = { prompt = "b", results = "c" } + local opt = resolve.win_option(some_specified, "a") + eq("a", opt.preview) + eq("b", opt.prompt) + eq("c", opt.results) + end) + end) + + describe("resolve_height/width", function() + local test_sizes = { + { 24, 100 }, + { 35, 125 }, + { 60, 59 }, + { 100, 40 }, + } + it("should handle percentages", function() + local percentages = { 0.1, 0.33333, 0.5, 0.99 } + for _, s in ipairs(test_sizes) do + for _, p in ipairs(percentages) do + eq(math.floor(s[1] * p), resolve.resolve_width(p)(nil, unpack(s))) + eq(math.floor(s[2] * p), resolve.resolve_height(p)(nil, unpack(s))) + end + end + end) + + it("should handle percentages with min/max boundary", function() + eq(20, resolve.resolve_width { 0.1, min = 20 }(nil, 40, 120)) + eq(30, resolve.resolve_height { 0.1, min = 20 }(nil, 40, 300)) + + eq(24, resolve.resolve_width { 0.4, max = 80 }(nil, 60, 60)) + eq(80, resolve.resolve_height { 0.4, max = 80 }(nil, 60, 300)) + end) + + it("should handle fixed size", function() + local fixed = { 5, 8, 13, 21, 34 } + for _, s in ipairs(test_sizes) do + for _, f in ipairs(fixed) do + eq(math.min(f, s[1]), resolve.resolve_width(f)(nil, unpack(s))) + eq(math.min(f, s[2]), resolve.resolve_height(f)(nil, unpack(s))) + end + end + end) + + it("should handle functions", function() + local func = function(_, max_columns, max_lines) + if max_columns < 45 then + return math.min(max_columns, max_lines) + elseif max_columns < max_lines then + return max_columns * 0.8 + else + return math.min(max_columns, max_lines) * 0.5 + end + end + for _, s in ipairs(test_sizes) do + eq(func(nil, unpack(s)), resolve.resolve_height(func)(nil, unpack(s))) + end + end) + + it("should handle padding", function() + local func = function(_, max_columns, max_lines) + return math.floor(math.min(max_columns * 0.6, max_lines * 0.8)) + end + local pads = { 0.1, 5, func } + for _, s in ipairs(test_sizes) do + for _, p in ipairs(pads) do + eq(s[1] - 2 * resolve.resolve_width(p)(nil, unpack(s)), resolve.resolve_width { padding = p }(nil, unpack(s))) + eq( + s[2] - 2 * resolve.resolve_height(p)(nil, unpack(s)), + resolve.resolve_height { padding = p }(nil, unpack(s)) + ) + end + end + end) + end) + + describe("resolve_anchor_pos", function() + local test_sizes = { + { 6, 7, 8, 9 }, + { 10, 20, 30, 40 }, + { 15, 15, 16, 16 }, + { 17, 19, 23, 31 }, + { 21, 18, 26, 24 }, + { 50, 100, 150, 200 }, + } + + it([[should not adjust when "CENTER" or "" is the anchor]], function() + for _, s in ipairs(test_sizes) do + eq({ 0, 0 }, resolve.resolve_anchor_pos("", unpack(s))) + eq({ 0, 0 }, resolve.resolve_anchor_pos("center", unpack(s))) + eq({ 0, 0 }, resolve.resolve_anchor_pos("CENTER", unpack(s))) + end + end) + + it([[should end up at top when "N" in the anchor]], function() + local top_test = function(anchor, p_width, p_height, max_columns, max_lines) + local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines) + eq(1, pos[2] + math.floor((max_lines - p_height) / 2)) + end + for _, s in ipairs(test_sizes) do + top_test("NW", unpack(s)) + top_test("N", unpack(s)) + top_test("NE", unpack(s)) + end + end) + + it([[should end up at left when "W" in the anchor]], function() + local left_test = function(anchor, p_width, p_height, max_columns, max_lines) + local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines) + eq(1, pos[1] + math.floor((max_columns - p_width) / 2)) + end + for _, s in ipairs(test_sizes) do + left_test("NW", unpack(s)) + left_test("W", unpack(s)) + left_test("SW", unpack(s)) + end + end) + + it([[should end up at bottom when "S" in the anchor]], function() + local bot_test = function(anchor, p_width, p_height, max_columns, max_lines) + local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines) + eq(max_lines - 1, pos[2] + p_height + math.floor((max_lines - p_height) / 2)) + end + for _, s in ipairs(test_sizes) do + bot_test("SW", unpack(s)) + bot_test("S", unpack(s)) + bot_test("SE", unpack(s)) + end + end) + + it([[should end up at right when "E" in the anchor]], function() + local right_test = function(anchor, p_width, p_height, max_columns, max_lines) + local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines) + eq(max_columns - 1, pos[1] + p_width + math.floor((max_columns - p_width) / 2)) + end + for _, s in ipairs(test_sizes) do + right_test("NE", unpack(s)) + right_test("E", unpack(s)) + right_test("SE", unpack(s)) + end + end) + + it([[should ignore casing of the anchor]], function() + local case_test = function(a1, a2, p_width, p_height, max_columns, max_lines) + local pos1 = resolve.resolve_anchor_pos(a1, p_width, p_height, max_columns, max_lines) + local pos2 = resolve.resolve_anchor_pos(a2, p_width, p_height, max_columns, max_lines) + eq(pos1, pos2) + end + for _, s in ipairs(test_sizes) do + case_test("ne", "NE", unpack(s)) + case_test("w", "W", unpack(s)) + case_test("sW", "sw", unpack(s)) + case_test("cEnTeR", "CeNtEr", unpack(s)) + end + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/scroller_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/scroller_spec.lua new file mode 100644 index 0000000..96d64af --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/scroller_spec.lua @@ -0,0 +1,143 @@ +local p_scroller = require "telescope.pickers.scroller" + +local log = require "telescope.log" +log.use_console = false + +local eq = assert.are.same + +describe("scroller", function() + local max_results = 10 + + describe("ascending cycle", function() + local cycle_scroller = p_scroller.create("cycle", "ascending") + + it("should return values within the max results", function() + eq(5, cycle_scroller(max_results, max_results, 5)) + end) + + it("should return 0 at 0", function() + eq(0, cycle_scroller(max_results, max_results, 0)) + end) + + it("should cycle you to the top when you go below 0", function() + eq(max_results - 1, cycle_scroller(max_results, max_results, -1)) + end) + + it("should cycle you to 0 when you go past the results", function() + eq(0, cycle_scroller(max_results, max_results, max_results + 1)) + end) + + it("should cycle when current results is less than max_results", function() + eq(0, cycle_scroller(max_results, 5, 7)) + end) + end) + + describe("ascending limit", function() + local limit_scroller = p_scroller.create("limit", "ascending") + + it("should return values within the max results", function() + eq(5, limit_scroller(max_results, max_results, 5)) + end) + + it("should return 0 at 0", function() + eq(0, limit_scroller(max_results, max_results, 0)) + end) + + it("should not cycle", function() + eq(0, limit_scroller(max_results, max_results, -1)) + end) + + it("should not cycle you to 0 when you go past the results", function() + eq(max_results - 1, limit_scroller(max_results, max_results, max_results + 1)) + end) + + it("should stay at current results when current results is less than max_results", function() + local current = 5 + eq(current - 1, limit_scroller(max_results, current, 7)) + end) + end) + + describe("descending cycle", function() + local cycle_scroller = p_scroller.create("cycle", "descending") + + it("should return values within the max results", function() + eq(5, cycle_scroller(max_results, max_results, 5)) + end) + + it("should return max_results - 1 at 0", function() + eq(0, cycle_scroller(max_results, max_results, 0)) + end) + + it("should cycle you to the bot when you go below 0", function() + eq(max_results - 1, cycle_scroller(max_results, max_results, -1)) + end) + + it("should cycle you to 0 when you go past the results", function() + eq(0, cycle_scroller(max_results, max_results, max_results + 1)) + end) + + it("should cycle when current results is less than max_results", function() + eq(9, cycle_scroller(max_results, 5, 4)) + end) + end) + + describe("descending limit", function() + local limit_scroller = p_scroller.create("limit", "descending") + + it("should return values within the max results", function() + eq(5, limit_scroller(max_results, max_results, 5)) + end) + + it("should return 0 at 0", function() + eq(0, limit_scroller(max_results, max_results, 0)) + end) + + it("should not cycle", function() + eq(0, limit_scroller(max_results, max_results, -1)) + end) + + it("should not cycle you to 0 when you go past the results", function() + eq(max_results - 1, limit_scroller(max_results, max_results, max_results + 1)) + end) + + it("should stay at current results when current results is less than max_results", function() + local current = 5 + eq(max_results - current, limit_scroller(max_results, current, 4)) + end) + end) + + describe("https://github.com/nvim-telescope/telescope.nvim/pull/293#issuecomment-751463224", function() + it("should handle having many more results than necessary", function() + local scroller = p_scroller.create("cycle", "descending") + + -- 23 112 23 + eq(0, scroller(23, 112, 23)) + end) + end) + + describe("should give top, middle and bottom index", function() + it("should handle ascending", function() + eq(0, p_scroller.top("ascending", 20, 1000)) + eq(19, p_scroller.bottom("ascending", 20, 1000)) + + eq(0, p_scroller.top("ascending", 20, 10)) + eq(9, p_scroller.bottom("ascending", 20, 10)) + + eq(5, p_scroller.middle("ascending", 11, 100)) + eq(10, p_scroller.middle("ascending", 20, 100)) + eq(12, p_scroller.middle("ascending", 25, 100)) + end) + + it("should handle descending", function() + eq(0, p_scroller.top("descending", 20, 1000)) + eq(19, p_scroller.bottom("descending", 20, 1000)) + + eq(10, p_scroller.top("descending", 20, 10)) + eq(19, p_scroller.bottom("descending", 20, 10)) + + eq(25, p_scroller.middle("descending", 30, 10)) + eq(50, p_scroller.middle("descending", 60, 20)) + eq(105, p_scroller.middle("descending", 120, 30)) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/telescope_spec.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/telescope_spec.lua new file mode 100644 index 0000000..3bb76c3 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/automated/telescope_spec.lua @@ -0,0 +1,212 @@ +local picker = require "telescope.pickers" + +local eq = assert.are.same + +describe("telescope", function() + describe("Picker", function() + describe("window_dimensions", function() + it("", function() + assert(true) + end) + end) + + describe("attach_mappings", function() + local new_picker = function(a, b) + a.finder = true + return picker.new(a, b) + end + + it("should allow for passing in a function", function() + local p = new_picker({}, { + attach_mappings = function() + return 1 + end, + }) + eq(1, p.attach_mappings()) + end) + + it("should override an attach mappings passed in by opts", function() + local called_order = {} + local p = new_picker({ + attach_mappings = function() + table.insert(called_order, "opts") + end, + }, { + attach_mappings = function() + table.insert(called_order, "default") + end, + }) + + p.attach_mappings() + + eq({ "default", "opts" }, called_order) + end) + end) + end) + + describe("Sorters", function() + describe("generic_fuzzy_sorter", function() + it("sort matches well", function() + local sorter = require("telescope.sorters").get_generic_fuzzy_sorter() + + local exact_match = sorter:score("hello", { ordinal = "hello" }) + local no_match = sorter:score("abcdef", { ordinal = "ghijkl" }) + local ok_match = sorter:score("abcdef", { ordinal = "ab" }) + + assert(exact_match < no_match, "exact match better than no match") + assert(exact_match < ok_match, "exact match better than ok match") + assert(ok_match < no_match, "ok match better than no match") + end) + + it("sorts multiple finds better", function() + local sorter = require("telescope.sorters").get_generic_fuzzy_sorter() + + local multi_match = sorter:score("generics", "exercises/generics/generics2.rs") + local one_match = sorter:score("abcdef", "exercises/generics/README.md") + + -- assert(multi_match < one_match) + end) + end) + + describe("fuzzy_file", function() + it("sort matches well", function() + local sorter = require("telescope.sorters").get_fuzzy_file() + + local exact_match = sorter:score("abcdef", { ordinal = "abcdef" }) + local no_match = sorter:score("abcdef", { ordinal = "ghijkl" }) + local ok_match = sorter:score("abcdef", { ordinal = "ab" }) + + assert(exact_match < no_match, string.format("Exact match better than no match: %s %s", exact_match, no_match)) + assert(exact_match < ok_match, string.format("Exact match better than OK match: %s %s", exact_match, ok_match)) + assert(ok_match < no_match, "OK match better than no match") + end) + + it("sorts matches after last os sep better", function() + local sorter = require("telescope.sorters").get_fuzzy_file() + + local better_match = sorter:score("aaa", { ordinal = "bbb/aaa" }) + local worse_match = sorter:score("aaa", { ordinal = "aaa/bbb" }) + + assert(better_match < worse_match, "Final match should be stronger") + end) + + pending("sorts multiple finds better", function() + local sorter = require("telescope.sorters").get_fuzzy_file() + + local multi_match = sorter:score("generics", { ordinal = "exercises/generics/generics2.rs" }) + local one_match = sorter:score("abcdef", { ordinal = "exercises/generics/README.md" }) + + assert(multi_match < one_match) + end) + end) + + describe("fzy", function() + local sorter = require("telescope.sorters").get_fzy_sorter() + local function score(prompt, line) + return sorter:score(prompt, { ordinal = line }, function(val) + return val + end, function() + return -1 + end) + end + + describe("matches", function() + it("exact matches", function() + assert.True(score("a", "a") >= 0) + assert.True(score("a.bb", "a.bb") >= 0) + end) + it("ignore case", function() + assert.True(score("AbB", "abb") >= 0) + assert.True(score("abb", "ABB") >= 0) + end) + it("partial matches", function() + assert.True(score("a", "ab") >= 0) + assert.True(score("a", "ba") >= 0) + assert.True(score("aba", "baabbaab") >= 0) + end) + it("with delimiters between", function() + assert.True(score("abc", "a|b|c") >= 0) + end) + it("with empty query", function() + assert.True(score("", "") >= 0) + assert.True(score("", "a") >= 0) + end) + it("rejects non-matches", function() + assert.True(score("a", "") < 0) + assert.True(score("a", "b") < 0) + assert.True(score("aa", "a") < 0) + assert.True(score("ba", "a") < 0) + assert.True(score("ab", "a") < 0) + end) + end) + + describe("scoring", function() + it("prefers beginnings of words", function() + assert.True(score("amor", "app/models/order") < score("amor", "app/models/zrder")) + end) + it("prefers consecutive letters", function() + assert.True(score("amo", "app/models/foo") < score("amo", "app/m/foo")) + assert.True(score("erf", "perfect") < score("erf", "terrific")) + end) + it("prefers contiguous over letter following period", function() + assert.True(score("gemfil", "Gemfile") < score("gemfil", "Gemfile.lock")) + end) + it("prefers shorter matches", function() + assert.True(score("abce", "abcdef") < score("abce", "abc de")) + assert.True(score("abc", " a b c ") < score("abc", " a b c ")) + assert.True(score("abc", " a b c ") < score("abc", " a b c ")) + end) + it("prefers shorter candidates", function() + assert.True(score("test", "tests") < score("test", "testing")) + end) + it("prefers matches at the beginning", function() + assert.True(score("ab", "abbb") < score("ab", "babb")) + assert.True(score("test", "testing") < score("test", "/testing")) + end) + it("prefers matches at some locations", function() + assert.True(score("a", "/a") < score("a", "ba")) + assert.True(score("a", "bA") < score("a", "ba")) + assert.True(score("a", ".a") < score("a", "ba")) + end) + end) + + local function positions(prompt, line) + return sorter:highlighter(prompt, line) + end + + describe("positioning", function() + it("favors consecutive positions", function() + assert.same({ 1, 5, 6 }, positions("amo", "app/models/foo")) + end) + it("favors word beginnings", function() + assert.same({ 1, 5, 12, 13 }, positions("amor", "app/models/order")) + end) + it("works when there are no bonuses", function() + assert.same({ 2, 4 }, positions("as", "tags")) + assert.same({ 3, 8 }, positions("as", "examples.txt")) + end) + it("favors smaller groupings of positions", function() + assert.same({ 3, 5, 7 }, positions("abc", "a/a/b/c/c")) + assert.same({ 3, 5 }, positions("ab", "caacbbc")) + end) + it("handles exact matches", function() + assert.same({ 1, 2, 3 }, positions("foo", "foo")) + end) + it("ignores empty requests", function() + assert.same({}, positions("", "")) + assert.same({}, positions("", "foo")) + assert.same({}, positions("foo", "")) + end) + end) + end) + + describe("layout_strategies", function() + describe("center", function() + it("should handle large terminals", function() + -- TODO: This could call layout_strategies.center w/ some weird edge case. + -- and then assert stuff about the dimensions. + end) + end) + end) + end) +end) diff --git a/etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet/syntax-error-eoi.pp.stdout b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/fixtures/file_a.txt similarity index 100% rename from etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet/syntax-error-eoi.pp.stdout rename to etc/soft/nvim/+plugins/telescope.nvim/lua/tests/fixtures/file_a.txt diff --git a/etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet/syntax-error.pp.stdout b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/fixtures/file_abc.txt similarity index 100% rename from etc/soft/nvim/+plugins/neomake/tests/fixtures/output/puppet/syntax-error.pp.stdout rename to etc/soft/nvim/+plugins/telescope.nvim/lua/tests/fixtures/file_abc.txt diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/helpers.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/helpers.lua new file mode 100644 index 0000000..bdb5f17 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/helpers.lua @@ -0,0 +1,86 @@ +local finders = require "telescope.finders" +local make_entry = require "telescope.make_entry" +local previewers = require "telescope.previewers" +local pickers = require "telescope.pickers" +local sorters = require "telescope.sorters" + +local helpers = {} + +-- TODO: We should do something with builtins to get those easily. +helpers.auto_find_files = function(opts) + opts = opts or {} + opts.prompt_prefix = "" + + local find_command = opts.find_command + + if not find_command then + if 1 == vim.fn.executable "fd" then + find_command = { "fd", "--type", "f" } + elseif 1 == vim.fn.executable "fdfind" then + find_command = { "fdfind", "--type", "f" } + elseif 1 == vim.fn.executable "rg" then + find_command = { "rg", "--files" } + end + end + + if opts.cwd then + opts.cwd = vim.fn.expand(opts.cwd) + end + + opts.entry_maker = opts.entry_maker or make_entry.gen_from_file(opts) + + local p = pickers.new(opts, { + prompt = "Find Files", + finder = finders.new_oneshot_job(find_command, opts), + previewer = previewers.cat.new(opts), + sorter = sorters.get_fuzzy_file(), + + track = true, + }) + + local count = 0 + p:register_completion_callback(function(s) + print( + count, + vim.inspect(s.stats, { + process = function(item) + if type(item) == "string" and item:sub(1, 1) == "_" then + return nil + end + + return item + end, + }) + ) + + count = count + 1 + end) + + local feed = function(text, feed_opts) + feed_opts = feed_opts or "n" + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(text, true, false, true), feed_opts, true) + end + + p:register_completion_callback(coroutine.wrap(function() + local input = opts.input + + for i = 1, #input do + feed(input:sub(i, i)) + coroutine.yield() + end + + vim.wait(300, function() end) + feed("", "") + + vim.defer_fn(function() + PASSED = opts.condition() + COMPLETED = true + end, 500) + + coroutine.yield() + end)) + + p:find() +end + +return helpers diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__readme.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__readme.lua new file mode 100644 index 0000000..1b76ad6 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__readme.lua @@ -0,0 +1,8 @@ +local helper = require "telescope.testharness.helpers" +local runner = require "telescope.testharness.runner" + +runner.picker("find_files", "README.md", { + post_close = { + { "README.md", helper.get_file }, + }, +}) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__scrolling_descending_cycle.lua b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__scrolling_descending_cycle.lua new file mode 100644 index 0000000..6b3c023 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/lua/tests/pickers/find_files__scrolling_descending_cycle.lua @@ -0,0 +1,12 @@ +local tester = require "telescope.testharness" +local helper = require "telescope.testharness.helpers" +local runner = require "telescope.testharness.runner" + +runner.picker("find_files", "telescope", { + post_close = { + tester.not_ { "plugin/telescope.vim", helper.get_file }, + }, +}, { + sorting_strategy = "descending", + scroll_strategy = "cycle", +}) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/plugin/telescope.lua b/etc/soft/nvim/+plugins/telescope.nvim/plugin/telescope.lua new file mode 100644 index 0000000..0fa8c68 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/plugin/telescope.lua @@ -0,0 +1,142 @@ +if 1 ~= vim.fn.has "nvim-0.7.0" then + vim.api.nvim_err_writeln "Telescope.nvim requires at least nvim-0.7.0. See `:h telescope.changelog-1851`" + return +end + +if vim.g.loaded_telescope == 1 then + return +end +vim.g.loaded_telescope = 1 + +local highlights = { + -- Sets the highlight for selected items within the picker. + TelescopeSelection = { default = true, link = "Visual" }, + TelescopeSelectionCaret = { default = true, link = "TelescopeSelection" }, + TelescopeMultiSelection = { default = true, link = "Type" }, + TelescopeMultiIcon = { default = true, link = "Identifier" }, + + -- "Normal" in the floating windows created by telescope. + TelescopeNormal = { default = true, link = "Normal" }, + TelescopePreviewNormal = { default = true, link = "TelescopeNormal" }, + TelescopePromptNormal = { default = true, link = "TelescopeNormal" }, + TelescopeResultsNormal = { default = true, link = "TelescopeNormal" }, + + -- Border highlight groups. + -- Use TelescopeBorder to override the default. + -- Otherwise set them specifically + TelescopeBorder = { default = true, link = "TelescopeNormal" }, + TelescopePromptBorder = { default = true, link = "TelescopeBorder" }, + TelescopeResultsBorder = { default = true, link = "TelescopeBorder" }, + TelescopePreviewBorder = { default = true, link = "TelescopeBorder" }, + + -- Title highlight groups. + -- Use TelescopeTitle to override the default. + -- Otherwise set them specifically + TelescopeTitle = { default = true, link = "TelescopeBorder" }, + TelescopePromptTitle = { default = true, link = "TelescopeTitle" }, + TelescopeResultsTitle = { default = true, link = "TelescopeTitle" }, + TelescopePreviewTitle = { default = true, link = "TelescopeTitle" }, + + TelescopePromptCounter = { default = true, link = "NonText" }, + + -- Used for highlighting characters that you match. + TelescopeMatching = { default = true, link = "Special" }, + + -- Used for the prompt prefix + TelescopePromptPrefix = { default = true, link = "Identifier" }, + + -- Used for highlighting the matched line inside Previewer. Works only for (vim_buffer_ previewer) + TelescopePreviewLine = { default = true, link = "Visual" }, + TelescopePreviewMatch = { default = true, link = "Search" }, + + TelescopePreviewPipe = { default = true, link = "Constant" }, + TelescopePreviewCharDev = { default = true, link = "Constant" }, + TelescopePreviewDirectory = { default = true, link = "Directory" }, + TelescopePreviewBlock = { default = true, link = "Constant" }, + TelescopePreviewLink = { default = true, link = "Special" }, + TelescopePreviewSocket = { default = true, link = "Statement" }, + TelescopePreviewRead = { default = true, link = "Constant" }, + TelescopePreviewWrite = { default = true, link = "Statement" }, + TelescopePreviewExecute = { default = true, link = "String" }, + TelescopePreviewHyphen = { default = true, link = "NonText" }, + TelescopePreviewSticky = { default = true, link = "Keyword" }, + TelescopePreviewSize = { default = true, link = "String" }, + TelescopePreviewUser = { default = true, link = "Constant" }, + TelescopePreviewGroup = { default = true, link = "Constant" }, + TelescopePreviewDate = { default = true, link = "Directory" }, + TelescopePreviewMessage = { default = true, link = "TelescopePreviewNormal" }, + TelescopePreviewMessageFillchar = { default = true, link = "TelescopePreviewMessage" }, + + -- Used for Picker specific Results highlighting + TelescopeResultsClass = { default = true, link = "Function" }, + TelescopeResultsConstant = { default = true, link = "Constant" }, + TelescopeResultsField = { default = true, link = "Function" }, + TelescopeResultsFunction = { default = true, link = "Function" }, + TelescopeResultsMethod = { default = true, link = "Method" }, + TelescopeResultsOperator = { default = true, link = "Operator" }, + TelescopeResultsStruct = { default = true, link = "Struct" }, + TelescopeResultsVariable = { default = true, link = "SpecialChar" }, + + TelescopeResultsLineNr = { default = true, link = "LineNr" }, + TelescopeResultsIdentifier = { default = true, link = "Identifier" }, + TelescopeResultsNumber = { default = true, link = "Number" }, + TelescopeResultsComment = { default = true, link = "Comment" }, + TelescopeResultsSpecialComment = { default = true, link = "SpecialComment" }, + + -- Used for git status Results highlighting + TelescopeResultsDiffChange = { default = true, link = "DiffChange" }, + TelescopeResultsDiffAdd = { default = true, link = "DiffAdd" }, + TelescopeResultsDiffDelete = { default = true, link = "DiffDelete" }, + TelescopeResultsDiffUntracked = { default = true, link = "NonText" }, +} + +for k, v in pairs(highlights) do + vim.api.nvim_set_hl(0, k, v) +end + +-- This is like "" in your terminal. +-- To use it, do `cmap (TelescopeFuzzyCommandSearch) +vim.keymap.set( + "c", + "(TelescopeFuzzyCommandSearch)", + "e \"lua require('telescope.builtin').command_history " + .. '{ default_text = [=[" . escape(getcmdline(), \'"\') . "]=] }"', + { silent = true, noremap = true } +) + +vim.api.nvim_create_user_command("Telescope", function(opts) + require("telescope.command").load_command(unpack(opts.fargs)) +end, { + nargs = "*", + complete = function(_, line) + local builtin_list = vim.tbl_keys(require "telescope.builtin") + local extensions_list = vim.tbl_keys(require("telescope._extensions").manager) + + local l = vim.split(line, "%s+") + local n = #l - 2 + + if n == 0 then + return vim.tbl_filter(function(val) + return vim.startswith(val, l[2]) + end, vim.tbl_extend("force", builtin_list, extensions_list)) + end + + if n == 1 then + local is_extension = vim.tbl_filter(function(val) + return val == l[2] + end, extensions_list) + + if #is_extension > 0 then + local extensions_subcommand_dict = require("telescope.command").get_extensions_subcommand() + return vim.tbl_filter(function(val) + return vim.startswith(val, l[3]) + end, extensions_subcommand_dict[l[2]]) + end + end + + local options_list = vim.tbl_keys(require("telescope.config").values) + return vim.tbl_filter(function(val) + return vim.startswith(val, l[#l]) + end, options_list) + end, +}) diff --git a/etc/soft/nvim/+plugins/telescope.nvim/scripts/gendocs.lua b/etc/soft/nvim/+plugins/telescope.nvim/scripts/gendocs.lua new file mode 100644 index 0000000..cfec160 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/scripts/gendocs.lua @@ -0,0 +1,48 @@ +-- Setup telescope with defaults +if RELOAD then + RELOAD "telescope" +end +require("telescope").setup() + +local docgen = require "docgen" + +local docs = {} + +docs.test = function() + -- TODO: Fix the other files so that we can add them here. + local input_files = { + "./lua/telescope/init.lua", + "./lua/telescope/command.lua", + "./lua/telescope/builtin/init.lua", + "./lua/telescope/themes.lua", + "./lua/telescope/mappings.lua", + "./lua/telescope/pickers/layout_strategies.lua", + "./lua/telescope/config/resolve.lua", + "./lua/telescope/make_entry.lua", + "./lua/telescope/pickers/entry_display.lua", + "./lua/telescope/utils.lua", + "./lua/telescope/actions/init.lua", + "./lua/telescope/actions/state.lua", + "./lua/telescope/actions/set.lua", + "./lua/telescope/actions/layout.lua", + "./lua/telescope/actions/utils.lua", + "./lua/telescope/actions/generate.lua", + "./lua/telescope/previewers/init.lua", + "./lua/telescope/actions/history.lua", + } + + local output_file = "./doc/telescope.txt" + local output_file_handle = io.open(output_file, "w") + + for _, input_file in ipairs(input_files) do + docgen.write(input_file, output_file_handle) + end + + output_file_handle:write " vim:tw=78:ts=8:ft=help:norl:\n" + output_file_handle:close() + vim.cmd [[checktime]] +end + +docs.test() + +return docs diff --git a/etc/soft/nvim/+plugins/telescope.nvim/scripts/minimal_init.vim b/etc/soft/nvim/+plugins/telescope.nvim/scripts/minimal_init.vim new file mode 100644 index 0000000..c2fd6ea --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope.nvim/scripts/minimal_init.vim @@ -0,0 +1,9 @@ +set rtp+=. +set rtp+=../plenary.nvim/ +set rtp+=../tree-sitter-lua/ + +runtime! plugin/plenary.vim +runtime! plugin/telescope.lua +runtime! plugin/ts_lua.vim + +let g:telescope_test_delay = 100 diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/LICENSE b/etc/soft/nvim/+plugins/telescope_hoogle/LICENSE new file mode 100644 index 0000000..6a357bf --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2021 Luc Tielen + +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. diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/README.md b/etc/soft/nvim/+plugins/telescope_hoogle/README.md new file mode 100644 index 0000000..68707ea --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/README.md @@ -0,0 +1,38 @@ + +# telescope_hoogle + +A telescope plugin for Hoogle. + +## Keybindings + +- ``: opens selected entry in the browser: + +![](./hoogle_browser.gif) + +- ``: copies selected entry to clipboard: + +![](./hoogle_paste.gif) + +## Installation + +1. Install [Telescope](https://github.com/nvim-telescope/telescope.nvim) +1. Install a recent Hoogle (needs to support `--json` flag) +2. Run `hoogle generate` +3. Install this plugin (for example: `paq 'luc-tielen/telescope_hoogle'`) +4. Add the following Lua snippet to your nvim config: + +```lua +local telescope = require('telescope') +telescope.setup { + -- opts... +} +telescope.load_extension('hoogle') +``` + +## Development + +```bash +$ git clone git@github.com:luc-tielen/telescope_hoogle.git +$ cd telescope_hoogle +$ nvim --cmd "set rtp+=$(pwd)" +``` diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_browser.gif b/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_browser.gif new file mode 100644 index 0000000..fdc8339 Binary files /dev/null and b/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_browser.gif differ diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_paste.gif b/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_paste.gif new file mode 100644 index 0000000..89e0f9d Binary files /dev/null and b/etc/soft/nvim/+plugins/telescope_hoogle/hoogle_paste.gif differ diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle.lua b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle.lua new file mode 100644 index 0000000..d1870b5 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle.lua @@ -0,0 +1,11 @@ +local has_telescope, telescope = pcall(require, "telescope") +if not has_telescope then + error("This plugin requires nvim-telescope/telescope.nvim") +end + +local hoogle = require 'telescope._extensions.hoogle.live_hoogle_search' + +return telescope.register_extension { + exports = { hoogle = hoogle } +} + diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/json.lua b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/json.lua new file mode 100644 index 0000000..4769e4b --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/json.lua @@ -0,0 +1,106 @@ +--[[ json.parse: + +This function parses json, with the exception that it does not pay attention to +\u-escaped unicode code points in strings. + +It is difficult for Lua to return null as a value. In order to prevent the loss +of keys with a null value in a json string, this function uses the one-off +table value json.null (which is just an empty table) to indicate null values. +This way you can check if a value is null with the conditional +`val == json.null`. + +If you have control over the data and are using Lua, I would recommend just +avoiding null values in your data to begin with. +--]] + + +local json = {} + +-- Returns pos, did_find; there are two cases: +-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. +-- 2. Delimiter not found: pos = pos after leading space; did_find = false. +-- This throws an error if err_if_missing is true and the delim is not found. +local function skip_delim(str, pos, delim, err_if_missing) + pos = pos + #str:match('^%s*', pos) + if str:sub(pos, pos) ~= delim then + if err_if_missing then + error('Expected ' .. delim .. ' near position ' .. pos) + end + return pos, false + end + return pos + 1, true +end + +-- Expects the given pos to be the first character after the opening quote. +-- Returns val, pos; the returned pos is after the closing quote character. +local function parse_str_val(str, pos, val) + val = val or '' + local early_end_error = 'End of input found while parsing string.' + if pos > #str then error(early_end_error) end + local c = str:sub(pos, pos) + if c == '"' then return val, pos + 1 end + if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end + -- We must have a \ character. + local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} + local nextc = str:sub(pos + 1, pos + 1) + if not nextc then error(early_end_error) end + return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) +end + +-- Returns val, pos; the returned pos is after the number's final character. +local function parse_num_val(str, pos) + local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) + local val = tonumber(num_str) + if not val then error('Error parsing number at position ' .. pos .. '.') end + return val, pos + #num_str +end + + +-- Public values and functions. + +json.null = {} -- This is a one-off table to represent the null value. + +function json.parse(str, pos, end_delim) + pos = pos or 1 + if pos > #str then error('Reached unexpected end of input.') end + local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. + local first = str:sub(pos, pos) + if first == '{' then -- Parse an object. + local obj, key, delim_found = {}, true, true + pos = pos + 1 + while true do + key, pos = json.parse(str, pos, '}') + if key == nil then return obj, pos end + if not delim_found then error('Comma missing between object items.') end + pos = skip_delim(str, pos, ':', true) -- true -> error if missing. + obj[key], pos = json.parse(str, pos) + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '[' then -- Parse an array. + local arr, val, delim_found = {}, true, true + pos = pos + 1 + while true do + val, pos = json.parse(str, pos, ']') + if val == nil then return arr, pos end + if not delim_found then error('Comma missing between array items.') end + arr[#arr + 1] = val + pos, delim_found = skip_delim(str, pos, ',') + end + elseif first == '"' then -- Parse a string. + return parse_str_val(str, pos + 1) + elseif first == '-' or first:match('%d') then -- Parse a number. + return parse_num_val(str, pos) + elseif first == end_delim then -- End of an object or array. + return nil, pos + 1 + else -- Parse true, false, or null. + local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} + for lit_str, lit_val in pairs(literals) do + local lit_end = pos + #lit_str - 1 + if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end + end + local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) + error('Invalid json syntax starting at ' .. pos_info_str) + end +end + +return json diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/live_hoogle_search.lua b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/live_hoogle_search.lua new file mode 100644 index 0000000..f5c4412 --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/live_hoogle_search.lua @@ -0,0 +1,151 @@ +local pickers = require 'telescope.pickers' +local finders = require 'telescope.finders' +local actions = require 'telescope.actions' +local actions_state = require 'telescope.actions.state' +local previewers = require 'telescope.previewers' +local previewer_utils = require'telescope.previewers.utils' +local entry_display = require('telescope.pickers.entry_display') +local PreprocessJob = require 'telescope._extensions.hoogle.preprocess_job' +local json = require 'telescope._extensions.hoogle.json' + +local function prompt_to_hoogle_cmd(opts) + local function to_hoogle_cmd(_, prompt) + if not prompt or prompt == '' then + return nil + end + + -- TODO results showing up twice when typing quickly? + local count = opts.count or 50 + return { + command = 'hoogle', + args = vim.tbl_flatten { '--json', '--count=' .. count, prompt } + } + end + + return to_hoogle_cmd +end + +local function format_for_preview(doc) + return doc:gsub('<', '<') + :gsub('>', '>') + :gsub('&', '&') +end + +local function show_preview(entry, buf) + local docs = format_for_preview(entry.docs) + local lines = vim.split(docs, '\n') + vim.api.nvim_buf_set_lines(buf, 0, -1, true, lines) + previewer_utils.highlighter(buf, 'telescope_hoogle_doc') + + vim.api.nvim_buf_call(buf, function() + local win = vim.fn.win_findbuf(buf)[1] + vim.wo[win].conceallevel = 2 + vim.wo[win].wrap = true + vim.wo[win].linebreak = true + vim.bo[buf].textwidth = 80 + end) +end + +local function make_display(entry) + local module = entry.module_name + + local displayer = entry_display.create { + separator = '', + items = { + { width = module and #module + 1 or 0 }, + { remaining = true }, + } + } + return displayer { {module, "Structure"}, {entry.type_sig, "Type"} } +end + +local function entry_maker(data) + return { + valid = true, + module_name = (data.module or {}).name, + type_sig = data.item, + url = data.url, + docs = data.docs, + display = make_display, + ordinal = data.item .. data.url, + preview_command = show_preview + } +end + +local function preprocess_data(data) + if data == 'No results found' then + return {} + end + return json.parse(data) +end + +local function merge(...) + return vim.tbl_extend('keep', ...) +end + +local function copy_to_clipboard(text) + local reg = vim.o.clipboard == 'unnamedplus' and '+' or '"' + vim.fn.setreg(reg, text) +end + +local function open_browser(url) + local browser_cmd + if vim.fn.has('unix') == 1 then + if vim.fn.executable('sensible-browser') == 1 then + browser_cmd = 'sensible-browser' + else + browser_cmd = 'xdg-open' + end + end + if vim.fn.has('mac') == 1 then + browser_cmd = 'open' + end + -- TODO: windows support? + + vim.cmd(':silent !' .. browser_cmd .. ' ' .. vim.fn.fnameescape(url)) +end + +local function live_hoogle_search(opts) + local finder = PreprocessJob:new({ + fn_command = prompt_to_hoogle_cmd(opts), + fn_preprocess = preprocess_data, + entry_maker = entry_maker + }) + + pickers.new(opts, { + prompt_title = 'Live Hoogle search', + finder = finder, + -- TODO don't use display_content + previewer = previewers.display_content.new(opts), + attach_mappings = function(buf, map) + actions.select_default:replace(function() + local entry = actions_state.get_selected_entry() + copy_to_clipboard(entry.type_sig) + actions.close(buf) + end) + map('i', '', function() + local entry = actions_state.get_selected_entry() + open_browser(entry.url) + actions.close(buf) + end) + + return true + end + }):find() +end + +local function setup(opts) + if vim.fn.executable('hoogle') == '1' then + error("telescope_hoogle: 'hoogle' executable not found! Aborting.") + return + end + + opts = merge(opts or {}, { + layout_strategy = 'horizontal', + layout_config = { preview_width = 80 } + }) + + live_hoogle_search(opts) +end + +return setup diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/preprocess_job.lua b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/preprocess_job.lua new file mode 100644 index 0000000..847939b --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/lua/telescope/_extensions/hoogle/preprocess_job.lua @@ -0,0 +1,100 @@ +-- Slightly modified JobFinder from Telescope, able to return multiple results. + +local Job = require 'plenary.job' + +local _callable_obj = function() + local obj = {} + + obj.__index = obj + obj.__call = function(t, ...) return t:_find(...) end + obj.close = function() end + + return obj +end + +local JobFinder = _callable_obj() + +local function default_preprocess(data) + return {data} +end + +--- Create a new finder command +--- +---@param opts table Keys: +-- fn_command function The function to call +function JobFinder:new(opts) + opts = opts or {} + + local obj = setmetatable({ + entry_maker = opts.entry_maker or make_entry.from_string, + fn_command = opts.fn_command, + fn_preprocess = opts.fn_preprocess or default_preprocess, + cwd = opts.cwd, + writer = opts.writer, + + -- Maximum number of results to process. + -- Particularly useful for live updating large queries. + maximum_results = opts.maximum_results, + }, self) + + return obj +end + +function JobFinder:_find(prompt, process_result, process_complete) + if self.job and not self.job.is_shutdown then + self.job:shutdown() + end + + local on_output = function(_, data, _) + if not data or data == "" then + return + end + + -- NOTE: modified part, preprocess function returns array iso single value! + local lines = self.fn_preprocess(data) + + for _, line in ipairs(lines) do + if self.entry_maker then + line = self.entry_maker(line) + end + + -- NOTE: Because we are calling this within a loop, + -- we must defer processing until nvim API functions are safe to call. + vim.defer_fn(function() + process_result(line) + end, + 0) + end + end + + local opts = self:fn_command(prompt) + if not opts then return end + + local writer = nil + if opts.writer and Job.is_job(opts.writer) then + writer = opts.writer + elseif opts.writer then + writer = Job:new(opts.writer) + end + + self.job = Job:new { + command = opts.command, + args = opts.args, + cwd = opts.cwd or self.cwd, + + maximum_results = self.maximum_results, + + writer = writer, + + enable_recording = false, + + on_stdout = on_output, + on_stderr = on_output, + + on_exit = process_complete + } + + self.job:start() +end + +return JobFinder diff --git a/etc/soft/nvim/+plugins/telescope_hoogle/syntax/telescope_hoogle_doc.vim b/etc/soft/nvim/+plugins/telescope_hoogle/syntax/telescope_hoogle_doc.vim new file mode 100644 index 0000000..d64df2f --- /dev/null +++ b/etc/soft/nvim/+plugins/telescope_hoogle/syntax/telescope_hoogle_doc.vim @@ -0,0 +1,15 @@ +" Hoogle doc contains some HTML tags. These have to be concealed and the text +" in between needs to be highlighted. + +syn region codeBlock matchgroup=Tag start="
" end="
" concealends contains=TOP +syn region anchor matchgroup=Tag start="
" end="" concealends contains=TOP +syn region teletype matchgroup=Tag start="" end="" concealends contains=TOP +syn region idiomatic matchgroup=Tag start="" end="" concealends contains=TOP +syn region listItem matchgroup=Tag start="
  • " end="
  • " concealends contains=TOP +syn region unorderedList matchgroup=Tag start="
      " end="
    " concealends contains=TOP +syn region orderedList matchgroup=Tag start="
      " end="
    " concealends contains=TOP + +highlight def link codeBlock SpecialComment +highlight def link teletype Statement +highlight def link anchor Identifier +highlight def link idiomatic Structure diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/README.md b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/README.md new file mode 100644 index 0000000..04c3a31 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/README.md @@ -0,0 +1,147 @@ +## Vim Haskell Conceal+ + +This bundle provides extended Haskell Conceal feature for Vim. The feature +is used to display unicode operators in Haskell code without changing the +underlying file. + +This package offers more (and, more importantly, configurable) features +than the +[baseline vim-haskellConcealbundle](https://github.com/Twinside/vim-haskellConceal). +The baseline bundle has numerous forks, which is possible to combine, so +everyone is welcome to share, improve or contribute new notations to this +Conceal Plus package. + +GitHub: https://github.com/enomsg/vim-haskellConcealPlus + +### Why Concealing + +- Using things like '->' instead real arrows '→' was never a deliberate + choice, but a choice made due to limitations of teletypewriters and + input inconvenience. + +- With concealing you don't have to deal with cumbersome unicode-input + methods, yet you can enjoy proper notation. + +- It is not only about aesthetics. Excess of multi-character functions may + create visual noise, which negatively affects readability. Using special + symbols and true arrows, together with colors and bold/italic face seems + to improve the situation. The image shows Vim with and without + concealing, both running in a plain terminal emulator: + +![demo](https://github.com/enomsg/vim-haskellConcealPlus/raw/master/demo.png) + +- Using concealing instead of *-unicode* versions of packages also has + some advantages. Mainly, concealing does not require any changes to the + source code, it is backwards-compatible with idiomatic code. Secondly, + with concealing no special input methods are needed. Plus, currently + some features are hardly possible without editor's concealing (e.g. + power superscripts). + +### Installation + +Using Vim built-in `pack` support: + +``` +$ mkdir -p ~/.vim/pack/vim-haskellConcealPlus/start +$ cd ~/.vim/pack/vim-haskellConcealPlus/start +$ git clone https://github.com/enomsg/vim-haskellConcealPlus +$ echo "syn on" >> ~/.vimrc # If not already in .vimrc +$ echo "setlocal conceallevel=2" >> ~/.vimrc # If not already in .vimrc +$ echo "set concealcursor=nciv" >> ~/.vimrc # Optional +``` + +### Available Options + + 'q' option to disable concealing of scientific constants (e.g. π) + '℘' option to disable concealing of powerset function + '𝐒' option to disable String type to 𝐒 concealing + '𝐓' option to disable Text type to 𝐓 concealing + '𝐄' option to disable Either/Right/Left to 𝐄/𝑅/𝐿 concealing + '𝐌' option to disable Maybe/Just/Nothing to 𝐌/𝐽/𝑁 concealing + 'A' option to not try to preserve indentation + 's' option to disable space consumption after ∑,∏,√ and ¬ functions + '*' option to enable concealing of asterisk with '⋅' sign + 'x' option to disable default concealing of asterisk with '×' sign + 'E' option to enable ellipsis concealing with ‥ (two dot leader) + 'e' option to disable ellipsis concealing with … (ellipsis sign) + '⇒' option to disable `implies` concealing with ⇒ + '⇔' option to disable `iff` concealing with ⇔ + 'r' option to disable return (η) and join (µ) concealing + 'b' option to disable bind (left and right) concealing + 'f' option to enable formal (★) right bind concealing + 'c' option to enable encircled b/d (ⓑ/ⓓ) for right and left binds + 'h' option to enable partial concealing of binds (e.g. »=) + 'C' option to enable encircled 'm' letter ⓜ concealing for fmap + 'l' option to disable fmap/lift concealing with ↥ + '↱' option to disable mapM/forM concealing with ↱/↰ + 'w' option to disable 'where' concealing with "due to"/∵ symbol + '-' option to disable subtract/(-) concealing with ⊟ + 'I' option to enable alternative ':+' concealing with with ⨢ + 'i' option to disable default concealing of ':+' with ⅈ + 'R' option to disable realPart/imagPart concealing with ℜ/ℑ + 'T' option to enable True/False constants concealing with bold 𝐓/𝐅 + 't' option to disable True/False constants concealing with italic 𝑇/𝐹 + 'B' option to disable Bool type to 𝔹 concealing + 'Q' option to disable Rational type to ℚ concealing + 'Z' option to disable Integer type to ℤ concealing + 'N' option to disable Natural, Nat types to ℕ concealing + 'D' option to disable Double type to 𝔻 concealing + 'C' option to disable Complex type to ℂ concealing + '1' option to disable numeric superscripts concealing, e.g. x² + 'a' option to disable alphabet superscripts concealing, e.g. xⁿ + +The flags can be specified via hscoptions variable. For example, *let +hscoptions="fc"* in your *~/.vimrc*. + +### Known Issues and Hints: + +- Concealing may seriously mess up indentation. By default the bundle + tries to preserve spaces for commonly troublesome symbols (e.g. ->, <- + and => arrows). But to be sure about indentation, you still have to see + the non-concealed code. *set conceallevel=0* might be handy in these + cases. + +- *set concealcursor=nciv* seem to not play well with Vim matchparen + feature (which is enabled by default). You can either disable concealing + under the cursor, or disable matchparen by adding *let + loaded_matchparen=1* at the very top of your *~/.vimrc*. + +- With *set concealcursor=nciv* navigation through concealed parts of code + might be somewhat irritating because the cursor behaves a bit + differently. It becomes less of an issue if you are used to Vim's *w/b* + commands (word forward/backward). You can also try *set + concealcursor=ncv* instead. + +- Finding proper fonts might be a pain. Most of modern, so called + programming fonts (*Inconsolata*, *Anonymous Pro*, etc.) often lack + decent unicode support. As a recommendation, try *DejaVu Sans Mono*. + + **Update**: thanks to [monospacifier](https://github.com/cpitclaudel/monospacifier) + package, fonts are no longer a problem. Pick your favourite font, then in + addition download one of the "monospacified" fallback fonts, save into + `~/.fonts`, and adjust fontconfig, e.g. + `~/.config/fontconfig/fonts.conf`: + + ``` + + + ~/.fonts + + monospace + + TeX Gyre Schola Math monospacified for DejaVu Sans Mono + + + + ``` + +- Most of the terminal emulators have one or more issues with regard to the + unicode characters handling. Emulators that don't have problems with unicode + might be pretty slow. As a recommendation, try xst, or *evilvte* (it has weird + configuration, but draws things correctly) or *lxterminal* (seems to be quite + capable, but limited configurability) or any other terminal emulator that + happened to work for you. + + [xst](https://github.com/gnotclub/xst) is known to work well with DejaVu Sans + Mono, plus [monospacified](https://github.com/cpitclaudel/monospacifier) fonts + as a fallback. diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/haskell.vim b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/haskell.vim new file mode 100644 index 0000000..f27a9de --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/haskell.vim @@ -0,0 +1,482 @@ +" vim: sw=4 +"============================================================================= +" What Is This: Add some conceal operator for your haskell files +" File: haskell.vim (conceal enhancement) +" Last Change: 2011-09-07 +" Version: 1.3.2 +" Require: +" set nocompatible +" somewhere on your .vimrc +" +" Vim 7.3 or Vim compiled with conceal patch. +" Use --with-features=big or huge in order to compile it in. +" +" Usage: +" Drop this file in your +" ~/.vim/after/syntax folder (Linux/MacOSX/BSD...) +" ~/vimfiles/after/syntax folder (Windows) +" +" For this script to work, you have to set the encoding +" to utf-8 :set enc=utf-8 +" +" Additional: +" * if you want to avoid the loading, add the following +" line in your .vimrc : +" let g:no_haskell_conceal = 1 +" Changelog: +" - 1.3.1: putting undefined in extra conceal, not appearing on windows +" - 1.3: adding new arrow characters used by GHC in Unicode extension. +" - 1.2: Fixing conceal level to be local (thx Erlend Hamberg) +" - 1.1: Better handling of non utf-8 systems, and avoid some +" concealing operations on windows on some fonts +" + +" Cf - check a flag. Return true if the flag is specified. +function! Cf(flag) + return exists('g:hscoptions') && stridx(g:hscoptions, a:flag) >= 0 +endfunction + +if exists('g:no_haskell_conceal') || !has('conceal') || &enc != 'utf-8' + finish +endif + +" vim: set fenc=utf-8: +syntax match hsNiceOperator "\\\ze[[:alpha:][:space:]_([]" conceal cchar=λ + +" 'q' option to disable concealing of scientific constants (e.g. π). +if !Cf('q') + syntax match hsNiceOperator "\" conceal cchar=π + syntax match hsNiceOperator "\" conceal cchar=τ + syntax match hsNiceOperator "\" conceal cchar=ℎ + syntax match hsNiceOperator "\" conceal cchar=ℏ +endif + +syntax match hsNiceOperator "==" conceal cchar=≡ +syntax match hsNiceOperator "\/=" conceal cchar=≢ + +let s:extraConceal = 1 +" Some windows font don't support some of the characters, +" so if they are the main font, we don't load them :) +if has("win32") + let s:incompleteFont = [ 'Consolas' + \ , 'Lucida Console' + \ , 'Courier New' + \ ] + let s:mainfont = substitute( &guifont, '^\([^:,]\+\).*', '\1', '') + for s:fontName in s:incompleteFont + if s:mainfont ==? s:fontName + let s:extraConceal = 0 + break + endif + endfor +endif + +if s:extraConceal + syntax match hsNiceOperator "\" conceal cchar=⊥ + + " Match greater than and lower than w/o messing with Kleisli composition + syntax match hsNiceOperator "<=\ze[^<]" conceal cchar=≤ + syntax match hsNiceOperator ">=\ze[^>]" conceal cchar=≥ + + " Redfining to get proper '::' concealing + syntax match hs_DeclareFunction /^[a-z_(]\S*\(\s\|\n\)*::/me=e-2 nextgroup=hsNiceOperator contains=hs_FunctionName,hs_OpFunctionName + + syntax match hsNiceoperator "!!" conceal cchar=‼ + syntax match hsNiceoperator "++\ze[^+]" conceal cchar=⧺ + syntax match hsNiceOperator "\" conceal cchar=∀ + syntax match hsNiceOperator "-<" conceal cchar=↢ + syntax match hsNiceOperator ">-" conceal cchar=↣ + syntax match hsNiceOperator "-<<" conceal cchar=⤛ + syntax match hsNiceOperator ">>-" conceal cchar=⤜ + " the star does not seem so good... + " syntax match hsNiceOperator "*" conceal cchar=★ + syntax match hsNiceOperator "`div`" conceal cchar=÷ + + " Only replace the dot, avoid taking spaces around. + syntax match hsNiceOperator /\s\.\s/ms=s+1,me=e-1 conceal cchar=∘ + + syntax match hsQQEnd "|\]" contained conceal cchar=〛 + " sy match hsQQEnd "|\]" contained conceal=〚 + + syntax match hsNiceOperator "`elem`" conceal cchar=∈ + syntax match hsNiceOperator "`notElem`" conceal cchar=∉ + syntax match hsNiceOperator "`isSubsetOf`" conceal cchar=⊆ + syntax match hsNiceOperator "`union`" conceal cchar=∪ + syntax match hsNiceOperator "`intersect`" conceal cchar=∩ + syntax match hsNiceOperator "\\\\\ze[[:alpha:][:space:]_([]" conceal cchar=∖ + + syntax match hsNiceOperator "||\ze[[:alpha:][:space:]_([]" conceal cchar=∨ + syntax match hsNiceOperator "&&\ze[[:alpha:][:space:]_([]" conceal cchar=∧ + + syntax match hsNiceOperator "<\*>" conceal cchar=⊛ + syntax match hsNiceOperator "`mappend`" conceal cchar=⊕ + syntax match hsNiceOperator "\" conceal cchar=⊕ + syntax match hsNiceOperator "<>" conceal cchar=⊕ + syntax match hsNiceOperator "\" conceal cchar=∅ + syntax match hsNiceOperator "\" conceal cchar=∅ + syntax match hsNiceOperator "\" conceal cchar=∅ +endif + +hi link hsNiceOperator Operator +hi! link Conceal Operator +setlocal conceallevel=2 + +" '℘' option to disable concealing of powerset function +if !Cf('℘') + syntax match hsNiceOperator "\" conceal cchar=℘ +endif + +" '𝐒' option to disable String type to 𝐒 concealing +if !Cf('𝐒') + syntax match hsNiceOperator "\" conceal cchar=𝐒 +endif + +" '𝐓' option to disable Text type to 𝐓 concealing +if !Cf('𝐓') + syntax match hsNiceOperator "\" conceal cchar=𝐓 +endif + +" '𝐄' option to disable Either/Right/Left to 𝐄/𝑅/𝐿 concealing +if !Cf('𝐄') + syntax match hsNiceOperator "\" conceal cchar=𝐄 + syntax match hsNiceOperator "\" conceal cchar=𝑅 + syntax match hsNiceOperator "\" conceal cchar=𝐿 +endif + +" '𝐌' option to disable Maybe/Just/Nothing to 𝐌/𝐽/𝑁 concealing +if !Cf('𝐌') + syntax match hsNiceOperator "\" conceal cchar=𝐌 + syntax match hsNiceOperator "\" conceal cchar=𝐽 + syntax match hsNiceOperator "\" conceal cchar=𝑁 +endif + +" 'A' option to not try to preserve indentation. +if Cf('A') + syntax match hsNiceOperator "<-" conceal cchar=← + syntax match hsNiceOperator "->" conceal cchar=→ + syntax match hsNiceOperator "=>" conceal cchar=⇒ + syntax match hsNiceOperator "\:\:" conceal cchar=∷ +else + syntax match hsLRArrowHead contained ">" conceal cchar= + syntax match hsLRArrowTail contained "-" conceal cchar=→ + syntax match hsLRArrowFull "->" contains=hsLRArrowHead,hsLRArrowTail + + syntax match hsRLArrowHead contained "<" conceal cchar=← + syntax match hsRLArrowTail contained "-" conceal cchar= + syntax match hsRLArrowFull "<-" contains=hsRLArrowHead,hsRLArrowTail + + syntax match hsLRDArrowHead contained ">" conceal cchar= + syntax match hsLRDArrowTail contained "=" conceal cchar=⇒ + syntax match hsLRDArrowFull "=>" contains=hsLRDArrowHead,hsLRDArrowTail +endif + +" 's' option to disable space consumption after ∑,∏,√ and ¬ functions. +if Cf('s') + syntax match hsNiceOperator "\" conceal cchar=∑ + syntax match hsNiceOperator "\" conceal cchar=∏ + syntax match hsNiceOperator "\" conceal cchar=√ + syntax match hsNiceOperator "\" conceal cchar=¬ +else + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=∑ + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=∏ + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=√ + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=¬ +endif + +" '*' option to enable concealing of asterisk with '⋅' sign. +if Cf('*') + syntax match hsNiceOperator "*" conceal cchar=⋅ +" 'x' option to disable default concealing of asterisk with '×' sign. +elseif !Cf('x') + syntax match hsNiceOperator "*" conceal cchar=× +endif + +" 'E' option to enable ellipsis concealing with ‥ (two dot leader). +if Cf('E') + " The two dot leader is not guaranteed to be at the bottom. So, it + " will break on some fonts. + syntax match hsNiceOperator "\.\." conceal cchar=‥ +" 'e' option to disable ellipsis concealing with … (ellipsis sign). +elseif !Cf('e') + syntax match hsNiceOperator "\.\." conceal cchar=… +end + +" '⇒' option to disable `implies` concealing with ⇒ +if !Cf('⇒') + " Easily distinguishable from => keyword since the keyword can only be + " used in type signatures. + syntax match hsNiceOperator "`implies`" conceal cchar=⇒ +endif + +" '⇔' option to disable `iff` concealing with ⇔ +if !Cf('⇔') + syntax match hsNiceOperator "`iff`" conceal cchar=⇔ +endif + +" 'r' option to disable return (η) and join (µ) concealing. +if !Cf('r') + syntax match hsNiceOperator "\" conceal cchar=η + syntax match hsNiceOperator "\" conceal cchar=µ +endif + +" 'b' option to disable bind (left and right) concealing +if Cf('b') + " Vim has some issues concealing with composite symbols like '«̳', and + " unfortunately there is no other common short notation for both + " binds. So 'b' option to disable bind concealing altogether. +" 'f' option to enable formal (★) right bind concealing +elseif Cf('f') + syntax match hsNiceOperator ">>=" conceal cchar=★ +" 'c' option to enable encircled b/d (ⓑ/ⓓ) for right and left binds. +elseif Cf('c') + syntax match hsNiceOperator ">>=" conceal cchar=ⓑ + syntax match hsNiceOperator "=<<" conceal cchar=ⓓ +" 'h' option to enable partial concealing of binds (e.g. »=). +elseif Cf('h') + syntax match hsNiceOperator ">>" conceal cchar=» + syntax match hsNiceOperator "<<" conceal cchar=« + syntax match hsNiceOperator "=\zs<<" conceal cchar=« +" Left and right arrows with hooks are the default option for binds. +else + syntax match hsNiceOperator ">>=\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=↪ + syntax match hsNiceOperator "=<<\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=↩ +endif + +if !Cf('h') + syntax match hsNiceOperator ">>\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=» + syntax match hsNiceOperator "<<\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=« +endif + +" 'C' option to enable encircled 'm' letter ⓜ concealing for fmap. +if Cf('C') + syntax match hsNiceOperator "<$>" conceal cchar=ⓜ + syntax match hsNiceOperator "`fmap`" conceal cchar=ⓜ +" 'l' option to disable fmap/lift concealing with ↥. +elseif !Cf('l') + syntax match hsNiceOperator "`liftM`" conceal cchar=↥ + syntax match hsNiceOperator "`liftA`" conceal cchar=↥ + syntax match hsNiceOperator "`fmap`" conceal cchar=↥ + syntax match hsNiceOperator "<$>" conceal cchar=↥ + + syntax match LIFTQ contained "`" conceal + syntax match LIFTQl contained "l" conceal cchar=↥ + syntax match LIFTl contained "l" conceal cchar=↥ + syntax match LIFTi contained "i" conceal + syntax match LIFTf contained "f" conceal + syntax match LIFTt contained "t" conceal + syntax match LIFTA contained "A" conceal + syntax match LIFTM contained "M" conceal + syntax match LIFT2 contained "2" conceal cchar=² + syntax match LIFT3 contained "3" conceal cchar=³ + syntax match LIFT4 contained "4" conceal cchar=⁴ + syntax match LIFT5 contained "5" conceal cchar=⁵ + + syntax match hsNiceOperator "`liftM2`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT2 + syntax match hsNiceOperator "`liftM3`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT3 + syntax match hsNiceOperator "`liftM4`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT4 + syntax match hsNiceOperator "`liftM5`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT5 + syntax match hsNiceOperator "`liftA2`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT2 + syntax match hsNiceOperator "`liftA3`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT3 + + syntax match FMAPf contained "f" conceal cchar=↥ + syntax match FMAPm contained "m" conceal + syntax match FMAPa contained "a" conceal + syntax match FMAPp contained "p" conceal + syntax match FMAPSPC contained " " conceal + syntax match hsNiceOperator "\\s*" contains=FMAPf,FMAPm,FMAPa,FMAPp,FMAPSPC + + syntax match LIFTSPC contained " " conceal + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT2,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT3,LIFTSPC + + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT2,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT3,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT4,LIFTSPC + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT5,LIFTSPC + + " TODO: Move liftIO to its own flag? + syntax match LIFTIOL contained "l" conceal + syntax match LIFTI contained "I" conceal cchar=i + syntax match LIFTO contained "O" conceal cchar=o + syntax match hsNiceOperator "\" contains=LIFTIOl,LIFTi,LIFTf,LIFTt,LIFTI,LIFTO +endif + +" '↱' option to disable mapM/forM concealing with ↱/↰ +if !Cf('↱') + syntax match MAPMQ contained "`" conceal + syntax match MAPMm contained "m" conceal cchar=↱ + syntax match MAPMmQ contained "m" conceal cchar=↰ + syntax match MAPMa contained "a" conceal + syntax match MAPMp contained "p" conceal + syntax match MAPMM contained "M" conceal + syntax match MAPMM contained "M" conceal + syntax match MAPMU contained "_" conceal cchar=_ + syntax match SPC contained " " conceal + syntax match hsNiceOperator "`mapM_`" contains=MAPMQ,MAPMmQ,MAPMa,MAPMp,MAPMM,MAPMU + syntax match hsNiceOperator "`mapM`" contains=MAPMQ,MAPMmQ,MAPMa,MAPMp,MAPMM + syntax match hsNiceOperator "\\s*" contains=MAPMm,MAPMa,MAPMp,MAPMM,SPC + syntax match hsNiceOperator "\\s*" contains=MAPMm,MAPMa,MAPMp,MAPMM,MAPMU,SPC + + syntax match FORMQ contained "`" conceal + syntax match FORMfQ contained "f" conceal cchar=↱ + syntax match FORMf contained "f" conceal cchar=↰ + syntax match FORMo contained "o" conceal + syntax match FORMr contained "r" conceal + syntax match FORMM contained "M" conceal + syntax match FORMU contained "_" conceal cchar=_ + + syntax match hsNiceOperator "`forM`" contains=FORMQ,FORMfQ,FORMo,FORMr,FORMM + syntax match hsNiceOperator "`forM_`" contains=FORMQ,FORMfQ,FORMo,FORMr,FORMM,FORMU + + syntax match hsNiceOperator "\\s*" contains=FORMf,FORMo,FORMr,FORMM,SPC + syntax match hsNiceOperator "\\s*" contains=FORMf,FORMo,FORMr,FORMM,FORMU,SPC +endif + +" 'w' option to disable 'where' concealing with "due to"/∵ symbol. +if !Cf('w') + " ∵ means "because/since/due to." With quite a stretch this can be + " used for 'where'. We preserve spacing, otherwise it breaks indenting + " in a major way. + syntax match WS contained "w" conceal cchar=∵ + syntax match HS contained "h" conceal cchar= + syntax match ES contained "e" conceal cchar= + syntax match RS contained "r" conceal cchar= + syntax match hsNiceOperator "\" contains=WS,HS,ES,RS,ES +endif + +" '-' option to disable subtract/(-) concealing with ⊟. +if !Cf('-') + " Minus is a special syntax construct in Haskell. We use squared minus to + " tell the syntax from the binary function. + syntax match hsNiceOperator "(-)" conceal cchar=⊟ + syntax match hsNiceOperator "`subtract`" conceal cchar=⊟ +endif + +" 'I' option to enable alternative ':+' concealing with with ⨢. +if Cf('I') + " With some fonts might look better than ⅈ. + syntax match hsNiceOperator ":+" conceal cchar=⨢ +" 'i' option to disable default concealing of ':+' with ⅈ. +elseif !Cf('i') + syntax match COLON contained ":" conceal cchar=+ + syntax match PLUS contained "+" conceal cchar= + syntax match SPACE contained " " conceal cchar=ⅈ + syntax match hsNiceOperator ":+ " contains=COLON,PLUS,SPACE + "syntax match hsNiceOperator ":+" conceal cchar=ⅈ +endif + +" 'R' option to disable realPart/imagPart concealing with ℜ/ℑ. +if !Cf('R') + syntax match hsNiceOperator "\" conceal cchar=ℜ + syntax match hsNiceOperator "\" conceal cchar=ℑ +endif + +" 'T' option to enable True/False constants concealing with bold 𝐓/𝐅. +if Cf('T') + syntax match hsNiceSpecial "\" conceal cchar=𝐓 + syntax match hsNiceSpecial "\" conceal cchar=𝐅 +" 't' option to disable True/False constants concealing with italic 𝑇/𝐹. +elseif !Cf('t') + syntax match hsNiceSpecial "\" conceal cchar=𝑇 + syntax match hsNiceSpecial "\" conceal cchar=𝐹 +endif + +" 'B' option to disable Bool type to 𝔹 concealing +if !Cf('B') + " Not an official notation ttbomk. But at least + " http://www.haskell.org/haskellwiki/Unicode-symbols mentions it. + syntax match hsNiceOperator "\" conceal cchar=𝔹 +endif + +" 'Q' option to disable Rational type to ℚ concealing. +if !Cf('Q') + syntax match hsNiceOperator "\" conceal cchar=ℚ +endif + +" 'Z' option to disable Integer type to ℤ concealing. +if !Cf('Z') + syntax match hsNiceOperator "\" conceal cchar=ℤ +endif + +" 'N' option to disable Natural, Nat types to ℕ concealing. +if !Cf('N') + syntax match hsNiceOperator "\" conceal cchar=ℕ + syntax match hsNiceOperator "\" conceal cchar=ℕ +endif + +" 'D' option to disable Double type to 𝔻 concealing +if !Cf('D') + syntax match hsNiceOperator "\" conceal cchar=𝔻 +endif + +" 'C' option to disable Complex type to ℂ concealing +if !Cf('C') + syntax match hasNiceOperator "\" conceal cchar=ℂ +endif + +" '1' option to disable numeric superscripts concealing, e.g. x². +if !Cf('1') + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)0\ze\_W" conceal cchar=⁰ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)1\ze\_W" conceal cchar=¹ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)2\ze\_W" conceal cchar=² + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)3\ze\_W" conceal cchar=³ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)4\ze\_W" conceal cchar=⁴ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)5\ze\_W" conceal cchar=⁵ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)6\ze\_W" conceal cchar=⁶ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)7\ze\_W" conceal cchar=⁷ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)8\ze\_W" conceal cchar=⁸ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)9\ze\_W" conceal cchar=⁹ +endif + +" 'a' option to disable alphabet superscripts concealing, e.g. xⁿ. +if !Cf('a') + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)a\ze\_W" conceal cchar=ᵃ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)b\ze\_W" conceal cchar=ᵇ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)c\ze\_W" conceal cchar=ᶜ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)d\ze\_W" conceal cchar=ᵈ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)e\ze\_W" conceal cchar=ᵉ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)f\ze\_W" conceal cchar=ᶠ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)g\ze\_W" conceal cchar=ᵍ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)h\ze\_W" conceal cchar=ʰ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)i\ze\_W" conceal cchar=ⁱ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)j\ze\_W" conceal cchar=ʲ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)k\ze\_W" conceal cchar=ᵏ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)l\ze\_W" conceal cchar=ˡ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)m\ze\_W" conceal cchar=ᵐ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)n\ze\_W" conceal cchar=ⁿ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)o\ze\_W" conceal cchar=ᵒ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)p\ze\_W" conceal cchar=ᵖ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)r\ze\_W" conceal cchar=ʳ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)s\ze\_W" conceal cchar=ˢ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)t\ze\_W" conceal cchar=ᵗ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)u\ze\_W" conceal cchar=ᵘ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)v\ze\_W" conceal cchar=ᵛ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)w\ze\_W" conceal cchar=ʷ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)x\ze\_W" conceal cchar=ˣ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)y\ze\_W" conceal cchar=ʸ + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)z\ze\_W" conceal cchar=ᶻ +endif + +" Not really Haskell, but quite handy for writing proofs in pseudo-code. +if Cf('∴') + syntax match hsNiceOperator "\" conceal cchar=∴ + syntax match hsNiceOperator "\" conceal cchar=∃ + syntax match hsNiceOperator "\" conceal cchar=∄ + syntax match hsNiceOperator ":=" conceal cchar=≝ +endif + +" TODO: +" See Basic Syntax Extensions - School of Haskell | FP Complete +" intersection = (∩) +" +" From the Data.IntMap.Strict.Unicode +" notMember = (∉) = flip (∌) +" member = (∈) = flip (∋) +" isProperSubsetOf = (⊂) = flip (⊃) +" +" From Data.Sequence.Unicode +" (<|) = (⊲ ) +" (|>) = (⊳ ) +" (><) = (⋈ ) diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/lhaskell.vim b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/lhaskell.vim new file mode 100644 index 0000000..f6edcfb --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/after/syntax/lhaskell.vim @@ -0,0 +1,477 @@ +" vim: sw=4 +"============================================================================= +" What Is This: Add some conceal operator for your literate haskell files +" File: lhaskell.vim (conceal enhancement) +" Last Change: 2015-11-13 +" Version: 1.3.3 +" Require: +" set nocompatible +" somewhere on your .vimrc +" +" Vim 7.3 or Vim compiled with conceal patch. +" Use --with-features=big or huge in order to compile it in. +" +" Usage: +" Drop this file in your +" ~/.vim/after/syntax folder (Linux/MacOSX/BSD...) +" ~/vimfiles/after/syntax folder (Windows) +" +" For this script to work, you have to set the encoding +" to utf-8 :set enc=utf-8 +" +" Additional: +" * if you want to avoid the loading, add the following +" line in your .vimrc : +" let g:no_haskell_conceal = 1 +" Changelog: +" - 1.3.3: lhaskell.vim added, only concealing inside code (\begin, >>) tags. +" - 1.3.1: putting undefined in extra conceal, not appearing on windows +" - 1.3: adding new arrow characters used by GHC in Unicode extension. +" - 1.2: Fixing conceal level to be local (thx Erlend Hamberg) +" - 1.1: Better handling of non utf-8 systems, and avoid some +" concealing operations on windows on some fonts +" + + +" Cf - check a flag. Return true if the flag is specified. +function! Cf(flag) + return exists('g:hscoptions') && stridx(g:hscoptions, a:flag) >= 0 +endfunction + +if exists('g:no_haskell_conceal') || !has('conceal') || &enc != 'utf-8' + finish +endif + +syntax cluster haskellTop add=hsNiceOperator + +" vim: set fenc=utf-8: +syntax match hsNiceOperator "\\\ze[[:alpha:][:space:]_([]" conceal cchar=λ contained + +" 'q' option to disable concealing of scientific constants (e.g. π). +if !Cf('q') + syntax match hsNiceOperator "\" conceal cchar=π contained + syntax match hsNiceOperator "\" conceal cchar=τ contained + syntax match hsNiceOperator "\" conceal cchar=ℎ contained + syntax match hsNiceOperator "\" conceal cchar=ℏ contained +endif + +syntax match hsNiceOperator "==" conceal cchar=≡ contained +syntax match hsNiceOperator "\/=" conceal cchar=≢ contained + +let s:extraConceal = 1 +" Some windows font don't support some of the characters, +" so if they are the main font, we don't load them :) +if has("win32") + let s:incompleteFont = [ 'Consolas' + \ , 'Lucida Console' + \ , 'Courier New' + \ ] + let s:mainfont = substitute( &guifont, '^\([^:,]\+\).*', '\1', '') + for s:fontName in s:incompleteFont + if s:mainfont ==? s:fontName + let s:extraConceal = 0 + break + endif + endfor +endif + +if s:extraConceal + syntax match hsNiceOperator "\" conceal cchar=⊥ contained + + " Match greater than and lower than w/o messing with Kleisli composition + syntax match hsNiceOperator "<=\ze[^<]" conceal cchar=≤ contained + syntax match hsNiceOperator ">=\ze[^>]" conceal cchar=≥ contained + + " Redfining to get proper '::' concealing + syntax match hs_DeclareFunction /^[a-z_(]\S*\(\s\|\n\)*::/me=e-2 nextgroup=hsNiceOperator contains=hs_FunctionName,hs_OpFunctionName contained + + syntax match hsNiceoperator "!!" conceal cchar=‼ contained + syntax match hsNiceoperator "++\ze[^+]" conceal cchar=⧺ contained + syntax match hsNiceOperator "\" conceal cchar=∀ contained + syntax match hsNiceOperator "-<" conceal cchar=↢ contained + syntax match hsNiceOperator ">-" conceal cchar=↣ contained + syntax match hsNiceOperator "-<<" conceal cchar=⤛ contained + syntax match hsNiceOperator ">>-" conceal cchar=⤜ contained + " the star does not seem so good... + " syntax match hsNiceOperator "*" conceal cchar=★ + syntax match hsNiceOperator "`div`" conceal cchar=÷ contained + + " Only replace the dot, avoid taking spaces around. + syntax match hsNiceOperator /\s\.\s/ms=s+1,me=e-1 conceal cchar=∘ contained + + syntax match hsQQEnd "|\]" contained conceal cchar=〛 contained + " sy match hsQQEnd "|\]" contained conceal=〚 + + syntax match hsNiceOperator "`elem`" conceal cchar=∈ contained + syntax match hsNiceOperator "`notElem`" conceal cchar=∉ contained + syntax match hsNiceOperator "`isSubsetOf`" conceal cchar=⊆ contained + syntax match hsNiceOperator "`union`" conceal cchar=∪ contained + syntax match hsNiceOperator "`intersect`" conceal cchar=∩ contained + syntax match hsNiceOperator "\\\\\ze[[:alpha:][:space:]_([]" conceal cchar=∖ contained + + syntax match hsNiceOperator "||\ze[[:alpha:][:space:]_([]" conceal cchar=∨ contained + syntax match hsNiceOperator "&&\ze[[:alpha:][:space:]_([]" conceal cchar=∧ contained + + syntax match hsNiceOperator "<\*>" conceal cchar=⊛ contained + syntax match hsNiceOperator "`mappend`" conceal cchar=⊕ contained + syntax match hsNiceOperator "\" conceal cchar=⊕ + syntax match hsNiceOperator "<>" conceal cchar=⊕ contained + syntax match hsNiceOperator "\" conceal cchar=∅ contained + syntax match hsNiceOperator "\" conceal cchar=∅ contained + syntax match hsNiceOperator "\" conceal cchar=∅ contained +endif + +hi link hsNiceOperator Operator +hi! link Conceal Operator +setlocal conceallevel=2 + +" '℘' option to disable concealing of powerset function +if !Cf('℘') + syntax match hsNiceOperator "\" conceal cchar=℘ contained +endif + +" '𝐒' option to disable String type to 𝐒 concealing +if !Cf('𝐒') + syntax match hsNiceOperator "\" conceal cchar=𝐒 contained +endif + +" '𝐓' option to disable Text type to 𝐓 concealing +if !Cf('𝐓') + syntax match hsNiceOperator "\" conceal cchar=𝐓 contained +endif + +" '𝐄' option to disable Either/Right/Left to 𝐄/𝑅/𝐿 concealing +if !Cf('𝐄') + syntax match hsNiceOperator "\" conceal cchar=𝐄 contained + syntax match hsNiceOperator "\" conceal cchar=𝑅 contained + syntax match hsNiceOperator "\" conceal cchar=𝐿 contained +endif + +" '𝐌' option to disable Maybe/Just/Nothing to 𝐌/𝐽/𝑁 concealing +if !Cf('𝐌') + syntax match hsNiceOperator "\" conceal cchar=𝐌 contained + syntax match hsNiceOperator "\" conceal cchar=𝐽 contained + syntax match hsNiceOperator "\" conceal cchar=𝑁 contained +endif + +" 'A' option to not try to preserve indentation. +if Cf('A') + syntax match hsNiceOperator "<-" conceal cchar=← contained + syntax match hsNiceOperator "->" conceal cchar=→ contained + syntax match hsNiceOperator "=>" conceal cchar=⇒ contained + syntax match hsNiceOperator "\:\:" conceal cchar=∷ contained +else + syntax match hsLRArrowHead contained ">" conceal cchar= contained + syntax match hsLRArrowTail contained "-" conceal cchar=→ contained + syntax match hsLRArrowFull "->" contains=hsLRArrowHead,hsLRArrowTail contained + + syntax match hsRLArrowHead contained "<" conceal cchar=← contained + syntax match hsRLArrowTail contained "-" conceal cchar= contained + syntax match hsRLArrowFull "<-" contains=hsRLArrowHead,hsRLArrowTail contained + + syntax match hsLRDArrowHead contained ">" conceal cchar= contained + syntax match hsLRDArrowTail contained "=" conceal cchar=⇒ contained + syntax match hsLRDArrowFull "=>" contains=hsLRDArrowHead,hsLRDArrowTail contained +endif + +" 's' option to disable space consumption after ∑,∏,√ and ¬ functions. +if Cf('s') + syntax match hsNiceOperator "\" conceal cchar=∑ contained + syntax match hsNiceOperator "\" conceal cchar=∏ contained + syntax match hsNiceOperator "\" conceal cchar=√ contained + syntax match hsNiceOperator "\" conceal cchar=¬ contained +else + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=∑ contained + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=∏ contained + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=√ contained + syntax match hsNiceOperator "\\(\ze\s*[.$]\|\s*\)" conceal cchar=¬ contained +endif + +" '*' option to enable concealing of asterisk with '⋅' sign. +if Cf('*') + syntax match hsNiceOperator "*" conceal cchar=⋅ contained +" 'x' option to disable default concealing of asterisk with '×' sign. +elseif !Cf('x') + syntax match hsNiceOperator "*" conceal cchar=× contained +endif + +" 'E' option to enable ellipsis concealing with ‥ (two dot leader). +if Cf('E') + " The two dot leader is not guaranteed to be at the bottom. So, it + " will break on some fonts. + syntax match hsNiceOperator "\.\." conceal cchar=‥ contained +" 'e' option to disable ellipsis concealing with … (ellipsis sign). +elseif !Cf('e') + syntax match hsNiceOperator "\.\." conceal cchar=… contained +end + +" '⇒' option to disable `implies` concealing with ⇒ +if !Cf('⇒') + " Easily distinguishable from => keyword since the keyword can only be + " used in type signatures. + syntax match hsNiceOperator "`implies`" conceal cchar=⇒ contained +endif + +" '⇔' option to disable `iff` concealing with ⇔ +if !Cf('⇔') + syntax match hsNiceOperator "`iff`" conceal cchar=⇔ contained +endif + +" 'r' option to disable return (η) and join (µ) concealing. +if !Cf('r') + syntax match hsNiceOperator "\" conceal cchar=η contained + syntax match hsNiceOperator "\" conceal cchar=µ contained +endif + +" 'b' option to disable bind (left and right) concealing +if Cf('b') + " Vim has some issues concealing with composite symbols like '«̳', and + " unfortunately there is no other common short notation for both + " binds. So 'b' option to disable bind concealing altogether. +" 'f' option to enable formal (★) right bind concealing +elseif Cf('f') + syntax match hsNiceOperator ">>=" conceal cchar=★ contained +" 'c' option to enable encircled b/d (ⓑ/ⓓ) for right and left binds. +elseif Cf('c') + syntax match hsNiceOperator ">>=" conceal cchar=ⓑ contained + syntax match hsNiceOperator "=<<" conceal cchar=ⓓ contained +" 'h' option to enable partial concealing of binds (e.g. »=). +elseif Cf('h') + syntax match hsNiceOperator ">>" conceal cchar=» contained + syntax match hsNiceOperator "<<" conceal cchar=« contained + syntax match hsNiceOperator "=\zs<<" conceal cchar=« contained +" Left and right arrows with hooks are the default option for binds. +else + syntax match hsNiceOperator ">>=\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=↪ contained + syntax match hsNiceOperator "=<<\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=↩ contained +endif + +if !Cf('h') + syntax match hsNiceOperator ">>\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=» contained + syntax match hsNiceOperator "<<\ze\_[[:alpha:][:space:]_()[\]]" conceal cchar=« contained +endif + +" 'C' option to enable encircled 'm' letter ⓜ concealing for fmap. +if Cf('C') + syntax match hsNiceOperator "<$>" conceal cchar=ⓜ contained + syntax match hsNiceOperator "`fmap`" conceal cchar=ⓜ contained +" 'l' option to disable fmap/lift concealing with ↥. +elseif !Cf('l') + syntax match hsNiceOperator "`liftM`" conceal cchar=↥ contained + syntax match hsNiceOperator "`liftA`" conceal cchar=↥ contained + syntax match hsNiceOperator "`fmap`" conceal cchar=↥ contained + syntax match hsNiceOperator "<$>" conceal cchar=↥ contained + + syntax match LIFTQ contained "`" conceal contained + syntax match LIFTQl contained "l" conceal cchar=↥ contained + syntax match LIFTl contained "l" conceal cchar=↥ contained + syntax match LIFTi contained "i" conceal contained + syntax match LIFTf contained "f" conceal contained + syntax match LIFTt contained "t" conceal contained + syntax match LIFTA contained "A" conceal contained + syntax match LIFTM contained "M" conceal contained + syntax match LIFT2 contained "2" conceal cchar=² contained + syntax match LIFT3 contained "3" conceal cchar=³ contained + syntax match LIFT4 contained "4" conceal cchar=⁴ contained + syntax match LIFT5 contained "5" conceal cchar=⁵ contained + + syntax match hsNiceOperator "`liftM2`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT2 contained + syntax match hsNiceOperator "`liftM3`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT3 contained + syntax match hsNiceOperator "`liftM4`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT4 contained + syntax match hsNiceOperator "`liftM5`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT5 contained + syntax match hsNiceOperator "`liftA2`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT2 contained + syntax match hsNiceOperator "`liftA3`" contains=LIFTQ,LIFTQl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT3 contained + + syntax match FMAPf contained "f" conceal cchar=↥ contained + syntax match FMAPm contained "m" conceal contained + syntax match FMAPa contained "a" conceal contained + syntax match FMAPp contained "p" conceal contained + syntax match FMAPSPC contained " " conceal contained + syntax match hsNiceOperator "\\s*" contains=FMAPf,FMAPm,FMAPa,FMAPp,FMAPSPC contained + + syntax match LIFTSPC contained " " conceal contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT2,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTA,LIFT3,LIFTSPC contained + + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT2,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT3,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT4,LIFTSPC contained + syntax match hsNiceOperator "\\s*" contains=LIFTl,LIFTi,LIFTf,LIFTt,LIFTM,LIFT5,LIFTSPC contained + + " TODO: Move liftIO to its own flag? + syntax match LIFTIOL contained "l" conceal contained + syntax match LIFTI contained "I" conceal cchar=i contained + syntax match LIFTO contained "O" conceal cchar=o contained + syntax match hsNiceOperator "\" contains=LIFTIOl,LIFTi,LIFTf,LIFTt,LIFTI,LIFTO contained +endif + +" '↱' option to disable mapM/forM concealing with ↱/↰ +if !Cf('↱') + syntax match MAPMQ contained "`" conceal contained + syntax match MAPMm contained "m" conceal cchar=↱ contained + syntax match MAPMmQ contained "m" conceal cchar=↰ contained + syntax match MAPMa contained "a" conceal contained + syntax match MAPMp contained "p" conceal contained + syntax match MAPMM contained "M" conceal contained + syntax match MAPMM contained "M" conceal contained + syntax match MAPMU contained "_" conceal cchar=_ contained + syntax match SPC contained " " conceal contained + syntax match hsNiceOperator "`mapM_`" contains=MAPMQ,MAPMmQ,MAPMa,MAPMp,MAPMM,MAPMU contained + syntax match hsNiceOperator "`mapM`" contains=MAPMQ,MAPMmQ,MAPMa,MAPMp,MAPMM contained + syntax match hsNiceOperator "\\s*" contains=MAPMm,MAPMa,MAPMp,MAPMM,SPC contained + syntax match hsNiceOperator "\\s*" contains=MAPMm,MAPMa,MAPMp,MAPMM,MAPMU,SPC contained + + syntax match FORMQ contained "`" conceal contained + syntax match FORMfQ contained "f" conceal cchar=↱ contained + syntax match FORMf contained "f" conceal cchar=↰ contained + syntax match FORMo contained "o" conceal contained + syntax match FORMr contained "r" conceal contained + syntax match FORMM contained "M" conceal contained + syntax match FORMU contained "_" conceal cchar=_ contained + + syntax match hsNiceOperator "`forM`" contains=FORMQ,FORMfQ,FORMo,FORMr,FORMM contained + syntax match hsNiceOperator "`forM_`" contains=FORMQ,FORMfQ,FORMo,FORMr,FORMM,FORMU contained + + syntax match hsNiceOperator "\\s*" contains=FORMf,FORMo,FORMr,FORMM,SPC contained + syntax match hsNiceOperator "\\s*" contains=FORMf,FORMo,FORMr,FORMM,FORMU,SPC contained +endif + +" 'w' option to disable 'where' concealing with "due to"/∵ symbol. +if !Cf('w') + " ∵ means "because/since/due to." With quite a stretch this can be + " used for 'where'. We preserve spacing, otherwise it breaks indenting + " in a major way. + syntax match WS contained "w" conceal cchar=∵ contained + syntax match HS contained "h" conceal cchar= contained + syntax match ES contained "e" conceal cchar= contained + syntax match RS contained "r" conceal cchar= contained + syntax match hsNiceOperator "\" contains=WS,HS,ES,RS,ES contained +endif + +" '-' option to disable subtract/(-) concealing with ⊟. +if !Cf('-') + " Minus is a special syntax construct in Haskell. We use squared minus to + " tell the syntax from the binary function. + syntax match hsNiceOperator "(-)" conceal cchar=⊟ contained + syntax match hsNiceOperator "`subtract`" conceal cchar=⊟ contained +endif + +" 'I' option to enable alternative ':+' concealing with with ⨢. +if Cf('I') + " With some fonts might look better than ⅈ. + syntax match hsNiceOperator ":+" conceal cchar=⨢ contained +" 'i' option to disable default concealing of ':+' with ⅈ. +elseif !Cf('i') + syntax match hsNiceOperator ":+" conceal cchar=ⅈ contained +endif + +" 'R' option to disable realPart/imagPart concealing with ℜ/ℑ. +if !Cf('R') + syntax match hsNiceOperator "\" conceal cchar=ℜ contained + syntax match hsNiceOperator "\" conceal cchar=ℑ contained +endif + +" 'T' option to enable True/False constants concealing with bold 𝐓/𝐅. +if Cf('T') + syntax match hsNiceSpecial "\" conceal cchar=𝐓 contained + syntax match hsNiceSpecial "\" conceal cchar=𝐅 contained +" 't' option to disable True/False constants concealing with italic 𝑇/𝐹. +elseif !Cf('t') + syntax match hsNiceSpecial "\" conceal cchar=𝑇 contained + syntax match hsNiceSpecial "\" conceal cchar=𝐹 contained +endif + +" 'B' option to disable Bool type to 𝔹 concealing +if !Cf('B') + " Not an official notation ttbomk. But at least + " http://www.haskell.org/haskellwiki/Unicode-symbols mentions it. + syntax match hsNiceOperator "\" conceal cchar=𝔹 contained +endif + +" 'Q' option to disable Rational type to ℚ concealing. +if !Cf('Q') + syntax match hsNiceOperator "\" conceal cchar=ℚ +endif + +" 'Z' option to disable Integer type to ℤ concealing. +if !Cf('Z') + syntax match hsNiceOperator "\" conceal cchar=ℤ contained +endif + +" 'N' option to disable Natural, Nat types to ℕ concealing. +if !Cf('N') + syntax match hsNiceOperator "\" conceal cchar=ℕ contained + syntax match hsNiceOperator "\" conceal cchar=ℕ contained +endif + +" '𝔻' option to disable Double type to 𝔻 concealing +if !Cf('𝔻') + syntax match hsNiceOperator "\" conceal cchar=𝔻 contained +endif + +" '1' option to disable numeric superscripts concealing, e.g. x². +if !Cf('1') + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)0\ze\_W" conceal cchar=⁰ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)1\ze\_W" conceal cchar=¹ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)2\ze\_W" conceal cchar=² contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)3\ze\_W" conceal cchar=³ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)4\ze\_W" conceal cchar=⁴ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)5\ze\_W" conceal cchar=⁵ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)6\ze\_W" conceal cchar=⁶ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)7\ze\_W" conceal cchar=⁷ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)8\ze\_W" conceal cchar=⁸ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)9\ze\_W" conceal cchar=⁹ contained +endif + +" 'a' option to disable alphabet superscripts concealing, e.g. xⁿ. +if !Cf('a') + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)a\ze\_W" conceal cchar=ᵃ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)b\ze\_W" conceal cchar=ᵇ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)c\ze\_W" conceal cchar=ᶜ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)d\ze\_W" conceal cchar=ᵈ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)e\ze\_W" conceal cchar=ᵉ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)f\ze\_W" conceal cchar=ᶠ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)g\ze\_W" conceal cchar=ᵍ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)h\ze\_W" conceal cchar=ʰ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)i\ze\_W" conceal cchar=ⁱ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)j\ze\_W" conceal cchar=ʲ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)k\ze\_W" conceal cchar=ᵏ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)l\ze\_W" conceal cchar=ˡ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)m\ze\_W" conceal cchar=ᵐ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)n\ze\_W" conceal cchar=ⁿ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)o\ze\_W" conceal cchar=ᵒ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)p\ze\_W" conceal cchar=ᵖ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)r\ze\_W" conceal cchar=ʳ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)s\ze\_W" conceal cchar=ˢ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)t\ze\_W" conceal cchar=ᵗ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)u\ze\_W" conceal cchar=ᵘ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)v\ze\_W" conceal cchar=ᵛ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)w\ze\_W" conceal cchar=ʷ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)x\ze\_W" conceal cchar=ˣ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)y\ze\_W" conceal cchar=ʸ contained + syntax match hsNiceOperator "\(\*\*\|\^\|\^\^\)z\ze\_W" conceal cchar=ᶻ contained +endif + +" Not really Haskell, but quite handy for writing proofs in pseudo-code. +if Cf('∴') + syntax match hsNiceOperator "\" conceal cchar=∴ contained + syntax match hsNiceOperator "\" conceal cchar=∃ contained + syntax match hsNiceOperator "\" conceal cchar=∄ contained + syntax match hsNiceOperator ":=" conceal cchar=≝ contained +endif + +" TODO: +" See Basic Syntax Extensions - School of Haskell | FP Complete +" intersection = (∩) +" +" From the Data.IntMap.Strict.Unicode +" notMember = (∉) = flip (∌) +" member = (∈) = flip (∋) +" isProperSubsetOf = (⊂) = flip (⊃) +" +" From Data.Sequence.Unicode +" (<|) = (⊲ ) +" (|>) = (⊳ ) +" (><) = (⋈ ) diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.hs b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.hs new file mode 100644 index 0000000..932b643 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE ExistentialQuantification, RankNTypes #-} +import Control.Applicative +import Data.Monoid +import Control.Monad.ST.Lazy +import Control.Monad +import Numeric +import Data.Complex +import Data.List + +factorial :: Integer -> Integer +factorial n = product as + where as = [n, n-1..1] + +integral :: (Num a, Enum a) => (a -> a) -> a -> (a,a) -> a +integral f dx (a,b) = sum ((\x -> f(x)*dx) <$> ab) + where ab = [a,a+dx..b-dx] + +isValid :: Integer -> Bool -> Bool -> Bool +isValid a b c = (a >= 0 && a <= 10) || (b && not c) + +rs :: forall a. (forall s. ST s a) -> a +rs = runST + +main :: IO () +main = do + let tau = 2*pi + putSL $ showF 2 $ integral sin 0.001 (pi,tau) + print $ unsafe [pi,tau] + print $ factorial <$> [1..13`div`2] + print $ texNum . showF 2 <$> (mag <$> [1,2] <*> [3,4]) + print $ Just True >>= (\x -> return $ x `elem` [True, False, False]) + >>= (\x -> if x /= True + then Nothing + else return True) + >>= (\x -> return $ isValid 1 True x) + print $ [1,2] `union` [3,4] == [-9,-8..4] `intersect` [1,2..9] + print $ (++"il") <$> (Just "fa" >> guard False >> return undefined) + print $ realPart(4:+2) == imagPart(2:+4) + print $ liftM3 (\x y z -> x+y+z) [1] [2] [39] + putSL $ "Hask" <> "ell" + where + mag a b = sqrt(a^2 + b^2) + showF n f = showFFloat (Just n) f empty + unsafe xs = (xs!!0,xs!!1) + texNum num = "$\\num{" ++ num ++ "}$" + putSL = putStrLn diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.lhs b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.lhs new file mode 100644 index 0000000..ca75204 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/demo.lhs @@ -0,0 +1,90 @@ +{-# LANGUAGE ExistentialQuantification, RankNTypes #-} +import Control.Applicative +>> import Control.Applicative +import Data.Monoid +>> import Data.Monoid +import Control.Monad.ST.Lazy +>> import Control.Monad.ST.Lazy +import Control.Monad +>> import Control.Monad +import Numeric +>> import Numeric +import Data.Complex +>> import Data.Complex +import Data.List +>> import Data.List + +factorial :: Integer -> Integer +factorial n = product as + where as = [n, n-1..1] + +>> factorial :: Integer -> Integer +>> factorial n = product as +>> where as = [n, n-1..1] + +integral :: (Num a, Enum a) => (a -> a) -> a -> (a,a) -> a +integral f dx (a,b) = sum ((\x -> f(x)*dx) <$> ab) + where ab = [a,a+dx..b-dx] + +>> integral :: (Num a, Enum a) => (a -> a) -> a -> (a,a) -> a +>> integral f dx (a,b) = sum ((\x -> f(x)*dx) <$> ab) +>> where ab = [a,a+dx..b-dx] + +isValid :: Integer -> Bool -> Bool -> Bool +isValid a b c = (a >= 0 && a <= 10) || (b && not c) + +>> isValid :: Integer -> Bool -> Bool -> Bool +>> isValid a b c = (a >= 0 && a <= 10) || (b && not c) + +rs :: forall a. (forall s. ST s a) -> a +rs = runST +>> rs :: forall a. (forall s. ST s a) -> a +>> rs = runST + +main :: IO () +main = do + let tau = 2*pi + putSL $ showF 2 $ integral sin 0.001 (pi,tau) + print $ unsafe [pi,tau] + print $ factorial <$> [1..13`div`2] + print $ texNum . showF 2 <$> (mag <$> [1,2] <*> [3,4]) + print $ Just True >>= (\x -> return $ x `elem` [True, False, False]) + >>= (\x -> if x /= True + then Nothing + else return True) + >>= (\x -> return $ isValid 1 True x) + print $ [1,2] `union` [3,4] == [-9,-8..4] `intersect` [1,2..9] + print $ (++"il") <$> (Just "fa" >> guard False >> return undefined) + print $ realPart(4:+2) == imagPart(2:+4) + print $ liftM3 (\x y z -> x+y+z) [1] [2] [39] + putSL $ "Hask" <> "ell" + where + mag a b = sqrt(a^2 + b^2) + showF n f = showFFloat (Just n) f empty + unsafe xs = (xs!!0,xs!!1) + texNum num = "$\\num{" ++ num ++ "}$" + putSL = putStrLn + +>> main :: IO () +>> main = do +>> let tau = 2*pi +>> putSL $ showF 2 $ integral sin 0.001 (pi,tau) +>> print $ unsafe [pi,tau] +>> print $ factorial <$> [1..13`div`2] +>> print $ texNum . showF 2 <$> (mag <$> [1,2] <*> [3,4]) +>> print $ Just True >>= (\x -> return $ x `elem` [True, False, False]) +>> >>= (\x -> if x /= True +>> then Nothing +>> else return True) +>> >>= (\x -> return $ isValid 1 True x) +>> print $ [1,2] `union` [3,4] == [-9,-8..4] `intersect` [1,2..9] +>> print $ (++"il") <$> (Just "fa" >> guard False >> return undefined) +>> print $ realPart(4:+2) == imagPart(2:+4) +>> print $ liftM3 (\x y z -> x+y+z) [1] [2] [39] +>> putSL $ "Hask" <> "ell" +>> where +>> mag a b = sqrt(a^2 + b^2) +>> showF n f = showFFloat (Just n) f empty +>> unsafe xs = (xs!!0,xs!!1) +>> texNum num = "$\\num{" ++ num ++ "}$" +>> putSL = putStrLn diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/gendemo.sh b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/gendemo.sh new file mode 100755 index 0000000..38fb02b --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/gendemo.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +rm s{l,r}.png +convert -crop 490x685+0+35 l.png sl.png +convert -crop 640x685+0+35 r.png sr.png +convert sl.png sr.png +append demo.png diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pack.sh b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pack.sh new file mode 100755 index 0000000..2c048bb --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pack.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -e + +tag=test + +docker build -t "$tag" - << EOF_DOCKERFILE +from debian:buster + +run apt-get update + +run apt-get install -y locales +run echo en_US.UTF-8 UTF-8 > /etc/locale.gen +run dpkg-reconfigure locales --frontend=noninteractive + +run apt-get install -y vim +run apt-get install -y git +run apt-get install -y screen + +run useradd -m -s /bin/bash user +env SHELL /bin/bash +env LANG en_US.UTF-8 +env LC_CTYPE en_US.UTF8 + +user user +run mkdir -p ~/.vim/pack/vim-haskellConcealPlus/start && \ + cd ~/.vim/pack/vim-haskellConcealPlus/start && \ + git clone https://github.com/enomsg/vim-haskellConcealPlus +run echo "syn on\nsetlocal conceallevel=2\nset concealcursor=nciv" > ~/.vimrc +# Run in screen as it handles terminal capabilities better than most of the raw +# terminals. +cmd screen vim ~/.vim/pack/vim-haskellConcealPlus/start/vim-haskellConcealPlus/demo.hs +#cmd bash +EOF_DOCKERFILE + +docker run \ + -e TERM="$TERM" \ + -w /home/user \ + -it "$tag" "$@" diff --git a/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pathogen.sh b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pathogen.sh new file mode 100755 index 0000000..385a118 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-haskellConcealPlus/tests/test-with-pathogen.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -e + +tag=test + +docker build -t "$tag" - << EOF_DOCKERFILE +from debian:buster + +run apt-get update + +run apt-get install -y locales +run echo en_US.UTF-8 UTF-8 > /etc/locale.gen +run dpkg-reconfigure locales --frontend=noninteractive + +run apt-get install -y vim +run apt-get install -y vim-pathogen +run apt-get install -y git +run apt-get install -y screen + +run useradd -m -s /bin/bash user +env SHELL /bin/bash +env LANG en_US.UTF-8 +env LC_CTYPE en_US.UTF8 + +user user +run mkdir -p ~/.vim/bundle && cd ~/.vim/bundle && git clone https://github.com/enomsg/vim-haskellConcealPlus +run echo "execute pathogen#infect()\nsyn on\nsetlocal conceallevel=2\nset concealcursor=nciv" > ~/.vimrc +# Run in screen as it handles terminal capabilities better than most of the raw +# terminals. +cmd screen vim ~/.vim/bundle/vim-haskellConcealPlus/demo.hs +#cmd bash +EOF_DOCKERFILE + +docker run \ + -e TERM="$TERM" \ + -w /home/user \ + -it "$tag" "$@" diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/LICENSE b/etc/soft/nvim/+plugins/vim-pandoc-syntax/LICENSE new file mode 100644 index 0000000..a543e10 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2019 vim-pandoc-syntax contributors + +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. diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/README.md b/etc/soft/nvim/+plugins/vim-pandoc-syntax/README.md new file mode 100644 index 0000000..d90ccd9 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/README.md @@ -0,0 +1,68 @@ +# vim-pandoc-syntax + +[![Vint](https://github.com/vim-pandoc/vim-pandoc-syntax/workflows/Vint/badge.svg)](https://github.com/vim-pandoc/vim-pandoc-syntax/actions?workflow=Vint) + +Standalone pandoc syntax module, to be used alongside +[vim-pandoc](http://github.com/vim-pandoc/vim-pandoc). + +Forked from the version provided by `fmoralesc/vim-pantondoc`, in turn taken +from `vim-pandoc/vim-pandoc`. + +## Requirements + +* A vim version with `+conceal` +* [vim-pandoc](http://github.com/vim-pandoc/vim-pandoc), to set the + `pandoc` filetype (otherwise you'll have to set it up yourself). + +## Installation + +The repository follows the usual bundle structure, so it's easy to install it +using [pathogen](https://github.com/tpope/vim-pathogen), +[Vundle](https://github.com/gmarik/vundle) or NeoBundle. + +For Vundle users, it should be enough to add + + Plugin 'vim-pandoc/vim-pandoc-syntax' + +to `.vimrc`, and then run `:PluginInstall`. + +For those who need it, a tarball is available from +[here](https://github.com/vim-pandoc/vim-pandoc-syntax/archive/master.zip). + +### Standalone + +If you want to use `vim-pandoc-syntax` without vim-pandoc, you'll need to tell +Vim to load it for certain files. Just add something like this to your vimrc: + +~~~ vim +augroup pandoc_syntax + au! BufNewFile,BufFilePre,BufRead *.md set filetype=markdown.pandoc +augroup END +~~~ + +For vimwiki users, use + +~~~ vim +augroup pandoc_syntax + autocmd! FileType vimwiki set syntax=markdown.pandoc +augroup END +~~~ + + +## Features + +* Supports most (if not all) pandoc's markdown features, including tables, + delimited codeblocks, references, etc. +* Can handle multiple embedded languages (LaTeX, YAML headers, many languages + in delimited codeblocks). Some commands are provided to help with this (see + `:help pandoc-syntax-commands`) +* Pretty display using `conceal` (optional). +* Configurable (see `:help pandoc-syntax-configuration` for an overview of the + options). + +## Screenshots + +![img1](http://i.imgur.com/UKXbG2V.png) +![img2](http://i.imgur.com/z8FpxRP.png) +![img3](http://i.imgur.com/ziNjQiE.png) +![img4](http://i.imgur.com/UKoOxzP.png) diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/autoload/pandoc/syntax/color.vim b/etc/soft/nvim/+plugins/vim-pandoc-syntax/autoload/pandoc/syntax/color.vim new file mode 100644 index 0000000..75a5a67 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/autoload/pandoc/syntax/color.vim @@ -0,0 +1,134 @@ +" vim: set fdm=marker : +" +" file: autoload/pandoc/syntax/color.vim +" author: Felipe Morales +" version: 0.1 +" description: functions to manipulate color + +" Conversion: {{{1 +" +" color conversion algorithms adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html + +function! s:RGB2HSV(r, g, b) abort "{{{2 + let rp = a:r/255.0 + let gp = a:g/255.0 + let bp = a:b/255.0 + let cmax = max([float2nr(rp*100.0), float2nr(gp*100.0), float2nr(bp*100.0)])/100.0 + let cmin = min([float2nr(rp*100.0), float2nr(gp*100.0), float2nr(bp*100.0)])/100.0 + let delta = cmax - cmin + let v = cmax + if cmax == 0.0 + let s = 0.0 + let h = -1.0 + else + let s = delta/cmax + if rp == cmax + let h = (gp - bp)/ delta + elseif gp == cmax + let h = (2.0 + (bp - rp))/delta + else + let h = (4.0 + (rp - gp))/delta + endif + let h = h * 60.0 + if h < 0.0 + let h = h+ 360.0 + endif + endif + return [h, s, v] +endfunction + +function! s:HSV2RGB(h, s, v) abort "{{{2 + if a:s == 0 "achromatic + return [a:v, a:v, a:v] + endif + let h = a:h/60 " sector 0 to 5 + let i = floor(h) + let f = h - i + let p = a:v * ( 1 - a:s) + let q = a:v * ( 1 - a:s * f) + let t = a:v * ( 1 - a:s * (1 - f)) + + if i == 0 + let r = a:v + let g = t + let b = p + elseif i == 1 + let r = q + let g = a:v + let b = p + elseif i == 2 + let r = p + let g = a:v + let b = t + elseif i == 3 + let r = p + let g = q + let b = a:v + elseif i == 4 + let r = t + let g = p + let b = a:v + else + let r = a:v + let g = p + let b = q + endif + return [float2nr(r*255), float2nr(g*255), float2nr(b*255)] +endfunction + +function! s:Hex2RGB(hex) abort "{{{2 + let hex = split(a:hex, '\zs') + let h_r = '0x'.join(hex[:1], '') + let h_g = '0x'.join(hex[2:3], '') + let h_b = '0x'.join(hex[4:6], '') + return map([h_r, h_g, h_b], 'eval(v:val)') +endfunction + +function! s:RGB2Hex(r, g, b) abort "{{{2 + let h_r = printf('%02x', a:r) + let h_g = printf('%02x', a:g) + let h_b = printf('%02x', a:b) + return join([h_r, h_g, h_b], '') +endfunction + +" Instrospection: {{{1 + +function! pandoc#syntax#color#Instrospect(group) abort + redir => hi_output + exe 'silent hi '. a:group + redir END + let hi_output = split(hi_output, '\n')[0] + let parts = split(hi_output, '\s\+')[2:] + if parts[0] ==# 'links' + let info = Hi_Info(parts[2]) + let info['linked_to'] = parts[2] + else + let info = {} + for i in parts + let data = split(i, '=') + let info[data[0]] = data[1] + endfor + endif + return info +endfunction + +" Palette: {{{1 + +function! pandoc#syntax#color#SaturationPalette(hex, partitions) abort + let rgb = s:Hex2RGB(a:hex) + let hsv = s:RGB2HSV(rgb[0], rgb[1], rgb[2]) + let hsv_palette = [] + for i in range(1, a:partitions) + let s = 1.0/a:partitions * i " linear + call add(hsv_palette, [hsv[0], s, hsv[2]]) + endfor + let rgb_palette = [] + for hsv in hsv_palette + call add(rgb_palette, s:HSV2RGB(hsv[0], hsv[1], hsv[2])) + endfor + let hex_palette = [] + for rgb in rgb_palette + call add(hex_palette, s:RGB2Hex(rgb[0], rgb[1], rgb[2])) + endfor + return hex_palette +endfunction diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/doc/pandoc-syntax.txt b/etc/soft/nvim/+plugins/vim-pandoc-syntax/doc/pandoc-syntax.txt new file mode 100644 index 0000000..fff0b10 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/doc/pandoc-syntax.txt @@ -0,0 +1,137 @@ +*vim-pandoc-syntax* +*pandoc-syntax* + +OVERVIEW + +vim-pandoc-syntax is a standalone syntax file for highlighting pandoc flavored +markdown documents, to be used alongside vim-pandoc (see |pandoc|). It is +based on the version once provided by vim-pandoc-legacy, which it obsoletes. + +The project resides at http://www.github.com/vim-pandoc/vim-pandoc-syntax. You +are welcome to help, suggest ideas, report bugs or contribute code. + + +CONFIGURATION *vim-pandoc-syntax-configuration* + ++ *g:pandoc#syntax#conceal#use* + Use |conceal| for pretty highlighting. Default is 1 for vim version > 7.3 + ++ *g:pandoc#syntax#conceal#blacklist* A list of rules |conceal| should not be + used with. Works as a blacklist, and defaults to [] (use conceal everywhere). + + This is a list of the rules wich can be used here: + + - titleblock + - image + - block + - subscript + - superscript + - strikeout + - atx + - codeblock_start + - codeblock_delim + - footnote + - definition + - list + - newline + - dashes + - ellipses + - quotes + - inlinecode + + To review what are the rules for, look for the call to |s:WithConceal| in + syntax/pandoc.vim that takes the corresponding rulename as first argument. + ++ *g:pandoc#syntax#conceal#cchar_overrides* + A dictionary of what characters should be used in conceal rules. These + override the defaults (see those in |s:cchars|). For example, if you prefer + to mark footnotes with the `*` symbol: + + let g:pandoc#syntax#conceal#cchar_overrides = {"footnote" : "*"} + ++ *g:pandoc#syntax#conceal#urls* + Conceal the urls in links. + ++ *g:pandoc#syntax#codeblocks#ignore* + Prevent highlighting specific codeblock types so that they remain Normal. + Codeblock types include 'definition' for codeblocks inside definition blocks + and 'delimited' for delimited codeblocks. Default = [] + ++ *g:pandoc#syntax#codeblocks#embeds#use* + Use embedded highlighting for delimited codeblocks where a language is + specified. Default = 1 + ++ *g:pandoc#syntax#codeblocks#embeds#langs* + For what languages and using what syntax files to highlight embeds. This is + a list of language names. When the language pandoc and vim use don't match, + you can use the "PANDOC=VIM" syntax. For example: + + let g:pandoc#syntax#codeblocks#embeds#langs = ["ruby", + "literatehaskell=lhaskell", "bash=sh"] + ++ *g:pandoc#syntax#style#emphases* + Use italics and strong in emphases. Default = 1 + 0 will add "block" to |g:pandoc#syntax#conceal#blacklist|, because otherwise + you couldn't tell where the styles are applied. + ++ *g:pandoc#syntax#style#underline_special* + Underline subscript, superscript and strikeout text styles. Default = 1 + ++ *g:pandoc#syntax#style#use_definition_lists* + Detect and highlight definition lists. Disabling this can improve + performance. Default = 1 (i.e., enabled by default) + +COMMANDS *pandoc-syntax-commands* + ++ *:PandocHighlight* LANG + + Enable embedded highlighting for language LANG in codeblocks. Uses the + syntax for items in |g:pandoc#syntax#codeblocks#embeds#langs|. + ++ *:PandocUnhighlight* LANG + + Disable embedded highlighting for language LANG in codeblocks. + +FUNCTIONS *pandoc-syntax-functions* + ++ *EnableEmbedsForCodeblocksWithLang(langname)* + As |:PandocHighlight|. + ++ *DisableEmbedsForCodeblocksWithLang(langname)* + As |:PandocUnhighlight|. + ++ *s:WithConceal(RULE_GROUP,RULE,CONCEAL_RULE)* + Executes a |:syntax| command RULE, which could incorporate conceal rules + (CONCEAL_RULE) if conceals are enabled. The rule gets named RULE_GROUP, + as used in |g:pandoc#syntax#conceal#blacklist|. + + For example, if conceals are enabled + + call s:WithConceal("atx", 'syn match AtxStart /#/ contained + containedin=pandocAtxHeader', 'conceal cchar='.s:cchars["atx"]) + + will execute + + syn match AtxStart /#/ contained containedin=pandocAtxHeader conceal cchar=§ + + and + + syn match AtxStart /#/ contained containedin=pandocAtxHeader + + otherwise. + +TODO + +This is a list of know stuff missing from the syntax file. + + 1. Highlight code within verbatim text when a language is specified in the attrib, + like in + + hey, `print 1 + 3`.{python} is no longer valid in python 3. + + +MISC + +The plugin sets a global variable, *g:vim_pandoc_syntax_exists* to indicate its +existence. This is currently used by vim-pandoc to switch between syntax +assisted and basic folding rules. diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/plugin/pandoc-syntax-check.vim b/etc/soft/nvim/+plugins/vim-pandoc-syntax/plugin/pandoc-syntax-check.vim new file mode 100644 index 0000000..8b15508 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/plugin/pandoc-syntax-check.vim @@ -0,0 +1 @@ +let g:vim_pandoc_syntax_exists = 1 diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/beamer.vim b/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/beamer.vim new file mode 100644 index 0000000..e2c2694 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/beamer.vim @@ -0,0 +1,5 @@ +" vim: set fdm=marker: + +" Pandoc: {{{1 +" load vim-pandoc-syntax {{{2 +runtime syntax/pandoc.vim diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/pandoc.vim b/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/pandoc.vim new file mode 100644 index 0000000..658ad69 --- /dev/null +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/syntax/pandoc.vim @@ -0,0 +1,709 @@ +scriptencoding utf-8 +" vim: set fdm=marker foldlevel=0: +" +" Vim syntax file +" +" Language: Pandoc (superset of Markdown) +" Maintainer: Felipe Morales +" Maintainer: Caleb Maclennan +" Contributor: David Sanson +" Contributor: Jorge Israel Peña +" OriginalAuthor: Jeremy Schultz +" Version: 5.0 + +" Configuration: {{{1 +" +" use conceal? {{{2 +if !exists('g:pandoc#syntax#conceal#use') + if v:version < 703 + let g:pandoc#syntax#conceal#use = 0 + else + let g:pandoc#syntax#conceal#use = 1 + endif +else + " exists, but we cannot use it, disable anyway + if v:version < 703 + let g:pandoc#syntax#conceal#use = 0 + endif +endif +"}}}2 + +" what groups not to use conceal in. works as a blacklist {{{2 +if !exists('g:pandoc#syntax#conceal#blacklist') + let g:pandoc#syntax#conceal#blacklist = [] +endif +" }}}2 + +" cchars used in conceal rules {{{2 +" utf-8 defaults (preferred) +if &encoding ==# 'utf-8' + let s:cchars = { + \'newline': '↵', + \'image': '▨', + \'super': 'ⁿ', + \'sub': 'ₙ', + \'strike': 'x̶', + \'atx': '§', + \'codelang': 'λ', + \'codeend': '—', + \'abbrev': '→', + \'footnote': '†', + \'definition': ' ', + \'li': '•', + \'html_c_s': '‹', + \'html_c_e': '›', + \'quote_s': '“', + \'quote_e': '”'} +else + " ascii defaults + let s:cchars = { + \'newline': ' ', + \'image': 'i', + \'super': '^', + \'sub': '_', + \'strike': '~', + \'atx': '#', + \'codelang': 'l', + \'codeend': '-', + \'abbrev': 'a', + \'footnote': 'f', + \'definition': ' ', + \'li': '*', + \'html_c_s': '+', + \'html_c_e': '+'} +endif +" }}}2 + +" if the user has a dictionary with replacements for the default cchars, use those {{{2 +if exists('g:pandoc#syntax#conceal#cchar_overrides') + let s:cchars = extend(s:cchars, g:pandoc#syntax#conceal#cchar_overrides) +endif +" }}}2 + +"should the urls in links be concealed? {{{2 +if !exists('g:pandoc#syntax#conceal#urls') + let g:pandoc#syntax#conceal#urls = 0 +endif +" should backslashes in escapes be concealed? {{{2 +if !exists('g:pandoc#syntax#conceal#backslash') + let g:pandoc#syntax#conceal#backslash = 0 +endif +" }}}2 + +" leave specified codeblocks as Normal (i.e. 'unhighlighted') {{{2 +if !exists('g:pandoc#syntax#codeblocks#ignore') + let g:pandoc#syntax#codeblocks#ignore = [] +endif +" }}}2 + +" use embedded highlighting for delimited codeblocks where a language is specifed. {{{2 +if !exists('g:pandoc#syntax#codeblocks#embeds#use') + let g:pandoc#syntax#codeblocks#embeds#use = 1 +endif +" }}}2 + +" for what languages and using what vim syntax files highlight those embeds. {{{2 +" defaults to None. +if !exists('g:pandoc#syntax#codeblocks#embeds#langs') + let g:pandoc#syntax#codeblocks#embeds#langs = [] +endif +" }}}2 + +" use italics ? {{{2 +if !exists('g:pandoc#syntax#style#emphases') + let g:pandoc#syntax#style#emphases = 1 +endif +" if 0, we don't conceal the emphasis marks, otherwise there wouldn't be a way +" to tell where the styles apply. +if g:pandoc#syntax#style#emphases == 0 + call add(g:pandoc#syntax#conceal#blacklist, 'block') +endif +" }}}2 + +" underline subscript, superscript and strikeout? {{{2 +if !exists('g:pandoc#syntax#style#underline_special') + let g:pandoc#syntax#style#underline_special = 1 +endif +" }}}2 + +" protect code blocks? {{{2 +if !exists('g:pandoc#syntax#protect#codeblocks') + let g:pandoc#syntax#protect#codeblocks = 1 +endif +" }}}2 + +" use color column? {{{2 +if !exists('g:pandoc#syntax#colorcolumn') + let g:pandoc#syntax#colorcolumn = 0 +endif +" }}}2 + +" highlight new lines? {{{2 +if !exists('g:pandoc#syntax#newlines') + let g:pandoc#syntax#newlines = 1 +endif +" }}} + +" detect roman-numeral list items? {{{2 +if !exists('g:pandoc#syntax#roman_lists') + let g:pandoc#syntax#roman_lists = 0 +endif +" }}}2 + +" disable syntax highlighting for definition lists? (better performances) {{{2 +if !exists('g:pandoc#syntax#use_definition_lists') + let g:pandoc#syntax#use_definition_lists = 1 +endif +" }}}2 + +" }}}1 + +" Functions: {{{1 +" EnableEmbedsforCodeblocksWithLang {{{2 +function! EnableEmbedsforCodeblocksWithLang(entry) + " prevent embedded language syntaxes from changing 'foldmethod' + if has('folding') + let s:foldmethod = &l:foldmethod + let s:foldtext = &l:foldtext + endif + + try + let s:langname = matchstr(a:entry, '^[^=]*') + let s:langsyntaxfile = matchstr(a:entry, '[^=]*$') + unlet! b:current_syntax + exe 'syn include @'.toupper(s:langname).' syntax/'.s:langsyntaxfile.'.vim' + " We might have just turned off spellchecking by including the file, + " so we turn it back on here. + exe 'syntax spell toplevel' + exe 'syn region pandocDelimitedCodeBlock_' . s:langname . ' start=/\(\_^\( \+\|\t\)\=\(`\{3,}`*\|\~\{3,}\~*\)\s*\%({[^.]*\.\)\=' . s:langname . '\>.*\n\)\@<=\_^/' . + \' end=/\_$\n\(\( \+\|\t\)\=\(`\{3,}`*\|\~\{3,}\~*\)\_$\n\_$\)\@=/ contained containedin=pandocDelimitedCodeBlock' . + \' contains=@' . toupper(s:langname) + exe 'syn region pandocDelimitedCodeBlockinBlockQuote_' . s:langname . ' start=/>\s\(`\{3,}`*\|\~\{3,}\~*\)\s*\%({[^.]*\.\)\=' . s:langname . '\>/' . + \ ' end=/\(`\{3,}`*\|\~\{3,}\~*\)/ contained containedin=pandocDelimitedCodeBlock' . + \' contains=@' . toupper(s:langname) . + \',pandocDelimitedCodeBlockStart,pandocDelimitedCodeBlockEnd,pandodDelimitedCodeblockLang,pandocBlockQuoteinDelimitedCodeBlock' + catch /E484/ + echo "No syntax file found for '" . s:langsyntaxfile . "'" + endtry + + if exists('s:foldmethod') && s:foldmethod !=# &l:foldmethod + let &l:foldmethod = s:foldmethod + endif + if exists('s:foldtext') && s:foldtext !=# &l:foldtext + let &l:foldtext = s:foldtext + endif +endfunction +" }}}2 + +" DisableEmbedsforCodeblocksWithLang {{{2 +function! DisableEmbedsforCodeblocksWithLang(langname) + try + exe 'syn clear pandocDelimitedCodeBlock_'.a:langname + exe 'syn clear pandocDelimitedCodeBlockinBlockQuote_'.a:langname + catch /E28/ + echo "No existing highlight definitions found for '" . a:langname . "'" + endtry +endfunction +" }}}2 + +" WithConceal {{{2 +function! s:WithConceal(rule_group, rule, conceal_rule) + let l:rule_tail = '' + if g:pandoc#syntax#conceal#use != 0 + if index(g:pandoc#syntax#conceal#blacklist, a:rule_group) == -1 + let l:rule_tail = ' ' . a:conceal_rule + endif + endif + execute a:rule . l:rule_tail +endfunction +" }}}2 + +" }}}1 + +" Commands: {{{1 +command! -buffer -nargs=1 -complete=syntax PandocHighlight call EnableEmbedsforCodeblocksWithLang() +command! -buffer -nargs=1 -complete=syntax PandocUnhighlight call DisableEmbedsforCodeblocksWithLang() +" }}}1 + +" BASE: +syntax clear +syntax spell toplevel +" apply extra settings: {{{1 +if g:pandoc#syntax#colorcolumn == 1 + exe 'setlocal colorcolumn='.string(&textwidth+5) +elseif g:pandoc#syntax#colorcolumn == 2 + exe 'setlocal colorcolumn='.join(range(&textwidth+5, 2*&columns), ',') +endif +if g:pandoc#syntax#conceal#use != 0 + setlocal conceallevel=2 +endif +" }}}1 + +" Syntax Rules: {{{1 + +" Embeds: {{{2 + +" prevent embedded language syntaxes from changing 'foldmethod' +if has('folding') + let s:foldmethod = &l:foldmethod +endif + +" HTML: {{{3 +" Set embedded HTML highlighting +syn include @HTML syntax/html.vim +syn match pandocHTML /<\/\?\a\_.\{-}>/ contains=@HTML +" Support HTML multi line comments +syn region pandocHTMLComment start=// keepend contains=pandocHTMLCommentStart,pandocHTMLCommentEnd +call s:WithConceal('html_c_s', 'syn match pandocHTMLCommentStart // contained', 'conceal cchar='.s:cchars['html_c_e']) +" }}}3 + +" LaTeX: {{{3 +" Set embedded LaTex (pandoc extension) highlighting +" Unset current_syntax so the 2nd include will work +unlet b:current_syntax +syn include @LATEX syntax/tex.vim +syn region pandocLaTeXInlineMath start=/\v\\@.*\n\(.*\n\@1/ contained containedin=pandocEmphasis,pandocStrong,pandocPCite,pandocSuperscript,pandocSubscript,pandocStrikeout,pandocUListItem,pandocNoFormatted +" }}}2 + +" Code Blocks: {{{2 +if g:pandoc#syntax#protect#codeblocks == 1 + syn match pandocCodeblock /\([ ]\{4}\|\t\).*$/ +endif +syn region pandocCodeBlockInsideIndent start=/\(\(\d\|\a\|*\).*\n\)\@/ contains=NONE +" }}}3 + +" }}}2 + +" Citations: {{{2 +" parenthetical citations +syn match pandocPCite "\^\@~/]*.\{-}\]" contains=pandocEmphasis,pandocStrong,pandocLatex,pandocCiteKey,@Spell,pandocAmpersandEscape display +" in-text citations with location +syn match pandocICite "@[[:alnum:]_][[:digit:][:lower:][:upper:]_:.#$%&\-+?<>~/]*\s\[.\{-1,}\]" contains=pandocCiteKey,@Spell display +" cite keys +syn match pandocCiteKey /\(-\=@[[:alnum:]_][[:digit:][:lower:][:upper:]_:.#$%&\-+?<>~/]*\)/ containedin=pandocPCite,pandocICite contains=@NoSpell display +syn match pandocCiteAnchor /[-@]/ contained containedin=pandocCiteKey display +syn match pandocCiteLocator /[\[\]]/ contained containedin=pandocPCite,pandocICite +" }}}2 + +" Text Styles: {{{2 + +" Emphasis: {{{3 +call s:WithConceal('block', 'syn region pandocEmphasis matchgroup=pandocOperator start=/\\\@1.*\n\|^\s*\n\)\@<=#\{1,6}.*\n/ contains=pandocEmphasis,pandocStrong,pandocNoFormatted,pandocLaTeXInlineMath,pandocEscapedDollar,@Spell,pandocAmpersandEscape,pandocReferenceLabel,pandocReferenceURL display +syn match pandocAtxHeaderMark /\(^#\{1,6}\|\\\@/ contained containedin=pandocGridTableHeader,pandocPipeTableHeader contains=@Spell +" }}}2 + +" Delimited Code Blocks: {{{2 +" this is here because we can override strikeouts and subscripts +syn region pandocDelimitedCodeBlock start=/^\(>\s\)\?\z(\([ ]\+\|\t\)\=\~\{3,}\~*\)/ end=/^\z1\~*/ skipnl contains=pandocDelimitedCodeBlockStart,pandocDelimitedCodeBlockEnd keepend +syn region pandocDelimitedCodeBlock start=/^\(>\s\)\?\z(\([ ]\+\|\t\)\=`\{3,}`*\)/ end=/^\z1`*/ skipnl contains=pandocDelimitedCodeBlockStart,pandocDelimitedCodeBlockEnd keepend +call s:WithConceal('codeblock_start', 'syn match pandocDelimitedCodeBlockStart /\(\(\_^\n\_^\|\%^\)\(>\s\)\?\( \+\|\t\)\=\)\@<=\(\~\{3,}\~*\|`\{3,}`*\)/ contained containedin=pandocDelimitedCodeBlock nextgroup=pandocDelimitedCodeBlockLanguage', 'conceal cchar='.s:cchars['codelang']) +syn match pandocDelimitedCodeBlockLanguage /\(\s\?\)\@<=.\+\(\_$\)\@=/ contained +call s:WithConceal('codeblock_delim', 'syn match pandocDelimitedCodeBlockEnd /\(`\{3,}`*\|\~\{3,}\~*\)\(\_$\n\(>\s\)\?\_$\)\@=/ contained containedin=pandocDelimitedCodeBlock', 'conceal cchar='.s:cchars['codeend']) +syn match pandocBlockQuoteinDelimitedCodeBlock '^>' contained containedin=pandocDelimitedCodeBlock +syn match pandocCodePre /
    .\{-}<\/pre>/ skipnl
    +syn match pandocCodePre /.\{-}<\/code>/ skipnl
    +
    +" enable highlighting for embedded region in codeblocks if there exists a
    +" g:pandoc#syntax#codeblocks#embeds#langs *list*.
    +"
    +" entries in this list are the language code interpreted by pandoc,
    +" if this differs from the name of the vim syntax file, append =vimname
    +" e.g. let g:pandoc#syntax#codeblocks#embeds#langs = ["haskell", "literatehaskell=lhaskell"]
    +"
    +if g:pandoc#syntax#codeblocks#embeds#use != 0
    +    for l in g:pandoc#syntax#codeblocks#embeds#langs
    +      call EnableEmbedsforCodeblocksWithLang(l)
    +    endfor
    +endif
    +" }}}2
    +
    +" Abbreviations: {{{2
    +syn region pandocAbbreviationDefinition start=/^\*\[.\{-}\]:\s*/ end='$' contains=pandocNoFormatted,@Spell,pandocAmpersandEscape
    +call s:WithConceal('abbrev', 'syn match pandocAbbreviationSeparator /:/ contained containedin=pandocAbbreviationDefinition', 'conceal cchar='.s:cchars['abbrev'])
    +syn match pandocAbbreviation /\*\[.\{-}\]/ contained containedin=pandocAbbreviationDefinition
    +call s:WithConceal('abbrev', 'syn match pandocAbbreviationHead /\*\[/ contained containedin=pandocAbbreviation', 'conceal')
    +call s:WithConceal('abbrev', 'syn match pandocAbbreviationTail /\]/ contained containedin=pandocAbbreviation', 'conceal')
    +" }}}2
    +
    +" Footnotes: {{{2
    +" we put these here not to interfere with superscripts.
    +syn match pandocFootnoteID /\[\^[^\]]\+\]/ nextgroup=pandocFootnoteDef
    +
    +"   Inline footnotes
    +syn region pandocFootnoteDef start=/\^\[/ skip=/\[.\{-}]/ end=/\]/ contains=pandocReferenceLabel,pandocReferenceURL,pandocLatex,pandocPCite,pandocCiteKey,pandocStrong,pandocEmphasis,pandocStrongEmphasis,pandocNoFormatted,pandocSuperscript,pandocSubscript,pandocStrikeout,pandocEnDash,pandocEmDash,pandocEllipses,pandocBeginQuote,pandocEndQuote,@Spell,pandocAmpersandEscape skipnl keepend
    +call s:WithConceal('footnote', 'syn match pandocFootnoteDefHead /\^\[/ contained containedin=pandocFootnoteDef', 'conceal cchar='.s:cchars['footnote'])
    +call s:WithConceal('footnote', 'syn match pandocFootnoteDefTail /\]/ contained containedin=pandocFootnoteDef', 'conceal')
    +
    +" regular footnotes
    +syn region pandocFootnoteBlock start=/\[\^.\{-}\]:\s*\n*/ end=/^\n^\s\@!/ contains=pandocReferenceLabel,pandocReferenceURL,pandocLatex,pandocPCite,pandocCiteKey,pandocStrong,pandocEmphasis,pandocNoFormatted,pandocSuperscript,pandocSubscript,pandocStrikeout,pandocEnDash,pandocEmDash,pandocNewLine,pandocStrongEmphasis,pandocEllipses,pandocBeginQuote,pandocEndQuote,pandocLaTeXInlineMath,pandocEscapedDollar,pandocLaTeXCommand,pandocLaTeXMathBlock,pandocLaTeXRegion,pandocAmpersandEscape,@Spell skipnl
    +syn match pandocFootnoteBlockSeparator /:/ contained containedin=pandocFootnoteBlock
    +syn match pandocFootnoteID /\[\^.\{-}\]/ contained containedin=pandocFootnoteBlock
    +call s:WithConceal('footnote', 'syn match pandocFootnoteIDHead /\[\^/ contained containedin=pandocFootnoteID', 'conceal cchar='.s:cchars['footnote'])
    +call s:WithConceal('footnote', 'syn match pandocFootnoteIDTail /\]/ contained containedin=pandocFootnoteID', 'conceal')
    +" }}}2
    +
    +" List Items: {{{2
    +" Unordered lists
    +syn match pandocUListItem /^>\=\s*[*+-]\s\+-\@!.*$/ nextgroup=pandocUListItem,pandocLaTeXMathBlock,pandocLaTeXInlineMath,pandocEscapedDollar,pandocDelimitedCodeBlock,pandocListItemContinuation contains=@Spell,pandocEmphasis,pandocStrong,pandocNoFormatted,pandocStrikeout,pandocSubscript,pandocSuperscript,pandocStrongEmphasis,pandocStrongEmphasis,pandocPCite,pandocICite,pandocCiteKey,pandocReferenceLabel,pandocLaTeXCommand,pandocLaTeXMathBlock,pandocLaTeXInlineMath,pandocEscapedDollar,pandocReferenceURL,pandocAutomaticLink,pandocFootnoteDef,pandocFootnoteBlock,pandocFootnoteID,pandocAmpersandEscape skipempty display
    +call s:WithConceal('list', 'syn match pandocUListItemBullet /^>\=\s*\zs[*+-]/ contained containedin=pandocUListItem', 'conceal cchar='.s:cchars['li'])
    +
    +" Ordered lists
    +syn match pandocListItem /^\s*(\?\(\d\+\|\l\|\#\|@\)[.)].*$/ nextgroup=pandocListItem,pandocLaTeXMathBlock,pandocLaTeXInlineMath,pandocEscapedDollar,pandocDelimitedCodeBlock,pandocListItemContinuation contains=@Spell,pandocEmphasis,pandocStrong,pandocNoFormatted,pandocStrikeout,pandocSubscript,pandocSuperscript,pandocStrongEmphasis,pandocStrongEmphasis,pandocPCite,pandocICite,pandocCiteKey,pandocReferenceLabel,pandocLaTeXCommand,pandocLaTeXMathBlock,pandocLaTeXInlineMath,pandocEscapedDollar,pandocAutomaticLink,pandocFootnoteDef,pandocFootnoteBlock,pandocFootnoteID,pandocAmpersandEscape skipempty display
    +
    +" support for roman numerals up to 'c'
    +if g:pandoc#syntax#roman_lists != 0
    +    syn match pandocListItem /^\s*(\?x\=l\=\(i\{,3}[vx]\=\)\{,3}c\{,3}[.)].*$/ nextgroup=pandocListItem,pandocMathBlock,pandocLaTeXInlineMath,pandocEscapedDollar,pandocDelimitedCodeBlock,pandocListItemContinuation,pandocAutomaticLink skipempty display
    +endif
    +syn match pandocListItemBullet /^(\?.\{-}[.)]/ contained containedin=pandocListItem
    +syn match pandocListItemBulletId /\(\d\+\|\l\|\#\|@.\{-}\|x\=l\=\(i\{,3}[vx]\=\)\{,3}c\{,3}\)/ contained containedin=pandocListItemBullet
    +
    +syn match pandocListItemContinuation /^\s\+\([-+*]\s\+\|(\?.\+[).]\)\@[[:punct:]]*\)\@<="[[:blank:][:punct:]\n]\@=/  containedin=pandocEmphasis,pandocStrong,pandocUListItem,pandocListItem,pandocListItemContinuation display', 'conceal cchar='.s:cchars['quote_e'])
    +endif
    +" }}}3
    +
    +" Hrule: {{{3
    +syn match pandocHRule /^\s*\([*\-_]\)\s*\%(\1\s*\)\{2,}$/ display
    +" }}}3
    +
    +" Backslashes: {{{3
    +if g:pandoc#syntax#conceal#backslash == 1
    +    syn match pandocBackslash /\v\\@ this is a test block
    +> this is another *test*. block
    +  a block
    +> less block
    +aaa block
    +
    +no block.
    +
    +# code blocks
    +
    +    def func(a):
    +	return 1
    +
    +no code
    +
    +~~~python
    +def func(a):
    +    return 1
    +~~~
    +
    +~~~ {.python}
    +print("hi")
    +~~~
    +
    +test
    +
    +    ~~~haskell
    +    test
    +    ~~~
    +
    +```vim
    +:exe "cmd args"
    +```
    +
    +> ``` c
    +> void (*signal(int sig, void (*disp)(int)))(int);
    +> ```
    +> 
    +> If `signal()` is used, `disp` is the address of a signal handler, and `sig`
    +> is not `SIGILL`, `SIGTRAP`, or `SIGPWR`, the system first sets the signal's
    +> disposition to `SIG_DFL` before executing the signal handler.
    +
    +# definitions
    +
    +hello
    +:  this is a definition.
    +
    +`hello`
    +:  this is a definition
    +
    +# lists
    +
    +* this
    +* this
    +    * and this
    +	+ and this
    +
    +1) a
    +2) b
    +
    +# smart punctuation
    +
    +ellipses: ....
    +quote "signs". This only works at the beggining and end of words.
    +quote "signs."
    +
    +issue #31:
    +
    +"Foo"
    +*"Foo"*
    +**"Foo"**
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/from-issues.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/from-issues.pdc
    new file mode 100644
    index 0000000..48ea598
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/from-issues.pdc
    @@ -0,0 +1,5 @@
    +- `blah`. aaaaaa (FIXED)
    +
    +[[abc]](xxx) issue #55 vim-pandoc
    +
    +[abc](xyz) `123`. (FIXED)
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/headings.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/headings.pdc
    new file mode 100644
    index 0000000..2610890
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/headings.pdc
    @@ -0,0 +1,37 @@
    +% headings test
    +% vim-pandoc-syntax  
    +  with multiple lines
    +
    +# heading #
    +
    +a
    +
    +## heading with # in it ##
    +
    +a
    +
    +### heading
    +
    +a
    +
    +#### heading
    +
    +a
    +
    +##### heading
    +
    +a
    +
    +###### heading
    +
    +test
    +
    +heading
    +-------
    +
    +aaaaa
    +
    +heading
    +=======
    +
    +aaaaa
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/issue1.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/issue1.pdc
    new file mode 100644
    index 0000000..83f79d5
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/issue1.pdc
    @@ -0,0 +1,16 @@
    +CMake works fine with Visual Studio but there are a few things to consider. The
    +property for the working directory in the Debugging section should most likely
    +be set to `$(OutDir)`. Likewise, the start-up project must be set manually as
    +it's set to ALL_BUILD by default. ALL_BUILD is a project that builds all
    +projects and correctly triggers any scripts. ZERO_CHECK is a project that runs
    +and, if any CMake files have been changed, asks to reload Visual Studio.
    +
    +Furthermore, if you're making a Windows application, you should add the WIN32
    +parameter to `add_executable` to instruct the compiler to use the `WinMain`
    +[entry-point](http://msdn.microsoft.com/en-us/library/f9t8842e.aspx) and
    +WINDOWS
    +[subsystem](http://msdn.microsoft.com/en-us/library/fcc1zstk%28v=vs.110%29.aspx):
    +
    +possible regressions:
    +
    +*test* test *test*. _ this 
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/latex.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/latex.pdc
    new file mode 100644
    index 0000000..645bbdc
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/latex.pdc
    @@ -0,0 +1,33 @@
    +This is a paragraph with $\text{inline} \LaTeX$.
    +This is a paragraph with \(\text{inline} \LaTeX\) using the tex_math_single_backslash extension.
    +LaTeX: \(m\)
    +LaTeX: \(m\)1 (safe for decimal to follow)
    +\[
    +  \frac{1}{2}
    +\]
    +
    +This \$ is a dollar sign. $ this too. $30000 dollars are mentioned in pandoc's
    +Example. I don't have that amount.
    +
    +$$
    +This is \LaTeX too. \sum a b
    +$$
    +
    +\LaTeX
    +
    +\newcommand{\tuple}[1]{\langle #1 \rangle}
    +
    +\begin{tabular}{|l|l|}\hline
    +Age & Frequency \\ \hline
    +18--25  & 15 \\
    +26--35  & 33 \\
    +36--45  & 22 \\ \hline
    +\end{tabular}
    +
    +# test
    +
    +Unicode!
    +$α^i_i$
    +$a^{i}_i$
    +$a^i_{i}$
    +$a^{i}_{i}$
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/refs.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/refs.pdc
    new file mode 100644
    index 0000000..8b33218
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/refs.pdc
    @@ -0,0 +1,65 @@
    +# this is a test for links, references and footnotes. 
    +
    +a link to [the example.com website](http://www.example.com "example") 
    +
    +well, [the example.com website](http://www.example.com) 
    +
    +testing footnotes[^footnote1]. this is kinda silly.^[another footnote.] 
    +^superscrips^ are ok, but be careful to keep spaces between the inline
    +footnotes and the superscripts.^[inline footnotes-- *test*]
    +
    +[^footnote1]: this is a footnote --.
    +
    +[see @Lovejoy, pp. 12]
    +
    +Mister @Lovejoy95, at [-@Lovejoy], cites @Lovejoy [pp. 12-12].
    +
    +Some printable characters might not be matched (in @Krämer, `ä` caused
    +issues). @Krámer @Kraßmer @Kràmer
    +
    +*[PCL]: P C L
    +
    +[hello][welcome] aaaaaa this with [inline link](http://site.com) gets messed up
    +
    +[hello][hello]aaaaaa this
    +
    +[hello] [hello] this is a test.
    +
    +[hello][]
    +
    +[hello1]: www.google.com
    +
    +[hello2]: www.google.com "pandoc"
    +
    +[hello3]: www.google.com
    +  "pandoc"
    +this is no longer part of the reference definition
    +
    +![test](test.png)
    +
    +![testimg][]
    +
    +[testimg]: test.png
    +
    +What follows are some notes on algorithms I've been reviewing from [Algorithms](http://amzn.com/032157351X) by Robert Sedgewick and Kevin Wayne, [The Algorithm Design Manual](http://amzn.com/1849967202) by Steven S. Skiena, and other sources around the Internet [^mit] [^umd] [^umgd]. I wanted to write some notes on the material so that I could easily look back on it, but mainly so that I could be sure that I understand the material --- since I have to understand it to explain it.
    +
    +[Convolution](http://en.wikipedia.org/wiki/Convolution) is a mathematical
    +method of combining two signals to form a third signal. Passing the [Dirac
    +delta function](http://en.wikipedia.org/wiki/Dirac_delta_function) (unit
    +impulse) $\delta[n]$ through a linear system results in the impulse response
    +$h[n]$. The impulse response is simply the signal resulting from passing the
    +unit impulse (Dirac delta function) through a linear system.
    +
    +# issue 25 
    +
    +[[abc]](xxx)
    +
    +[[abc]][]
    +
    +[[abc]]: x
    +
    +# issue 107 (vim-pandoc)
    +
    +[`]`](http://foo.com) foo
    +
    +[Why isn't `ListT []`{.haskell} a monad](http://blog.sigfpe.com/2006/11/why-isnt-listt-monad.html)
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/roman-numerals.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/roman-numerals.pdc
    new file mode 100644
    index 0000000..4c494d2
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/roman-numerals.pdc
    @@ -0,0 +1,46 @@
    +i. a
    +ii. a
    +iii. a
    +iv. a
    +v. a
    +vi. a
    +vii. a
    +viii. a
    +ix. a
    +x. a
    +xi. a
    +xii. a
    +xiii. a
    +xiv. a
    +xv. a
    +xvi. a
    +xvii. a
    +xviii. a
    +xix. a
    +xx. a
    +xxi. a
    +xxii. a
    +xxx. a
    +xl. a
    +xliii. a
    +l. a
    +lx. a
    +lxix. a
    +xc. a
    +c. a
    +
    +it breaks down at c, but maybe that's enough...
    +
    +ci. a
    +cx. a
    +cxl. a
    +cl. a
    +cxc. a
    +cc. a
    +ccc. a
    +cd. a
    +d. a
    +cm. a
    +m. a
    +
    +
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/tables.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/tables.pdc
    new file mode 100644
    index 0000000..3e3b67e
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/tables.pdc
    @@ -0,0 +1,82 @@
    +# Simple tables
    +
    +  Right     Left     Center     Default
    +-------     ------ ----------   -------
    +     12     12        12            12
    +    123     123       123          123
    +      1     1          1             1
    +
    +error         2nd-order   10th-order
    +------       ----------- ------------
    +$\insample$  $0.029$     $10^{-5}$
    +$\outsample$ $0.120$     $7680$
    +
    +-------     ------ ----------   -------
    +     12     12        12             12
    +    123     123       123           123
    +      1     1          1              1
    +-------     ------ ----------   -------
    +
    +# Mutiline tables
    +
    +-------------------------------------------------------------
    + Centered   Default           Right Left
    +  Header    Aligned         Aligned Aligned
    +----------- ------- --------------- -------------------------
    +   First    row                12.0 Example of a row that
    +                                    spans multiple lines.
    +
    +  Second    row                 5.0 Here's another one. Note
    +                                    the blank line between
    +                                    rows.
    +-------------------------------------------------------------
    +
    +----------- ------- --------------- -------------------------
    +   First    row                12.0 Example of a row that
    +                                    spans multiple lines.
    +
    +  Second    row                 5.0 Here's another one. Note
    +                                    the blank line between
    +                                    rows.
    +----------- ------- --------------- -------------------------
    +
    +# grid table
    +
    ++---------------+---------------+--------------------+
    +| Fruit         | Price         | Advantages         |
    ++===============+===============+====================+
    +| Bananas       | $1.34         | - built-in wrapper |
    +|               |               | - bright color     |
    ++---------------+---------------+--------------------+
    +| Oranges       | $2.10         | - cures scurvy     |
    +|               |               | - tasty            |
    ++---------------+---------------+--------------------+
    +
    ++----------------------+------------------+
    +| test                 | a                |
    ++----------------------+------------------+
    +
    +# pipe tables 
    +
    +| Right | Left | Default | Center |
    +|------:|:-----|---------|:------:|
    +|   12  |  12  |    12   |    12  |
    +|  123  |  123 |   123   |   123  |
    +|    1  |    1 |     1   |     1  |
    +
    +Right | Left | Default | Center
    +-----:|:-----|---------|:------:
    +  12  |  12  |    12   |    12  
    + 123  |  123 |   123   |   123  
    +   1  |    1 |     1   |     1  
    +
    +fruit| price
    +-----|-----:
    +apple|2.05
    +pear|1.37
    +orange|3.09
    +
    +| One | Two   |
    +|-----+-------|
    +| my  | table |
    +| is  | nice  |
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/test.png b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/test.png
    new file mode 100644
    index 0000000..15e0eaa
    Binary files /dev/null and b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/test.png differ
    diff --git a/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/yaml.pdc b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/yaml.pdc
    new file mode 100644
    index 0000000..1a06554
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/vim-pandoc-syntax/tests/yaml.pdc
    @@ -0,0 +1,11 @@
    +---
    +title: Scala
    +published: October 12, 2013
    +excerpt: Promising mixture of OO and FP
    +comments: off
    +---
    +
    +---
    +
    +hello
    +-----
    diff --git a/etc/soft/nvim/+plugins/vim-signature/.gitignore b/etc/soft/nvim/+plugins/vim-signature/.gitignore
    deleted file mode 100644
    index 0a56e3f..0000000
    --- a/etc/soft/nvim/+plugins/vim-signature/.gitignore
    +++ /dev/null
    @@ -1 +0,0 @@
    -/doc/tags
    diff --git a/etc/soft/nvim/+plugins/which-key.nvim/LICENSE b/etc/soft/nvim/+plugins/which-key.nvim/LICENSE
    new file mode 100644
    index 0000000..261eeb9
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/which-key.nvim/LICENSE
    @@ -0,0 +1,201 @@
    +                                 Apache License
    +                           Version 2.0, January 2004
    +                        http://www.apache.org/licenses/
    +
    +   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
    +
    +   1. Definitions.
    +
    +      "License" shall mean the terms and conditions for use, reproduction,
    +      and distribution as defined by Sections 1 through 9 of this document.
    +
    +      "Licensor" shall mean the copyright owner or entity authorized by
    +      the copyright owner that is granting the License.
    +
    +      "Legal Entity" shall mean the union of the acting entity and all
    +      other entities that control, are controlled by, or are under common
    +      control with that entity. For the purposes of this definition,
    +      "control" means (i) the power, direct or indirect, to cause the
    +      direction or management of such entity, whether by contract or
    +      otherwise, or (ii) ownership of fifty percent (50%) or more of the
    +      outstanding shares, or (iii) beneficial ownership of such entity.
    +
    +      "You" (or "Your") shall mean an individual or Legal Entity
    +      exercising permissions granted by this License.
    +
    +      "Source" form shall mean the preferred form for making modifications,
    +      including but not limited to software source code, documentation
    +      source, and configuration files.
    +
    +      "Object" form shall mean any form resulting from mechanical
    +      transformation or translation of a Source form, including but
    +      not limited to compiled object code, generated documentation,
    +      and conversions to other media types.
    +
    +      "Work" shall mean the work of authorship, whether in Source or
    +      Object form, made available under the License, as indicated by a
    +      copyright notice that is included in or attached to the work
    +      (an example is provided in the Appendix below).
    +
    +      "Derivative Works" shall mean any work, whether in Source or Object
    +      form, that is based on (or derived from) the Work and for which the
    +      editorial revisions, annotations, elaborations, or other modifications
    +      represent, as a whole, an original work of authorship. For the purposes
    +      of this License, Derivative Works shall not include works that remain
    +      separable from, or merely link (or bind by name) to the interfaces of,
    +      the Work and Derivative Works thereof.
    +
    +      "Contribution" shall mean any work of authorship, including
    +      the original version of the Work and any modifications or additions
    +      to that Work or Derivative Works thereof, that is intentionally
    +      submitted to Licensor for inclusion in the Work by the copyright owner
    +      or by an individual or Legal Entity authorized to submit on behalf of
    +      the copyright owner. For the purposes of this definition, "submitted"
    +      means any form of electronic, verbal, or written communication sent
    +      to the Licensor or its representatives, including but not limited to
    +      communication on electronic mailing lists, source code control systems,
    +      and issue tracking systems that are managed by, or on behalf of, the
    +      Licensor for the purpose of discussing and improving the Work, but
    +      excluding communication that is conspicuously marked or otherwise
    +      designated in writing by the copyright owner as "Not a Contribution."
    +
    +      "Contributor" shall mean Licensor and any individual or Legal Entity
    +      on behalf of whom a Contribution has been received by Licensor and
    +      subsequently incorporated within the Work.
    +
    +   2. Grant of Copyright License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      copyright license to reproduce, prepare Derivative Works of,
    +      publicly display, publicly perform, sublicense, and distribute the
    +      Work and such Derivative Works in Source or Object form.
    +
    +   3. Grant of Patent License. Subject to the terms and conditions of
    +      this License, each Contributor hereby grants to You a perpetual,
    +      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
    +      (except as stated in this section) patent license to make, have made,
    +      use, offer to sell, sell, import, and otherwise transfer the Work,
    +      where such license applies only to those patent claims licensable
    +      by such Contributor that are necessarily infringed by their
    +      Contribution(s) alone or by combination of their Contribution(s)
    +      with the Work to which such Contribution(s) was submitted. If You
    +      institute patent litigation against any entity (including a
    +      cross-claim or counterclaim in a lawsuit) alleging that the Work
    +      or a Contribution incorporated within the Work constitutes direct
    +      or contributory patent infringement, then any patent licenses
    +      granted to You under this License for that Work shall terminate
    +      as of the date such litigation is filed.
    +
    +   4. Redistribution. You may reproduce and distribute copies of the
    +      Work or Derivative Works thereof in any medium, with or without
    +      modifications, and in Source or Object form, provided that You
    +      meet the following conditions:
    +
    +      (a) You must give any other recipients of the Work or
    +          Derivative Works a copy of this License; and
    +
    +      (b) You must cause any modified files to carry prominent notices
    +          stating that You changed the files; and
    +
    +      (c) You must retain, in the Source form of any Derivative Works
    +          that You distribute, all copyright, patent, trademark, and
    +          attribution notices from the Source form of the Work,
    +          excluding those notices that do not pertain to any part of
    +          the Derivative Works; and
    +
    +      (d) If the Work includes a "NOTICE" text file as part of its
    +          distribution, then any Derivative Works that You distribute must
    +          include a readable copy of the attribution notices contained
    +          within such NOTICE file, excluding those notices that do not
    +          pertain to any part of the Derivative Works, in at least one
    +          of the following places: within a NOTICE text file distributed
    +          as part of the Derivative Works; within the Source form or
    +          documentation, if provided along with the Derivative Works; or,
    +          within a display generated by the Derivative Works, if and
    +          wherever such third-party notices normally appear. The contents
    +          of the NOTICE file are for informational purposes only and
    +          do not modify the License. You may add Your own attribution
    +          notices within Derivative Works that You distribute, alongside
    +          or as an addendum to the NOTICE text from the Work, provided
    +          that such additional attribution notices cannot be construed
    +          as modifying the License.
    +
    +      You may add Your own copyright statement to Your modifications and
    +      may provide additional or different license terms and conditions
    +      for use, reproduction, or distribution of Your modifications, or
    +      for any such Derivative Works as a whole, provided Your use,
    +      reproduction, and distribution of the Work otherwise complies with
    +      the conditions stated in this License.
    +
    +   5. Submission of Contributions. Unless You explicitly state otherwise,
    +      any Contribution intentionally submitted for inclusion in the Work
    +      by You to the Licensor shall be under the terms and conditions of
    +      this License, without any additional terms or conditions.
    +      Notwithstanding the above, nothing herein shall supersede or modify
    +      the terms of any separate license agreement you may have executed
    +      with Licensor regarding such Contributions.
    +
    +   6. Trademarks. This License does not grant permission to use the trade
    +      names, trademarks, service marks, or product names of the Licensor,
    +      except as required for reasonable and customary use in describing the
    +      origin of the Work and reproducing the content of the NOTICE file.
    +
    +   7. Disclaimer of Warranty. Unless required by applicable law or
    +      agreed to in writing, Licensor provides the Work (and each
    +      Contributor provides its Contributions) on an "AS IS" BASIS,
    +      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
    +      implied, including, without limitation, any warranties or conditions
    +      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
    +      PARTICULAR PURPOSE. You are solely responsible for determining the
    +      appropriateness of using or redistributing the Work and assume any
    +      risks associated with Your exercise of permissions under this License.
    +
    +   8. Limitation of Liability. In no event and under no legal theory,
    +      whether in tort (including negligence), contract, or otherwise,
    +      unless required by applicable law (such as deliberate and grossly
    +      negligent acts) or agreed to in writing, shall any Contributor be
    +      liable to You for damages, including any direct, indirect, special,
    +      incidental, or consequential damages of any character arising as a
    +      result of this License or out of the use or inability to use the
    +      Work (including but not limited to damages for loss of goodwill,
    +      work stoppage, computer failure or malfunction, or any and all
    +      other commercial damages or losses), even if such Contributor
    +      has been advised of the possibility of such damages.
    +
    +   9. Accepting Warranty or Additional Liability. While redistributing
    +      the Work or Derivative Works thereof, You may choose to offer,
    +      and charge a fee for, acceptance of support, warranty, indemnity,
    +      or other liability obligations and/or rights consistent with this
    +      License. However, in accepting such obligations, You may act only
    +      on Your own behalf and on Your sole responsibility, not on behalf
    +      of any other Contributor, and only if You agree to indemnify,
    +      defend, and hold each Contributor harmless for any liability
    +      incurred by, or claims asserted against, such Contributor by reason
    +      of your accepting any such warranty or additional liability.
    +
    +   END OF TERMS AND CONDITIONS
    +
    +   APPENDIX: How to apply the Apache License to your work.
    +
    +      To apply the Apache License to your work, attach the following
    +      boilerplate notice, with the fields enclosed by brackets "[]"
    +      replaced with your own identifying information. (Don't include
    +      the brackets!)  The text should be enclosed in the appropriate
    +      comment syntax for the file format. We also recommend that a
    +      file or class name and description of purpose be included on the
    +      same "printed page" as the copyright notice for easier
    +      identification within third-party archives.
    +
    +   Copyright [yyyy] [name of copyright owner]
    +
    +   Licensed under the Apache License, Version 2.0 (the "License");
    +   you may not use this file except in compliance with the License.
    +   You may obtain a copy of the License at
    +
    +       http://www.apache.org/licenses/LICENSE-2.0
    +
    +   Unless required by applicable law or agreed to in writing, software
    +   distributed under the License is distributed on an "AS IS" BASIS,
    +   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    +   See the License for the specific language governing permissions and
    +   limitations under the License.
    diff --git a/etc/soft/nvim/+plugins/which-key.nvim/README.md b/etc/soft/nvim/+plugins/which-key.nvim/README.md
    new file mode 100644
    index 0000000..687a59f
    --- /dev/null
    +++ b/etc/soft/nvim/+plugins/which-key.nvim/README.md
    @@ -0,0 +1,345 @@
    +# 💥 Which Key
    +
    +**WhichKey** is a lua plugin for Neovim 0.5 that displays a popup with possible key bindings of
    +the command you started typing. Heavily inspired by the original [emacs-which-key](https://github.com/justbur/emacs-which-key) and [vim-which-key](https://github.com/liuchengxu/vim-which-key).
    +
    +![image](https://user-images.githubusercontent.com/292349/116439438-669f8d00-a804-11eb-9b5b-c7122bd9acac.png)
    +
    +## ✨ Features
    +
    +- for Neovim 0.7 and higher, it uses the `desc` attributes of your mappings as the default label
    +- for Neovim 0.7 and higher, new mappings will be created with a `desc` attribute
    +- opens a popup with suggestions to complete a key binding
    +- works with any setting for [timeoutlen](https://neovim.io/doc/user/options.html#'timeoutlen'), including instantly (`timeoutlen=0`)
    +- works correctly with built-in key bindings
    +- works correctly with buffer-local mappings
    +- extensible plugin architecture
    +- built-in plugins:
    +  - **marks:** shows your marks when you hit one of the jump keys.
    +  - **registers:** shows the contents of your registers
    +  - **presets:** built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g`
    +  - **spelling:** spelling suggestions inside the which-key popup
    +
    +## ⚡️ Requirements
    +
    +- Neovim >= 0.5.0
    +
    +## 📦 Installation
    +
    +Install the plugin with your preferred package manager:
    +
    +### [vim-plug](https://github.com/junegunn/vim-plug)
    +
    +```vim
    +" Vim Script
    +Plug 'folke/which-key.nvim'
    +
    +lua << EOF
    +  require("which-key").setup {
    +    -- your configuration comes here
    +    -- or leave it empty to use the default settings
    +    -- refer to the configuration section below
    +  }
    +EOF
    +```
    +
    +### [packer](https://github.com/wbthomason/packer.nvim)
    +
    +```lua
    +-- Lua
    +use {
    +  "folke/which-key.nvim",
    +  config = function()
    +    require("which-key").setup {
    +      -- your configuration comes here
    +      -- or leave it empty to use the default settings
    +      -- refer to the configuration section below
    +    }
    +  end
    +}
    +```
    +
    +## ⚙️ Configuration
    +
    +> ❗️ IMPORTANT: the timeout when **WhichKey** opens is controlled by the vim setting [timeoutlen](https://neovim.io/doc/user/options.html#'timeoutlen').
    +> Please refer to the documentation to properly set it up. Setting it to `0`, will effectively
    +> always show **WhichKey** immediately, but a setting of `500` (500ms) is probably more appropriate.
    +
    +> ❗️ don't create any keymappings yourself to trigger WhichKey. Unlike with _vim-which-key_, we do this fully automatically.
    +> Please remove any left-over triggers you might have from using _vim-which-key_.
    +
    +> 🚑 You can run `:checkhealth which_key` to see if there's any conflicting keymaps that will prevent triggering **WhichKey**
    +
    +WhichKey comes with the following defaults:
    +
    +```lua
    +{
    +  plugins = {
    +    marks = true, -- shows a list of your marks on ' and `
    +    registers = true, -- shows your registers on " in NORMAL or  in INSERT mode
    +    spelling = {
    +      enabled = false, -- enabling this will show WhichKey when pressing z= to select spelling suggestions
    +      suggestions = 20, -- how many suggestions should be shown in the list?
    +    },
    +    -- the presets plugin, adds help for a bunch of default keybindings in Neovim
    +    -- No actual key bindings are created
    +    presets = {
    +      operators = true, -- adds help for operators like d, y, ... and registers them for motion / text object completion
    +      motions = true, -- adds help for motions
    +      text_objects = true, -- help for text objects triggered after entering an operator
    +      windows = true, -- default bindings on 
    +      nav = true, -- misc bindings to work with windows
    +      z = true, -- bindings for folds, spelling and others prefixed with z
    +      g = true, -- bindings for prefixed with g
    +    },
    +  },
    +  -- add operators that will trigger motion and text object completion
    +  -- to enable all native operators, set the preset / operators plugin above
    +  operators = { gc = "Comments" },
    +  key_labels = {
    +    -- override the label used to display some keys. It doesn't effect WK in any other way.
    +    -- For example:
    +    -- [""] = "SPC",
    +    -- [""] = "RET",
    +    -- [""] = "TAB",
    +  },
    +  icons = {
    +    breadcrumb = "»", -- symbol used in the command line area that shows your active key combo
    +    separator = "➜", -- symbol used between a key and it's label
    +    group = "+", -- symbol prepended to a group
    +  },
    +  popup_mappings = {
    +    scroll_down = '', -- binding to scroll down inside the popup
    +    scroll_up = '', -- binding to scroll up inside the popup
    +  },
    +  window = {
    +    border = "none", -- none, single, double, shadow
    +    position = "bottom", -- bottom, top
    +    margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left]
    +    padding = { 2, 2, 2, 2 }, -- extra window padding [top, right, bottom, left]
    +    winblend = 0
    +  },
    +  layout = {
    +    height = { min = 4, max = 25 }, -- min and max height of the columns
    +    width = { min = 20, max = 50 }, -- min and max width of the columns
    +    spacing = 3, -- spacing between columns
    +    align = "left", -- align columns left, center or right
    +  },
    +  ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label
    +  hidden = { "", "", "", "", "call", "lua", "^:", "^ "}, -- hide mapping boilerplate
    +  show_help = true, -- show help message on the command line when the popup is visible
    +  show_keys = true, -- show the currently pressed key and its label as a message in the command line
    +  triggers = "auto", -- automatically setup triggers
    +  -- triggers = {""} -- or specify a list manually
    +  triggers_blacklist = {
    +    -- list of mode / prefixes that should never be hooked by WhichKey
    +    -- this is mostly relevant for key maps that start with a native binding
    +    -- most people should not need to change this
    +    i = { "j", "k" },
    +    v = { "j", "k" },
    +  },
    +  -- disable the WhichKey popup for certain buf types and file types.
    +  -- Disabled by deafult for Telescope
    +  disable = {
    +    buftypes = {},
    +    filetypes = { "TelescopePrompt" },
    +  },
    +}
    +```
    +
    +## 🪄 Setup
    +
    +With the default settings, **WhichKey** will work out of the box for most builtin keybindings,
    +but the real power comes from documenting and organizing your own keybindings.
    +
    +To document and/or setup your own mappings, you need to call the `register` method
    +
    +```lua
    +local wk = require("which-key")
    +wk.register(mappings, opts)
    +```
    +
    +Default options for `opts`
    +
    +```lua
    +{
    +  mode = "n", -- NORMAL mode
    +  -- prefix: use "f" for example for mapping everything related to finding files
    +  -- the prefix is prepended to every mapping part of `mappings`
    +  prefix = "",
    +  buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings
    +  silent = true, -- use `silent` when creating keymaps
    +  noremap = true, -- use `noremap` when creating keymaps
    +  nowait = false, -- use `nowait` when creating keymaps
    +}
    +```
    +
    +> ❕ When you specify a command in your mapping that starts with ``, then we automatically set `noremap=false`, since you always want recursive keybindings in this case
    +
    +### ⌨️ Mappings
    +
    +> ⌨ for **Neovim 0.7** and higher, which key will use the `desc` attribute of existing mappings as the default label
    +
    +Group names use the special `name` key in the tables. There's multiple ways to define the mappings. `wk.register` can be called multiple times from anywhere in your config files.
    +
    +```lua
    +local wk = require("which-key")
    +-- As an example, we will create the following mappings:
    +--  * ff find files
    +--  * fr show recent files
    +--  * fb Foobar
    +-- we'll document:
    +--  * fn new file
    +--  * fe edit file
    +-- and hide 1
    +
    +wk.register({
    +  f = {
    +    name = "file", -- optional group name
    +    f = { "Telescope find_files", "Find File" }, -- create a binding with label
    +    r = { "Telescope oldfiles", "Open Recent File", noremap=false, buffer = 123 }, -- additional options for creating the keymap
    +    n = { "New File" }, -- just a label. don't create any mapping
    +    e = "Edit File", -- same as above
    +    ["1"] = "which_key_ignore",  -- special label to hide it in the popup
    +    b = { function() print("bar") end, "Foobar" } -- you can also pass functions!
    +  },
    +}, { prefix = "" })
    +```
    +
    +
    +Click to see more examples + +```lua +-- all of the mappings below are equivalent + +-- method 2 +wk.register({ + [""] = { + f = { + name = "+file", + f = { "Telescope find_files", "Find File" }, + r = { "Telescope oldfiles", "Open Recent File" }, + n = { "enew", "New File" }, + }, + }, +}) + +-- method 3 +wk.register({ + ["f"] = { + name = "+file", + f = { "Telescope find_files", "Find File" }, + r = { "Telescope oldfiles", "Open Recent File" }, + n = { "enew", "New File" }, + }, +}) + +-- method 4 +wk.register({ + ["f"] = { name = "+file" }, + ["ff"] = { "Telescope find_files", "Find File" }, + ["fr"] = { "Telescope oldfiles", "Open Recent File" }, + ["fn"] = { "enew", "New File" }, +}) +``` + +
    + +**Tips:** The default label is `keymap.desc` or `keymap.rhs` or `""`, +`:h nvim_set_keymap()` to get more details about `desc` and `rhs`. + +### 🚙 Operators, Motions and Text Objects + +**WhichKey** provides help to work with operators, motions and text objects. + +> `[count]operator[count][text-object]` + +- operators can be configured with the `operators` option + - set `plugins.presets.operators` to `true` to automatically configure vim built-in operators + - set this to `false`, to only include the list you configured in the `operators` option. + - see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L5) for the full list part of the preset +- text objects are automatically retrieved from **operator pending** key maps (`omap`) + - set `plugins.presets.text_objects` to `true` to configure built-in text objects + - see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L43) +- motions are part of the preset `plugins.presets.motions` setting + - see [here](https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua#L20) + +
    +How to disable some operators? (like v) + +```lua +-- make sure to run this code before calling setup() +-- refer to the full lists at https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua +local presets = require("which-key.plugins.presets") +presets.operators["v"] = nil +``` + +
    + +## 🚀 Usage + +When the **WhichKey** popup is open, you can use the following key bindings (they are also displayed at the bottom of the screen): + +- hit one of the keys to open a group or execute a key binding +- `` to cancel and close the popup +- `` go up one level +- `` scroll down +- `` scroll up + +Apart from the automatic opening, you can also manually open **WhichKey** for a certain `prefix`: + +> ❗️ don't create any keymappings yourself to trigger WhichKey. Unlike with _vim-which-key_, we do this fully automatically. +> Please remove any left-over triggers you might have from using _vim-which-key_. + +```vim +:WhichKey " show all mappings +:WhichKey " show all mappings +:WhichKey v " show all mappings for VISUAL mode +:WhichKey '' v " show ALL mappings for VISUAL mode +``` + +## 🔥 Plugins + +Four built-in plugins are included with **WhichKey**. + +### Marks + +Shows a list of your buffer local and global marks when you hit \` or ' + +![image](https://user-images.githubusercontent.com/292349/116439573-8f278700-a804-11eb-80ca-bb9263e6d937.png) + +### Registers + +Shows a list of your buffer local and global registers when you hit " in _NORMAL_ mode, or `` in _INSERT_ mode. + +![image](https://user-images.githubusercontent.com/292349/116439609-98b0ef00-a804-11eb-9385-97c7d5ff4113.png) + +### Presets + +Built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g` + +![image](https://user-images.githubusercontent.com/292349/116439871-df9ee480-a804-11eb-9529-800e167db65c.png) + +### Spelling + +When enabled, this plugin hooks into `z=` and replaces the full-screen spelling suggestions window by a list of suggestions within **WhichKey**. + +![image](https://user-images.githubusercontent.com/292349/118102022-1c361880-b38d-11eb-8e82-79ad266d9bb8.png) + +## 🎨 Colors + +The table below shows all the highlight groups defined for **WhichKey** with their default link. + +| Highlight Group | Defaults to | Description | +| ------------------- | ----------- | ------------------------------------------- | +| _WhichKey_ | Function | the key | +| _WhichKeyGroup_ | Keyword | a group | +| _WhichKeySeparator_ | DiffAdd | the separator between the key and its label | +| _WhichKeyDesc_ | Identifier | the label of the key | +| _WhichKeyFloat_ | NormalFloat | Normal in the popup window | +| _WhichKeyBorder_ | FloatBorder | Normal in the popup window | +| _WhichKeyValue_ | Comment | used by plugins that provide values | + + + + diff --git a/etc/soft/nvim/+plugins/which-key.nvim/TODO.md b/etc/soft/nvim/+plugins/which-key.nvim/TODO.md new file mode 100644 index 0000000..15afdb0 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/TODO.md @@ -0,0 +1,21 @@ +# Todo + +* [x] hook into all groups +* [x] show mappings without keymap (zz etc) +* [x] plugin support for marks, registers, text objects +* [x] `` to go up a level +* [x] config modes +* [x] update buf only +* [x] + thingy for groups +* [x] text objects +* [x] get label from global when not found for buffer +* [x] operators & motions +* [x] show window after timeout? +* [x] make plugins a list of key value with config in value +* [x] cleanup text objects text +* [x] buf local mappings seems to interfere with global mappings (push K in help) +* [x] fix help in visual mode +* [x] Plug>whichkey nop +* [x] preset plugin +* [x] command should auto stuff +* [x] timeoutlen is always respected and should still work when zero diff --git a/etc/soft/nvim/+plugins/which-key.nvim/autoload/health/which_key.vim b/etc/soft/nvim/+plugins/which-key.nvim/autoload/health/which_key.vim new file mode 100644 index 0000000..d0d959f --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/autoload/health/which_key.vim @@ -0,0 +1,3 @@ +function! health#which_key#check() + lua require 'which-key.keys'.check_health() +endfunction diff --git a/etc/soft/nvim/+plugins/which-key.nvim/doc/which-key.txt b/etc/soft/nvim/+plugins/which-key.nvim/doc/which-key.txt new file mode 100644 index 0000000..49babd3 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/doc/which-key.txt @@ -0,0 +1,422 @@ +*which-key.txt* For NVIM v0.5.0 Last change: 2022 October 28 + +============================================================================== +Table of Contents *which-key-table-of-contents* + +1. 💥 Which Key |which-key-💥-which-key| + - ✨ Features |which-key-✨-features| + - ⚡️ Requirements |which-key-⚡️-requirements| + - 📦 Installation |which-key-📦-installation| + - ⚙️ Configuration |which-key-⚙️-configuration| + - 🪄 Setup |which-key-🪄-setup| + - 🚀 Usage |which-key-🚀-usage| + - 🔥 Plugins |which-key-🔥-plugins| + - 🎨 Colors |which-key-🎨-colors| + +============================================================================== +1. 💥 Which Key *which-key-💥-which-key* + +**WhichKey** is a lua plugin for Neovim 0.5 that displays a popup with possible +key bindings of the command you started typing. Heavily inspired by the +original emacs-which-key and +vim-which-key . + +
    + +

    image

    +
    + +✨ FEATURES *which-key-✨-features* + + +- for Neovim 0.7 and higher, it uses the `desc` attributes of your mappings as the default label +- for Neovim 0.7 and higher, new mappings will be created with a `desc` attribute +- opens a popup with suggestions to complete a key binding +- works with any setting for |timeoutlen|, including instantly (`timeoutlen=0`) +- works correctly with built-in key bindings +- works correctly with buffer-local mappings +- extensible plugin architecture +- built-in plugins: + - **marks:** shows your marks when you hit one of the jump keys. + - **registers:** shows the contents of your registers + - **presets:** built-in key binding help for `motions`, `text-objects`, `operators`, `windows`, `nav`, `z` and `g` + - **spelling:** spelling suggestions inside the which-key popup + + +⚡️ REQUIREMENTS *which-key-⚡️-requirements* + + +- Neovim >= 0.5.0 + + +📦 INSTALLATION *which-key-📦-installation* + +Install the plugin with your preferred package manager: + +VIM-PLUG ~ + +> + " Vim Script + Plug 'folke/which-key.nvim' + + lua << EOF + require("which-key").setup { + -- your configuration comes here + -- or leave it empty to use the default settings + -- refer to the configuration section below + } + EOF +< + + +PACKER ~ + +> + -- Lua + use { + "folke/which-key.nvim", + config = function() + require("which-key").setup { + -- your configuration comes here + -- or leave it empty to use the default settings + -- refer to the configuration section below + } + end + } +< + + +⚙️ CONFIGURATION *which-key-⚙️-configuration* + + + ❗️ IMPORTANT: the timeout when **WhichKey** opens is controlled by the vim + setting |timeoutlen|. Please refer to the documentation to properly set it up. + Setting it to `0`, will effectively always show **WhichKey** immediately, but a + setting of `500` (500ms) is probably more appropriate. + + + + ❗️ don’t create any keymappings yourself to trigger WhichKey. Unlike with + _vim-which-key_, we do this fully automatically. Please remove any left-over + triggers you might have from using _vim-which-key_. + + + + 🚑 You can run `:checkhealth which_key` to see if there’s any conflicting + keymaps that will prevent triggering **WhichKey** + + +WhichKey comes with the following defaults: + +> + { + plugins = { + marks = true, -- shows a list of your marks on ' and ` + registers = true, -- shows your registers on " in NORMAL or in INSERT mode + spelling = { + enabled = false, -- enabling this will show WhichKey when pressing z= to select spelling suggestions + suggestions = 20, -- how many suggestions should be shown in the list? + }, + -- the presets plugin, adds help for a bunch of default keybindings in Neovim + -- No actual key bindings are created + presets = { + operators = true, -- adds help for operators like d, y, ... and registers them for motion / text object completion + motions = true, -- adds help for motions + text_objects = true, -- help for text objects triggered after entering an operator + windows = true, -- default bindings on + nav = true, -- misc bindings to work with windows + z = true, -- bindings for folds, spelling and others prefixed with z + g = true, -- bindings for prefixed with g + }, + }, + -- add operators that will trigger motion and text object completion + -- to enable all native operators, set the preset / operators plugin above + operators = { gc = "Comments" }, + key_labels = { + -- override the label used to display some keys. It doesn't effect WK in any other way. + -- For example: + -- [""] = "SPC", + -- [""] = "RET", + -- [""] = "TAB", + }, + icons = { + breadcrumb = "»", -- symbol used in the command line area that shows your active key combo + separator = "➜", -- symbol used between a key and it's label + group = "+", -- symbol prepended to a group + }, + popup_mappings = { + scroll_down = '', -- binding to scroll down inside the popup + scroll_up = '', -- binding to scroll up inside the popup + }, + window = { + border = "none", -- none, single, double, shadow + position = "bottom", -- bottom, top + margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left] + padding = { 2, 2, 2, 2 }, -- extra window padding [top, right, bottom, left] + winblend = 0 + }, + layout = { + height = { min = 4, max = 25 }, -- min and max height of the columns + width = { min = 20, max = 50 }, -- min and max width of the columns + spacing = 3, -- spacing between columns + align = "left", -- align columns left, center or right + }, + ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label + hidden = { "", "", "", "", "call", "lua", "^:", "^ "}, -- hide mapping boilerplate + show_help = true, -- show help message on the command line when the popup is visible + show_keys = true, -- show the currently pressed key and its label as a message in the command line + triggers = "auto", -- automatically setup triggers + -- triggers = {""} -- or specify a list manually + triggers_blacklist = { + -- list of mode / prefixes that should never be hooked by WhichKey + -- this is mostly relevant for key maps that start with a native binding + -- most people should not need to change this + i = { "j", "k" }, + v = { "j", "k" }, + }, + -- disable the WhichKey popup for certain buf types and file types. + -- Disabled by deafult for Telescope + disable = { + buftypes = {}, + filetypes = { "TelescopePrompt" }, + }, + } +< + + +🪄 SETUP *which-key-🪄-setup* + +With the default settings, **WhichKey** will work out of the box for most +builtin keybindings, but the real power comes from documenting and organizing +your own keybindings. + +To document and/or setup your own mappings, you need to call the `register` +method + +> + local wk = require("which-key") + wk.register(mappings, opts) +< + + +Default options for `opts` + +> + { + mode = "n", -- NORMAL mode + -- prefix: use "f" for example for mapping everything related to finding files + -- the prefix is prepended to every mapping part of `mappings` + prefix = "", + buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings + silent = true, -- use `silent` when creating keymaps + noremap = true, -- use `noremap` when creating keymaps + nowait = false, -- use `nowait` when creating keymaps + } +< + + + + ❕ When you specify a command in your mapping that starts with ``, then + we automatically set `noremap=false`, since you always want recursive + keybindings in this case + + +⌨️ MAPPINGS ~ + + + ⌨ for **Neovim 0.7** and higher, which key will use the `desc` attribute of + existing mappings as the default label + + +Group names use the special `name` key in the tables. There’s multiple ways +to define the mappings. `wk.register` can be called multiple times from +anywhere in your config files. + +> + local wk = require("which-key") + -- As an example, we will create the following mappings: + -- * ff find files + -- * fr show recent files + -- * fb Foobar + -- we'll document: + -- * fn new file + -- * fe edit file + -- and hide 1 + + wk.register({ + f = { + name = "file", -- optional group name + f = { "Telescope find_files", "Find File" }, -- create a binding with label + r = { "Telescope oldfiles", "Open Recent File", noremap=false, buffer = 123 }, -- additional options for creating the keymap + n = { "New File" }, -- just a label. don't create any mapping + e = "Edit File", -- same as above + ["1"] = "which_key_ignore", -- special label to hide it in the popup + b = { function() print("bar") end, "Foobar" } -- you can also pass functions! + }, + }, { prefix = "" }) +< + + +Click to see more examples + +> + -- all of the mappings below are equivalent + + -- method 2 + wk.register({ + [""] = { + f = { + name = "+file", + f = { "Telescope find_files", "Find File" }, + r = { "Telescope oldfiles", "Open Recent File" }, + n = { "enew", "New File" }, + }, + }, + }) + + -- method 3 + wk.register({ + ["f"] = { + name = "+file", + f = { "Telescope find_files", "Find File" }, + r = { "Telescope oldfiles", "Open Recent File" }, + n = { "enew", "New File" }, + }, + }) + + -- method 4 + wk.register({ + ["f"] = { name = "+file" }, + ["ff"] = { "Telescope find_files", "Find File" }, + ["fr"] = { "Telescope oldfiles", "Open Recent File" }, + ["fn"] = { "enew", "New File" }, + }) +< + + +**Tips:** The default label is `keymap.desc` or `keymap.rhs` or `""`, `:h +nvim_set_keymap()` to get more details about `desc` and `rhs`. + +🚙 OPERATORS, MOTIONS AND TEXT OBJECTS ~ + +**WhichKey** provides help to work with operators, motions and text objects. + + + `[count]operator[count][text-object]` + + + +- operators can be configured with the `operators` option + - set `plugins.presets.operators` to `true` to automatically configure vim built-in operators + - set this to `false`, to only include the list you configured in the `operators` option. + - see here for the full list part of the preset +- text objects are automatically retrieved from **operator pending** key maps (`omap`) + - set `plugins.presets.text_objects` to `true` to configure built-in text objects + - see here +- motions are part of the preset `plugins.presets.motions` setting + - see here + + +How to disable some operators? (like v) + +> + -- make sure to run this code before calling setup() + -- refer to the full lists at https://github.com/folke/which-key.nvim/blob/main/lua/which-key/plugins/presets/init.lua + local presets = require("which-key.plugins.presets") + presets.operators["v"] = nil +< + + +🚀 USAGE *which-key-🚀-usage* + +When the **WhichKey** popup is open, you can use the following key bindings +(they are also displayed at the bottom of the screen): + + +- hit one of the keys to open a group or execute a key binding +- `` to cancel and close the popup +- `` go up one level +- `` scroll down +- `` scroll up + + +Apart from the automatic opening, you can also manually open **WhichKey** for a +certain `prefix`: + + + ❗️ don’t create any keymappings yourself to trigger WhichKey. Unlike with + _vim-which-key_, we do this fully automatically. Please remove any left-over + triggers you might have from using _vim-which-key_. + + +> + :WhichKey " show all mappings + :WhichKey " show all mappings + :WhichKey v " show all mappings for VISUAL mode + :WhichKey '' v " show ALL mappings for VISUAL mode +< + + +🔥 PLUGINS *which-key-🔥-plugins* + +Four built-in plugins are included with **WhichKey**. + +MARKS ~ + +Shows a list of your buffer local and global marks when you hit ` or ’ + +
    + +

    image

    +
    + +REGISTERS ~ + +Shows a list of your buffer local and global registers when you hit ” in +_NORMAL_ mode, or `` in _INSERT_ mode. + +
    + +

    image

    +
    + +PRESETS ~ + +Built-in key binding help for `motions`, `text-objects`, `operators`, +`windows`, `nav`, `z` and `g` + +
    + +

    image

    +
    + +SPELLING ~ + +When enabled, this plugin hooks into `z=` and replaces the full-screen spelling +suggestions window by a list of suggestions within **WhichKey**. + +
    + +

    image

    +
    + +🎨 COLORS *which-key-🎨-colors* + +The table below shows all the highlight groups defined for **WhichKey** with +their default link. + +│ Highlight Group │Defaults to│ Description │ +│_WhichKey_ │Function │the key │ +│_WhichKeyGroup_ │Keyword │a group │ +│_WhichKeySeparator_│DiffAdd │the separator between the key and its label│ +│_WhichKeyDesc_ │Identifier │the label of the key │ +│_WhichKeyFloat_ │NormalFloat│Normal in the popup window │ +│_WhichKeyBorder_ │FloatBorder│Normal in the popup window │ +│_WhichKeyValue_ │Comment │used by plugins that provide values │ + + + + +Generated by panvimdoc + +vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/colors.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/colors.lua new file mode 100644 index 0000000..c092263 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/colors.lua @@ -0,0 +1,19 @@ +local M = {} + +local links = { + [""] = "Function", + Separator = "Comment", + Group = "Keyword", + Desc = "Identifier", + Float = "NormalFloat", + Border = "FloatBorder", + Value = "Comment", +} + +function M.setup() + for k, v in pairs(links) do + vim.api.nvim_set_hl(0, "WhichKey" .. k, { link = v, default = true }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/config.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/config.lua new file mode 100644 index 0000000..b0fd69e --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/config.lua @@ -0,0 +1,92 @@ +local M = {} + +M.namespace = vim.api.nvim_create_namespace("WhichKey") + +---@class Options +local defaults = { + plugins = { + marks = true, -- shows a list of your marks on ' and ` + registers = true, -- shows your registers on " in NORMAL or in INSERT mode + -- the presets plugin, adds help for a bunch of default keybindings in Neovim + -- No actual key bindings are created + spelling = { + enabled = false, -- enabling this will show WhichKey when pressing z= to select spelling suggestions + suggestions = 20, -- how many suggestions should be shown in the list? + }, + presets = { + operators = true, -- adds help for operators like d, y, ... + motions = true, -- adds help for motions + text_objects = true, -- help for text objects triggered after entering an operator + windows = true, -- default bindings on + nav = true, -- misc bindings to work with windows + z = true, -- bindings for folds, spelling and others prefixed with z + g = true, -- bindings for prefixed with g + }, + }, + -- add operators that will trigger motion and text object completion + -- to enable all native operators, set the preset / operators plugin above + operators = { gc = "Comments" }, + key_labels = { + -- override the label used to display some keys. It doesn't effect WK in any other way. + -- For example: + -- [""] = "SPC", + -- [""] = "RET", + -- [""] = "TAB", + }, + motions = { + count = true, + }, + icons = { + breadcrumb = "»", -- symbol used in the command line area that shows your active key combo + separator = "➜", -- symbol used between a key and it's label + group = "+", -- symbol prepended to a group + }, + popup_mappings = { + scroll_down = "", -- binding to scroll down inside the popup + scroll_up = "", -- binding to scroll up inside the popup + }, + window = { + border = "none", -- none, single, double, shadow + position = "bottom", -- bottom, top + margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left] + padding = { 1, 2, 1, 2 }, -- extra window padding [top, right, bottom, left] + winblend = 0, -- value between 0-100 0 for fully opaque and 100 for fully transparent + }, + layout = { + height = { min = 4, max = 25 }, -- min and max height of the columns + width = { min = 20, max = 50 }, -- min and max width of the columns + spacing = 3, -- spacing between columns + align = "left", -- align columns left, center or right + }, + ignore_missing = false, -- enable this to hide mappings for which you didn't specify a label + hidden = { "", "", "", "", "^:", "^ ", "^call ", "^lua " }, -- hide mapping boilerplate + show_help = true, -- show a help message in the command line for using WhichKey + show_keys = true, -- show the currently pressed key and its label as a message in the command line + triggers = "auto", -- automatically setup triggers + -- triggers = {""} -- or specifiy a list manually + triggers_nowait = {}, -- list of triggers, where WhichKey should not wait for timeoutlen and show immediately + triggers_blacklist = { + -- list of mode / prefixes that should never be hooked by WhichKey + -- this is mostly relevant for keymaps that start with a native binding + i = { "j", "k" }, + v = { "j", "k" }, + }, + -- disable the WhichKey popup for certain buf types and file types. + -- Disabled by deafult for Telescope + disable = { + buftypes = {}, + filetypes = {}, + }, +} + +---@type Options +M.options = {} + +---@param options? Options +function M.setup(options) + M.options = vim.tbl_deep_extend("force", {}, defaults, options or {}) +end + +M.setup() + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/init.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/init.lua new file mode 100644 index 0000000..a1f34c4 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/init.lua @@ -0,0 +1,108 @@ +local Keys = require("which-key.keys") +local Util = require("which-key.util") + +---@class WhichKey +local M = {} + +local loaded = false -- once we loaded everything +local scheduled = false + +local function schedule_load() + if scheduled then + return + end + scheduled = true + if vim.v.vim_did_enter == 0 then + vim.cmd([[au VimEnter * ++once lua require("which-key").load()]]) + else + M.load() + end +end + +---@param options? Options +function M.setup(options) + require("which-key.config").setup(options) + schedule_load() +end + +function M.execute(id) + local func = Keys.functions[id] + return func() +end + +function M.show(keys, opts) + opts = opts or {} + if type(opts) == "string" then + opts = { mode = opts } + end + + keys = keys or "" + + opts.mode = opts.mode or Util.get_mode() + local buf = vim.api.nvim_get_current_buf() + -- make sure the trees exist for update + Keys.get_tree(opts.mode) + Keys.get_tree(opts.mode, buf) + -- update only trees related to buf + Keys.update(buf) + -- trigger which key + require("which-key.view").open(keys, opts) +end + +function M.show_command(keys, mode) + keys = keys or "" + keys = (keys == '""' or keys == "''") and "" or keys + mode = (mode == '""' or mode == "''") and "" or mode + mode = mode or "n" + keys = Util.t(keys) + if not Util.check_mode(mode) then + Util.error( + "Invalid mode passed to :WhichKey (Don't create any keymappings to trigger WhichKey. WhichKey does this automatically)" + ) + else + M.show(keys, { mode = mode }) + end +end + +local queue = {} + +-- Defer registering keymaps until VimEnter +function M.register(mappings, opts) + schedule_load() + if loaded then + Keys.register(mappings, opts) + Keys.update() + else + table.insert(queue, { mappings, opts }) + end +end + +-- Load mappings and update only once +function M.load() + if loaded then + return + end + require("which-key.plugins").setup() + require("which-key.colors").setup() + Keys.register({}, { prefix = "", mode = "n" }) + Keys.register({}, { prefix = "", mode = "v" }) + Keys.setup() + + for _, reg in pairs(queue) do + local opts = reg[2] or {} + opts.update = false + Keys.register(reg[1], opts) + end + Keys.update() + queue = {} + loaded = true +end + +function M.reset() + -- local mappings = Keys.mappings + require("plenary.reload").reload_module("which-key") + -- require("which-key.Keys").mappings = mappings + require("which-key").setup() +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/keys.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/keys.lua new file mode 100644 index 0000000..fbf6483 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/keys.lua @@ -0,0 +1,473 @@ +local Tree = require("which-key.tree") +local Util = require("which-key.util") +local Config = require("which-key.config") + +-- secret character that will be used to create mappings +local secret = "Þ" + +---@class Keys +local M = {} + +M.functions = {} +M.operators = {} +M.nowait = {} +M.blacklist = {} + +function M.setup() + local builtin_ops = require("which-key.plugins.presets").operators + for op, _ in pairs(builtin_ops) do + M.operators[op] = true + end + local mappings = {} + for op, label in pairs(Config.options.operators) do + M.operators[op] = true + if builtin_ops[op] then + mappings[op] = { name = label, i = { name = "inside" }, a = { name = "around" } } + end + end + for _, t in pairs(Config.options.triggers_nowait) do + M.nowait[t] = true + end + M.register(mappings, { mode = "n", preset = true }) + M.register({ i = { name = "inside" }, a = { name = "around" } }, { mode = "v", preset = true }) + for mode, blacklist in pairs(Config.options.triggers_blacklist) do + for _, prefix_n in ipairs(blacklist) do + M.blacklist[mode] = M.blacklist[mode] or {} + M.blacklist[mode][prefix_n] = true + end + end +end + +function M.get_operator(prefix_i) + for op_n, _ in pairs(Config.options.operators) do + local op_i = Util.t(op_n) + if prefix_i:sub(1, #op_i) == op_i then + return op_i, op_n + end + end +end + +function M.process_motions(ret, mode, prefix_i, buf) + local op_i, op_n = "", "" + if mode ~= "v" then + op_i, op_n = M.get_operator(prefix_i) + end + if (mode == "n" or mode == "v") and op_i then + local op_prefix_i = prefix_i:sub(#op_i + 1) + local op_count = op_prefix_i:match("^(%d+)") + if op_count == "0" then + op_count = nil + end + if Config.options.motions.count == false then + op_count = nil + end + if op_count then + op_prefix_i = op_prefix_i:sub(#op_count + 1) + end + local op_results = M.get_mappings("o", op_prefix_i, buf) + + if not ret.mapping and op_results.mapping then + ret.mapping = op_results.mapping + ret.mapping.prefix = op_n .. (op_count or "") .. ret.mapping.prefix + ret.mapping.keys = Util.parse_keys(ret.mapping.prefix) + end + + for _, mapping in pairs(op_results.mappings) do + mapping.prefix = op_n .. (op_count or "") .. mapping.prefix + mapping.keys = Util.parse_keys(mapping.prefix) + table.insert(ret.mappings, mapping) + end + end +end + +---@return MappingGroup +function M.get_mappings(mode, prefix_i, buf) + ---@class MappingGroup + ---@field mode string + ---@field prefix_i string + ---@field buf number + ---@field mapping? Mapping + ---@field mappings VisualMapping[] + local ret + ret = { mapping = nil, mappings = {}, mode = mode, buf = buf, prefix_i = prefix_i } + + local prefix_len = #Util.parse_internal(prefix_i) + + ---@param node? Node + local function add(node) + if node then + if node.mapping then + ret.mapping = vim.tbl_deep_extend("force", {}, ret.mapping or {}, node.mapping) + end + for k, child in pairs(node.children) do + if child.mapping and child.mapping.label ~= "which_key_ignore" then + ret.mappings[k] = vim.tbl_deep_extend("force", {}, ret.mappings[k] or {}, child.mapping) + end + end + end + end + + local plugin_context = { buf = buf, mode = mode } + add(M.get_tree(mode).tree:get(prefix_i, nil, plugin_context)) + add(M.get_tree(mode, buf).tree:get(prefix_i, nil, plugin_context)) + + -- Handle motions + M.process_motions(ret, mode, prefix_i, buf) + + -- Fix labels + local tmp = {} + for _, value in pairs(ret.mappings) do + value.key = value.keys.notation[prefix_len + 1] + if Config.options.key_labels[value.key] then + value.key = Config.options.key_labels[value.key] + end + local skip = not value.label and Config.options.ignore_missing == true + if Util.t(value.key) == Util.t("") then + skip = true + end + if not skip then + if value.group then + value.label = value.label or "+prefix" + value.label = value.label:gsub("^%+", "") + value.label = Config.options.icons.group .. value.label + elseif not value.label then + value.label = value.desc or value.cmd or "" + for _, v in ipairs(Config.options.hidden) do + value.label = value.label:gsub(v, "") + end + end + if value.value then + value.value = vim.fn.strtrans(value.value) + end + table.insert(tmp, value) + end + end + + -- Sort items, but not for plugins + table.sort(tmp, function(a, b) + if a.order and b.order then + return a.order < b.order + end + if a.group == b.group then + local ak = (a.key or ""):lower() + local bk = (b.key or ""):lower() + local aw = ak:match("[a-z]") and 1 or 0 + local bw = bk:match("[a-z]") and 1 or 0 + if aw == bw then + return ak < bk + end + return aw < bw + else + return (a.group and 1 or 0) < (b.group and 1 or 0) + end + end) + ret.mappings = tmp + + return ret +end + +---@type table +M.mappings = {} +M.duplicates = {} + +function M.map(mode, prefix_n, cmd, buf, opts) + local other = vim.api.nvim_buf_call(buf or 0, function() + local ret = vim.fn.maparg(prefix_n, mode, false, true) + ---@diagnostic disable-next-line: undefined-field + return (ret and ret.lhs and ret.rhs and ret.rhs ~= cmd) and ret or nil + end) + if other then + table.insert(M.duplicates, { mode = mode, prefix = prefix_n, cmd = cmd, buf = buf, other = other }) + end + if buf ~= nil then + pcall(vim.api.nvim_buf_set_keymap, buf, mode, prefix_n, cmd, opts) + else + pcall(vim.api.nvim_set_keymap, mode, prefix_n, cmd, opts) + end +end + +function M.register(mappings, opts) + opts = opts or {} + + mappings = require("which-key.mappings").parse(mappings, opts) + + -- always create the root node for the mode, even if there's no mappings, + -- to ensure we have at least a trigger hooked for non documented keymaps + local modes = {} + + for _, mapping in pairs(mappings) do + if not modes[mapping.mode] then + modes[mapping.mode] = true + M.get_tree(mapping.mode) + end + if mapping.cmd ~= nil then + M.map(mapping.mode, mapping.prefix, mapping.cmd, mapping.buf, mapping.opts) + end + M.get_tree(mapping.mode, mapping.buf).tree:add(mapping) + end +end + +M.hooked = {} + +function M.hook_id(prefix_n, mode, buf) + return mode .. (buf or "") .. Util.t(prefix_n) +end + +function M.is_hooked(prefix_n, mode, buf) + return M.hooked[M.hook_id(prefix_n, mode, buf)] +end + +function M.hook_del(prefix_n, mode, buf) + local id = M.hook_id(prefix_n, mode, buf) + M.hooked[id] = nil + if buf then + pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n) + pcall(vim.api.nvim_buf_del_keymap, buf, mode, prefix_n .. secret) + else + pcall(vim.api.nvim_del_keymap, mode, prefix_n) + pcall(vim.api.nvim_del_keymap, mode, prefix_n .. secret) + end +end + +function M.hook_add(prefix_n, mode, buf, secret_only) + -- check if this trigger is blacklisted + if M.blacklist[mode] and M.blacklist[mode][prefix_n] then + return + end + -- don't hook numbers. See #118 + if tonumber(prefix_n) then + return + end + -- don't hook to j or k in INSERT mode + if mode == "i" and (prefix_n == "j" or prefix_n == "k") then + return + end + -- never hook q + if mode == "n" and prefix_n == "q" then + return + end + -- never hook into select mode + if mode == "s" then + return + end + -- never hook into operator pending mode + -- this is handled differently + if mode == "o" then + return + end + if Util.t(prefix_n) == Util.t("") then + return + end + -- never hook into operators in visual mode + if (mode == "v" or mode == "x") and M.operators[prefix_n] then + return + end + + -- Check if we need to create the hook + if type(Config.options.triggers) == "string" and Config.options.triggers ~= "auto" then + if Util.t(prefix_n) ~= Util.t(Config.options.triggers) then + return + end + end + if type(Config.options.triggers) == "table" then + local ok = false + for _, trigger in pairs(Config.options.triggers) do + if Util.t(trigger) == Util.t(prefix_n) then + ok = true + break + end + end + if not ok then + return + end + end + + local opts = { noremap = true, silent = true } + local id = M.hook_id(prefix_n, mode, buf) + local id_global = M.hook_id(prefix_n, mode) + -- hook up if needed + if not M.hooked[id] and not M.hooked[id_global] then + local cmd = [[lua require("which-key").show(%q, {mode = %q, auto = true})]] + cmd = string.format(cmd, Util.t(prefix_n), mode) + -- map group triggers and nops + -- nops are needed, so that WhichKey always respects timeoutlen + + local mapmode = mode == "v" and "x" or mode + if secret_only ~= true then + M.map(mapmode, prefix_n, cmd, buf, opts) + end + if not M.nowait[prefix_n] then + M.map(mapmode, prefix_n .. secret, "", buf, opts) + end + + M.hooked[id] = true + end +end + +function M.update(buf) + for k, tree in pairs(M.mappings) do + if tree.buf and not vim.api.nvim_buf_is_valid(tree.buf) then + -- remove group for invalid buffers + M.mappings[k] = nil + elseif not buf or not tree.buf or buf == tree.buf then + -- only update buffer maps, if: + -- 1. we dont pass a buffer + -- 2. this is a global node + -- 3. this is a local buffer node for the passed buffer + M.update_keymaps(tree.mode, tree.buf) + M.add_hooks(tree.mode, tree.buf, tree.tree.root) + end + end +end + +---@param node Node +function M.add_hooks(mode, buf, node, secret_only) + if not node.mapping then + node.mapping = { prefix = node.prefix_n, group = true, keys = Util.parse_keys(node.prefix_n) } + end + if node.prefix_n ~= "" and node.mapping.group == true and not node.mapping.cmd then + -- first non-cmd level, so create hook and make all decendents secret only + M.hook_add(node.prefix_n, mode, buf, secret_only) + secret_only = true + end + for _, child in pairs(node.children) do + M.add_hooks(mode, buf, child, secret_only) + end +end + +function M.dump() + local ok = {} + local todo = {} + for _, tree in pairs(M.mappings) do + M.update_keymaps(tree.mode, tree.buf) + tree.tree:walk( + ---@param node Node + function(node) + if node.mapping then + if node.mapping.label then + ok[node.mapping.prefix] = true + todo[node.mapping.prefix] = nil + elseif not ok[node.mapping.prefix] then + todo[node.mapping.prefix] = { node.mapping.cmd or "" } + end + end + end + ) + end + return todo +end + +function M.check_health() + vim.fn["health#report_start"]("WhichKey: checking conflicting keymaps") + for _, tree in pairs(M.mappings) do + M.update_keymaps(tree.mode, tree.buf) + tree.tree:walk( + ---@param node Node + function(node) + local count = 0 + for _ in pairs(node.children) do + count = count + 1 + end + + local auto_prefix = not node.mapping or (node.mapping.group == true and not node.mapping.cmd) + if node.prefix_i ~= "" and count > 0 and not auto_prefix then + local msg = ("conflicting keymap exists for mode **%q**, lhs: **%q**"):format(tree.mode, node.mapping.prefix) + vim.fn["health#report_warn"](msg) + local cmd = node.mapping.cmd or " " + vim.fn["health#report_info"](("rhs: `%s`"):format(cmd)) + end + end + ) + end + for _, dup in pairs(M.duplicates) do + local msg = "" + if dup.buf == dup.other.buffer then + msg = "duplicate keymap" + else + msg = "buffer-local keymap overriding global" + end + msg = (msg .. " for mode **%q**, buf: %d, lhs: **%q**"):format(dup.mode, dup.buf or 0, dup.prefix) + if dup.buf == dup.other.buffer then + vim.fn["health#report_error"](msg) + else + vim.fn["health#report_warn"](msg) + end + vim.fn["health#report_info"](("old rhs: `%s`"):format(dup.other.rhs or "")) + vim.fn["health#report_info"](("new rhs: `%s`"):format(dup.cmd or "")) + end +end + +---@param mode string +---@param buf? buffer +function M.get_tree(mode, buf) + if mode == "s" or mode == "x" then + mode = "v" + end + Util.check_mode(mode, buf) + local idx = mode .. (buf or "") + if not M.mappings[idx] then + M.mappings[idx] = { mode = mode, buf = buf, tree = Tree:new() } + end + return M.mappings[idx] +end + +function M.is_hook(prefix, cmd) + -- skip mappings with our secret nop command + local has_secret = prefix:find(secret) + -- skip auto which-key mappings + local has_wk = cmd and cmd:find("which%-key") and cmd:find("auto") or false + return has_wk or has_secret +end + +---@param mode string +---@param buf number +function M.update_keymaps(mode, buf) + ---@type Keymap[] + local keymaps = buf and vim.api.nvim_buf_get_keymap(buf, mode) or vim.api.nvim_get_keymap(mode) + local tree = M.get_tree(mode, buf).tree + + local function is_no_op(keymap) + return not keymap.callback and Util.t(keymap.rhs) == "" + end + + for _, keymap in pairs(keymaps) do + local skip = M.is_hook(keymap.lhs, keymap.rhs) + + if is_no_op(keymap) then + skip = true + end + + -- check if was remapped + if not skip and Util.t(keymap.lhs) == Util.t("") and mode == "n" then + if is_no_op(keymap) then + skip = true + else + Util.warn( + string.format( + "Your key for %q mode in buf %d is currently mapped to %q. " + .. "WhichKey automatically creates triggers, so please remove the mapping", + mode, + buf or 0, + keymap.rhs + ) + ) + end + end + + if not skip then + local mapping = { + id = Util.t(keymap.lhs), + prefix = keymap.lhs, + cmd = keymap.rhs, + desc = keymap.desc, + keys = Util.parse_keys(keymap.lhs), + } + -- don't include Plug keymaps + if mapping.keys.notation[1]:lower() ~= "" then + tree:add(mapping) + end + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/layout.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/layout.lua new file mode 100644 index 0000000..6b41049 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/layout.lua @@ -0,0 +1,210 @@ +local Config = require("which-key.config") +local Text = require("which-key.text") +local Keys = require("which-key.keys") +local Util = require("which-key.util") + +---@class Layout +---@field mapping Mapping +---@field items VisualMapping[] +---@field options Options +---@field text Text +---@field results MappingGroup +local Layout = {} +Layout.__index = Layout + +---@param mappings MappingGroup +---@param options? Options +function Layout:new(mappings, options) + options = options or Config.options + local this = { + results = mappings, + mapping = mappings.mapping, + items = mappings.mappings, + options = options, + text = Text:new(), + } + setmetatable(this, self) + return this +end + +function Layout:max_width(key) + local max = 0 + for _, item in pairs(self.items) do + if item[key] and Text.len(item[key]) > max then + max = Text.len(item[key]) + end + end + return max +end + +function Layout:trail() + local prefix_i = self.results.prefix_i + local buf_path = Keys.get_tree(self.results.mode, self.results.buf).tree:path(prefix_i) + local path = Keys.get_tree(self.results.mode).tree:path(prefix_i) + local len = #self.results.mapping.keys.notation + local cmd_line = { { " " } } + for i = 1, len, 1 do + local node = buf_path[i] + if not (node and node.mapping and node.mapping.label) then + node = path[i] + end + local step = self.mapping.keys.notation[i] + if node and node.mapping and node.mapping.label then + step = self.options.icons.group .. node.mapping.label + end + if Config.options.key_labels[step] then + step = Config.options.key_labels[step] + end + if Config.options.show_keys then + table.insert(cmd_line, { step, "WhichKeyGroup" }) + if i ~= #self.mapping.keys.notation then + table.insert(cmd_line, { " " .. self.options.icons.breadcrumb .. " ", "WhichKeySeparator" }) + end + end + end + local width = 0 + if Config.options.show_keys then + for _, line in pairs(cmd_line) do + width = width + Text.len(line[1]) + end + end + local help = { -- + [""] = "go up one level", + [""] = "close", + } + if #self.text.lines > self.options.layout.height.max then + help[Config.options.popup_mappings.scroll_down] = "scroll down" + help[Config.options.popup_mappings.scroll_up] = "scroll up" + end + local help_line = {} + local help_width = 0 + for key, label in pairs(help) do + help_width = help_width + Text.len(key) + Text.len(label) + 2 + table.insert(help_line, { key .. " ", "WhichKey" }) + table.insert(help_line, { label .. " ", "WhichKeySeparator" }) + end + if Config.options.show_keys then + table.insert(cmd_line, { string.rep(" ", math.floor(vim.o.columns / 2 - help_width / 2) - width) }) + end + + if self.options.show_help then + for _, l in pairs(help_line) do + table.insert(cmd_line, l) + end + end + vim.api.nvim_echo(cmd_line, false, {}) + vim.cmd([[redraw]]) +end + +function Layout:layout(win) + local window_width = vim.api.nvim_win_get_width(win) + local width = window_width + width = width - self.options.window.padding[2] - self.options.window.padding[4] + + local max_key_width = self:max_width("key") + local max_label_width = self:max_width("label") + local max_value_width = self:max_width("value") + + local intro_width = max_key_width + 2 + Text.len(self.options.icons.separator) + self.options.layout.spacing + local max_width = max_label_width + intro_width + max_value_width + if max_width > width then + max_width = width + end + + local column_width = max_width + + if max_value_width == 0 then + if column_width > self.options.layout.width.max then + column_width = self.options.layout.width.max + end + if column_width < self.options.layout.width.min then + column_width = self.options.layout.width.min + end + else + max_value_width = math.min(max_value_width, math.floor((column_width - intro_width) / 2)) + end + + max_label_width = column_width - (intro_width + max_value_width) + + local columns = math.floor(width / column_width) + + local height = math.ceil(#self.items / columns) + if height < self.options.layout.height.min then + height = self.options.layout.height.min + end + -- if height > self.options.layout.height.max then height = self.options.layout.height.max end + + local col = 1 + local row = 1 + local pad_top = self.options.window.padding[3] + local pad_left = self.options.window.padding[4] + + local columns_used = math.min(columns, math.ceil(#self.items / height)) + local offset_x = 0 + if columns_used < columns then + if self.options.layout.align == "right" then + offset_x = (columns - columns_used) * column_width + elseif self.options.layout.align == "center" then + offset_x = math.floor((columns - columns_used) * column_width / 2) + end + end + + for _, item in pairs(self.items) do + local start = (col - 1) * column_width + self.options.layout.spacing + offset_x + if col == 1 then + start = start + pad_left + end + local key = item.key or "" + if key == "" then + key = "<" + end + if key == Util.t("") then + key = "" + end + if Text.len(key) < max_key_width then + key = string.rep(" ", max_key_width - Text.len(key)) .. key + end + + self.text:set(row + pad_top, start, key, "") + start = start + Text.len(key) + 1 + + self.text:set(row + pad_top, start, self.options.icons.separator, "Separator") + start = start + Text.len(self.options.icons.separator) + 1 + + if item.value then + local value = item.value + start = start + 1 + if Text.len(value) > max_value_width then + value = vim.fn.strcharpart(value, 0, max_value_width - 2) .. " …" + end + self.text:set(row + pad_top, start, value, "Value") + if item.highlights then + for _, hl in pairs(item.highlights) do + self.text:highlight(row + pad_top, start + hl[1] - 1, start + hl[2] - 1, hl[3]) + end + end + start = start + max_value_width + 2 + end + + local label = item.label + if Text.len(label) > max_label_width then + label = vim.fn.strcharpart(label, 0, max_label_width - 2) .. " …" + end + self.text:set(row + pad_top, start, label, item.group and "Group" or "Desc") + + if row % height == 0 then + col = col + 1 + row = 1 + else + row = row + 1 + end + end + + for _ = 1, self.options.window.padding[3], 1 do + self.text:nl() + end + self:trail() + return self.text +end + +return Layout diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/mappings.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/mappings.lua new file mode 100644 index 0000000..3446f37 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/mappings.lua @@ -0,0 +1,229 @@ +local Util = require("which-key.util") + +local M = {} + +local function lookup(...) + local ret = {} + for _, t in ipairs({ ... }) do + for _, v in ipairs(t) do + ret[v] = v + end + end + return ret +end + +local mapargs = { + "noremap", + "desc", + "expr", + "silent", + "nowait", + "script", + "unique", + "callback", + "replace_keycodes", -- TODO: add config setting for default value +} +local wkargs = { + "prefix", + "mode", + "plugin", + "buffer", + "remap", + "cmd", + "name", + "group", + "preset", + "cond", +} +local transargs = lookup({ + "noremap", + "expr", + "silent", + "nowait", + "script", + "unique", + "prefix", + "mode", + "buffer", + "preset", + "replace_keycodes", +}) +local args = lookup(mapargs, wkargs) + +function M.child_opts(opts) + local ret = {} + for k, v in pairs(opts) do + if transargs[k] then + ret[k] = v + end + end + return ret +end + +function M._process(value, opts) + local list = {} + local children = {} + for k, v in pairs(value) do + if type(k) == "number" then + if type(v) == "table" then + -- nested child, without key + table.insert(children, v) + else + -- list value + table.insert(list, v) + end + elseif args[k] then + -- option + opts[k] = v + else + -- nested child, with key + children[k] = v + end + end + return list, children +end + +function M._parse(value, mappings, opts) + if type(value) ~= "table" then + value = { value } + end + + local list, children = M._process(value, opts) + + if opts.plugin then + opts.group = true + end + if opts.name then + -- remove + from group names + opts.name = opts.name and opts.name:gsub("^%+", "") + opts.group = true + end + + -- fix remap + if opts.remap then + opts.noremap = not opts.remap + opts.remap = nil + end + + -- fix buffer + if opts.buffer == 0 then + opts.buffer = vim.api.nvim_get_current_buf() + end + + if opts.cond ~= nil then + if type(opts.cond) == "function" then + if not opts.cond() then + return + end + elseif not opts.cond then + return + end + end + + -- process any array child mappings + for k, v in pairs(children) do + local o = M.child_opts(opts) + if type(k) == "string" then + o.prefix = (o.prefix or "") .. k + end + M._try_parse(v, mappings, o) + end + + -- { desc } + if #list == 1 then + assert(type(list[1]) == "string", "Invalid mapping for " .. vim.inspect({ value = value, opts = opts })) + opts.desc = list[1] + -- { cmd, desc } + elseif #list == 2 then + -- desc + assert(type(list[2]) == "string") + opts.desc = list[2] + + -- cmd + if type(list[1]) == "string" then + opts.cmd = list[1] + elseif type(list[1]) == "function" then + opts.cmd = "" + opts.callback = list[1] + else + error("Incorrect mapping " .. vim.inspect(list)) + end + elseif #list > 2 then + error("Incorrect mapping " .. vim.inspect(list)) + end + + if opts.desc or opts.group then + if type(opts.mode) == "table" then + for _, mode in pairs(opts.mode) do + local mode_opts = vim.deepcopy(opts) + mode_opts.mode = mode + table.insert(mappings, mode_opts) + end + else + table.insert(mappings, opts) + end + end +end + +---@return Mapping +function M.to_mapping(mapping) + mapping.silent = mapping.silent ~= false + mapping.noremap = mapping.noremap ~= false + if mapping.cmd and mapping.cmd:lower():find("^") then + mapping.noremap = false + end + + mapping.buf = mapping.buffer + mapping.buffer = nil + + mapping.mode = mapping.mode or "n" + mapping.label = mapping.desc or mapping.name + mapping.keys = Util.parse_keys(mapping.prefix or "") + + local opts = {} + for _, o in ipairs(mapargs) do + opts[o] = mapping[o] + mapping[o] = nil + end + + if vim.fn.has("nvim-0.7.0") == 0 then + opts.replace_keycodes = nil + + -- Neovim < 0.7.0 doesn't support descriptions + opts.desc = nil + + -- use lua functions proxy for Neovim < 0.7.0 + if opts.callback then + local functions = require("which-key.keys").functions + table.insert(functions, opts.callback) + if opts.expr then + opts.cmd = string.format([[luaeval('require("which-key").execute(%d)')]], #functions) + else + opts.cmd = string.format([[lua require("which-key").execute(%d)]], #functions) + end + opts.callback = nil + end + end + + mapping.opts = opts + return mapping +end + +function M._try_parse(value, mappings, opts) + local ok, err = pcall(M._parse, value, mappings, opts) + if not ok then + Util.error(err) + end +end + +---@return Mapping[] +function M.parse(mappings, opts) + opts = opts or {} + local ret = {} + M._try_parse(mappings, ret, opts) + return vim.tbl_map(function(m) + return M.to_mapping(m) + end, ret) +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/init.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/init.lua new file mode 100644 index 0000000..5933961 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/init.lua @@ -0,0 +1,61 @@ +local Keys = require("which-key.keys") +local Util = require("which-key.util") +local Config = require("which-key.config") + +local M = {} + +M.plugins = {} + +function M.setup() + for name, opts in pairs(Config.options.plugins) do + -- only setup plugin if we didnt load it before + if not M.plugins[name] then + if type(opts) == "boolean" then + opts = { enabled = opts } + end + opts.enabled = opts.enabled ~= false + if opts.enabled then + M.plugins[name] = require("which-key.plugins." .. name) + M._setup(M.plugins[name], opts) + end + end + end +end + +---@param plugin Plugin +function M._setup(plugin, opts) + if plugin.actions then + for _, trigger in pairs(plugin.actions) do + local prefix = trigger.trigger + local mode = trigger.mode or "n" + local label = trigger.label or plugin.name + Keys.register({ [prefix] = { label, plugin = plugin.name } }, { mode = mode }) + end + end + + if plugin.setup then + plugin.setup(require("which-key"), opts, Config.options) + end +end + +---@param mapping Mapping +function M.invoke(mapping, context) + local plugin = M.plugins[mapping.plugin] + local prefix = mapping.prefix + local items = plugin.run(prefix, context.mode, context.buf) + + local ret = {} + for i, item in + ipairs( + items --[[@as VisualMapping[] ]] + ) + do + item.order = i + item.keys = Util.parse_keys(prefix .. item.key) + item.prefix = prefix .. item.key + table.insert(ret, item) + end + return ret +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/marks.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/marks.lua new file mode 100644 index 0000000..fdcab4f --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/marks.lua @@ -0,0 +1,73 @@ +local M = {} + +M.name = "marks" + +M.actions = { + { trigger = "`", mode = "n" }, + { trigger = "'", mode = "n" }, + { trigger = "g`", mode = "n" }, + { trigger = "g'", mode = "n" }, +} + +function M.setup(_wk, _config, options) + for _, action in ipairs(M.actions) do + table.insert(options.triggers_nowait, action.trigger) + end +end + +local labels = { + ["^"] = "Last position of cursor in insert mode", + ["."] = "Last change in current buffer", + ['"'] = "Last exited current buffer", + ["0"] = "In last file edited", + ["'"] = "Back to line in current buffer where jumped from", + ["`"] = "Back to position in current buffer where jumped from", + ["["] = "To beginning of previously changed or yanked text", + ["]"] = "To end of previously changed or yanked text", + [""] = "To beginning of last visual selection", + [">"] = "To end of last visual selection", +} + +---@type Plugin +---@return PluginItem[] +function M.run(_trigger, _mode, buf) + local items = {} + + local marks = {} + for _, mark in pairs(vim.fn.getmarklist(buf)) do + table.insert(marks, mark) + end + for _, mark in pairs(vim.fn.getmarklist()) do + table.insert(marks, mark) + end + + for _, mark in pairs(marks) do + local key = mark.mark:sub(2, 2) + if key == "<" then + key = "" + end + local lnum = mark.pos[2] + + local line + if mark.pos[1] and mark.pos[1] ~= 0 then + local lines = vim.fn.getbufline(mark.pos[1], lnum) + if lines and lines[1] then + line = lines[1] + end + end + + local file = mark.file and vim.fn.fnamemodify(mark.file, ":p:.") + + local value = string.format("%4d ", lnum) + + table.insert(items, { + key = key, + label = labels[key] or "", + value = value .. (line or file or ""), + highlights = { { 1, #value - 1, "Number" } }, + }) + end + return items +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/init.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/init.lua new file mode 100644 index 0000000..c255c7d --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/init.lua @@ -0,0 +1,107 @@ +local M = {} + +M.name = "presets" + +M.operators = { + d = "Delete", + c = "Change", + y = "Yank (copy)", + ["g~"] = "Toggle case", + ["gu"] = "Lowercase", + ["gU"] = "Uppercase", + [">"] = "Indent right", + [""] = "Indent left", + ["zf"] = "Create fold", + ["!"] = "Filter though external program", + ["v"] = "Visual Character Mode", + -- ["V"] = "Visual Line Mode", +} + +M.motions = { + ["h"] = "Left", + ["j"] = "Down", + ["k"] = "Up", + ["l"] = "Right", + ["w"] = "Next word", + ["%"] = "Matching character: '()', '{}', '[]'", + ["b"] = "Previous word", + ["e"] = "Next end of word", + ["ge"] = "Previous end of word", + ["0"] = "Start of line", + ["^"] = "Start of line (non-blank)", + ["$"] = "End of line", + ["f"] = "Move to next char", + ["F"] = "Move to previous char", + ["t"] = "Move before next char", + ["T"] = "Move before previous char", + ["gg"] = "First line", + ["G"] = "Last line", + ["{"] = "Previous empty line", + ["}"] = "Next empty line", +} + +M.objects = { + a = { name = "around" }, + i = { name = "inside" }, + ['a"'] = [[double quoted string]], + ["a'"] = [[single quoted string]], + ["a("] = [[same as ab]], + ["a)"] = [[same as ab]], + ["a"] = [[a <> from '<' to the matching '>']], + ["a>"] = [[same as a<]], + ["aB"] = [[a Block from [{ to ]} (with brackets)]], + ["aW"] = [[a WORD (with white space)]], + ["a["] = [[a [] from '[' to the matching ']']], + ["a]"] = [[same as a[]], + ["a`"] = [[string in backticks]], + ["ab"] = [[a block from [( to ]) (with braces)]], + ["ap"] = [[a paragraph (with white space)]], + ["as"] = [[a sentence (with white space)]], + ["at"] = [[a tag block (with white space)]], + ["aw"] = [[a word (with white space)]], + ["a{"] = [[same as aB]], + ["a}"] = [[same as aB]], + ['i"'] = [[double quoted string without the quotes]], + ["i'"] = [[single quoted string without the quotes]], + ["i("] = [[same as ib]], + ["i)"] = [[same as ib]], + ["i"] = [[inner <> from '<' to the matching '>']], + ["i>"] = [[same as i<]], + ["iB"] = [[inner Block from [{ and ]}]], + ["iW"] = [[inner WORD]], + ["i["] = [[inner [] from '[' to the matching ']']], + ["i]"] = [[same as i[]], + ["i`"] = [[string in backticks without the backticks]], + ["ib"] = [[inner block from [( to ])]], + ["ip"] = [[inner paragraph]], + ["is"] = [[inner sentence]], + ["it"] = [[inner tag block]], + ["iw"] = [[inner word]], + ["i{"] = [[same as iB]], + ["i}"] = [[same as iB]], +} + +---@param config Options +function M.setup(wk, opts, config) + require("which-key.plugins.presets.misc").setup(wk, opts) + + -- Operators + if opts.operators then + for op, label in pairs(M.operators) do + config.operators[op] = label + end + end + + -- Motions + if opts.motions then + wk.register(M.motions, { mode = "n", prefix = "", preset = true }) + wk.register(M.motions, { mode = "o", prefix = "", preset = true }) + end + + -- Text objects + if opts.text_objects then + wk.register(M.objects, { mode = "o", prefix = "", preset = true }) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/misc.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/misc.lua new file mode 100644 index 0000000..8b0bad2 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/presets/misc.lua @@ -0,0 +1,92 @@ +local M = {} + +M.name = "misc" + +local misc = { + windows = { + [""] = { + name = "window", + s = "Split window", + v = "Split window vertically", + w = "Switch windows", + q = "Quit a window", + T = "Break out into a new tab", + x = "Swap current with next", + ["-"] = "Decrease height", + ["+"] = "Increase height", + [""] = "Decrease width", + [">"] = "Increase width", + ["|"] = "Max out the width", + ["_"] = "Max out the height", + ["="] = "Equally high and wide", + h = "Go to the left window", + l = "Go to the right window", + k = "Go to the up window", + j = "Go to the down window", + }, + }, + z = { + ["z"] = { + o = "Open fold under cursor", + O = "Open all folds under cursor", + c = "Close fold under cursor", + C = "Close all folds under cursor", + a = "Toggle fold under cursor", + A = "Toggle all folds under cursor", + v = "Show cursor line", + M = "Close all folds", + R = "Open all folds", + m = "Fold more", + r = "Fold less", + x = "Update folds", + z = "Center this line", + t = "Top this line", + b = "Bottom this line", + g = "Add word to spell list", + w = "Mark word as bad/misspelling", + e = "Right this line", + s = "Left this line", + H = "Half screen to the left", + L = "Half screen to the right", + ["="] = "Spelling suggestions", + }, + }, + nav = { + ["[{"] = "Previous {", + ["[("] = "Previous (", + ["["] = "Previous <", + ["[m"] = "Previous method start", + ["[M"] = "Previous method end", + ["[%"] = "Previous unmatched group", + ["[s"] = "Previous misspelled word", + ["]{"] = "Next {", + ["]("] = "Next (", + ["]"] = "Next <", + ["]m"] = "Next method start", + ["]M"] = "Next method end", + ["]%"] = "Next unmatched group", + ["]s"] = "Next misspelled word", + ["H"] = "Home line of window (top)", + ["M"] = "Middle line of window", + ["L"] = "Last line of window", + }, + g = { + ["gf"] = "Go to file under cursor", + ["gx"] = "Open the file under cursor with system app", + ["gi"] = "Move to the last insertion and INSERT", + ["gv"] = "Switch to VISUAL using last selection", + ["gn"] = "Search forwards and select", + ["gN"] = "Search backwards and select", + ["g%"] = "Cycle backwards through results", + }, +} + +function M.setup(wk, config) + for key, mappings in pairs(misc) do + if config[key] ~= false then + wk.register(mappings, { mode = "n", prefix = "", preset = true }) + end + end +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/registers.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/registers.lua new file mode 100644 index 0000000..3e5abee --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/registers.lua @@ -0,0 +1,58 @@ +---@type Plugin +local M = {} + +M.name = "registers" + +M.actions = { + { trigger = '"', mode = "n" }, + { trigger = '"', mode = "v" }, + { trigger = "@", mode = "n", delay = true }, + { trigger = "", mode = "i" }, + { trigger = "", mode = "c" }, +} + +function M.setup(_wk, _config, options) + for _, action in ipairs(M.actions) do + if not action.delay then + table.insert(options.triggers_nowait, action.trigger) + end + end +end + +M.registers = '*+"-:.%/#=_abcdefghijklmnopqrstuvwxyz0123456789' + +local labels = { + ['"'] = "last deleted, changed, or yanked content", + ["0"] = "last yank", + ["-"] = "deleted or changed content smaller than one line", + ["."] = "last inserted text", + ["%"] = "name of the current file", + [":"] = "most recent executed command", + ["#"] = "alternate buffer", + ["="] = "result of an expression", + ["+"] = "synchronized with the system clipboard", + ["*"] = "synchronized with the selection clipboard", + ["_"] = "black hole", + ["/"] = "last search pattern", +} + +---@type Plugin +---@return PluginItem[] +function M.run(_trigger, _mode, _buf) + local items = {} + + for i = 1, #M.registers, 1 do + local key = M.registers:sub(i, i) + local ok, value = pcall(vim.fn.getreg, key, 1) + if not ok then + value = "" + end + + if value ~= "" then + table.insert(items, { key = key, label = labels[key] or "", value = value }) + end + end + return items +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/spelling.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/spelling.lua new file mode 100644 index 0000000..0a7b901 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/plugins/spelling.lua @@ -0,0 +1,52 @@ +local M = {} + +M.name = "spelling" + +M.actions = { { trigger = "z=", mode = "n" } } + +M.opts = {} + +function M.setup(_, config, options) + table.insert(options.triggers_nowait, "z=") + M.opts = config +end + +---@type Plugin +---@return PluginItem[] +function M.run() + -- if started with a count, let the default keybinding work + local count = vim.api.nvim_get_vvar("count") + if count and count > 0 then + return {} + end + + ---@diagnostic disable-next-line: missing-parameter + local cursor_word = vim.fn.expand("") + -- get a misspellled word from under the cursor, if not found, then use the cursor_word instead + ---@diagnostic disable-next-line: redundant-parameter + local bad = vim.fn.spellbadword(cursor_word) + local word = bad[1] + if word == "" then + word = cursor_word + end + + local suggestions = vim.fn.spellsuggest(word, M.opts.suggestions or 20, bad[2] == "caps" and 1 or 0) + + local items = {} + local keys = "1234567890abcdefghijklmnopqrstuvwxyz" + + for i, label in ipairs(suggestions) do + local key = keys:sub(i, i) + + table.insert(items, { + key = key, + label = label, + fn = function() + vim.cmd("norm! ciw" .. label) + end, + }) + end + return items +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/text.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/text.lua new file mode 100644 index 0000000..f7b120f --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/text.lua @@ -0,0 +1,72 @@ +---@class Highlight +---@field group string +---@field line number +---@field from number +---@field to number + +---@class Text +---@field lines string[] +---@field hl Highlight[] +---@field lineNr number +---@field current string +local Text = {} +Text.__index = Text + +function Text.len(str) + return vim.fn.strwidth(str) +end + +function Text:new() + local this = { lines = {}, hl = {}, lineNr = 0, current = "" } + setmetatable(this, self) + return this +end + +function Text:fix_nl(line) + return line:gsub("[\n]", "﬋") +end + +function Text:nl() + local line = self:fix_nl(self.current) + table.insert(self.lines, line) + self.current = "" + self.lineNr = self.lineNr + 1 +end + +function Text:set(row, col, str, group) + str = self:fix_nl(str) + + -- extend lines if needed + for i = 1, row, 1 do + if not self.lines[i] then + self.lines[i] = "" + end + end + + -- extend columns when needed + local width = Text.len(self.lines[row]) + if width < col then + self.lines[row] = self.lines[row] .. string.rep(" ", col - width) + end + + local before = vim.fn.strcharpart(self.lines[row], 0, col) + local after = vim.fn.strcharpart(self.lines[row], col) + self.lines[row] = before .. str .. after + + if not group then + return + end + -- set highlights + self:highlight(row, col, col + Text.len(str), "WhichKey" .. group) +end + +function Text:highlight(row, from, to, group) + local line = self.lines[row] + local before = vim.fn.strcharpart(line, 0, from) + local str = vim.fn.strcharpart(line, 0, to) + from = vim.fn.strlen(before) + to = vim.fn.strlen(str) + table.insert(self.hl, { line = row - 1, from = from, to = to, group = group }) +end + +return Text diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/tree.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/tree.lua new file mode 100644 index 0000000..d337b5a --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/tree.lua @@ -0,0 +1,95 @@ +local Util = require("which-key.util") + +---@class Tree +---@field root Node +local Tree = {} +Tree.__index = Tree + +---@class Node +---@field mapping Mapping +---@field prefix_i string +---@field prefix_n string +---@field children table +-- selene: allow(unused_variable) +local Node + +---@return Tree +function Tree:new() + local this = { root = { children = {}, prefix_i = "", prefix_n = "" } } + setmetatable(this, self) + return this +end + +---@param prefix_i string +---@param index? number defaults to last. If < 0, then offset from last +---@param plugin_context? any +---@return Node? +function Tree:get(prefix_i, index, plugin_context) + local prefix = Util.parse_internal(prefix_i) + local node = self.root + index = index or #prefix + if index < 0 then + index = #prefix + index + end + for i = 1, index, 1 do + node = node.children[prefix[i]] + if node and plugin_context and node.mapping and node.mapping.plugin then + local children = require("which-key.plugins").invoke(node.mapping, plugin_context) + node.children = {} + for _, child in pairs(children) do + self:add(child) + end + end + if not node then + return nil + end + end + return node +end + +-- Returns the path (possibly incomplete) for the prefix +---@param prefix_i string +---@return Node[] +function Tree:path(prefix_i) + local prefix = Util.parse_internal(prefix_i) + local node = self.root + local path = {} + for i = 1, #prefix, 1 do + node = node.children[prefix[i]] + table.insert(path, node) + if not node then + break + end + end + return path +end + +---@param mapping Mapping +function Tree:add(mapping) + local prefix_i = mapping.keys.internal + local prefix_n = mapping.keys.notation + local node = self.root + local path_i = "" + local path_n = "" + for i = 1, #prefix_i, 1 do + path_i = path_i .. prefix_i[i] + path_n = path_n .. prefix_n[i] + if not node.children[prefix_i[i]] then + node.children[prefix_i[i]] = { children = {}, prefix_i = path_i, prefix_n = path_n } + end + node = node.children[prefix_i[i]] + end + node.mapping = vim.tbl_deep_extend("force", node.mapping or {}, mapping) +end + +---@param cb fun(node:Node) +---@param node? Node +function Tree:walk(cb, node) + node = node or self.root + cb(node) + for _, child in pairs(node.children) do + self:walk(cb, child) + end +end + +return Tree diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/types.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/types.lua new file mode 100644 index 0000000..78671c7 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/types.lua @@ -0,0 +1,74 @@ +---@meta + +--# selene: allow(unused_variable) + +---@class Keymap +---@field rhs string +---@field lhs string +---@field buffer number +---@field expr number +---@field lnum number +---@field mode string +---@field noremap number +---@field nowait number +---@field script number +---@field sid number +---@field silent number +---@field callback fun()|nil +---@field id string terminal keycodes for lhs +---@field desc string + +---@class KeyCodes +---@field keys string +---@field internal string[] +---@field notation string[] + +---@class MappingOptions +---@field noremap boolean +---@field silent boolean +---@field nowait boolean +---@field expr boolean + +---@class Mapping +---@field buf number +---@field group boolean +---@field label string +---@field desc string +---@field prefix string +---@field cmd string +---@field opts MappingOptions +---@field keys KeyCodes +---@field mode? string +---@field callback fun()|nil +---@field preset boolean +---@field plugin string +---@field fn fun() + +---@class MappingTree +---@field mode string +---@field buf? number +---@field tree Tree + +---@class VisualMapping : Mapping +---@field key string +---@field highlights table +---@field value string + +---@class PluginItem +---@field key string +---@field label string +---@field value string +---@field cmd string +---@field highlights table + +---@class PluginAction +---@field trigger string +---@field mode string +---@field label? string +---@field delay? boolean + +---@class Plugin +---@field name string +---@field actions PluginAction[] +---@field run fun(trigger:string, mode:string, buf:number):PluginItem[] +---@field setup fun(wk, opts, Options) diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/util.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/util.lua new file mode 100644 index 0000000..5f02c16 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/util.lua @@ -0,0 +1,158 @@ +---@class Util +local M = {} + +function M.count(tab) + local ret = 0 + for _, _ in pairs(tab) do + ret = ret + 1 + end + return ret +end + +function M.get_mode() + local mode = vim.api.nvim_get_mode().mode + mode = mode:gsub(M.t(""), "v") + mode = mode:gsub(M.t(""), "s") + return mode:lower() +end + +function M.is_empty(tab) + return M.count(tab) == 0 +end + +function M.t(str) + -- https://github.com/neovim/neovim/issues/17369 + local ret = vim.api.nvim_replace_termcodes(str, false, true, true):gsub("\128\254X", "\128") + return ret +end + +-- stylua: ignore start +local utf8len_tab = { + -- ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9 ?A ?B ?C ?D ?E ?F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 0? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 1? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 2? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 3? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 4? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 5? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 6? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 7? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 8? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- 9? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- A? + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -- B? + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- C? + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -- D? + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, -- E? + 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1, -- F? +} +-- stylua: ignore end + +---@return KeyCodes +function M.parse_keys(keystr) + local keys = {} + local cur = "" + local todo = 1 + local special = nil + for i = 1, #keystr, 1 do + local c = keystr:sub(i, i) + if special then + if todo == 0 then + if c == ">" then + table.insert(keys, special .. ">") + cur = "" + todo = 1 + special = nil + elseif c == "-" then + -- When getting a special key notation: + -- todo = 0 means it can be ended by a ">" now. + -- todo = 1 means ">" should be treated as the modified character. + todo = 1 + end + else + todo = 0 + end + if special then + special = special .. c + end + elseif c == "<" then + special = "<" + todo = 0 + else + if todo == 1 then + todo = utf8len_tab[c:byte() + 1] + end + cur = cur .. c + todo = todo - 1 + if todo == 0 then + table.insert(keys, cur) + cur = "" + todo = 1 + end + end + end + local ret = { keys = M.t(keystr), internal = {}, notation = {} } + for i, key in pairs(keys) do + if key == " " then + key = "" + end + if i == 1 and vim.g.mapleader and M.t(key) == M.t(vim.g.mapleader) then + key = "" + end + table.insert(ret.internal, M.t(key)) + table.insert(ret.notation, key) + end + return ret +end + +-- @return string[] +function M.parse_internal(keystr) + local keys = {} + local cur = "" + local todo = 1 + local utf8 = false + for i = 1, #keystr, 1 do + local c = keystr:sub(i, i) + if not utf8 then + if todo == 1 and c == "\128" then + -- K_SPECIAL: get 3 bytes + todo = 3 + elseif cur == "\128" and c == "\252" then + -- K_SPECIAL KS_MODIFIER: repeat after getting 3 bytes + todo = todo + 1 + elseif todo == 1 then + -- When the second byte of a K_SPECIAL sequence is not KS_MODIFIER, + -- the third byte is guaranteed to be between 0x02 and 0x7f. + todo = utf8len_tab[c:byte() + 1] + utf8 = todo > 1 + end + end + cur = cur .. c + todo = todo - 1 + if todo == 0 then + table.insert(keys, cur) + cur = "" + todo = 1 + utf8 = false + end + end + return keys +end + +function M.warn(msg) + vim.notify(msg, vim.log.levels.WARN, { title = "WhichKey" }) +end + +function M.error(msg) + vim.notify(msg, vim.log.levels.ERROR, { title = "WhichKey" }) +end + +function M.check_mode(mode, buf) + if not ("nvsxoiRct"):find(mode) then + M.error(string.format("Invalid mode %q for buf %d", mode, buf or 0)) + return false + end + return true +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/view.lua b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/view.lua new file mode 100644 index 0000000..fe501b7 --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/lua/which-key/view.lua @@ -0,0 +1,341 @@ +local Keys = require("which-key.keys") +local config = require("which-key.config") +local Layout = require("which-key.layout") +local Util = require("which-key.util") + +local highlight = vim.api.nvim_buf_add_highlight + +---@class View +local M = {} + +M.keys = "" +M.mode = "n" +M.reg = nil +M.auto = false +M.count = 0 +M.buf = nil +M.win = nil +M.is_visual_multi_mode = nil + +function M.is_valid() + return M.buf + and M.win + and vim.api.nvim_buf_is_valid(M.buf) + and vim.api.nvim_buf_is_loaded(M.buf) + and vim.api.nvim_win_is_valid(M.win) +end + +function M.show() + M.is_visual_multi_mod = vim.b.visual_multi + if M.is_valid() then + return + end + local opts = { + relative = "editor", + width = vim.o.columns + - config.options.window.margin[2] + - config.options.window.margin[4] + - (vim.fn.has("nvim-0.6") == 0 and config.options.window.border ~= "none" and 2 or 0), + height = config.options.layout.height.min, + focusable = false, + anchor = "SW", + border = config.options.window.border, + row = vim.o.lines + - config.options.window.margin[3] + - (vim.fn.has("nvim-0.6") == 0 and config.options.window.border ~= "none" and 2 or 0) + - vim.o.cmdheight, + col = config.options.window.margin[2], + style = "minimal", + noautocmd = true, + } + if config.options.window.position == "top" then + opts.anchor = "NW" + opts.row = config.options.window.margin[1] + end + M.buf = vim.api.nvim_create_buf(false, true) + M.win = vim.api.nvim_open_win(M.buf, false, opts) + vim.api.nvim_buf_set_option(M.buf, "filetype", "WhichKey") + vim.api.nvim_buf_set_option(M.buf, "buftype", "nofile") + vim.api.nvim_buf_set_option(M.buf, "bufhidden", "wipe") + + local winhl = "NormalFloat:WhichKeyFloat" + if vim.fn.hlexists("FloatBorder") == 1 then + winhl = winhl .. ",FloatBorder:WhichKeyBorder" + end + vim.api.nvim_win_set_option(M.win, "winhighlight", winhl) + vim.api.nvim_win_set_option(M.win, "foldmethod", "manual") + vim.api.nvim_win_set_option(M.win, "winblend", config.options.window.winblend) +end + +function M.read_pending() + local esc = "" + while true do + local n = vim.fn.getchar(0) + if n == 0 then + break + end + local c = (type(n) == "number" and vim.fn.nr2char(n) or n) + + -- HACK: for some reason, when executing a :norm command, + -- vim keeps feeding at the end + if c == Util.t("") then + esc = esc .. c + -- more than 10 in a row? most likely the norm bug + if #esc > 10 then + return + end + else + -- we have characters, so add them to keys + if esc ~= "" then + M.keys = M.keys .. esc + esc = "" + end + M.keys = M.keys .. c + end + end + if esc ~= "" then + M.keys = M.keys .. esc + esc = "" + end +end + +function M.getchar() + local ok, n = pcall(vim.fn.getchar) + + -- bail out on keyboard interrupt + if not ok then + return Util.t("") + end + + local c = (type(n) == "number" and vim.fn.nr2char(n) or n) + return c +end + +function M.scroll(up) + local height = vim.api.nvim_win_get_height(M.win) + local cursor = vim.api.nvim_win_get_cursor(M.win) + if up then + cursor[1] = math.max(cursor[1] - height, 1) + else + cursor[1] = math.min(cursor[1] + height, vim.api.nvim_buf_line_count(M.buf)) + end + vim.api.nvim_win_set_cursor(M.win, cursor) +end + +function M.on_close() + M.hide() +end + +function M.hide() + vim.api.nvim_echo({ { "" } }, false, {}) + M.hide_cursor() + if M.buf and vim.api.nvim_buf_is_valid(M.buf) then + vim.api.nvim_buf_delete(M.buf, { force = true }) + M.buf = nil + end + if M.win and vim.api.nvim_win_is_valid(M.win) then + vim.api.nvim_win_close(M.win, true) + M.win = nil + end + if M.is_visual_multi_mod then + M.is_visual_multi_mod = false + vim.cmd([[normal \\gS]]) -- reselect visual-multi text + end + vim.cmd("redraw") +end + +function M.show_cursor() + local buf = vim.api.nvim_get_current_buf() + local cursor = vim.api.nvim_win_get_cursor(0) + vim.api.nvim_buf_add_highlight(buf, config.namespace, "Cursor", cursor[1] - 1, cursor[2], cursor[2] + 1) +end + +function M.hide_cursor() + local buf = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_clear_namespace(buf, config.namespace, 0, -1) +end + +function M.back() + local node = Keys.get_tree(M.mode, M.buf).tree:get(M.keys, -1) or Keys.get_tree(M.mode).tree:get(M.keys, -1) + if node then + M.keys = node.prefix_i + end +end + +---@param path Node[] +function M.has_cmd(path) + for _, node in pairs(path) do + if node.mapping and node.mapping.cmd then + return true + end + end + return false +end + +function M.execute(prefix_i, mode, buf) + local global_node = Keys.get_tree(mode).tree:get(prefix_i) + local buf_node = buf and Keys.get_tree(mode, buf).tree:get(prefix_i) or nil + + if global_node and global_node.mapping and Keys.is_hook(prefix_i, global_node.mapping.cmd) then + return + end + if buf_node and buf_node.mapping and Keys.is_hook(prefix_i, buf_node.mapping.cmd) then + return + end + + local hooks = {} + + local function unhook(nodes, nodes_buf) + for _, node in pairs(nodes) do + if Keys.is_hooked(node.mapping.prefix, mode, nodes_buf) then + table.insert(hooks, { node.mapping.prefix, nodes_buf }) + Keys.hook_del(node.mapping.prefix, mode, nodes_buf) + end + end + end + + -- make sure we remove all WK hooks before executing the sequence + -- this is to make existing keybindongs work and prevent recursion + unhook(Keys.get_tree(mode).tree:path(prefix_i)) + if buf then + unhook(Keys.get_tree(mode, buf).tree:path(prefix_i), buf) + end + + -- feed CTRL-O again if called from CTRL-O + local full_mode = Util.get_mode() + if full_mode == "nii" or full_mode == "nir" or full_mode == "niv" or full_mode == "vs" then + vim.api.nvim_feedkeys(Util.t(""), "n", false) + end + + -- handle registers that were passed when opening the popup + if M.reg ~= '"' and M.mode ~= "i" and M.mode ~= "c" then + vim.api.nvim_feedkeys('"' .. M.reg, "n", false) + end + + if M.count and M.count ~= 0 then + prefix_i = M.count .. prefix_i + end + + -- feed the keys with remap + vim.api.nvim_feedkeys(prefix_i, "m", true) + + -- defer hooking WK until after the keys were executed + vim.defer_fn(function() + for _, hook in pairs(hooks) do + Keys.hook_add(hook[1], mode, hook[2]) + end + end, 0) +end + +function M.open(keys, opts) + opts = opts or {} + M.keys = keys or "" + M.mode = opts.mode or Util.get_mode() + M.count = vim.api.nvim_get_vvar("count") + M.reg = vim.api.nvim_get_vvar("register") + + if string.find(vim.o.clipboard, "unnamedplus") and M.reg == "+" then + M.reg = '"' + end + + if string.find(vim.o.clipboard, "unnamed") and M.reg == "*" then + M.reg = '"' + end + + M.show_cursor() + M.on_keys(opts) +end + +function M.is_enabled(buf) + local buftype = vim.api.nvim_buf_get_option(buf, "buftype") + for _, bt in ipairs(config.options.disable.buftypes) do + if bt == buftype then + return false + end + end + + local filetype = vim.api.nvim_buf_get_option(buf, "filetype") + for _, bt in ipairs(config.options.disable.filetypes) do + if bt == filetype then + return false + end + end + + return true +end + +function M.on_keys(opts) + local buf = vim.api.nvim_get_current_buf() + + while true do + -- loop + M.read_pending() + + local results = Keys.get_mappings(M.mode, M.keys, buf) + + --- Check for an exact match. Feedkeys with remap + if results.mapping and not results.mapping.group and #results.mappings == 0 then + M.hide() + if results.mapping.fn then + results.mapping.fn() + else + M.execute(M.keys, M.mode, buf) + end + return + end + + -- Check for no mappings found. Feedkeys without remap + if #results.mappings == 0 then + M.hide() + -- only execute if an actual key was typed while WK was open + if opts.auto then + M.execute(M.keys, M.mode, buf) + end + return + end + + local layout = Layout:new(results) + + if M.is_enabled(buf) then + if not M.is_valid() then + M.show() + end + + M.render(layout:layout(M.win)) + end + vim.cmd([[redraw]]) + + local c = M.getchar() + + if c == Util.t("") then + M.hide() + break + elseif c == Util.t(config.options.popup_mappings.scroll_down) then + M.scroll(false) + elseif c == Util.t(config.options.popup_mappings.scroll_up) then + M.scroll(true) + elseif c == Util.t("") then + M.back() + else + M.keys = M.keys .. c + end + end +end + +---@param text Text +function M.render(text) + vim.api.nvim_buf_set_lines(M.buf, 0, -1, false, text.lines) + local height = #text.lines + if height > config.options.layout.height.max then + height = config.options.layout.height.max + end + vim.api.nvim_win_set_height(M.win, height) + if vim.api.nvim_buf_is_valid(M.buf) then + vim.api.nvim_buf_clear_namespace(M.buf, config.namespace, 0, -1) + end + for _, data in ipairs(text.hl) do + highlight(M.buf, config.namespace, data.group, data.line, data.from, data.to) + end +end + +return M diff --git a/etc/soft/nvim/+plugins/which-key.nvim/plugin/which-key.vim b/etc/soft/nvim/+plugins/which-key.nvim/plugin/which-key.vim new file mode 100644 index 0000000..2d14bfa --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/plugin/which-key.vim @@ -0,0 +1 @@ +command! -nargs=* WhichKey lua require('which-key').show_command() diff --git a/etc/soft/nvim/+plugins/which-key.nvim/selene.toml b/etc/soft/nvim/+plugins/which-key.nvim/selene.toml new file mode 100644 index 0000000..6540d6f --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/selene.toml @@ -0,0 +1 @@ +std="lua51+vim" diff --git a/etc/soft/nvim/+plugins/which-key.nvim/stylua.toml b/etc/soft/nvim/+plugins/which-key.nvim/stylua.toml new file mode 100644 index 0000000..5d6c50d --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/stylua.toml @@ -0,0 +1,3 @@ +indent_type = "Spaces" +indent_width = 2 +column_width = 120 \ No newline at end of file diff --git a/etc/soft/nvim/+plugins/which-key.nvim/vim.toml b/etc/soft/nvim/+plugins/which-key.nvim/vim.toml new file mode 100644 index 0000000..0fa5c4f --- /dev/null +++ b/etc/soft/nvim/+plugins/which-key.nvim/vim.toml @@ -0,0 +1,2 @@ +[vim] +any = true diff --git a/etc/soft/nvim/+plugins/yankring/plugin/yankring.vim b/etc/soft/nvim/+plugins/yankring/plugin/yankring.vim deleted file mode 100644 index 6f5d999..0000000 --- a/etc/soft/nvim/+plugins/yankring/plugin/yankring.vim +++ /dev/null @@ -1,2880 +0,0 @@ -" yankring.vim - Yank / Delete Ring for Vim -" --------------------------------------------------------------- -" Version: 17.0 -" Author: David Fishburn -" Maintainer: David Fishburn -" Last Modified: 2013 Apr 28 -" Script: http://www.vim.org/scripts/script.php?script_id=1234 -" Based On: Mocked up version by Yegappan Lakshmanan -" http://groups.yahoo.com/group/vim/post?act=reply&messageNum=34406 -" License: GPL (Gnu Public License) -" GetLatestVimScripts: 1234 1 :AutoInstall: yankring.vim - -if exists('loaded_yankring') - finish -endif - -if v:version < 700 - echomsg 'yankring: You need at least Vim 7.0' - finish -endif - -let loaded_yankring = 170 - -" Turn on support for line continuations when creating the script -let s:cpo_save = &cpo -set cpo&vim - -let s:yr_has_voperator = 0 -if v:version > 701 || ( v:version == 701 && has("patch205") ) - let s:yr_has_voperator = 1 -endif - -if !exists('g:yankring_history_dir') - let g:yankring_history_dir = expand('$HOME') -else - " let g:yankring_history_dir = expand(g:yankring_history_dir) - for dir in split(g:yankring_history_dir, ",") - if isdirectory(expand(dir)) - let g:yankring_history_dir = expand(dir) - break - endif - endfor -endif - -if !exists('g:yankring_history_file') - let g:yankring_history_file = 'yankring_history' -endif - -" Allow the user to override the # of yanks/deletes recorded -if !exists('g:yankring_max_history') - let g:yankring_max_history = 100 -elseif g:yankring_max_history < 0 - let g:yankring_max_history = 100 -endif - -" Specify the minimum length of 1 entry -if !exists('g:yankring_min_element_length') - let g:yankring_min_element_length = 1 -endif - -" Specify the maximum length of 1 entry (1MB default) -if !exists('g:yankring_max_element_length') - let g:yankring_max_element_length = 1048576 -endif - -" Warn if truncation occurs -if !exists('g:yankring_warn_on_truncate') - let g:yankring_warn_on_truncate = 1 -endif - -" Allow the user to specify if the plugin is enabled or not -if !exists('g:yankring_enabled') - let g:yankring_enabled = 1 -endif - -" Specify max display length for each element for YRShow -if !exists('g:yankring_max_display') - let g:yankring_max_display = 0 -endif - -" Check if yankring should persist between Vim instances -if !exists('g:yankring_persist') - let g:yankring_persist = 1 -endif - -" Check if yankring share 1 file between all instances of Vim -if !exists('g:yankring_share_between_instances') - let g:yankring_share_between_instances = 1 -endif - -" Specify whether the results of the ring should be displayed -" in a separate buffer window instead of the use of echo -if !exists('g:yankring_window_use_separate') - let g:yankring_window_use_separate = 1 -endif - -" Specifies whether the window is closed after an action -" is performed -if !exists('g:yankring_window_auto_close') - let g:yankring_window_auto_close = 1 -endif - -" When displaying the buffer, how many lines should it be -if !exists('g:yankring_window_height') - let g:yankring_window_height = 8 -endif - -" When displaying the buffer, how many columns should it be -if !exists('g:yankring_window_width') - let g:yankring_window_width = 30 -endif - -" When displaying the buffer, where it should be placed -if !exists('g:yankring_window_use_horiz') - let g:yankring_window_use_horiz = 1 -endif - -" When displaying the buffer, where it should be placed -if !exists('g:yankring_window_use_bottom') - let g:yankring_window_use_bottom = 1 -endif - -" When displaying the buffer, where it should be placed -if !exists('g:yankring_window_use_right') - let g:yankring_window_use_right = 1 -endif - -" If the user presses , toggle the width of the window -if !exists('g:yankring_window_increment') - let g:yankring_window_increment = 50 -endif - -" Controls whether the . operator will repeat yank operations -" The default is based on cpoptions: |cpo-y| -" y A yank command can be redone with ".". -if !exists('g:yankring_dot_repeat_yank') - let g:yankring_dot_repeat_yank = (s:cpo_save=~'y'?1:0) -endif - -" Only adds unique items to the yankring. -" If the item already exists, that element is set as the -" top of the yankring. -if !exists('g:yankring_ignore_duplicate') - let g:yankring_ignore_duplicate = 1 -endif - -" Determine whether to record inserted data -if !exists('g:yankring_record_insert') - let g:yankring_record_insert = 0 -endif - -" Vim automatically manages the numbered registers: -" 0 - last yanked text -" 1-9 - last deleted items -" If this option is turned on, the yankring will manage the -" values in them. -if !exists('g:yankring_manage_numbered_reg') - let g:yankring_manage_numbered_reg = 0 -endif - -" Allow the user to specify what characters to use for the mappings. -if !exists('g:yankring_n_keys') - " 7.1.patch205 introduces the v:operator function which was essential - " to gain the omap support. - if s:yr_has_voperator == 1 - " Use omaps for the rest of the functionality - let g:yankring_n_keys = 'Y D x X' - else - let g:yankring_n_keys = 'x yy dd yw dw ye de yE dE yiw diw yaw daw y$ d$ Y D yG dG ygg dgg' - endif -endif - -" Allow the user to specify what operator pending motions to map -if !exists('g:yankring_o_keys') - " o-motions and text objects, without zap-to-char motions - let g:yankring_o_keys = 'b B w W e E d h j k l H M L y G ^ 0 $ , ;' - let g:yankring_o_keys .= ' g_ g^ gm g$ gk gj gg ge gE - + _ ' - let g:yankring_o_keys .= ' iw iW aw aW as is ap ip a] a[ i] i[ a) a( ab i) i( ib a> a< i> i< at it a} a{ aB i} i{ iB a" a'' a` i" i'' i`' -endif - -if !exists('g:yankring_zap_keys') - let g:yankring_zap_keys = 'f F t T / ?' -endif - -" Allow the user to specify what operator pending motions to map -if !exists('g:yankring_ignore_operator') - let g:yankring_ignore_operator = 'g~ gu gU ! = gq g? > < zf g@' -endif -let g:yankring_ignore_operator = ' '.g:yankring_ignore_operator.' ' - -" Whether we should map the . operator -if !exists('g:yankring_map_dot') - let g:yankring_map_dot = 1 -endif - -" Whether we sould map the "g" paste operators -if !exists('g:yankring_paste_using_g') - let g:yankring_paste_using_g = 1 -endif - -if !exists('g:yankring_v_key') - let g:yankring_v_key = 'y' -endif - -if !exists('g:yankring_del_v_key') - let g:yankring_del_v_key = 'd x' -endif - -if !exists('g:yankring_paste_n_bkey') - let g:yankring_paste_n_bkey = 'P' -endif - -if !exists('g:yankring_paste_n_akey') - let g:yankring_paste_n_akey = 'p' -endif - -if !exists('g:yankring_paste_v_bkey') - let g:yankring_paste_v_bkey = 'P' -endif - -if !exists('g:yankring_paste_v_akey') - let g:yankring_paste_v_akey = 'p' -endif - -if exists('g:yankring_paste_check_default_buffer') - let g:yankring_paste_check_default_register = g:yankring_paste_check_default_buffer -endif - -if !exists('g:yankring_paste_check_default_register') - let g:yankring_paste_check_default_register = 1 -endif - -if !exists('g:yankring_replace_n_pkey') - let g:yankring_replace_n_pkey = '' -endif - -if !exists('g:yankring_replace_n_nkey') - let g:yankring_replace_n_nkey = '' -endif - -if !exists('g:yankring_clipboard_monitor') - let g:yankring_clipboard_monitor = (has('clipboard')?1:0) -endif - -if !exists('g:yankring_manual_clipboard_check') - let g:yankring_manual_clipboard_check = 0 - if g:yankring_clipboard_monitor == 1 - if has("gui_running") - " FocusGained event will take care of - " monitoring the clipboard. - let g:yankring_manual_clipboard_check = 0 - else - " If the GUI is not running and the user wants to monitor the - " clipboard, we need to manually check for clipboard entries as - " the FocusGained event does not fire in console mode. - let g:yankring_manual_clipboard_check = 1 - endif - endif -endif - -if !exists('g:yankring_default_menu_mode') - let g:yankring_default_menu_mode = 3 -endif - -" Script variables for the yankring buffer -let s:yr_buffer_name = '[YankRing]' -let s:yr_buffer_last_winnr = -1 -let s:yr_buffer_last = -1 -let s:yr_buffer_id = -1 -let s:yr_search = '' -let s:yr_remove_omap_dot = 0 -let s:yr_history_version = 'v2' -let s:yr_history_v1_nl = '@@@' -let s:yr_history_v1_nl_pat = '\%(\\\)\@ 0 - let new_state = ((a:1 == 1) ? 1 : 0) - endif - - " YRToggle accepts an integer value to specify the state - if new_state == g:yankring_enabled - return - elseif new_state == 1 - call s:YRMapsCreate() - call s:YRWarningMsg('YR: The YankRing is now enabled') - else - call s:YRMapsDelete() - call s:YRWarningMsg('YR: The YankRing is now disabled') - endif -endfunction - -" Enables or disables the yankring -function! s:YRDisplayElem(disp_nbr, script_var) - if g:yankring_max_display == 0 - if g:yankring_window_use_separate == 1 - let max_display = 500 - else - let max_display = g:yankring_window_width + - \ g:yankring_window_increment - - \ 12 - endif - else - let max_display = g:yankring_max_display - endif - - let elem = matchstr(a:script_var, '^.*\ze,.*$') - if s:yr_history_version == 'v1' - " v1 - " let elem = substitute(elem, '\%(\\\)\@max_display)? - \ (strpart(elem,0,max_display). - \ '...'): - \ elem - \ ) - \ ) - - return "" -endfunction - -" Enables or disables the yankring -function! s:YRShow(...) - " Prevent recursion - if exists('s:yankring_showing') && s:yankring_showing == 1 - " call s:YRWarningMsg('YR: YRShow aborting for recursion') - return - endif - - if g:yankring_enabled == 0 - call s:YRWarningMsg('YR: The YankRing is disabled, use YRToggle to re-enable') - return - endif - - " Prevent recursion - let s:yankring_showing = 1 - - " If the GUI is not running, we need to manually check for clipboard - " entries as the FocusGained event does not fire in console mode. - if g:yankring_manual_clipboard_check == 1 - call s:YRCheckClipboard() - endif - - " If no parameter was provided assume the user wants to - " toggle the display. - let toggle = 1 - if a:0 > 0 - let toggle = matchstr(a:1, '\d\+') - endif - - let show_registers = 0 - if a:0 > 1 && a:2 ==# 'R' - let show_registers = 1 - endif - - if toggle == 1 - if bufwinnr(s:yr_buffer_id) > -1 - " If the YankRing window is already open close it - exec bufwinnr(s:yr_buffer_id) . "wincmd w" - " Quit the YankRing - call s:YRWindowAction('q', 'n') - - " Switch back to the window which the YankRing - " window was opened from - if bufwinnr(s:yr_buffer_last) != -1 - " If the buffer is visible, switch to it - exec s:yr_buffer_last_winnr . "wincmd w" - endif - - " Prevent recursion - let s:yankring_showing = 0 - - return - endif - endif - - " Reset the search string, since this is automatically called - " if the yankring window is open. A previous search must be - " cleared since we do not want to show new items. The user can - " always run the search again. - let s:yr_search = "" - - " It is possible for registers to be changed outside of the - " maps of the YankRing. Perform this quick check when we - " show the contents (or when it is refreshed). - if g:yankring_paste_check_default_register == 1 - let save_reg = 0 - let register = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - - if &clipboard =~ '\' && getreg('*') != s:yr_prev_clipboard_star - let save_reg = 1 - endif - if has('unnamedplus') && &clipboard =~ '\' && getreg('+') != s:yr_prev_clipboard_plus - let save_reg = 1 - endif - if register == '"' && getreg('"') != s:yr_prev_reg_unnamed - let save_reg = 1 - endif - - if save_reg == 1 - " The user has performed a yank / delete operation - " outside of the yankring maps. Add this - " value to the yankring. - call YRRecord(register) - endif - endif - - " List is shown in order of replacement - " assuming using previous yanks - let output = "--- YankRing ---\n" - let output = output . (show_registers == 1 ? 'Reg ' : 'Elem')." Content\n" - - if show_registers == 1 - for reg_name in map( range(char2nr('0'), char2nr('9')) + - \ (range(char2nr('a'), char2nr('z'))) - \, 'nr2char(v:val)' - \ ) - let output = output . s:YRDisplayElem(reg_name, getreg(reg_name).',') . "\n" - endfor - else - call s:YRHistoryRead() - let disp_item_nr = 1 - for elem in s:yr_history_list - let output = output . s:YRDisplayElem(disp_item_nr, elem) . "\n" - let disp_item_nr += 1 - endfor - endif - - if g:yankring_window_use_separate == 1 - call s:YRWindowOpen(output) - else - echo output - endif - - " Prevent recursion - let s:yankring_showing = 0 -endfunction - - -" Used in omaps if a following character is required -" like with motions (f,t) -function! s:YRGetChar() - let msg = "YR:Enter character:" - " echomsg msg - echo msg - let c = getchar() - if c =~ '^\d\+$' - let c = nr2char(c) - " echomsg msg.c - echon c - endif - return c -endfunction - - -" Used in omaps if a following string is required -" like with motions (/,?) -" function! s:YRGetSearch() -" " let msg = "YR:Enter string:" -" " echomsg msg -" let str = input("YR:Enter string:") -" " let str = '' -" " while 1==1 -" " let c = getchar() -" " if c =~ '^\d\+$' -" " let c = nr2char(c) -" " if c == "\" -" " return c -" " endif -" " if c == "\" -" " break -" " endif -" " let str = str.c -" " echomsg msg.str -" " else -" " break -" " endif -" " endwhile -" return str -" endfunction - - -" Paste a certain item from the yankring -" If no parameter is provided, this function becomes interactive. It will -" display the list (using YRShow) and allow the user to choose an element. -function! s:YRGetElem(...) - if g:yankring_manual_clipboard_check == 1 - call s:YRCheckClipboard() - endif - - if s:yr_count == 0 - call s:YRWarningMsg('YR: yankring is empty') - return -1 - endif - - let default_buffer = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - - let direction = 'p' - if a:0 > 1 - " If the user indicated to paste above or below - " let direction = ((a:2 ==# 'P') ? 'P' : 'p') - if a:2 =~ '\(p\|gp\|P\|gP\)' - let direction = a:2 - endif - endif - - " Check to see if a specific value has been provided - let elem = 0 - if a:0 > 0 - " Ensure we get only the numeric value (trim it) - let elem = matchstr(a:1, '\d\+') - let elem = elem - 1 - else - " If no parameter was supplied display the yankring - " and prompt the user to enter the value they want pasted. - call s:YRShow(0) - - if g:yankring_window_use_separate == 1 - " The window buffer is used instead of command line - return - endif - - let elem = input("Enter # to paste:") - - " Ensure we get only the numeric value (trim it) - let elem = matchstr(elem, '\d\+') - - if elem == '' - " They most likely pressed enter without entering a value - return - endif - - let elem = elem - 1 - endif - - if elem < 0 || elem >= s:yr_count - call s:YRWarningMsg("YR: Invalid choice:".elem) - return -1 - endif - - let default_buffer = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - call setreg(default_buffer - \ , s:YRGetValElemNbr((elem), 'v') - \ , s:YRGetValElemNbr((elem), 't') - \ ) - exec 'normal! "'.default_buffer.direction - - " Set the previous action as a paste in case the user - " press . to repeat - call s:YRSetPrevOP('p', '', default_buffer, 'n') - -endfunction - - -" Starting the top of the ring it will paste x items from it -function! s:YRGetMultiple(reverse_order, ...) - if g:yankring_manual_clipboard_check == 1 - call s:YRCheckClipboard() - endif - - if s:yr_count == 0 - call s:YRWarningMsg('YR: yankring is empty') - return - endif - - let max = 1 - if a:0 == 1 - " If the user provided a range, exit after that many - " have been displayed - let max = matchstr(a:1, '\d\+') - endif - if max > s:yr_count - " Default to all items if they specified a very high value - let max = s:yr_count - endif - - " Base the increment on the sort order of the results - let increment = ((a:reverse_order==0)?(1):(-1)) - if a:reverse_order == 0 - let increment = 1 - let elem = 1 - else - let increment = -1 - let elem = max - endif - - if a:0 > 1 - let iter = 1 - while iter <= a:0 - let elem = (a:{iter} - 1) - call s:YRGetElem(elem) - let iter = iter + 1 - endwhile - else - while max > 0 - " Paste the first item, and move on to the next. - call s:YRGetElem(elem) - let elem = elem + increment - let max = max - 1 - endwhile - endif -endfunction - - -" Given a regular expression, check each element within -" the yankring, display only the matching items and prompt -" the user for which item to paste -function! s:YRSearch(...) - if s:yr_count == 0 - call s:YRWarningMsg('YR: yankring is empty') - return - endif - - let s:yr_search = "" - " If the user provided a range, exit after that many - " have been displayed - if a:0 == 0 || (a:0 == 1 && a:1 == "") - let s:yr_search = input('Enter [optional] regex:') - else - let s:yr_search = a:1 - endif - - if s:yr_search == "" - " Show the entire yankring - call s:YRShow(0) - return - endif - - " List is shown in order of replacement - " assuming using previous yanks - let output = "--- YankRing ---\n" - let output = output . "Elem Content\n" - let valid_choices = [] - - let search_result = filter(copy(s:yr_history_list), "v:val =~ '".s:yr_search."'") - - let disp_item_nr = 1 - - for elem in s:yr_history_list - if elem =~ s:yr_search - let output = output . s:YRDisplayElem(disp_item_nr, elem) . "\n" - call add(valid_choices, disp_item_nr.'') - endif - let disp_item_nr += 1 - endfor - - if empty(valid_choices) - let output = output . "Search for [".s:yr_search."] did not match any items " - endif - - if g:yankring_window_use_separate == 1 - call s:YRWindowOpen(output) - else - if !empty(valid_choices) - echo output - let elem = input("Enter # to paste:") - - " Ensure we get only the numeric value (trim it) - let elem = matchstr(elem, '\d\+') - - if elem == '' - " They most likely pressed enter without entering a value - return - endif - - if index(valid_choices, elem) != -1 - exec 'YRGetElem ' . elem - else - " User did not choose one of the elements that were found - " Remove leading , - call s:YRWarningMsg( "YR: Item[" . elem . "] not found, only valid choices are[" . - \ join(valid_choices, ',') . - \ "]" - \ ) - return -1 - endif - - else - call s:YRWarningMsg( "YR: The pattern [" . - \ s:yr_search . - \ "] does not match any items in the yankring" - \ ) - endif - endif - -endfunction - - -" Resets the common script variables for managing the ring. -function! s:YRReset() - call s:YRHistoryDelete() - " Update the history file - call s:YRHistorySave() -endfunction - - -" Clears the yankring by simply setting the # of items in it to 0. -" There is no need physically unlet each variable. -function! s:YRInit(...) - let s:yr_next_idx = 0 - let s:yr_last_paste_idx = 1 - let s:yr_count = 0 - let s:yr_history_last_upd = 0 - let s:yr_history_list = [] - let s:yr_paste_dir = 'p' - - " For the . op support - let s:yr_prev_op_code = '' - let s:yr_prev_op_mode = 'n' - let s:yr_prev_count = '' - let s:yr_prev_reg = '' - let s:yr_prev_reg_unnamed = getreg('"') - let s:yr_prev_reg_small = '' - let s:yr_prev_reg_insert = '' - let s:yr_prev_reg_expres = '' - let s:yr_prev_clipboard_star = '' - let s:yr_prev_clipboard_plus = '' - let s:yr_prev_vis_lstart = 0 - let s:yr_prev_vis_lend = 0 - let s:yr_prev_vis_cstart = 0 - let s:yr_prev_vis_cend = 0 - let s:yr_prev_changenr = 0 - let s:yr_prev_repeating = 0 - - " This is used to determine if the visual selection should be - " reset prior to issuing the YRReplace - let s:yr_prev_vis_mode = 0 - - if a:0 == 0 && g:yankring_persist != 1 - " The user wants the yankring reset each time Vim is started - call s:YRClear() - endif - - call s:YRHistoryRead() -endfunction - - -" Clears the yankring by simply setting the # of items in it to 0. -function! s:YRClear() - call s:YRReset() - call s:YRInit('DoNotClear') - - " If the yankring window is open, refresh it - call s:YRWindowUpdate() -endfunction - - -" Determine which register the user wants to use -" For example the 'a' register: "ayy -function! s:YRRegister() - " v:register can be blank in some (unknown) cases - " so test for this condition and return the - " default register - let user_register = ((v:register=='')?('"'):(v:register)) - let clipboard_default = matchstr( &clipboard, '\' ) - - " clipboard can have a comma separated list of values. - " Depending on which order the unnamed is referenced - " determines which register to use. - " set clipboard=unnamedplus,unnamed - " set clipboard=unnamed,unnamedplus - if clipboard_default == '\' && user_register == '"' - let user_register = '*' - endif - if has('unnamedplus') && clipboard_default == '\' && user_register == '"' - let user_register = '+' - endif - return user_register -endfunction - - -" Allows you to push a new item on the yankring. Useful if something -" is in the clipboard and you want to add it to the yankring. -" Or if you yank something that is not mapped. -function! s:YRPush(...) - let user_register = s:YRRegister() - - if a:0 > 0 - " If no yank command has been supplied, assume it is - " a full line yank - let user_register = ((a:1 == '') ? user_register : a:1) - endif - - " If we are pushing something on to the yankring, add it to - " the default buffer as well so the next item pasted will - " be the item pushed - let default_buffer = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - call setreg(default_buffer, getreg(user_register), - \ getregtype(user_register)) - - call s:YRSetPrevOP('', '', '', 'n') - call YRRecord(user_register) -endfunction - - -" Allows you to pop off any element from the yankring. -" If no parameters are provided the first element is removed. -" If a vcount is provided, that many elements are removed -" from the top. -function! s:YRPop(...) - if s:yr_count == 0 - call s:YRWarningMsg('YR: yankring is empty') - return - endif - - let v_count = 1 - if a:0 > 1 - let v_count = a:2 - endif - - " If the user provided a parameter, remove that element - " from the yankring. - " If no parameter was provided assume the first element. - let elem_index = 0 - if a:0 > 0 - " Get the element # from the parameter - let elem_index = matchstr(a:1, '\d\+') - let elem_index = elem_index - 1 - endif - - " If the user entered a count, then remove that many - " elements from the ring. - while v_count > 0 - call s:YRMRUDel('s:yr_history_list', elem_index) - let v_count = v_count - 1 - endwhile - - " If the yankring window is open, refresh it - call s:YRWindowUpdate() -endfunction - - -" Adds this value to the yankring. -function! YRRecord(...) - - let register = '"' - if a:0 > 0 - " If no yank command has been supplied, assume it is - " a full line yank - let register = ((a:1 == '') ? register : a:1) - endif - - " v:register can be blank in some (unknown) cases - " if v:register == '' || v:register == '_' - if v:register == '_' - " Black hole register, ignore recording the operation - return "" - endif - - let register = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':register)) - - " let s:yr_prev_changenr = changenr() - if register == '"' - " If the change has occurred via an omap, we must delay - " the capture of the default register until this event - " since register updates are not reflected until the - " omap function completes - let s:yr_prev_reg_unnamed = getreg('"') - let s:yr_prev_reg_small = getreg('-') - endif - - " Add item to list - " This will also account for duplicates. - call s:YRMRUAdd( 's:yr_history_list' - \ , getreg(register) - \ , getregtype(register) - \ ) - - if g:yankring_clipboard_monitor == 1 - let s:yr_prev_clipboard_plus = getreg('+') - let s:yr_prev_clipboard_star = getreg('*') - endif - - " Manage the numbered registers - if g:yankring_manage_numbered_reg == 1 - " Allow the user to define an autocmd to dynamically - " setup their connection information. - silent! doautocmd User YRSetNumberedReg - endif - - " If the yankring window is open, refresh it - call s:YRWindowUpdate() - - " Reset the past paste entry to the top of the ring. - " When the user hits replace last entry it should - " start from the top (not from the last previous - " replace) after capturing a new value in the YankRing. - let s:yr_last_paste_idx = 1 - - return "" -endfunction - - -" Adds this value to the yankring. -function! YRRecord3(...) - let register = '"' - - if a:0 > 0 && a:1 != '' - let register = a:1 - else - " v:register can be blank in some (unknown) cases - " if v:register == '' || v:register == '_' - if v:register == '_' - " Black hole register, ignore recording the operation - return "" - endif - - let register = s:YRRegister() - - if &clipboard =~ '\' && register == '*' - " unnamed A variant of "unnamed" flag which uses the clipboard - " register '*' (|quote|) for all operations except yank. - " Yank shall copy the text into register '*' when "unnamed" - " is included. - " - let register = '*' - - " The + and * registers are not modified by yank operations. - " We do not know what operation triggered this event so do a - " simple check if the register values have changed. - " If not, check it against the " register. Use which ever - " one has changed. - if s:yr_prev_clipboard_star == '' || getreg(register) == s:yr_prev_clipboard_star - if getreg('"') != getreg(register) - let register = '"' - endif - endif - endif - - if has('unnamedplus') && &clipboard =~ '\' && register == '+' - " unnamedplus A variant of "unnamed" flag which uses the clipboard - " register '+' (|quoteplus|) instead of register '*' for all - " operations except yank. Yank shall copy the text into - " register '+' and also into '*' when "unnamed" is included. - " - let register = '+' - - " The + and * registers are not modified by yank operations. - " We do not know what operation triggered this event so do a - " simple check if the register values have changed. - " If not, check it against the " register. Use which ever - " one has changed. - if s:yr_prev_clipboard_plus == '' || getreg(register) == s:yr_prev_clipboard_plus - if getreg('"') != getreg(register) - let register = '"' - endif - endif - endif - endif - - if register == '"' - " If the change has occurred via an omap, we must delay - " the capture of the default register until this event - " since register updates are not reflected until the - " omap function completes - let s:yr_prev_reg_unnamed = getreg('"') - let s:yr_prev_reg_small = getreg('-') - endif - - if s:yr_remove_omap_dot == 1 - call s:YRMapsCreate('add_only_zap_keys') - endif - - " Add item to list - " This will also account for duplicates. - call s:YRMRUAdd( 's:yr_history_list' - \ , getreg(register) - \ , getregtype(register) - \ ) - - if g:yankring_clipboard_monitor == 1 - let s:yr_prev_clipboard_plus = getreg('+') - let s:yr_prev_clipboard_star = getreg('*') - endif - - " Manage the numbered registers - if g:yankring_manage_numbered_reg == 1 - " Allow the user to define an autocmd to dynamically - " setup their connection information. - silent! doautocmd User YRSetNumberedReg - endif - - " If the yankring window is open, refresh it - call s:YRWindowUpdate() - - " Reset the past paste entry to the top of the ring. - " When the user hits replace last entry it should - " start from the top (not from the last previous - " replace) after capturing a new value in the YankRing. - let s:yr_last_paste_idx = 1 - - return "" -endfunction - - -" Record the operation for the dot operator -function! s:YRSetPrevOP(op_code, count, reg, mode) - let s:yr_prev_op_code = a:op_code - let s:yr_prev_op_mode = a:mode - let s:yr_prev_count = a:count - let s:yr_prev_changenr = changenr() - let s:yr_prev_reg = a:reg - let s:yr_prev_reg_unnamed = getreg('"') - let s:yr_prev_reg_small = getreg('-') - let s:yr_prev_reg_insert = getreg('.') - let s:yr_prev_vis_lstart = line("'<") - let s:yr_prev_vis_lend = line("'>") - let s:yr_prev_vis_cstart = col("'<") - let s:yr_prev_vis_cend = col("'>") - let s:yr_prev_reg_expres = histget('=', -1) - - if a:mode == 'n' - " In normal mode, the change has already - " occurred, therefore we can mark the - " actual position of the change. - let s:yr_prev_chg_lstart = line("'[") - let s:yr_prev_chg_lend = line("']") - let s:yr_prev_chg_cstart = col("'[") - let s:yr_prev_chg_cend = col("']") - else - " If in operator pending mode, the change - " has not yet occurred. Therefore we cannot - " use the '[ and ]' markers. But we can - " store the current line position. - let s:yr_prev_chg_lstart = line(".") - let s:yr_prev_chg_lend = line(".") - let s:yr_prev_chg_cstart = col(".") - let s:yr_prev_chg_cend = col(".") - endif - - " If storing the last change position (using '[, ']) - " is not good enough, then another option is to: - " Use :redir on the :changes command - " and grab the last item. Store this value - " and compare it is YRDoRepeat. -endfunction - - -" Adds this value to the yankring. -function! s:YRDoRepeat() - let dorepeat = 0 - - if s:yr_has_voperator == 1 - " Let Vim handle the repeat, just capture the updates - " as usual. - return 0 - endif - - if s:yr_prev_op_code =~ '^c' - " You cannot repeat change operations, let Vim's - " standard mechanism handle these, or the user will - " be prompted again, instead of repeating the - " previous change. - return 0 - endif - - if g:yankring_manage_numbered_reg == 1 - " When resetting the numbered register we are - " must ignore the comparison of the " register. - if s:yr_prev_reg_small == getreg('-') && - \ s:yr_prev_reg_insert == getreg('.') && - \ s:yr_prev_reg_expres == histget('=', -1) && - \ s:yr_prev_vis_lstart == line("'<") && - \ s:yr_prev_vis_lend == line("'>") && - \ s:yr_prev_vis_cstart == col("'<") && - \ s:yr_prev_vis_cend == col("'>") && - \ s:yr_prev_chg_lstart == line("'[") && - \ s:yr_prev_chg_lend == line("']") && - \ s:yr_prev_chg_cstart == col("'[") && - \ s:yr_prev_chg_cend == col("']") - let dorepeat = 1 - endif - else - " Check the previously recorded value of the registers - " if they are the same, we need to reissue the previous - " yankring command. - " If any are different, the user performed a command - " command that did not involve the yankring, therefore - " we should just issue the standard "normal! ." to repeat it. - if s:yr_prev_reg_unnamed == getreg('"') && - \ s:yr_prev_reg_small == getreg('-') && - \ s:yr_prev_reg_insert == getreg('.') && - \ s:yr_prev_reg_expres == histget('=', -1) && - \ s:yr_prev_vis_lstart == line("'<") && - \ s:yr_prev_vis_lend == line("'>") && - \ s:yr_prev_vis_cstart == col("'<") && - \ s:yr_prev_vis_cend == col("'>") - let dorepeat = 1 - endif - if dorepeat == 1 && s:yr_prev_op_mode == 'n' - " Hmm, not sure why I was doing this now - " so I will remove it - " let dorepeat = 0 - " if s:yr_prev_chg_lstart == line("'[") && - " \ s:yr_prev_chg_lend == line("']") && - " \ s:yr_prev_chg_cstart == col("'[") && - " \ s:yr_prev_chg_cend == col("']") - " let dorepeat = 1 - " endif - elseif dorepeat == 1 && s:yr_prev_op_mode == 'o' - " Hmm, not sure why I was doing this now - " so I will remove it - " let dorepeat = 0 - " if s:yr_prev_chg_lstart == line("'[") && - " \ s:yr_prev_chg_lend == line("']") && - " \ s:yr_prev_chg_cstart == col("'[") && - " \ s:yr_prev_chg_cend == col("']") - " let dorepeat = 1 - " endif - endif - endif - - " " If another change has happened that was not part of the - " " yankring we cannot replay it (from the yankring). Use - " " the standard ".". - " " If the previous op was a change, do not use the yankring - " " to repeat it. - " " changenr() is buffer specific, so anytime you move to - " " a different buffer you will definitely perform a - " " standard "." - " " Any previous op that was a change, must be replaced using "." - " " since we do not want the user prompted to enter text again. - " if s:yr_prev_changenr == changenr() && s:yr_prev_op_code !~ '^c' - " let dorepeat = 1 - " endif - - " If we are going to repeat check to see if the - " previous command was a yank operation. If so determine - " if yank operations are allowed to be repeated. - if dorepeat == 1 && s:yr_prev_op_code =~ '^y' - " This value be default is set based on cpoptions. - if g:yankring_dot_repeat_yank == 0 - let dorepeat = 0 - endif - endif - return dorepeat -endfunction - - -" Manages the Vim's numbered registers -function! s:YRSetNumberedReg() - - let i = 0 - - while i <= 10 - if i > s:yr_count - break - endif - - call setreg( (i) - \ , s:YRGetValElemNbr((i),'v') - \ , s:YRGetValElemNbr((i),'t') - \ ) - let i += 1 - endwhile - - " There are a few actions that Vim automatically takes - " when modifying the numbered registers. - " Modifying register 1 - changes the named register. - " It is impossible to set register 2 to a value, since Vim will change it. - - " This will at least preserve the default register - let @" = @0 -endfunction - - -" This internal function will add and subtract values from a starting -" point and return the correct element number. It takes into account -" the circular nature of the yankring. -function! s:YRGetNextElem(start, iter) - - let needed_elem = a:start + a:iter - - " The yankring is a ring, so if an element is - " requested beyond the number of elements, we - " must wrap around the ring. - if needed_elem > s:yr_count - let needed_elem = needed_elem % s:yr_count - endif - - if needed_elem == 0 - " Can happen at the end or beginning of the ring - if a:iter == -1 - " Wrap to the bottom of the ring - let needed_elem = s:yr_count - else - " Wrap to the top of the ring - let needed_elem = 1 - endif - elseif needed_elem < 1 - " As we step backwards through the ring we could ask for a negative - " value, this will wrap it around to the end - let needed_elem = s:yr_count - endif - - return needed_elem - -endfunction - - -" Lets Vim natively perform the operation and then stores what -" was yanked (or deleted) into the yankring. -" Supports this for example - 5"ayy -" -" This is a legacy function now since the release of Vim 7.2 -" and the use of omaps with YankRing 5.0 and above. -" If Vim 7.1 has patch205, then the new omaps and the v:operator -" variable is used instead. -function! s:YRYankCount(...) range - - let user_register = s:YRRegister() - let v_count = v:count - - " Default yank command to the entire line - let op_code = 'yy' - if a:0 > 0 - " If no yank command has been supplied, assume it is - " a full line yank - let op_code = ((a:1 == '') ? op_code : a:1) - endif - - if op_code == '.' - if s:YRDoRepeat() == 1 - if s:yr_prev_op_code != '' - let op_code = s:yr_prev_op_code - let v_count = s:yr_prev_count - let user_register = s:yr_prev_reg - endif - else - " Set this flag so that YRRecord will - " ignore repeats - let s:yr_prev_repeating = 1 - exec "normal! ." - return - endif - else - let s:yr_prev_repeating = 0 - endif - - " Supports this for example - 5"ayy - " A delete operation will still place the items in the - " default registers as well as the named register - exec "normal! ". - \ ((v_count > 0)?(v_count):''). - \ (user_register=='"'?'':'"'.user_register). - \ op_code - - if user_register == '_' - " Black hole register, ignore recording the operation - return - endif - - call s:YRSetPrevOP(op_code, v_count, user_register, 'n') - - call YRRecord(user_register) -endfunction - - -" Handles ranges. There are visual ranges and command line ranges. -" Visual ranges are easy, since we pass through and let Vim deal -" with those directly. -" Command line ranges means we must yank the entire line, and not -" just a portion of it. -function! s:YRYankRange(do_delete_selection, ...) range - - let user_register = s:YRRegister() - let default_buffer = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - - " Default command mode to normal mode 'n' - let cmd_mode = 'n' - if a:0 > 0 - " Change to visual mode, if command executed via - " a visual map - let cmd_mode = ((a:1 == 'v') ? 'v' : 'n') - endif - - if a:do_delete_selection == 1 - " Save register 0 - " Register 0 should only be changed by yank operations - " if we are deleting text, this could inadvertently - " update this register. - let zero_register = [0, getreg(0), getregtype('0')] - endif - - if cmd_mode == 'v' - " We are yanking either an entire line, or a range - " We want to yank the text first (even in a delete) since - " the rules around which registers get updated are a bit - " complicated. For deletes, it depends on how large - " the delete is for which registers get updated. - exec "normal! gv". - \ (user_register==default_buffer?'':'"'.user_register). - \ 'y' - if a:do_delete_selection == 1 - exec "normal! gv". - \ (user_register==default_buffer?'':'"'.user_register). - \ 'd' - endif - else - " In normal mode, always yank the complete line, since this - " command is for a range. YRYankCount is used for parts - " of a single line - if a:do_delete_selection == 1 - exec a:firstline . ',' . a:lastline . 'delete '.user_register - else - exec a:firstline . ',' . a:lastline . 'yank ' . user_register - endif - endif - - if a:do_delete_selection == 1 - " Restore register zero - call call('setreg', zero_register) - endif - - if user_register == '_' - " Black hole register, ignore - return - endif - - call s:YRSetPrevOP('', '', user_register, 'n') - call YRRecord(user_register) -endfunction - - -" Paste from either the yankring or from a specified register -" Optionally a count can be provided, so paste the same value 10 times -function! s:YRPaste(replace_last_paste_selection, nextvalue, direction, ...) - if g:yankring_manual_clipboard_check == 1 - call s:YRCheckClipboard() - endif - - " Disabling the yankring removes the default maps. - " But there are some maps the user can create on their own, and - " these would most likely call this function. So place an extra - " check and display a message. - if g:yankring_enabled == 0 - call s:YRWarningMsg( - \ 'YR: The yankring is currently disabled, use YRToggle.' - \ ) - return - endif - - let user_register = s:YRRegister() - let default_register = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - let default_register = '"' - let clipboard_default = matchstr( &clipboard, '\' ) - if has('unnamedplus') && clipboard_default == '\' && (v:register == '' || v:register == '"') - let default_register = '+' - endif - if clipboard_default =~ '\' && (v:register == '' || v:register == '"') - let default_register = '*' - endif - let v_count = v:count - - " Default command mode to normal mode 'n' - let cmd_mode = 'n' - if a:0 > 0 - " Change to visual mode, if command executed via - " a visual map - let cmd_mode = ((a:1 == 'v') ? 'v' : 'n') - endif - - if a:replace_last_paste_selection == 1 - " Replacing the previous put - let start = line("'[") - let end = line("']") - - if start != line('.') - call s:YRWarningMsg( 'YR: You must paste text first, before you can replace' ) - return - endif - - if start == 0 || end == 0 - return - endif - - " If a count was provided (ie 5), multiply the - " nextvalue accordingly and position the next paste index - " let which_elem = a:nextvalue * ((v_count > 0)?(v_count):1) * -1 - let which_elem = matchstr(a:nextvalue, '-\?\d\+') * ((v_count > 0)?(v_count):1) * -1 - let s:yr_last_paste_idx = s:YRGetNextElem( - \ s:yr_last_paste_idx, which_elem - \ ) - - let save_reg = getreg(default_register) - let save_reg_type = getregtype(default_register) - call setreg( default_register - \ , s:YRGetValElemNbr((s:yr_last_paste_idx-1),'v') - \ , s:YRGetValElemNbr((s:yr_last_paste_idx-1),'t') - \ ) - - " First undo the previous paste - exec "normal! u" - " Check if the visual selection should be reselected - " Next paste the correct item from the ring - " This is done as separate statements since it appeared that if - " there was nothing to undo, the paste never happened. - exec "normal! ". - \ ((s:yr_prev_vis_mode==0) ? "" : "gv"). - \ '"'.default_register. - \ s:yr_paste_dir - call setreg(default_register, save_reg, save_reg_type) - call s:YRSetPrevOP('', '', '', 'n') - - return - endif - - " User has decided to bypass the yankring and specify a specific - " register - if user_register != default_register - " if a:replace_last_paste_selection == 1 - " call s:YRWarningMsg( 'YR: A register cannot be specified in replace mode' ) - " return - " endif - - " Check for the expression register, in this special case - " we must copy it's evaluation into the default buffer and paste - if user_register == '=' - " Save the default register since Vim will only - " allow the expression register to be pasted once - " and will revert back to the default buffer - let save_default_reg = @" - call setreg(default_register, eval(histget('=', -1)) ) - else - let user_register = '"'.user_register - endif - exec "normal! ". - \ ((cmd_mode=='n') ? "" : "gv"). - \ ((v_count > 0)?(v_count):''). - \ ((user_register=='=')?'':user_register). - \ a:direction - if user_register == '=' - let @" = save_default_reg - endif - " In this case, we have bypassed the yankring - " If the user hits next or previous we want the - " next item pasted to be the top of the yankring. - let s:yr_last_paste_idx = 1 - - let s:yr_paste_dir = a:direction - let s:yr_prev_vis_mode = ((cmd_mode=='n') ? 0 : 1) - return - endif - - " Try to second guess the user to make these mappings less intrusive. - " If the user hits paste, compare the contents of the paste register - " to the current entry in the yankring. If they are different, lets - " assume the user wants the contents of the paste register. - " The user could have: - " let @" = 'test string' - " would not be in the yankring as no mapping triggered this action. - if a:replace_last_paste_selection != 1 - " Only check the default buffer is the user wants us to. - " This was necessary prior to version 4.0 since we did not - " capture as many items as 4.0 and above does. (A. Budden) - if g:yankring_paste_check_default_register == 1 - if ( default_register == '"' && getreg(default_register) != s:yr_prev_reg_unnamed ) - " There are only a couple of scenarios where this would happen - " 1. set clipboard = unnamed[plus] - " The user performs an action which changes the - " unnamed register (i.e. x - delete character) - " 2. Any type of direct manipulation of the registers - " let @" = 'something' - " 3. Something changed the system clipboard outside of Vim - if getreg('"') != s:yr_prev_reg_unnamed - let default_register = '"' - endif - - " The user has performed a yank / delete operation - " outside of the yankring maps. First, add this - " value to the yankring. - call YRRecord(default_register) - elseif ( default_register == '+' && - \ !empty(getreg(default_register)) && - \ getreg(default_register) != s:yr_prev_clipboard_plus - \ ) - - " The user has performed a yank / delete operation - " outside of the yankring maps. First, add this - " value to the yankring. - call YRRecord(default_register) - elseif ( default_register == '*' && - \ !empty(getreg(default_register)) && - \ getreg(default_register) != s:yr_prev_clipboard_star - \ ) - - " The user has performed a yank / delete operation - " outside of the yankring maps. First, add this - " value to the yankring. - call YRRecord(default_register) - endif - endif - - exec "normal! ". - \ ((cmd_mode=='n') ? "" : "gv"). - \ ((v_count > 0)?(v_count):''). - \ '"'.default_register. - \ a:direction - let s:yr_paste_dir = a:direction - let s:yr_prev_vis_mode = ((cmd_mode=='n') ? 0 : 1) - return - endif - - " if s:yr_count > 0 || ( - " \ default_register != '"' && - " \ empty(getreg(default_register)) - " \ ) - " " Nothing to paste - " return - " endif - - " User hit p or P - " Supports this for example - 5"ayy - " And restores the current register - let save_reg = getreg(default_register) - let save_reg_type = getregtype(default_register) - let s:yr_last_paste_idx = 1 - call setreg(default_register - \ , s:YRGetValElemNbr(0,'v') - \ , s:YRGetValElemNbr(0,'t') - \ ) - exec "normal! ". - \ ((cmd_mode=='n') ? "" : "gv"). - \ ((v_count > 0)?(v_count):''). - \ '"'.default_register. - \ a:direction - call setreg(default_register, save_reg, save_reg_type) - call s:YRSetPrevOP( - \ a:direction - \ , v_count - \ , default_register - \ , 'n' - \ ) - let s:yr_paste_dir = a:direction - let s:yr_prev_vis_mode = ((cmd_mode=='n') ? 0 : 1) - -endfunction - - -" Handle any omaps -function! YRMapsExpression(sid, motion, ...) - let cmds = a:motion - " echomsg "YRMapsE:".localtime() - " echomsg "YRMapsE 1:".cmds.":".v:operator.":".s:yr_maps_created_zap - - if (a:motion =~ '\.' && s:yr_remove_omap_dot == 1) || a:motion =~ '@' - " If we are repeating a series of commands we must - " unmap the _zap_ keys so that the user is not - " prompted when a command is replayed. - " These maps must be re-instated in YRRecord3() - " after the action of the replay is completed. - call s:YRMapsDelete('remove_only_zap_keys') - endif - - " Check if we are in operator-pending mode - if a:motion =~ '\('.substitute(g:yankring_zap_keys, ' ', '\\|', 'g').'\)' - if a:motion =~ '\(/\|?\)' - let zapto = (a:0==0 ? "" : input("YR:Enter string:")) - if zapto != "" - let zapto = zapto . "\" - else - let zapto = "\" - endif - else - let zapto = (a:0==0 ? "" : s:YRGetChar()) - endif - - if zapto == "\" - " Abort if the user hits Control C - call s:YRWarningMsg( "YR:Aborting command:".v:operator.a:motion ) - return "\" - endif - - let cmds = cmds . zapto - endif - - " There are a variety of commands which do not change the - " registers, so these operators should be ignored when - " determining which operations to record - " Simple example is '=' which simply formats the - " the selected text. - if ' \('.escape(join(split(g:yankring_ignore_operator), '\|'), '/.*~$^[]' ).'\) ' !~ escape(v:operator, '/.*~$^[]') - " Check if we are performing an action that will - " take us into insert mode - if '[cCsS]' !~ escape(v:operator, '/.*~$^[]') && a:motion !~ '@' - " if '[cCsS]' !~ escape(v:operator, '/.*~$^[]') - " If we have not entered insert mode, feed the call - " to record the current change when the function ends. - " This is necessary since omaps do not update registers - " until the function completes. - " The InsertLeave event will handle the motions - " that place us in insert mode and record the - " changes when insert mode ends. - " let cmds .= a:sid. "yrrecord ".a:motion - let cmds .= a:sid. "yrrecord" - endif - endif - - " This will not work since we are already executing an expression - " if a:motion =~ '@' - " let cmds = 'normal! ' . cmds - " endif - - " YRRecord3() will use this value to determine what operation - " the user just initiated. - let s:yr_last_motion = cmds - - " echomsg "YRMapsE 5:".a:motion.":'".cmds."':".s:yr_maps_created_zap - return cmds - -endfunction - - -" Handle macros (@). -" This routine is not used, YRMapsExpression is used to -" handle the @ symbol. -function! s:YRMapsMacro(bang, ...) - " If we are repeating a series of commands we must - " unmap the _zap_ keys so that the user is not - " prompted when a command is replayed. - " These maps must be re-instated in YRRecord3() - " after the action of the replay is completed. - call s:YRMapsDelete('remove_only_zap_keys') - - " Greg Sexton indicated the use of nr2char() removes - " a "Press ENTER ..." prompt when executing a macro. - " let zapto = nr2char(getchar()) - " let zapto = (a:0==0 ? "" : s:YRGetChar()) - let zapto = s:YRGetChar() - - if zapto == "\" - " Abort if the user hits Control C - call s:YRWarningMsg( "YR:Aborting macro" ) - return "" - endif - - if zapto !~ '\(\w\|@\|:\)' - " Abort if the user does not specify a register - call s:YRWarningMsg( "YR:No register specified, aborting macro" ) - return "" - endif - - let v_count = v:count - " If no count was specified it will have a value of 0 - " so set it to at least 1 - let v_count = ((v_count > 0)?(v_count):'') - - " let range = '' - " if a:firstline != a:lastline - " let rannge = a:firstline.','.a:lastline - " endif - - " let cmd = range."normal! ".v_count.'@'.zapto - let cmd = "normal! ".v_count.'@'.zapto - " DEBUG - " echomsg cmd - exec cmd - - call s:YRMapsCreate('add_only_zap_keys') -endfunction - - -" Create the default maps -function! s:YRMapsCreate(...) - " 7.1.patch205 introduces the v:operator function which was - " essential to gain the omap support. - if s:yr_has_voperator == 1 - let s:yr_remove_omap_dot = 1 - for key in split(g:yankring_zap_keys) - try - if key != '@' - exec 'omap ' key 'YRMapsExpression("", "'. key. '", 1)' - endif - catch - endtry - endfor - endif - - " silent! nmap @ YRMapsExpression("", "@", "1") - silent! nmap @ :YRMapsMacro - - let s:yr_maps_created_zap = 1 - - if a:0 > 0 - " We have only removed the _zap_ keys temporarily - " so abandon further changes. - return - endif - - " 7.1.patch205 introduces the v:operator function which was essential - " to gain the omap support. - if s:yr_has_voperator == 1 - let s:yr_remove_omap_dot = 1 - " Set option to add and remove _zap_ keys when - " repeating commands - let o_maps = split(g:yankring_o_keys) - " Loop through and prompt the user for all buffer connection parameters. - for key in o_maps - exec 'omap ' key 'YRMapsExpression("", "'. escape(key,'\"'). '")' - endfor - endif - - " Iterate through a space separated list of mappings and create - " calls to the YRYankCount function - let n_maps = split(g:yankring_n_keys) - " Loop through and prompt the user for all buffer connection parameters. - for key in n_maps - " exec 'nnoremap '.key." :YRYankCount '".key."'" - " exec 'nnoremap '.key." :YRYankCount '".key."'" - " Andy Wokula's suggestion - exec 'nmap' key key."yrrecord" - endfor - - if g:yankring_map_dot == 1 - if s:yr_has_voperator == 1 - nmap . YRMapsExpression("", ".") - else - nnoremap . :YRYankCount '.' - endif - endif - - if g:yankring_v_key != '' - exec 'xnoremap '.g:yankring_v_key." :YRYankRange 'v'" - endif - if g:yankring_del_v_key != '' - for v_map in split(g:yankring_del_v_key) - if !empty(v_map) - try - exec 'xnoremap '.v_map." :YRDeleteRange 'v'" - catch - endtry - endif - endfor - endif - if g:yankring_paste_n_bkey != '' - exec 'nnoremap '.g:yankring_paste_n_bkey." :YRPaste 'P'" - if g:yankring_paste_using_g == 1 - exec 'nnoremap g'.g:yankring_paste_n_bkey." :YRPaste 'gP'" - endif - endif - if g:yankring_paste_n_akey != '' - exec 'nnoremap '.g:yankring_paste_n_akey." :YRPaste 'p'" - if g:yankring_paste_using_g == 1 - exec 'nnoremap g'.g:yankring_paste_n_akey." :YRPaste 'gp'" - endif - endif - if g:yankring_paste_v_bkey != '' - exec 'xnoremap '.g:yankring_paste_v_bkey." :YRPaste 'P', 'v'" - endif - if g:yankring_paste_v_akey != '' - exec 'xnoremap '.g:yankring_paste_v_akey." :YRPaste 'p', 'v'" - endif - if g:yankring_replace_n_pkey != '' - exec 'nnoremap '.g:yankring_replace_n_pkey." :YRReplace '-1', P" - endif - if g:yankring_replace_n_nkey != '' - exec 'nnoremap '.g:yankring_replace_n_nkey." :YRReplace '1', p" - endif - - let g:yankring_enabled = 1 - let s:yr_maps_created = 1 - - if exists('*YRRunAfterMaps') - " This will allow you to override the default maps if necessary - call YRRunAfterMaps() - endif -endfunction - - -" Create the default maps -function! s:YRMapsDelete(...) - - let o_maps = split(g:yankring_zap_keys) - for key in o_maps - try - " Why not remove the @ map? - if key != '@' - silent! exec 'ounmap' key - endif - catch - endtry - endfor - - let s:yr_maps_created_zap = 0 - - if a:0 > 0 - " We have only removed the _zap_ keys temporarily - " so abandon further changes. - return - endif - - " Iterate through a space separated list of mappings and create - " calls to an appropriate YankRing function - let n_maps = split(g:yankring_n_keys) - " Loop through and prompt the user for all buffer connection parameters. - for key in n_maps - try - silent! exec 'nunmap' key - catch - endtry - endfor - - let o_maps = split(g:yankring_o_keys) - for key in o_maps - try - silent! exec 'ounmap' key - catch - endtry - endfor - - if g:yankring_map_dot == 1 - silent! exec "nunmap ." - endif - if g:yankring_v_key != '' - silent! exec 'vunmap '.g:yankring_v_key - endif - if g:yankring_del_v_key != '' - for v_map in split(g:yankring_del_v_key) - if !empty(v_map) - try - silent! exec 'vunmap '.v_map - catch - endtry - endif - endfor - endif - if g:yankring_paste_n_bkey != '' - silent! exec 'nunmap '.g:yankring_paste_n_bkey - if g:yankring_paste_using_g == 1 - silent! exec 'nunmap g'.g:yankring_paste_n_bkey - endif - endif - if g:yankring_paste_n_akey != '' - silent! exec 'nunmap '.g:yankring_paste_n_akey - if g:yankring_paste_using_g == 1 - silent! exec 'nunmap g'.g:yankring_paste_n_akey - endif - endif - if g:yankring_paste_v_bkey != '' - silent! exec 'vunmap '.g:yankring_paste_v_bkey - endif - if g:yankring_paste_v_akey != '' - silent! exec 'vunmap '.g:yankring_paste_v_akey - endif - if g:yankring_replace_n_pkey != '' - silent! exec 'nunmap '.g:yankring_replace_n_pkey - endif - if g:yankring_replace_n_nkey != '' - silent! exec 'nunmap '.g:yankring_replace_n_nkey - endif - - silent! exec 'nunmap @' - - let g:yankring_enabled = 0 - let s:yr_maps_created = 0 -endfunction - -function! s:YRGetValElemNbr( position, type ) - let needed_elem = a:position - - " The List which contains the items in the yankring - " history is also ordered, most recent at the top - let elem = s:YRMRUGet('s:yr_history_list', needed_elem) - - if a:type == 't' - let elem = matchstr(elem, '^.*,\zs.*$') - else - let elem = matchstr(elem, '^.*\ze,.*$') - if s:yr_history_version == 'v1' - " Match three @@@ in a row as long as it is not - " preceded by a @@@ - " v1 - let elem = substitute(elem, s:yr_history_v1_nl_pat, "\n", 'g') - let elem = substitute(elem, '\\@', '@', 'g') - else - let elem = substitute(elem, s:yr_history_v2_nl_pat, "\n", 'g') - endif - endif - - return elem -endfunction - -function! s:YRMRUReset( mru_list ) - let {a:mru_list} = [] - - return 1 -endfunction - -function! s:YRMRUSize( mru_list ) - return len({a:mru_list}) -endfunction - -function! s:YRMRUElemFormat( element, element_type ) - let elem = a:element - if g:yankring_max_element_length != 0 - let elem = strpart(a:element, 0, g:yankring_max_element_length) - if (g:yankring_warn_on_truncate > 0) - let bytes = len (a:element) - len(elem) - if (bytes > 0) - call s:YRWarningMsg("Yankring truncated its element by ". - \ bytes. - \ " bytes due to a g:yankring_max_element_length of ". - \ g:yankring_max_element_length - \ ) - endif - endif - endif - if s:yr_history_version == 'v1' - let elem = escape(elem, '@') - let elem = substitute(elem, "\n", s:yr_history_v1_nl, 'g') - else - let elem = substitute(elem, "\n", s:yr_history_v2_nl, 'g') - endif - " Append the regtype to the end so we have it available - let elem = elem.",".a:element_type - - return elem -endfunction - -function! s:YRMRUHas( mru_list, find_str ) - " This function will find a string and return the element # - let find_idx = index({a:mru_list}, a:find_str) - - return find_idx -endfunction - -function! s:YRMRUGet( mru_list, position ) - " This function will return the value of the item at a:position - " Find the value of one element - let value = get({a:mru_list}, a:position, -2) - - return value -endfunction - -function! s:YRMRUAdd( mru_list, element, element_type ) - " Only add new items if they do not already exist in the MRU. - " If the item is found, move it to the start of the MRU. - let found = -1 - " let elem = a:element - " if g:yankring_max_element_length != 0 - " let elem = strpart(a:element, 0, g:yankring_max_element_length) - " endif - " if s:yr_history_version == 'v1' - " let elem = escape(elem, '@') - " let elem = substitute(elem, "\n", s:yr_history_v1_nl, 'g') - " else - " let elem = substitute(elem, "\n", s:yr_history_v2_nl, 'g') - " endif - " " Append the regtype to the end so we have it available - " let elem = elem.",".a:element_type - - if strlen(a:element) < g:yankring_min_element_length - return 1 - endif - - let elem = s:YRMRUElemFormat(a:element, a:element_type) - - " Refresh the List - call s:YRHistoryRead() - - let found = s:YRMRUHas(a:mru_list, elem) - - " Special case for efficiency, if it is first item in the - " List, do nothing - if found != 0 - if found != -1 - " Remove found item since we will add it to the top - call remove({a:mru_list}, found) - endif - call insert({a:mru_list}, elem, 0) - let s:yr_count = len({a:mru_list}) - call s:YRHistorySave() - endif - - return 1 -endfunction - -function! s:YRMRUDel( mru_list, elem_nbr ) - if a:elem_nbr >= 0 && a:elem_nbr < s:yr_count - call remove({a:mru_list}, a:elem_nbr) - let s:yr_count = len({a:mru_list}) - call s:YRHistorySave() - endif - - return 1 -endfunction - -function! s:YRHistoryDelete() - let s:yr_history_list = [] - let s:yr_count = 0 - - if g:yankring_persist != 1 - return - endif - let yr_filename = s:yr_history_file_{s:yr_history_version} - - if filereadable(yr_filename) - let rc = delete(yr_filename) - if rc != 0 - call s:YRErrorMsg( - \ 'YRHistoryDelete: Unable to delete the yankring history file: '. - \ yr_filename - \ ) - endif - endif - - return 0 -endfunction - -function! s:YRHistoryRead() - if g:yankring_persist != 1 - return - endif - let refresh_needed = 1 - let yr_history_list = [] - let yr_filename = s:yr_history_file_{s:yr_history_version} - - if filereadable(yr_filename) - let last_upd = getftime(yr_filename) - - if s:yr_history_last_upd != 0 && last_upd <= s:yr_history_last_upd - let refresh_needed = 0 - endif - - if refresh_needed == 1 - let s:yr_history_list = readfile(yr_filename) - let s:yr_history_last_upd = last_upd - let s:yr_count = len(s:yr_history_list) - return - else - return - endif - else - if s:yr_history_version == 'v2' - " Check to see if an upgrade is required - " else, let the empty yr_history_list be returned. - if filereadable(s:yr_history_file_v1) - " Perform upgrade to v2 of the history file - call s:YRHistoryUpgrade('v1') - return - endif - endif - endif - - let s:yr_history_list = yr_history_list - call s:YRHistorySave() -endfunction - -function! s:YRHistorySave() - - if len(s:yr_history_list) > g:yankring_max_history - " Remove items which exceed the max # specified - call remove(s:yr_history_list, g:yankring_max_history, (len(s:yr_history_list)-1)) - let s:yr_count = len(s:yr_history_list) - endif - - if g:yankring_persist != 1 - return - endif - - let yr_filename = s:yr_history_file_{s:yr_history_version} - let rc = writefile(s:yr_history_list, yr_filename) - - if rc == 0 - let s:yr_history_last_upd = getftime(yr_filename) - else - call s:YRErrorMsg( - \ 'YRHistorySave: Unable to save yankring history file: '. - \ yr_filename - \ ) - endif -endfunction - -function! s:YRHistoryUpgrade(version) - if a:version == 'v1' - if filereadable(s:yr_history_file_v1) - let v1_list = readfile(s:yr_history_file_v1) - let v2_list = [] - for elem in v1_list - " Restore from version 1 - let elem = substitute(elem, s:yr_history_v1_nl_pat, "\n", 'g') - let elem = substitute(elem, '\\@', '@', 'g') - " Encode to version 2 - let elem = substitute(elem, "\n", s:yr_history_v2_nl, 'g') - call add(v2_list, elem) - endfor - let s:yr_history_list = v2_list - call s:YRHistorySave() - call s:YRWarningMsg( - \ "YR:History file:". - \ s:yr_history_file_v1. - \ ' has been upgraded.' - \ ) - endif - endif -endfunction - -" YRWindowUpdate -" Checks if the yankring window is already open. -" If it is, it will refresh it. -function! s:YRWindowUpdate() - let orig_win_bufnr = bufwinnr('%') - - " Switch to the yankring buffer - " only if it is already visible - if bufwinnr(s:yr_buffer_id) != -1 - call s:YRShow(0) - " Switch back to the original buffer - exec orig_win_bufnr . "wincmd w" - endif -endfunction - -" YRWindowStatus -" Displays a brief command list and option settings. -" It also will toggle the Help text. -function! s:YRWindowStatus(show_help) - let full_help = 0 - let orig_win_bufnr = bufwinnr('%') - let yr_win_bufnr = bufwinnr(s:yr_buffer_id) - - if yr_win_bufnr == -1 - " Do not update the window status since the - " yankring is not currently displayed. - return "" - endif - " Switch to the yankring buffer - if orig_win_bufnr != yr_win_bufnr - " If the buffer is visible, switch to it - exec yr_win_bufnr . "wincmd w" - endif - - let msg = 'AutoClose='.g:yankring_window_auto_close. - \ ';ClipboardMonitor='.g:yankring_clipboard_monitor. - \ ';Inserts='.g:yankring_record_insert. - \ ';Cmds:,[g]p,[g]P,1-9,d,r,s,a,c,i,u,R,q,;Help=?'. - \ (s:yr_search==""?"":';SearchRegEx='.s:yr_search) - - if s:yr_has_voperator == 0 - let msg = msg . "\nYankRing has limited functionality without Vim 7.2 or higher" - endif - - " Toggle help by checking the first line of the buffer - if a:show_help == 1 && getline(1) !~ 'selection' - let full_help = 1 - let msg = - \ '" : [p]aste selection'."\n". - \ '" double-click : [p]aste selection'."\n". - \ '" [g]p : [g][p]aste selection'."\n". - \ '" [g]P : [g][P]aste selection'."\n". - \ '" 1-9 : Paste # entry from the YankRing (shortcut for speed)'."\n". - \ '" d : [d]elete item from the YankRing'."\n". - \ '" r : [p]aste selection in reverse order'."\n". - \ '" s : [s]earch the yankring for text'."\n". - \ '" u : [u]pdate display show YankRing'."\n". - \ '" R : [R]egisters display'."\n". - \ '" a : toggle [a]utoclose setting'."\n". - \ '" c : toggle [c]lipboard monitor setting'."\n". - \ '" i : toggle [i]nsert recording'."\n". - \ '" q : [q]uit / close the yankring window'."\n". - \ '" ? : Remove help text'."\n". - \ '" : toggles the width of the window'."\n". - \ '" Visual mode is supported for above commands'."\n". - \ '" YankRing Version: '.g:loaded_yankring."\n". - \ msg - endif - - let saveMod = &modifiable - - " Go to the top of the buffer and remove any previous status - " Use the blackhole register so it does not affect the yankring - setlocal modifiable - exec 0 - silent! exec 'norm! "_d/^---'."\n" - call histdel("search", -1) - - silent! 0put =msg - - " Erase it's contents to the blackhole - silent! exec '%g/^\s*$/delete _' - call histdel("search", -1) - - call cursor(1,1) - if full_help == 0 - call search('^\d', 'W') - endif - - let &modifiable = saveMod - - if orig_win_bufnr != s:yr_buffer_id - exec orig_win_bufnr . "wincmd w" - endif -endfunction - -" YRWindowOpen -" Display the Most Recently Used file list in a temporary window. -function! s:YRWindowOpen(results) - - " Setup the cpoptions properly for the maps to work - " and to not set the alternate buffer - let old_cpoptions = &cpoptions - set cpoptions&vim - set cpoptions-=a - set cpoptions-=A - - " Save the current buffer number. The yankring will switch back to - " this buffer when an action is taken. - let s:yr_buffer_last = bufnr('%') - let s:yr_buffer_last_winnr = winnr() - - if bufwinnr(s:yr_buffer_id) == -1 - if g:yankring_window_use_horiz == 1 - if g:yankring_window_use_bottom == 1 - let location = 'botright' - else - let location = 'topleft' - " Creating the new window will offset all other - " window numbers. Account for that so we switch - " back to the correct window. - let s:yr_buffer_last_winnr = s:yr_buffer_last_winnr + 1 - endif - let win_size = g:yankring_window_height - else - " Open a horizontally split window. Increase the window size, if - " needed, to accommodate the new window - if g:yankring_window_width && - \ &columns < (80 + g:yankring_window_width) - " Store the previous window size of the YankRing - let s:yr_winsize_chgd = &columns - " One extra column is needed to include the vertical split - let &columns = &columns + g:yankring_window_width + 1 - else - let s:yr_winsize_chgd = 0 - endif - - if g:yankring_window_use_right == 1 - " Open the window at the rightmost place - let location = 'botright vertical' - else - " Open the window at the leftmost place - let location = 'topleft vertical' - " Creating the new window will offset all other - " window numbers. Account for that so we switch - " back to the correct window. - let s:yr_buffer_last_winnr = s:yr_buffer_last_winnr + 1 - endif - let win_size = g:yankring_window_width - endif - - " Special consideration was involved with these sequence - " of commands. - " First, split the current buffer. - " Second, edit a new file. - " Third record the buffer number. - " If a different sequence is followed when the yankring - " buffer is closed, Vim's alternate buffer is the yanking - " instead of the original buffer before the yankring - " was shown. - let cmd_mod = '' - if v:version >= 700 - let cmd_mod = 'keepalt ' - endif - exec 'silent! ' . cmd_mod . location . ' ' . win_size . 'split ' - - " Using :e and hide prevents the alternate buffer - " from being changed. - exec ":e " . escape(s:yr_buffer_name, ' ') - " Save buffer id - let s:yr_buffer_id = bufnr('%') + 0 - else - " If the buffer is visible, switch to it - exec bufwinnr(s:yr_buffer_id) . "wincmd w" - endif - - " Perform a double check to ensure we have entered the correct - " buffer since we don't want to do the %d_ in the wrong buffer! - if (bufnr('%') + 0) != s:yr_buffer_id - call s:YRWarningMsg( - \ "YR:Failed to change to the yankring buffer, please contact author id:". - \ s:yr_buffer_id. - \ ' last:'.s:yr_buffer_last - \ ) - return -1 - endif - - " Mark the buffer as scratch - setlocal buftype=nofile - setlocal bufhidden=hide - setlocal noswapfile - setlocal nowrap - setlocal nonumber - setlocal nobuflisted - setlocal noreadonly - setlocal nospell - setlocal modifiable - if v:version >= 703 - setlocal norelativenumber - endif - - " set up syntax highlighting - syn match yankringTitle #^--- YankRing ---$#hs=s+4,he=e-4 - syn match yankringHeaders #^Elem Content$# - syn match yankringItemNumber #^\d\+# - - syn match yankringKey #^AutoClose.*#hs=e-6 - syn match yankringKey #^AutoClose.*\[g\]p#hs=e-3 contains=yankringKey - syn match yankringKey #^AutoClose.*\[p\]P#hs=e-3 contains=yankringKey - syn match yankringKey #^AutoClose.*,d,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,r,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,s,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,a,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,c,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,u,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*,q,#hs=e-1,he=e-1 contains=yankringKey - syn match yankringKey #^AutoClose.*#hs=e-6 contains=yankringKey - syn match yankringKey #^AutoClose.*?$#hs=e contains=yankringKey - - syn match yankringKey #^".*:#hs=s+1,he=e-1 - syn match yankringHelp #^".*$# contains=yankringKey - - hi link yankringTitle directory - hi link yankringHeaders keyword - hi link yankringItemNumber constant - hi link yankringKey identifier - hi link yankringHelp string - - " Clear all existing maps for this buffer - " We should do this for all maps, but I am not sure how to do - " this for this buffer/window only without affecting all the - " other buffers. - mapclear - " Create a mapping to act upon the yankring - nnoremap <2-LeftMouse> :call YRWindowActionN('p' , 'n') - nnoremap :call YRWindowActionN('p' , 'n') - xnoremap :call YRWindowAction ('p' , 'v') - nnoremap p :call YRWindowActionN('p' , 'n') - nnoremap [p :call YRWindowActionN('[p' , 'n') - nnoremap ]p :call YRWindowActionN(']p' , 'n') - xnoremap p :call YRWindowAction ('p' , 'v') - xnoremap [p :call YRWindowAction ('[p' , 'v') - xnoremap ]p :call YRWindowAction (']p' , 'v') - nnoremap P :call YRWindowActionN('P' , 'n') - nnoremap [P :call YRWindowActionN('[P' , 'n') - nnoremap ]P :call YRWindowActionN(']P' , 'n') - xnoremap P :call YRWindowAction ('P' , 'v') - xnoremap [P :call YRWindowAction ('[P' , 'v') - xnoremap ]P :call YRWindowAction (']P' , 'v') - nnoremap gp :call YRWindowActionN('gp' , 'n') - xnoremap gp :call YRWindowAction ('gp' , 'v') - nnoremap gP :call YRWindowActionN('gP' , 'n') - xnoremap gP :call YRWindowAction ('gP' , 'v') - nnoremap d :call YRWindowActionN('d' , 'n') - xnoremap d :call YRWindowAction ('d' , 'v') - xnoremap r :call YRWindowAction ('r' , 'v') - nnoremap s :call YRWindowAction ('s' , 'n') - nnoremap a :call YRWindowAction ('a' , 'n') - nnoremap c :call YRWindowAction ('c' , 'n') - nnoremap i :call YRWindowAction ('i' , 'n') - nnoremap ? :call YRWindowAction ('?' , 'n') - nnoremap u :call YRWindowAction ('u' , 'n') - nnoremap q :call YRWindowAction ('q' , 'n') - nnoremap R :call YRWindowAction ('R' , 'n') - nnoremap 1 :call YRWindowAction ('q' ,'n'):call YRGetElem(1) - nnoremap 2 :call YRWindowAction ('q' ,'n'):call YRGetElem(2) - nnoremap 3 :call YRWindowAction ('q' ,'n'):call YRGetElem(3) - nnoremap 4 :call YRWindowAction ('q' ,'n'):call YRGetElem(4) - nnoremap 5 :call YRWindowAction ('q' ,'n'):call YRGetElem(5) - nnoremap 6 :call YRWindowAction ('q' ,'n'):call YRGetElem(6) - nnoremap 7 :call YRWindowAction ('q' ,'n'):call YRGetElem(7) - nnoremap 8 :call YRWindowAction ('q' ,'n'):call YRGetElem(8) - nnoremap 9 :call YRWindowAction ('q' ,'n'):call YRGetElem(9) - nnoremap \|:silent exec 'vertical resize '. - \ ( - \ g:yankring_window_use_horiz!=1 && winwidth('.') > g:yankring_window_width - \ ?(g:yankring_window_width) - \ :(winwidth('.') + g:yankring_window_increment) - \ ) - - " Erase it's contents to the blackhole - silent! exec '%delete _' - - " Display the status line / help - call s:YRWindowStatus(0) - exec 'normal! G' - - " Display the contents of the yankring - silent! put =a:results - - if getline('$') == '' - " Erase last blank line - silent! exec '$delete _' - endif - - " Move the cursor to the first line with an element - exec 0 - call search('^\d','W') - - setlocal nomodifiable - " - " Restore the previous cpoptions settings - let &cpoptions = old_cpoptions - -endfunction - -function! s:YRWindowActionN(op, cmd_mode) - let v_count = v:count - " If no count was specified it will have a value of 0 - " so set it to at least 1 - let v_count = ((v_count > 0)?(v_count):1) - - if v_count > 1 - if !exists("b:yankring_show_range_error") - let b:yankring_show_range_error = v_count - else - let b:yankring_show_range_error = b:yankring_show_range_error - 1 - endif - - if b:yankring_show_range_error == 1 - call s:YRWarningMsg("YR:Use visual mode if you need to specify a count") - unlet b:yankring_show_range_error - endif - return - endif - - call s:YRWindowAction(a:op, a:cmd_mode) - let v_count = v_count - 1 - - if g:yankring_window_auto_close == 1 && v_count == 0 && a:op != 'd' - " If autoclose is set close the window unless - " you are removing items from the YankRing - exec 'bdelete '.s:yr_buffer_id - return "" - endif - - return "" -endfunction - -function! s:YRWindowAction(op, cmd_mode) range - let default_buffer = ((&clipboard=~'\')?'*':((&clipboard=~'\' && has('unnamedplus'))?'+':'"')) - let opcode = a:op - let lines = [] - let v_count = v:count - let cmd_mode = a:cmd_mode - let firstline = a:firstline - let lastline = a:lastline - - if a:lastline < a:firstline - let firstline = a:lastline - let lastline = a:firstline - endif - - if cmd_mode == 'n' - let v_count = 1 - " If a count was provided (5p), we want to repeat the paste - " 5 times, but this also alters the a:firstline and a:lastline - " ranges, which while in normal mode we do not want - let lastline = firstline - endif - " If no count was specified it will have a value of 0 - " so set it to at least 1 - let v_count = ((v_count > 0)?(v_count):1) - - if '[dr]' =~ opcode - " Reverse the order of the lines to act on - let begin = lastline - while begin >= firstline - call add(lines, getline(begin)) - let begin = begin - 1 - endwhile - else - " Process the selected items in order - let begin = firstline - while begin <= lastline - call add(lines, getline(begin)) - let begin = begin + 1 - endwhile - endif - - if opcode ==# 'q' - " Close the yankring window - if s:yr_winsize_chgd > 0 - " Adjust the Vim window width back to the width - " it was before we showed the yankring window - let &columns = s:yr_winsize_chgd - " Reset the indicator the window size was changed - let s:yr_winsize_chgd = 0 - endif - - " Hide the YankRing window - hide - - if bufwinnr(s:yr_buffer_last) != -1 - " If the buffer is visible, switch to it - exec s:yr_buffer_last_winnr . "wincmd w" - endif - - return - elseif opcode ==# 's' - " Switch back to the original buffer - exec s:yr_buffer_last_winnr . "wincmd w" - - call s:YRSearch() - return - elseif opcode ==# 'u' - " Switch back to the original buffer - exec s:yr_buffer_last_winnr . "wincmd w" - - call s:YRShow(0) - return - elseif opcode ==# 'R' - " Switch back to the original buffer - exec s:yr_buffer_last_winnr . "wincmd w" - - call s:YRShow(0, 'R') - return - elseif opcode ==# 'a' - let l:curr_line = line(".") - " Toggle the auto close setting - let g:yankring_window_auto_close = - \ (g:yankring_window_auto_close == 1?0:1) - " Display the status line / help - call s:YRWindowStatus(0) - call cursor(l:curr_line,0) - return - elseif opcode ==# 'c' - let l:curr_line = line(".") - " Toggle the clipboard monitor setting - let g:yankring_clipboard_monitor = - \ (g:yankring_clipboard_monitor == 1?0:1) - " Display the status line / help - call s:YRWindowStatus(0) - call cursor(l:curr_line,0) - return - elseif opcode ==# 'i' - let l:curr_line = line(".") - " Toggle the auto close setting - let g:yankring_record_insert = - \ (g:yankring_record_insert == 1?0:1) - " Display the status line / help - call s:YRWindowStatus(0) - call cursor(l:curr_line,0) - return - elseif opcode ==# '?' - " Display the status line / help - call s:YRWindowStatus(1) - return - endif - - " Switch back to the original buffer - exec s:yr_buffer_last_winnr . "wincmd w" - - " Intentional case insensitive comparison - if opcode =~? 'p' - let cmd = 'YRGetElem ' - let parms = ", '".opcode."' " - elseif opcode ==? 'r' - let opcode = 'p' - let cmd = 'YRGetElem ' - let parms = ", 'p' " - elseif opcode ==# 'd' - let cmd = 'YRPop ' - let parms = "" - endif - - " Only execute this code if we are operating on elements - " within the yankring - if '[auq?]' !~# opcode - while v_count > 0 - " let iter = 0 - " let index = 0 - for line in lines - let elem = matchstr(line, '^\d\+') - if elem > 0 - if elem > 0 && elem <= s:yr_count - " if iter > 0 && opcode =~# 'p' - if opcode =~# 'p' - " Move to the end of the last pasted item - " only if pasting after (not above) - " '] - endif - exec cmd . elem . parms - " let iter += 1 - endif - endif - endfor - let v_count = v_count - 1 - endwhile - - if opcode ==# 'd' - call s:YRShow(0) - " Return the user to their last known position, assuming - " it is still available after the delete - if firstline < line("$") - call cursor(firstline,0) - else - call cursor(line("$"),0) - endif - return "" - endif - - if g:yankring_window_auto_close == 1 && cmd_mode == 'v' - exec 'bdelete '.s:yr_buffer_id - return "" - endif - - endif - - return "" - -endfunction - -function! s:YRWarningMsg(msg) - echohl WarningMsg - echomsg a:msg - echohl None -endfunction - -function! s:YRErrorMsg(msg) - echohl ErrorMsg - echomsg a:msg - echohl None -endfunction - -function! s:YRWinLeave() - " Track which window we are last in. We will use this information - " to determine where we need to paste any contents, or which - " buffer to return to. - - if s:yr_buffer_id < 0 - " The yankring window has never been activated - return - endif - - if winbufnr(winnr()) == s:yr_buffer_id - " Ignore leaving the yankring window - return - endif - - if bufwinnr(s:yr_buffer_id) != -1 - " YankRing window is visible, so save off the previous buffer ids - let s:yr_buffer_last_winnr = winnr() - let s:yr_buffer_last = winbufnr(s:yr_buffer_last_winnr) - " else - " let s:yr_buffer_last_winnr = -1 - " let s:yr_buffer_last = -1 - endif -endfunction - -function! s:YRFocusGained() - " FocusGained is not available in general by console vim. - " There are some terminal windows which support it though. - " This thread on vim_use covers some of it: - " http://groups.google.com/group/vim_use/browse_thread/thread/8dd3fb054ee922c6/59bee226473a9eea?lnk=gst&q=console+FocusGained#59bee226473a9eea - " http://groups.google.com/group/vim_dev/browse_thread/thread/ba58fb493a3cf4ba/dc1a22ba1e92579d?lnk=gst&q=terminal+FocusGained#dc1a22ba1e92579d - " Does not work: - " urxvt terminal - " Works: - " GTK2 GUI, on Fedora 11, both as gvim and as vim in a GNOME Terminal - " - " Simple test, create the following autocmd and gain and loose focus in - " the terminal: - " autocmd FocusLost * echomsg "focus lost" - " autocmd FocusGained * echomsg "focus gained" - call s:YRCheckClipboard() - - " If the yankring window is open, refresh it - call s:YRWindowUpdate() -endfunction - -function! s:YRCheckClipboard() - if g:yankring_clipboard_monitor == 1 - " If the clipboard has changed record it inside the yankring - " echomsg "YRCheckClipboard[".len(@*)."][".@*.']['.s:yr_prev_clipboard_star.']' - if has('unnamedplus') && &clipboard =~ '\' - if !empty(@+) && @+ != s:yr_prev_clipboard_plus - let elem = s:YRMRUElemFormat( - \ getreg('+') - \ , getregtype('+') - \ ) - let found = s:YRMRUHas('s:yr_history_list', elem) - - " Only add the item to the "top" of the ring if it is - " not in the ring already. - if found == -1 - call YRRecord3("+") - endif - let s:yr_prev_clipboard_plus = @+ - endif - else - if !empty(@*) && @* != s:yr_prev_clipboard_star - let elem = s:YRMRUElemFormat( - \ getreg('*') - \ , getregtype('*') - \ ) - let found = s:YRMRUHas('s:yr_history_list', elem) - - " Only add the item to the "top" of the ring if it is - " not in the ring already. - if found == -1 - call YRRecord3("*") - endif - let s:yr_prev_clipboard_star = @* - endif - endif - endif -endfunction - -function! s:YRInsertLeave() - " The YankRing uses omaps to execute the prescribed motion - " and then appends to the motion a call to a YankRing - " function to record the contents of the changed register. - " - " We cannot append a function call to the end of a motion - " that results in Insert mode. For example, any command - " like 'cw' enters insert mode. Appending a function call - " after the w, simply writes out the call as if the user - " typed it. - " - " Using the InsertLeave event, allows us to capture the - " contents of any changed register after it completes. - - call YRRecord(s:YRRegister()) - - " When performing a change (not a yank or delete) - " it is not possible to call yrrecord at the end - " of the command (or it's contents will be inserted - " into the buffer instead of executed). - " So, when using ".", we have to remove the _zap_ - " keys and then re-add them back again after we - " record the updates. - if s:yr_remove_omap_dot == 1 - call s:YRMapsCreate('add_only_zap_keys') - endif - - " Check if we should record inserted text - if g:yankring_record_insert == 1 - if !empty(@.) && @. != s:yr_prev_reg_insert - let elem = s:YRMRUElemFormat( - \ getreg('.') - \ , getregtype('.') - \ ) - let found = s:YRMRUHas('s:yr_history_list', elem) - - " Only add the item to the "top" of the ring if it is - " not in the ring already. - if found == -1 - call YRRecord3(".") - endif - let s:yr_prev_reg_insert = @. - endif - endif - -endfunction - -" Deleting autocommands first is a good idea especially if we want to reload -" the script without restarting vim. -" Call YRFocusGained to check if the clipboard has been updated -augroup YankRing - autocmd! - autocmd VimEnter * :if has('clipboard') | call YRFocusGained() | endif - autocmd WinLeave * :call YRWinLeave() - autocmd FocusGained * :if has('clipboard') | call YRFocusGained() | endif - autocmd InsertLeave * :call YRInsertLeave() - autocmd User YRSetNumberedReg :call YRSetNumberedReg() - " autocmd User YRSetNumberedReg :let i = 0 | while i <= 10 | if i > s:yr_count | break | endif | call setreg( (i), s:YRGetValElemNbr((i),'v'), s:YRGetValElemNbr((i),'t') ) | let i += 1 | endwhile -augroup END - - -" copy register -inoremap