mirror of
https://github.com/inkarkat/vim-ingo-library.git
synced 2026-05-29 11:18:51 +02:00
367 lines
15 KiB
VimL
367 lines
15 KiB
VimL
" ingo/comments.vim: Functions around comment handling.
|
|
"
|
|
" DEPENDENCIES:
|
|
" - ingo/compat.vim autoload script
|
|
"
|
|
" Copyright: (C) 2011-2019 Ingo Karkat
|
|
" The VIM LICENSE applies to this script; see ':help copyright'.
|
|
"
|
|
" Maintainer: Ingo Karkat <ingo@karkat.de>
|
|
|
|
function! s:CommentDefinitions()
|
|
return map(split(&l:comments, ','), 'matchlist(v:val, ''\([^:]*\):\(.*\)'')[1:2]')
|
|
endfunction
|
|
|
|
function! s:IsPrefixMatch( string, prefix )
|
|
return strpart(a:string, 0, len(a:prefix)) ==# a:prefix
|
|
endfunction
|
|
|
|
function! ingo#comments#CheckComment( text, ... )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Check whether a:text is a comment according to 'comments' definitions.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* 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; 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.
|
|
" often indent in the middle part of a
|
|
" three-piece) is stripped.
|
|
"* RETURN VALUES:
|
|
" [] if a:text is not a comment.
|
|
" [commentstring, type, nestingLevel, isBlankRequired] if a:text is a comment.
|
|
" commentstring is the found comment prefix; if an offset was defined,
|
|
" this is included.
|
|
" type is empty for a normal comment leader, and either "s", "m" or "e"
|
|
" for a three-piece comment.
|
|
" nestingLevel is > 0 if the "n" flag is contained in 'comments' and
|
|
" indicates the number of nested comments. Only repetitive same comments
|
|
" are counted for nesting.
|
|
" isBlankRequired is a boolean flag
|
|
"******************************************************************************
|
|
let l:options = (a:0 ? a:1 : {})
|
|
let l:isIgnoreIndent = get(l:options, 'isIgnoreIndent', 1)
|
|
let l:isStripNonEssentialWhiteSpaceFromCommentString = get(l:options, 'isStripNonEssentialWhiteSpaceFromCommentString', 1)
|
|
|
|
let l:text = (l:isIgnoreIndent ? substitute(a:text, '^\s*', '', '') : a:text)
|
|
|
|
for [l:flags, l:string] in s:CommentDefinitions()
|
|
if l:flags =~# '[se]'
|
|
if l:flags =~# '[se].*\d' && l:flags !~# '-\d'
|
|
if l:isIgnoreIndent
|
|
let l:threePieceOffset = ''
|
|
else
|
|
" Consider positive offset for the middle of a three-piece
|
|
" comment when matching with a:text.
|
|
let l:threePieceOffset = repeat(' ', matchstr(l:flags, '\d\+'))
|
|
endif
|
|
elseif l:flags =~# 's'
|
|
" Clear any offset from previous three-piece comment.
|
|
let l:threePieceOffset = ''
|
|
endif
|
|
endif
|
|
" TODO: Handle "r" right-align flag through offset, too.
|
|
|
|
let l:commentstring = ''
|
|
if s:IsPrefixMatch(l:text, l:string)
|
|
let l:commentstring = l:string
|
|
elseif (l:flags =~# '[me]' && ! empty(l:threePieceOffset) && s:IsPrefixMatch(l:text, l:threePieceOffset . l:string))
|
|
let l:commentstring = l:threePieceOffset . l:string
|
|
endif
|
|
if ! empty(l:commentstring)
|
|
let l:isBlankRequired = (l:flags =~# 'b')
|
|
if l:isBlankRequired && l:text[stridx(l:text, l:string) + len(l:string)] !~# '\s'
|
|
" The whitespace after the comment is missing.
|
|
continue
|
|
endif
|
|
|
|
let l:nestingLevel = 0
|
|
if l:flags =~# 'n'
|
|
let l:comments = matchstr(l:text, '\V\C\^\s\*\zs\%(' . escape(l:string, '\') . '\s' . (l:isBlankRequired ? '\+' : '\*') . '\)\+')
|
|
let l:nestingLevel = strlen(substitute(l:comments, '\V\C' . escape(l:string, '\') . '\s\*', 'x', 'g'))
|
|
endif
|
|
|
|
if l:isStripNonEssentialWhiteSpaceFromCommentString
|
|
let l:commentstring = substitute(l:commentstring, '\s*$', '', '')
|
|
endif
|
|
return [l:commentstring, matchstr(l:flags, '\C[sme]'), l:nestingLevel, l:isBlankRequired]
|
|
endif
|
|
endfor
|
|
|
|
return []
|
|
endfunction
|
|
|
|
function! s:AvoidDuplicateIndent( commentstring, text )
|
|
" When the text starts with indent identical to what 'commentstring' would
|
|
" render, avoid having duplicate indent.
|
|
let l:renderedIndent = matchstr(a:commentstring, '\s\+\ze%s')
|
|
return (a:text =~# '^\V' . l:renderedIndent ? strpart(a:text, len(l:renderedIndent)) : a:text)
|
|
endfunction
|
|
function! ingo#comments#RenderComment( text, checkComment )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Render a:text as a comment.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" Uses comment format from 'commentstring', if defined.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* INPUTS:
|
|
" a:text The text to be rendered.
|
|
" a:checkComment Comment information returned by ingo#comments#CheckComment().
|
|
"* RETURN VALUES:
|
|
" Returns a:text unchanged if a:checkComment is empty.
|
|
" Otherwise, returns a:text rendered as a comment (as good as it can).
|
|
"******************************************************************************
|
|
if empty(a:checkComment)
|
|
return a:text
|
|
endif
|
|
|
|
let [l:commentprefix, l:type, l:nestingLevel, l:isBlankRequired] = a:checkComment
|
|
|
|
if &commentstring =~# '\V\C' . escape(l:commentprefix, '\') . (l:isBlankRequired ? '\s' : '')
|
|
" The found comment is the same as 'commentstring' will generate.
|
|
" Generate with the proper nesting.
|
|
let l:render = s:AvoidDuplicateIndent(&commentstring, a:text)
|
|
for l:ii in range(max([1, l:nestingLevel]))
|
|
let l:render = printf(&commentstring, l:render)
|
|
endfor
|
|
return l:render
|
|
elseif ! empty(&commentstring)
|
|
" No match, just use 'commentstring'.
|
|
return printf(&commentstring, s:AvoidDuplicateIndent(&commentstring, a:text))
|
|
else
|
|
" No 'commentstring' defined, use same comment prefix.
|
|
return repeat(l:commentprefix . (l:isBlankRequired ? ' ' : ''), max([1, l:nestingLevel])) . (l:isBlankRequired ? '' : ' ') . s:AvoidDuplicateIndent(' %s', a:text)
|
|
endif
|
|
endfunction
|
|
|
|
function! ingo#comments#RemoveCommentPrefix( line )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Remove the comment prefix from a:line while keeping the overall indent.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* INPUTS:
|
|
" a:line The text of the line to be rendered comment-less.
|
|
"* RETURN VALUES:
|
|
" 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#GetSplitIndentPattern( minNumberOfCommentPrefixesExpr, lineOrStartLnum, ... )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Analyze a:line (or the a:startLnum, a:endLnum range of lines in the current
|
|
" buffer) and generate a regular expression that matches possible indent with
|
|
" comment prefix. If there's no comment, just match indent.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* INPUTS:
|
|
" a:minNumberOfCommentPrefixesExpr Number of comment prefixes (if any are
|
|
" detected) that must exist. If empty, the
|
|
" exact number of detected (nested)
|
|
" comment prefixes has to exist. If 1, at
|
|
" least one comment prefix has to exist.
|
|
" If 0, indent and comment prefixes are
|
|
" purely optional; the returned pattern
|
|
" may match nothing at all at the
|
|
" beginning of a line.
|
|
" a:line The line to be analyzed for splitting, or:
|
|
" a:startLnum First line number in the current buffer to be analyzed.
|
|
" a:endLnum Last line number in the current buffer to be analyzed; the first
|
|
" line in the range that has a comment prefix is used.
|
|
"* RETURN VALUES:
|
|
" Regular expression matching the indent plus potential comment prefix,
|
|
" anchored to the start of a line.
|
|
"******************************************************************************
|
|
if a:0
|
|
for l:lnum in range(a:lineOrStartLnum, a:1)
|
|
let l:checkComment = ingo#comments#CheckComment(getline(l:lnum))
|
|
if ! empty(l:checkComment)
|
|
return s:GetSplitIndentPattern(l:checkComment, a:minNumberOfCommentPrefixesExpr)
|
|
endif
|
|
endfor
|
|
return s:GetSplitIndentPattern([], a:minNumberOfCommentPrefixesExpr)
|
|
else
|
|
return s:GetSplitIndentPattern(ingo#comments#CheckComment(a:lineOrStartLnum), a:minNumberOfCommentPrefixesExpr)
|
|
endif
|
|
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:GetSplitIndentPattern( checkComment, ... )
|
|
let l:minNumberOfCommentPrefixesExpr = (a:0 && a:1 isnot# '' ? a:1 . ',' : '')
|
|
if empty(a:checkComment)
|
|
return '^\%(\s*\)'
|
|
endif
|
|
|
|
let [l:commentprefix, l:type, l:nestingLevel, l:isBlankRequired] = a:checkComment
|
|
|
|
return '\V\C\^' .
|
|
\ '\s\*\%(' . escape(l:commentprefix, '\') . (l:isBlankRequired ? '\s\+' : '\s\*'). '\)\{' . l:minNumberOfCommentPrefixesExpr . max([1, l:nestingLevel]) . '}' .
|
|
\ '\m'
|
|
endfunction
|
|
function! s:GetSplitIndentAndTextPattern( checkComment )
|
|
return '\(' . s:GetSplitIndentPattern(a:checkComment) . '\)\(.*\)$'
|
|
endfunction
|
|
function! s:SplitIndentAndText( line, checkComment )
|
|
return matchlist(a:line, s:GetSplitIndentAndTextPattern(a:checkComment))[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], 0]
|
|
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 )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Check whether a:prefix is a comment leader as defined in 'comments'.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* INPUTS:
|
|
" a:prefix Text to be checked for being a comment prefix. There must either
|
|
" be no leading whitespace or exactly the amount mandated by the
|
|
" indent of a three-piece comment. No blank is required in
|
|
" a:prefix, even if the "b" flag is contained in 'comments', so
|
|
" this function can be used for checking as-you-type.
|
|
"* RETURN VALUES:
|
|
" [] if a:prefix is not a comment leader.
|
|
" [type, isBlankRequired] if a:prefix is a comment leader.
|
|
" type is empty for a normal comment leader, and either "s", "m" or "e"
|
|
" for a three-piece comment.
|
|
" isBlankRequired is a boolean flag
|
|
"******************************************************************************
|
|
for [l:flags, l:string] in s:CommentDefinitions()
|
|
if l:flags =~# '[se]'
|
|
if l:flags =~# '[se].*\d' && l:flags !~# '-\d'
|
|
" Consider positive offset for the middle of a three-piece
|
|
" comment when matching with a:prefix.
|
|
let l:threePieceOffset = repeat(' ', matchstr(l:flags, '\d\+'))
|
|
elseif l:flags =~# 's'
|
|
" Clear any offset from previous three-piece comment.
|
|
let l:threePieceOffset = ''
|
|
endif
|
|
endif
|
|
" TODO: Handle "r" right-align flag through offset, too.
|
|
|
|
if a:prefix ==# l:string || (l:flags =~# '[me]' && a:prefix ==# (l:threePieceOffset . l:string))
|
|
return [matchstr(l:flags, '\C[sme]'), (l:flags =~# 'b')]
|
|
endif
|
|
endfor
|
|
|
|
return []
|
|
endfunction
|
|
|
|
function! ingo#comments#GetThreePieceIndent( prefix )
|
|
"******************************************************************************
|
|
"* PURPOSE:
|
|
" Check whether a:prefix is a comment leader of a three-piece comment as
|
|
" defined in 'comments', and return the indent in case of a middle or end
|
|
" comment prefix.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" None.
|
|
"* INPUTS:
|
|
" a:prefix Text that may be a comment prefix. Must not include leading or
|
|
" trailing whitespace, only the actual comment characters.
|
|
"* RETURN VALUES:
|
|
" Indent, or 0.
|
|
"******************************************************************************
|
|
let l:threePieceOffset = 0
|
|
for [l:flags, l:string] in s:CommentDefinitions()
|
|
if l:flags =~# '[se]'
|
|
if l:flags =~# '[se].*\d' && l:flags !~# '-\d'
|
|
" Extract positive offset for the middle or end of a three-piece
|
|
" comment.
|
|
let l:threePieceOffset = matchstr(l:flags, '\d\+')
|
|
elseif l:flags =~# 's'
|
|
" Clear any offset from previous three-piece comment.
|
|
let l:threePieceOffset = 0
|
|
endif
|
|
endif
|
|
if l:flags =~# '[me]' && a:prefix ==# l:string
|
|
return l:threePieceOffset
|
|
endif
|
|
endfor
|
|
|
|
return 0
|
|
endfunction
|
|
|
|
" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax :
|