diff --git a/autoload/doge/generate.vim b/autoload/doge/generate.vim index 5e61a3dd..7430ccd0 100644 --- a/autoload/doge/generate.vim +++ b/autoload/doge/generate.vim @@ -22,28 +22,35 @@ function! doge#generate#pattern(pattern) abort return 0 endif - let l:tokens = doge#token#extract(l:curr_line, a:pattern['match'], a:pattern['match_group_names']) + let l:tokens = get(doge#token#extract(l:curr_line, a:pattern['match'], a:pattern['match_group_names']), 0) " Split the 'parameters' token value into a list. let l:params_dict = a:pattern['parameters'] - let l:parameter_match_group_name = l:params_dict['parent_match_group_name'] - let l:params = map(split(l:tokens[l:parameter_match_group_name], ','), {k, v -> trim(v)}) + let l:params = l:tokens['parameters'] " Go through each parameter, match the regex, extract the token values and " replace the 'parameters' key with the formatted version. + let l:param_tokens = doge#token#extract(l:params, l:params_dict['match'], l:params_dict['match_group_names']) let l:formatted_params = [] - for l:param in l:params - let l:param_tokens = doge#token#extract(l:param, l:params_dict['match'], l:params_dict['match_group_names']) - let l:format = doge#token#replace(l:param_tokens, l:params_dict['format']) + for l:param_token in l:param_tokens + let l:format = doge#token#replace(l:param_token, l:params_dict['format']) let l:format = join(filter(l:format, 'v:val !=# ""'), ' ') call add(l:formatted_params, l:format) endfor - let l:tokens[l:parameter_match_group_name] = l:formatted_params + let l:tokens['parameters'] = l:formatted_params " Create the comment by replacing the tokens in the template with their " corresponding values. let l:comment = [] for l:line in a:pattern['comment']['template'] + " If empty lines are present, just append them to ensure a whiteline is + " inserted rather then completely removed. This allows us to insert some + " whitelines in the comment template. + if empty(l:line) + call add(l:comment, l:line) + continue + endif + let l:line_replaced = split(doge#token#replace(l:tokens, l:line), "\n") for l:replaced in l:line_replaced call add(l:comment, l:replaced) @@ -52,29 +59,62 @@ function! doge#generate#pattern(pattern) abort " If an existing comment exists, remove it before we insert a new one. let l:old_comment_pattern = fnameescape(a:pattern['comment']['opener']) . '\_.\{-}' . fnameescape(a:pattern['comment']['closer']) . '$' - let l:old_comment_start_lnum = search(l:old_comment_pattern, 'bn') - let l:old_comment_end_lnum = search(l:old_comment_pattern, 'bne') + + if a:pattern['comment']['insert'] ==# 'below' + let l:old_comment_start_lnum = search(l:old_comment_pattern, 'n') + let l:old_comment_end_lnum = search(l:old_comment_pattern, 'ne') + let l:has_old_comment = l:old_comment_start_lnum == line('.') + 1 + else + let l:old_comment_start_lnum = search(l:old_comment_pattern, 'bn') + let l:old_comment_end_lnum = search(l:old_comment_pattern, 'bne') + let l:has_old_comment = l:old_comment_end_lnum == line('.') - 1 + endif let l:old_comment_lines_amount = l:old_comment_end_lnum - l:old_comment_start_lnum + 1 + " When deleting the old comment and inserting the new one, it might be that " some things have been added or deleted. " For example: a new parameter might have been added. " If so, we have to increment the line number as well. " The same goes for deleting but then we subtract the line number. - let l:adjusted_cursor_lnum = line('.') + (len(l:comment) - l:old_comment_lines_amount) + if a:pattern['comment']['insert'] ==# 'below' + let l:adjusted_cursor_lnum = line('.') + (1 + len(l:comment) - l:old_comment_lines_amount) + else + let l:adjusted_cursor_lnum = line('.') + (len(l:comment) - l:old_comment_lines_amount) + endif let l:cursor_pos = [0, l:adjusted_cursor_lnum, col('.'), 0] - " If the old comment ends at the line above our cursor, then remove it. - " If it returns any other number then the prev line, it belongs to another code block. - let l:has_old_comment = l:old_comment_end_lnum == line('.') - 1 if l:has_old_comment + let l:cursor_pos = getpos('.') execute(l:old_comment_start_lnum . 'd' . l:old_comment_lines_amount) + + " If we have deleted a comment that is 'below' the function expression then + " our cursor moved a line too much, so revert its position. This is the + " same for 'above' the function expression, but we land on our old position + " automatically, hence that we don't have to take that situation into + " account. + if a:pattern['comment']['insert'] ==# 'below' + call setpos('.', l:cursor_pos) + endif + endif + + " If the old comment ends at the line above or below our cursor, then remove + " it. If it returns any other number then the prev or next line, it belongs to + " another code block. + if a:pattern['comment']['insert'] ==# 'below' + let l:comment_lnum_insert_position = line('.') + let l:comment_lnum_inherited_indent = line('.') + 1 + else + let l:comment_lnum_insert_position = line('.') - 1 + let l:comment_lnum_inherited_indent = line('.') endif " Write the comment. - call append(line('.') - 1, map(l:comment, {k, line -> doge#indent#line(line('.'), line)})) + call append( + \ l:comment_lnum_insert_position, + \ map(l:comment, {k, line -> doge#indent#line(l:comment_lnum_inherited_indent, line)}) + \ ) - " Set the cursor position back at where we started. if l:has_old_comment call setpos('.', l:cursor_pos) endif diff --git a/autoload/doge/token.vim b/autoload/doge/token.vim index 49103cff..88e2c013 100644 --- a/autoload/doge/token.vim +++ b/autoload/doge/token.vim @@ -92,8 +92,11 @@ function! doge#token#replace(tokens, text) abort endfunction function! doge#token#extract(line, regex, regex_group_names) abort - let l:matches = map(matchlist(a:line, a:regex), {key, val -> trim(val)}) - let l:tokens = {} + let l:submatches = [] + call substitute(a:line, a:regex, '\=add(l:submatches, submatch(0))', 'g') + + let l:matches = map(l:submatches, {key, val -> trim(val)}) + let l:tokens = [] " We can expect a list of matches like: " ['val1', 'val2', '', 'val3'] @@ -108,10 +111,17 @@ function! doge#token#extract(line, regex, regex_group_names) abort " 'type': 'val1', " } if len(l:matches) > 0 && len(a:regex_group_names) > 0 - for l:token in a:regex_group_names - let l:index = index(a:regex_group_names, l:token) - let l:match = l:matches[l:index + 1] - let l:tokens[l:token] = l:match + for l:match in l:matches + let l:values = matchlist(l:match, a:regex) + let l:group = {} + + for l:token in a:regex_group_names + let l:group_idx = index(a:regex_group_names, l:token) + let l:token_value = l:values[l:group_idx + 1] + let l:group[l:token] = trim(l:token_value) + endfor + + call add(l:tokens, l:group) endfor endif diff --git a/ftplugin/javascript.vim b/ftplugin/javascript.vim index 804d94cc..1bc08e70 100644 --- a/ftplugin/javascript.vim +++ b/ftplugin/javascript.vim @@ -5,7 +5,7 @@ " ============================================================================== " " The javascript documentation should follow the 'jsdoc' conventions. -" @see https://jsdoc.app +" see https://jsdoc.app let s:save_cpo = &cpoptions set cpoptions&vim @@ -40,51 +40,50 @@ let b:doge_patterns = [] " denoted as '\s*{'. " " {parameters.match}: Should match at least the following scenarios: -" - arg1 -" - arg1 = 5 -" - arg1 = 'string' -" - arg1: bool -" - arg1: bool = 5 -" - arg1: Person = false -" - arg1: string = 'string' +" - param1 +" - param1 = 5 +" - param1 = 'string' +" - param1: bool +" - param1: bool = 5 +" - param1: Person = false +" - param1: string = 'string' " " Regex explanation " \m " Use magic notation. " -" ^ -" Matches the position before the first character in the string. -" " \([^,:]\+\) -" Matches a group which may contain every character besides ',' or ':'. " This group should match the parameter name. +" ------------------------------------------------------------------------ +" Matches a group which may contain every character besides ',' or ':'. " " \%(\%(\s*:\s*\([a-zA-Z_]\+\)\)\)\? +" This group +" ------------------------------------------------------------------------ " Matches an optional non-capturing group containing 1 sub-group which may " contain 1 or more of the following characters: [a-zA-Z_]. " " \%(\s*=\s*.\+\)\? -" Matches an optional and non-capturing group -" where it should match the format ' = VALUE'. " This group should match the parameter default value. -" -" $ -" Matches right after the last character in the string. +" ------------------------------------------------------------------------ +" Matches an optional and non-capturing group where it should match +" the format ' = VALUE'. call add(b:doge_patterns, { - \ 'match': '\m^function \([^(]\+\)\s*(\(.\{-}\))\s*{', - \ 'match_group_names': ['funcName', 'params'], + \ 'match': '\m^function \([^(]\+\)\s*(\(.\{-}\))\%(\s*:\s*\(.\{-}\)\)\?{', + \ 'match_group_names': ['funcName', 'parameters', 'returnType'], \ 'parameters': { - \ 'parent_match_group_name': 'params', - \ 'match': '\m^\([^,:]\+\)\%(\%(\s*:\s*\([a-zA-Z_]\+\)\)\)\?\%(\s*=\s*.\+\)\?$', + \ 'match': '\m\([^,:]\+\)\%(\%(\s*:\s*\([a-zA-Z_]\+\)\)\)\?\%(\s*=\s*[^,]\+\)\?', \ 'match_group_names': ['name', 'type'], \ 'format': ['@param', '!{{type|*}}', '{name}', 'TODO'], \ }, \ 'comment': { + \ 'insert': 'above', \ 'opener': '/**', \ 'closer': '*/', \ 'template': [ \ '/**', - \ ' * {params}', + \ ' * {parameters}', + \ '! * @return {{returnType}}: TODO', \ ' */', \ ], \ }, diff --git a/ftplugin/php.vim b/ftplugin/php.vim index 61347370..1585ffba 100644 --- a/ftplugin/php.vim +++ b/ftplugin/php.vim @@ -5,7 +5,7 @@ " ============================================================================== " " The PHP documentation should follow the 'phpdoc' conventions. -" @see https://www.phpdoc.org +" see https://www.phpdoc.org let s:save_cpo = &cpoptions set cpoptions&vim @@ -13,7 +13,9 @@ set cpoptions&vim let b:doge_patterns = [] "" +" ============================================================================== " Matches regular function expressions and class methods. +" ============================================================================== " " {match}: Should match at least the following scenarios: " - function myFunction(...) { @@ -29,17 +31,20 @@ let b:doge_patterns = [] " ^ " Matches the position before the first character in the string. " -" \%(\%(public\|private\|protected\)\s\)\? -" Match an optional and non-captured group that may -" contain the keywords: 'public', 'private' or 'protected'. +" \%(\%(public\|private\|protected\)\s*\)\? +" Match an optional and non-captured group that may contain the keywords: +" 'public', 'private' or 'protected', followed by 0 or more whitespaces, +" denoted as '\s*'. " -" \%(static\s\)\? +" \%(static\s*\)\? " Match an optional and non-captured group that may -" contain the keyword: 'static'. +" contain the keyword: 'static', followed by 0 or more whitespaces, +" denoted as '\s*'. " -" \%(final\s\)\? +" \%(final\s*\)\? " Match an optional and non-captured group that may -" contain the keyword: 'final'. +" contain the keyword: 'final', followed by 0 or more whitespaces, denoted +" as '\s*'. " " function \([^(]\+\)\s*(\(.\{-}\))\s*{ " Match two groups where group #1 is the function name, @@ -56,51 +61,55 @@ let b:doge_patterns = [] " denoted as '\s*{'. " " {parameters.match}: Should match at least the following scenarios: -" - $arg1 -" - $arg1 = FALSE -" - string $arg1 -" - string $arg1 = TRUE -" - \Lorem\Ipsum\Dor\Sit\Amet $arg1 = NULL -" +" - $param1 +" - $param1 = FALSE +" - string $param1 +" - string $param1 = TRUE +" - array $param1 = [] +" - array $param1 = array() +" - \Lorem\Ipsum\Dor\Sit\Amet $param1 = NULL +" +" \m\([a-zA-Z0-9_\\]\+\s*\)\? +" \($[a-zA-Z0-9_]\+\) +" +" \%(\s*=\s*[^,]\+\)\? " Regex explanation " \m " Use magic notation. " -" ^ -" Matches the position before the first character in the string. -" " \([a-zA-Z0-9_\\]\+\s*\)\? +" This group should match the parameter type. +" ------------------------------------------------------------------------ " Matches an optional group containing 1 or more of the following -" characters: [a-zA-Z0-9_\\] followed by 0 or more spaces. -" This group should match the typing in a parameter. +" characters: '[a-zA-Z0-9_\\]' followed by 0 or more spaces. " " \($[a-zA-Z0-9_]\+\) -" Matches a group containing the character '$' followed by 1 or more -" characters of the following: [a-zA-Z0-9_]. " This group should match the parameter name. +" ------------------------------------------------------------------------ +" Matches a group containing the character '$' followed by 1 or more +" characters of the following: '[a-zA-Z0-9_]'. " -" \%(\s*=\s*[a-zA-Z0-9_']\+\)\? -" Matches an optional and non-capturing group -" where it should match the format ' = VALUE'. +" \%(\s*=\s*[^,]\+\)\? " This group should match the parameter default value. -" -" $ -" Matches right after the last character in the string. +" ------------------------------------------------------------------------ +" Matches an optional and non-capturing group where it should match the +" format ' = VALUE'. The 'VALUE' should contain 1 or more of the following +" characters: '[^,]'. call add(b:doge_patterns, { - \ 'match': '\m^\%(\%(public\|private\|protected\)\s\)\?\%(static\s\)\?\%(final\s\)\?function \([^(]\+\)\s*(\(.\{-}\))\s*{', - \ 'match_group_names': ['funcName', 'params'], + \ 'match': '\m^\%(\%(public\|private\|protected\)\s*\)\?\%(static\s*\)\?\%(final\s*\)\?function \([^(]\+\)\s*(\(.\{-}\))\s*{', + \ 'match_group_names': ['funcName', 'parameters'], \ 'parameters': { - \ 'parent_match_group_name': 'params', - \ 'match': '\m^\([a-zA-Z0-9_\\]\+\s*\)\?\($[a-zA-Z0-9_]\+\)\%(\s*=\s*.\+\)\?$', + \ 'match': '\m\([a-zA-Z0-9_\\]\+\s*\)\?\($[a-zA-Z0-9_]\+\)\%(\s*=\s*[^,]\+\)\?', \ 'match_group_names': ['type', 'name'], \ 'format': ['@param', '{type|mixed}', '{name}', 'TODO'], \ }, \ 'comment': { + \ 'insert': 'above', \ 'opener': '/**', \ 'closer': '*/', \ 'template': [ \ '/**', - \ ' * {params}', + \ ' * {parameters}', \ ' */', \ ], \ }, diff --git a/ftplugin/python.vim b/ftplugin/python.vim new file mode 100644 index 00000000..90af7160 --- /dev/null +++ b/ftplugin/python.vim @@ -0,0 +1,124 @@ +" ============================================================================== +" Filename: python.vim +" Maintainer: Kim Koomen +" License: MIT +" ============================================================================== +" +" The python documentation should follow the 'Sphinx reST' conventions. +" see: http://daouzli.com/blog/docstring.html#restructuredtext + +let s:save_cpo = &cpoptions +set cpoptions&vim + +let b:doge_patterns = [] + +"" +" ============================================================================== +" Matches regular function expressions and class methods. +" ============================================================================== +" +" {match}: Should match at least the following scenarios: +" - def myFunc(...): +" - def myFunc(...) -> None: +" - def myFunc(...) -> Sequence[T]: +" - def myFunc(...) -> Generator[int, float, str] +" +" Regex explanation +" \m +" Use magic notation. +" +" ^ +" Matches the position before the first character in the string. +" +" def \([^(]\+\)\s* +" This group should match the function name. +" ------------------------------------------------------------------------ +" Matches the word 'def', followed by a captured group, followed by some +" additional whitespace. The group uses the pattern '[^(]\+' to allow any +" character until a '(' character is found. Followed by 0 or more spaces, +" denoted as '\s*'. +" +" (\(.\{-}\)) +" This group should match the parameters. +" ------------------------------------------------------------------------ +" Matches a single captured group which matches any character, but as few +" as possible, denoted as '.\{-}'. We use \{-} to ensure it will match as +" few matches as possible, which prevents wrong parsing when the input +" contains nested functions. +" +" \%(\s*->\s*\(.\{-}\)\)\?\s*: +" The captured group should match the return type. +" ------------------------------------------------------------------------ +" Matches a non-captured group containing a single captured group which +" matches any character, but as few as possible, denoted as '.\{-}', +" followed by 0 or more spaces and a colon, denoted as '\s*:'. +" +" {parameters.match}: Should match at least the following scenarios: +" - param1 +" - param1: float +" - param1 = None +" - param1: str = 'string' +" - param1: Callable[[int], None] +" - param1: Callable[[int], None] = {} +" - param1: Callable[[int], None] = True +" - param1: Callable[[int], None] = False +" - param1: Callable[[int, Exception], None] +" - param1: Sequence[T] +" +" Regex explanation +" \m +" Use magic notation. +" +" \([a-zA-Z0-9_]\+\) +" This group should match the parameter name. +" ------------------------------------------------------------------------ +" Matches a captured group containing 1 or more of the following +" characters: '[a-zA-Z0-9_]'. +" +" \%(:\s*\([a-zA-Z0-9_]\+\%(\[[a-zA-Z0-9_\[\], ]\+\]\)\?\)\)\? +" This group should match the parameter type. +" ------------------------------------------------------------------------ +" Matches an optional and non-capturing group, denoted as \%( ... \)\? +" which should start with the character ':' followed by 0 or more spaces, +" followed by the type itself, which is denoted as: +" +" \([a-zA-Z0-9_]\+\%(\[[a-zA-Z0-9_\[\], ]\+\]\)\?\) +" +" [a-zA-Z0-9_]\+ +" This will match words as: 'str', 'int', etc. +" +" \%(\[[a-zA-Z0-9_\[\], ]\+\]\)\? +" This will match an optional and non-capturing group which is used +" for typings like: 'Callable[[int, Exception], None]'. +" +" \%(\s*=\s*\([^,]\+\)\)\? +" This group should match the parameter default value. +" ------------------------------------------------------------------------ +" Matches an optional and non-capturing group, denoted as \%( ... \)\? +" which may contain the pattern ' = VALUE'. The 'VALUE' should contain 1 +" or more of the following characters: '[^,]'. +call add(b:doge_patterns, { + \ 'match': '\m^def \([^(]\+\)\s*(\(.\{-}\))\%(\s*->\s*\(.\{-}\)\)\?\s*:', + \ 'match_group_names': ['funcName', 'parameters', 'returnType'], + \ 'parameters': { + \ 'match': '\m\([a-zA-Z0-9_]\+\)\%(:\s*\([a-zA-Z0-9_]\+\%(\[[a-zA-Z0-9_\[\], ]\+\]\)\?\)\)\?\%(\s*=\s*\([^,]\+\)\)\?', + \ 'match_group_names': ['name', 'type', 'default'], + \ 'format': [':param', '{name}', '{type|any}:', 'TODO'], + \ }, + \ 'comment': { + \ 'insert': 'below', + \ 'opener': '"""', + \ 'closer': '"""', + \ 'template': [ + \ '"""', + \ 'TODO', + \ '', + \ '{parameters}', + \ '!:rtype {returnType}: TODO', + \ '"""', + \ ], + \ }, + \ }) + +let &cpoptions = s:save_cpo +unlet s:save_cpo diff --git a/tests/javascript/test.js b/tests/javascript/test.js index edc7e2e9..b2541804 100644 --- a/tests/javascript/test.js +++ b/tests/javascript/test.js @@ -8,9 +8,10 @@ // ============================================================================= /** - * @param {*} arg1 TODO + * @param {string} arg1 TODO + * @param {int} arg2 TODO */ -function myFunc(arg1) { +function myFunc(arg1: string = 'lol', arg2: int = 2) { var a = 2; } @@ -45,6 +46,11 @@ const myObj = { // ============================================================================= // Flow js (can be used e.g. in React projects) // ============================================================================= +/** + * @param {any} one TODO + * @param {any} two TODO + * @return {number}: TODO + */ function add(one: any, two: any = 'default'): number { // } @@ -65,6 +71,9 @@ function f(x: number, y?: number) {} export function configureStore(history: History, initialState: object): Store {} +/** + * @return {1 | 2 | 3 | 4 | 5 | 6}: TODO + */ function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {} function pluck(o: T, names: K[]): T[K][] {} diff --git a/tests/php/test.php b/tests/php/test.php index 9420c919..790c537b 100644 --- a/tests/php/test.php +++ b/tests/php/test.php @@ -55,9 +55,9 @@ public static function myPublicStaticMethod(array $arg1, \Test\Namespacing\With\ * @param \Test\Namespacing\With\A\ClassInterface $arg2 TODO * @param int $arg3 TODO * @param mixed $arg4 TODO - * @param mixed $arg5 TODO + * @param array $arg5 TODO */ - public static final function myPublicStaticFinalMethod(array $arg1, \Test\Namespacing\With\A\ClassInterface $arg2, int $arg3, $arg4, $arg5 = array()) { + public static final function myPublicStaticFinalMethod(array $arg1, \Test\Namespacing\With\A\ClassInterface $arg2, int $arg3, $arg4 = [], array $arg5 = array()) { // } diff --git a/tests/python/test.py b/tests/python/test.py new file mode 100644 index 00000000..90472dc9 --- /dev/null +++ b/tests/python/test.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim: fenc=utf-8 ts=4 sw=4 et + + +def myFunc(param1: str = 'string', param2: int = 5): + """ + TODO + + :param param1 str: TODO + :param param2 int: TODO + """ + pass + +def myFunc(param1: Callable[[int], None] = {}) -> None: + """ + TODO + + :param param1 Callable[[int], None]: TODO + :rtype None: TODO + """ + pass + +def myFunc(param1: Callable[[int], None] = False, param2: Callable[[int, Exception], None]) -> Sequence[T]: + """ + TODO + + :param param1 Callable[[int], None]: TODO + :param param2 Callable[[int, Exception], None]: TODO + :rtype Sequence[T]: TODO + """ + pass + +def myFunc(param1: int = 5, param2: str = 'string', param3: bool = True, param4: Callable[[int, Exception], None]) -> float: + """ + TODO + + :param param1 int: TODO + :param param2 str: TODO + :param param3 bool: TODO + :param param4 Callable[[int, Exception], None]: TODO + :rtype float: TODO + """ + pass + +def myFunc(param1: Sequence[T]) -> Generator[int, float, str]: + """ + TODO + + :param param1 Sequence[T]: TODO + :rtype Generator[int, float, str]: TODO + """ + pass + +class MyClass(object): + + """Docstring for MyClass. """ + + def __init__(self: MyClass): + """ + TODO + + :param self MyClass: TODO + """ + pass + + def myMethod(self: MyClass, param1: Sequence[T]) -> Generator[int, float, str]: + """ + TODO + + :param self MyClass: TODO + :param param1 Sequence[T]: TODO + :rtype Generator[int, float, str]: TODO + """ + pass + + def call(self, *args: str, **kwargs: str) -> str: + """ + TODO + + :param self any: TODO + :param args str: TODO + :param kwargs str: TODO + :rtype str: TODO + """ + pass