""" 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' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?' + 'function' # function fnName + '(?:\s+(?P' + self.settings['fnIdentifier'] + '))?' # (arg1, arg2) + '\s*\(\s*(?P.*)\)', 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' + self.settings['varIdentifier'] + ')\s*[=:]\s*(?P.*?)(?:[;,]|$)', 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' + self.settings['fnIdentifier'] + ')' # function fnName # (arg1, arg2) + '\\s*\\(\\s*(?P.*)\)', 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' + self.settings['varIdentifier'] + ")\\s*=\\s*(?P.*)", 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' + self.settings['varIdentifier'] + ')\\s*=>?\\s*(?P.*?)(?:[;,]|$)', line ) if res: return (res.group('name'), res.group('val').strip()) res = re.search( '\\b(?:var|public|private|protected|static)\\s+(?P' + 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' + self.settings['varIdentifier'] + ')[&*\\s]+' + '(?P' + self.settings['varIdentifier'] + ')' # void fnName # (arg1, arg2) + '\\s*\\(\\s*(?P.*)\)', 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' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?' + '(?:\\((?P[^()]*?)\\))?\\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' + self.settings['varIdentifier'] + ')\s*[=:]\s*(?P.*?)(?:[;,]|$)', 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' + self.settings['varIdentifier'] + ')\s*[:=]\s*)?' + 'function(?:\s+(?P[gs]et))?' # function fnName + '(?:\s+(?P' + self.settings['fnIdentifier'] + '))?' # (arg1, arg2) + '\s*\(\s*(?P.*)\)', 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' + typeRE + ')\\s*\\)\\s*' + '(?P[a-zA-Z_$][a-zA-Z0-9_$]*)' # void fnName # (arg1, arg2) + '\\s*(?::(?P.*))?', 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[a-zA-Z_$][\<\>\., a-zA-Z_$0-9]+)\s+' # Method name + '(?P' + self.settings['fnIdentifier'] + ')\s*' # Params + '\((?P.*)\)\s*' # # Throws , + '(?:throws){0,1}\s*(?P[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\\s*@(?:param|property)%s\\s+\\S+\\s+)\\S" % extraIndent, line) \ or re.search("^\\s*\\*(?P\\s*@(?:returns?|define)%s\\s+\\S+\\s+)\\S" % extraIndent, line) \ or re.search("^\\s*\\*(?P\\s*@[a-z]+\\s+)\\S", line) \ or re.search("^\\s*\\*(?P\\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)