" 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 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 :