mirror of
https://github.com/junegunn/vim-easy-align.git
synced 2025-11-11 19:33:50 -05:00
Recursive alignment in blockwise-visual mode will align around all the delimiters found after the start of the block. This behavior is inconsistent with non-recursive alignment where the delimiters after the end of the block are ignored. Making it respect the end of the block is non-trivial and may not be intuitive as each alignment will change the position of the delimiters inside the block and some of them will end up beyond the scope of the block.
1114 lines
32 KiB
VimL
1114 lines
32 KiB
VimL
" Copyright (c) 2013 Junegunn Choi
|
|
"
|
|
" MIT License
|
|
"
|
|
" 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.
|
|
|
|
if exists("g:loaded_easy_align")
|
|
finish
|
|
endif
|
|
let g:loaded_easy_align = 1
|
|
|
|
let s:cpo_save = &cpo
|
|
set cpo&vim
|
|
|
|
let s:easy_align_delimiters_default = {
|
|
\ ' ': { 'pattern': ' ', 'left_margin': 0, 'right_margin': 0, 'stick_to_left': 0 },
|
|
\ '=': { 'pattern': '===\|<=>\|\(&&\|||\|<<\|>>\)=\|=\~[#?]\?\|=>\|[:+/*!%^=><&|.-]\?=[#?]\?',
|
|
\ 'left_margin': 1, 'right_margin': 1, 'stick_to_left': 0 },
|
|
\ ':': { 'pattern': ':', 'left_margin': 0, 'right_margin': 1, 'stick_to_left': 1 },
|
|
\ ',': { 'pattern': ',', 'left_margin': 0, 'right_margin': 1, 'stick_to_left': 1 },
|
|
\ '|': { 'pattern': '|', 'left_margin': 1, 'right_margin': 1, 'stick_to_left': 0 },
|
|
\ '.': { 'pattern': '\.', 'left_margin': 0, 'right_margin': 0, 'stick_to_left': 0 },
|
|
\ '#': { 'pattern': '#\+', 'delimiter_align': 'l', 'ignore_groups': ['!Comment'] },
|
|
\ '&': { 'pattern': '\\\@<!&\|\\\\',
|
|
\ 'left_margin': 1, 'right_margin': 1, 'stick_to_left': 0 },
|
|
\ '{': { 'pattern': '(\@<!{',
|
|
\ 'left_margin': 1, 'right_margin': 1, 'stick_to_left': 0 },
|
|
\ '}': { 'pattern': '}', 'left_margin': 1, 'right_margin': 0, 'stick_to_left': 0 }
|
|
\ }
|
|
|
|
let s:mode_labels = { 'l': '', 'r': '[R]', 'c': '[C]' }
|
|
|
|
let s:known_options = {
|
|
\ 'margin_left': [0, 1], 'margin_right': [0, 1], 'stick_to_left': [0],
|
|
\ 'left_margin': [0, 1], 'right_margin': [0, 1], 'indentation': [1],
|
|
\ 'ignore_groups': [3 ], 'ignore_unmatched': [0 ], 'delimiter_align': [1],
|
|
\ 'mode_sequence': [1 ], 'ignores': [3], 'filter': [1],
|
|
\ 'align': [1 ]
|
|
\ }
|
|
|
|
let s:option_values = {
|
|
\ 'indentation': ['shallow', 'deep', 'none', 'keep', -1],
|
|
\ 'delimiter_align': ['left', 'center', 'right', -1],
|
|
\ 'ignore_unmatched': [0, 1, -1],
|
|
\ 'ignore_groups': [[], ['String'], ['Comment'], ['String', 'Comment'], -1]
|
|
\ }
|
|
|
|
let s:shorthand = {
|
|
\ 'margin_left': 'lm', 'margin_right': 'rm', 'stick_to_left': 'stl',
|
|
\ 'left_margin': 'lm', 'right_margin': 'rm', 'indentation': 'idt',
|
|
\ 'ignore_groups': 'ig', 'ignore_unmatched': 'iu', 'delimiter_align': 'da',
|
|
\ 'mode_sequence': 'a', 'ignores': 'ig', 'filter': 'f',
|
|
\ 'align': 'a'
|
|
\ }
|
|
|
|
if exists("*strdisplaywidth")
|
|
function! s:strwidth(str)
|
|
return strdisplaywidth(a:str)
|
|
endfunction
|
|
else
|
|
function! s:strwidth(str)
|
|
return len(split(a:str, '\zs')) + len(matchstr(a:str, '^\t*')) * (&tabstop - 1)
|
|
endfunction
|
|
endif
|
|
|
|
function! s:ceil2(v)
|
|
return a:v % 2 == 0 ? a:v : a:v + 1
|
|
endfunction
|
|
|
|
function! s:floor2(v)
|
|
return a:v % 2 == 0 ? a:v : a:v - 1
|
|
endfunction
|
|
|
|
function! s:highlighted_as(line, col, groups)
|
|
if empty(a:groups) | return 0 | endif
|
|
let hl = synIDattr(synID(a:line, a:col, 0), 'name')
|
|
for grp in a:groups
|
|
if grp[0] == '!'
|
|
if hl !~# grp[1:-1]
|
|
return 1
|
|
endif
|
|
elseif hl =~# grp
|
|
return 1
|
|
endif
|
|
endfor
|
|
return 0
|
|
endfunction
|
|
|
|
function! s:ignored_syntax()
|
|
if has('syntax') && exists('g:syntax_on')
|
|
" Backward-compatibility
|
|
return get(g:, 'easy_align_ignore_groups',
|
|
\ get(g:, 'easy_align_ignores',
|
|
\ (get(g:, 'easy_align_ignore_comment', 1) == 0) ?
|
|
\ ['String'] : ['String', 'Comment']))
|
|
else
|
|
return []
|
|
endif
|
|
endfunction
|
|
|
|
function! s:echon_(tokens)
|
|
" http://vim.wikia.com/wiki/How_to_print_full_screen_width_messages
|
|
let xy = [&ruler, &showcmd]
|
|
try
|
|
set noruler noshowcmd
|
|
|
|
let winlen = winwidth(winnr()) - 2
|
|
let len = len(join(map(copy(a:tokens), 'v:val[1]'), ''))
|
|
let ellipsis = len > winlen ? '..' : ''
|
|
|
|
echon "\r"
|
|
let yet = 0
|
|
for [hl, msg] in a:tokens
|
|
if empty(msg) | continue | endif
|
|
execute "echohl ". hl
|
|
let yet += len(msg)
|
|
if yet > winlen - len(ellipsis)
|
|
echon msg[ 0 : (winlen - len(ellipsis) - yet - 1) ] . ellipsis
|
|
break
|
|
else
|
|
echon msg
|
|
endif
|
|
endfor
|
|
finally
|
|
echohl None
|
|
let [&ruler, &showcmd] = xy
|
|
endtry
|
|
endfunction
|
|
|
|
function! s:echon(l, n, r, d, o, warn)
|
|
let tokens = [
|
|
\ ['Function', ':EasyAlign'],
|
|
\ ['ModeMsg', get(s:mode_labels, a:l, a:l)],
|
|
\ ['None', ' ']]
|
|
|
|
if a:r == -1 | call add(tokens, ['Comment', '(']) | endif
|
|
call add(tokens, [a:n =~ '*' ? 'Repeat' : 'Number', a:n])
|
|
call extend(tokens, a:r == 1 ?
|
|
\ [['Delimiter', '/'], ['String', a:d], ['Delimiter', '/']] :
|
|
\ [['Identifier', a:d == ' ' ? '\ ' : (a:d == '\' ? '\\' : a:d)]])
|
|
if a:r == -1 | call extend(tokens, [['Normal', '_'], ['Comment', ')']]) | endif
|
|
call add(tokens, ['Statement', empty(a:o) ? '' : ' '.string(a:o)])
|
|
if !empty(a:warn)
|
|
call add(tokens, ['WarningMsg', ' ('.a:warn.')'])
|
|
endif
|
|
|
|
call s:echon_(tokens)
|
|
return join(map(tokens, 'v:val[1]'), '')
|
|
endfunction
|
|
|
|
function! s:exit(msg)
|
|
call s:echon_([['ErrorMsg', a:msg]])
|
|
throw 'exit'
|
|
endfunction
|
|
|
|
function! s:ltrim(str)
|
|
return substitute(a:str, '^\s\+', '', '')
|
|
endfunction
|
|
|
|
function! s:rtrim(str)
|
|
return substitute(a:str, '\s\+$', '', '')
|
|
endfunction
|
|
|
|
function! s:trim(str)
|
|
return substitute(a:str, '^\s*\(.\{-}\)\s*$', '\1', '')
|
|
endfunction
|
|
|
|
function! s:fuzzy_lu(key)
|
|
if has_key(s:known_options, a:key)
|
|
return a:key
|
|
endif
|
|
let key = tolower(a:key)
|
|
|
|
" stl -> ^s.*_t.*_l.*
|
|
let regexp1 = '^' .key[0]. '.*' .substitute(key[1 : -1], '\(.\)', '_\1.*', 'g')
|
|
let matches = filter(keys(s:known_options), 'v:val =~ regexp1')
|
|
if len(matches) == 1
|
|
return matches[0]
|
|
endif
|
|
|
|
" stl -> ^s.*t.*l.*
|
|
let regexp2 = '^' . substitute(substitute(key, '-', '_', 'g'), '\(.\)', '\1.*', 'g')
|
|
let matches = filter(keys(s:known_options), 'v:val =~ regexp2')
|
|
|
|
if empty(matches)
|
|
call s:exit("Unknown option key: ". a:key)
|
|
elseif len(matches) == 1
|
|
return matches[0]
|
|
else
|
|
" Avoid ambiguity introduced by deprecated margin_left and margin_right
|
|
if sort(matches) == ['margin_left', 'margin_right', 'mode_sequence']
|
|
return 'mode_sequence'
|
|
endif
|
|
if sort(matches) == ['ignore_groups', 'ignores']
|
|
return 'ignore_groups'
|
|
endif
|
|
call s:exit("Ambiguous option key: ". a:key ." (" .join(matches, ', '). ")")
|
|
endif
|
|
endfunction
|
|
|
|
function! s:shift(modes, cycle)
|
|
let item = remove(a:modes, 0)
|
|
if a:cycle || empty(a:modes)
|
|
call add(a:modes, item)
|
|
endif
|
|
return item
|
|
endfunction
|
|
|
|
function! s:normalize_options(opts)
|
|
let ret = {}
|
|
for k in keys(a:opts)
|
|
let v = a:opts[k]
|
|
let k = s:fuzzy_lu(k)
|
|
" Backward-compatibility
|
|
if k == 'margin_left' | let k = 'left_margin' | endif
|
|
if k == 'margin_right' | let k = 'right_margin' | endif
|
|
if k == 'mode_sequence' | let k = 'align' | endif
|
|
let ret[k] = v
|
|
unlet v
|
|
endfor
|
|
return s:validate_options(ret)
|
|
endfunction
|
|
|
|
function! s:compact_options(opts)
|
|
let ret = {}
|
|
for k in keys(a:opts)
|
|
let ret[s:shorthand[k]] = a:opts[k]
|
|
endfor
|
|
return ret
|
|
endfunction
|
|
|
|
function! s:validate_options(opts)
|
|
for k in keys(a:opts)
|
|
let v = a:opts[k]
|
|
if index(s:known_options[k], type(v)) == -1
|
|
call s:exit("Invalid type for option: ". k)
|
|
endif
|
|
unlet v
|
|
endfor
|
|
return a:opts
|
|
endfunction
|
|
|
|
function! s:split_line(line, nth, modes, cycle, fc, lc, pattern, stick_to_left, ignore_unmatched, ignore_groups)
|
|
let mode = ''
|
|
|
|
let string = a:lc ?
|
|
\ strpart(getline(a:line), a:fc - 1, a:lc - a:fc + 1) :
|
|
\ strpart(getline(a:line), a:fc - 1)
|
|
let idx = 0
|
|
" Do not allow \zs
|
|
" 1: whole match
|
|
" 2: token
|
|
" 3: delimiter
|
|
let pattern = '^\(\(.\{-}\s*\)\(' .a:pattern. '\)\s' . (a:stick_to_left ? '*' : '\{-}') . '\)'
|
|
let tokens = []
|
|
let delims = []
|
|
|
|
" Phase 1: split
|
|
let ignorable = 0
|
|
let token = ''
|
|
let phantom = 0
|
|
while 1
|
|
let matches = matchlist(string, pattern, idx)
|
|
" No match
|
|
if empty(matches) | break | endif
|
|
|
|
" Match, but empty delimiter
|
|
if empty(matches[1])
|
|
let char = strpart(string, idx, 1)
|
|
if empty(char) | break | endif
|
|
let [match, part, delim] = [char, char, '']
|
|
" Match
|
|
else
|
|
let [match, part, delim] = matches[1 : 3]
|
|
endif
|
|
|
|
let ignorable = s:highlighted_as(a:line, idx + len(part) + a:fc, a:ignore_groups)
|
|
if ignorable
|
|
let token .= match
|
|
else
|
|
let [pmode, mode] = [mode, s:shift(a:modes, a:cycle)]
|
|
call add(tokens, token . match)
|
|
call add(delims, delim)
|
|
let token = ''
|
|
endif
|
|
|
|
let idx += len(match)
|
|
|
|
" If the string is non-empty and ends with the delimiter,
|
|
" append an empty token to the list
|
|
if idx == len(string)
|
|
let phantom = 1
|
|
break
|
|
endif
|
|
endwhile
|
|
|
|
let leftover = token . strpart(string, idx)
|
|
if !empty(leftover)
|
|
let ignorable = s:highlighted_as(a:line, len(string) + a:fc - 1, a:ignore_groups)
|
|
call add(tokens, leftover)
|
|
call add(delims, '')
|
|
elseif phantom
|
|
call add(tokens, '')
|
|
call add(delims, '')
|
|
endif
|
|
let [pmode, mode] = [mode, s:shift(a:modes, a:cycle)]
|
|
|
|
" Preserve indentation - merge first two tokens
|
|
if len(tokens) > 1 && empty(s:rtrim(tokens[0]))
|
|
let tokens[1] = tokens[0] . tokens[1]
|
|
call remove(tokens, 0)
|
|
call remove(delims, 0)
|
|
let mode = pmode
|
|
endif
|
|
|
|
" Skip comment line
|
|
if ignorable && len(tokens) == 1 && a:ignore_unmatched
|
|
let tokens = []
|
|
let delims = []
|
|
" Append an empty item to enable right/center alignment of the last token
|
|
" - if the last token is not ignorable or ignorable but not the only token
|
|
elseif a:ignore_unmatched != 1 &&
|
|
\ (mode ==? 'r' || mode ==? 'c') &&
|
|
\ (!ignorable || len(tokens) > 1) &&
|
|
\ a:nth >= 0 " includes -0
|
|
call add(tokens, '')
|
|
call add(delims, '')
|
|
endif
|
|
|
|
return [tokens, delims]
|
|
endfunction
|
|
|
|
function! s:do_align(todo, modes, all_tokens, all_delims, fl, ll, fc, lc, nth, recur, dict)
|
|
let mode = a:modes[0]
|
|
let lines = {}
|
|
let min_indent = -1
|
|
let max = { 'pivot_len2': 0, 'token_len': 0, 'just_len': 0, 'delim_len': 0,
|
|
\ 'indent': 0, 'tokens': 0, 'strip_len': 0 }
|
|
let d = a:dict
|
|
let [f, fx] = s:parse_filter(d.filter)
|
|
|
|
" Phase 1
|
|
for line in range(a:fl, a:ll)
|
|
if f == 1 && getline(line) !~ fx
|
|
continue
|
|
elseif f == -1 && getline(line) =~ fx
|
|
continue
|
|
endif
|
|
|
|
if !has_key(a:all_tokens, line)
|
|
" Split line into the tokens by the delimiters
|
|
let [tokens, delims] = s:split_line(
|
|
\ line, a:nth, copy(a:modes), a:recur == 2,
|
|
\ a:fc, a:lc, d.pattern,
|
|
\ d.stick_to_left, d.ignore_unmatched, d.ignore_groups)
|
|
|
|
" Remember tokens for subsequent recursive calls
|
|
let a:all_tokens[line] = tokens
|
|
let a:all_delims[line] = delims
|
|
else
|
|
let tokens = a:all_tokens[line]
|
|
let delims = a:all_delims[line]
|
|
endif
|
|
|
|
" Skip empty lines
|
|
if empty(tokens)
|
|
continue
|
|
endif
|
|
|
|
" Calculate the maximum number of tokens for a line within the range
|
|
let max.tokens = max([max.tokens, len(tokens)])
|
|
|
|
if a:nth > 0 " Positive N-th
|
|
if len(tokens) < a:nth
|
|
continue
|
|
endif
|
|
let nth = a:nth - 1 " make it 0-based
|
|
else " -0 or Negative N-th
|
|
if a:nth == 0 && mode !=? 'l'
|
|
let nth = len(tokens) - 1
|
|
else
|
|
let nth = len(tokens) + a:nth
|
|
endif
|
|
if empty(delims[len(delims) - 1])
|
|
let nth -= 1
|
|
endif
|
|
|
|
if nth < 0 || nth == len(tokens)
|
|
continue
|
|
endif
|
|
endif
|
|
|
|
let prefix = nth > 0 ? join(tokens[0 : nth - 1], '') : ''
|
|
let delim = delims[nth]
|
|
let token = s:rtrim( tokens[nth] )
|
|
let token = s:rtrim( strpart(token, 0, len(token) - len(s:rtrim(delim))) )
|
|
if empty(delim) && !exists('tokens[nth + 1]') && d.ignore_unmatched
|
|
continue
|
|
endif
|
|
|
|
let indent = s:strwidth(matchstr(tokens[0], '^\s*'))
|
|
if min_indent < 0 || indent < min_indent
|
|
let min_indent = indent
|
|
endif
|
|
if mode ==? 'c'
|
|
let token .= substitute(matchstr(token, '^\s*'), '\t', repeat(' ', &tabstop), 'g')
|
|
endif
|
|
let [pw, tw] = [s:strwidth(prefix), s:strwidth(token)]
|
|
let max.indent = max([max.indent, indent])
|
|
let max.token_len = max([max.token_len, tw])
|
|
let max.just_len = max([max.just_len, pw + tw])
|
|
let max.delim_len = max([max.delim_len, s:strwidth(delim)])
|
|
|
|
if mode ==? 'c'
|
|
let pivot_len2 = pw * 2 + tw
|
|
if max.pivot_len2 < pivot_len2
|
|
let max.pivot_len2 = pivot_len2
|
|
endif
|
|
let max.strip_len = max([max.strip_len, s:strwidth(s:trim(token))])
|
|
endif
|
|
let lines[line] = [nth, prefix, token, delim]
|
|
endfor
|
|
|
|
" Phase 1-5: indentation handling (only on a:nth == 1)
|
|
if a:nth == 1
|
|
let idt = d.indentation
|
|
if idt ==? 'd'
|
|
let indent = max.indent
|
|
elseif idt ==? 's'
|
|
let indent = min_indent
|
|
elseif idt ==? 'n'
|
|
let indent = 0
|
|
elseif idt !=? 'k'
|
|
call s:exit('Invalid indentation: ' . idt)
|
|
end
|
|
|
|
if idt !=? 'k'
|
|
let max.just_len = 0
|
|
let max.token_len = 0
|
|
let max.pivot_len2 = 0
|
|
|
|
for [line, elems] in items(lines)
|
|
let [nth, prefix, token, delim] = elems
|
|
|
|
let tindent = matchstr(token, '^\s*')
|
|
while 1
|
|
let len = s:strwidth(tindent)
|
|
if len < indent
|
|
let tindent .= repeat(' ', indent - len)
|
|
break
|
|
elseif len > indent
|
|
let tindent = tindent[0 : -2]
|
|
else
|
|
break
|
|
endif
|
|
endwhile
|
|
|
|
let token = tindent . s:ltrim(token)
|
|
if mode ==? 'c'
|
|
let token = substitute(token, '\s*$', repeat(' ', indent), '')
|
|
endif
|
|
let [pw, tw] = [s:strwidth(prefix), s:strwidth(token)]
|
|
let max.token_len = max([max.token_len, tw])
|
|
let max.just_len = max([max.just_len, pw + tw])
|
|
if mode ==? 'c'
|
|
let pivot_len2 = pw * 2 + tw
|
|
if max.pivot_len2 < pivot_len2
|
|
let max.pivot_len2 = pivot_len2
|
|
endif
|
|
endif
|
|
|
|
let lines[line][2] = token
|
|
endfor
|
|
endif
|
|
endif
|
|
|
|
" Phase 2
|
|
for [line, elems] in items(lines)
|
|
let tokens = a:all_tokens[line]
|
|
let delims = a:all_delims[line]
|
|
let [nth, prefix, token, delim] = elems
|
|
|
|
" Remove the leading whitespaces of the next token
|
|
if len(tokens) > nth + 1
|
|
let tokens[nth + 1] = s:ltrim(tokens[nth + 1])
|
|
endif
|
|
|
|
" Pad the token with spaces
|
|
let [pw, tw] = [s:strwidth(prefix), s:strwidth(token)]
|
|
let rpad = ''
|
|
if mode ==? 'l'
|
|
let pad = repeat(' ', max.just_len - pw - tw)
|
|
if d.stick_to_left
|
|
let rpad = pad
|
|
else
|
|
let token = token . pad
|
|
endif
|
|
elseif mode ==? 'r'
|
|
let pad = repeat(' ', max.just_len - pw - tw)
|
|
let indent = matchstr(token, '^\s*')
|
|
let token = indent . pad . s:ltrim(token)
|
|
elseif mode ==? 'c'
|
|
let p1 = max.pivot_len2 - (pw * 2 + tw)
|
|
let p2 = max.token_len - tw
|
|
let pf1 = s:floor2(p1)
|
|
if pf1 < p1 | let p2 = s:ceil2(p2)
|
|
else | let p2 = s:floor2(p2)
|
|
endif
|
|
let strip = s:ceil2(max.token_len - max.strip_len) / 2
|
|
let indent = matchstr(token, '^\s*')
|
|
let token = indent. repeat(' ', pf1 / 2) .s:ltrim(token). repeat(' ', p2 / 2)
|
|
let token = substitute(token, repeat(' ', strip) . '$', '', '')
|
|
|
|
if d.stick_to_left
|
|
if empty(s:rtrim(token))
|
|
let center = len(token) / 2
|
|
let [token, rpad] = [strpart(token, 0, center), strpart(token, center)]
|
|
else
|
|
let [token, rpad] = [s:rtrim(token), matchstr(token, '\s*$')]
|
|
endif
|
|
endif
|
|
endif
|
|
let tokens[nth] = token
|
|
|
|
" Pad the delimiter
|
|
let dpadl = max.delim_len - s:strwidth(delim)
|
|
let da = d.delimiter_align
|
|
if da ==? 'l'
|
|
let [dl, dr] = ['', repeat(' ', dpadl)]
|
|
elseif da ==? 'c'
|
|
let dl = repeat(' ', dpadl / 2)
|
|
let dr = repeat(' ', dpadl - dpadl / 2)
|
|
elseif da ==? 'r'
|
|
let [dl, dr] = [repeat(' ', dpadl), '']
|
|
else
|
|
call s:exit('Invalid delimiter_align: ' . da)
|
|
endif
|
|
|
|
" Before and after the range (for blockwise visual mode)
|
|
let cline = getline(line)
|
|
let before = strpart(cline, 0, a:fc - 1)
|
|
let after = a:lc ? strpart(cline, a:lc) : ''
|
|
|
|
" Determine the left and right margin around the delimiter
|
|
let rest = join(tokens[nth + 1 : -1], '')
|
|
let nomore = empty(rest.after)
|
|
let ml = (empty(prefix . token) || empty(delim) && nomore) ? '' : d.ml
|
|
let mr = nomore ? '' : d.mr
|
|
|
|
" Adjust indentation of the lines starting with a delimiter
|
|
let lpad = ''
|
|
if nth == 0
|
|
let ipad = repeat(' ', min_indent - s:strwidth(token.ml))
|
|
if mode ==? 'l'
|
|
let token = ipad . token
|
|
else
|
|
let lpad = ipad
|
|
endif
|
|
endif
|
|
|
|
" Align the token
|
|
let aligned = join([lpad, token, ml, dl, delim, dr, mr, rpad], '')
|
|
let tokens[nth] = aligned
|
|
|
|
" Update the line
|
|
let a:todo[line] = before.join(tokens, '').after
|
|
endfor
|
|
|
|
if a:nth < max.tokens && (a:recur || len(a:modes) > 1)
|
|
call s:shift(a:modes, a:recur == 2)
|
|
return [a:todo, a:modes, a:all_tokens, a:all_delims,
|
|
\ a:fl, a:ll, a:fc, a:lc, a:nth + 1, a:recur, a:dict]
|
|
endif
|
|
return [a:todo]
|
|
endfunction
|
|
|
|
function! s:input(str, default, vis)
|
|
if a:vis
|
|
normal! gv
|
|
redraw
|
|
execute "normal! \<esc>"
|
|
else
|
|
" EasyAlign command can be called without visual selection
|
|
redraw
|
|
endif
|
|
let got = input(a:str, a:default)
|
|
return got
|
|
endfunction
|
|
|
|
function! s:atoi(str)
|
|
return (a:str =~ '^[0-9]\+$') ? str2nr(a:str) : a:str
|
|
endfunction
|
|
|
|
function! s:shift_opts(opts, key, vals)
|
|
let val = s:shift(a:vals, 1)
|
|
if type(val) == 0 && val == -1
|
|
call remove(a:opts, a:key)
|
|
else
|
|
let a:opts[a:key] = val
|
|
endif
|
|
endfunction
|
|
|
|
function! s:interactive(range, modes, n, d, opts, rules, vis, live, bvis)
|
|
let mode = s:shift(a:modes, 1)
|
|
let n = a:n
|
|
let d = a:d
|
|
let ch = ''
|
|
let opts = s:compact_options(a:opts)
|
|
let vals = deepcopy(s:option_values)
|
|
let regx = 0
|
|
let warn = ''
|
|
let undo = 0
|
|
|
|
while 1
|
|
" Live preview
|
|
let rdrw = 0
|
|
if undo
|
|
silent! undo
|
|
let undo = 0
|
|
let rdrw = 1
|
|
endif
|
|
if a:live && !empty(d)
|
|
let output = s:process(a:range, mode, n, d, s:normalize_options(opts), regx, a:rules, a:bvis)
|
|
let &undolevels = &undolevels " Break undo block
|
|
call s:update_lines(output.todo)
|
|
let undo = 1
|
|
let rdrw = 1
|
|
endif
|
|
if rdrw
|
|
if a:vis
|
|
normal! gv
|
|
endif
|
|
redraw
|
|
if a:vis | execute "normal! \<esc>" | endif
|
|
endif
|
|
call s:echon(mode, n, -1, regx ? '/'.d.'/' : d, opts, warn)
|
|
|
|
let check = 0
|
|
let warn = ''
|
|
|
|
let c = getchar()
|
|
let ch = nr2char(c)
|
|
if c == 3 || c == 27 " CTRL-C / ESC
|
|
if undo
|
|
silent! undo
|
|
endif
|
|
throw 'exit'
|
|
elseif c == "\<bs>"
|
|
if !empty(d)
|
|
let d = ''
|
|
let regx = 0
|
|
elseif len(n) > 0
|
|
let n = strpart(n, 0, len(n) - 1)
|
|
endif
|
|
elseif c == 13 " Enter key
|
|
let mode = s:shift(a:modes, 1)
|
|
if has_key(opts, 'a')
|
|
let opts.m = mode . strpart(opts.m, 1)
|
|
endif
|
|
elseif ch == '-'
|
|
if empty(n) | let n = '-'
|
|
elseif n == '-' | let n = ''
|
|
else | let check = 1
|
|
endif
|
|
elseif ch == '*'
|
|
if empty(n) | let n = '*'
|
|
elseif n == '*' | let n = '**'
|
|
elseif n == '**' | let n = ''
|
|
else | let check = 1
|
|
endif
|
|
elseif empty(d) && ((c == 48 && len(n) > 0) || c > 48 && c <= 57) " Numbers
|
|
if n[0] == '*' | let check = 1
|
|
else | let n = n . ch
|
|
end
|
|
elseif ch == "\<C-D>"
|
|
call s:shift_opts(opts, 'da', vals['delimiter_align'])
|
|
elseif ch == "\<C-I>"
|
|
call s:shift_opts(opts, 'idt', vals['indentation'])
|
|
elseif ch == "\<C-L>"
|
|
let lm = s:input("Left margin: ", get(opts, 'lm', ''), a:vis)
|
|
if empty(lm)
|
|
let warn = 'Set to default. Input 0 to remove it'
|
|
silent! call remove(opts, 'lm')
|
|
else
|
|
let opts['lm'] = s:atoi(lm)
|
|
endif
|
|
elseif ch == "\<C-R>"
|
|
let rm = s:input("Right margin: ", get(opts, 'rm', ''), a:vis)
|
|
if empty(rm)
|
|
let warn = 'Set to default. Input 0 to remove it'
|
|
silent! call remove(opts, 'rm')
|
|
else
|
|
let opts['rm'] = s:atoi(rm)
|
|
endif
|
|
elseif ch == "\<C-U>"
|
|
call s:shift_opts(opts, 'iu', vals['ignore_unmatched'])
|
|
elseif ch == "\<C-G>"
|
|
call s:shift_opts(opts, 'ig', vals['ignore_groups'])
|
|
elseif c == "\<Left>"
|
|
let opts['stl'] = 1
|
|
let opts['lm'] = 0
|
|
elseif c == "\<Right>"
|
|
let opts['stl'] = 0
|
|
let opts['lm'] = 1
|
|
elseif c == "\<Down>"
|
|
let opts['lm'] = 0
|
|
let opts['rm'] = 0
|
|
elseif c == "\<Up>"
|
|
silent! call remove(opts, 'stl')
|
|
silent! call remove(opts, 'lm')
|
|
silent! call remove(opts, 'rm')
|
|
elseif ch == "\<C-A>" || ch == "\<C-O>"
|
|
let modes = tolower(s:input("Alignment ([lrc...][[*]*]): ", get(opts, 'a', mode), a:vis))
|
|
if match(modes, '^[lrc]\+\*\{0,2}$') != -1
|
|
let opts['a'] = modes
|
|
let mode = modes[0]
|
|
while mode != s:shift(a:modes, 1)
|
|
endwhile
|
|
else
|
|
silent! call remove(opts, 'a')
|
|
endif
|
|
elseif ch == "\<C-_>" || ch == "\<C-X>"
|
|
if a:live && regx && !empty(d)
|
|
break
|
|
endif
|
|
|
|
let prompt = 'Regular expression: '
|
|
let ch = s:input(prompt, '', a:vis)
|
|
if !empty(ch) && s:valid_regexp(ch)
|
|
let regx = 1
|
|
let d = ch
|
|
if !a:live | break | endif
|
|
else
|
|
let warn = 'Invalid regular expression: '.ch
|
|
endif
|
|
elseif ch == "\<C-F>"
|
|
let f = s:input("Filter (g/../ or v/../): ", get(opts, 'f', ''), a:vis)
|
|
let m = matchlist(f, '^[gv]/\(.\{-}\)/\?$')
|
|
if empty(f)
|
|
silent! call remove(opts, 'f')
|
|
elseif !empty(m) && s:valid_regexp(m[1])
|
|
let opts['f'] = f
|
|
else
|
|
let warn = 'Invalid filter expression'
|
|
endif
|
|
elseif ch =~ '[[:print:]]'
|
|
let check = 1
|
|
else
|
|
let warn = 'Invalid character'
|
|
endif
|
|
|
|
if check
|
|
if empty(d)
|
|
if has_key(a:rules, ch)
|
|
let d = ch
|
|
if !a:live
|
|
break
|
|
endif
|
|
else
|
|
let warn = 'Unknown delimiter key: '.ch
|
|
endif
|
|
else
|
|
if regx
|
|
let warn = 'Press <CTRL-X> to finish'
|
|
else
|
|
if d == ch
|
|
break
|
|
else
|
|
let warn = 'Press '''.d.''' again to finish'
|
|
endif
|
|
end
|
|
endif
|
|
endif
|
|
endwhile
|
|
if a:live
|
|
let copts = call('s:summarize', output.summarize)
|
|
let g:easy_align_last_command = s:echon('', n, regx, d, copts, '')
|
|
end
|
|
return [mode, n, ch, opts, regx]
|
|
endfunction
|
|
|
|
function! s:valid_regexp(regexp)
|
|
try
|
|
call matchlist('', a:regexp)
|
|
catch
|
|
return 0
|
|
endtry
|
|
return 1
|
|
endfunction
|
|
|
|
function! s:test_regexp(regexp)
|
|
if !s:valid_regexp(a:regexp)
|
|
call s:exit('Invalid regular expression: '. a:regexp)
|
|
endif
|
|
return a:regexp
|
|
endfunction
|
|
|
|
let s:shorthand_regex =
|
|
\ '\s*\%('
|
|
\ .'\(lm\?[0-9]\+\)\|\(rm\?[0-9]\+\)\|\(iu[01]\)\|\(\%(s\%(tl\)\?[01]\)\|[<>]\)\|'
|
|
\ .'\(da\?[clr]\)\|\(\%(ms\?\|a\)[lrc*]\+\)\|\(i\%(dt\)\?[kdsn]\)\|\([gv]/.*/\)\|\(ig\[.*\]\)'
|
|
\ .'\)\+\s*$'
|
|
|
|
function! s:parse_shorthand_opts(expr)
|
|
let opts = {}
|
|
let expr = substitute(a:expr, '\s', '', 'g')
|
|
let regex = '^'. s:shorthand_regex
|
|
|
|
if empty(expr)
|
|
return opts
|
|
elseif expr !~ regex
|
|
call s:exit("Invalid expression: ". a:expr)
|
|
else
|
|
let match = matchlist(expr, regex)
|
|
if empty(match) | break | endif
|
|
for m in filter(match[ 1 : -1 ], '!empty(v:val)')
|
|
for key in ['lm', 'rm', 'l', 'r', 'stl', 's', '<', '>', 'iu', 'da', 'd', 'ms', 'm', 'ig', 'i', 'g', 'v', 'a']
|
|
if stridx(tolower(m), key) == 0
|
|
let rest = strpart(m, len(key))
|
|
if key == 'i' | let key = 'idt' | endif
|
|
if key == 'g' || key == 'v'
|
|
let rest = key.rest
|
|
let key = 'f'
|
|
endif
|
|
|
|
if key == 'idt' || index(['d', 'f', 'm', 'a'], key[0]) >= 0
|
|
let opts[key] = rest
|
|
elseif key == 'ig'
|
|
try
|
|
let arr = eval(rest)
|
|
if type(arr) == 3
|
|
let opts[key] = arr
|
|
else
|
|
throw 'Not an array'
|
|
endif
|
|
catch
|
|
call s:exit("Invalid ignore_groups: ". a:expr)
|
|
endtry
|
|
elseif key =~ '[<>]'
|
|
let opts['stl'] = key == '<'
|
|
else
|
|
let opts[key] = str2nr(rest)
|
|
endif
|
|
break
|
|
endif
|
|
endfor
|
|
endfor
|
|
endif
|
|
return s:normalize_options(opts)
|
|
endfunction
|
|
|
|
function! s:parse_args(args)
|
|
if empty(a:args)
|
|
return ['', '', {}, 0]
|
|
endif
|
|
let n = ''
|
|
let ch = ''
|
|
let args = a:args
|
|
let cand = ''
|
|
let opts = {}
|
|
|
|
" Poor man's option parser
|
|
let idx = 0
|
|
while 1
|
|
let midx = match(args, '\s*{.*}\s*$', idx)
|
|
if midx == -1 | break | endif
|
|
|
|
let cand = strpart(args, midx)
|
|
try
|
|
let [l, r, c, k, s, d, n] = ['l', 'r', 'c', 'k', 's', 'd', 'n']
|
|
let [L, R, C, K, S, D, N] = ['l', 'r', 'c', 'k', 's', 'd', 'n']
|
|
let o = eval(cand)
|
|
if type(o) == 4
|
|
let opts = o
|
|
if args[midx - 1 : midx] == '\ '
|
|
let midx += 1
|
|
endif
|
|
let args = strpart(args, 0, midx)
|
|
break
|
|
endif
|
|
catch
|
|
" Ignore
|
|
endtry
|
|
let idx = midx + 1
|
|
endwhile
|
|
|
|
" Invalid option dictionary
|
|
if len(substitute(cand, '\s', '', 'g')) > 2 && empty(opts)
|
|
call s:exit("Invalid option: ". cand)
|
|
else
|
|
let opts = s:normalize_options(opts)
|
|
endif
|
|
|
|
" Shorthand option notation
|
|
let sopts = matchstr(args, s:shorthand_regex)
|
|
if !empty(sopts)
|
|
let args = strpart(args, 0, len(args) - len(sopts))
|
|
let opts = extend(s:parse_shorthand_opts(sopts), opts)
|
|
endif
|
|
|
|
" Has /Regexp/?
|
|
let matches = matchlist(args, '^\(.\{-}\)\s*/\(.*\)/\s*$')
|
|
|
|
" Found regexp
|
|
if !empty(matches)
|
|
return [matches[1], s:test_regexp(matches[2]), opts, 1]
|
|
else
|
|
let tokens = matchlist(args, '^\([1-9][0-9]*\|-[0-9]*\|\*\*\?\)\?\s*\(.\{-}\)\?$')
|
|
" Try swapping n and ch
|
|
let [n, ch] = empty(tokens[2]) ? reverse(tokens[1:2]) : tokens[1:2]
|
|
|
|
" Resolving command-line ambiguity
|
|
" '\ ' => ' '
|
|
" '\' => ' '
|
|
if ch =~ '^\\\s*$'
|
|
let ch = ' '
|
|
" '\\' => '\'
|
|
elseif ch =~ '^\\\\\s*$'
|
|
let ch = '\'
|
|
endif
|
|
|
|
return [n, ch, opts, 0]
|
|
endif
|
|
endfunction
|
|
|
|
function! s:parse_filter(f)
|
|
let m = matchlist(a:f, '^\([gv]\)/\(.\{-}\)/\?$')
|
|
if empty(m)
|
|
return [0, '']
|
|
else
|
|
return [m[1] == 'g' ? 1 : -1, m[2]]
|
|
endif
|
|
endfunction
|
|
|
|
function! s:interactive_modes(bang)
|
|
return get(g:,
|
|
\ (a:bang ? 'easy_align_bang_interactive_modes' : 'easy_align_interactive_modes'),
|
|
\ (a:bang ? ['r', 'l', 'c'] : ['l', 'r', 'c']))
|
|
endfunction
|
|
|
|
function! s:alternating_modes(mode)
|
|
return a:mode ==? 'r' ? 'rl' : 'lr'
|
|
endfunction
|
|
|
|
function! s:update_lines(todo)
|
|
for [line, content] in items(a:todo)
|
|
call setline(line, s:rtrim(content))
|
|
endfor
|
|
endfunction
|
|
|
|
function! s:parse_nth(n)
|
|
let n = a:n
|
|
let recur = 0
|
|
if n == '*' | let [nth, recur] = [1, 1]
|
|
elseif n == '**' | let [nth, recur] = [1, 2]
|
|
elseif n == '-' | let nth = -1
|
|
elseif empty(n) | let nth = 1
|
|
elseif n == '0' || ( n != '-0' && n != string(str2nr(n)) )
|
|
call s:exit('Invalid N-th parameter: '. n)
|
|
else
|
|
let nth = n
|
|
endif
|
|
return [nth, recur]
|
|
endfunction
|
|
|
|
function! s:build_dict(delimiters, ch, regexp, opts)
|
|
if a:regexp
|
|
let dict = { 'pattern': a:ch }
|
|
else
|
|
if !has_key(a:delimiters, a:ch)
|
|
call s:exit('Unknown delimiter key: '. a:ch)
|
|
endif
|
|
let dict = copy(a:delimiters[a:ch])
|
|
endif
|
|
call extend(dict, a:opts)
|
|
|
|
let ml = get(dict, 'left_margin', ' ')
|
|
let mr = get(dict, 'right_margin', ' ')
|
|
if type(ml) == 0 | let ml = repeat(' ', ml) | endif
|
|
if type(mr) == 0 | let mr = repeat(' ', mr) | endif
|
|
call extend(dict, { 'ml': ml, 'mr': mr })
|
|
|
|
let dict.pattern = get(dict, 'pattern', a:ch)
|
|
let dict.delimiter_align =
|
|
\ get(dict, 'delimiter_align', get(g:, 'easy_align_delimiter_align', 'r'))[0]
|
|
let dict.indentation =
|
|
\ get(dict, 'indentation', get(g:, 'easy_align_indentation', 'k'))[0]
|
|
let dict.stick_to_left =
|
|
\ get(dict, 'stick_to_left', 0)
|
|
let dict.ignore_unmatched =
|
|
\ get(dict, 'ignore_unmatched', get(g:, 'easy_align_ignore_unmatched', 2))
|
|
let dict.ignore_groups =
|
|
\ get(dict, 'ignore_groups', get(dict, 'ignores', s:ignored_syntax()))
|
|
let dict.filter =
|
|
\ get(dict, 'filter', '')
|
|
return dict
|
|
endfunction
|
|
|
|
function! s:build_mode_sequence(expr, recur)
|
|
let [expr, recur] = [a:expr, a:recur]
|
|
let suffix = matchstr(a:expr, '\*\+$')
|
|
if suffix == '*'
|
|
let expr = expr[0 : -2]
|
|
let recur = 1
|
|
elseif suffix == '**'
|
|
let expr = expr[0 : -3]
|
|
let recur = 2
|
|
endif
|
|
return [tolower(expr), recur]
|
|
endfunction
|
|
|
|
function! s:process(range, mode, n, ch, opts, regexp, rules, bvis)
|
|
let [nth, recur] = s:parse_nth(a:n)
|
|
let dict = s:build_dict(a:rules, a:ch, a:regexp, a:opts)
|
|
let [mode_sequence, recur] = s:build_mode_sequence(
|
|
\ get(dict, 'align', recur == 2 ? s:alternating_modes(a:mode) : a:mode),
|
|
\ recur)
|
|
|
|
let args = [
|
|
\ {}, split(mode_sequence, '\zs'),
|
|
\ {}, {}, a:range[0], a:range[1],
|
|
\ a:bvis ? min([col("'<"), col("'>")]) : 1,
|
|
\ (!recur && a:bvis) ? max([col("'<"), col("'>")]) : 0,
|
|
\ nth, recur, dict ]
|
|
while len(args) > 1
|
|
let args = call('s:do_align', args)
|
|
endwhile
|
|
|
|
" todo: lines to update
|
|
" summarize: arguments to s:summarize
|
|
return { 'todo': args[0], 'summarize': [ a:opts, recur, mode_sequence ] }
|
|
endfunction
|
|
|
|
function s:summarize(opts, recur, mode_sequence)
|
|
let copts = s:compact_options(a:opts)
|
|
let nbmode = s:interactive_modes(0)[0]
|
|
if !has_key(copts, 'a') && (
|
|
\ (a:recur == 2 && s:alternating_modes(nbmode) != a:mode_sequence) ||
|
|
\ (a:recur != 2 && (a:mode_sequence[0] != nbmode || len(a:mode_sequence) > 1))
|
|
\ )
|
|
call extend(copts, { 'a': a:mode_sequence })
|
|
endif
|
|
return copts
|
|
endfunction
|
|
|
|
function! s:align(bang, live, visualmode, first_line, last_line, expr)
|
|
" Heuristically determine if the user was in visual mode
|
|
if empty(a:visualmode)
|
|
let vis = a:first_line == line("'<") && a:last_line == line("'>")
|
|
let bvis = vis && visualmode() == "\<C-V>"
|
|
" Visual-mode explicitly given
|
|
else
|
|
let vis = 1
|
|
let bvis = a:visualmode == "\<C-V>"
|
|
end
|
|
let range = [a:first_line, a:last_line]
|
|
let modes = s:interactive_modes(a:bang)
|
|
let mode = modes[0]
|
|
|
|
let rules = s:easy_align_delimiters_default
|
|
if exists('g:easy_align_delimiters')
|
|
let rules = extend(copy(rules), g:easy_align_delimiters)
|
|
endif
|
|
|
|
let [n, ch, opts, regexp] = s:parse_args(a:expr)
|
|
|
|
let bypass_fold = get(g:, 'easy_align_bypass_fold', 0)
|
|
let ofm = &l:foldmethod
|
|
try
|
|
if bypass_fold | let &l:foldmethod = 'manual' | endif
|
|
|
|
if empty(n) && empty(ch) || a:live
|
|
let [mode, n, ch, opts, regexp] = s:interactive(range, copy(modes), n, ch, opts, rules, vis, a:live, bvis)
|
|
endif
|
|
|
|
if !a:live
|
|
let output = s:process(range, mode, n, ch, s:normalize_options(opts), regexp, rules, bvis)
|
|
call s:update_lines(output.todo)
|
|
let copts = call('s:summarize', output.summarize)
|
|
let g:easy_align_last_command = s:echon('', n, regexp, ch, copts, '')
|
|
endif
|
|
finally
|
|
if bypass_fold | let &l:foldmethod = ofm | endif
|
|
endtry
|
|
endfunction
|
|
|
|
function! easy_align#align(bang, live, visualmode, expr) range
|
|
try
|
|
call s:align(a:bang, a:live, a:visualmode, a:firstline, a:lastline, a:expr)
|
|
catch 'exit'
|
|
endtry
|
|
endfunction
|
|
|
|
let &cpo = s:cpo_save
|
|
unlet s:cpo_save
|
|
|