1885 changed files with 190433 additions and 68556 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
autocmd BufWinEnter <buffer> wincmd L |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
autocmd BufWinEnter <buffer> wincmd L |
@ -1,2 +1,4 @@
@@ -1,2 +1,4 @@
|
||||
" setlocal syntax=off |
||||
" set re=1 |
||||
setlocal spell |
||||
setlocal spelllang=ru,en |
||||
|
||||
set filetype=markdown.pandoc |
||||
|
@ -1,57 +0,0 @@
@@ -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 |
||||
|
@ -1,55 +0,0 @@
@@ -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 |
||||
|
@ -1,70 +0,0 @@
@@ -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 , / |
||||
|
@ -1,32 +0,0 @@
@@ -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 |
||||
``` |
@ -1,88 +0,0 @@
@@ -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} |
@ -1,199 +0,0 @@
@@ -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. |
@ -1,178 +0,0 @@
@@ -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('<Plug>IncrementCommandLineNumber', 'c') |
||||
cmap <c-a> <Plug>IncrementCommandLineNumber |
||||
endif |
||||
cnoremap <Plug>IncrementCommandLineNumber <c-b>"<cr>:call g:IncrementCommandLineNumbering(1)<cr>:<c-r>=g:IncrementedCommandLine()<cr> |
||||
if !hasmapto('<Plug>DecrementCommandLineNumber', 'c') |
||||
cmap <c-x> <Plug>DecrementCommandLineNumber |
||||
endif |
||||
cnoremap <Plug>DecrementCommandLineNumber <c-b>"<cr>:call g:IncrementCommandLineNumbering(-1)<cr>:<c-r>=g:IncrementedCommandLine()<cr> |
||||
|
||||
" 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 . |
||||
|
||||
<C-a> increment commandline last appearing number. |
||||
<C-x> 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 <S-Up> <Plug>IncrementCommandLineNumber |
||||
" decrement with Shift-Down |
||||
cmap <S-Down> <Plug>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 <c-a>, <c-x>. |
||||
- custom mapping is supported. |
||||
|
||||
|
||||
============================================================================== |
||||
" vim: set ff=unix et ft=vim nowrap : |
@ -0,0 +1,60 @@
@@ -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 |
@ -0,0 +1,21 @@
@@ -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. |
@ -0,0 +1,357 @@
@@ -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. |
||||
|
||||
 |
||||
|
||||
## 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 `<C-a>` / `<C-x>` / `g<C-a>` / `g<C-x>` 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 <C-a> <Plug>(dial-increment) |
||||
nmap <C-x> <Plug>(dial-decrement) |
||||
vmap <C-a> <Plug>(dial-increment) |
||||
vmap <C-x> <Plug>(dial-decrement) |
||||
vmap g<C-a> g<Plug>(dial-increment) |
||||
vmap g<C-x> g<Plug>(dial-decrement) |
||||
``` |
||||
|
||||
Or you can configure it with Lua as follows: |
||||
|
||||
```lua |
||||
vim.api.nvim_set_keymap("n", "<C-a>", require("dial.map").inc_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("n", "<C-x>", require("dial.map").dec_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-a>", require("dial.map").inc_visual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-x>", require("dial.map").dec_visual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<C-a>", require("dial.map").inc_gvisual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<C-x>", 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<CR><C-a> |
||||
``` |
||||
|
||||
If it is tedious to specify the expression register for each operation, you can "map" it: |
||||
|
||||
```vim |
||||
nmap <Leader>a "=mygroup<CR><Plug>(dial-increment) |
||||
``` |
||||
|
||||
Alternatively, you can set the same mapping without expression register: |
||||
|
||||
```lua |
||||
vim.api.nvim_set_keymap("n", "<Leader>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", "<C-a>", require("dial.map").inc_normal("visual"), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-x>", 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", "<C-a>", 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). |
@ -0,0 +1,354 @@
@@ -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 製プラグイン。 |
||||
既存の `<C-a>` や `<C-x>` コマンドを拡張し、数値以外も増減・トグルできるようにします。 |
||||
|
||||
 |
||||
|
||||
## 特徴 |
||||
|
||||
* 数値をはじめとする様々なものの増減 |
||||
* n 進数 (`2 <= n <= 36`) の整数 |
||||
* 日付・時刻 |
||||
* キーワードや演算子など、所定文字列のトグル |
||||
* `true` ⇄ `false` |
||||
* `&&` ⇄ `||` |
||||
* `a` ⇄ `b` ⇄ ... ⇄ `z` |
||||
* `日` ⇄ `月` ⇄ ... ⇄ `土` ⇄ `日` ⇄ ... |
||||
* Hex color |
||||
* SemVer |
||||
* VISUAL mode での `<C-a>` / `<C-x>` / `g<C-a>` / `g<C-x>` に対応 |
||||
* 増減対象の柔軟な設定 |
||||
* 特定のファイルタイプでのみ有効なルールの設定 |
||||
* 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 <C-a> <Plug>(dial-increment) |
||||
nmap <C-x> <Plug>(dial-decrement) |
||||
vmap <C-a> <Plug>(dial-increment) |
||||
vmap <C-x> <Plug>(dial-decrement) |
||||
vmap g<C-a> g<Plug>(dial-increment) |
||||
vmap g<C-x> g<Plug>(dial-decrement) |
||||
``` |
||||
|
||||
または Lua 上で以下のように設定することもできます。 |
||||
|
||||
```lua |
||||
vim.api.nvim_set_keymap("n", "<C-a>", require("dial.map").inc_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("n", "<C-x>", require("dial.map").dec_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-a>", require("dial.map").inc_visual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-x>", require("dial.map").dec_visual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<C-a>", require("dial.map").inc_gvisual(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<C-x>", 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<CR><C-a> |
||||
``` |
||||
|
||||
増減のたびに expression register を指定するのが面倒であれば、以下のようにマッピングすることも可能です。 |
||||
|
||||
```vim |
||||
nmap <Leader>a "=mygroup<CR><Plug>(dial-increment) |
||||
``` |
||||
|
||||
また、 Lua 上で以下のように記述すれば expression register を使わずにマッピングを設定できます。 |
||||
|
||||
```lua |
||||
vim.api.nvim_set_keymap("n", "<Leader>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", "<C-a>", require("dial.map").inc_normal("visual"), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<C-x>", require("dial.map").dec_normal("visual"), {noremap = true}) |
||||
EOF |
||||
|
||||
" 特定のファイルタイプでのみ有効にする |
||||
autocmd FileType typescript lua vim.api.nvim_buf_set_keymap(0, "n", "<C-a>", require("dial.map").inc_normal("typescript"), {noremap = true}) |
||||
autocmd FileType typescript lua vim.api.nvim_buf_set_keymap(0, "n", "<C-x>", 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` を用いています。 |
@ -0,0 +1,61 @@
@@ -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` | |
@ -0,0 +1,59 @@
@@ -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` | |
@ -0,0 +1,23 @@
@@ -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 |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
tags* |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
@@ -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, |
||||
} |
@ -0,0 +1,266 @@
@@ -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<casetype, casepattern> |
||||
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 |
@ -0,0 +1,76 @@
@@ -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 |
@ -0,0 +1,226 @@
@@ -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 |
@ -0,0 +1,728 @@
@@ -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<datekind, integer> |
||||
---@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<string, dateelement> |
||||
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<string, dateelement> |
||||
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<string, dateelement> |
||||
---@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<string, dateelement>} |
||||
---@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 |
@ -0,0 +1,94 @@
@@ -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 |
@ -0,0 +1,180 @@
@@ -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 |
@ -0,0 +1,40 @@
@@ -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 |
@ -0,0 +1,243 @@
@@ -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 |
@ -0,0 +1,107 @@
@@ -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 |
@ -0,0 +1,37 @@
@@ -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 |
@ -0,0 +1,224 @@
@@ -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<C-a>). |
||||
---@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 |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
local augend = require "dial.augend" |
||||
local util = require "dial.util" |
||||
local M = {} |
||||
|
||||
---@class augends |
||||
---@field group table<string, Augend[]> |
||||
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<string, Augend[]> |
||||
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 |
@ -0,0 +1,252 @@
@@ -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 <C-a>/<C-x>, 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 |
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
local M = {} |
||||
|
||||
local command = require "dial.command" |
||||
local util = require "dial.util" |
||||
|
||||
---Sandwich input string between <Cmd> and <CR>. |
||||
---@param body string |
||||
local function cmdcr(body) |
||||
local cmd_sequences = "<Cmd>" |
||||
local cr_sequences = "<CR>" |
||||
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 |
@ -0,0 +1,18 @@
@@ -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 |
@ -0,0 +1,189 @@
@@ -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 |
@ -0,0 +1,38 @@
@@ -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) |
@ -0,0 +1,5 @@
@@ -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) |
@ -0,0 +1,297 @@
@@ -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) |
@ -0,0 +1,201 @@
@@ -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) |
@ -0,0 +1,29 @@
@@ -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) |
@ -0,0 +1,181 @@
@@ -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) |
@ -0,0 +1,21 @@
@@ -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", "<Plug>(dial-increment)", require("dial.map").inc_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("n", "<Plug>(dial-decrement)", require("dial.map").dec_normal(), {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<Plug>(dial-increment)", require("dial.map").inc_visual() .. "gv", {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "<Plug>(dial-decrement)", require("dial.map").dec_visual() .. "gv", {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<Plug>(dial-increment)", require("dial.map").inc_gvisual() .. "gv", {noremap = true}) |
||||
vim.api.nvim_set_keymap("v", "g<Plug>(dial-decrement)", require("dial.map").dec_gvisual() .. "gv", {noremap = true}) |
||||
EOF |
||||
|
||||
command! -range -nargs=? DialIncrement lua require"dial.command".command("increment", {from = <line1>, to = <line2>}, {<f-args>}) |
||||
command! -range -nargs=? DialDecrement lua require"dial.command".command("decrement", {from = <line1>, to = <line2>}, {<f-args>}) |
||||
|
||||
let &cpo = s:save_cpo " and restore after |
||||
unlet s:save_cpo |
||||
|
||||
let g:loaded_dial = 1 |
@ -0,0 +1,21 @@
@@ -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. |
@ -0,0 +1,356 @@
@@ -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) |
||||
|
||||
 |
||||
|
||||
`vim.select` (telescope) |
||||
|
||||
 |
||||
|
||||
`vim.select` (fzf) |
||||
|
||||
 |
||||
|
||||
`vim.select` (nui) |
||||
|
||||
 |
||||
|
||||
`vim.select` (built-in) |
||||
|
||||
 |
||||
|
||||
## Installation |
||||
|
||||
dressing.nvim supports all the usual plugin managers |
||||
|
||||
<details> |
||||
<summary>Packer</summary> |
||||
|
||||
```lua |
||||
require('packer').startup(function() |
||||
use {'stevearc/dressing.nvim'} |
||||
end) |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
<details> |
||||
<summary>Paq</summary> |
||||
|
||||
```lua |
||||
require "paq" { |
||||
{'stevearc/dressing.nvim'}; |
||||
} |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
<details> |
||||
<summary>vim-plug</summary> |
||||
|
||||
```vim |
||||
Plug 'stevearc/dressing.nvim' |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
<details> |
||||
<summary>dein</summary> |
||||
|
||||
```vim |
||||
call dein#add('stevearc/dressing.nvim') |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
<details> |
||||
<summary>Pathogen</summary> |
||||
|
||||
```sh |
||||
git clone --depth=1 https://github.com/stevearc/dressing.nvim.git ~/.vim/bundle/ |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
<details> |
||||
<summary>Neovim native package</summary> |
||||
|
||||
```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 |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
## 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, <Esc> 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 = { |
||||
["<Esc>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
}, |
||||
i = { |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
["<Up>"] = "HistoryPrev", |
||||
["<Down>"] = "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 = { |
||||
["<Esc>"] = "Close", |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "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 |
@ -0,0 +1,12 @@
@@ -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 |
@ -0,0 +1,208 @@
@@ -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, <Esc> 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 = { |
||||
["<Esc>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
}, |
||||
i = { |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
["<Up>"] = "HistoryPrev", |
||||
["<Down>"] = "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 = { |
||||
["<Esc>"] = "Close", |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "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 |
@ -0,0 +1,213 @@
@@ -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, <Esc> 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 = { |
||||
["<Esc>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
}, |
||||
i = { |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "Confirm", |
||||
["<Up>"] = "HistoryPrev", |
||||
["<Down>"] = "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 = { |
||||
["<Esc>"] = "Close", |
||||
["<C-c>"] = "Close", |
||||
["<CR>"] = "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 |
@ -0,0 +1,28 @@
@@ -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 |
@ -0,0 +1,364 @@
@@ -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 = "<Plug>DressingInput:Close", |
||||
rhs = function() |
||||
M.close() |
||||
end, |
||||
}, |
||||
{ |
||||
desc = "Close vim.ui.input with the current buffer contents", |
||||
plug = "<Plug>DressingInput:Confirm", |
||||
rhs = function() |
||||
M.confirm() |
||||
end, |
||||
}, |
||||
{ |
||||
desc = "Show previous vim.ui.input history entry", |
||||
plug = "<Plug>DressingInput:HistoryPrev", |
||||
rhs = function() |
||||
M.history_prev() |
||||
end, |
||||
}, |
||||
{ |
||||
desc = "Show next vim.ui.input history entry", |
||||
plug = "<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("<C-e>", 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 "<C-n>" |
||||
else |
||||
return "<C-x><C-u>" |
||||
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", "<Esc>", 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", "<Tab>", 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 |
@ -0,0 +1,38 @@
@@ -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<string, string> |
||||
---@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 <Plug> unless this is a <Cmd> or :Cmd mapping |
||||
if type(rhs) == "string" and not rhs:match("[<:]") then |
||||
rhs = "<Plug>" .. 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 |
@ -0,0 +1,42 @@
@@ -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 |
@ -0,0 +1,110 @@
@@ -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 = "<Plug>DressingSelect:Close", |
||||
rhs = function() |
||||
M.cancel() |
||||
end, |
||||
}, |
||||
{ |
||||
desc = "Select the current vim.ui.select item under the cursor", |
||||
plug = "<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 |
@ -0,0 +1,41 @@
@@ -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 <buffer> ++once lua require('dressing.select.fzf')._on_term_close()]]) |
||||
end |
||||
|
||||
return M |
@ -0,0 +1,40 @@
@@ -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 |
@ -0,0 +1,74 @@
@@ -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) |
@ -0,0 +1,63 @@
@@ -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", "<Down>", "<Tab>" }, |
||||
focus_prev = { "k", "<Up>", "<S-Tab>" }, |
||||
close = { "<Esc>", "<C-c>" }, |
||||
submit = { "<CR>" }, |
||||
}, |
||||
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 |
@ -0,0 +1,138 @@
@@ -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 |
@ -0,0 +1,177 @@
@@ -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 |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
require("dressing").patch() |
||||
vim.cmd([[highlight default link FloatTitle FloatBorder]]) |
@ -0,0 +1,22 @@
@@ -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" |
@ -0,0 +1,181 @@
@@ -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", |
||||
"<CR>", |
||||
}) |
||||
assert(ret == "my text", string.format("Got '%s' expected 'my text'", ret)) |
||||
end) |
||||
|
||||
a.it("Cancels input on <C-c>", function() |
||||
local ret = run_input({ |
||||
"my text", |
||||
"<C-c>", |
||||
}) |
||||
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 <Esc> when insert_only = true", function() |
||||
require("dressing.config").input.insert_only = true |
||||
local ret = run_input({ |
||||
"my text", |
||||
"<Esc>", |
||||
}) |
||||
assert(ret == nil, string.format("Got '%s' expected nil", ret)) |
||||
end) |
||||
|
||||
a.it("does not cancel on <Esc> when insert_only = false", function() |
||||
require("dressing.config").input.insert_only = false |
||||
local ret = run_input({ |
||||
"my text", |
||||
"<Esc>", |
||||
"<CR>", |
||||
}) |
||||
assert(ret == "my text", string.format("Got '%s' expected 'my text'", ret)) |
||||
end) |
||||
|
||||
a.it("returns cancelreturn when input is canceled <C-c>", function() |
||||
local ret = run_input({ |
||||
"my text", |
||||
"<C-c>", |
||||
}, { 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({ |
||||
"<CR>", |
||||
}) |
||||
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({ |
||||
"<CR>", |
||||
}, { 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", |
||||
"<CR>", |
||||
}, { |
||||
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({ |
||||
"<Tab><Tab><C-n><CR>", -- 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({ |
||||
"<Tab>", |
||||
"<C-c>", |
||||
}, { |
||||
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({ |
||||
"<Tab>", |
||||
"<C-c>", |
||||
}, { |
||||
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 <C-c> with popup menu open deleted buffer text o.0") |
||||
end) |
||||
end) |
@ -0,0 +1,67 @@
@@ -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) |
@ -0,0 +1,30 @@
@@ -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) |
@ -0,0 +1,21 @@
@@ -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") |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
vim.cmd([[set runtimepath+=.]]) |
||||
vim.cmd([[runtime! plugin/plenary.vim]]) |
@ -0,0 +1,19 @@
@@ -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 |
@ -0,0 +1,114 @@
@@ -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 |
||||
|
||||
<img width="475" alt="Screen Shot 2022-03-27 at 12 03 43" src="https://user-images.githubusercontent.com/22454918/160278511-705a0361-a387-4fc1-8b20-bd799bf85b82.png"> |
||||
|
||||
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('<afile>')) |
||||
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: |
||||
|
||||
- <kbd>c</kbd><kbd>o</kbd> — choose ours |
||||
- <kbd>c</kbd><kbd>t</kbd> — choose theirs |
||||
- <kbd>c</kbd><kbd>b</kbd> — choose both |
||||
- <kbd>c</kbd><kbd>0</kbd> — choose none |
||||
- <kbd>]</kbd><kbd>x</kbd> — move to previous conflict |
||||
- <kbd>[</kbd><kbd>x</kbd> — 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', '<Plug>(git-conflict-ours)') |
||||
vim.keymap.set('n', 'ct', '<Plug>(git-conflict-theirs)') |
||||
vim.keymap.set('n', 'cb', '<Plug>(git-conflict-both)') |
||||
vim.keymap.set('n', 'c0', '<Plug>(git-conflict-none)') |
||||
vim.keymap.set('n', ']x', '<Plug>(git-conflict-prev-conflict)') |
||||
vim.keymap.set('n', '[x', '<Plug>(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. |
@ -0,0 +1,20 @@
@@ -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 |
@ -0,0 +1,734 @@
@@ -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<integer, boolean> 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<string, ConflictBufferCache> |
||||
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, integer[]>, 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<string, userdata> |
||||
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', '<Plug>(git-conflict-ours)', '<Cmd>GitConflictChooseOurs<CR>', opts('Choose Ours')) |
||||
map('n', '<Plug>(git-conflict-both)', '<Cmd>GitConflictChooseBoth<CR>', opts('Choose Both')) |
||||
map('n', '<Plug>(git-conflict-none)', '<Cmd>GitConflictChooseNone<CR>', opts('Choose None')) |
||||
map('n', '<Plug>(git-conflict-theirs)', '<Cmd>GitConflictChooseTheirs<CR>', opts('Choose Theirs')) |
||||
map( |
||||
'n', |
||||
'<Plug>(git-conflict-next-conflict)', |
||||
'<Cmd>GitConflictNextConflict<CR>', |
||||
opts('Next Conflict') |
||||
) |
||||
map( |
||||
'n', |
||||
'<Plug>(git-conflict-prev-conflict)', |
||||
'<Cmd>GitConflictPrevConflict<CR>', |
||||
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', '<Plug>(git-conflict-ours)', opts('Choose Ours')) |
||||
map('n', 'cb', '<Plug>(git-conflict-both)', opts('Choose Both')) |
||||
map('n', 'c0', '<Plug>(git-conflict-none)', opts('Choose None')) |
||||
map('n', 'ct', '<Plug>(git-conflict-theirs)', opts('Choose Theirs')) |
||||
map('n', '[x', '<Plug>(git-conflict-prev-conflict)', opts('Previous Conflict')) |
||||
map('n', ']x', '<Plug>(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<string, integer|string> |
||||
---@param items table<string, integer|string>[] |
||||
---@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<string, integer[]>) |
||||
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 |
@ -0,0 +1,35 @@
@@ -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 |
@ -0,0 +1,76 @@
@@ -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<string, string> |
||||
function M.get_hl(name) |
||||
if not name then return {} end |
||||
return api.nvim_get_hl_by_name(name, true) |
||||
end |
||||
|
||||
return M |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
column_width = 100 |
||||
indent_type = 'Spaces' |
||||
quote_style = 'AutoPreferSingle' |
||||
indent_width = 2 |
||||
no_call_parentheses = false |
||||
collapse_simple_statement = "Always" |
@ -0,0 +1,85 @@
@@ -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 {} |
||||
``` |
@ -0,0 +1,21 @@
@@ -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. |
@ -0,0 +1,85 @@
@@ -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
|
@ -0,0 +1,334 @@
@@ -0,0 +1,334 @@
|
||||
# gitsigns.nvim |
||||
|
||||
[](https://github.com/lewis6991/gitsigns.nvim/actions?query=workflow%3ACI) |
||||
[](https://github.com/lewis6991/gitsigns.nvim/releases) |
||||
[](https://opensource.org/licenses/MIT) |
||||
[](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 | |
||||
| --- | ----------- | |
||||
| <img src="https://raw.githubusercontent.com/lewis6991/media/main/gitsigns_actions.gif" width="450em"/> | <img src="https://raw.githubusercontent.com/lewis6991/media/main/gitsigns_blame.gif" width="450em"/> | |
||||
|
||||
## 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 = '<author>, <author_time:%Y-%m-%d> - <summary>', |
||||
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 '<Ignore>' |
||||
end, {expr=true}) |
||||
|
||||
map('n', '[c', function() |
||||
if vim.wo.diff then return '[c' end |
||||
vim.schedule(function() gs.prev_hunk() end) |
||||
return '<Ignore>' |
||||
end, {expr=true}) |
||||
|
||||
-- Actions |
||||
map({'n', 'v'}, '<leader>hs', ':Gitsigns stage_hunk<CR>') |
||||
map({'n', 'v'}, '<leader>hr', ':Gitsigns reset_hunk<CR>') |
||||
map('n', '<leader>hS', gs.stage_buffer) |
||||
map('n', '<leader>hu', gs.undo_stage_hunk) |
||||
map('n', '<leader>hR', gs.reset_buffer) |
||||
map('n', '<leader>hp', gs.preview_hunk) |
||||
map('n', '<leader>hb', function() gs.blame_line{full=true} end) |
||||
map('n', '<leader>tb', gs.toggle_current_line_blame) |
||||
map('n', '<leader>hd', gs.diffthis) |
||||
map('n', '<leader>hD', function() gs.diffthis('~') end) |
||||
map('n', '<leader>td', gs.toggle_deleted) |
||||
|
||||
-- Text object |
||||
map({'o', 'x'}, 'ih', ':<C-U>Gitsigns select_hunk<CR>') |
||||
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: |
||||
<details> |
||||
<summary>Click to expand</summary> |
||||
|
||||
```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' : '<cmd>Gitsigns next_hunk<CR>'", {expr=true}) |
||||
map('n', '[c', "&diff ? '[c' : '<cmd>Gitsigns prev_hunk<CR>'", {expr=true}) |
||||
|
||||
-- Actions |
||||
map('n', '<leader>hs', ':Gitsigns stage_hunk<CR>') |
||||
map('v', '<leader>hs', ':Gitsigns stage_hunk<CR>') |
||||
map('n', '<leader>hr', ':Gitsigns reset_hunk<CR>') |
||||
map('v', '<leader>hr', ':Gitsigns reset_hunk<CR>') |
||||
map('n', '<leader>hS', '<cmd>Gitsigns stage_buffer<CR>') |
||||
map('n', '<leader>hu', '<cmd>Gitsigns undo_stage_hunk<CR>') |
||||
map('n', '<leader>hR', '<cmd>Gitsigns reset_buffer<CR>') |
||||
map('n', '<leader>hp', '<cmd>Gitsigns preview_hunk<CR>') |
||||
map('n', '<leader>hb', '<cmd>lua require"gitsigns".blame_line{full=true}<CR>') |
||||
map('n', '<leader>tb', '<cmd>Gitsigns toggle_current_line_blame<CR>') |
||||
map('n', '<leader>hd', '<cmd>Gitsigns diffthis<CR>') |
||||
map('n', '<leader>hD', '<cmd>lua require"gitsigns".diffthis("~")<CR>') |
||||
map('n', '<leader>td', '<cmd>Gitsigns toggle_deleted<CR>') |
||||
|
||||
-- Text object |
||||
map('o', 'ih', ':<C-U>Gitsigns select_hunk<CR>') |
||||
map('x', 'ih', ':<C-U>Gitsigns select_hunk<CR>') |
||||
end |
||||
} |
||||
``` |
||||
|
||||
</details> |
||||
|
||||
## 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] |
||||
|
||||
<!-- links --> |
||||
[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 |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,165 @@
@@ -0,0 +1,165 @@
|
||||
*gitsigns.txt* Gitsigns |
||||
*gitsigns.nvim* |
||||
|
||||
Author: Lewis Russell <lewis6991@gmail.com> |
||||
Version: {{VERSION}} |
||||
Homepage: <https://github.com/lewis6991/gitsigns.nvim> |
||||
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 |<f-args>|. |
||||
|
||||
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: |
@ -0,0 +1,327 @@
@@ -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() |
@ -0,0 +1,37 @@
@@ -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' |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -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. |
@ -0,0 +1,546 @@
@@ -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, |
||||
}) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,91 @@
@@ -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 |
@ -0,0 +1,87 @@
@@ -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 |
@ -0,0 +1,85 @@
@@ -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 |
@ -0,0 +1,834 @@
@@ -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(<PATTERN>) 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', '<cmd>lua require"gitsigns".stage_hunk()<CR>', {}) |
||||
... -- 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 = ' <author>, <author_time> - <summary>', |
||||
description = [[ |
||||
String or function used to format the virtual text of |
||||
|gitsigns-config-current_line_blame|. |
||||
|
||||
When a string, accepts the following format specifiers: |
||||
|
||||
• `<abbrev_sha>` |
||||
• `<orig_lnum>` |
||||
• `<final_lnum>` |
||||
• `<author>` |
||||
• `<author_mail>` |
||||
• `<author_time>` or `<author_time:FORMAT>` |
||||
• `<author_tz>` |
||||
• `<committer>` |
||||
• `<committer_mail>` |
||||
• `<committer_time>` or `<committer_time:FORMAT>` |
||||
• `<committer_tz>` |
||||
• `<summary>` |
||||
• `<previous>` |
||||
• `<filename>` |
||||
|
||||
For `<author_time:FORMAT>` and `<committer_time:FORMAT>`, `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 = ' <author>', |
||||
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 |
@ -0,0 +1,213 @@
@@ -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 |
@ -0,0 +1,84 @@
@@ -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 |
@ -0,0 +1,150 @@
@@ -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 |
@ -0,0 +1,18 @@
@@ -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 |
@ -0,0 +1,80 @@
@@ -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 |
@ -0,0 +1,153 @@
@@ -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 |
@ -0,0 +1,145 @@
@@ -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 |
@ -0,0 +1,201 @@
@@ -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 |
@ -0,0 +1,620 @@
@@ -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'] = '<not.committed.yet>', |
||||
committer = 'Not Committed Yet', |
||||
['committer_mail'] = '<not.committed.yet>', |
||||
} |
||||
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 |
@ -0,0 +1,17 @@
@@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue