1230 lines
40 KiB
Python
1230 lines
40 KiB
Python
"""
|
|
DocBlockr v2.10.0
|
|
by Nick Fisher
|
|
https://github.com/spadgos/sublime-jsdocs
|
|
"""
|
|
import sublime
|
|
import sublime_plugin
|
|
import re
|
|
from functools import reduce
|
|
|
|
|
|
def read_line(view, point):
|
|
if (point >= view.size()):
|
|
return
|
|
|
|
next_line = view.line(point)
|
|
return view.substr(next_line)
|
|
|
|
|
|
def write(view, str):
|
|
view.run_command(
|
|
'insert_snippet', {
|
|
'contents': str
|
|
}
|
|
)
|
|
|
|
|
|
def counter():
|
|
count = 0
|
|
while True:
|
|
count += 1
|
|
yield(count)
|
|
|
|
|
|
def escape(str):
|
|
return str.replace('$', '\$').replace('{', '\{').replace('}', '\}')
|
|
|
|
|
|
def is_numeric(val):
|
|
try:
|
|
float(val)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def getParser(view):
|
|
scope = view.scope_name(view.sel()[0].end())
|
|
res = re.search('\\bsource\\.([a-z+\-]+)', scope)
|
|
sourceLang = res.group(1) if res else 'js'
|
|
viewSettings = view.settings()
|
|
|
|
if sourceLang == "php":
|
|
return JsdocsPHP(viewSettings)
|
|
elif sourceLang == "coffee":
|
|
return JsdocsCoffee(viewSettings)
|
|
elif sourceLang == "actionscript" or sourceLang == 'haxe':
|
|
return JsdocsActionscript(viewSettings)
|
|
elif sourceLang == "c++" or sourceLang == 'c' or sourceLang == 'cuda-c++':
|
|
return JsdocsCPP(viewSettings)
|
|
elif sourceLang == 'objc' or sourceLang == 'objc++':
|
|
return JsdocsObjC(viewSettings)
|
|
elif sourceLang == 'java':
|
|
return JsdocsJava(viewSettings)
|
|
return JsdocsJavascript(viewSettings)
|
|
|
|
|
|
class JsdocsCommand(sublime_plugin.TextCommand):
|
|
|
|
def run(self, edit, inline=False):
|
|
|
|
self.initialize(self.view, inline)
|
|
|
|
# erase characters in the view (will be added to the output later)
|
|
self.view.erase(edit, self.trailingRgn)
|
|
|
|
out = None
|
|
|
|
if self.parser.isExistingComment(self.line):
|
|
write(self.view, "\n *" + self.indentSpaces)
|
|
return
|
|
|
|
# match against a function declaration.
|
|
out = self.parser.parse(self.line)
|
|
|
|
snippet = self.generateSnippet(out, inline)
|
|
|
|
write(self.view, snippet)
|
|
|
|
def initialize(self, v, inline=False):
|
|
point = v.sel()[0].end()
|
|
|
|
self.settings = v.settings()
|
|
|
|
# trailing characters are put inside the body of the comment
|
|
self.trailingRgn = sublime.Region(point, v.line(point).end())
|
|
self.trailingString = v.substr(self.trailingRgn).strip()
|
|
# drop trailing '*/'
|
|
self.trailingString = escape(re.sub('\\s*\\*\\/\\s*$', '', self.trailingString))
|
|
|
|
self.indentSpaces = " " * max(0, self.settings.get("jsdocs_indentation_spaces", 1))
|
|
self.prefix = "*"
|
|
|
|
settingsAlignTags = self.settings.get("jsdocs_align_tags", 'deep')
|
|
self.deepAlignTags = settingsAlignTags == 'deep'
|
|
self.shallowAlignTags = settingsAlignTags in ('shallow', True)
|
|
|
|
self.parser = parser = getParser(v)
|
|
parser.inline = inline
|
|
|
|
# use trailing string as a description of the function
|
|
if self.trailingString:
|
|
parser.setNameOverride(self.trailingString)
|
|
|
|
# read the next line
|
|
self.line = parser.getDefinition(v, point + 1)
|
|
|
|
def generateSnippet(self, out, inline=False):
|
|
# align the tags
|
|
if out and (self.shallowAlignTags or self.deepAlignTags) and not inline:
|
|
out = self.alignTags(out)
|
|
|
|
# fix all the tab stops so they're consecutive
|
|
if out:
|
|
out = self.fixTabStops(out)
|
|
|
|
if inline:
|
|
if out:
|
|
return " " + out[0] + " */"
|
|
else:
|
|
return " $0 */"
|
|
else:
|
|
return self.createSnippet(out)
|
|
|
|
def alignTags(self, out):
|
|
def outputWidth(str):
|
|
# get the length of a string, after it is output as a snippet,
|
|
# "${1:foo}" --> 3
|
|
return len(re.sub("[$][{]\\d+:([^}]+)[}]", "\\1", str).replace('\$', '$'))
|
|
|
|
# count how many columns we have
|
|
maxCols = 0
|
|
# this is a 2d list of the widths per column per line
|
|
widths = []
|
|
|
|
# Skip the return tag if we're faking "per-section" indenting.
|
|
lastItem = len(out)
|
|
if (self.settings.get('jsdocs_per_section_indent')):
|
|
if (self.settings.get('jsdocs_return_tag') in out[-1]):
|
|
lastItem -= 1
|
|
|
|
# skip the first one, since that's always the "description" line
|
|
for line in out[1:lastItem]:
|
|
widths.append(list(map(outputWidth, line.split(" "))))
|
|
maxCols = max(maxCols, len(widths[-1]))
|
|
|
|
# initialise a list to 0
|
|
maxWidths = [0] * maxCols
|
|
|
|
if (self.shallowAlignTags):
|
|
maxCols = 1
|
|
|
|
for i in range(0, maxCols):
|
|
for width in widths:
|
|
if (i < len(width)):
|
|
maxWidths[i] = max(maxWidths[i], width[i])
|
|
|
|
# Convert to a dict so we can use .get()
|
|
maxWidths = dict(enumerate(maxWidths))
|
|
|
|
# Minimum spaces between line columns
|
|
minColSpaces = self.settings.get('jsdocs_min_spaces_between_columns', 1)
|
|
|
|
for index, line in enumerate(out):
|
|
if (index > 0):
|
|
newOut = []
|
|
for partIndex, part in enumerate(line.split(" ")):
|
|
newOut.append(part)
|
|
newOut.append(" " * minColSpaces + (" " * (maxWidths.get(partIndex, 0) - outputWidth(part))))
|
|
out[index] = "".join(newOut).strip()
|
|
return out
|
|
|
|
def fixTabStops(self, out):
|
|
tabIndex = counter()
|
|
|
|
def swapTabs(m):
|
|
return "%s%d%s" % (m.group(1), next(tabIndex), m.group(2))
|
|
|
|
for index, outputLine in enumerate(out):
|
|
out[index] = re.sub("(\\$\\{)\\d+(:[^}]+\\})", swapTabs, outputLine)
|
|
|
|
return out
|
|
|
|
def createSnippet(self, out):
|
|
snippet = ""
|
|
closer = self.parser.settings['commentCloser']
|
|
if out:
|
|
if self.settings.get('jsdocs_spacer_between_sections'):
|
|
lastTag = None
|
|
for idx, line in enumerate(out):
|
|
res = re.match("^\\s*@([a-zA-Z]+)", line)
|
|
if res and (lastTag != res.group(1)):
|
|
lastTag = res.group(1)
|
|
out.insert(idx, "")
|
|
for line in out:
|
|
snippet += "\n " + self.prefix + (self.indentSpaces + line if line else "")
|
|
else:
|
|
snippet += "\n " + self.prefix + self.indentSpaces + "${0:" + self.trailingString + '}'
|
|
|
|
snippet += "\n" + closer
|
|
return snippet
|
|
|
|
|
|
class JsdocsParser(object):
|
|
|
|
def __init__(self, viewSettings):
|
|
self.viewSettings = viewSettings
|
|
self.setupSettings()
|
|
self.nameOverride = None
|
|
|
|
def isExistingComment(self, line):
|
|
return re.search('^\\s*\\*', line)
|
|
|
|
def setNameOverride(self, name):
|
|
""" overrides the description of the function - used instead of parsed description """
|
|
self.nameOverride = name
|
|
|
|
def getNameOverride(self):
|
|
return self.nameOverride
|
|
|
|
def parse(self, line):
|
|
out = self.parseFunction(line) # (name, args, retval, options)
|
|
if (out):
|
|
return self.formatFunction(*out)
|
|
|
|
out = self.parseVar(line)
|
|
if out:
|
|
return self.formatVar(*out)
|
|
|
|
return None
|
|
|
|
def formatVar(self, name, val):
|
|
out = []
|
|
if not val or val == '': # quick short circuit
|
|
valType = "[type]"
|
|
else:
|
|
valType = self.guessTypeFromValue(val) or self.guessTypeFromName(name) or "[type]"
|
|
|
|
if self.inline:
|
|
out.append("@%s %s${1:%s}%s ${1:[description]}" % (
|
|
self.settings['typeTag'],
|
|
"{" if self.settings['curlyTypes'] else "",
|
|
valType,
|
|
"}" if self.settings['curlyTypes'] else ""
|
|
))
|
|
else:
|
|
out.append("${1:[%s description]}" % (escape(name)))
|
|
out.append("@%s %s${1:%s}%s" % (
|
|
self.settings['typeTag'],
|
|
"{" if self.settings['curlyTypes'] else "",
|
|
valType,
|
|
"}" if self.settings['curlyTypes'] else ""
|
|
))
|
|
|
|
return out
|
|
|
|
def getTypeInfo(self, argType, argName):
|
|
typeInfo = ''
|
|
if self.settings['typeInfo']:
|
|
typeInfo = '%s${1:%s}%s ' % (
|
|
"{" if self.settings['curlyTypes'] else "",
|
|
escape(argType or self.guessTypeFromName(argName) or "[type]"),
|
|
"}" if self.settings['curlyTypes'] else "",
|
|
)
|
|
|
|
return typeInfo
|
|
|
|
def formatFunction(self, name, args, retval, options={}):
|
|
out = []
|
|
if 'as_setter' in options:
|
|
out.append('@private')
|
|
return out
|
|
|
|
description = self.getNameOverride() or ('[%s description]' % escape(name))
|
|
out.append("${1:%s}" % description)
|
|
|
|
if (self.viewSettings.get("jsdocs_autoadd_method_tag") == True):
|
|
out.append("@%s %s" % (
|
|
"method",
|
|
escape(name)
|
|
))
|
|
|
|
self.addExtraTags(out)
|
|
|
|
# if there are arguments, add a @param for each
|
|
if (args):
|
|
# remove comments inside the argument list.
|
|
args = re.sub("/\*.*?\*/", '', args)
|
|
for argType, argName in self.parseArgs(args):
|
|
typeInfo = self.getTypeInfo(argType, argName)
|
|
|
|
format_str = "@param %s%s"
|
|
if (self.viewSettings.get('jsdocs_param_description')):
|
|
format_str += " ${1:[description]}"
|
|
|
|
out.append(format_str % (
|
|
typeInfo,
|
|
escape(argName)
|
|
))
|
|
|
|
# return value type might be already available in some languages but
|
|
# even then ask language specific parser if it wants it listed
|
|
retType = self.getFunctionReturnType(name, retval)
|
|
if retType is not None:
|
|
typeInfo = ''
|
|
if self.settings['typeInfo']:
|
|
typeInfo = ' %s${1:%s}%s' % (
|
|
"{" if self.settings['curlyTypes'] else "",
|
|
retType or "[type]",
|
|
"}" if self.settings['curlyTypes'] else ""
|
|
)
|
|
format_args = [
|
|
self.viewSettings.get('jsdocs_return_tag') or '@return',
|
|
typeInfo
|
|
]
|
|
|
|
if (self.viewSettings.get('jsdocs_return_description')):
|
|
format_str = "%s%s %s${1:[description]}"
|
|
third_arg = ""
|
|
|
|
# the extra space here is so that the description will align with the param description
|
|
if args and self.viewSettings.get('jsdocs_align_tags') == 'deep':
|
|
if not self.viewSettings.get('jsdocs_per_section_indent'):
|
|
third_arg = " "
|
|
|
|
format_args.append(third_arg)
|
|
else:
|
|
format_str = "%s%s"
|
|
|
|
out.append(format_str % tuple(format_args))
|
|
|
|
for notation in self.getMatchingNotations(name):
|
|
if 'tags' in notation:
|
|
out.extend(notation['tags'])
|
|
|
|
return out
|
|
|
|
def getFunctionReturnType(self, name, retval):
|
|
""" returns None for no return type. False meaning unknown, or a string """
|
|
|
|
if re.match("[A-Z]", name):
|
|
# no return, but should add a class
|
|
return None
|
|
|
|
if re.match('[$_]?(?:set|add)($|[A-Z_])', name):
|
|
# setter/mutator, no return
|
|
return None
|
|
|
|
if re.match('[$_]?(?:is|has)($|[A-Z_])', name): # functions starting with 'is' or 'has'
|
|
return self.settings['bool']
|
|
|
|
return self.guessTypeFromName(name) or False
|
|
|
|
def parseArgs(self, args):
|
|
""" an array of tuples, the first being the best guess at the type, the second being the name """
|
|
out = []
|
|
|
|
if not args:
|
|
return out
|
|
|
|
for arg in re.split('\s*,\s*', args):
|
|
arg = arg.strip()
|
|
out.append((self.getArgType(arg), self.getArgName(arg)))
|
|
return out
|
|
|
|
def getArgType(self, arg):
|
|
return None
|
|
|
|
def getArgName(self, arg):
|
|
return arg
|
|
|
|
def addExtraTags(self, out):
|
|
extraTags = self.viewSettings.get('jsdocs_extra_tags', [])
|
|
if (len(extraTags) > 0):
|
|
out.extend(extraTags)
|
|
|
|
def guessTypeFromName(self, name):
|
|
matches = self.getMatchingNotations(name)
|
|
if len(matches):
|
|
rule = matches[0]
|
|
if ('type' in rule):
|
|
return self.settings[rule['type']] if rule['type'] in self.settings else rule['type']
|
|
|
|
if (re.match("(?:is|has)[A-Z_]", name)):
|
|
return self.settings['bool']
|
|
|
|
if (re.match("^(?:cb|callback|done|next|fn)$", name)):
|
|
return self.settings['function']
|
|
|
|
return False
|
|
|
|
def getMatchingNotations(self, name):
|
|
def checkMatch(rule):
|
|
if 'prefix' in rule:
|
|
regex = re.escape(rule['prefix'])
|
|
if re.match('.*[a-z]', rule['prefix']):
|
|
regex += '(?:[A-Z_]|$)'
|
|
return re.match(regex, name)
|
|
elif 'regex' in rule:
|
|
return re.search(rule['regex'], name)
|
|
|
|
return list(filter(checkMatch, self.viewSettings.get('jsdocs_notation_map', [])))
|
|
|
|
def getDefinition(self, view, pos):
|
|
"""
|
|
get a relevant definition starting at the given point
|
|
returns string
|
|
"""
|
|
maxLines = 25 # don't go further than this
|
|
openBrackets = 0
|
|
|
|
definition = ''
|
|
|
|
def countBrackets(total, bracket):
|
|
return total + (1 if bracket == '(' else -1)
|
|
|
|
for i in range(0, maxLines):
|
|
line = read_line(view, pos)
|
|
if line is None:
|
|
break
|
|
|
|
pos += len(line) + 1
|
|
# strip comments
|
|
line = re.sub("//.*", "", line)
|
|
line = re.sub(r"/\*.*\*/", "", line)
|
|
if definition == '':
|
|
if not self.settings['fnOpener'] or not re.search(self.settings['fnOpener'], line):
|
|
definition = line
|
|
break
|
|
definition += line
|
|
openBrackets = reduce(countBrackets, re.findall('[()]', line), openBrackets)
|
|
if openBrackets == 0:
|
|
break
|
|
return definition
|
|
|
|
|
|
class JsdocsJavascript(JsdocsParser):
|
|
def setupSettings(self):
|
|
identifier = '[a-zA-Z_$][a-zA-Z_$0-9]*'
|
|
self.settings = {
|
|
# curly brackets around the type information
|
|
"curlyTypes": True,
|
|
'typeInfo': True,
|
|
"typeTag": "type",
|
|
# technically, they can contain all sorts of unicode, but w/e
|
|
"varIdentifier": identifier,
|
|
"fnIdentifier": identifier,
|
|
"fnOpener": 'function(?:\\s+' + identifier + ')?\\s*\\(',
|
|
"commentCloser": " */",
|
|
"bool": "Boolean",
|
|
"function": "Function"
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
res = re.search(
|
|
# fnName = function, fnName : function
|
|
'(?:(?P<name1>' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?'
|
|
+ 'function'
|
|
# function fnName
|
|
+ '(?:\s+(?P<name2>' + self.settings['fnIdentifier'] + '))?'
|
|
# (arg1, arg2)
|
|
+ '\s*\(\s*(?P<args>.*)\)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
# grab the name out of "name1 = function name2(foo)" preferring name1
|
|
name = res.group('name1') or res.group('name2') or ''
|
|
args = res.group('args')
|
|
|
|
return (name, args, None)
|
|
|
|
def parseVar(self, line):
|
|
res = re.search(
|
|
# var foo = blah,
|
|
# foo = blah;
|
|
# baz.foo = blah;
|
|
# baz = {
|
|
# foo : blah
|
|
# }
|
|
|
|
'(?P<name>' + self.settings['varIdentifier'] + ')\s*[=:]\s*(?P<val>.*?)(?:[;,]|$)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
return (res.group('name'), res.group('val').strip())
|
|
|
|
def guessTypeFromValue(self, val):
|
|
if is_numeric(val):
|
|
return "Number"
|
|
if val[0] == '"' or val[0] == "'":
|
|
return "String"
|
|
if val[0] == '[':
|
|
return "Array"
|
|
if val[0] == '{':
|
|
return "Object"
|
|
if val == 'true' or val == 'false':
|
|
return 'Boolean'
|
|
if re.match('RegExp\\b|\\/[^\\/]', val):
|
|
return 'RegExp'
|
|
if val[:4] == 'new ':
|
|
res = re.search('new (' + self.settings['fnIdentifier'] + ')', val)
|
|
return res and res.group(1) or None
|
|
return None
|
|
|
|
|
|
class JsdocsPHP(JsdocsParser):
|
|
def setupSettings(self):
|
|
nameToken = '[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*'
|
|
self.settings = {
|
|
# curly brackets around the type information
|
|
'curlyTypes': False,
|
|
'typeInfo': True,
|
|
'typeTag': "var",
|
|
'varIdentifier': '[$]' + nameToken + '(?:->' + nameToken + ')*',
|
|
'fnIdentifier': nameToken,
|
|
'fnOpener': 'function(?:\\s+' + nameToken + ')?\\s*\\(',
|
|
'commentCloser': ' */',
|
|
'bool': "boolean",
|
|
'function': "function"
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
res = re.search(
|
|
'function\\s+&?(?:\\s+)?'
|
|
+ '(?P<name>' + self.settings['fnIdentifier'] + ')'
|
|
# function fnName
|
|
# (arg1, arg2)
|
|
+ '\\s*\\(\\s*(?P<args>.*)\)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
return (res.group('name'), res.group('args'), None)
|
|
|
|
def getArgType(self, arg):
|
|
# function add($x, $y = 1)
|
|
res = re.search(
|
|
'(?P<name>' + self.settings['varIdentifier'] + ")\\s*=\\s*(?P<val>.*)",
|
|
arg
|
|
)
|
|
if res:
|
|
return self.guessTypeFromValue(res.group('val'))
|
|
|
|
# function sum(Array $x)
|
|
if re.search('\\S\\s', arg):
|
|
return re.search("^(\\S+)", arg).group(1)
|
|
else:
|
|
return None
|
|
|
|
def getArgName(self, arg):
|
|
return re.search("(" + self.settings['varIdentifier'] + ")(?:\\s*=.*)?$", arg).group(1)
|
|
|
|
def parseVar(self, line):
|
|
res = re.search(
|
|
# var $foo = blah,
|
|
# $foo = blah;
|
|
# $baz->foo = blah;
|
|
# $baz = array(
|
|
# 'foo' => blah
|
|
# )
|
|
|
|
'(?P<name>' + self.settings['varIdentifier'] + ')\\s*=>?\\s*(?P<val>.*?)(?:[;,]|$)',
|
|
line
|
|
)
|
|
if res:
|
|
return (res.group('name'), res.group('val').strip())
|
|
|
|
res = re.search(
|
|
'\\b(?:var|public|private|protected|static)\\s+(?P<name>' + self.settings['varIdentifier'] + ')',
|
|
line
|
|
)
|
|
if res:
|
|
return (res.group('name'), None)
|
|
|
|
return None
|
|
|
|
def guessTypeFromValue(self, val):
|
|
if is_numeric(val):
|
|
return "float" if '.' in val else "integer"
|
|
if val[0] == '"' or val[0] == "'":
|
|
return "string"
|
|
if val[:5] == 'array':
|
|
return "array"
|
|
if val.lower() in ('true', 'false', 'filenotfound'):
|
|
return 'boolean'
|
|
if val[:4] == 'new ':
|
|
res = re.search('new (' + self.settings['fnIdentifier'] + ')', val)
|
|
return res and res.group(1) or None
|
|
return None
|
|
|
|
def getFunctionReturnType(self, name, retval):
|
|
if (name[:2] == '__'):
|
|
if name in ('__construct', '__destruct', '__set', '__unset', '__wakeup'):
|
|
return None
|
|
if name == '__sleep':
|
|
return 'array'
|
|
if name == '__toString':
|
|
return 'string'
|
|
if name == '__isset':
|
|
return 'boolean'
|
|
return JsdocsParser.getFunctionReturnType(self, name, retval)
|
|
|
|
|
|
class JsdocsCPP(JsdocsParser):
|
|
def setupSettings(self):
|
|
nameToken = '[a-zA-Z_][a-zA-Z0-9_]*'
|
|
identifier = '(%s)(::%s)?' % (nameToken, nameToken)
|
|
self.settings = {
|
|
'typeInfo': False,
|
|
'curlyTypes': False,
|
|
'typeTag': 'param',
|
|
'commentCloser': ' */',
|
|
'fnIdentifier': identifier,
|
|
'varIdentifier': identifier,
|
|
'fnOpener': identifier + '\\s+' + identifier + '\\s*\\(',
|
|
'bool': 'bool',
|
|
'function': 'function'
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
res = re.search(
|
|
'(?P<retval>' + self.settings['varIdentifier'] + ')[&*\\s]+'
|
|
+ '(?P<name>' + self.settings['varIdentifier'] + ')'
|
|
# void fnName
|
|
# (arg1, arg2)
|
|
+ '\\s*\\(\\s*(?P<args>.*)\)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
return (res.group('name'), res.group('args'), res.group('retval'))
|
|
|
|
def parseArgs(self, args):
|
|
if args.strip() == 'void':
|
|
return []
|
|
return super(JsdocsCPP, self).parseArgs(args)
|
|
|
|
def getArgType(self, arg):
|
|
return None
|
|
|
|
def getArgName(self, arg):
|
|
return re.search("(" + self.settings['varIdentifier'] + r")(?:\s*\[\s*\])?(?:\s*=.*)?$", arg).group(1)
|
|
|
|
def parseVar(self, line):
|
|
return None
|
|
|
|
def guessTypeFromValue(self, val):
|
|
return None
|
|
|
|
def getFunctionReturnType(self, name, retval):
|
|
return retval if retval != 'void' else None
|
|
|
|
|
|
class JsdocsCoffee(JsdocsParser):
|
|
def setupSettings(self):
|
|
identifier = '[a-zA-Z_$][a-zA-Z_$0-9]*'
|
|
self.settings = {
|
|
# curly brackets around the type information
|
|
'curlyTypes': True,
|
|
'typeTag': "type",
|
|
'typeInfo': True,
|
|
# technically, they can contain all sorts of unicode, but w/e
|
|
'varIdentifier': identifier,
|
|
'fnIdentifier': identifier,
|
|
'fnOpener': None, # no multi-line function definitions for you, hipsters!
|
|
'commentCloser': '###',
|
|
'bool': 'Boolean',
|
|
'function': 'Function'
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
res = re.search(
|
|
# fnName = function, fnName : function
|
|
'(?:(?P<name>' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?'
|
|
+ '(?:\\((?P<args>[^()]*?)\\))?\\s*([=-]>)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
# grab the name out of "name1 = function name2(foo)" preferring name1
|
|
name = res.group('name') or ''
|
|
args = res.group('args')
|
|
|
|
return (name, args, None)
|
|
|
|
def parseVar(self, line):
|
|
res = re.search(
|
|
# var foo = blah,
|
|
# foo = blah;
|
|
# baz.foo = blah;
|
|
# baz = {
|
|
# foo : blah
|
|
# }
|
|
|
|
'(?P<name>' + self.settings['varIdentifier'] + ')\s*[=:]\s*(?P<val>.*?)(?:[;,]|$)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
return (res.group('name'), res.group('val').strip())
|
|
|
|
def guessTypeFromValue(self, val):
|
|
if is_numeric(val):
|
|
return "Number"
|
|
if val[0] == '"' or val[0] == "'":
|
|
return "String"
|
|
if val[0] == '[':
|
|
return "Array"
|
|
if val[0] == '{':
|
|
return "Object"
|
|
if val == 'true' or val == 'false':
|
|
return 'Boolean'
|
|
if re.match('RegExp\\b|\\/[^\\/]', val):
|
|
return 'RegExp'
|
|
if val[:4] == 'new ':
|
|
res = re.search('new (' + self.settings['fnIdentifier'] + ')', val)
|
|
return res and res.group(1) or None
|
|
return None
|
|
|
|
|
|
class JsdocsActionscript(JsdocsParser):
|
|
|
|
def setupSettings(self):
|
|
nameToken = '[a-zA-Z_][a-zA-Z0-9_]*'
|
|
self.settings = {
|
|
'typeInfo': False,
|
|
'curlyTypes': False,
|
|
'typeTag': '',
|
|
'commentCloser': ' */',
|
|
'fnIdentifier': nameToken,
|
|
'varIdentifier': '(%s)(?::%s)?' % (nameToken, nameToken),
|
|
'fnOpener': 'function(?:\\s+[gs]et)?(?:\\s+' + nameToken + ')?\\s*\\(',
|
|
'bool': 'bool',
|
|
'function': 'function'
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
res = re.search(
|
|
# fnName = function, fnName : function
|
|
'(?:(?P<name1>' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?'
|
|
+ 'function(?:\s+(?P<getset>[gs]et))?'
|
|
# function fnName
|
|
+ '(?:\s+(?P<name2>' + self.settings['fnIdentifier'] + '))?'
|
|
# (arg1, arg2)
|
|
+ '\s*\(\s*(?P<args>.*)\)',
|
|
line
|
|
)
|
|
if not res:
|
|
return None
|
|
|
|
name = res.group('name1') and re.sub(self.settings['varIdentifier'], r'\1', res.group('name1')) \
|
|
or res.group('name2') \
|
|
or ''
|
|
|
|
args = res.group('args')
|
|
options = {}
|
|
if res.group('getset') == 'set':
|
|
options['as_setter'] = True
|
|
|
|
return (name, args, None, options)
|
|
|
|
def parseVar(self, line):
|
|
return None
|
|
|
|
def getArgName(self, arg):
|
|
return re.sub(self.settings['varIdentifier'] + r'(\s*=.*)?', r'\1', arg)
|
|
|
|
def getArgType(self, arg):
|
|
# could actually figure it out easily, but it's not important for the documentation
|
|
return None
|
|
|
|
|
|
class JsdocsObjC(JsdocsParser):
|
|
|
|
def setupSettings(self):
|
|
identifier = '[a-zA-Z_$][a-zA-Z_$0-9]*'
|
|
self.settings = {
|
|
# curly brackets around the type information
|
|
"curlyTypes": True,
|
|
'typeInfo': True,
|
|
"typeTag": "type",
|
|
# technically, they can contain all sorts of unicode, but w/e
|
|
"varIdentifier": identifier,
|
|
"fnIdentifier": identifier,
|
|
"fnOpener": '^\s*[-+]',
|
|
"commentCloser": " */",
|
|
"bool": "Boolean",
|
|
"function": "Function"
|
|
}
|
|
|
|
def getDefinition(self, view, pos):
|
|
maxLines = 25 # don't go further than this
|
|
|
|
definition = ''
|
|
for i in range(0, maxLines):
|
|
line = read_line(view, pos)
|
|
if line is None:
|
|
break
|
|
|
|
pos += len(line) + 1
|
|
# strip comments
|
|
line = re.sub("//.*", "", line)
|
|
if definition == '':
|
|
if not self.settings['fnOpener'] or not re.search(self.settings['fnOpener'], line):
|
|
definition = line
|
|
break
|
|
definition += line
|
|
if line.find(';') > -1 or line.find('{') > -1:
|
|
definition = re.sub(r'\s*[;{]\s*$', '', definition)
|
|
break
|
|
return definition
|
|
|
|
def parseFunction(self, line):
|
|
# this is terrible, don't judge me
|
|
|
|
typeRE = r'[a-zA-Z_$][a-zA-Z0-9_$]*\s*\**'
|
|
res = re.search(
|
|
'[-+]\s+\\(\\s*(?P<retval>' + typeRE + ')\\s*\\)\\s*'
|
|
+ '(?P<name>[a-zA-Z_$][a-zA-Z0-9_$]*)'
|
|
# void fnName
|
|
# (arg1, arg2)
|
|
+ '\\s*(?::(?P<args>.*))?',
|
|
line
|
|
)
|
|
if not res:
|
|
return
|
|
name = res.group('name')
|
|
argStr = res.group('args')
|
|
args = []
|
|
if argStr:
|
|
groups = re.split('\\s*:\\s*', argStr)
|
|
numGroups = len(groups)
|
|
for i in range(0, numGroups):
|
|
group = groups[i]
|
|
if i < numGroups - 1:
|
|
result = re.search(r'\s+(\S*)$', group)
|
|
name += ':' + result.group(1)
|
|
group = group[:result.start()]
|
|
|
|
args.append(group)
|
|
|
|
if (numGroups):
|
|
name += ':'
|
|
return (name, '|||'.join(args), res.group('retval'))
|
|
|
|
def parseArgs(self, args):
|
|
out = []
|
|
for arg in args.split('|||'): # lol
|
|
lastParen = arg.rfind(')')
|
|
out.append((arg[1:lastParen], arg[lastParen + 1:]))
|
|
return out
|
|
|
|
def getFunctionReturnType(self, name, retval):
|
|
return retval if retval != 'void' and retval != 'IBAction' else None
|
|
|
|
def parseVar(self, line):
|
|
return None
|
|
|
|
|
|
class JsdocsJava(JsdocsParser):
|
|
def setupSettings(self):
|
|
identifier = '[a-zA-Z_$][a-zA-Z_$0-9]*'
|
|
self.settings = {
|
|
"curlyTypes": False,
|
|
'typeInfo': False,
|
|
"typeTag": "type",
|
|
"varIdentifier": identifier,
|
|
"fnIdentifier": identifier,
|
|
"fnOpener": identifier + '(?:\\s+' + identifier + ')?\\s*\\(',
|
|
"commentCloser": " */",
|
|
"bool": "Boolean",
|
|
"function": "Function"
|
|
}
|
|
|
|
def parseFunction(self, line):
|
|
line = line.strip()
|
|
res = re.search(
|
|
# Modifiers
|
|
'(?:public|protected|private|static|abstract|final|transient|synchronized|native|strictfp){0,1}\s*'
|
|
# Return value
|
|
+ '(?P<retval>[a-zA-Z_$][\<\>\., a-zA-Z_$0-9]+)\s+'
|
|
# Method name
|
|
+ '(?P<name>' + self.settings['fnIdentifier'] + ')\s*'
|
|
# Params
|
|
+ '\((?P<args>.*)\)\s*'
|
|
# # Throws ,
|
|
+ '(?:throws){0,1}\s*(?P<throws>[a-zA-Z_$0-9\.,\s]*)',
|
|
line
|
|
)
|
|
|
|
if not res:
|
|
return None
|
|
group_dict = res.groupdict()
|
|
name = group_dict["name"]
|
|
retval = group_dict["retval"]
|
|
full_args = group_dict["args"]
|
|
throws = group_dict["throws"] or ""
|
|
|
|
arg_list = []
|
|
for arg in full_args.split(","):
|
|
arg_list.append(arg.strip().split(" ")[-1])
|
|
args = ",".join(arg_list)
|
|
throws_list = []
|
|
for arg in throws.split(","):
|
|
throws_list.append(arg.strip().split(" ")[-1])
|
|
throws = ",".join(throws_list)
|
|
return (name, args, retval, throws)
|
|
|
|
def parseVar(self, line):
|
|
return None
|
|
|
|
def guessTypeFromValue(self, val):
|
|
return None
|
|
|
|
def formatFunction(self, name, args, retval, throws_args, options={}):
|
|
out = JsdocsParser.formatFunction(self, name, args, retval, options)
|
|
|
|
if throws_args != "":
|
|
for unused, exceptionName in self.parseArgs(throws_args):
|
|
typeInfo = self.getTypeInfo(unused, exceptionName)
|
|
out.append("@throws %s%s ${1:[description]}" % (
|
|
typeInfo,
|
|
escape(exceptionName)
|
|
))
|
|
|
|
return out
|
|
|
|
def getFunctionReturnType(self, name, retval):
|
|
if retval == "void":
|
|
return None
|
|
return retval
|
|
|
|
def getDefinition(self, view, pos):
|
|
maxLines = 25 # don't go further than this
|
|
|
|
definition = ''
|
|
open_curly_annotation = False
|
|
open_paren_annotation = False
|
|
for i in xrange(0, maxLines):
|
|
line = read_line(view, pos)
|
|
if line is None:
|
|
break
|
|
|
|
pos += len(line) + 1
|
|
# Move past empty lines
|
|
if re.search("^\s*$", line):
|
|
continue
|
|
# strip comments
|
|
line = re.sub("//.*", "", line)
|
|
line = re.sub(r"/\*.*\*/", "", line)
|
|
if definition == '':
|
|
# Must check here for function opener on same line as annotation
|
|
if self.settings['fnOpener'] and re.search(self.settings['fnOpener'], line):
|
|
pass
|
|
# Handle Annotations
|
|
elif re.search("^\s*@", line):
|
|
if re.search("{", line) and not re.search("}", line):
|
|
open_curly_annotation = True
|
|
if re.search("\(", line) and not re.search("\)", line):
|
|
open_paren_annotation = True
|
|
continue
|
|
elif open_curly_annotation:
|
|
if re.search("}", line):
|
|
open_curly_annotation = False
|
|
continue
|
|
elif open_paren_annotation:
|
|
if re.search("\)", line):
|
|
open_paren_annotation = False
|
|
elif re.search("^\s*$", line):
|
|
continue
|
|
# Check for function
|
|
elif not self.settings['fnOpener'] or not re.search(self.settings['fnOpener'], line):
|
|
definition = line
|
|
break
|
|
definition += line
|
|
if line.find(';') > -1 or line.find('{') > -1:
|
|
definition = re.sub(r'\s*[;{]\s*$', '', definition)
|
|
break
|
|
return definition
|
|
|
|
############################################################33
|
|
|
|
|
|
class JsdocsIndentCommand(sublime_plugin.TextCommand):
|
|
|
|
def run(self, edit):
|
|
v = self.view
|
|
currPos = v.sel()[0].begin()
|
|
currLineRegion = v.line(currPos)
|
|
currCol = currPos - currLineRegion.begin() # which column we're currently in
|
|
prevLine = v.substr(v.line(v.line(currPos).begin() - 1))
|
|
spaces = self.getIndentSpaces(prevLine)
|
|
if spaces:
|
|
toStar = len(re.search("^(\\s*\\*)", prevLine).group(1))
|
|
toInsert = spaces - currCol + toStar
|
|
if spaces is None or toInsert <= 0:
|
|
v.run_command(
|
|
'insert_snippet', {
|
|
'contents': "\t"
|
|
}
|
|
)
|
|
return
|
|
|
|
v.insert(edit, currPos, " " * toInsert)
|
|
else:
|
|
v.insert(edit, currPos, "\t")
|
|
|
|
def getIndentSpaces(self, line):
|
|
hasTypes = getParser(self.view).settings['typeInfo']
|
|
extraIndent = '\\s+\\S+' if hasTypes else ''
|
|
res = re.search("^\\s*\\*(?P<fromStar>\\s*@(?:param|property)%s\\s+\\S+\\s+)\\S" % extraIndent, line) \
|
|
or re.search("^\\s*\\*(?P<fromStar>\\s*@(?:returns?|define)%s\\s+\\S+\\s+)\\S" % extraIndent, line) \
|
|
or re.search("^\\s*\\*(?P<fromStar>\\s*@[a-z]+\\s+)\\S", line) \
|
|
or re.search("^\\s*\\*(?P<fromStar>\\s*)", line)
|
|
if res:
|
|
return len(res.group('fromStar'))
|
|
return None
|
|
|
|
|
|
class JsdocsJoinCommand(sublime_plugin.TextCommand):
|
|
def run(self, edit):
|
|
v = self.view
|
|
for sel in v.sel():
|
|
for lineRegion in reversed(v.lines(sel)):
|
|
v.replace(edit, v.find("[ \\t]*\\n[ \\t]*((?:\\*|//|#)[ \\t]*)?", lineRegion.begin()), ' ')
|
|
|
|
|
|
class JsdocsDecorateCommand(sublime_plugin.TextCommand):
|
|
def run(self, edit):
|
|
v = self.view
|
|
re_whitespace = re.compile("^(\\s*)//")
|
|
v.run_command('expand_selection', {'to': 'scope'})
|
|
for sel in v.sel():
|
|
maxLength = 0
|
|
lines = v.lines(sel)
|
|
for lineRegion in lines:
|
|
leadingWS = len(re_whitespace.match(v.substr(lineRegion)).group(1))
|
|
maxLength = max(maxLength, lineRegion.size())
|
|
|
|
lineLength = maxLength - leadingWS
|
|
leadingWS = " " * leadingWS
|
|
v.insert(edit, sel.end(), leadingWS + "/" * (lineLength + 3) + "\n")
|
|
|
|
for lineRegion in reversed(lines):
|
|
line = v.substr(lineRegion)
|
|
rPadding = 1 + (maxLength - lineRegion.size())
|
|
v.replace(edit, lineRegion, leadingWS + line + (" " * rPadding) + "//")
|
|
# break
|
|
|
|
v.insert(edit, sel.begin(), "/" * (lineLength + 3) + "\n")
|
|
|
|
|
|
class JsdocsDeindent(sublime_plugin.TextCommand):
|
|
"""
|
|
When pressing enter at the end of a docblock, this takes the cursor back one space.
|
|
/**
|
|
*
|
|
*/| <-- from here
|
|
| <-- to here
|
|
"""
|
|
def run(self, edit):
|
|
v = self.view
|
|
lineRegion = v.line(v.sel()[0])
|
|
line = v.substr(lineRegion)
|
|
v.insert(edit, lineRegion.end(), re.sub("^(\\s*)\\s\\*/.*", "\n\\1", line))
|
|
|
|
|
|
class JsdocsReparse(sublime_plugin.TextCommand):
|
|
"""
|
|
Reparse a docblock to make the fields 'active' again, so that pressing tab will jump to the next one
|
|
"""
|
|
def run(self, edit):
|
|
tabIndex = counter()
|
|
|
|
def tabStop(m):
|
|
return "${%d:%s}" % (next(tabIndex), m.group(1))
|
|
|
|
v = self.view
|
|
v.run_command('clear_fields')
|
|
v.run_command('expand_selection', {'to': 'scope'})
|
|
sel = v.sel()[0]
|
|
|
|
# escape string, so variables starting with $ won't be removed
|
|
text = escape(v.substr(sel))
|
|
|
|
# strip out leading spaces, since inserting a snippet keeps the indentation
|
|
text = re.sub("\\n\\s+\\*", "\n *", text)
|
|
|
|
# replace [bracketed] [text] with a tabstop
|
|
text = re.sub("(\\[.+?\\])", tabStop, text)
|
|
|
|
v.erase(edit, sel)
|
|
write(v, text)
|
|
|
|
|
|
class JsdocsTrimAutoWhitespace(sublime_plugin.TextCommand):
|
|
"""
|
|
Trim the automatic whitespace added when creating a new line in a docblock.
|
|
"""
|
|
def run(self, edit):
|
|
v = self.view
|
|
lineRegion = v.line(v.sel()[0])
|
|
line = v.substr(lineRegion)
|
|
spaces = max(0, v.settings().get("jsdocs_indentation_spaces", 1))
|
|
v.replace(edit, lineRegion, re.sub("^(\\s*\\*)\\s*$", "\\1\n\\1" + (" " * spaces), line))
|
|
|
|
|
|
class JsdocsWrapLines(sublime_plugin.TextCommand):
|
|
"""
|
|
Reformat description text inside a comment block to wrap at the correct length.
|
|
Wrap column is set by the first ruler (set in Default.sublime-settings), or 80 by default.
|
|
Shortcut Key: alt+q
|
|
"""
|
|
|
|
def run(self, edit):
|
|
v = self.view
|
|
settings = v.settings()
|
|
rulers = settings.get('rulers')
|
|
tabSize = settings.get('tab_size')
|
|
|
|
wrapLength = rulers[0] or 80
|
|
numIndentSpaces = max(0, settings.get("jsdocs_indentation_spaces", 1))
|
|
indentSpaces = " " * numIndentSpaces
|
|
indentSpacesSamePara = " " * max(0, settings.get("jsdocs_indentation_spaces_same_para", numIndentSpaces))
|
|
spacerBetweenSections = settings.get("jsdocs_spacer_between_sections")
|
|
|
|
v.run_command('expand_selection', {'to': 'scope'})
|
|
|
|
# find the first word
|
|
startPoint = v.find("\n\\s*\\* ", v.sel()[0].begin()).begin()
|
|
# find the first tag, or the end of the comment
|
|
endPoint = v.find("\\s*\n\\s*\\*(/)", v.sel()[0].begin()).begin()
|
|
|
|
# replace the selection with this ^ new selection
|
|
v.sel().clear()
|
|
v.sel().add(sublime.Region(startPoint, endPoint))
|
|
|
|
# get the description text
|
|
text = v.substr(v.sel()[0])
|
|
|
|
# find the indentation level
|
|
indentation = len(re.sub('\t', ' ' * tabSize, re.search("\n(\\s*\\*)", text).group(1)))
|
|
wrapLength -= indentation - tabSize
|
|
|
|
# join all the lines, collapsing "empty" lines
|
|
text = re.sub("\n(\\s*\\*\\s*\n)+", "\n\n", text)
|
|
|
|
def wrapPara(para):
|
|
para = re.sub("(\n|^)\\s*\\*\\s*", " ", para)
|
|
|
|
# split the paragraph into words
|
|
words = para.strip().split(' ')
|
|
text = '\n'
|
|
line = ' *' + indentSpaces
|
|
lineTagged = False # indicates if the line contains a doc tag
|
|
paraTagged = False # indicates if this paragraph contains a doc tag
|
|
lineIsNew = True
|
|
tag = ''
|
|
|
|
# join all words to create lines, no longer than wrapLength
|
|
for i, word in enumerate(words):
|
|
if not word and not lineTagged:
|
|
continue
|
|
|
|
if lineIsNew and word[0] == '@':
|
|
lineTagged = True
|
|
paraTagged = True
|
|
tag = word
|
|
|
|
if len(line) + len(word) >= wrapLength - 1:
|
|
# appending the word to the current line whould exceed its
|
|
# length requirements
|
|
text += line.rstrip() + '\n'
|
|
line = ' *' + indentSpacesSamePara + word + ' '
|
|
lineTagged = False
|
|
lineIsNew = True
|
|
else:
|
|
line += word + ' '
|
|
|
|
lineIsNew = False
|
|
|
|
text += line.rstrip()
|
|
return {'text': text,
|
|
'lineTagged': lineTagged,
|
|
'tagged': paraTagged,
|
|
'tag': tag}
|
|
|
|
# split the text into paragraphs, where each paragraph is eighter
|
|
# defined by an empty line or the start of a doc parameter
|
|
paragraphs = re.split('\n{2,}|\n\\s*\\*\\s*(?=@)', text)
|
|
wrappedParas = []
|
|
text = ''
|
|
for p, para in enumerate(paragraphs):
|
|
# wrap the lines in the current paragraph
|
|
wrappedParas.append(wrapPara(para))
|
|
|
|
# combine all the paragraphs into a single piece of text
|
|
for i in range(0, len(wrappedParas)):
|
|
para = wrappedParas[i]
|
|
last = i == len(wrappedParas) - 1
|
|
|
|
nextIsTagged = not last and wrappedParas[i + 1]['tagged']
|
|
nextIsSameTag = nextIsTagged and para['tag'] == wrappedParas[i + 1]['tag']
|
|
|
|
if last or (para['lineTagged'] or nextIsTagged) and \
|
|
not (spacerBetweenSections and not nextIsSameTag):
|
|
text += para['text']
|
|
else:
|
|
text += para['text'] + '\n *'
|
|
|
|
write(v, text)
|