diff --git a/autoload/ingo/actions/iterations.vim b/autoload/ingo/actions/iterations.vim new file mode 100644 index 0000000..9ef151a --- /dev/null +++ b/autoload/ingo/actions/iterations.vim @@ -0,0 +1,339 @@ +" ingo/actions/iterations.vim: Repeated action execution over several targets. +" +" DEPENDENCIES: +" - ingo/actions.vim autoload script +" - ingo/escape/file.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.025.001 29-Jul-2016 file creation + +function! ingo#actions#iterations#WinDo( alreadyVisitedBuffers, ... ) +"****************************************************************************** +"* PURPOSE: +" Invoke a:Action on each window in the current tab page, unless the buffer is +" in a:alreadyVisitedBuffers. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:alreadyVisitedBuffers Dictionary with already visited buffer numbers +" as keys. Will be added to, and the same buffers +" in other windows will be skipped. Pass 0 to +" visit _all_ windows, regardless of the buffers +" they display. +" a:Action Either a Funcref or Ex commands to be executed +" in each window. +" ... Arguments passed to an a:Action Funcref. +"* RETURN VALUES: +" None. +"****************************************************************************** + let l:originalWinNr = winnr() + let l:previousWinNr = winnr('#') ? winnr('#') : 1 + + " By entering a window, its height is potentially increased from 0 to 1 (the + " minimum for the current window). To avoid any modification, save the window + " sizes and restore them after visiting all windows. + let l:originalWindowLayout = winrestcmd() + let l:didSwitchWindows = 0 + + try + for l:winNr in range(1, winnr('$')) + let l:bufNr = winbufnr(l:winNr) + if a:alreadyVisitedBuffers is# 0 || ! has_key(a:alreadyVisitedBuffers, l:bufNr) + if l:winNr != winnr() + execute 'noautocmd' l:winNr . 'wincmd w' + let l:didSwitchWindows = 1 + endif + if type(a:alreadyVisitedBuffers) == type({}) | let a:alreadyVisitedBuffers[bufnr('')] = 1 | endif + + call call(function('ingo#actions#ExecuteOrFunc'), a:000) + endif + endfor + finally + if l:didSwitchWindows + noautocmd execute l:previousWinNr . 'wincmd w' + noautocmd execute l:originalWinNr . 'wincmd w' + silent! execute l:originalWindowLayout + endif + endtry +endfunction + +function! ingo#actions#iterations#TabWinDo( alreadyVisitedTabPages, alreadyVisitedBuffers, ... ) +"****************************************************************************** +"* PURPOSE: +" Invoke a:Action on each window in each tab page, unless the buffer is in +" a:alreadyVisitedBuffers. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:alreadyVisitedTabPages Dictionary with already visited tabpage numbers +" as keys. Will be added to, those tab pages will +" be skipped. Pass empty Dictionary to visit _all_ +" tab pages. +" a:alreadyVisitedBuffers Dictionary with already visited buffer numbers +" as keys. Will be added to, and the same buffers +" in other windows / tab pages will be skipped. +" Pass 0 to visit _all_ windows and tab pages, +" regardless of the buffers they display. +" a:Action Either a Funcref or Ex commands to be executed +" in each window. +" ... Arguments passed to an a:Action Funcref. +"* RETURN VALUES: +" None. +"****************************************************************************** + let l:originalTabNr = tabpagenr() + let l:didSwitchTabs = 0 + try + for l:tabNr in range(1, tabpagenr('$')) + if ! has_key(a:alreadyVisitedTabPages, l:tabNr) + let a:alreadyVisitedTabPages[l:tabNr] = 1 + if ! empty(a:alreadyVisitedBuffers) && ingo#collections#differences#ContainsLoosely(keys(a:alreadyVisitedBuffers), tabpagebuflist(l:tabNr)) + " All buffers of that tab page have already been visited; no + " need to go there. + continue + endif + + if l:tabNr != tabpagenr() + execute 'noautocmd' l:tabNr . 'tabnext' + let l:didSwitchTabs = 1 + endif + + let l:originalWinNr = winnr() + let l:previousWinNr = winnr('#') ? winnr('#') : 1 + " By entering a window, its height is potentially increased from 0 to 1 (the + " minimum for the current window). To avoid any modification, save the window + " sizes and restore them after visiting all windows. + let l:originalWindowLayout = winrestcmd() + let l:didSwitchWindows = 0 + + try + for l:winNr in range(1, winnr('$')) + let l:bufNr = winbufnr(l:winNr) + if a:alreadyVisitedBuffers is# 0 || ! has_key(a:alreadyVisitedBuffers, l:bufNr) + execute 'noautocmd' l:winNr . 'wincmd w' + + let l:didSwitchWindows = 1 + if type(a:alreadyVisitedBuffers) == type({}) | let a:alreadyVisitedBuffers[bufnr('')] = 1 | endif + + call call(function('ingo#actions#ExecuteOrFunc'), a:000) + endif + endfor + finally + if l:didSwitchWindows + noautocmd execute l:previousWinNr . 'wincmd w' + noautocmd execute l:originalWinNr . 'wincmd w' + silent! execute l:originalWindowLayout + endif + endtry + endif + endfor + finally + if l:didSwitchTabs + noautocmd execute l:originalTabNr . 'tabnext' + endif + endtry +endfunction + +function! s:GetNextArgNr( argNr, alreadyVisitedBuffers ) + let l:argNr = a:argNr + 1 " Try next argument. + while l:argNr <= argc() + let l:bufNr = bufnr(ingo#escape#file#bufnameescape(argv(a:argNr - 1))) + if l:bufNr == -1 || type(a:alreadyVisitedBuffers) != type({}) || ! has_key(a:alreadyVisitedBuffers, l:bufNr) + return l:argNr + endif + + " That one was already visited; continue searching. + let l:argNr += 1 + endwhile + return -1 +endfunction +function! ingo#actions#iterations#ArgDo( alreadyVisitedBuffers, ... ) +"****************************************************************************** +"* PURPOSE: +" Invoke a:Action on each argument in the argument list, unless the buffer is +" in a:alreadyVisitedBuffers. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" Prints any Vim exception as error message. +"* INPUTS: +" a:alreadyVisitedBuffers Dictionary with already visited buffer numbers +" as keys. Will be added to, and the same buffers +" in other arguments will be skipped. Pass 0 to +" visit _all_ arguments. +" a:Action Either a Funcref or Ex commands to be executed +" in each window. +" ... Arguments passed to an a:Action Funcref. +"* RETURN VALUES: +" Number of Vim exceptions raised while iterating through the argument list +" (e.g. errors when loading buffers) or from executing a:Action. +"****************************************************************************** + let l:originalBufNr = bufnr('') + let l:originalWindowLayout = winrestcmd() + let l:originalWinNr = winnr() + let l:previousWinNr = winnr('#') ? winnr('#') : 1 + + let l:nextArgNr = s:GetNextArgNr(0, a:alreadyVisitedBuffers) + if l:nextArgNr == -1 + return | " No arguments left. + endif + + let l:didSplit = 0 + let l:failureCnt = 0 + try + try + execute 'noautocmd silent keepalt leftabove' l:nextArgNr . 'sargument' + let l:didSplit = 1 + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + if bufnr('') == l:originalBufNr + " We failed to split to the target buffer; bail out, as we need + " the split. + return l:failureCnt + endif + endtry + + while 1 + let l:bufNr = bufnr('') + if type(a:alreadyVisitedBuffers) == type({}) | let a:alreadyVisitedBuffers[bufnr('')] = 1 | endif + + try + call call(function('ingo#actions#ExecuteOrFunc'), a:000) + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + endtry + + let l:nextArgNr = s:GetNextArgNr(l:nextArgNr, a:alreadyVisitedBuffers) + if l:nextArgNr == -1 + break + endif + + try + execute 'noautocmd silent keepalt' l:nextArgNr . 'argument' + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + endtry + endwhile + finally + if l:didSplit + noautocmd silent! close! + noautocmd execute l:previousWinNr . 'wincmd w' + noautocmd execute l:originalWinNr . 'wincmd w' + silent! execute l:originalWindowLayout + endif + endtry + + return l:failureCnt +endfunction + +function! s:GetNextBufNr( bufNr, alreadyVisitedBuffers ) + let l:bufNr = a:bufNr + 1 " Try next buffer. + let l:lastBufNr = bufnr('$') + while l:bufNr <= l:lastBufNr + if buflisted(l:bufNr) && (type(a:alreadyVisitedBuffers) != type({}) || ! has_key(a:alreadyVisitedBuffers, l:bufNr)) + return l:bufNr + endif + + " That one was already visited; continue searching. + let l:bufNr += 1 + endwhile + return -1 +endfunction +function! ingo#actions#iterations#BufDo( alreadyVisitedBuffers, ... ) +"****************************************************************************** +"* PURPOSE: +" Invoke a:Action on each listed buffer, unless the buffer is in +" a:alreadyVisitedBuffers. +"* SEE ALSO: +" To execute an Action in a single visible buffer, use +" ingo#buffer#visible#Execute() / ingo#buffer#visible#Call(). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" Prints any Vim exception as error message. +"* INPUTS: +" a:alreadyVisitedBuffers Dictionary with already visited buffer numbers +" as keys. Will be added to. Pass 0 or {} to visit +" _all_ buffers. +" a:Action Either a Funcref or Ex commands to be executed +" in each buffer. +" ... Arguments passed to an a:Action Funcref. +"* RETURN VALUES: +" Number of Vim exceptions raised while iterating through the buffer list +" (e.g. errors when loading buffers) or from executing a:Action. +"****************************************************************************** + let l:originalWindowLayout = winrestcmd() + let l:originalWinNr = winnr() + let l:previousWinNr = winnr('#') ? winnr('#') : 1 + + let l:nextBufNr = s:GetNextBufNr(0, a:alreadyVisitedBuffers) + if l:nextBufNr == -1 + return | " No buffers left. + endif + + let l:didSplit = 0 + let l:failureCnt = 0 + let l:save_switchbuf = &switchbuf | set switchbuf= | " :sbuffer should always open a new split (so we can :close it without checking). + try + try + execute 'noautocmd silent keepalt leftabove' l:nextBufNr . 'sbuffer' + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + if bufnr('') != l:nextBufNr + " We failed to split to the target buffer; bail out, as we need + " the split. + return l:failureCnt + endif + finally + let &switchbuf = l:save_switchbuf + endtry + + let l:didSplit = 1 + while 1 + let l:bufNr = bufnr('') + if type(a:alreadyVisitedBuffers) == type({}) | let a:alreadyVisitedBuffers[bufnr('')] = 1 | endif + + try + call call(function('ingo#actions#ExecuteOrFunc'), a:000) + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + endtry + + let l:nextBufNr = s:GetNextBufNr(l:nextBufNr, a:alreadyVisitedBuffers) + if l:nextBufNr == -1 + break + endif + + try + execute 'noautocmd silent keepalt' l:nextBufNr . 'buffer' + catch + call ingo#msg#VimExceptionMsg() + let l:failureCnt += 1 + endtry + endwhile + finally + if l:didSplit + noautocmd silent! close! + noautocmd execute l:previousWinNr . 'wincmd w' + noautocmd execute l:originalWinNr . 'wincmd w' + silent! execute l:originalWindowLayout + endif + endtry + + return l:failureCnt +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/actions/special.vim b/autoload/ingo/actions/special.vim new file mode 100644 index 0000000..459c031 --- /dev/null +++ b/autoload/ingo/actions/special.vim @@ -0,0 +1,52 @@ +" ingo/actions/special.vim: Action execution within special environments. +" +" DEPENDENCIES: +" - ingo/actions.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.025.001 29-Jul-2016 file creation + +function! ingo#actions#special#NoAutoChdir( ... ) +"****************************************************************************** +"* PURPOSE: +" Execute a:Action with :set noautochdir. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:Action Either a Funcref or Ex commands to be executed. +" ... Arguments passed to an a:Action Funcref. +"* RETURN VALUES: +" Result of Funcref, or empty string in case of Ex commands. +"****************************************************************************** + " Unfortunately, restoring the 'autochdir' option clobbers any temporary CWD + " override. So we may have to restore the CWD, too. + let l:save_cwd = getcwd() + let l:chdirCommand = (exists('*haslocaldir') && haslocaldir() ? 'lchdir!' : 'chdir!') + + " The 'autochdir' option adapts the CWD, so any (relative) filepath to the + " filename in the other window would be omitted. Temporarily turn this off; + " may be a little bit faster, too. + if exists('+autochdir') + let l:save_autochdir = &autochdir + set noautochdir + endif + try + return call(function('ingo#actions#ExecuteOrFunc'), a:000) + finally + if exists('l:save_autochdir') + let &autochdir = l:save_autochdir + endif + if getcwd() !=# l:save_cwd + execute l:chdirCommand ingo#compat#fnameescape(l:save_cwd) + endif + endtry +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/avoidprompt.vim b/autoload/ingo/avoidprompt.vim index 8774b45..8918715 100644 --- a/autoload/ingo/avoidprompt.vim +++ b/autoload/ingo/avoidprompt.vim @@ -15,17 +15,33 @@ " and double-width characters (e.g. Japanese Kanji) are taken into account. " DEPENDENCIES: +" - ingo/compat.vim autoload script " - ingo/strdisplaywidth.vim autoload script " " TODO: " - Consider 'cmdheight', add argument isSingleLine. " -" Copyright: (C) 2008-2014 Ingo Karkat +" Copyright: (C) 2008-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.003 18-Nov-2016 ENH: Add optional a:reservedColumns also to +" ingo#avoidprompt#TruncateTo(), and pass this +" from ingo#avoidprompt#Truncate(). +" ingo#avoidprompt#TruncateTo(): The strright() +" cannot precisely account for the rendering of +" tab widths. Check the result, and if necessary, +" remove further characters until we go below the +" limit. +" 1.026.002 11-Aug-2016 ENH: ingo#avoidprompt#TruncateTo() has a +" configurable ellipsis string +" g:IngoLibrary_TruncateEllipsis, now defaulting +" to a single-char UTF-8 variant if we're in such +" encoding. Thanks to Daniel Hahler for sending a +" patch! It also handles pathologically small +" lengths that only show / cut into the ellipsis. " 1.008.001 07-Jun-2013 file creation from EchoWithoutScrolling.vim function! ingo#avoidprompt#MaxLength() @@ -62,7 +78,7 @@ function! ingo#avoidprompt#MaxLength() return l:maxLength endfunction -function! ingo#avoidprompt#TruncateTo( text, length ) +function! ingo#avoidprompt#TruncateTo( text, length, ... ) "******************************************************************************* "* PURPOSE: " Truncate a:text to a maximum of a:length virtual columns by dropping text in @@ -72,18 +88,24 @@ function! ingo#avoidprompt#TruncateTo( text, length ) " - ingo#strdisplaywidth#TruncateTo() does something similar, but truncates at " the end, and doesn't account for buffer-local tabstop values. "* ASSUMPTIONS / PRECONDITIONS: -" none +" The ellipsis can be configured by g:IngoLibrary_TruncateEllipsis. "* EFFECTS / POSTCONDITIONS: " none "* INPUTS: " a:text Text which may be truncated to fit. " a:length Maximum virtual columns for a:text. +" a:reservedColumns Optional number of columns that are already taken in the +" line (before a:text, this matters for tab rendering); if +" specified, a:text will be truncated to (MaxLength() - +" a:reservedColumns). "* RETURN VALUES: " Truncated a:text. "******************************************************************************* if a:length <= 0 return '' endif + let l:reservedColumns = (a:0 > 0 ? a:1 : 0) + let l:reservedPadding = repeat(' ', l:reservedColumns) " The \%<23v regexp item uses the local 'tabstop' value to determine the " virtual column. As we want to echo with default tabstop 8, we need to @@ -93,13 +115,41 @@ function! ingo#avoidprompt#TruncateTo( text, length ) let l:text = a:text try - if ingo#strdisplaywidth#HasMoreThan(l:text, a:length) - " We need 3 characters for the '...'; 1 must be added to both lengths - " because columns start at 1, not 0. - let l:frontCol = a:length / 2 - let l:backCol = (a:length % 2 == 0 ? (l:frontCol - 1) : l:frontCol) + if ingo#strdisplaywidth#HasMoreThan(l:reservedPadding . l:text, a:length + l:reservedColumns) + let l:ellipsisLength = ingo#compat#strchars(g:IngoLibrary_TruncateEllipsis) + + " Handle pathological cases. + if a:length == l:ellipsisLength + return g:IngoLibrary_TruncateEllipsis + elseif a:length < l:ellipsisLength + return ingo#compat#strcharpart(g:IngoLibrary_TruncateEllipsis, 0, a:length) + endif + + " Consider the length of the (configurable) "..." ellipsis. + " 1 must be added because columns start at 1, not 0. + let l:length = a:length - l:ellipsisLength + 1 + let l:frontCol = l:length / 2 + let l:backCol = (l:length % 2 == 0 ? (l:frontCol - 1) : l:frontCol) "**** echomsg '**** ' a:length ':' l:frontCol '-' l:backCol - let l:text = ingo#strdisplaywidth#strleft(l:text, l:frontCol) . '...' . ingo#strdisplaywidth#strright(l:text, l:backCol) + while 1 + let l:fullText = + \ ingo#strdisplaywidth#strleft(l:reservedPadding . l:text, l:frontCol) . + \ g:IngoLibrary_TruncateEllipsis . + \ ingo#strdisplaywidth#strright(l:text, l:backCol) + + " The strright() cannot precisely account for the rendering of + " tab widths. Check the result, and if necessary, remove further + " characters until we go below the limit. + if ! ingo#strdisplaywidth#HasMoreThan(l:fullText, a:length + l:reservedColumns) + let l:text = strpart(l:fullText, l:reservedColumns) + break + endif + if l:backCol > 0 + let l:backCol -= 1 + else + let l:frontCol -= 1 + endif + endwhile endif finally let &l:tabstop = l:save_ts @@ -119,8 +169,9 @@ function! ingo#avoidprompt#Truncate( text, ... ) "* INPUTS: " a:text Text which may be truncated to fit. " a:reservedColumns Optional number of columns that are already taken in the -" line; if specified, a:text will be truncated to -" (MaxLength() - a:reservedColumns). +" line (before a:text, this matters for tab rendering); if +" specified, a:text will be truncated to (MaxLength() - +" a:reservedColumns). "* RETURN VALUES: " Truncated a:text. "******************************************************************************* @@ -133,7 +184,7 @@ function! ingo#avoidprompt#Truncate( text, ... ) let l:reservedColumns = (a:0 > 0 ? a:1 : 0) let l:maxLength = ingo#avoidprompt#MaxLength() - l:reservedColumns - return ingo#avoidprompt#TruncateTo( a:text, l:maxLength ) + return ingo#avoidprompt#TruncateTo( a:text, l:maxLength, l:reservedColumns ) endfunction function! ingo#avoidprompt#TranslateLineBreaks( text ) diff --git a/autoload/ingo/binary.vim b/autoload/ingo/binary.vim new file mode 100644 index 0000000..0e23ffe --- /dev/null +++ b/autoload/ingo/binary.vim @@ -0,0 +1,96 @@ +" ingo/binary.vim: Functions for working with binary numbers. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 28-Dec-2016 file creation + +function! ingo#binary#FromNumber( number, ... ) +"****************************************************************************** +"* PURPOSE: +" Turn the integer a:number into a (little-endian) List of boolean values. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:number Positive integer. +" a:bitNum Optional number of bits to use. If specified and a:number cannot +" be represented by it, a exception is thrown. If a:bitNum is +" negative, only the lower bits will be returned. If omitted, the +" minimal amount of bits is used. +"* RETURN VALUES: +" List of [b0, b1, b2, ...] boolean values; lowest bits come first. +"****************************************************************************** + let l:number = a:number + let l:result = [] + let l:bitCnt = 0 + let l:bitMax = (a:0 ? ingo#compat#abs(a:1) : 0) + + while 1 + " Encode this little-endian. + call add(l:result, l:number % 2) + let l:number = l:number / 2 + let l:bitCnt += 1 + + if l:bitMax && l:bitCnt == l:bitMax + if a:1 > 0 && l:number != 0 + throw printf('FromNumber: Cannot represent %d in %d bits', a:number, l:bitMax) + endif + break + elseif ! a:0 && l:number == 0 + break + endif + endwhile + return l:result +endfunction +function! ingo#binary#ToNumber( bits ) +"****************************************************************************** +"* PURPOSE: +" Turn the (little-endian) List of boolean values into a number. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:bits List of [b0, b1, b2, ...] boolean values; lowest bits come first. +"* RETURN VALUES: +" Positive integer represented by a:bits. +"****************************************************************************** + let l:number = 0 + let l:factor = 1 + while ! empty(a:bits) + let l:number += l:factor * remove(a:bits, 0) + let l:factor = l:factor * 2 + endwhile + return l:number +endfunction + +function! ingo#binary#BitsRequired( number ) +"****************************************************************************** +"* PURPOSE: +" Determine the number of bits required to represent a:number. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:number Positive integer. +"* RETURN VALUES: +" Number of bits required to represent numbers between 0 and a:number. +"****************************************************************************** + let l:bitCnt = 1 + let l:max = 2 + while a:number >= l:max + let l:bitCnt += 1 + let l:max = l:max * 2 + endwhile + return l:bitCnt +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/buffer.vim b/autoload/ingo/buffer.vim index 18e71cb..510df41 100644 --- a/autoload/ingo/buffer.vim +++ b/autoload/ingo/buffer.vim @@ -2,12 +2,13 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.005 29-Jul-2016 Add ingo#buffer#ExistOtherLoadedBuffers(). " 1.015.004 18-Nov-2013 Make buffer argument of ingo#buffer#IsBlank() " optional, defaulting to the current buffer. " Allow use of ingo#buffer#IsEmpty() with other @@ -43,6 +44,9 @@ endfunction function! ingo#buffer#ExistOtherBuffers( targetBufNr ) return ! empty(filter(range(1, bufnr('$')), 'buflisted(v:val) && v:val != a:targetBufNr')) endfunction +function! ingo#buffer#ExistOtherLoadedBuffers( targetBufNr ) + return ! empty(filter(range(1, bufnr('$')), 'buflisted(v:val) && bufloaded(v:val) && v:val != a:targetBufNr')) +endfunction function! ingo#buffer#IsEmptyVim() let l:currentBufNr = bufnr('') diff --git a/autoload/ingo/buffer/locate.vim b/autoload/ingo/buffer/locate.vim new file mode 100644 index 0000000..bf4311f --- /dev/null +++ b/autoload/ingo/buffer/locate.vim @@ -0,0 +1,161 @@ +" ingo/buffer/locate.vim: Functions to locate a buffer. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.028.002 19-Nov-2016 Also prefer current / previous window in other +" tab pages. +" 1.028.001 18-Nov-2016 file creation + +function! s:FindBufferOnTabPage( isConsiderNearest, tabPageNr, bufNr ) + let l:bufferNumbers = tabpagebuflist(a:tabPageNr) + + if a:isConsiderNearest + let l:currentIdx = tabpagewinnr(a:tabPageNr) - 1 + if l:bufferNumbers[l:currentIdx] == a:bufNr + return l:currentIdx + 1 + endif + let l:previousIdx = tabpagewinnr(a:tabPageNr, '#') - 1 + if l:previousIdx >= 0 && l:bufferNumbers[l:previousIdx] == a:bufNr + return l:previousIdx + 1 + endif + endif + + for l:i in range(len(l:bufferNumbers)) + if l:bufferNumbers[l:i] == a:bufNr + return l:i + 1 + endif + endfor + return 0 +endfunction + +function! ingo#buffer#locate#BufTabPageWinNr( bufNr ) +"****************************************************************************** +"* PURPOSE: +" Locate the first window that contains a:bufNr, in this tab page (like +" bufwinnr()), or in other tab pages. Can be used to emulate the behavior of +" :sbuffer with 'switchbuf' containing "useopen,usetab". +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:bufNr Buffer number of the target buffer. +"* RETURN VALUES: +" [tabpagenr, winnr] if the buffer is on a different tab page +" [0, winnr] if the buffer is on the current tab page +" [0, 0] if a:bufNr is not found in other windows +"****************************************************************************** + let l:winNr = bufwinnr(a:bufNr) + if l:winNr > 0 + return [0, l:winNr] + endif + + for l:tabPageNr in filter(range(1, tabpagenr('$')), 'v:val != ' . tabpagenr()) + let l:winNr = s:FindBufferOnTabPage(0, l:tabPageNr, a:bufNr) + if l:winNr != 0 + return [l:tabPageNr, l:winNr] + endif + endfor + + return [0, 0] +endfunction + +function! ingo#buffer#locate#NearestWindow( isSearchOtherTabPages, bufNr ) +"****************************************************************************** +"* PURPOSE: +" Locate the window closest to the current one that contains a:bufNr. Like +" bufwinnr() with different precedences, and optionally looking into other tab +" pages. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:isSearchOtherTabPages Flag whether windows in other tab pages should also +" be considered. +" a:bufNr Buffer number of the target buffer. +"* RETURN VALUES: +" [tabpagenr, winnr] if a:isSearchOtherTabPages and the buffer is on a +" different tab page +" [0, winnr] if the buffer is on the current tab page +" [0, 0] if a:bufNr is not found in other windows +"****************************************************************************** + let l:lastWinNr = winnr('#') + if l:lastWinNr != 0 && winbufnr(l:lastWinNr) == a:bufNr + return [tabpagenr(), l:lastWinNr] + endif + + let [l:currentWinNr, l:lastWinNr] = [winnr(), winnr('$')] + let l:offset = 1 + while l:currentWinNr - l:offset > 0 || l:currentWinNr + l:offset <= l:lastWinNr + if winbufnr(l:currentWinNr - l:offset) == a:bufNr + return [tabpagenr(), l:currentWinNr - l:offset] + elseif winbufnr(l:currentWinNr + l:offset) == a:bufNr + return [tabpagenr(), l:currentWinNr + l:offset] + endif + let l:offset += 1 + endwhile + + if ! a:isSearchOtherTabPages + return [0, 0] + endif + + let [l:currentTabPageNr, l:lastTabPageNr] = [tabpagenr(), tabpagenr('$')] + let l:offset = 1 + while l:currentTabPageNr - l:offset > 0 || l:currentTabPageNr + l:offset <= l:lastTabPageNr + let l:winNr = s:FindBufferOnTabPage(1, l:currentTabPageNr - l:offset, a:bufNr) + if l:winNr != 0 + return [l:currentTabPageNr - l:offset, l:winNr] + endif + let l:winNr = s:FindBufferOnTabPage(1, l:currentTabPageNr + l:offset, a:bufNr) + if l:winNr != 0 + return [l:currentTabPageNr + l:offset, l:winNr] + endif + let l:offset += 1 + endwhile + + return [0, 0] +endfunction + +function! ingo#buffer#locate#Window( strategy, isSearchOtherTabPages, bufNr ) +"****************************************************************************** +"* PURPOSE: +" Locate a window that contains a:bufNr, with a:strategy to determine +" precedences. Similar to bufwinnr() with configurable precedences, and +" optionally looking into other tab pages. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:strategy One of "first" or "nearest". +" a:isSearchOtherTabPages Flag whether windows in other tab pages should also +" be considered. +" a:bufNr Buffer number of the target buffer. +"* RETURN VALUES: +" [tabpagenr, winnr] if a:isSearchOtherTabPages and the buffer is on a +" different tab page +" [0, winnr] if the buffer is on the current tab page +" [0, 0] if a:bufNr is not found in other windows +"****************************************************************************** + if a:strategy ==# 'first' + if a:isSearchOtherTabPages + return ingo#buffer#locate#BufTabPageWinNr(a:bufNr) + else + let l:winNr = bufwinnr(a:bufNr) + return (l:winNr > 0 ? [0, l:winNr] : [0, 0]) + endif + elseif a:strategy ==# 'nearest' + return ingo#buffer#locate#NearestWindow(a:isSearchOtherTabPages, a:bufNr) + else + throw 'ASSERT: Unknown strategy ' . string(a:strategy) + endif +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/buffer/temp.vim b/autoload/ingo/buffer/temp.vim index 501f864..02f1503 100644 --- a/autoload/ingo/buffer/temp.vim +++ b/autoload/ingo/buffer/temp.vim @@ -2,12 +2,23 @@ " " DEPENDENCIES: " -" Copyright: (C) 2011-2014 Ingo Karkat +" Copyright: (C) 2011-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.006 16-Nov-2016 FIX: Correct delegation in +" ingo#buffer#temp#Execute(); wrong recursive call +" was used (after 1.027). +" ENH: Add optional a:isSilent argument to +" ingo#buffer#temp#Execute(). +" 1.027.005 20-Aug-2016 Add ingo#buffer#temp#ExecuteWithText() and +" ingo#buffer#temp#CallWithText() variants that +" pre-initialize the buffer (a common use case). +" 1.025.004 29-Jul-20167 FIX: Temporarily reset 'switchbuf' in +" ingo#buffer#temp#Execute(), to avoid that +" "usetab" switched to another tab page. " 1.023.003 07-Nov-2014 ENH: Add optional a:isReturnAsList flag to " ingo#buffer#temp#Execute() and " ingo#buffer#temp#Call(). @@ -17,8 +28,13 @@ " and output of :ls!. " 1.008.001 11-Jun-2013 file creation from ingobuffer.vim +function! s:SetBuffer( text ) + if empty(a:text) | return | endif + call append(1, (type(a:text) == type([]) ? a:text : split(a:text, '\n', 1))) + silent 1delete _ +endfunction let s:tempBufNr = 0 -function! ingo#buffer#temp#Execute( command, ... ) +function! ingo#buffer#temp#Execute( ... ) "****************************************************************************** "* PURPOSE: " Invoke an Ex command in an empty temporary scratch buffer and return the @@ -35,10 +51,39 @@ function! ingo#buffer#temp#Execute( command, ... ) " contents and just execute a:command for its side " effects. " a:isReturnAsList Flag whether to return the contents as a List of lines. +" a:isSilent Flag whether a:command is executed silently (default: +" true). "* RETURN VALUES: " Contents of the buffer, by default as one newline-delimited string, with " a:isReturnAsList as a List, like getline() does. "****************************************************************************** + return call('ingo#buffer#temp#ExecuteWithText', [''] + a:000) +endfunction +function! ingo#buffer#temp#ExecuteWithText( text, command, ... ) +"****************************************************************************** +"* PURPOSE: +" Invoke an Ex command in a temporary scratch buffer filled with a:text and +" return the contents of the buffer after the execution. +"* ASSUMPTIONS / PRECONDITIONS: +" - a:command should have no side effects to the buffer (other than changing +" its contents), as it will be reused on subsequent invocations. If you +" change any buffer-local option, also undo the change! +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:text List of lines, or String with newline-delimited lines. +" a:command Ex command to be invoked. +" a:isIgnoreOutput Flag whether to skip capture of the scratch buffer +" contents and just execute a:command for its side +" effects. +" a:isReturnAsList Flag whether to return the contents as a List of lines. +" a:isSilent Flag whether a:command is executed silently (default: +" true). +"* RETURN VALUES: +" Contents of the buffer, by default as one newline-delimited string, with +" a:isReturnAsList as a List, like getline() does. +"****************************************************************************** + let l:isSilent = (a:0 >= 3 ? a:3 : 1) " It's hard to create a temp buffer in a safe way without side effects. " Switching the buffer can change the window view, may have a noticable " delay even with autocmds suppressed (maybe due to 'autochdir', or just a @@ -47,7 +92,12 @@ function! ingo#buffer#temp#Execute( command, ... ) " window or tab. And autocmds may do all sorts of uncontrolled changes. let l:originalWindowLayout = winrestcmd() if s:tempBufNr && bufexists(s:tempBufNr) - noautocmd silent keepalt leftabove execute s:tempBufNr . 'sbuffer' + let l:save_switchbuf = &switchbuf | set switchbuf= | " :sbuffer should always open a new split / must not apply "usetab" (so we can :close it without checking). + try + noautocmd silent keepalt leftabove execute s:tempBufNr . 'sbuffer' + finally + let &switchbuf = l:save_switchbuf + endtry " The :bdelete got rid of the buffer contents; no need to clean the " revived buffer. else @@ -55,7 +105,8 @@ function! ingo#buffer#temp#Execute( command, ... ) let s:tempBufNr = bufnr('') endif try - silent execute a:command + call s:SetBuffer(a:text) + execute (l:isSilent ? 'silent' : '') a:command if ! a:0 || ! a:1 let l:lines = getline(1, line('$')) return (a:0 >= 2 && a:2 ? l:lines : join(l:lines, "\n")) @@ -66,7 +117,10 @@ function! ingo#buffer#temp#Execute( command, ... ) endtry endfunction function! ingo#buffer#temp#Call( Funcref, arguments, ... ) - return call('ingo#buffer#temp#Execute', ['call call(' . string(a:Funcref) . ',' . string(a:arguments) . ')'] + a:000) + return call('ingo#buffer#temp#ExecuteWithText', ['', 'call call(' . string(a:Funcref) . ',' . string(a:arguments) . ')'] + a:000) +endfunction +function! ingo#buffer#temp#CallWithText( text, Funcref, arguments, ... ) + return call('ingo#buffer#temp#ExecuteWithText', [a:text, 'call call(' . string(a:Funcref) . ',' . string(a:arguments) . ')'] + a:000) endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/buffer/visible.vim b/autoload/ingo/buffer/visible.vim index f3cf659..a76d1c8 100644 --- a/autoload/ingo/buffer/visible.vim +++ b/autoload/ingo/buffer/visible.vim @@ -2,12 +2,15 @@ " " DEPENDENCIES: " -" Copyright: (C) 2011-2015 Ingo Karkat +" Copyright: (C) 2011-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.004 29-Jul-20167 FIX: Temporarily reset 'switchbuf' in +" ingo#buffer#visible#Execute(), to avoid that +" "usetab" switched to another tab page. " 1.024.003 17-Mar-2015 ingo#buffer#visible#Execute(): Restore the " window layout when the buffer is visible but in " a window with 0 height / width. And restore the @@ -29,9 +32,11 @@ function! ingo#buffer#visible#Execute( bufnr, command ) " and must therefore be visible in a window to be invoked. This function " ensures that the passed command is executed in the context of the passed " buffer number. - +"* SEE ALSO: +" To execute an Action in all buffers (temporarily made visible), use +" ingo#actions#iterations#BufDo(). "* ASSUMPTIONS / PRECONDITIONS: -" ? List of any external variable, control, or other element whose state affects this procedure. +" None. "* EFFECTS / POSTCONDITIONS: " The current window and buffer loaded into it remain the same. "* INPUTS: @@ -51,7 +56,9 @@ function! ingo#buffer#visible#Execute( bufnr, command ) " The buffer is hidden. Make it visible to execute the passed function. " Use a temporary split window as ingo#buffer#temp#Execute() does, for " all the reasons outlined there. - execute 'noautocmd silent keepalt leftabove sbuffer' a:bufnr + let l:save_switchbuf = &switchbuf | set switchbuf= | " :sbuffer should always open a new split / must not apply "usetab" (so we can :close it without checking). + execute 'noautocmd silent keepalt leftabove sbuffer' a:bufnr + let &switchbuf = l:save_switchbuf | unlet l:save_switchbuf let l:newWinNr = winnr() try execute a:command @@ -67,6 +74,7 @@ function! ingo#buffer#visible#Execute( bufnr, command ) execute a:command endif finally + if exists('l:save_switchbuf') | let &switchbuf = l:save_switchbuf | endif silent execute l:previousWinNr . 'wincmd w' silent execute l:currentWinNr . 'wincmd w' silent! execute l:originalWindowLayout diff --git a/autoload/ingo/cmdargs/glob.vim b/autoload/ingo/cmdargs/glob.vim index 109bf5e..6b2eb35 100644 --- a/autoload/ingo/cmdargs/glob.vim +++ b/autoload/ingo/cmdargs/glob.vim @@ -5,12 +5,16 @@ " - ingo/compat.vim autoload script " - ingo/os.vim autoload script " -" Copyright: (C) 2012-2014 Ingo Karkat +" Copyright: (C) 2012-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.004 08-Jul-2016 ENH: Add second optional flag +" a:isKeepDirectories to +" ingo#cmdargs#glob#Expand() / +" ingo#cmdargs#glob#ExpandSingle(). " 1.022.003 22-Sep-2014 Use ingo#compat#glob(). " 1.013.002 13-Sep-2013 Use operating system detection functions from " ingo/os.vim. @@ -30,6 +34,7 @@ function! ingo#cmdargs#glob#ExpandSingle( fileglob, ... ) " a:isKeepNoMatch Optional flag that lets globs that have no matches be kept " and returned as-is, instead of being removed. Set this when " you want to support creating new files. +" a:isKeepDirectories Optional flag that keeps directories in the list. "* RETURN VALUES: " List of normal filespecs; globs have been expanded. To consume this in " another Vim command, use: @@ -45,7 +50,8 @@ function! ingo#cmdargs#glob#ExpandSingle( fileglob, ... ) return [a:fileglob] else " Filter out directories; we're usually only interested in files. - return filter((a:0 && a:1 ? split(expand(a:fileglob), '\n') : ingo#compat#glob(a:fileglob, 0, 1)), '! isdirectory(v:val)') + let l:specs = (a:0 && a:1 ? split(expand(a:fileglob), '\n') : ingo#compat#glob(a:fileglob, 0, 1)) + return (a:0 >= 2 && a:2 ? l:specs : filter(l:specs, '! isdirectory(v:val)')) endif endfunction function! ingo#cmdargs#glob#Expand( fileglobs, ... ) @@ -64,6 +70,7 @@ function! ingo#cmdargs#glob#Expand( fileglobs, ... ) " a:isKeepNoMatch Optional flag that lets globs that have no matches be kept " and returned as-is, instead of being removed. Set this when " you want to support creating new files. +" a:isKeepDirectories Optional flag that keeps directories in the list. "* RETURN VALUES: " List of filespecs; globs have been expanded. To consume this in another Vim " command, use: diff --git a/autoload/ingo/cmdargs/pattern.vim b/autoload/ingo/cmdargs/pattern.vim index f93dde6..03458af 100644 --- a/autoload/ingo/cmdargs/pattern.vim +++ b/autoload/ingo/cmdargs/pattern.vim @@ -3,12 +3,29 @@ " DEPENDENCIES: " - ingo/escape.vim autoload script " -" Copyright: (C) 2013-2015 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.008 17-Oct-2016 BUG: Support of optional a:flagsMatchCount in +" ingo#cmdargs#pattern#ParseUnescaped() and +" ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord() +" broke no-flags String type return value by +" returning a one-element list. +" 1.028.007 06-Oct-2016 Add ingo#cmdargs#pattern#Render(). +" 1.028.006 05-Oct-2016 ENH: Also support optional a:flagsMatchCount in +" ingo#cmdargs#pattern#ParseUnescaped() and +" ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord(). +" Add missing +" ingo#cmdargs#pattern#ParseWithLiteralWholeWord() +" variant. +" 1.027.005 29-Sep-2016 ENH: ingo#cmdargs#pattern#Parse(): Add second +" optional a:flagsMatchCount argument, similar to +" what ingo#cmdargs#substitute#Parse() has in +" a:options. +" Add ingo#cmdargs#pattern#RawParse(). " 1.023.004 03-Jan-2015 Add ingo#cmdargs#pattern#IsDelimited(). " 1.020.003 29-May-2014 Use ingo#escape#Unescape() in " ingo#cmdargs#pattern#Unescape(). @@ -30,6 +47,33 @@ function! s:Parse( arguments, ... ) return matchlist(a:arguments, '^\([[:alnum:]\\"|]\@![\x00-\xFF]\)\(.\{-}\)\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@= 2 ? a:2 + 2 : 3) : 2)] + endif +endfunction function! ingo#cmdargs#pattern#Parse( arguments, ... ) "****************************************************************************** "* PURPOSE: @@ -42,17 +86,52 @@ function! ingo#cmdargs#pattern#Parse( arguments, ... ) "* INPUTS: " a:arguments Command arguments to parse. " a:flagsExpr Pattern that captures any optional part after the pattern. +" a:flagsMatchCount Number of capture groups returned from a:flagsExpr. +"* RETURN VALUES: +" [separator, escapedPattern]; if a:flagsExpr is given +" [separator, escapedPattern, flags, ...]. +" In a:escapedPattern, the a:separator is consistently escaped (i.e. also when +" the original arguments haven't been enclosed in such). +"****************************************************************************** + " Note: We could delegate to ingo#cmdargs#pattern#RawParse(), but let's + " duplicate this for now to avoid another redirection. + let l:match = call('s:Parse', [a:arguments] + a:000) + if empty(l:match) + return ['/', escape(a:arguments, '/')] + (a:0 ? repeat([''], a:0 >= 2 ? a:2 : 1) : []) + else + return l:match[1: (a:0 ? (a:0 >= 2 ? a:2 + 2 : 3) : 2)] + endif +endfunction +function! ingo#cmdargs#pattern#ParseWithLiteralWholeWord( arguments, ... ) +"****************************************************************************** +"* PURPOSE: +" Parse a:arguments as a pattern delimited by optional /.../ (or similar) +" characters, and with optional following flags match. When the pattern isn't +" delimited by /.../, the returned pattern is modified so that only literal +" whole words are matched. Built-in commands like |:djump| also have this +" behavior: |:search-args| +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:arguments Command arguments to parse. +" a:flagsExpr Pattern that captures any optional part after the pattern. +" a:flagsMatchCount Number of capture groups returned from a:flagsExpr. "* RETURN VALUES: " [separator, escapedPattern]; if a:flagsExpr is given -" [separator, escapedPattern, flags]. In a:escapedPattern, the a:separator is -" consistently escaped (i.e. also when the original arguments haven't been -" enclosed in such). +" [separator, escapedPattern, flags, ...]. +" In a:escapedPattern, the a:separator is consistently escaped (i.e. also when +" the original arguments haven't been enclosed in such). "****************************************************************************** + " Note: We could delegate to ingo#cmdargs#pattern#RawParse(), but let's + " duplicate this for now to avoid another redirection. let l:match = call('s:Parse', [a:arguments] + a:000) if empty(l:match) - return ['/', escape(a:arguments, '/')] + (a:0 ? [''] : []) + let l:pattern = ingo#regexp#FromLiteralText(a:arguments, 1, '/') + return ['/', l:pattern] + (a:0 ? repeat([''], a:0 >= 2 ? a:2 : 1) : []) else - return l:match[1: (a:0 ? 3 : 2)] + return l:match[1: (a:0 ? (a:0 >= 2 ? a:2 + 2 : 3) : 2)] endif endfunction function! ingo#cmdargs#pattern#ParseUnescaped( arguments, ... ) @@ -67,17 +146,18 @@ function! ingo#cmdargs#pattern#ParseUnescaped( arguments, ... ) "* INPUTS: " a:arguments Command arguments to parse. " a:flagsExpr Pattern that captures any optional part after the pattern. +" a:flagsMatchCount Number of capture groups returned from a:flagsExpr. "* RETURN VALUES: " unescapedPattern (String); if a:flagsExpr is given instead List of -" [unescapedPattern, flags]. In a:unescapedPattern, any separator used in +" [unescapedPattern, flags, ...]. In a:unescapedPattern, any separator used in " a:arguments is unescaped. "****************************************************************************** let l:match = call('s:Parse', [a:arguments] + a:000) if empty(l:match) - return (a:0 ? [a:arguments, ''] : a:arguments) + return (a:0 ? [a:arguments] + repeat([''], a:0 >= 2 ? a:2 : 1) : a:arguments) else let l:unescapedPattern = ingo#escape#Unescape(l:match[2], l:match[1]) - return (a:0 ? [l:unescapedPattern, l:match[3]] : l:unescapedPattern) + return (a:0 ? [l:unescapedPattern] + l:match[3: (a:0 >= 2 ? a:2 + 2 : 3)] : l:unescapedPattern) endif endfunction function! ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord( arguments, ... ) @@ -95,18 +175,19 @@ function! ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord( arguments, .. "* INPUTS: " a:arguments Command arguments to parse. " a:flagsExpr Pattern that captures any optional part after the pattern. +" a:flagsMatchCount Number of capture groups returned from a:flagsExpr. "* RETURN VALUES: " unescapedPattern (String); if a:flagsExpr is given instead List of -" [unescapedPattern, flags]. In a:unescapedPattern, any separator used in +" [unescapedPattern, flags, ...]. In a:unescapedPattern, any separator used in " a:arguments is unescaped. "****************************************************************************** let l:match = call('s:Parse', [a:arguments] + a:000) if empty(l:match) let l:unescapedPattern = ingo#regexp#FromLiteralText(a:arguments, 1, '') - return (a:0 ? [l:unescapedPattern, ''] : l:unescapedPattern) + return (a:0 ? [l:unescapedPattern] + repeat([''], a:0 >= 2 ? a:2 : 1) : l:unescapedPattern) else let l:unescapedPattern = ingo#escape#Unescape(l:match[2], l:match[1]) - return (a:0 ? [l:unescapedPattern, l:match[3]] : l:unescapedPattern) + return (a:0 ? [l:unescapedPattern] + l:match[3: (a:0 >= 2 ? a:2 + 2 : 3)] : l:unescapedPattern) endif endfunction @@ -160,4 +241,21 @@ function! ingo#cmdargs#pattern#IsDelimited( arguments, ... ) return (! empty(l:match)) endfunction +function! ingo#cmdargs#pattern#Render( arguments ) +"****************************************************************************** +"* PURPOSE: +" Create a single string from the parsed pattern arguments. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:arguments Return value from any of the ...#Parse... methods defined here. +"* RETURN VALUES: +" String with separator-delimited pattern, followed by any additional flags, +" etc. +"****************************************************************************** + return a:arguments[0] . a:arguments[1] . a:arguments[0] . join(a:arguments[2:], '') +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/cmdargs/register.vim b/autoload/ingo/cmdargs/register.vim index 8fa16d1..742c145 100644 --- a/autoload/ingo/cmdargs/register.vim +++ b/autoload/ingo/cmdargs/register.vim @@ -2,19 +2,81 @@ " " DEPENDENCIES: " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.002 08-Dec-2016 Add +" ingo#cmdargs#register#ParsePrependedWritableRegister() +" alternative to +" ingo#cmdargs#register#ParseAppendedWritableRegister(). " 1.017.001 10-Mar-2014 file creation +let s:save_cpo = &cpo +set cpo&vim let s:writableRegisterExpr = '\([-a-zA-Z0-9"*+_/]\)' +function! s:GetDirectSeparator( optionalArguments ) + return (len(a:optionalArguments) > 0 ? + \ (empty(a:optionalArguments[0]) ? + \ '\%$\%^' : + \ a:optionalArguments[0] + \ ) : + \ '[[:alnum:][:space:]\\"|]\@![\x00-\xFF]' + \) +endfunction + function! ingo#cmdargs#register#ParseAppendedWritableRegister( arguments, ... ) - let l:directSeparator = (a:0 ? a:1 : '[[:alnum:][:space:]\\"|]\@![\x00-\xFF]') - let l:matches = matchlist(a:arguments, '^\(.\{-}\)\%(\%(\%(' . l:directSeparator . '\)\@<=\s*\|\s\+\)' . s:writableRegisterExpr . '\)$') +"****************************************************************************** +"* PURPOSE: +" Parse a:arguments into any stuff and a writable register at the end, +" separated by non-alphanumeric character or whitespace (or the optional +" a:directSeparator). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:arguments Command arguments to parse. +" a:directSeparator Optional regular expression for the separator (parsed +" into text) between the text and register (with optional +" whitespace in between; mandatory whitespace is always an +" alternative). Defaults to any non-alphanumeric +" character. If empty: there must be whitespace between +" text and register. +"* RETURN VALUES: +" [text, register], or [a:arguments, ''] if no register could be parsed. +"****************************************************************************** + let l:matches = matchlist(a:arguments, '^\(.\{-}\)\%(\%(\%(' . s:GetDirectSeparator(a:000) . '\)\@<=\s*\|\s\+\)' . s:writableRegisterExpr . '\)$') return (empty(l:matches) ? [a:arguments, ''] : l:matches[1:2]) endfunction +function! ingo#cmdargs#register#ParsePrependedWritableRegister( arguments, ... ) +"****************************************************************************** +"* PURPOSE: +" Parse a:arguments into a writable register at the beginning, and any +" following stuff, separated by non-alphanumeric character or whitespace (or +" the optional a:directSeparator). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:arguments Command arguments to parse. +" a:directSeparator Optional regular expression for the separator (parsed +" into text) between the text and register (with optional +" whitespace in between; mandatory whitespace is always an +" alternative). Defaults to any non-alphanumeric +" character. If empty: there must be whitespace between +" text and register. +"* RETURN VALUES: +" [register, text], or ['', a:arguments] if no register could be parsed. +"****************************************************************************** + let l:matches = matchlist(a:arguments, '^' . s:writableRegisterExpr . '\%(\%(\s*' . s:GetDirectSeparator(a:000) . '\)\@=\|\s\+\)\(.*\)$') + return (empty(l:matches) ? ['', a:arguments] : l:matches[1:2]) +endfunction + +let &cpo = s:save_cpo +unlet s:save_cpo " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/cmdargs/substitute.vim b/autoload/ingo/cmdargs/substitute.vim index e911504..83e437a 100644 --- a/autoload/ingo/cmdargs/substitute.vim +++ b/autoload/ingo/cmdargs/substitute.vim @@ -3,7 +3,7 @@ " DEPENDENCIES: " - ingo/list.vim autoload script " -" Copyright: (C) 2012-2014 Ingo Karkat +" Copyright: (C) 2012-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat @@ -110,7 +110,7 @@ function! ingo#cmdargs#substitute#Parse( arguments, ... ) " A list of [separator, pattern, replacement, flags, count] (default) " A list of [separator, pattern, replacement] when a:options.flagsExpr is " empty or a:options.flagsMatchCount is 0. -" A list of [separator, pattern, replacement, flags, count, submatch1, ...]; +" A list of [separator, pattern, replacement, submatch1, ...]; " elements added depending on a:options.flagsMatchCount. " flags and count are meant to be directly concatenated; count therefore keeps " leading whitespace, but be aware that this is optional with :substitute, diff --git a/autoload/ingo/cmdline/showmode.vim b/autoload/ingo/cmdline/showmode.vim index 5ed6ac3..a575909 100644 --- a/autoload/ingo/cmdline/showmode.vim +++ b/autoload/ingo/cmdline/showmode.vim @@ -2,19 +2,35 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.003 26-Jan-2016 Add ingo#cmdline#showmode#TemporaryNoShowMode() +" variant of +" ingo#cmdline#showmode#OneLineTemporaryNoShowMode(). " 1.009.002 20-Jun-2013 Indicate activation with return code. " 1.009.001 18-Jun-2013 file creation from SnippetComplete.vim -function! ingo#cmdline#showmode#OneLineTemporaryNoShowMode() - " An active 'showmode' setting may prevent the user from seeing the message - " in a one-line command line. Thus, we temporarily disable the 'showmode' - " setting. +function! ingo#cmdline#showmode#TemporaryNoShowMode() +"****************************************************************************** +"* PURPOSE: +" An active 'showmode' setting may prevent the user from seeing the message in +" a command line. Thus, we temporarily disable the 'showmode' setting. +" Sometimes, this only happens in a single-line command line, but :echo'd text +" is visible when 'cmdline' is larger than 1. For that, use +" ingo#cmdline#showmode#OneLineTemporaryNoShowMode(). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" None. +"* RETURN VALUES: +" Boolean flag whether the temporary mode has been activated. +"****************************************************************************** if ! &showmode || &cmdheight > 1 return 0 endif @@ -25,9 +41,22 @@ function! ingo#cmdline#showmode#OneLineTemporaryNoShowMode() " is moved or insert mode is left. augroup IngoLibraryNoShowMode autocmd! - autocmd CursorMovedI,InsertLeave * set showmode | autocmd! IngoLibraryNoShowMode + + " XXX: After a cursor move, the mode message doesn't instantly appear + " again. A jump with scrolling or another mode change has to happen. + " Neither :redraw nor :redrawstatus will do, but apparently :echo + " triggers an update. + autocmd CursorMovedI * set showmode | echo '' | autocmd! IngoLibraryNoShowMode + + autocmd InsertLeave * set showmode | autocmd! IngoLibraryNoShowMode augroup END return 1 endfunction +function! ingo#cmdline#showmode#OneLineTemporaryNoShowMode() + if &cmdheight > 1 + return 0 + endif + return ingo#cmdline#showmode#TemporaryNoShowMode() +endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/codec/URL.vim b/autoload/ingo/codec/URL.vim index a4c957f..19b2d7f 100644 --- a/autoload/ingo/codec/URL.vim +++ b/autoload/ingo/codec/URL.vim @@ -1,8 +1,9 @@ " ingo/codec/URL.vim: URL encoding / decoding. " " DEPENDENCIES: +" - ingo/collections/fromsplit.vim autoload script " -" Copyright: (C) 2012-2014 Ingo Karkat +" Copyright: (C) 2012-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Source: @@ -12,6 +13,10 @@ " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.004 10-Oct-2016 ingo#codec#URL#Decode(): Also convert the +" character set to UTF-8 to properly handle +" non-ASCII characters. For example, %C3%9C should +" decode to "Ü", not to "ɜ". " 1.019.003 20-May-2014 Move into ingo library, because this is now used " by multiple plugins. " 002 09-May-2012 Add subs#URL#FilespecEncode() that does not @@ -29,9 +34,15 @@ function! ingo#codec#URL#FilespecEncode( text ) return s:Encode('[^A-Za-z0-9_./~-]', substitute(a:text, '\\', '/', 'g')) endfunction +function! ingo#codec#URL#DecodeAndConvertCharset( urlEscapedText ) + let l:decodedText = substitute(a:urlEscapedText, '%\(\x\x\)', '\=nr2char(''0x'' . submatch(1))', 'g') + "let l:convertedText = subs#Charset#LatinToUtf8(l:decodedText) + let l:convertedText = iconv(l:decodedText, 'utf-8', 'latin1') + return l:convertedText +endfunction function! ingo#codec#URL#Decode( text ) let l:text = substitute(substitute(substitute(a:text, '%0[Aa]\n$', '%0A', ''), '%0[Aa]', '\n', 'g'), '+', ' ', 'g') - return substitute(l:text, '%\(\x\x\)', '\=nr2char("0x" . submatch(1))', 'g') + return join(ingo#collections#fromsplit#MapSeparators(l:text, '\%(%\x\x\)\+', 'ingo#codec#URL#DecodeAndConvertCharset(v:val)'), '') endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/collections.vim b/autoload/ingo/collections.vim index bb0cf33..a31afc4 100644 --- a/autoload/ingo/collections.vim +++ b/autoload/ingo/collections.vim @@ -4,12 +4,18 @@ " - ingo/dict.vim autoload script " - ingo/list.vim autoload script " -" Copyright: (C) 2011-2015 Ingo Karkat +" Copyright: (C) 2011-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.015 10-Oct-2016 Add +" ingo#collections#SeparateItemsAndSeparators(), a +" variant of +" ingo#collections#SplitKeepSeparators(). +" 1.025.014 24-Jul-2016 Add ingo#collections#Reduce(). +" 1.025.013 01-May-2015 Add ingo#collections#Partition(). " 1.023.012 22-Oct-2014 Add ingo#collections#mapsort(). " 1.014.011 15-Oct-2013 Use the extracted ingo#list#AddOrExtend(). " 1.011.010 12-Jul-2013 Make ingo#collections#ToDict() handle empty list @@ -218,6 +224,60 @@ function! ingo#collections#SplitKeepSeparators( expr, pattern, ... ) return l:items endfunction +function! ingo#collections#SeparateItemsAndSeparators( expr, pattern, ... ) +"****************************************************************************** +"* PURPOSE: +" Like the built-in |split()|, but return both items and the separators +" matched by a:pattern as two separate Lists. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" If a:keepempty, len(items) == len(separators) + 1 +"* INPUTS: +" a:expr Text to be split. +" a:pattern Regular expression that specifies the separator text that +" delimits the items. +" a:keepempty When the first or last item is empty it is omitted, unless the +" {keepempty} argument is given and it's non-zero. +" Other empty items are kept when {pattern} matches at least one +" character or when {keepempty} is non-zero. +"* RETURN VALUES: +" List of [items, separators]. +"****************************************************************************** + let l:keepempty = (a:0 ? a:1 : 0) + let l:prevIndex = 0 + let l:index = 0 + let l:separator = '' + let l:items = [] + let l:separators = [] + + while ! empty(a:expr) + let l:index = match(a:expr, a:pattern, l:prevIndex) + if l:index == -1 + call s:add(l:items, strpart(a:expr, l:prevIndex), l:keepempty) + break + endif + let l:item = strpart(a:expr, l:prevIndex, (l:index - l:prevIndex)) + call s:add(l:items, l:item, (l:keepempty || ! empty(l:separator))) + + let l:prevIndex = matchend(a:expr, a:pattern, l:prevIndex) + let l:separator = strpart(a:expr, l:index, (l:prevIndex - l:index)) + + if empty(l:item) && empty(l:separator) + " We have a zero-width separator; consume at least one character to + " avoid the endless loop. + let l:prevIndex = matchend(a:expr, '\_.', l:index) + if l:prevIndex == -1 + break + endif + call add(l:items, strpart(a:expr, l:index, (l:prevIndex - l:index))) + else + call s:add(l:separators, l:separator, l:keepempty) + endif + endwhile + + return [l:items, l:separators] +endfunction function! ingo#collections#isort( i1, i2 ) "****************************************************************************** @@ -310,4 +370,79 @@ function! ingo#collections#Flatten( list ) return l:result endfunction +function! ingo#collections#Partition( list, Predicate ) +"****************************************************************************** +"* PURPOSE: +" Separate a List / Dictionary into two, depending on whether a:Predicate is +" true for each member of the collection. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List or Dictionary. +" a:Predicate Either a Funcref or an expression to be eval()ed. +"* RETURN VALUES: +" List of [in, out], which are disjunct sub-Lists / sub-Dictionaries +" containing the items where a:Predicate is true / is false. +"****************************************************************************** + if type(a:list) == type([]) + let [l:in, l:out] = [[], []] + for l:item in a:list + if ingo#actions#EvaluateWithValOrFunc(a:Predicate, l:item) + call add(l:in, l:item) + else + call add(l:out, l:item) + endif + endfor + elseif type(a:list) == type({}) + let [l:in, l:out] = [{}, {}] + for l:item in items(a:list) + if ingo#actions#EvaluateWithValOrFunc(a:Predicate, l:item) + let l:in[l:item[0]] = l:item[1] + else + let l:out[l:item[0]] = l:item[1] + endif + endfor + else + throw 'ASSERT: Invalid type for list' + endif + + return [l:in, l:out] +endfunction + +function! ingo#collections#Reduce( list, Callback, initialValue ) +"****************************************************************************** +"* PURPOSE: +" Reduce a List / Dictionary into a single value by repeatedly applying +" a:Callback to the accumulator (as v:val[0]) and a List element / [key, +" value] Dictionary element (as v:val[1]). Also known as "fold left". +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List or Dictionary. +" a:Callback Either a Funcref or an expression to be eval()ed. +" a:initialValue Initial value for the accumulator. +"* RETURN VALUES: +" Accumulated value. +"****************************************************************************** + let l:accumulator = a:initialValue + + if type(a:list) == type([]) + for l:item in a:list + let l:accumulator = ingo#actions#EvaluateWithValOrFunc(a:Callback, l:accumulator, l:item) + endfor + elseif type(a:list) == type({}) + for l:item in items(a:list) + let l:accumulator = ingo#actions#EvaluateWithValOrFunc(a:Callback, l:accumulator, l:item) + endfor + else + throw 'ASSERT: Invalid type for list' + endif + + return l:accumulator +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/collections/differences.vim b/autoload/ingo/collections/differences.vim index 54cdfb3..af08084 100644 --- a/autoload/ingo/collections/differences.vim +++ b/autoload/ingo/collections/differences.vim @@ -2,12 +2,16 @@ " " DEPENDENCIES: " -" Copyright: (C) 2015 Ingo Karkat +" Copyright: (C) 2015-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.002 29-Jul-2016 Add +" ingo#collections#differences#ContainsLoosely() +" and +" ingo#collections#differences#ContainsStrictly(). " 1.024.001 13-Feb-2015 file creation function! s:GetMissing( list1, list2 ) @@ -41,4 +45,47 @@ function! ingo#collections#differences#Get( list1, list2 ) return [l:notIn2, l:notIn1] endfunction +function! ingo#collections#differences#ContainsLoosely( list1, list2 ) +"****************************************************************************** +"* PURPOSE: +" Test whether all elements in a:list2 are also contained in a:list1. Each +" equal element need only be contained once. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list1 A list that may contain all elements from a:list2. +" a:list2 List where all elements may also be contained in a:list1. +"* RETURN VALUES: +" 1 if all elements from a:list2 are also contained in a:list1, 0 otherwise. +"****************************************************************************** + return empty(s:GetMissing(a:list2, a:list1)) +endfunction +function! ingo#collections#differences#ContainsStrictly( list1, list2 ) +"****************************************************************************** +"* PURPOSE: +" Test whether all elements in a:list2 are also contained in a:list1. Each +" equal element must be contained at least as often. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list1 A list that may contain all elements from a:list2. +" a:list2 List where all elements may also be contained in a:list1. +"* RETURN VALUES: +" 1 if all elements from a:list2 are also contained in a:list1, 0 otherwise. +"****************************************************************************** + let l:copy = copy(a:list1) + for l:item in a:list2 + let l:idx = index(l:copy, l:item) + if l:idx == -1 + return 0 + endif + call remove(l:copy, l:idx) + endfor + return 1 +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/collections/fromsplit.vim b/autoload/ingo/collections/fromsplit.vim new file mode 100644 index 0000000..13579c5 --- /dev/null +++ b/autoload/ingo/collections/fromsplit.vim @@ -0,0 +1,89 @@ +" ingo/collections/fromsplit.vim: Functions to split a string and operate on the results. +" +" DEPENDENCIES: +" - ingo/collections.vim autoload script +" - ingo/list.vim autoload script +" +" Copyright: (C) 2016-2017 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.002 24-Jan-2017 Add +" ingo#collections#fromsplit#MapItemsAndSeparators(). +" 1.028.001 10-Oct-2016 file creation + +function! s:Map( isItems, expr, pattern, Expr2 ) + let l:result = ingo#collections#SeparateItemsAndSeparators(a:expr, a:pattern, 1) + call map(l:result[! a:isItems], a:Expr2) + return ingo#list#Join(l:result[0], l:result[1]) +endfunction +function! ingo#collections#fromsplit#MapItems( expr, pattern, Expr2 ) +"****************************************************************************** +"* PURPOSE: +" Split a:expr on a:pattern, then apply a:Expr2 over the List of items (i.e. +" the elements in between the a:pattern matches), and return a List of [ +" mapped-item1, delimiter1, mapped-item2, delimiter2, ...] +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be split. +" a:pattern Regular expression that specifies the separator text that +" delimits the items. +" a:Expr2 String or Funcref argument to |map()|. +"* RETURN VALUES: +" Single list of split items and separators interspersed. +"****************************************************************************** + return s:Map(1, a:expr, a:pattern, a:Expr2) +endfunction +function! ingo#collections#fromsplit#MapSeparators( expr, pattern, Expr2 ) +"****************************************************************************** +"* PURPOSE: +" Split a:expr on a:pattern, then apply a:Expr2 over the List of separators +" (i.e. the a:pattern matches), and return a List of [ item1, +" mapped-delimiter1, item2, mapped-delimiter2, ...] +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be split. +" a:pattern Regular expression that specifies the separator text that +" delimits the items. +" a:Expr2 String or Funcref argument to |map()|. +"* RETURN VALUES: +" Single list of split items and separators interspersed. +"****************************************************************************** + return s:Map(0, a:expr, a:pattern, a:Expr2) +endfunction +function! ingo#collections#fromsplit#MapItemsAndSeparators( expr, pattern, ItemExpr2, SeparatorExpr2 ) +"****************************************************************************** +"* PURPOSE: +" Split a:expr on a:pattern, then apply a:ItemExpr2 over the List of items +" (i.e. the elements in between the a:pattern matches) and apply +" a:SeparatorExpr2 over the List of separators (i.e. the a:pattern matches), +" and return a List of [ mapped-item1, mapped-delimiter1, mapped-item2, +" mapped-delimiter2, ...] +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be split. +" a:pattern Regular expression that specifies the separator text that +" delimits the items. +" a:ItemExpr2 String or Funcref argument to |map()|. +" a:SeparatorExpr2 String or Funcref argument to |map()|. +"* RETURN VALUES: +" Single list of split items and separators interspersed. +"****************************************************************************** + let l:result = ingo#collections#SeparateItemsAndSeparators(a:expr, a:pattern, 1) + call map(l:result[0], a:ItemExpr2) + call map(l:result[1], a:SeparatorExpr2) + return ingo#list#Join(l:result[0], l:result[1]) +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/collections/unique.vim b/autoload/ingo/collections/unique.vim index ea9c432..946a381 100644 --- a/autoload/ingo/collections/unique.vim +++ b/autoload/ingo/collections/unique.vim @@ -2,12 +2,14 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.003 17-Feb-2016 Add ingo#collections#unique#Insert() and +" ingo#collections#unique#Add(). " 1.010.002 03-Jul-2013 Add ingo#collections#unique#AddNew() and " ingo#collections#unique#InsertNew(). " 1.009.001 25-Jun-2013 file creation @@ -97,4 +99,43 @@ function! ingo#collections#unique#ExtendWithNew( expr1, expr2, ... ) return call('extend', [a:expr1, l:newItems] + a:000) endfunction +function! ingo#collections#unique#Insert( list, expr, ... ) +"****************************************************************************** +"* PURPOSE: +" Insert a:expr at the start of a:list (if a:idx is specified before the item +" with index a:idx), and remove any other elements equal to a:expr from the +" list (which effectively moves a:expr to the front). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List to be modified. +" a:expr Item to be added. +" a:idx Optional index before which a:expr is inserted. +"* RETURN VALUES: +" a:list +"****************************************************************************** + call filter(a:list, 'v:val isnot# a:expr') + return call('insert', [a:list, a:expr] + a:000) +endfunction +function! ingo#collections#unique#Add( list, expr ) +"****************************************************************************** +"* PURPOSE: +" Append a:expr to a:list, and remove any other elements equal to a:expr from +" the list (which effectively moves a:expr to the back). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List to be modified. +" a:expr Item to be added. +"* RETURN VALUES: +" a:list +"****************************************************************************** + call filter(a:list, 'v:val isnot# a:expr') + return add(a:list, a:expr) +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/comments.vim b/autoload/ingo/comments.vim index dfd12a0..ebc512f 100644 --- a/autoload/ingo/comments.vim +++ b/autoload/ingo/comments.vim @@ -1,13 +1,27 @@ " ingo/comments.vim: Functions around comment handling. " " DEPENDENCIES: +" - ingo/compat.vim autoload script " -" Copyright: (C) 2011-2013 Ingo Karkat +" Copyright: (C) 2011-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.007 09-Jan-2017 Add ingo#comments#SplitAll(), a more powerful +" variant of ingo#comments#SplitIndentAndText(). +" 1.029.006 02-Dec-2016 CHG: ingo#comments#RemoveCommentPrefix() isn't +" useful as it omits any indent before the comment +" prefix. Change its implementation to just erase +" the prefix itself. +" Add ingo#comments#SplitIndentAndText() to +" provide what ingo#comments#RemoveCommentPrefix() +" was previously used to: The line broken into +" indent (before, after, and with the comment +" prefix), and the remaining text. +" FIX: Wrong negation in a:options.isIgnoreIndent +" documentation. " 1.013.005 06-Sep-2013 CHG: Make a:isIgnoreIndent flag to " ingo#comments#CheckComment() optional and add " a:isStripNonEssentialWhiteSpaceFromCommentString, @@ -48,9 +62,10 @@ function! ingo#comments#CheckComment( text, ... ) "* INPUTS: " a:text The text to be checked. If the "b" flag is contained in " 'comments', the proper whitespace must exist. -" a:options.isIgnoreIndent Flag; if set (the default), there must either be -" no leading whitespace or exactly the amount -" mandated by the indent of a three-piece comment. +" a:options.isIgnoreIndent Flag; unless set (the default), there must +" either be no leading whitespace or exactly the +" amount mandated by the indent of a three-piece +" comment. " a:options.isStripNonEssentialWhiteSpaceFromCommentString " Flag; if set (the default), any trailing " whitespace in the returned commentstring (e.g. @@ -164,30 +179,100 @@ function! ingo#comments#RenderComment( text, checkComment ) endif endfunction -function! ingo#comments#RemoveCommentPrefix( text, checkComment ) +function! ingo#comments#RemoveCommentPrefix( line ) "****************************************************************************** "* PURPOSE: -" Remove the detected a:checkComment from a:text. Whitespace between the -" comment prefix and actual comment (that does not belong to the commentstring -" itself) is kept. +" Remove the comment prefix from a:line while keeping the overall indent. "* ASSUMPTIONS / PRECONDITIONS: " None. "* EFFECTS / POSTCONDITIONS: " None. "* INPUTS: -" a:text The text to be rendered comment-less. -" a:checkComment Comment information returned by ingo#comments#CheckComment(). +" a:line The text of the line to be rendered comment-less. "* RETURN VALUES: -" Returns a:text unchanged if a:checkComment is empty. -" Otherwise, returns a:text rendered without the comment prefix. +" Return a:line rendered with the comment prefix erased and replaced by the +" appropriate whitespace. +"****************************************************************************** + let l:checkComment = ingo#comments#CheckComment(a:line) + if empty(l:checkComment) + return a:line + endif + + let [l:indentWithCommentPrefix, l:text] = s:SplitIndentAndText(a:line, l:checkComment) + let l:indentNum = ingo#compat#strdisplaywidth(l:indentWithCommentPrefix) + + let l:indent = repeat(' ', l:indentNum) + if ! &l:expandtab + let l:indent = substitute(l:indent, ' \{' . &l:tabstop . '}', '\t', 'g') + endif + return l:indent . l:text +endfunction +function! ingo#comments#SplitIndentAndText( line ) +"****************************************************************************** +"* PURPOSE: +" Split the line into any leading indent before the comment prefix plus the +" prefix itself plus indent after it, and the text after it. If there's no +" comment, split indent from text. +"* SEE ALSO: +" ingo#indent#Split() directly takes a line number and does not consider +" comment prefixes. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:line The line to be split. +"* RETURN VALUES: +" Returns [indent, text]. "****************************************************************************** + return s:SplitIndentAndText(a:line, ingo#comments#CheckComment(a:line)) +endfunction +function! s:SplitIndentAndText( line, checkComment ) if empty(a:checkComment) - return a:text + return matchlist(a:line, '^\(\s*\)\(.*\)$')[1:2] endif let [l:commentprefix, l:type, l:nestingLevel, l:isBlankRequired] = a:checkComment - return substitute(a:text, '\s*\V\C' . escape(l:commentprefix, '\') . (l:isBlankRequired ? '\s\@=' : ''), '', 'g') + return matchlist( + \ a:line, + \ '\V\C\^\(\s\*\%(' . escape(l:commentprefix, '\') . (l:isBlankRequired ? '\s\+' : '\s\*'). '\)\{' . (l:nestingLevel + 1) . '}\)' . + \ '\(\.\*\)\$' + \)[1:2] +endfunction +function! ingo#comments#SplitAll( line ) +"****************************************************************************** +"* PURPOSE: +" Split the line into any leading indent before the comment prefix, the prefix +" (-es, if nested) itself, indent after it, and the text after it. If there's +" no comment, split indent from text. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:line The line to be split. +"* RETURN VALUES: +" Returns [indentBefore, commentPrefix, indentAfter, text, isBlankRequired]. +"****************************************************************************** + let l:checkComment = ingo#comments#CheckComment(a:line) + if empty(l:checkComment) + let l:split = matchlist(a:line, '^\(\s*\)\(.*\)$')[1:2] + return [l:split[0], '', '', l:split[1]] + endif + + let [l:commentprefix, l:type, l:nestingLevel, l:isBlankRequired] = l:checkComment + + return matchlist( + \ a:line, + \ '\V\C\^\(\s\*\)\(' . + \ (l:nestingLevel > 1 ? + \ '\%(' . escape(l:commentprefix, '\') . (l:isBlankRequired ? '\s\+' : '\s\*') . '\)\{' . l:nestingLevel . '}\)' : + \ '' + \ ) . escape(l:commentprefix, '\') . '\)' . + \ '\(\s\*\)' . + \ '\(\.\*\)\$' + \)[1:4] + [l:isBlankRequired] endfunction function! ingo#comments#GetCommentPrefixType( prefix ) diff --git a/autoload/ingo/compat.vim b/autoload/ingo/compat.vim index 50fb381..f42fbef 100644 --- a/autoload/ingo/compat.vim +++ b/autoload/ingo/compat.vim @@ -5,12 +5,21 @@ " - ingo/options.vim autoload script " - ingo/strdisplaywidth.vim autoload script " -" Copyright: (C) 2013-2015 Ingo Karkat +" Copyright: (C) 2013-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.019 10-Jan-2017 Add ingo#compat#systemlist(). +" 1.028.018 25-Nov-2016 Add ingo#compat#getcurpos(). +" 1.026.017 11-Aug-2016 Add ingo#compat#strgetchar() and +" ingo#compat#strcharpart(), introduced in Vim +" 7.4.1730. +" Support ingo#compat#strchars() optional {skipcc} +" argument, introduced in Vim 7.4.755. +" 1.025.016 15-Jul-2016 Add ingo#compat#sha256(), with a fallback to an +" external sha256sum command. " 1.024.015 23-Apr-2015 Add ingo#compat#shiftwidth(), taken from :h " shiftwidth(). " 1.022.014 25-Sep-2014 FIX: Non-list argument to glob() for old Vim @@ -42,11 +51,11 @@ " 1.004.001 04-Apr-2013 file creation if exists('*shiftwidth') - function ingo#compat#shiftwidth() + function! ingo#compat#shiftwidth() return shiftwidth() endfunction else - function ingo#compat#shiftwidth() + function! ingo#compat#shiftwidth() return &shiftwidth endfunction endif @@ -69,15 +78,47 @@ else endif if exists('*strchars') - function! ingo#compat#strchars( expr ) - return strchars(a:expr) - endfunction + if v:version == 704 && has('patch755') || v:version > 704 + function! ingo#compat#strchars( ... ) + return call('strchars', a:000) + endfunction + else + function! ingo#compat#strchars( expr, ... ) + return (a:0 && a:1 ? strlen(substitute(a:expr, ".", "x", "g")) : strchars(a:expr)) + endfunction + endif else - function! ingo#compat#strchars( expr ) + function! ingo#compat#strchars( expr, ... ) return len(split(a:expr, '\zs')) endfunction endif +if exists('*strgetchar') + function! ingo#compat#strgetchar( expr, index ) + return strgetchar(a:expr, a:index) + endfunction +else + function! ingo#compat#strgetchar( expr, index ) + return char2nr(matchstr(a:expr, '.\{' . a:index . '}\zs.')) + endfunction +endif + +if exists('*strcharpart') + function! ingo#compat#strcharpart( ... ) + return call('strcharpart', a:000) + endfunction +else + function! ingo#compat#strcharpart( src, start, ... ) + let [l:start, l:len] = [a:start, a:0 ? a:1 : 0] + if l:start < 0 + let l:len += l:start + let l:start = 0 + endif + + return matchstr(a:src, '.\{' . l:start . '}\zs.' . (a:0 ? '\{,' . max([0, l:len]) . '}' : '*')) + endfunction +endif + if exists('*abs') function! ingo#compat#abs( expr ) return abs(a:expr) @@ -98,6 +139,25 @@ else endfunction endif +if exists('*getcurpos') + function! ingo#compat#getcurpos() + return getcurpos() + endfunction +else + function! ingo#compat#getcurpos() + return getpos('.') + endfunction +endif + +if exists('*systemlist') + function! ingo#compat#systemlist( ... ) + return call('systemlist', a:000) + endfunction +else + function! ingo#compat#systemlist( ... ) + return split(call('system', a:000), '\n') + endfunction +endif function! ingo#compat#fnameescape( filespec ) "******************************************************************************* @@ -334,4 +394,19 @@ else endfunction endif +if exists('*sha256') + function! ingo#compat#sha256( string ) + return sha256(a:string) + endfunction +elseif executable('sha256sum') + let s:printStringCommandTemplate = (ingo#os#IsWinOrDos() ? 'echo.%s' : 'printf %%s %s') + function! ingo#compat#sha256( string ) + return get(split(system(printf(s:printStringCommandTemplate . "|sha256sum", ingo#compat#shellescape(a:string)))), 0, '') + endfunction +else + function! ingo#compat#sha256( string ) + throw 'ingo#compat#sha256: Not implemented here' + endfunction +endif + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/compat/window.vim b/autoload/ingo/compat/window.vim new file mode 100644 index 0000000..7f502f5 --- /dev/null +++ b/autoload/ingo/compat/window.vim @@ -0,0 +1,27 @@ +" ingo/compat/window.vim: Compatibility functions for windows. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.028.001 10-Oct-2016 file creation + +if exists('*getcmdwintype') +function! ingo#compat#window#IsCmdlineWindow() + return ! empty(getcmdwintype()) +endfunction +elseif v:version >= 702 +function! ingo#compat#window#IsCmdlineWindow() + return bufname('') ==# '[Command Line]' +endfunction +else +function! ingo#compat#window#IsCmdlineWindow() + return bufname('') ==# 'command-line' +endfunction +endif + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/err.vim b/autoload/ingo/err.vim index b752e6e..0506ad1 100644 --- a/autoload/ingo/err.vim +++ b/autoload/ingo/err.vim @@ -2,12 +2,17 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.005 17-Dec-2016 Add ingo#err#SetAndBeep(). +" 1.028.004 18-Nov-2016 ENH: Add optional {context} to all ingo#err#... +" functions, in case other custom commands can be +" called between error setting and checking, to +" avoid clobbering of your error message. " 1.009.003 14-Jun-2013 Minor: Make substitute() robust against " 'ignorecase'. " 1.005.002 17-Apr-2013 Add ingo#err#IsSet() for those cases when @@ -44,25 +49,42 @@ " command(s) between your function and the :echoerr, and also query " ingo#err#IsSet() to avoid having to use a temporary variable to get the " returned error flag across the command(s). +" If there's a chance that other custom commands (that may also use these +" error functions) are invoked between your error setting and checking (also +" maybe triggered by autocmds), you can pass an optional {context} (e.g. your +" plugin / command name) to any of the commands. "****************************************************************************** +let s:err = {} let s:errmsg = '' -function! ingo#err#Get() - return s:errmsg +function! ingo#err#Get( ... ) + return (a:0 ? get(s:err, a:1, '') : s:errmsg) endfunction -function! ingo#err#Clear() - let s:errmsg = '' +function! ingo#err#Clear( ... ) + if a:0 + let s:err[a:1] = '' + else + let s:errmsg = '' + endif endfunction -function! ingo#err#IsSet() - return ! empty(s:errmsg) +function! ingo#err#IsSet( ... ) + return ! empty(a:0 ? get(s:err, a:1, '') : s:errmsg) endfunction -function! ingo#err#Set( errmsg ) - let s:errmsg = a:errmsg +function! ingo#err#Set( errmsg, ... ) + if a:0 + let s:err[a:1] = a:errmsg + else + let s:errmsg = a:errmsg + endif endfunction -function! ingo#err#SetVimException() - call ingo#err#Set(ingo#msg#MsgFromVimException()) +function! ingo#err#SetVimException( ... ) + call call('ingo#err#Set', [ingo#msg#MsgFromVimException()] + a:000) endfunction -function! ingo#err#SetCustomException( customPrefixPattern ) - call ingo#err#Set(substitute(v:exception, printf('^\C\%%(%s\):\s*', a:customPrefixPattern), '', '')) +function! ingo#err#SetCustomException( customPrefixPattern, ... ) + call call('ingo#err#Set', [substitute(v:exception, printf('^\C\%%(%s\):\s*', a:customPrefixPattern), '', '')] + a:000) +endfunction +function! ingo#err#SetAndBeep( text ) + call ingo#err#Set(a:text) + execute "normal! \\\" | " Beep. endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/escape.vim b/autoload/ingo/escape.vim index fef53e3..edcd527 100644 --- a/autoload/ingo/escape.vim +++ b/autoload/ingo/escape.vim @@ -2,12 +2,13 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013-2014 Ingo Karkat +" Copyright: (C) 2013-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.003 16-Dec-2016 Add ingo#escape#OnlyUnescaped(). " 1.017.002 20-Feb-2014 Add ingo#escape#UnescapeExpr(). " 1.009.001 15-Jun-2013 file creation @@ -48,4 +49,22 @@ function! ingo#escape#Unescape( string, chars ) return substitute(a:string, '\C\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@ " " REVISION DATE REMARKS +" 1.025.002 29-Apr-2016 Add ingo#escape#command#mapunescape(). " 1.012.001 09-Aug-2013 file creation function! ingo#escape#command#mapescape( command ) @@ -31,4 +32,23 @@ function! ingo#escape#command#mapescape( command ) return l:command endfunction +function! ingo#escape#command#mapunescape( command ) +"****************************************************************************** +"* PURPOSE: +" Unescape special mapping characters (, ) in a:command. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:command Ex command(s). +"* RETURN VALUES: +" a:command for use in a :map command. +"****************************************************************************** + let l:command = a:command + let l:command = substitute(l:command, '', '<', 'g') + let l:command = substitute(l:command, '', '|', 'g') + return l:command +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/escape/file.vim b/autoload/ingo/escape/file.vim index f814a2c..ab33f34 100644 --- a/autoload/ingo/escape/file.vim +++ b/autoload/ingo/escape/file.vim @@ -3,12 +3,16 @@ " DEPENDENCIES: " - ingo/os.vim autoload script " -" Copyright: (C) 2013-2014 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.005 01-Mar-2016 BUG: Unescaped backslash resulted in unclosed +" [...] regexp collection causing +" ingo#escape#file#fnameunescape() to fail to +" escape on Unix. " 1.023.004 17-Dec-2014 ENH: Add a:isFile flag to " ingo#escape#file#bufnameescape() in order to do " full matching on scratch buffer names. There, @@ -100,7 +104,7 @@ function! ingo#escape#file#fnameunescape( exfilespec, ... ) " Unescaped, normal filespec. "******************************************************************************* let l:isMakeFullPath = (a:0 ? a:1 : 0) - return fnamemodify( a:exfilespec, ':gs+\\\([ \t\n*?`%#''"|!<' . (ingo#os#IsWinOrDos() ? '' : '[{$\') . ']\)+\1+' . (l:isMakeFullPath ? ':p' : '')) + return fnamemodify(a:exfilespec, ':gs+\\\([ \t\n*?`%#''"|!<' . (ingo#os#IsWinOrDos() ? '' : '[{$\\') . ']\)+\1+' . (l:isMakeFullPath ? ':p' : '')) endfunction function! ingo#escape#file#autocmdescape( filespec ) diff --git a/autoload/ingo/format.vim b/autoload/ingo/format.vim index a5aa8d3..ceaea7d 100644 --- a/autoload/ingo/format.vim +++ b/autoload/ingo/format.vim @@ -2,12 +2,19 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.002 23-Jan-2017 FIX: ingo#format#Format(): An invalid %0$ +" references the last passed argument instead of +" yielding the empty string (as [argument-index$] +" is 1-based). Add bounds check to avoid that +" get() references index of -1. +" FIX: ingo#format#Format(): Also support escaping +" via "%%", as in printf(). " 1.015.001 18-Nov-2013 file creation function! ingo#format#Format( fmt, ... ) @@ -15,8 +22,8 @@ function! ingo#format#Format( fmt, ... ) "* PURPOSE: " Return a String with a:fmt, where "%" items are replaced by the formatted " form of their respective arguments. Like |printf()|, but like Java's -" String.format(), additionally supports explicit positioning with -" %[argument-index$]. +" String.format(), additionally supports explicit positioning with (1-based) +" %[argument-index$], e.g. "The %2$s is %1$d". "* ASSUMPTIONS / PRECONDITIONS: " None. "* EFFECTS / POSTCONDITIONS: @@ -33,7 +40,7 @@ function! ingo#format#Format( fmt, ... ) "****************************************************************************** let l:args = [] let s:consumedOriginalArgIdx = -1 - let l:printfFormat = substitute(a:fmt, '%\%(\(\d\+\)\$\|.\)', '\=s:ProcessFormat(a:000, l:args, submatch(1))', 'g') + let l:printfFormat = substitute(a:fmt, '%\@ 0 ? get(a:originalArgs, (a:argCnt - 1), '') : '') + call add(a:args, l:indexedArg) return '%' endif endfunction diff --git a/autoload/ingo/format/columns.vim b/autoload/ingo/format/columns.vim new file mode 100644 index 0000000..39ab599 --- /dev/null +++ b/autoload/ingo/format/columns.vim @@ -0,0 +1,59 @@ +" ingo/format/columns.vim: Functions for formatting in multiple columns. +" +" DEPENDENCIES: +" - ingo/strdisplaywidth.vim autoload script +" - ingo/strdisplaywidth/pad.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 001 11-Aug-2016 file creation + +function! ingo#format#columns#Distribute( strings, ... ) +"****************************************************************************** +"* PURPOSE: +" Distribute a:strings to a number of (equally sized) columns, fitting a +" maximum width of a:width / &columns. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:strings: List of strings. +" a:alignment One of "left", "middle", "right"; default is "left". +" a:width Maximum width of all columns taken together. +" a:columnSeparatorWidth Width of the column separator with which the +" returned inner List elements will be join()ed; +" default 1. +"* RETURN VALUES: +" List of [[c1s1, c2s1, ...], [c1s2, c2s2, ...], ...] +"****************************************************************************** + let l:PadFunction = function('ingo#strdisplaywidth#pad#' . {'left': 'Right', 'middle': 'Middle', 'right': 'Left'}[a:0 ? a:1 : 'left']) + let l:columnSeparatorWidth = (a:0 >= 3 ? a:3 : 1) + let l:maxWidth = ingo#strdisplaywidth#GetMinMax(a:strings)[1] + let l:colNum = (a:0 >= 2 ? a:2 : &columns) / (l:maxWidth + l:columnSeparatorWidth) + let l:rowNum = len(a:strings) / l:colNum + (len(a:strings) % l:colNum == 0 ? 0 : 1) + + "let l:result = repeat([[]], l:rowNum) " Unfortunately duplicates the same empty List. + let l:result = [] + for l:i in range(l:rowNum) + call add(l:result, []) + endfor + + let l:i = 0 + for l:string in a:strings + if l:i >= l:rowNum + let l:i = 0 + endif + + call add(l:result[l:i], call(l:PadFunction, [l:string, l:maxWidth])) + let l:i += 1 + endfor + + return l:result +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/fs/path/asfilename.vim b/autoload/ingo/fs/path/asfilename.vim new file mode 100644 index 0000000..06be598 --- /dev/null +++ b/autoload/ingo/fs/path/asfilename.vim @@ -0,0 +1,76 @@ +" ingo/fs/path/asfilename.vim: Encode / decode any filespec as a single filename. +" +" DEPENDENCIES: +" - ingo/dict.vim autoload script +" - ingo/fs/path.vim autoload script +" - ingo/fs/path/split.vim autoload script +" - ingo/os.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.028.001 18-Oct-2016 file creation +let s:save_cpo = &cpo +set cpo&vim + + " ex_docmd.c:11754 + " We want a file name without separators, because we're not going to make + " a directory. + " "normal" path separator -> "=+" + " "=" -> "==" + " ":" path separator -> "=-" +let s:encoder = { +\ ingo#fs#path#Separator(): '=+', +\ '=': '==', +\ ':': '=-', +\} +let s:decoder = ingo#dict#Mirror(s:encoder) + +function! ingo#fs#path#asfilename#Encode( filespec ) +"****************************************************************************** +"* PURPOSE: +" Encode a:filespec as a single filename, like :mkview does. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:filespec file spec (existing or non-existing, expansion to absolute path +" and normalization will be attempted) +"* RETURN VALUES: +" file name representing the absolute a:filespec; any path separators are +" escaped. +"****************************************************************************** + let l:filespec = ingo#fs#path#Normalize(fnamemodify(a:filespec, ':p')) + if ! empty($HOME) + let l:homeRelativeFilespec = ingo#fs#path#split#AtBasePath(l:filespec, $HOME) + if type(l:homeRelativeFilespec) != type([]) + let l:filespec = ingo#fs#path#Combine('~', l:homeRelativeFilespec) + endif + endif + + return substitute(l:filespec, '[=' . escape(ingo#fs#path#Separator(), '\') . (ingo#os#IsWinOrDos() ? ':' : '') . ']', '\=s:encoder[submatch(0)]', 'g') +endfunction +function! ingo#fs#path#asfilename#Decode( filename ) +"****************************************************************************** +"* PURPOSE: +" Decode a filespec encoded in a single filename via +" ingo#fs#path#asfilename#Encode() to a filespec. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:filename Encoded filespec +"* RETURN VALUES: +" filespec +"****************************************************************************** + return expand(substitute(a:filename, '=[+=-]', '\=s:decoder[submatch(0)]', 'g')) +endfunction + +let &cpo = s:save_cpo +unlet s:save_cpo +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/fs/path/split.vim b/autoload/ingo/fs/path/split.vim index 845ffce..97cdafd 100644 --- a/autoload/ingo/fs/path/split.vim +++ b/autoload/ingo/fs/path/split.vim @@ -2,16 +2,33 @@ " " DEPENDENCIES: " - ingo/fs/path.vim autoload script +" - ingo/str.vim autoload script " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.002 30-Apr-2015 Add ingo#fs#path#split#Contains(). " 1.019.001 22-May-2014 file creation function! ingo#fs#path#split#AtBasePath( filespec, basePath ) +"****************************************************************************** +"* PURPOSE: +" Split off a:basePath from a:filespec. The check will be done on normalized +" paths. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:filespec Filespec. +" a:basePath Filespec to the base directory that contains a:filespec. +"* RETURN VALUES: +" Remainder of a:filespec, after removing a:basePath, or empty List if +" a:filespec did not start with a:basePath. +"****************************************************************************** let l:filespec = ingo#fs#path#Combine(ingo#fs#path#Normalize(a:filespec, '/'), '') let l:basePath = ingo#fs#path#Combine(ingo#fs#path#Normalize(a:basePath, '/'), '') if ingo#str#StartsWith(l:filespec, l:basePath, ingo#fs#path#IsCaseInsensitive(l:filespec)) @@ -20,4 +37,26 @@ function! ingo#fs#path#split#AtBasePath( filespec, basePath ) return [] endfunction +function! ingo#fs#path#split#Contains( filespec, fragment ) +"****************************************************************************** +"* PURPOSE: +" Test whether a:filespec contains a:fragment anywhere. To match entire +" (anchored) path fragments, pass a fragment surrounded by forward slashes +" (e.g. "/foo/"); you can always use forward slashes, as these will be +" internally normalized. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:filespec Filespec to be examined. +" a:fragment Path fragment that may be contained inside a:filespec. +"* RETURN VALUES: +" 1 if contained, 0 if not. +"****************************************************************************** + let l:filespec = ingo#fs#path#Combine(ingo#fs#path#Normalize(a:filespec, '/'), '') + let l:fragment = ingo#fs#path#Normalize(a:fragment, '/') + return ingo#str#Contains(l:filespec, l:fragment, ingo#fs#path#IsCaseInsensitive(l:filespec)) +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/fs/traversal.vim b/autoload/ingo/fs/traversal.vim index f9f3bec..f653b25 100644 --- a/autoload/ingo/fs/traversal.vim +++ b/autoload/ingo/fs/traversal.vim @@ -6,12 +6,14 @@ " - ingo/fs/path.vim autoload script " - ingo/os.vim autoload script " -" Copyright: (C) 2013-2014 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.006 05-Dec-2016 Add +" ingo#fs#traversal#FindFirstContainedInUpDir(). " 1.022.005 22-Sep-2014 Use ingo#compat#globpath(). " 1.013.004 13-Sep-2013 Use operating system detection functions from " ingo/os.vim. @@ -22,6 +24,8 @@ " 1.003.002 26-Mar-2013 Rename to " ingo#fs#traversal#FindLastContainedInUpDir() " 001 22-Mar-2013 file creation +let s:save_cpo = &cpo +set cpo&vim function! ingo#fs#traversal#FindDirUpwards( Predicate, ... ) "****************************************************************************** @@ -61,6 +65,30 @@ function! ingo#fs#traversal#FindDirUpwards( Predicate, ... ) return '' endfunction +function! ingo#fs#traversal#FindFirstContainedInUpDir( expr, ... ) +"****************************************************************************** +"* PURPOSE: +" Traversing upwards from the current buffer's directory, find the first +" directory that yields a match for the a:expr glob. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr File glob that must match in the target upwards directory. +" a:dirspec Optional starting directory. Should be absolute or at least in a +" format that allows upward traversal via :h. If omitted, the +" search starts from the current buffer's directory. +"* RETURN VALUES: +" Dirspec of the first parent directory that matches a:expr. +" Empty string if a:expr every matches up to the filesytem's root. +"****************************************************************************** + return call( + \ function('ingo#fs#traversal#FindDirUpwards'), + \ [printf('! empty(ingo#compat#glob(ingo#fs#path#Combine(v:val, %s), 1))', string(a:expr))] + a:000 + \) +endfunction + function! ingo#fs#traversal#FindLastContainedInUpDir( expr, ... ) "****************************************************************************** "* PURPOSE: @@ -95,4 +123,6 @@ function! ingo#fs#traversal#FindLastContainedInUpDir( expr, ... ) return l:dir endfunction +let &cpo = s:save_cpo +unlet s:save_cpo " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/indent.vim b/autoload/ingo/indent.vim new file mode 100644 index 0000000..0f8a285 --- /dev/null +++ b/autoload/ingo/indent.vim @@ -0,0 +1,46 @@ +" ingo/indent.vim: Functions for working with indent. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.002 02-Dec-2016 Add ingo#indent#Split(), a simpler version of +" ingo#comments#SplitIndentAndText(). +" 1.028.001 25-Nov-2016 file creation + +function! ingo#indent#RangeSeveralTimes( firstLnum, lastLnum, command, times ) + for l:i in range(a:times) + silent execute a:firstLnum . ',' . a:lastLnum . a:command + endfor +endfunction + +function! ingo#indent#GetIndent( lnum ) + return matchstr(getline(a:lnum), '^\s*') +endfunction +function! ingo#indent#GetIndentLevel( lnum ) + return indent(a:lnum) / &l:shiftwidth +endfunction +function! ingo#indent#Split( lnum ) +"****************************************************************************** +"* PURPOSE: +" Split the line into any leading indent, and the text after it. +"* SEE ALSO: +" ingo#comments#SplitIndentAndText() also considers any comment prefix as part +" of the indent. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:lnum Number of the line to be split. +"* RETURN VALUES: +" Returns [a:indent, a:text]. +"****************************************************************************** + return matchlist(getline(a:lnum), '^\(\s*\)\(.*\)$')[1:2] +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/join.vim b/autoload/ingo/join.vim index 5f00eb9..fecaecc 100644 --- a/autoload/ingo/join.vim +++ b/autoload/ingo/join.vim @@ -3,7 +3,7 @@ " DEPENDENCIES: " - ingo/folds.vim autoload script " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat @@ -14,6 +14,24 @@ " 1.020.001 08-Jun-2014 file creation from ingocommands.vim function! ingo#join#Lines( lnum, isKeepSpace, separator ) +"****************************************************************************** +"* PURPOSE: +" Join a:lnum with the next line, putting a:separator in between (and +" optionally deleting any separating whitespace). +"* ASSUMPTIONS / PRECONDITIONS: +" The 'formatoptions' option may affect the join, especially M, B, j. +"* EFFECTS / POSTCONDITIONS: +" Joins lines. +"* INPUTS: +" a:lnum Line number of the first line to be joined. +" a:isKeepSpace Flag whether to keep whitespace (i.e. trailing in a:lnum, +" indent in a:lnum + 1) or remove it altogether. The joining +" itself does not add whitespace. +" a:separator String to be put in between the lines (also when one of them +" is completely empty). +"* RETURN VALUES: +" None. +"****************************************************************************** if a:isKeepSpace let l:lineLen = len(getline(a:lnum)) execute a:lnum . 'join!' @@ -36,6 +54,25 @@ function! ingo#join#Lines( lnum, isKeepSpace, separator ) endfunction function! ingo#join#Ranges( isKeepSpace, startLnum, endLnum, separator, ranges ) +"****************************************************************************** +"* PURPOSE: +" Join each range of lines in a:ranges. +"* ASSUMPTIONS / PRECONDITIONS: +" The 'formatoptions' option may affect the join, especially M, B, j. +"* EFFECTS / POSTCONDITIONS: +" Joins lines. +"* INPUTS: +" a:isKeepSpace Flag whether to keep whitespace (i.e. trailing in a:lnum, +" indent in a:lnum + 1) or remove it altogether. The joining +" itself does not add whitespace. +" a:startLnum Ignored. +" a:endLnum Ignored. +" a:separator String to be put in between the lines (also when one of them +" is completely empty). +" a:ranges List of [startLnum, endLnum] pairs. +"* RETURN VALUES: +" [ number of ranges, number of joined lines ] +"****************************************************************************** if empty(a:ranges) return [0, 0] endif @@ -58,6 +95,24 @@ function! ingo#join#Ranges( isKeepSpace, startLnum, endLnum, separator, ranges ) endfunction function! ingo#join#FoldedLines( isKeepSpace, startLnum, endLnum, separator ) +"****************************************************************************** +"* PURPOSE: +" Join all folded lines. +"* ASSUMPTIONS / PRECONDITIONS: +" The 'formatoptions' option may affect the join, especially M, B, j. +"* EFFECTS / POSTCONDITIONS: +" Joins lines. +"* INPUTS: +" a:isKeepSpace Flag whether to keep whitespace (i.e. trailing in a:lnum, +" indent in a:lnum + 1) or remove it altogether. The joining +" itself does not add whitespace. +" a:startLnum First line number to be considered. +" a:endLnum last line number to be considered. +" a:separator String to be put in between the lines (also when one of them +" is completely empty). +"* RETURN VALUES: +" [ number of ranges, number of joined lines ] +"****************************************************************************** let l:folds = ingo#folds#GetClosedFolds(a:startLnum, a:endLnum) return ingo#join#Ranges(a:isKeepSpace, a:startLnum, a:endLnum, a:separator, l:folds) endfunction diff --git a/autoload/ingo/line/replace.vim b/autoload/ingo/line/replace.vim new file mode 100644 index 0000000..e7811a8 --- /dev/null +++ b/autoload/ingo/line/replace.vim @@ -0,0 +1,39 @@ +" ingo/line/replace.vim: Functions to replace text in a single line. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 22-Dec-2016 file creation + +function! ingo#line#replace#Substitute( lnum, pat, sub, flags ) +"****************************************************************************** +"* PURPOSE: +" Substitute a pattern in a single line in the current buffer. Low-level +" alternative to :substitute without the need to suppress messages, undo +" search history clobbering, cursor move. +"* SEE ALSO: +" - ingo#lines#replace#Substitute() handles multiple lines, but is more +" costly. +"* ASSUMPTIONS / PRECONDITIONS: +" Does not handle inserted newlines; i.e. no additional lines will be created, +" the newline will be persisted as-is (^@). +"* EFFECTS / POSTCONDITIONS: +" Updates a:lnum. +"* INPUTS: +" a:lnum Existing line number. +" a:pat Regular expression to match. +" a:sub Replacement string. +" a:flags "g" for global replacement. +"* RETURN VALUES: +" If this succeeds, 0 is returned. If this fails (most likely because a:lnum is +" invalid) 1 is returned. +"****************************************************************************** + return setline(a:lnum, substitute(getline(a:lnum), a:pat, a:sub, a:flags)) +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/lines.vim b/autoload/ingo/lines.vim index 3966d95..dc91f26 100644 --- a/autoload/ingo/lines.vim +++ b/autoload/ingo/lines.vim @@ -77,8 +77,10 @@ function! ingo#lines#Replace( startLnum, endLnum, lines, ... ) "* EFFECTS / POSTCONDITIONS: " Sets change marks '[,'] to the replaced lines. "* INPUTS: -" a:startLnum First line to be replaced. -" a:endLnum Last line to be replaced. +" a:startLnum First line to be replaced. Use ingo#range#NetStart() if +" necessary. +" a:endLnum Last line to be replaced. Use ingo#range#NetEnd() if +" necessary. " a:lines List of lines or string (where lines are separated by \n " characters). " a:register Optional register to store the replaced lines. By default diff --git a/autoload/ingo/lines/replace.vim b/autoload/ingo/lines/replace.vim new file mode 100644 index 0000000..2381791 --- /dev/null +++ b/autoload/ingo/lines/replace.vim @@ -0,0 +1,66 @@ +" replace.vim: Functions to replace text in lines. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 23-Dec-2016 file creation + +function! ingo#lines#replace#Substitute( startLnum, endLnum, pat, sub, flags ) +"****************************************************************************** +"* PURPOSE: +" Substitute a pattern in lines in the current buffer. Low-level +" alternative to :[range]substitute without the need to suppress messages, +" undo search history clobbering, cursor move. +"* SEE ALSO: +" - ingo#line#replace#Substitute() is a cheaper alternative that only handles +" a single line, though. +"* ASSUMPTIONS / PRECONDITIONS: +" Handles inserted newlines and removed lines. +"* EFFECTS / POSTCONDITIONS: +" Updates the buffer. +"* INPUTS: +" a:startLnum Existing line number. +" a:endLnum Existing line number. +" a:pat Regular expression to match. It is applied to all lines joined via +" \n; the last line does not end in \n. That means that even if you +" match everything in all lines (.*), and replace it with the empty +" string, a single empty line will remain. +" a:sub Replacement string. +" a:flags "g" for global replacement. +"* RETURN VALUES: +" If this succeeds, 0 is returned. If this fails 1 is returned. +"****************************************************************************** + let l:lines = getline(a:startLnum, a:endLnum) + if empty(l:lines) | return 1 | endif + let l:lineNum = len(l:lines) + + let l:newLines = split(substitute(join(l:lines, "\n"), a:pat, a:sub, a:flags), '\n', 1) + let l:newLineNum = len(l:newLines) + + " Update existing lines first, then handle any additions / deletions. + for l:i in range(min([l:lineNum, l:newLineNum])) + call setline(a:startLnum + l:i, l:newLines[l:i]) + endfor + if l:newLineNum < l:lineNum + " We have less lines now; remove the surplus original ones. + " Unfortunately, there's no low-level function for deletion, so we need + " to use :delete. + let l:save_view = winsaveview() + let l:save_foldenable = &l:foldenable + setlocal nofoldenable + silent! execute printf('keepjumps %d,%ddelete _', a:startLnum + l:newLineNum, a:endLnum) + let &l:foldenable = l:save_foldenable + call winrestview(l:save_view) + return 0 + elseif l:newLineNum > l:lineNum + " Additional lines need to be appended. + return append(a:endLnum, l:newLines[l:lineNum : ]) + endif +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/list.vim b/autoload/ingo/list.vim index 23ede50..f5284cf 100644 --- a/autoload/ingo/list.vim +++ b/autoload/ingo/list.vim @@ -2,12 +2,13 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013-2015 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.003 10-Oct-2016 Add ingo#list#Join(). " 1.024.002 16-Mar-2015 Add ingo#list#Zip() and ingo#list#ZipLongest(). " 1.014.001 15-Oct-2013 file creation @@ -62,6 +63,21 @@ function! ingo#list#AddOrExtend( list, val, ... ) endfunction function! ingo#list#Zip( ... ) +"****************************************************************************** +"* PURPOSE: +" From several Lists, create a combined List. The first item is a List of all +" first items of the original Lists, the second a List of all second items, +" and so on, until one List is exhausted. Surplus items in other Lists are +" omitted. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list1, a:list2 +"* RETURN VALUES: +" List of Lists, each containing a certain index item of all source Lists. +"****************************************************************************** let l:result = [] for l:i in range(min(map(copy(a:000), 'len(v:val)'))) call add(l:result, map(copy(a:000), 'v:val[l:i]')) @@ -70,6 +86,21 @@ function! ingo#list#Zip( ... ) endfunction function! ingo#list#ZipLongest( defaultValue, ... ) +"****************************************************************************** +"* PURPOSE: +" From several Lists, create a combined List. The first item is a List of all +" first items of the original Lists, the second a List of all second items, +" and so on, until all Lists are exhausted. Missing items in shorter Lists are +" replaced by a:defaultValue. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list1, a:list2 +"* RETURN VALUES: +" List of Lists, each containing a certain index item of all source Lists. +"****************************************************************************** let l:result = [] for l:i in range(max(map(copy(a:000), 'len(v:val)'))) call add(l:result, map(copy(a:000), 'get(v:val, l:i, a:defaultValue)')) @@ -77,4 +108,34 @@ function! ingo#list#ZipLongest( defaultValue, ... ) return l:result endfunction +function! ingo#list#Join( ... ) +"****************************************************************************** +"* PURPOSE: +" From several Lists, create a combined List, starting with all first items, +" then all second items, and so on. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list1, a:list2 +"* RETURN VALUES: +" List of joined source Lists, from low to high indices. +"****************************************************************************** + let l:result = [] + let l:i = 0 + let l:isAdded = 1 + while l:isAdded + let l:isAdded = 0 + for l:j in range(a:0) + if l:i < len(a:000[l:j]) + call add(l:result, a:000[l:j][l:i]) + let l:isAdded = 1 + endif + endfor + let l:i += 1 + endwhile + return l:result +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/list/find.vim b/autoload/ingo/list/find.vim new file mode 100644 index 0000000..91aa26c --- /dev/null +++ b/autoload/ingo/list/find.vim @@ -0,0 +1,69 @@ +" find.vim: Functions for finding indices in Lists. +" +" DEPENDENCIES: +" - ingo/actions.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.028.001 21-Oct-2016 file creation + +function! ingo#list#find#FirstIndex( list, Filter ) +"****************************************************************************** +"* PURPOSE: +" Find the first index of an item in a:list where a:filter is true. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List to be searched. +" a:Filter Expression to be evaluated; v:val has the value of the current +" item. Returns false to skip the item. +" If a:Filter is a Funcref it is called with the value of the +" current item. +"* RETURN VALUES: +" First found index, or -1. +"****************************************************************************** + let l:idx = 0 + while l:idx < len(a:list) + if ingo#actions#EvaluateWithValOrFunc(a:Filter, a:list[l:idx]) + return l:idx + endif + let l:idx += 1 + endwhile + return -1 +endfunction + +function! ingo#list#find#Indices( list, Filter ) +"****************************************************************************** +"* PURPOSE: +" Find all indices of items in a:list where a:filter is true. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:list List to be searched. +" a:Filter Expression to be evaluated; v:val has the value of the current +" item. Returns false to skip the item. +" If a:Filter is a Funcref it is called with the value of the +" current item. +"* RETURN VALUES: +" List of found indices, or empty List. +"****************************************************************************** + let l:indices = [] + let l:idx = 0 + while l:idx < len(a:list) + if ingo#actions#EvaluateWithValOrFunc(a:Filter, a:list[l:idx]) + call add(l:indices, l:idx) + endif + let l:idx += 1 + endwhile + return l:indices +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/list/split.vim b/autoload/ingo/list/split.vim new file mode 100644 index 0000000..59bcea1 --- /dev/null +++ b/autoload/ingo/list/split.vim @@ -0,0 +1,45 @@ +" ingo/list/split.vim: Functions for splitting Lists. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 28-Dec-2016 file creation + +function! ingo#list#split#ChunksOf( list, n, ... ) +"****************************************************************************** +"* PURPOSE: +" Split a:list into a List of Lists of a:n elements. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" Clears a:list. +"* INPUTS: +" a:list Source list. +" a:n Number of elements for each sublist. +" a:fillValue Optional element that is used to fill the last sublist with if +" there are not a:n elements left for it. If omitted, the last +" sublist may have less than a:n elements. +"* RETURN VALUES: +" [[e1, e2, ... en], [...]] +"****************************************************************************** + let l:result = [] + while ! empty(a:list) + if len(a:list) >= a:n + let l:subList = remove(a:list, 0, a:n - 1) + else + let l:subList = remove(a:list, 0, -1) + if a:0 + call extend(l:subList, repeat([a:1], a:n - len(l:subList))) + endif + endif + call add(l:result, l:subList) + endwhile + return l:result +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/math.vim b/autoload/ingo/math.vim new file mode 100644 index 0000000..8aa81b8 --- /dev/null +++ b/autoload/ingo/math.vim @@ -0,0 +1,40 @@ +" ingo/math.vim: Mathematical functions. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 27-Dec-2016 file creation + +"****************************************************************************** +"* PURPOSE: +" Return the power of a:x to the exponent a:y as a Number. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:x Number. +" a:y Exponent. +"* RETURN VALUES: +" Number. +"****************************************************************************** +if exists('*pow') + function! ingo#math#PowNr( x, y ) + return float2nr(pow(a:x, a:y)) + endfunction +else + function! ingo#math#PowNr( x, y ) + let l:r = a:x + for l:i in range(a:y - 1) + let l:r = l:r * a:x + endfor + return l:r + endfunction +endif + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/msg.vim b/autoload/ingo/msg.vim index bb1f003..1d82e26 100644 --- a/autoload/ingo/msg.vim +++ b/autoload/ingo/msg.vim @@ -2,12 +2,20 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013-2014 Ingo Karkat +" Copyright: (C) 2013-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.010 10-Jan-2017 Add ingo#msg#ColoredMsg() and +" ingo#msg#ColoredStatusMsg(). +" 1.027.009 22-Aug-2016 Add ingo#msg#MsgFromShellError(). +" 1.025.008 01-Aug-2016 ingo#msg#HighlightMsg(): Make a:hlgroup +" optional, default to 'None' (so the function is +" useful to return to normal highlighting). +" Add ingo#msg#HighlightN(), an :echon variant. +" 1.025.007 15-Jul-2016 Add ingo#msg#VerboseMsg(). " 1.019.006 05-May-2014 Add optional a:isBeep argument to " ingo#msg#ErrorMsg(). " 1.009.005 21-Jun-2013 :echomsg sets v:statusmsg itself when there's no @@ -22,11 +30,16 @@ " 1.003.002 13-Mar-2013 Add ingo#msg#ShellError(). " 1.000.001 22-Jan-2013 file creation -function! ingo#msg#HighlightMsg( text, hlgroup ) - execute 'echohl' a:hlgroup +function! ingo#msg#HighlightMsg( text, ... ) + execute 'echohl' (a:0 ? a:1 : 'None') echomsg a:text echohl None endfunction +function! ingo#msg#HighlightN( text, ... ) + execute 'echohl' (a:0 ? a:1 : 'None') + echon a:text + echohl None +endfunction function! ingo#msg#StatusMsg( text, ... ) "****************************************************************************** @@ -40,6 +53,7 @@ function! ingo#msg#StatusMsg( text, ... ) " None. "* INPUTS: " a:text The message to be echoed and added to the message history. +" a:hlgroup Optional highlight group name. "* RETURN VALUES: " None. "****************************************************************************** @@ -52,6 +66,80 @@ function! ingo#msg#StatusMsg( text, ... ) endif endfunction +function! ingo#msg#ColoredMsg( ... ) +"****************************************************************************** +"* PURPOSE: +" Echo a message that contains various, differently highlighted parts. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:part | [a:part, a:hlgroup], ... Message parts or Pairs of message parts +" and highlight group names. For the +" former, reverts to "no highlighting". +"* RETURN VALUES: +" None. +"****************************************************************************** + let l:isFirst = 1 + + for l:element in a:000 + let [l:part, l:hlgroup] = (type(l:element) == type([]) ? l:element: [l:element, 'None']) + execute 'echohl' l:hlgroup + execute (l:isFirst ? 'echo' : 'echon') 'l:part' + let l:isFirst = 0 + endfor + echohl None +endfunction +function! ingo#msg#ColoredStatusMsg( ... ) +"****************************************************************************** +"* PURPOSE: +" Echo a message that contains various, differently highlighted parts, and +" store the full message in v:statusmsg. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" Performs a :redraw to put the message into the message history. +"* INPUTS: +" a:part | [a:part, a:hlgroup], ... Message parts or Pairs of message parts +" and highlight group names. For the +" former, reverts to "no highlighting". +"* RETURN VALUES: +" None. +"****************************************************************************** + let l:elements = map(copy(a:000), "(type(v:val) == type([]) ? v:val: [v:val, 'None'])") + let l:text = join(map(copy(l:elements), 'v:val[0]'), '') + echomsg l:text + redraw + + let l:isFirst = 1 + for [l:part, l:hlgroup] in l:elements + execute 'echohl' l:hlgroup + execute (l:isFirst ? 'echo' : 'echon') 'l:part' + let l:isFirst = 0 + endfor + echohl None +endfunction + +function! ingo#msg#VerboseMsg( text, ... ) +"****************************************************************************** +"* PURPOSE: +" Echo a message if 'verbose' is greater or equal 1 (or the optional +" a:verboselevel). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:text The message to be echoed in verbose mode. +"* RETURN VALUES: +" None. +"****************************************************************************** + if &verbose >= (a:0 ? a:1 : 1) + echomsg a:text + endif +endfunction + function! ingo#msg#WarningMsg( text ) let v:warningmsg = a:text call ingo#msg#HighlightMsg(v:warningmsg, 'WarningMsg') @@ -78,6 +166,14 @@ function! ingo#msg#CustomExceptionMsg( customPrefixPattern ) call ingo#msg#ErrorMsg(substitute(v:exception, printf('^\C\%%(%s\):\s*', a:customPrefixPattern), '', '')) endfunction +function! ingo#msg#MsgFromShellError( whatFailure, shellOutput ) + if empty(a:shellOutput) + let l:details = ['exit status ' . v:shell_error] + else + let l:details = split(a:shellOutput, "\n") + endif + return printf('Failed to %s: %s', a:whatFailure, join(l:details, ' ')) +endfunction function! ingo#msg#ShellError( whatFailure, shellOutput ) if empty(a:shellOutput) let l:details = ['exit status ' . v:shell_error] diff --git a/autoload/ingo/option.vim b/autoload/ingo/option.vim index b50a571..502283b 100644 --- a/autoload/ingo/option.vim +++ b/autoload/ingo/option.vim @@ -2,12 +2,13 @@ " " DEPENDENCIES: " -" Copyright: (C) 2014-2015 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.028.004 31-Oct-2016 Add ingo#option#Join(). " 1.024.003 06-Mar-2015 Add ingo#option#SplitAndUnescape(). " 1.021.002 12-Jun-2014 Add ingo#option#Contains() and " ingo#option#ContainsOneOf(). @@ -33,4 +34,16 @@ function! ingo#option#ContainsOneOf( optionValue, list ) return 0 endfunction +function! ingo#option#Join( val1, ... ) + let l:result = a:val1 + for l:val in a:000 + if empty(l:result) + let l:result = l:val + elseif ! empty(l:val) + let l:result .= ',' . l:val + endif + endfor + return l:result +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/pos.vim b/autoload/ingo/pos.vim index 427ac1a..53e2d91 100644 --- a/autoload/ingo/pos.vim +++ b/autoload/ingo/pos.vim @@ -2,12 +2,15 @@ " " DEPENDENCIES: " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.004 16-Dec-2016 Add ingo#pos#SameLineIs[OnOr]After/Before() +" variants. +" 1.025.003 29-Apr-2016 Add ingo#pos#IsInsideVisualSelection(). " 1.022.002 21-Jul-2014 Add ingo#pos#Before() and ingo#pos#After(). " 1.019.001 30-Apr-2014 file creation @@ -33,6 +36,16 @@ function! ingo#pos#IsInside( pos, start, end ) return ! ingo#pos#IsOutside(a:pos, a:start, a:end) endfunction +function! ingo#pos#IsInsideVisualSelection( pos, ... ) + let l:start = (a:0 == 2 ? a:1 : getpos("'<")[1:2]) + let l:end = (a:0 == 2 ? a:2 : getpos("'>")[1:2]) + if &selection ==# 'exclusive' + return ! (a:pos[0] < l:start[0] || a:pos[0] > l:end[0] || a:pos[0] == l:start[0] && a:pos[1] < l:start[1] || a:pos[0] == l:end[0] && a:pos[1] >= l:end[1]) + else + return ! (a:pos[0] < l:start[0] || a:pos[0] > l:end[0] || a:pos[0] == l:start[0] && a:pos[1] < l:start[1] || a:pos[0] == l:end[0] && a:pos[1] > l:end[1]) + endif +endfunction + function! ingo#pos#Before( pos ) let l:charBeforePosition = matchstr(getline(a:pos[0]), '.\%' . a:pos[1] . 'c') @@ -43,4 +56,20 @@ function! ingo#pos#After( pos ) return (empty(l:charAtPosition) ? [0, 0] : [a:pos[0], a:pos[1] + len(l:charAtPosition)]) endfunction + + +function! ingo#pos#SameLineIsOnOrAfter( posA, posB ) + return (a:posA[0] == a:posB[0] && a:posA[1] >= a:posB[1]) +endfunction +function! ingo#pos#SameLineIsAfter( posA, posB ) + return (a:posA[0] == a:posB[0] && a:posA[1] > a:posB[1]) +endfunction + +function! ingo#pos#SameLineIsOnOrBefore( posA, posB ) + return (a:posA[0] == a:posB[0] && a:posA[1] <= a:posB[1]) +endfunction +function! ingo#pos#SameLineIsBefore( posA, posB ) + return (a:posA[0] == a:posB[0] && a:posA[1] < a:posB[1]) +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/query.vim b/autoload/ingo/query.vim index 7535c47..4a9fa52 100644 --- a/autoload/ingo/query.vim +++ b/autoload/ingo/query.vim @@ -2,12 +2,14 @@ " " DEPENDENCIES: " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.027.004 27-Sep-2016 Expose ingo#query#StripAccellerator(). +" 1.025.003 27-Jan-2016 Refactoring: Factor out ingo#query#Question(). " 1.019.002 20-May-2014 confirm() automatically presets the first " character with an accelerator when no "&" " present; do that for s:EchoEmulatedConfirm(), @@ -18,7 +20,14 @@ let s:save_cpo = &cpo set cpo&vim -function! s:StripAccellerator( choice ) +function! ingo#query#Question( msg ) + echohl Question + echomsg a:msg + echohl None +endfunction + + +function! ingo#query#StripAccellerator( choice ) return substitute(a:choice, '&', '', 'g') endfunction function! s:EchoEmulatedConfirm( msg, choices, defaultIndex ) @@ -51,7 +60,7 @@ function! ingo#query#Confirm( msg, ... ) " necesary. let l:choices = (a:0 ? split(a:1, '\n', 1) : ['&Ok']) - let l:plainChoices = map(copy(l:choices), 's:StripAccellerator(v:val)') + let l:plainChoices = map(copy(l:choices), 'ingo#query#StripAccellerator(v:val)') " Emulate the console output of confirm(), so that it looks for a test " driver as if it were real. @@ -96,7 +105,7 @@ function! ingo#query#ConfirmAsText( msg, choices, ... ) " Choice text without the shortcut key '&'. Empty string if the dialog was " aborted. "****************************************************************************** - let l:plainChoices = map(copy(a:choices), 's:StripAccellerator(v:val)') + let l:plainChoices = map(copy(a:choices), 'ingo#query#StripAccellerator(v:val)') let l:confirmArgs = [a:msg, join(a:choices, "\n")] if a:0 @@ -118,7 +127,7 @@ function! ingo#query#ConfirmAsText( msg, choices, ... ) return (type(l:choice) == type(0) ? \ (l:choice == 0 ? \ '' : - \ s:StripAccellerator(get(a:choices, l:choice - 1, '')) + \ ingo#query#StripAccellerator(get(a:choices, l:choice - 1, '')) \ ) : \ l:choice \) diff --git a/autoload/ingo/query/fromlist.vim b/autoload/ingo/query/fromlist.vim index 999f0f3..608e3cd 100644 --- a/autoload/ingo/query/fromlist.vim +++ b/autoload/ingo/query/fromlist.vim @@ -1,15 +1,22 @@ " ingo/query/fromlist.vim: Functions for querying elements from a list. " " DEPENDENCIES: +" - ingo/query.vim autoload script " - ingo/query/confirm.vim autoload script " - ingo/query/get.vim autoload script " -" Copyright: (C) 2014-2015 Ingo Karkat +" Copyright: (C) 2014-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.027.005 27-Sep-2016 ENH: ingo#query#fromlist#Query(): Support +" headless (testing) mode via +" g:IngoLibrary_QueryChoices, like +" ingo#query#Confirm() already does. +" Expose ingo#query#fromlist#RenderList(). +" 1.025.004 27-Jan-2016 Refactoring: Factor out ingo#query#Question(). " 1.023.003 19-Jan-2015 Break listing of query choices into multiple " lines when the overall question doesn't fit in a " single line. @@ -19,7 +26,7 @@ let s:save_cpo = &cpo set cpo&vim -function! s:RenderList( list, defaultIndex, formatString ) +function! ingo#query#fromlist#RenderList( list, defaultIndex, formatString ) let l:result = [] for l:i in range(len(a:list)) call add(l:result, @@ -33,9 +40,17 @@ function! ingo#query#fromlist#Query( what, list, ... ) "****************************************************************************** "* PURPOSE: " Query for one entry from a:list; elements can be selected by accelerator key -" or the number of the element. +" or the number of the element. Supports "headless mode", i.e. bypassing the +" actual dialog so that no user intervention is necessary (in automated +" tests). +"* SEE ALSO: +" ingo#query#recall#Query() provides an alternative means to query one +" (longer) entry from a list. "* ASSUMPTIONS / PRECONDITIONS: -" None. +" The headless mode is activated by defining a List of choices (either +" numerical return values of confirm(), or the choice text without the +" shortcut key "&") in g:IngoLibrary_QueryChoices. Each invocation of this +" function removes the first element from that List and returns it. "* EFFECTS / POSTCONDITIONS: " None. "* INPUTS: @@ -49,20 +64,32 @@ function! ingo#query#fromlist#Query( what, list, ... ) let l:defaultIndex = (a:0 ? a:1 : -1) let l:confirmList = ingo#query#confirm#AutoAccelerators(copy(a:list), -1) let l:accelerators = map(copy(l:confirmList), 'matchstr(v:val, "&\\zs.")') - let l:list = s:RenderList(l:confirmList, l:defaultIndex, '%d:') + let l:list = ingo#query#fromlist#RenderList(l:confirmList, l:defaultIndex, '%d:') let l:renderedQuestion = printf('Select %s via [count] or (l)etter: %s ?', a:what, join(l:list, ', ')) if ingo#compat#strdisplaywidth(l:renderedQuestion) + 3 > &columns - echohl Question - echomsg printf('Select %s via [count] or (l)etter:', a:what) - echohl None - for l:listItem in s:RenderList(l:confirmList, l:defaultIndex, '%3d: ') + call ingo#query#Question(printf('Select %s via [count] or (l)etter:', a:what)) + for l:listItem in ingo#query#fromlist#RenderList(l:confirmList, l:defaultIndex, '%3d: ') echo l:listItem endfor else - echohl Question - echomsg l:renderedQuestion - echohl None + call ingo#query#Question(l:renderedQuestion) + endif + + if exists('g:IngoLibrary_QueryChoices') && len(g:IngoLibrary_QueryChoices) > 0 + " Headless mode: Bypass actual confirm so that no user intervention is + " necesary. + let l:plainChoices = map(copy(a:list), 'ingo#query#StripAccellerator(v:val)') + + " Return predefined choice. + let l:choice = remove(g:IngoLibrary_QueryChoices, 0) + return (type(l:choice) == type(0) ? + \ l:choice : + \ (l:choice == '' ? + \ 0 : + \ index(l:plainChoices, l:choice) + \ ) + \) endif let l:choice = ingo#query#get#Char() diff --git a/autoload/ingo/query/get.vim b/autoload/ingo/query/get.vim index dc9fb96..3b42547 100644 --- a/autoload/ingo/query/get.vim +++ b/autoload/ingo/query/get.vim @@ -2,12 +2,16 @@ " " DEPENDENCIES: " -" Copyright: (C) 2012-2014 Ingo Karkat +" Copyright: (C) 2012-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.007 17-Dec-2016 FIX: ingo#query#get#Char() does not beep when +" validExpr is given and invalid character pressed. +" Add ingo#query#get#ValidChar() variant that +" loops until a valid character has been pressed. " 1.017.006 04-Mar-2014 Make ingo#query#get#Char() only abort on " when that character is not in the validExpr (to " allow to explicitly query it). @@ -62,6 +66,14 @@ function! ingo#query#get#Number( maxNum, ... ) endwhile endfunction +function! s:GetChar() + " TODO: Handle digraphs via . + let l:char = getchar() + if type(l:char) == type(0) + let l:char = nr2char(l:char) + endif + return l:char +endfunction function! ingo#query#get#Char( ... ) "****************************************************************************** "* PURPOSE: @@ -73,7 +85,9 @@ function! ingo#query#get#Char( ... ) "* INPUTS: " a:options.isBeepOnInvalid Flag whether to beep on invalid pattern (but not " when aborting with ). Default on. -" a:options.validExpr Pattern for valid characters. +" a:options.validExpr Pattern for valid characters. Aborting with +" is always possible, but if you add \e, it +" will be returned as ^[. " a:options.invalidExpr Pattern for invalid characters. Takes precedence " over a:options.validExpr. "* RETURN VALUES: @@ -85,13 +99,8 @@ function! ingo#query#get#Char( ... ) let l:validExpr = get(l:options, 'validExpr', '') let l:invalidExpr = get(l:options, 'invalidExpr', '') - " TODO: Handle digraphs via . - let l:char = getchar() - if type(l:char) == type(0) - let l:char = nr2char(l:char) - endif - - if l:char ==# "\" && empty(l:validExpr) || l:char!~ l:validExpr + let l:char = s:GetChar() + if l:char ==# "\" && (empty(l:validExpr) || l:char !~ l:validExpr) return '' elseif (! empty(l:validExpr) && l:char !~ l:validExpr) || \ (! empty(l:invalidExpr) && l:char =~ l:invalidExpr) @@ -103,6 +112,48 @@ function! ingo#query#get#Char( ... ) return l:char endfunction +function! ingo#query#get#ValidChar( ... ) +"****************************************************************************** +"* PURPOSE: +" Query a character from the user until a valid one has been pressed (or +" aborted with ). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:options.isBeepOnInvalid Flag whether to beep on invalid pattern (but not +" when aborting with ). Default on. +" a:options.validExpr Pattern for valid characters. Aborting with +" is always possible, but if you add \e, it +" will be returned as ^[. +" a:options.invalidExpr Pattern for invalid characters. Takes precedence +" over a:options.validExpr. +"* RETURN VALUES: +" Either the valid character, or an empty string when aborted. +"****************************************************************************** + let l:options = (a:0 ? a:1 : {}) + let l:isBeepOnInvalid = get(l:options, 'isBeepOnInvalid', 1) + let l:validExpr = get(l:options, 'validExpr', '') + let l:invalidExpr = get(l:options, 'invalidExpr', '') + + while 1 + let l:char = s:GetChar() + + if l:char ==# "\" && (empty(l:validExpr) || l:char !~ l:validExpr) + return '' + elseif (! empty(l:validExpr) && l:char !~ l:validExpr) || + \ (! empty(l:invalidExpr) && l:char =~ l:invalidExpr) + if l:isBeepOnInvalid + execute "normal! \\\" | " Beep. + endif + else + break + endif + endwhile + + return l:char +endfunction function! ingo#query#get#Register( errorRegister, ... ) "****************************************************************************** diff --git a/autoload/ingo/query/recall.vim b/autoload/ingo/query/recall.vim new file mode 100644 index 0000000..1cc6f72 --- /dev/null +++ b/autoload/ingo/query/recall.vim @@ -0,0 +1,57 @@ +" ingo/query/recall.vim: Functions to recall a value from a list. +" +" DEPENDENCIES: +" - ingo/list.vim autoload script +" - ingo/msg.vim autoload script +" - ingo/query/get.vim autoload script +" +" Copyright: (C) 2017 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 10-Jan-2017 file creation + +function! ingo#query#recall#Query( title, list, isReverse ) +"****************************************************************************** +"* PURPOSE: +" Query one entry from a:list by number. +"* SEE ALSO: +" ingo#query#fromlist#Query() provides an alternative means to query one entry +" from a (longer) list. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:title Text describing the list elements; will be printed as a table +" header. +" a:list List of elements. Each element can also be a List of Strings (which +" are simply concatenated) or a List of [part, hlgroup] Pairs +" highlighted through ingo#msg#ColoredMsg(). +" a:isReverse Flag whether the first element from a:list comes last in the +" table, which makes it faster to visually parse a long list of +" MRU elements. +"* RETURN VALUES: +" List index, or -2 if a:list is empty, or -1 if an invalid number was +" chosen, or the query aborted via a non-numeric choice. +"****************************************************************************** + let l:len = len(a:list) + if l:len == 0 + return -2 + endif + + echohl Title + echo ' # ' . a:title + echohl None + + for l:i in (a:isReverse ? range(l:len - 1, 0, -1) : range(l:len)) + call call('ingo#msg#ColoredMsg', [printf('%7d ', l:i + 1)] + ingo#list#Make(a:list[l:i])) + endfor + echo 'Type number ( cancels): ' + let l:choice = ingo#query#get#Number(l:len) + return (l:choice < 1 || l:choice > l:len ? -1 : l:choice - 1) +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/query/substitute.vim b/autoload/ingo/query/substitute.vim index 54f9bdc..2810e4d 100644 --- a/autoload/ingo/query/substitute.vim +++ b/autoload/ingo/query/substitute.vim @@ -1,19 +1,19 @@ " ingo/query/substitute.vim: Functions for confirming a command like :substitute//c. " " DEPENDENCIES: +" - ingo/query.vim autoload script " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.002 27-Jan-2016 Refactoring: Factor out ingo#query#Question(). " 1.017.001 04-Mar-2014 file creation function! s:Question( msg ) - echohl Question - echo a:msg . ' (y/n/a/q/l/^E/^Y)?' - echohl None + call ingo#query#Question(a:msg . ' (y/n/a/q/l/^E/^Y)?') endfunction function! ingo#query#substitute#Get( msg ) "****************************************************************************** diff --git a/autoload/ingo/range/borders.vim b/autoload/ingo/range/borders.vim new file mode 100644 index 0000000..6a1e0b9 --- /dev/null +++ b/autoload/ingo/range/borders.vim @@ -0,0 +1,43 @@ +" ingo/range/borders.vim: Functions for determining ranges at the borders of the buffer. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 001 13-Jul-2016 file creation + +function! ingo#range#borders#StartAndEndRange( startOffset, endOffset ) +"****************************************************************************** +"* PURPOSE: +" Determine non-overlapping range(s) for a:startOffset lines from the start of +" the current buffer, and a:endOffset lines from the end of the current +" buffer. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:startOffset Number of lines to get from the start. +" a:endOffset Number of lines to get from the end. +"* RETURN VALUES: +" List of ranges, in the form ['1,3', '8,$'] +"****************************************************************************** + let l:ranges = [] + let l:lastStartLnum = min([line('$'), a:startOffset]) + if a:startOffset > 0 + call add(l:ranges, '1,' . l:lastStartLnum) + endif + + let l:firstEndLnum = max([1, line('$') - a:endOffset + 1]) + let l:firstEndLnum = max([l:lastStartLnum + 1, l:firstEndLnum]) + if l:firstEndLnum <= line('$') + call add(l:ranges, l:firstEndLnum . ',$') + endif + return l:ranges +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/range/invert.vim b/autoload/ingo/range/invert.vim new file mode 100644 index 0000000..18bcfc3 --- /dev/null +++ b/autoload/ingo/range/invert.vim @@ -0,0 +1,50 @@ +" ingo/range/invert.vim: Functions for inverting ranges. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 21-Dec-2016 file creation + +function! ingo#range#invert#Invert( startLnum, endLnum, ranges ) +"****************************************************************************** +"* PURPOSE: +" Invert the ranges in a:ranges. Lines within a:startLnum, a:endLnum that were +" contained in the ranges will be out, and all other lines will be in. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:startLnum First line number to be considered. +" a:endLnum Last line number to be considered. +" a:ranges List of [start, end] pairs in ascending, non-overlapping order. +" Invoke ingo#range#merge#Merge() first if necessary. +"* RETURN VALUES: +" List of [start, end] pairs in ascending order. +"****************************************************************************** + let l:result = [] + + let l:lastIncludedLnum = a:startLnum - 1 + for [l:fromLnum, l:toLnum] in a:ranges + call s:Add(l:result, a:startLnum, a:endLnum, l:lastIncludedLnum + 1, l:fromLnum - 1) + let l:lastIncludedLnum = l:toLnum + endfor + call s:Add(l:result, a:startLnum, a:endLnum, l:lastIncludedLnum + 1, a:endLnum) + return l:result +endfunction +function! s:Add( target, startLnum, endLnum, fromLnum, toLnum ) + let l:fromLnum = max([a:startLnum, a:fromLnum]) + let l:toLnum = min([a:endLnum, a:toLnum]) + + if l:fromLnum > l:toLnum + return + endif + call add(a:target, [l:fromLnum, l:toLnum]) +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/range/lines.vim b/autoload/ingo/range/lines.vim index 54546a9..3d4d920 100644 --- a/autoload/ingo/range/lines.vim +++ b/autoload/ingo/range/lines.vim @@ -1,14 +1,30 @@ " ingo/range/Lines.vim: Functions for retrieving line numbers of ranges. " " DEPENDENCIES: +" - ingo/cmdsargs/pattern.vim autoload script " - ingo/range.vim autoload script " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.005 23-Dec-2016 ingo#range#lines#Get(): If the range is a +" backwards-looking ?{pattern}?, we need to +" attempt the match on any line with :global/^/... +" Else, the border behavior is inconsistent: +" ranges that extend the passed range at the +" bottom are (partially) included, but ranges that +" extend at the front would not be. +" 1.029.004 07-Dec-2016 ingo#range#lines#Get(): A single +" (a:isGetAllRanges = 0) /.../ range already +" clobbers the last search pattern. Save and +" restore if necessary, and base +" didClobberSearchHistory on that check. +" ingo#range#lines#Get(): Drop the ^ anchor for +" the range check to also detect /.../ as the +" end of the range. " 1.023.003 26-Dec-2014 ENH: Add a:isGetAllRanges optional argument to " ingo#range#lines#Get(). " 1.022.002 23-Sep-2014 ingo#range#lines#Get() needs to consider and @@ -70,23 +86,34 @@ function! ingo#range#lines#Get( startLnum, endLnum, range, ... ) let l:recordedLines = {} let l:startLines = [] let l:endLines = [] + let l:save_search = @/ + let l:didClobberSearchHistory = 0 - if l:isGetAllRanges && a:range =~# '^[/?]' + if l:isGetAllRanges && a:range =~# '[/?]' " For patterns, we need :global to find _all_ (not just the first) " matching ranges. For that, folds must be open / disabled. And because " of that, the actual ranges must be determined first. let l:save_foldenable = &l:foldenable setlocal nofoldenable + + let l:searchRange = a:range + if ingo#cmdargs#pattern#RawParse(a:range, [''], '\s*[,;]\s*\S.*')[0] ==# '?' + " If this is a simple /{pattern}/, we can just match that with + " :global. But for actual ranges, these should extend both upwards + " (?foo?,/bar/) as well as downwards (/foo/,/bar/). To handle the + " former, we must make :global attempt a match at any line. + let l:searchRange = '/^/' . a:range + endif + try execute printf('silent! %d,%dglobal %s call RecordLines(l:recordedLines, l:startLines, l:endLines, %d, %d)', \ l:startLnum, l:endLnum, - \ a:range, + \ l:searchRange, \ l:startLnum, l:endLnum \) finally let &l:foldenable = l:save_foldenable endtry - let l:didClobberSearchHistory = 1 else " For line number, marks, etc., we can just record them (limited to " those that fall into the command's range). @@ -94,7 +121,11 @@ function! ingo#range#lines#Get( startLnum, endLnum, range, ... ) \ a:range, \ l:startLnum, l:endLnum \) - let l:didClobberSearchHistory = 0 + endif + + if @/ !=# l:save_search + let @/ = l:save_search + let l:didClobberSearchHistory = 1 endif return [l:recordedLines, l:startLines, l:endLines, l:didClobberSearchHistory] diff --git a/autoload/ingo/range/merge.vim b/autoload/ingo/range/merge.vim index 18236f3..cd8bc4b 100644 --- a/autoload/ingo/range/merge.vim +++ b/autoload/ingo/range/merge.vim @@ -1,14 +1,19 @@ " ingo/range/merge.vim: Functions for merging ranges. " " DEPENDENCIES: +" - ingo/collections.vim autoload script " -" Copyright: (C) 2015 Ingo Karkat +" Copyright: (C) 2015-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.002 23-Dec-2016 Extract ingo#range#merge#FromLnums() from +" ingo#range#merge#Merge(). " 1.023.001 22-Jan-2015 file creation +let s:save_cpo = &cpo +set cpo&vim function! ingo#range#merge#Merge( ranges ) "****************************************************************************** @@ -30,7 +35,28 @@ function! ingo#range#merge#Merge( ranges ) endfor endfor - let l:lnums = sort(keys(l:dict), 'ingo#collections#numsort') + return ingo#range#merge#FromLnums(l:dict) +endfunction +function! ingo#range#merge#FromLnums( lnumsCollection ) +"****************************************************************************** +"* PURPOSE: +" Turn the collection of line numbers into a List of ranges. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:lnumsCollection Either Dictionary where each key represens a line +" number, or List (not necessarily unique or sorted) of +" line numbers. +"* RETURN VALUES: +" List of joined, non-overlapping [start, end] pairs in ascending order. +"****************************************************************************** + let l:lnums = (type(a:lnumsCollection) == type({}) ? + \ sort(keys(a:lnumsCollection), 'ingo#collections#numsort') : + \ ingo#collections#UniqueSorted(sort(a:lnumsCollection, 'ingo#collections#numsort')) + \) + let l:result = [] while ! empty(l:lnums) let l:start = str2nr(remove(l:lnums, 0)) @@ -46,4 +72,6 @@ function! ingo#range#merge#Merge( ranges ) return l:result endfunction +let &cpo = s:save_cpo +unlet s:save_cpo " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/record.vim b/autoload/ingo/record.vim index 37cf723..ea8bee0 100644 --- a/autoload/ingo/record.vim +++ b/autoload/ingo/record.vim @@ -2,12 +2,14 @@ " " DEPENDENCIES: " -" Copyright: (C) 2014 Ingo Karkat +" Copyright: (C) 2014-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.002 23-Mar-2016 Add optional a:characterOffset to +" ingo#record#PositionAndLocation(). " 1.020.001 30-May-2014 file creation function! ingo#record#Position( isRecordChange ) @@ -17,14 +19,39 @@ function! ingo#record#Position( isRecordChange ) " window and tab page. return getpos('.') + [bufnr('')] + (a:isRecordChange ? [b:changedtick] : []) endfunction -function! ingo#record#PositionAndLocation( isRecordChange ) - " The position record consists of the current cursor position, the buffer, - " window and tab page number and optionally the buffer's current change - " state. - " As soon as you make an edit, move to another buffer or even the same - " buffer in another tab page or window (or as a minor side effect just close - " a window above the current), the position changes. - return getpos('.') + [bufnr(''), winnr(), tabpagenr()] + (a:isRecordChange ? [b:changedtick] : []) +function! ingo#record#PositionAndLocation( isRecordChange, ... ) +"****************************************************************************** +"* PURPOSE: +" The position record consists of the current cursor position, the buffer, +" window and tab page number and optionally the buffer's current change state. +" As soon as you make an edit, move to another buffer or even the same buffer +" in another tab page or window (or as a minor side effect just close a window +" above the current), the position changes. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:isRecordChange Flag whether b:changedtick should be part of the record. +" a:characterOffset Offset in characters from the current cursor position. +" Can be -1, 0, or 1. +"* RETURN VALUES: +" List of recorded values (to be compared with later results from this +" function). +"****************************************************************************** + let l:pos = getpos('.') + + if a:0 + if a:1 == 1 + let l:pos[2] += len(ingo#text#GetChar(l:pos[1:2])) + elseif a:1 == -1 + let l:pos[2] -= len(ingo#text#GetCharBefore(l:pos[1:2])) + elseif a:1 != 0 + throw 'ASSERT: Offsets other than -1, 0, 1 not supported yet' + endif + endif + + return l:pos + [bufnr(''), winnr(), tabpagenr()] + (a:isRecordChange ? [b:changedtick] : []) endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/regexp.vim b/autoload/ingo/regexp.vim index 1efde9a..850b517 100644 --- a/autoload/ingo/regexp.vim +++ b/autoload/ingo/regexp.vim @@ -2,12 +2,15 @@ " " DEPENDENCIES: " -" Copyright: (C) 2010-2015 Ingo Karkat +" Copyright: (C) 2010-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.014 28-Apr-2016 Add ingo#regexp#MakeStartWordSearch() +" ingo#regexp#MakeEndWordSearch() variants of +" ingo#regexp#MakeWholeWordSearch(). " 1.023.013 25-Nov-2014 Expose ingo#regexp#MakeWholeWordSearch() (with " the second a:pattern argument made optional). " 1.020.012 29-May-2014 CHG: At ingo#regexp#FromLiteralText(), add the @@ -81,16 +84,50 @@ function! ingo#regexp#EscapeLiteralText( text, additionalEscapeCharacters ) endfunction function! ingo#regexp#MakeWholeWordSearch( text, ... ) - " The star command only creates a \ search pattern if the - " actually only consists of keyword characters. Since - " ingo#regexp#FromLiteralText() could handle a superset (e.g. also - " "foo...bar"), just ensure that the keyword boundaries can be enforced at - " either side, to avoid enclosing a non-keyword side and making a match - " impossible with it (e.g. "\<..bar\>"). +"****************************************************************************** +"* PURPOSE: +" Generate a pattern that searches only for whole words of a:text, but only if +" a:text actually starts / ends with keyword characters (so that non-word +" a:text still matches (anywhere)). +" The star command only creates a \ search pattern if the +" actually only consists of keyword characters. Since +" ingo#regexp#FromLiteralText() could handle a superset (e.g. also +" "foo...bar"), just ensure that the keyword boundaries can be enforced at +" either side, to avoid enclosing a non-keyword side and making a match +" impossible with it (e.g. "\<..bar\>"). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:text Text / pattern to be searched for. Note that this isn't escaped in any form; +" you probably want to escape backslashes beforehand and use \V "very +" nomagic" on the result. +" a:pattern If passed, this is adapted according to what a:text is about. +" Useful if the pattern has already been so warped (e.g. by +" enclosing in /\(...\|...\)/) that word boundary detection on the +" original text wouldn't work. +"* RETURN VALUES: +" a:text / a:pattern, with additional \< / \> atoms if applicable. +"****************************************************************************** +let l:pattern = (a:0 ? a:1 : a:text) + if a:text =~# '^\k' + let l:pattern = '\<' . l:pattern + endif + if a:text =~# '\k$' + let l:pattern .= '\>' + endif + return l:pattern +endfunction +function! ingo#regexp#MakeStartWordSearch( text, ... ) let l:pattern = (a:0 ? a:1 : a:text) if a:text =~# '^\k' let l:pattern = '\<' . l:pattern endif + return l:pattern +endfunction +function! ingo#regexp#MakeEndWordSearch( text, ... ) + let l:pattern = (a:0 ? a:1 : a:text) if a:text =~# '\k$' let l:pattern .= '\>' endif diff --git a/autoload/ingo/regexp/build.vim b/autoload/ingo/regexp/build.vim new file mode 100644 index 0000000..33d3b50 --- /dev/null +++ b/autoload/ingo/regexp/build.vim @@ -0,0 +1,47 @@ +" ingo/regexp/build.vim: Functions to build regular expressions. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 17-Dec-2016 file creation + +function! ingo#regexp#build#Prepend( target, fragment ) +"****************************************************************************** +"* PURPOSE: +" Add a:fragment at the beginning of a:target, considering the anchor ^. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:target Regular expression to manipulate. +" a:fragment Regular expression fragment to insert. +"* RETURN VALUES: +" New regexp. +"****************************************************************************** + return substitute(a:target, '^\%(\\%\?(\)*^\?', '&' . escape(a:fragment, '\&'), '') +endfunction + +function! ingo#regexp#build#Append( target, fragment ) +"****************************************************************************** +"* PURPOSE: +" Add a:fragment at the end of a:target, considering the anchor $. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:target Regular expression to manipulate. +" a:fragment Regular expression fragment to insert. +"* RETURN VALUES: +" New regexp. +"****************************************************************************** + return substitute(a:target, '$\?\%(\\)\)*$', escape(a:fragment, '\&') . '&', '') +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/regexp/collection.vim b/autoload/ingo/regexp/collection.vim new file mode 100644 index 0000000..8f8c4bf --- /dev/null +++ b/autoload/ingo/regexp/collection.vim @@ -0,0 +1,48 @@ +" ingo/regexp/collection.vim: Functions around handling collections in regular expressions. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016-2017 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.002 23-Jan-2017 Add ingo#regexp#collection#Expr(). +" 1.027.001 30-Sep-2016 file creation + +function! ingo#regexp#collection#Expr() +"****************************************************************************** +"* PURPOSE: +" Returns a regular expression that matches any collection atom. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" None. +"* RETURN VALUES: +" Regular expression. +"****************************************************************************** + return '\[\%(\]$\)\@!\]\?\%(\[:\a\+:\]\|\[=.\{-}=\]\|\[\..\.\]\|[^\]]\)*\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@ " " REVISION DATE REMARKS +" 1.029.004 24-Jan-2017 Test failure in +" PatternsOnText/t7580-SubstituteMultiple-magic.vim +" alerted me to the fact that \V[1] needs to be +" escaped as \[1]. Also process the collections +" through new +" ingo#regexp#magic#ConvertMagicnessOfCollection(). +" 1.029.003 23-Jan-2017 BUG: ingo#regexp#magic#Normalize() also +" processes the contents of collections [...]; +" especially the escaping of "]" wreaks havoc on +" the pattern. Rename s:ConvertMagicness() into +" ingo#regexp#magic#ConvertMagicnessOfElement() +" and introduce intermediate +" s:ConvertMagicnessOfFragment() that first +" separates collections from other elements and +" only invokes the former on those other elements. " 1.009.002 14-Jun-2013 Minor: Make substitute() robust against " 'ignorecase'. " 1.006.001 24-May-2013 file creation from ingosearch.vim. +let s:save_cpo = &cpo +set cpo&vim function! ingo#regexp#magic#GetNormalizeMagicnessAtom( pattern ) "****************************************************************************** @@ -45,9 +64,26 @@ let s:specialSearchCharacterExpressions = { \ 'M': '[\\^$]', \ 'V': '\\', \} -function! s:ConvertMagicness( pattern, sourceSpecialCharacterExpr, targetSpecialCharacterExpr ) +function! s:ConvertMagicnessOfFragment( fragment, sourceSpecialCharacterExpr, targetSpecialCharacterExpr ) + let l:elements = ingo#collections#fromsplit#MapItemsAndSeparators(a:fragment, '\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@ " " REVISION DATE REMARKS +" 1.029.004 13-Jan-2017 Add ingo#register#GetAsList(). " 1.015.003 18-Nov-2013 FIX: Actually return the result of a Funcref " passed to " ingo#register#KeepRegisterExecuteOrFunc(). @@ -60,4 +61,26 @@ function! ingo#register#KeepRegisterExecuteOrFunc( Action, ... ) endtry endfunction +function! ingo#register#GetAsList( register ) +"****************************************************************************** +"* PURPOSE: +" Get the contents of a:register as a List of lines. For a linewise register, +" there is no trailing empty element (so the returned List can be directly +" passed to append(), and it will insert just like :put {reg}. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:register Name of register. +"* RETURN VALUES: +" List of lines. +"****************************************************************************** + let l:lines = split(getreg(a:register), '\n', 1) + if len(l:lines) > 1 && empty(l:lines[-1]) + call remove(l:lines, -1) + endif + return l:lines +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/selection.vim b/autoload/ingo/selection.vim index 5f041ae..836bcea 100644 --- a/autoload/ingo/selection.vim +++ b/autoload/ingo/selection.vim @@ -21,6 +21,7 @@ function! ingo#selection#Get() " Visual selection is / has been made. "* EFFECTS / POSTCONDITIONS: " Moves the cursor to the beginning of the selected text. +" Clobbers v:count. "* INPUTS: " None. "* RETURN VALUES: diff --git a/autoload/ingo/str.vim b/autoload/ingo/str.vim index 2736a88..8147550 100644 --- a/autoload/ingo/str.vim +++ b/autoload/ingo/str.vim @@ -1,14 +1,17 @@ " ingo/str.vim: String functions. " " DEPENDENCIES: +" - ingo/regexp/collection.vim autoload script " - ingo/regexp/virtcols.vim autoload script " -" Copyright: (C) 2013-2015 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.027.007 30-Sep-2016 Add ingo#str#trd(). +" 1.025.006 30-Apr-2015 Add ingo#str#Contains(). " 1.024.005 01-Apr-2015 Add ingo#str#GetVirtCols(). " 1.019.004 21-May-2014 Allow optional a:ignorecase argument for " ingo#str#StartsWith() and ingo#str#EndsWith(). @@ -66,6 +69,14 @@ function! ingo#str#Equals( string1, string2, ...) return a:string1 ==# a:string2 endif endfunction +function! ingo#str#Contains( string, part, ...) + let l:ignorecase = (a:0 && a:1) + if l:ignorecase + return (stridx(a:string, a:part) != -1 || a:string =~? '\V' . escape(a:part, '\')) + else + return (stridx(a:string, a:part) != -1) + endif +endfunction function! ingo#str#GetVirtCols( string, virtcol, width, isAllowSmaller ) "****************************************************************************** @@ -92,4 +103,22 @@ function! ingo#str#GetVirtCols( string, virtcol, width, isAllowSmaller ) return matchstr(a:string, ingo#regexp#virtcols#ExtractCells(a:virtcol, a:width, a:isAllowSmaller)) endfunction +function! ingo#str#trd( src, fromstr ) +"****************************************************************************** +"* PURPOSE: +" Delete characters in a:fromstr in a copy of a:src. Like tr -d, but the +" built-in tr() doesn't support this. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:src Source string. +" a:fromstr Characters that will each be removed from a:src. +"* RETURN VALUES: +" Copy of a:src that has all instances of the characters in a:fromstr removed. +"****************************************************************************** + return substitute(a:src, '\C' . ingo#regexp#collection#LiteralToRegexp(a:fromstr), '', 'g') +endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/str/find.vim b/autoload/ingo/str/find.vim new file mode 100644 index 0000000..8b291bb --- /dev/null +++ b/autoload/ingo/str/find.vim @@ -0,0 +1,36 @@ +" ingo/str/find.vim: Functions to find stuff in a string. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.001 14-Dec-2016 file creation + +function! ingo#str#find#NotContaining( string, characterSet ) +"****************************************************************************** +"* PURPOSE: +" Find the first character of a:characterSet not contained in a:string. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:string Source string to be inspected. +" a:characterSet String or List of candidate characters. +"* RETURN VALUES: +" First character in a:characterSet that is not contained in a:string, or +" empty string if all characters are contained. +"****************************************************************************** + for l:candidate in (type(a:characterSet) == type([]) ? a:characterSet : split(a:characterSet, '\zs')) + if stridx(a:string, l:candidate) == -1 + return l:candidate + endif + endfor + return '' +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/str/fromrange.vim b/autoload/ingo/str/fromrange.vim new file mode 100644 index 0000000..66b457b --- /dev/null +++ b/autoload/ingo/str/fromrange.vim @@ -0,0 +1,123 @@ +" ingo/str/fromrange.vim: Functions to create strings by transforming codepoint ranges. +" +" DEPENDENCIES: +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.029.002 28-Dec-2016 Split off +" ingo#str#fromrange#GetTranslationStrings() from +" ingo#str#fromrange#Tr(). +" 1.029.001 14-Dec-2016 file creation from subs/Homoglyphs.vim + +function! ingo#str#fromrange#GetAsList( ... ) +"****************************************************************************** +"* PURPOSE: +" Get a List of characters with codepoints in the passed range. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr [, a:max [, a:stride]], as with |range()|. +"* RETURN VALUES: +" List of characters. +"****************************************************************************** + return map(call('range', a:000), 'nr2char(v:val)') +endfunction +function! ingo#str#fromrange#Get( ... ) +"****************************************************************************** +"* PURPOSE: +" Get a string of characters with codepoints in the passed range. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr [, a:max [, a:stride]], as with |range()|. +"* RETURN VALUES: +" String of characters. +"****************************************************************************** + return join(map(call('range', a:000), 'nr2char(v:val)'), '') +endfunction + + +function! s:RangeToString( start, end ) + return join( + \ map( + \ range(a:start, a:end), + \ 'nr2char(v:val)' + \ ), + \ '' + \) +endfunction +function! ingo#str#fromrange#GetTranslationStrings( mirrorMode, ranges ) +"****************************************************************************** +"* PURPOSE: +" Generate source and destination character ranges from a:ranges. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:mirrorMode 0: Do not mirror +" 1: Mirror a:range so that translation also works in the +" other direction. +" 2: Only mirror, i.e. only translate back. +" a:ranges List of ranges; one of (also mixed) [source, destination] or +" [start, end, transformStart] codepoints. +"* RETURN VALUES: +" [sourceRangeString, destinationRangeString] +"****************************************************************************** + let l:sources = '' + let l:destinations = '' + + for l:range in a:ranges + if len(l:range) == 3 + let [l:start, l:end, l:transformStart] = l:range + let s = s:RangeToString(l:start, l:end) + let d = s:RangeToString(l:transformStart, l:transformStart + l:end - l:start) + elseif len(l:range) == 2 + let [s, d] = [nr2char(l:range[0]), nr2char(l:range[1])] + else + throw 'ASSERT: Must pass either [start, end, transformStart] or [source, destination].' + endif + + if a:mirrorMode != 2 + let l:sources .= s + let l:destinations .= d + endif + if a:mirrorMode > 0 + let l:sources .= d + let l:destinations .= s + endif + endfor + + return [l:sources, l:destinations] +endfunction +function! ingo#str#fromrange#Tr( text, mirrorMode, ranges ) +"****************************************************************************** +"* PURPOSE: +" Translate the character ranges in a:ranges in a:text. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:text Text to be modified. +" a:mirrorMode 0: Do not mirror +" 1: Mirror a:range so that translation also works in the +" other direction. +" 2: Only mirror, i.e. only translate back. +" a:ranges List of ranges; one of (also mixed) [source, destination] or +" [start, end, transformStart] codepoints. +"* RETURN VALUES: +" Modified a:text. +"****************************************************************************** + return call('tr', [a:text] + ingo#str#fromrange#GetTranslationStrings(a:mirrorMode, a:ranges)) +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/strdisplaywidth.vim b/autoload/ingo/strdisplaywidth.vim index c458b85..f0a3274 100644 --- a/autoload/ingo/strdisplaywidth.vim +++ b/autoload/ingo/strdisplaywidth.vim @@ -3,12 +3,18 @@ " DEPENDENCIES: " - ingo/str.vim autoload script " -" Copyright: (C) 2008-2014 Ingo Karkat +" Copyright: (C) 2008-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.026.005 11-Aug-2016 ENH: ingo#strdisplaywidth#TruncateTo() has a +" configurable ellipsis string +" g:IngoLibrary_TruncateEllipsis, now defaulting +" to a single-char UTF-8 variant if we're in such +" encoding. It also handles pathologically small +" lengths that only show / cut into the ellipsis. " 1.023.004 29-Dec-2014 Add ingo#strdisplaywidth#TruncateTo(). " 1.019.003 17-Apr-2014 Add ingo#strdisplaywidth#GetMinMax(). " 1.011.002 26-Jul-2013 FIX: Off-by-one in @@ -17,6 +23,8 @@ " Factor out ingo#str#Reverse(). " 1.008.001 07-Jun-2013 file creation from EchoWithoutScrolling.vim. +scriptencoding utf-8 + function! ingo#strdisplaywidth#HasMoreThan( expr, virtCol ) return (match(a:expr, '^.*\%>' . (a:virtCol + 1) . 'v') != -1) endfunction @@ -33,6 +41,10 @@ function! ingo#strdisplaywidth#strleft( expr, virtCol ) " character), and include that before-column in the match, too. return matchstr(a:expr, '^.*\%<' . (a:virtCol + 1) . 'v.') endfunction + +if ! exists('g:IngoLibrary_TruncateEllipsis') + let g:IngoLibrary_TruncateEllipsis = (&encoding ==# 'utf-8' ? '…' : '...') +endif function! ingo#strdisplaywidth#TruncateTo( text, virtCol, ... ) "****************************************************************************** "* PURPOSE: @@ -43,7 +55,7 @@ function! ingo#strdisplaywidth#TruncateTo( text, virtCol, ... ) " the middle of a:text, not at the end, but it is meant for :echoing, as " it accounts for buffer-local tabstop values. "* ASSUMPTIONS / PRECONDITIONS: -" None. +" The default ellipsis can be configured by g:IngoLibrary_TruncateEllipsis. "* EFFECTS / POSTCONDITIONS: " None. "* INPUTS: @@ -51,12 +63,22 @@ function! ingo#strdisplaywidth#TruncateTo( text, virtCol, ... ) " a:virtCol Maximum virtual columns for a:text. " a:truncationIndicator Optional text to be appended when truncation " appears. a:text is further reduced to account for -" its width. Default is "...". +" its width. Default is "..." or the single-char UTF-8 +" variant if the encoding also is UTF-8. "* RETURN VALUES: " Truncated a:text. "****************************************************************************** - let l:truncationIndicator = (a:0 ? a:1 : '...') + let l:truncationIndicator = (a:0 ? a:1 : g:IngoLibrary_TruncateEllipsis) if ingo#strdisplaywidth#HasMoreThan(a:text, a:virtCol) + let l:ellipsisLength = ingo#compat#strchars(g:IngoLibrary_TruncateEllipsis) + + " Handle pathological cases. + if a:virtCol == l:ellipsisLength + return g:IngoLibrary_TruncateEllipsis + elseif a:virtCol < l:ellipsisLength + return ingo#compat#strcharpart(g:IngoLibrary_TruncateEllipsis, 0, a:virtCol) + endif + let l:truncatedText = ingo#strdisplaywidth#strleft(a:text, max([0, a:virtCol - ingo#compat#strdisplaywidth(l:truncationIndicator)])) return l:truncatedText . l:truncationIndicator else diff --git a/autoload/ingo/strdisplaywidth/pad.vim b/autoload/ingo/strdisplaywidth/pad.vim index 30a3118..3776993 100644 --- a/autoload/ingo/strdisplaywidth/pad.vim +++ b/autoload/ingo/strdisplaywidth/pad.vim @@ -4,12 +4,13 @@ " - ingo/compat.vim autoload script " - ingo/tabstop.vim autoload script " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.026.002 11-Aug-2016 Add ingo#strdisplaywidth#pad#Middle(). " 1.009.001 20-Jun-2013 file creation function! ingo#strdisplaywidth#pad#Width( text, width, ... ) @@ -25,5 +26,17 @@ endfunction function! ingo#strdisplaywidth#pad#Right( text, width, ... ) return a:text . repeat(' ', call('ingo#strdisplaywidth#pad#Width', [a:text, a:width] + a:000)) endfunction +function! ingo#strdisplaywidth#pad#Middle( text, width, ... ) + let l:renderedText = call('ingo#tabstops#Render', [a:text] + a:000) + let l:existingWidth = call('ingo#compat#strdisplaywidth', [l:renderedText] + a:000) + let l:pad = a:width - l:existingWidth + if l:pad <= 0 + return l:renderedText + endif + + let l:leftPad = l:pad / 2 + let l:rightPad = l:pad - l:leftPad + return repeat(' ', l:leftPad) . l:renderedText . repeat(' ', l:rightPad) +endfunction " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/subst.vim b/autoload/ingo/subst.vim index df6f0e9..ffe9b0e 100644 --- a/autoload/ingo/subst.vim +++ b/autoload/ingo/subst.vim @@ -2,13 +2,18 @@ " " DEPENDENCIES: " -" Copyright: (C) 2013 Ingo Karkat +" Copyright: (C) 2013-2017 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.002 23-Jan-2017 Add ingo#subst#FirstSubstitution(), +" ingo#subst#FirstPattern(), +" ingo#subst#FirstParameter(). " 1.009.001 14-Jun-2013 file creation +let s:save_cpo = &cpo +set cpo&vim function! ingo#subst#gsub( expr, pat, sub ) return substitute(a:expr, '\C' . a:pat, a:sub, 'g') @@ -37,4 +42,94 @@ function! ingo#subst#MultiGsub( expr, substitutions ) return l:expr endfunction +function! ingo#subst#FirstSubstitution( expr, flags, ... ) +"****************************************************************************** +"* PURPOSE: +" Perform a substitution with the first matching [a:pattern, a:replacement] +" substitution. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be transformed. +" [a:pattern0, a:replacement0], ... List of [pattern, substitution] tuples. +"* RETURN VALUES: +" [patternIndex, replacement]; if no supplied pattern matched, returns +" [-1, a:expr]. +"****************************************************************************** + for l:patternIndex in range(len(a:000)) + let [l:pattern, l:replacement] = a:000[l:patternIndex] + if a:expr =~ l:pattern + return [l:patternIndex, substitute(a:expr, l:pattern, l:replacement, a:flags)] + endif + endfor + return [-1, a:expr] +endfunction + +function! ingo#subst#FirstPattern( expr, replacement, flags, ... ) +"****************************************************************************** +"* PURPOSE: +" Perform a substitution with the first matching a:pattern0, a:pattern1, ... +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be transformed. +" a:replacement Replacement (applied regardless of the chosen a:patternX) +" a:pattern0, ... Search patterns. +"* RETURN VALUES: +" [patternIndex, replacement]; if no supplied pattern matched, returns +" [-1, a:expr]. +"****************************************************************************** + for l:patternIndex in range(len(a:000)) + let l:pattern = a:000[l:patternIndex] + if a:expr =~ l:pattern + return [l:patternIndex, substitute(a:expr, l:pattern, a:replacement, a:flags)] + endif + endfor + return [-1, a:expr] +endfunction + +function! ingo#subst#FirstParameter( expr, patternTemplate, replacement, flags, ... ) +"****************************************************************************** +"* PURPOSE: +" Insert a:parameter1, ... into a:patternTemplate and perform a substitution +" with the first matching resulting pattern. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:expr Text to be transformed. +" a:patternTemplate Regular expression template; parameters are inserted +" into the %s (or named %[argument-index]$s) inside the +" template. +" a:replacement Replacement. +" a:parameter1, ... Parameters (regexp fragments) to be inserted into +" a:patternTemplate. +"* RETURN VALUES: +" [patternIndex, replacement]; if no supplied pattern matched, returns +" [-1, a:expr]. +"****************************************************************************** + let l:isIndexedParameter = (a:patternTemplate =~# '%\@ " " REVISION DATE REMARKS +" 1.025.004 18-May-2015 ingo#subst#pairs#Substitute() and +" ingo#subst#pairs#Split(): Only canonicalize path +" separators in {replacement} on demand, via +" additional a:isCanonicalizeReplacement argument. +" Some clients may not need iterative replacement, +" and treat the wildcard as a convenient +" regexp-shorthand, not overly filesystem-related. +" 1.025.003 01-May-2015 ingo#subst#pairs#Substitute(): Canonicalize path +" separators in {replacement}, too. This is +" important to match further pairs, too, as the +" pattern is always in canonical form, so the +" replacement has to be, too. +" ENH: Allow passing to +" ingo#subst#pairs#Substitute() [wildcard, +" replacement] Lists instead of +" {wildcard}={replacement} Strings, too. " 1.016.002 17-Jan-2014 Change s:pairPattern so that the first, not the " last = is used as the pair delimiter. " 1.016.001 16-Jan-2014 file creation from " autoload/EditSimilar/Substitute.vim let s:pairPattern = '\(^[^=]\+\)=\(.*$\)' -function! s:SplitPair( pair ) - if a:pair !~# s:pairPattern - throw 'Substitute: Not a substitution: ' . a:pair +function! s:SplitPair( pair, isCanonicalizeReplacement ) + if type(a:pair) == type([]) + let [l:from, l:to] = a:pair + else + if a:pair !~# s:pairPattern + throw 'Substitute: Not a substitution: ' . a:pair + endif + let [l:from, l:to] = matchlist(a:pair, s:pairPattern)[1:2] endif - let [l:from, l:to] = matchlist(a:pair, s:pairPattern)[1:2] - return [ingo#regexp#fromwildcard#Convert(l:from), l:to] + return [ingo#regexp#fromwildcard#Convert(l:from), (a:isCanonicalizeReplacement ? ingo#fs#path#Normalize(l:to) : l:to)] endfunction -function! ingo#subst#pairs#Split( pairs ) - return map(a:pairs, 's:SplitPair(v:val)') +function! ingo#subst#pairs#Split( pairs, ... ) + let l:isCanonicalizeReplacement = (a:0 ? a:1 : 0) + return map(a:pairs, 's:SplitPair(v:val, l:isCanonicalizeReplacement)') endfunction -function! ingo#subst#pairs#Substitute( text, pairs ) +function! ingo#subst#pairs#Substitute( text, pairs, ... ) "****************************************************************************** "* PURPOSE: " Apply {wildcard}={replacement} pairs (modeled after the Korn shell's "cd @@ -37,16 +59,23 @@ function! ingo#subst#pairs#Substitute( text, pairs ) "* INPUTS: " a:text Text to be substituted. " a:pairs List of {wildcard}={replacement} Strings that should be applied to -" a:text. +" a:text. Or List of [wildcard, replacement] List elements. +" a:isCanonicalizeReplacement Optional flag whether path separators in +" {replacement} should be canonicalized. This is +" important when doing further substitutions on +" the result, but may be unwanted when wildcards +" are treated as a convenient regexp-shorthand. +" Default is false, no canonicalization. "* RETURN VALUES: " List of [replacement, failedPairs], where failedPairs is a subset of " a:pairs. "****************************************************************************** + let l:isCanonicalizeReplacement = (a:0 ? a:1 : 0) let l:replacement = a:text let l:failedPairs = [] for l:pair in a:pairs - let [l:from, l:to] = s:SplitPair(l:pair) + let [l:from, l:to] = s:SplitPair(l:pair, l:isCanonicalizeReplacement) let l:beforeReplacement = l:replacement let l:replacement = substitute(l:replacement, l:from, escape(l:to, '\&~'), 'g') if l:replacement ==# l:beforeReplacement @@ -58,4 +87,5 @@ function! ingo#subst#pairs#Substitute( text, pairs ) return [l:replacement, l:failedPairs] endfunction + " vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/swap.vim b/autoload/ingo/swap.vim new file mode 100644 index 0000000..2107f47 --- /dev/null +++ b/autoload/ingo/swap.vim @@ -0,0 +1,45 @@ +" ingo/swap.vim: Functions around the swap file. +" +" DEPENDENCIES: +" - ingo/buffer/visible.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.025.001 29-Jan-2016 file creation + +function! ingo#swap#GetNameImpl() + " Use silent! so a failing redir (e.g. recursive redir call) won't hurt. + silent! redir => o | silent swapname | redir END + return (o[1:] ==# 'No swap file' ? '' : o[1:]) + return '' + else + return o[1:] + endif +endfunction +function! ingo#swap#GetName( ... ) +"****************************************************************************** +"* PURPOSE: +" Obtain the filespec of the swap file (like :swapname), for the current +" buffer or the passed buffer number. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:bufnr Optional buffer number of an existing buffer where the swap file +" should be obtained from. +"* RETURN VALUES: +" filespec of current swapfile, or empty string. +"****************************************************************************** + if a:0 + silent! return ingo#buffer#visible#Call(a:1, 'ingo#swap#GetNameImpl', []) + else + return ingo#swap#GetNameImpl() + endif +endfunction + +" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/text.vim b/autoload/ingo/text.vim index 703628f..da2feee 100644 --- a/autoload/ingo/text.vim +++ b/autoload/ingo/text.vim @@ -3,12 +3,14 @@ " DEPENDENCIES: " - ingo/pos.vim autoload script " -" Copyright: (C) 2012-2015 Ingo Karkat +" Copyright: (C) 2012-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.010 23-Mar-2016 Add ingo#text#GetCharBefore() variant of +" ingo#text#GetChar(). " 1.024.009 22-Apr-2015 ingo#text#Insert(): Also allow insertion one " beyond the last line (in column 1), just like " setline() allows. @@ -96,6 +98,28 @@ function! ingo#text#GetChar( startPos, ... ) return matchstr(getline(l:line), '\%' . l:column . 'c' . '.' . (l:count ? '\{' . (l:isUpTo ? ',' : '') . l:count . '}' : '')) endfunction +function! ingo#text#GetCharBefore( startPos, ... ) +"******************************************************************************* +"* PURPOSE: +" Extract one / a:count character(s) before a:startPos from the current buffer. +" Only considers the current line. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:startPos [line, col] +" a:count Optional number of characters to extract; default 1. +" If this is a negative number, tries to extract as many as +" possible instead of not matching. +"* RETURN VALUES: +" string text, or empty string if no(t enough) character(s). +"******************************************************************************* + let [l:line, l:column] = a:startPos + let [l:count, l:isUpTo] = (a:0 ? (a:1 > 0 ? [a:1, 0] : [-1 * a:1, 1]) : [0, 0]) + + return matchstr(getline(l:line), '.' . (l:count ? '\{' . (l:isUpTo ? ',' : '') . l:count . '}' : '') . '\%' . l:column . 'c') +endfunction function! ingo#text#Insert( pos, text ) "****************************************************************************** diff --git a/autoload/ingo/text/frompattern.vim b/autoload/ingo/text/frompattern.vim index 4a60fe4..503970d 100644 --- a/autoload/ingo/text/frompattern.vim +++ b/autoload/ingo/text/frompattern.vim @@ -8,6 +8,9 @@ " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.004 06-May-2015 Add ingo#text#frompattern#GetAroundHere(), +" inspired by +" http://stackoverflow.com/questions/30073662/vim-copy-match-with-cursor-position-atom-to-local-variable " 1.024.003 17-Apr-2015 ingo#text#frompattern#GetHere(): Do not move the " cursor (to the end of the matched pattern); this " is unexpected and can be easily avoided. @@ -38,6 +41,37 @@ function! ingo#text#frompattern#GetHere( pattern, ... ) endif return ingo#text#Get(l:startPos, l:endPos) endfunction +function! ingo#text#frompattern#GetAroundHere( pattern, ... ) +"****************************************************************************** +"* PURPOSE: +" Extract the match of a:pattern starting the match from the current cursor +" position, but (unlike ingo#text#frompattern#GetHere()), also include matched +" characters _before_ the current position. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:pattern Regular expression to search. 'ignorecase', 'smartcase' and +" 'magic' applies. When empty, the last search pattern |"/| is +" used. +" a:lastLine End line number to search for the start of the pattern. +" Optional; defaults to the current line. +" a:firstLine First line number to search for the start of the pattern. +" Optional; defaults to the current line. +"* RETURN VALUES: +" Matched text, or empty string. +"****************************************************************************** + let l:startPos = searchpos(a:pattern, 'bcnW', (a:0 >= 2 ? a:2 : line('.'))) + if l:startPos == [0, 0] + return '' + endif + let l:endPos = searchpos(a:pattern, 'cenW', (a:0 ? a:1 : line('.'))) + if l:endPos == [0, 0] + return '' + endif + return ingo#text#Get(l:startPos, l:endPos) +endfunction function! s:UniqueAdd( list, expr ) diff --git a/autoload/ingo/window/locate.vim b/autoload/ingo/window/locate.vim new file mode 100644 index 0000000..70a808d --- /dev/null +++ b/autoload/ingo/window/locate.vim @@ -0,0 +1,173 @@ +" ingo/window/locate.vim: Functions to locate a window. +" +" DEPENDENCIES: +" - ingo/actions.vim autoload script +" +" Copyright: (C) 2016 Ingo Karkat +" The VIM LICENSE applies to this script; see ':help copyright'. +" +" Maintainer: Ingo Karkat +" +" REVISION DATE REMARKS +" 1.028.001 25-Nov-2016 file creation + +function! s:Match( winVarName, Predicate, winNr, ... ) + if a:0 >= 2 && a:winNr == a:2 + return 0 + endif + let l:tabNr = (a:0 ? a:1 : tabpagenr()) + + let l:value = gettabwinvar(l:tabNr, a:winNr, a:winVarName) + return !! ingo#actions#EvaluateWithValOrFunc(a:Predicate, l:value) +endfunction + +function! s:CheckTabPageNearest( tabNr, winVarName, Predicate, ... ) + let l:skipWinNr = (a:0 ? a:1 : 0) + let [l:currentWinNr, l:previousWinNr, l:lastWinNr] = [tabpagewinnr(a:tabNr), tabpagewinnr(a:tabNr, '#'), tabpagewinnr(a:tabNr, '$')] + if s:Match(a:winVarName, a:Predicate, l:currentWinNr, a:tabNr, l:skipWinNr) + return [a:tabNr, l:currentWinNr] + elseif s:Match(a:winVarName, a:Predicate, l:previousWinNr, a:tabNr, l:skipWinNr) + return [a:tabNr, l:previousWinNr] + endif + + let l:offset = 1 + while l:currentWinNr - l:offset > 0 || l:currentWinNr + l:offset <= l:lastWinNr + if s:Match(a:winVarName, a:Predicate, l:currentWinNr - l:offset, a:tabNr, l:skipWinNr) + return [a:tabNr, l:currentWinNr - l:offset] + elseif s:Match(a:winVarName, a:Predicate, l:currentWinNr + l:offset, a:tabNr, l:skipWinNr) + return [a:tabNr, l:currentWinNr + l:offset] + endif + let l:offset += 1 + endwhile + return [0, 0] +endfunction + +function! ingo#window#locate#NearestByPredicate( isSearchOtherTabPages, winVarName, Predicate ) +"****************************************************************************** +"* PURPOSE: +" Locate the window closest to the current one where the window variable a:winVarName makes +" a:Predicate (passed in as argument or v:val) true. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:isSearchOtherTabPages Flag whether windows in other tab pages should also +" be considered. +" a:winVarName Name of the window-local variable, like in +" |gettabwinvar()| +" a:Predicate Either a Funcref or an expression to be eval()ed. +" Gets the value of a:winVarName passed, should return +" a boolean value. +"* RETURN VALUES: +" [tabpagenr, winnr] if a:isSearchOtherTabPages and the found window is on a +" different tab page +" [0, winnr] if the window is on the current tab page +" [0, 0] if a:Predicate did not yield true in any other window +"****************************************************************************** + let l:lastWinNr = winnr('#') + if l:lastWinNr != 0 && s:Match(a:winVarName, a:Predicate, l:lastWinNr) + return [tabpagenr(), l:lastWinNr] + endif + + let l:result = s:CheckTabPageNearest(tabpagenr(), a:winVarName, a:Predicate, winnr()) + if l:result != [0, 0] || ! a:isSearchOtherTabPages + return l:result + endif + + + let [l:currentTabPageNr, l:lastTabPageNr] = [tabpagenr(), tabpagenr('$')] + let l:offset = 1 + while l:currentTabPageNr - l:offset > 0 || l:currentTabPageNr + l:offset <= l:lastTabPageNr + let l:result = s:CheckTabPageNearest(l:currentTabPageNr - l:offset, a:winVarName, a:Predicate) + if l:result != [0, 0] | return l:result | endif + + let l:result = s:CheckTabPageNearest(l:currentTabPageNr + l:offset, a:winVarName, a:Predicate) + if l:result != [0, 0] | return l:result | endif + + let l:offset += 1 + endwhile + + return [0, 0] +endfunction + +function! ingo#window#locate#FirstByPredicate( isSearchOtherTabPages, winVarName, Predicate ) +"****************************************************************************** +"* PURPOSE: +" Locate the first window (in this tab page, or with a:isSearchOtherTabPages +" in other tabs) where the window variable a:winVarName makes a:Predicate +" (passed in as argument or v:val) true. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:isSearchOtherTabPages Flag whether windows in other tab pages should also +" be considered. +" a:winVarName Name of the window-local variable, like in +" |gettabwinvar()| +" a:Predicate Either a Funcref or an expression to be eval()ed. +" Gets the value of a:winVarName passed, should return +" a boolean value. +"* RETURN VALUES: +" [tabpagenr, winnr] if a:isSearchOtherTabPages and the found window is on a +" different tab page +" [0, winnr] if the window is on the current tab page +" [0, 0] if a:Predicate did not yield true in any other window +"****************************************************************************** + for l:winNr in range(1, winnr('$')) + if s:Match(a:winVarName, a:Predicate, l:winNr) + return [0, l:winNr] + endif + endfor + if ! a:isSearchOtherTabPages + return [0, 0] + endif + + for l:tabPageNr in filter(range(1, tabpagenr('$')), 'v:val != ' . tabpagenr()) + let l:lastWinNr = tabpagewinnr(l:tabPageNr, '$') + for l:winNr in range(1, l:lastWinNr) + if s:Match(a:winVarName, a:Predicate, l:winNr, l:tabPageNr) + return [l:tabPageNr, l:winNr] + endif + endfor + endfor + + return [0, 0] +endfunction + +function! ingo#window#locate#ByPredicate( strategy, isSearchOtherTabPages, winVarName, Predicate ) +"****************************************************************************** +"* PURPOSE: +" Locate a window (in this tab page, or with a:isSearchOtherTabPages in other +" tabs), with a:strategy to determine precedences, where the window variable +" a:winVarName makes a:Predicate (passed in as argument or v:val) true. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:strategy One of "first" or "nearest". +" a:isSearchOtherTabPages Flag whether windows in other tab pages should also +" be considered. +" a:winVarName Name of the window-local variable, like in +" |gettabwinvar()| +" a:Predicate Either a Funcref or an expression to be eval()ed. +" Gets the value of a:winVarName passed, should return +" a boolean value. +"* RETURN VALUES: +" [tabpagenr, winnr] if a:isSearchOtherTabPages and the found window is on a +" different tab page +" [0, winnr] if the window is on the current tab page +" [0, 0] if a:Predicate did not yield true in any other window +"****************************************************************************** + if a:strategy ==# 'first' + return ingo#window#locate#FirstByPredicate(a:isSearchOtherTabPages, a:winVarName, a:Predicate) + elseif a:strategy ==# 'nearest' + return ingo#window#locate#NearestByPredicate(a:isSearchOtherTabPages, a:winVarName, a:Predicate) + else + throw 'ASSERT: Unknown strategy ' . string(a:strategy) + endif +endfunction + +" vism: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax : diff --git a/autoload/ingo/window/preview.vim b/autoload/ingo/window/preview.vim index 4e2f6c9..4c79567 100644 --- a/autoload/ingo/window/preview.vim +++ b/autoload/ingo/window/preview.vim @@ -2,12 +2,17 @@ " " DEPENDENCIES: " -" Copyright: (C) 2008-2014 Ingo Karkat +" Copyright: (C) 2008-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.029.005 13-Dec-2016 BUG: Optional a:position argument to +" ingo#window#preview#SplitToPreview() is +" mistakenly truncated to [1:2]. Inline the +" l:cursor and l:bufnr variables; they are only +" used in the function call, anyway. " 1.021.004 06-Jul-2014 Support all imaginable argument variants of " ingo#window#preview#OpenFilespec(), so that it " can be used as a wrapper that encapsulates the @@ -87,12 +92,9 @@ function! ingo#window#preview#SplitToPreview( ... ) if &l:previewwindow | return 0 | endif endif - let l:cursor = getpos('.') - let l:bufnr = bufnr('') - " Clone current cursor position to preview window (which now shows the same " file) or passed position. - call ingo#window#preview#OpenBuffer(l:bufnr, (a:0 ? a:1 : l:cursor)[1:2]) + call ingo#window#preview#OpenBuffer(bufnr(''), (a:0 ? a:1 : getpos('.')[1:2])) return 1 endfunction function! ingo#window#preview#GotoPreview() diff --git a/autoload/ingo/window/special.vim b/autoload/ingo/window/special.vim index b761413..87853bb 100644 --- a/autoload/ingo/window/special.vim +++ b/autoload/ingo/window/special.vim @@ -2,27 +2,75 @@ " " DEPENDENCIES: " -" Copyright: (C) 2008-2013 Ingo Karkat +" Copyright: (C) 2008-2016 Ingo Karkat " The VIM LICENSE applies to this script; see ':help copyright'. " " Maintainer: Ingo Karkat " " REVISION DATE REMARKS +" 1.025.003 28-Jan-2016 ENH: Make +" ingo#window#special#SaveSpecialWindowSize() +" return sum of special windows' widths and sum of +" special windows' heights. +" 1.025.002 26-Jan-2016 ENH: Enable customization of +" ingo#window#special#IsSpecialWindow() via +" g:IngoLibrary_SpecialWindowPredicates. " 1.004.001 08-Apr-2013 file creation from autoload/ingowindow.vim -" Special windows are preview, quickfix (and location lists, which is also of -" type 'quickfix'). function! ingo#window#special#IsSpecialWindow( ... ) +"****************************************************************************** +"* PURPOSE: +" Check whether the current / passed window is special; special windows are +" preview, quickfix (and location lists, which is also of type 'quickfix'). +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" None. +"* INPUTS: +" a:winnr Optional window number. +" The check can be customized via g:IngoLibrary_SpecialWindowPredicates, which +" takes a List of Expressions or Funcrefs that are passed the window number, +" and which should return a boolean flag. If any predicate is true, the window +" is deemed special. +"* RETURN VALUES: +" 1 if special; else 0. +"****************************************************************************** let l:winnr = (a:0 > 0 ? a:1 : winnr()) - return getwinvar(l:winnr, '&previewwindow') || getwinvar(l:winnr, '&buftype') ==# 'quickfix' + return getwinvar(l:winnr, '&previewwindow') || getwinvar(l:winnr, '&buftype') ==# 'quickfix' || + \ (exists('g:IngoLibrary_SpecialWindowPredicates') && ! empty( + \ filter( + \ map(copy(g:IngoLibrary_SpecialWindowPredicates), 'ingo#actions#EvaluateWithValOrFunc(v:val, l:winnr)'), + \ '!! v:val' + \ ) + \ )) endfunction function! ingo#window#special#SaveSpecialWindowSize() +"****************************************************************************** +"* PURPOSE: +" Calculate widths and heights of visible special windows, and store those for +" later restoration. +"* ASSUMPTIONS / PRECONDITIONS: +" None. +"* EFFECTS / POSTCONDITIONS: +" Stores window numbers of special windows, and their current widths and +" heights. +"* INPUTS: +" None. +"* RETURN VALUES: +" [sum of special windows' widths, sum of special windows' heights] +"****************************************************************************** let s:specialWindowSizes = {} + let [l:specialWidths, l:specialHeights] = [0, 0] for l:w in range(1, winnr('$')) if ingo#window#special#IsSpecialWindow(l:w) - let s:specialWindowSizes[l:w] = [winwidth(l:w), winheight(l:w)] + let [l:width, l:height] = [winwidth(l:w), winheight(l:w)] + let s:specialWindowSizes[l:w] = [l:width, l:height] + + let l:specialWidths += l:width + let l:specialHeights += l:height endif endfor + return [l:specialWidths, l:specialHeights] endfunction function! ingo#window#special#RestoreSpecialWindowSize() for l:w in keys(s:specialWindowSizes) diff --git a/doc/ingo-library.txt b/doc/ingo-library.txt index 9caeafd..8ee3b9f 100644 --- a/doc/ingo-library.txt +++ b/doc/ingo-library.txt @@ -83,6 +83,175 @@ IDEAS *ingo-library-ideas* ============================================================================== HISTORY *ingo-library-history* +1.029 24-Jan-2017 +- CHG: ingo#comments#RemoveCommentPrefix() isn't useful as it omits any indent + before the comment prefix. Change its implementation to just erase the + prefix itself. +- Add ingo#comments#SplitIndentAndText() to provide what + ingo#comments#RemoveCommentPrefix() was previously used to: The line broken + into indent (before, after, and with the comment prefix), and the remaining + text. +- Add ingo#indent#Split(), a simpler version of + ingo#comments#SplitIndentAndText(). +- Add ingo#fs#traversal#FindFirstContainedInUpDir(). +- ingo#range#lines#Get(): A single (a:isGetAllRanges = 0) /.../ range already + clobbers the last search pattern. Save and restore if necessary, and base + didClobberSearchHistory on that check. +- ingo#range#lines#Get(): Drop the ^ anchor for the range check to also detect + /.../ as the end of the range. +- Add ingo#cmdargs#register#ParsePrependedWritableRegister() alternative to + ingo#cmdargs#register#ParseAppendedWritableRegister(). +- BUG: Optional a:position argument to ingo#window#preview#SplitToPreview() is + mistakenly truncated to [1:2]. Inline the l:cursor and l:bufnr variables; + they are only used in the function call, anyway. +- Add ingo/str/find.vim module. +- Add ingo/str/fromrange.vim module. +- Add ingo#pos#SameLineIs[OnOr]After/Before() variants. +- Add ingo/regexp/build.vim module. +- Add ingo#err#SetAndBeep(). +- FIX: ingo#query#get#Char() does not beep when validExpr is given and invalid + character pressed. +- Add ingo#query#get#ValidChar() variant that loops until a valid character + has been pressed. +- Add ingo/range/invert.vim module. +- Add ingo/line/replace.vim and ingo/lines/replace.vim modules. +- Extract ingo#range#merge#FromLnums() from ingo#range#merge#Merge(). +- ingo#range#lines#Get(): If the range is a backwards-looking ?{pattern}?, we + need to attempt the match on any line with :global/^/... Else, the border + behavior is inconsistent: ranges that extend the passed range at the bottom + are (partially) included, but ranges that extend at the front would not be. +- Add ingo/math.vim, ingo/binary.vim and ingo/list/split.vim modules. +- Add ingo#comments#SplitAll(), a more powerful variant of + ingo#comments#SplitIndentAndText(). +- Add ingo#compat#systemlist(). +- Add ingo#escape#OnlyUnescaped(). +- Add ingo#msg#ColoredMsg() and ingo#msg#ColoredStatusMsg(). +- Add ingo/query/recall.vim module. +- Add ingo#register#GetAsList(). +- FIX: ingo#format#Format(): An invalid %0$ references the last passed + argument instead of yielding the empty string (as [argument-index$] is + 1-based). Add bounds check to avoid that +- FIX: ingo#format#Format(): Also support escaping via "%%", as in printf(). +- Add ingo#subst#FirstSubstitution(), ingo#subst#FirstPattern(), + ingo#subst#FirstParameter(). +- Add ingo#regexp#collection#Expr(). +- BUG: ingo#regexp#magic#Normalize() also processes the contents of + collections [...]; especially the escaping of "]" wreaks havoc on the + pattern. Rename s:ConvertMagicness() into + ingo#regexp#magic#ConvertMagicnessOfElement() and introduce intermediate + s:ConvertMagicnessOfFragment() that first separates collections from other + elements and only invokes the former on those other elements. +- Add ingo#collections#fromsplit#MapItemsAndSeparators(). + +1.028 30-Nov-2016 +- ENH: Also support optional a:flagsMatchCount in + ingo#cmdargs#pattern#ParseUnescaped() and + ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord(). +- Add missing ingo#cmdargs#pattern#ParseWithLiteralWholeWord() variant. +- ingo#codec#URL#Decode(): Also convert the character set to UTF-8 to properly + handle non-ASCII characters. For example, %C3%9C should decode to "Ü", not + to "ɜ". +- Add ingo#collections#SeparateItemsAndSeparators(), a variant of + ingo#collections#SplitKeepSeparators(). +- Add ingo/collections/fromsplit.vim module. +- Add ingo#list#Join(). +- Add ingo/compat/window.vim module. +- Add ingo/fs/path/asfilename.vim module. +- Add ingo/list/find.vim module. +- Add ingo#option#Join(). +- FIX: Correct delegation in ingo#buffer#temp#Execute(); wrong recursive call + was used (after 1.027). +- ENH: Add optional a:isSilent argument to ingo#buffer#temp#Execute(). +- ENH: Add optional a:reservedColumns also to ingo#avoidprompt#TruncateTo(), + and pass this from ingo#avoidprompt#Truncate(). +- ingo#avoidprompt#TruncateTo(): The strright() cannot precisely account for + the rendering of tab widths. Check the result, and if necessary, remove + further characters until we go below the limit. +- ENH: Add optional {context} to all ingo#err#... functions, in case other + custom commands can be called between error setting and checking, to avoid + clobbering of your error message. +- Add ingo/buffer/locate.vim module. +- Add ingo/window/locate.vim module. +- Add ingo/indent.vim module. +- Add ingo#compat#getcurpos(). + +1.027 30-Sep-2016 +- Add ingo#buffer#temp#ExecuteWithText() and ingo#buffer#temp#CallWithText() + variants that pre-initialize the buffer (a common use case). +- Add ingo#msg#MsgFromShellError(). +- ENH: ingo#query#fromlist#Query(): Support headless (testing) mode via + g:IngoLibrary_QueryChoices, like ingo#query#Confirm() already does. +- Expose ingo#query#fromlist#RenderList(). Expose + ingo#query#StripAccellerator(). +- ENH: ingo#cmdargs#pattern#Parse(): Add second optional a:flagsMatchCount + argument, similar to what ingo#cmdargs#substitute#Parse() has in a:options. +- Add ingo#cmdargs#pattern#RawParse(). +- Add ingo/regexp/collection.vim module. +- Add ingo#str#trd(). + +1.026 11-Aug-2016 +- Add ingo#strdisplaywidth#pad#Middle(). +- Add ingo/format/columns.vim module. +- ENH: ingo#avoidprompt#TruncateTo() and ingo#strdisplaywidth#TruncateTo() + have a configurable ellipsis string g:IngoLibrary_TruncateEllipsis, now + defaulting to a single-char UTF-8 variant if we're in such encoding. Thanks + to Daniel Hahler for sending a patch! It also handles pathologically small + lengths that only show / cut into the ellipsis. +- Add ingo#compat#strgetchar() and ingo#compat#strcharpart(), introduced in + Vim 7.4.1730. +- Support ingo#compat#strchars() optional {skipcc} argument, introduced in Vim + 7.4.755. + +1.025 09-Aug-2016 +- Add ingo#str#Contains(). +- Add ingo#fs#path#split#Contains(). +- ingo#subst#pairs#Substitute(): Canonicalize path separators in + {replacement}, too. This is important to match further pairs, too, as the + pattern is always in canonical form, so the replacement has to be, too. +- ingo#subst#pairs#Substitute() and ingo#subst#pairs#Split(): Only + canonicalize path separators in {replacement} on demand, via additional + a:isCanonicalizeReplacement argument. Some clients may not need iterative + replacement, and treat the wildcard as a convenient regexp-shorthand, not + overly filesystem-related. +- ENH: Allow passing to ingo#subst#pairs#Substitute() [wildcard, replacement] + Lists instead of {wildcard}={replacement} Strings, too. +- Add ingo#collections#Partition(). +- Add ingo#text#frompattern#GetAroundHere(). +- Add ingo#cmdline#showmode#TemporaryNoShowMode() variant of + ingo#cmdline#showmode#OneLineTemporaryNoShowMode(). +- ENH: Enable customization of ingo#window#special#IsSpecialWindow() via + g:IngoLibrary_SpecialWindowPredicates. +- Add ingo#query#Question(). +- ENH: Make ingo#window#special#SaveSpecialWindowSize() return sum of special + windows' widths and sum of special windows' heights. +- Add ingo/swap.vim module. +- Add ingo#collections#unique#Insert() and ingo#collections#unique#Add(). +- BUG: Unescaped backslash resulted in unclosed [...] regexp collection + causing ingo#escape#file#fnameunescape() to fail to escape on Unix. +- Add ingo#text#GetCharBefore() variant of ingo#text#GetChar(). +- Add optional a:characterOffset to ingo#record#PositionAndLocation(). +- Add ingo#regexp#MakeStartWordSearch() ingo#regexp#MakeEndWordSearch() + variants of ingo#regexp#MakeWholeWordSearch(). +- Add ingo#pos#IsInsideVisualSelection(). +- Add ingo#escape#command#mapunescape(). +- ENH: Add second optional flag a:isKeepDirectories to + ingo#cmdargs#glob#Expand() / ingo#cmdargs#glob#ExpandSingle(). +- Add ingo#range#borders#StartAndEndRange(). +- Add ingo#msg#VerboseMsg(). +- Add ingo#compat#sha256(), with a fallback to an external sha256sum command. +- Add ingo#collections#Reduce(). +- Add ingo/actions/iterations.vim module. +- Add ingo/actions/special.vim module. +- Add ingo#collections#differences#ContainsLoosely() and + ingo#collections#differences#ContainsStrictly(). +- Add ingo#buffer#ExistOtherLoadedBuffers(). +- FIX: Temporarily reset 'switchbuf' in ingo#buffer#visible#Execute() and + ingo#buffer#temp#Execute(), to avoid that "usetab" switched to another tab + page. +- ingo#msg#HighlightMsg(): Make a:hlgroup optional, default to 'None' (so the + function is useful to return to normal highlighting). +- Add ingo#msg#HighlightN(), an :echon variant. + 1.024 23-Apr-2015 - FIX: Also correctly set change marks when replacing entire buffer with ingo#lines#Replace(). @@ -509,7 +678,7 @@ First published version as separate shared library. Started development of shared autoload functionality. ============================================================================== -Copyright: (C) 2009-2015 Ingo Karkat +Copyright: (C) 2009-2017 Ingo Karkat Contains URL encoding / decoding algorithms written by Tim Pope. The VIM LICENSE applies to this plugin; see |copyright|.