diff --git a/.gitignore b/.gitignore index 1670801..fd20fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -*.pyc \ No newline at end of file +*.pyc diff --git a/.travis.yml b/.travis.yml index f2c830d..a10e80b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,11 @@ # http://lint.travis-ci.org/ language: python python: - - "2.6" # sublime text 2 - - "3.3" # sublime text 3 + - "2.6" # sublime text 2 + - "3.3" # sublime text 3 before_install: - - sudo apt-get install exuberant-ctags - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi + - sudo apt-get install exuberant-ctags + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi script: - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then python -m unittest2.__main__ discover; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then python -m unittest discover; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then python -m unittest2.__main__ discover; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then python -m unittest discover; fi diff --git a/CTAGS_README.rst b/CTAGS_README.rst deleted file mode 100644 index 234b135..0000000 --- a/CTAGS_README.rst +++ /dev/null @@ -1,77 +0,0 @@ -TAG FILE FORMAT -=============== - -When not running in etags mode, each entry in the tag file consists of a -separate line, each looking like this in the most general case:: - - tag_namefile_nameex_cmd;"extension_fields - -The fields and separators of these lines are specified as follows: - -#. Tag name -#. Single tab character -#. Name of the file in which the object associated with the tag is located -#. Single tab character -#. EX command used to locate the tag within the file; generally a search - pattern (either ``/pattern/`` or ``?pattern?``) or line number (see - ``−−excmd``). - Tag file format 2 (see ``−−format``) extends this EX command under certain - circumstances to include a set of extension fields (described below) - embedded in an EX comment immediately appended to the EX command, which - leaves it backward-compatible with original ``vi(1)`` implementations. - -A few special tags are written into the tag file for internal purposes. These -tags are composed in such a way that they always sort to the top of the file. -Therefore, the first two characters of these tags are used a magic number to -detect a tag file for purposes of determining whether a valid tag file is -being overwritten rather than a source file. Note that the name of each source -file will be recorded in the tag file exactly as it appears on the command -line. - -Therefore, if the path you specified on the command line was relative to the -current directory, then it will be recorded in that same manner in the tag -file. See, however, the ``−−tag−relative`` option for how this behavior can be -modified. - -Extension fields are tab-separated key-value pairs appended to the end of the -EX command as a comment, as described above. These key value pairs appear in -the general form ``key:value``. Their presence in the lines of the tag file -are controlled by the ``−−fields`` option. The possible keys and the meaning -of their values are as follows: - -access - Indicates the visibility of this class member, where value is specific to - the language. - -file - Indicates that the tag has file-limited visibility. This key has no - corresponding value. - -kind - Indicates the type, or kind, of tag. Its value is either one of the - corresponding one-letter flags described under the various - ``−−−kinds`` options above, or a full name. It is permitted (and is, - in fact, the default) for the key portion of this field to be omitted. The - optional behaviors are controlled with the ``−−fields`` option. - -implementation - When present, this indicates a limited implementation (abstract vs. concrete) - of a routine or class, where value is specific to the language ("virtual" or - "pure virtual" for C++; "abstract" for Java). - -inherits - When present, value is a comma-separated list of classes from which this - class is derived (i.e. inherits from). - -signature - When present, value is a language-dependent representation of the - signature of a routine. A routine signature in its complete form specifies - the return type of a routine and its formal argument list. This extension - field is presently supported only for C-based languages and does not - include the return type. - -In addition, information on the scope of the tag definition may be available, -with the key portion equal to some language-dependent construct name and its -value the name declared for that construct in the program. This scope entry -indicates the scope in which the tag was found. For example, a tag generated -for a C structure member would have a scope looking like ``struct:myStruct``. \ No newline at end of file diff --git a/CTags.sublime-settings b/CTags.sublime-settings index 15b02fc..3b60e94 100644 --- a/CTags.sublime-settings +++ b/CTags.sublime-settings @@ -1,56 +1,167 @@ // Place your settings in the file "User/CTags.sublime-settings", which // overrides the settings in here. { - // Enable debugging + // Enable debugging. + // + // When enabled, this will result in debug output being printed to the + // console. This can be useful for debugging issues. "debug": false, - // Enable autocomplete + // Enable auto-complete. + // + // When enabled, this turns on a basic "auto-complete" feature, similar to + // a very rudimentary "Intellisense(TM)". This is useful for providing + // better suggestions than stock Sublime Text could provide. "autocomplete": false, + // Path to ctags executable. + // // Alter this value if your ctags command is not in the PATH, or if using // a different version of ctags to that in the path (i.e. for OSX). // // NOTE: You *should not* place entire commands here. These commands are - // built automatically using the values below. For example: - // GOOD: "command": "/usr/bin/ctags" - // BAD: "command": "ctags -R -f .tags --exclude=some/path" + // built automatically using the values below. For example, this is OK: + // + // "command": "/usr/bin/ctags" + // + // This, on the other hand, won't work! + // + // "command": "ctags -R -f .tags --exclude=some/path" + // "command": "", - // Set to false to disable recursive search for ctag generation. This - // translates the `-R` parameter + // Enable recursive searching of directories when building tag files. + // + // When enabled, this is equivalent to `-R` parameter. Set to true to + // enable recursive search of directories when generating tag files. "recursive" : true, - // Default read/write location of the tags file. This translates to the - // `-f [FILENAME]` parameter + // Default read/write location of the tags file. + // + // This is equivalent to the `-f [FILENAME]` parameter. There is likely no + // reason to change this unless you have a large number of existing tags + // files you'd like to use that already have a different name. In this + // case perhaps consider using the 'extra_tag_files' setting instead. "tag_file" : ".tags", - // Additional options to pass to ctags, i.e. - // ["--exclude=some/path", "--exclude=some/other/path", ...] + // Additional tag files names to search. + // + // These are searched in addition to the file name given in 'tag_file' + "extra_tag_files": [".gemtags", "tags"], + + // Additional options to pass to ctags. + // + // Any addition options you may wish to pass to the ctags executable. For + // example: + // + // ["--exclude=some/path", "--exclude=some/other/path", ...] "opts" : [], + // Tag "kind"s to ignore. + // + // A ctags tagfile describes a number of different "kind"s, described in + // tag FORMAT file found here: + // + // http://ctags.sourceforge.net/FORMAT + // + // These can be filtered (i.e. ignored). For example - 'import' statements + // should be ignored in Python. These are of kind "i", e.g. + // + // "type":"^i$" // "filters": { "source.python": {"type":"^i$"} }, + // Definition "kind"s to ignore. // + // This is very similar to the 'filters' option. However, this only + // applies to the process that is used to find a definition. All filters + // placed here will be used when the plugin is searching for a definition + // in the file. "definition_filters": { "source.php": {"type":"^v$"} }, + // Enable the ctags menu in the context menus. + "show_context_menus": true, + + // Paths to additional tag files to include in tag search. // - "definition_current_first": true, + // This is a list of items in the following format: + // + // [["language", "platform"], "path"] + // + "extra_tag_paths": [ + [["source.python", "windows"], "C:\\Python27\\Lib\\tags"] + ], - // Show the ctags menu in the context menus - "show_context_menus": true, + // Enable highlighting of selected symbol. + // + // When enabled, searched symbols will be highlighted when found. This + // can be irritating in some instances, e.g. when in Vintage mode. In + // these cases, setting this to false will disable this highlighting. + "select_searched_symbol": true, - // Paths to additional tag files to include in tag search. This is a list - // of items in the form [["language", "platform"], "path"] - "extra_tag_paths": [[["source.python", "windows"], "C:\\Python27\\Lib\\tags"]], + // Set to false to not open an error dialog while tags are building + "display_rebuilding_message": true, - // Additional tag files to search - "extra_tag_files": [".gemtags", "tags"], + // Rank Manager language syntax regex and character sets + // + // Ex: Python 'and' ignore exp --> '\sand\s' - it must have whitespace + // around it so it is not part of real name: gates.Nand.evaluate() + "language_syntax": { + "splitters" : [".", "::", "->"], + "source.js": { + "member_exp": { + "chars": "[A-Za-z0-9_$]", + "splitters": ["\\."], + "open": ["\\{", "\\[", "\\("], + "close": ["\\}", "\\]" , "\\)"], //close[i] must match open[i] + "ignore": ["&", "\\|", "\\?", ":", "\\!", "'", "=", "\""], + "stop": ["\\s", ","], + "this": ["this", "me", "self", "that"] + }, + "reference_types": { + "__symbol__(\\.call|\\.apply){0,1}\\s*?\\(": ["f", "fa"], + "\\.fire\\s*?\\(\\s*?\\[\\'\"]__symbol__\\[\\'\"\\]": [ + "eventHandler" + ] + } + }, + "source.python": { + //python settings inherit JavaScript, with some overrides + "inherit": "source.js", + "member_exp": { + "ignore": ["\\sand\\s", "\\sor\\s", "\\snot\\s", ":", "\\!", + "'", "=", "\""], + "this" : ["self"] + } + }, + "source.java": { + "inherit": "source.js", + "member_exp": { + "this" : ["this"] + } + }, + "source.cs": { + "inherit": "source.js", + "member_exp": { + "this" : ["this"] + } + } + }, - // Set to false so as not to select searched symbol (in Vintage mode) - "select_searched_symbol": true + // Scope Filters + // + // Tags file may optionally contain tagfield for the scope of the tag. For + // example: + // + // item .\fileHelper.js 420;" vp lineno:420 scope:420:19-422:9 + // + // The re is used to extract 'scope:/beginLine:beginCol-endLine:endCol/' + // + // Different tags generators may generate this non-standard field in + // different formats + "scope_re": "(\\d.*?):(\\d.*?)-(\\d.*?):(\\d.*?)" } diff --git a/Context.sublime-menu b/Context.sublime-menu index cf27317..dc21dcc 100644 --- a/Context.sublime-menu +++ b/Context.sublime-menu @@ -1,15 +1,15 @@ [ - { - "caption": "-" - }, - { - "command": "navigate_to_definition", - "args": {}, - "caption": "Navigate to Definition" - }, - { - "command": "jump_prev", - "args": {}, - "caption": "Jump Back" - } + { + "caption": "-" + }, + { + "command": "navigate_to_definition", + "args": {}, + "caption": "Navigate to Definition" + }, + { + "command": "jump_prev", + "args": {}, + "caption": "Jump Back" + } ] diff --git a/Default.sublime-commands b/Default.sublime-commands index 356972f..a7bb43d 100644 --- a/Default.sublime-commands +++ b/Default.sublime-commands @@ -1,33 +1,33 @@ [ - { - "caption": "CTags: Rebuild Tags", - "command": "rebuild_tags" - }, - { - "caption": "CTags: Show Symbols (file)", - "command": "show_symbols", - "context": [ - { - "key": "selector", - "match_all": true, - "operand": "source -source.css", - "operator": "equal" - } - ] - }, - { - "caption": "CTags: Show Symbols (all)", - "command": "show_symbols", - "args": { - "type": "multi" + { + "caption": "CTags: Rebuild Tags", + "command": "rebuild_tags" }, - "context": [ - { - "key": "selector", - "match_all": true, - "operand": "source -source.css", - "operator": "equal" - } - ] - } + { + "caption": "CTags: Show Symbols (file)", + "command": "show_symbols", + "context": [ + { + "key": "selector", + "match_all": true, + "operand": "source -source.css", + "operator": "equal" + } + ] + }, + { + "caption": "CTags: Show Symbols (all)", + "command": "show_symbols", + "args": { + "type": "multi" + }, + "context": [ + { + "key": "selector", + "match_all": true, + "operand": "source -source.css", + "operator": "equal" + } + ] + } ] diff --git a/Default.sublime-keymap b/Default.sublime-keymap index 14aa09e..2015221 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -1,71 +1,71 @@ [ - { - "command": "navigate_to_definition", - "keys": ["ctrl+t", "ctrl+t"] - }, - { - "command": "navigate_to_definition", - "keys": ["ctrl+shift+period"] - }, - { - "command": "search_for_definition", - "keys": ["ctrl+t", "ctrl+y"] - }, - { - "command": "jump_prev", - "keys": ["ctrl+t", "ctrl+b"] - }, - { - "command": "jump_prev", - "keys": ["ctrl+shift+comma"] - }, - { - "command": "rebuild_tags", - "keys": ["ctrl+t", "ctrl+r"] - }, - { - "command": "show_symbols", - "context": [ - { - "key": "selector", - "match_all": true, - "operand": "source -source.css", - "operator": "equal" - } - ], - "keys": ["alt+s"] - }, - { - "command": "show_symbols", - "args": {"type": "multi"}, - "context": [ - { - "key": "selector", - "match_all": true, - "operand": "source -source.css", - "operator": "equal" - } - ], - "keys": ["alt+shift+s"] - }, - { - "command": "show_symbols", - "args": {"type": "lang"}, - "context": [ - { - "key": "selector", - "match_all": true, - "operand": "source -source.css", - "operator": "equal" - } - ], - "keys": ["ctrl+alt+shift+s"] - }, - { // override current default - "command": "transpose", - "context": [ - { "key": "num_selections", "operator": "not_equal", "operand": 1 } - ], - "keys": ["ctrl+t"] - } + { + "command": "navigate_to_definition", + "keys": ["ctrl+t", "ctrl+t"] + }, + { + "command": "navigate_to_definition", + "keys": ["ctrl+shift+period"] + }, + { + "command": "search_for_definition", + "keys": ["ctrl+t", "ctrl+y"] + }, + { + "command": "jump_prev", + "keys": ["ctrl+t", "ctrl+b"] + }, + { + "command": "jump_prev", + "keys": ["ctrl+shift+comma"] + }, + { + "command": "rebuild_tags", + "keys": ["ctrl+t", "ctrl+r"] + }, + { + "command": "show_symbols", + "context": [ + { + "key": "selector", + "match_all": true, + "operand": "source -source.css", + "operator": "equal" + } + ], + "keys": ["alt+s"] + }, + { + "command": "show_symbols", + "args": {"type": "multi"}, + "context": [ + { + "key": "selector", + "match_all": true, + "operand": "source -source.css", + "operator": "equal" + } + ], + "keys": ["alt+shift+s"] + }, + { + "command": "show_symbols", + "args": {"type": "lang"}, + "context": [ + { + "key": "selector", + "match_all": true, + "operand": "source -source.css", + "operator": "equal" + } + ], + "keys": ["ctrl+alt+shift+s"] + }, + { // override current default + "command": "transpose", + "context": [ + { "key": "num_selections", "operator": "not_equal", "operand": 1 } + ], + "keys": ["ctrl+t"] + } ] diff --git a/Default.sublime-mousemap b/Default.sublime-mousemap index 482b590..96ac796 100644 --- a/Default.sublime-mousemap +++ b/Default.sublime-mousemap @@ -1,15 +1,15 @@ [ - { - "button": "button1", - "count": 1, - "press_command": "drag_select", - "modifiers": ["ctrl","shift"], - "command": "navigate_to_definition" - }, - { - "button": "button2", - "count": 1, - "modifiers": ["ctrl","shift"], - "command": "jump_prev" - } + { + "button": "button1", + "count": 1, + "press_command": "drag_select", + "modifiers": ["ctrl","shift"], + "command": "navigate_to_definition" + }, + { + "button": "button2", + "count": 1, + "modifiers": ["ctrl","shift"], + "command": "jump_prev" + } ] diff --git a/Side Bar.sublime-menu b/Side Bar.sublime-menu index 7177eae..0ffcc86 100644 --- a/Side Bar.sublime-menu +++ b/Side Bar.sublime-menu @@ -7,5 +7,7 @@ "files": [] } }, - { "caption": "-", "id": "ctags_commands" } + { + "caption": "-", "id": "ctags_commands" + } ] diff --git a/ctags.py b/ctags.py index 1b2141b..66ca1e1 100644 --- a/ctags.py +++ b/ctags.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python - -"""A ctags wrapper, parser and sorter""" +""" +A ctags wrapper, parser and sorter. +""" import codecs import re @@ -10,14 +10,14 @@ import bisect import mmap -if sys.version_info<(2,7,0): +if sys.version_info < (2, 7): from helpers.check_output import check_output else: from subprocess import check_output -""" -Contants -""" +# +# Contants +# TAGS_RE = re.compile( r'(?P[^\t]+)\t' @@ -37,22 +37,20 @@ 'function', 'class', 'struct', ] -PATH_IGNORE_FIELDS = ('file', 'access', 'signature', - 'language', 'line', 'inherits') +PATH_IGNORE_FIELDS = ( + 'file', 'access', 'signature', 'language', 'line', 'inherits') TAG_PATH_SPLITTERS = ('/', '.', '::', ':') +# +# Functions +# -""" -Functions -""" - - -"""Helper functions""" - +# Helper functions def splits(string, *splitters): - """Split a string on a number of splitters. + """ + Split a string on a number of splitters. :param string: string to split :param splitters: characters to split string on @@ -60,19 +58,18 @@ def splits(string, *splitters): :returns: ``string`` split on characters in ``string``""" if splitters: split = string.split(splitters[0]) - for s in split: - for c in splits(s, *splitters[1:]): - yield c + for val in split: + for char in splits(val, *splitters[1:]): + yield char else: if string: yield string +# Tag processing functions -"""Tag processing functions""" - - -def parse_tag_lines(lines, order_by='symbol', tag_class=None, filters=[]): - """Parse and sort a list of tags. +def parse_tag_lines(lines, order_by='symbol', tag_class=None, filters=None): + """ + Parse and sort a list of tags. Parse and sort a list of tags one by using a combination of regexen and Python functions. The end result is a dictionary containing all 'tags' or @@ -109,11 +106,12 @@ def parse_tag_lines(lines, order_by='symbol', tag_class=None, filters=[]): if tag_class is not None: # if 'casting' to a class tag = tag_class(tag) - # apply filters, filtering out any matching entries - for f in filters: - for k, v in list(f.items()): - if re.match(v, tag[k]): - skip = True + if filters: + # apply filters, filtering out any matching entries + for filt in filters: + for key, val in list(filt.items()): + if re.match(val, tag[key]): + skip = True if skip: # if a filter was matched, ignore line (filter out) continue @@ -122,9 +120,9 @@ def parse_tag_lines(lines, order_by='symbol', tag_class=None, filters=[]): return tags_lookup - def post_process_tag(tag): - """Process 'EX Command'-related elements of a tag. + """ + Process 'EX Command'-related elements of a tag. Process all 'EX Command'-related elements. The 'Ex Command' element has previously been split into the 'fields', 'type' and 'ex_command' elements. @@ -172,9 +170,9 @@ def post_process_tag(tag): return tag - def process_ex_cmd(tag): - """Process the 'ex_command' element of a tag dictionary. + """ + Process the 'ex_command' element of a tag dictionary. Process the ex_command string - a line number or regex used to find symbol declaration - by unescaping the regex where used. @@ -190,9 +188,9 @@ def process_ex_cmd(tag): else: # else a regex, so unescape return re.sub(r"\\(\$|/|\^|\\)", r'\1', ex_cmd[2:-2]) # unescape regex - def process_fields(tag): - """Process the 'field' element of a tag dictionary. + """ + Process the 'field' element of a tag dictionary. Process the fields string - a comma-separated string of "key-value" pairs - by generating key-value pairs and appending them to the tag dictionary. @@ -216,9 +214,9 @@ def process_fields(tag): return result - def create_tag_path(tag): - """Create a tag path entry for a tag dictionary. + """ + Create a tag path entry for a tag dictionary. Creates a tag path entry for a tag dictionary from the field key-value pairs. Uses format:: @@ -262,21 +260,18 @@ def create_tag_path(tag): return result +# Tag building/sorting functions -"""Tag building/sorting functions""" - - -def build_ctags(path, tag_file=None, recursive=False, opts=None, cmd=None, - env=None): - """Execute the ``ctags`` command using ``Popen`` +def build_ctags(path, cmd=None, tag_file=None, recursive=False, opts=None): + """ + Execute the ``ctags`` command using ``Popen``. :param path: path to file or directory (with all files) to generate ctags for. - :param tag_file: filename to use for the tag file. Defaults to ``tags`` :param recursive: specify if search should be recursive in directory given by path. This overrides filename specified by ``path`` + :param tag_file: filename to use for the tag file. Defaults to ``tags`` :param opts: list of additional options to pass to the ctags executable - :param env: environment variables to be used when executing ``ctags`` :returns: original ``tag_file`` filename """ @@ -318,7 +313,7 @@ def build_ctags(path, tag_file=None, recursive=False, opts=None, cmd=None, cmd = ' '.join(cmd) # execute the command - check_output(cmd, cwd=cwd, shell=True, env=env, stdin=subprocess.PIPE, + check_output(cmd, cwd=cwd, shell=True, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) if not tag_file: # Exuberant ctags defaults to ``tags`` filename. @@ -332,9 +327,9 @@ def build_ctags(path, tag_file=None, recursive=False, opts=None, cmd=None, return tag_file - def resort_ctags(tag_file): - """Rearrange ctags file for speed. + """ + Rearrange ctags file for speed. Resorts (re-sort) a CTag file in order of file. This improves searching performance when searching tags by file as a binary search can be used. @@ -360,33 +355,34 @@ def resort_ctags(tag_file): """ keys = {} - with codecs.open(tag_file, encoding='utf-8', errors='replace') as fh: - for line in fh: + with codecs.open(tag_file, encoding='utf-8', errors='replace') as file_: + for line in file_: keys.setdefault(line.split('\t')[FILENAME], []).append(line) - with codecs.open(tag_file+'_sorted_by_file', 'w', encoding='utf-8', errors='replace') as fw: + with codecs.open(tag_file+'_sorted_by_file', 'w', encoding='utf-8', + errors='replace') as file_: for k in sorted(keys): for line in keys[k]: split = line.split('\t') split[FILENAME] = split[FILENAME].lstrip('.\\') - fw.write('\t'.join(split)) - - -""" -Models -""" + file_.write('\t'.join(split)) +# +# Models +# class TagElements(dict): - """Model the entries of a tag file""" + """ + Model the entries of a tag file. + """ def __init__(self, *args, **kw): """Initialise Tag object""" dict.__init__(self, *args, **kw) self.__dict__ = self - class Tag(object): - """Model a tag. + """ + Model a tag. This exists mainly to enable different types of sorting. """ @@ -405,9 +401,12 @@ def __gt__(self, other): def __getitem__(self, index): return self.line.split('\t')[index] + def __len__(self): + return len(self.line.split('\t')) class TagFile(object): - """Model a tag file. + """ + Model a tag file. This doesn't actually hold a entire tag file, due in part to the sheer size of some tag files (> 100 MB files are possible). Instead, it acts @@ -415,8 +414,12 @@ class TagFile(object): searching for a retrieving tags, finding tags based on given criteria (prefix, suffix, exact), getting the directory of a tag and so forth. """ + file_o = None + mapped = None + def __init__(self, path, column): - """Initialise object. + """ + Initialise object. The file indicated by ``path`` must be sorted by values in the column indicated by ``column``. @@ -430,7 +433,9 @@ def __init__(self, path, column): self.column = column def __getitem__(self, index): - """Provide sequence-type interface to tag file.""" + """ + Provide sequence-type interface to tag file. + """ self.mapped.seek(index) result = self.mapped.readline() @@ -442,36 +447,49 @@ def __getitem__(self, index): return Tag(result, self.column) def __len__(self): - """Get size of tag file in bytes""" + """ + Get size of tag file in bytes. + """ return len(self.mapped) def __enter__(self): - """Open file on enter when using ``with`` keyword""" + """ + Open file on enter when using ``with`` keyword. + """ self.open() return self - def __exit__(self, type, value, traceback): - """Close file on exit when using ``with`` keyword""" + def __exit__(self, type_, value, traceback): + """ + Close file on exit when using ``with`` keyword. + """ self.close() @property def dir(self): - """Get directory of tag file""" + """ + Get directory of tag file. + """ return os.path.dirname(self.path) def open(self): - """Open file""" + """ + Open file. + """ self.file_o = codecs.open(self.path, 'r+b', encoding='ascii') self.mapped = mmap.mmap(self.file_o.fileno(), 0, access=mmap.ACCESS_READ) def close(self): - """Close file""" + """ + Close file. + """ self.mapped.close() self.file_o.close() def search(self, exact_match=True, *tags): - """Search for one or more tags in the tag file. + """ + Search for one or more tags in the tag file. Search a tag file for given tags using a binary search. @@ -486,20 +504,21 @@ def search(self, exact_match=True, *tags): return for key in tags: - leftIndex = bisect.bisect_left(self, key) + left_index = bisect.bisect_left(self, key) if exact_match: - result = self[leftIndex] + result = self[left_index] while result.line and result[result.column] == key: yield(result) result = Tag(self.mapped.readline().strip(), self.column) else: - result = self[leftIndex] + result = self[left_index] while result.line and result[result.column].startswith(key): yield(result) result = Tag(self.mapped.readline().strip(), self.column) def search_by_suffix(self, suffix): - """Search for one or more tags with the given suffix in the tag file. + """ + Search for one or more tags with the given suffix in the tag file. Search a tag file for given tags with the given suffix, using a linear search. Note that this linear search requires the entire file be @@ -516,7 +535,8 @@ def search_by_suffix(self, suffix): continue def tag_class(self): - """Default class to wrap tag in. + """ + Default class to wrap tag in. Allows wrapping of a parsed tag dict in a class, so elements can be accessed as class variables (i.e. ``class.variable``, rather than @@ -525,13 +545,17 @@ def tag_class(self): return type('TagElements', (TagElements,), dict(root_dir=self.dir)) def get_tags_dict(self, *tags, **kw): - """Return the tags from a tag file as a dict""" + """ + Return the tags from a tag file as a dict. + """ filters = kw.get('filters', []) return parse_tag_lines(self.search(True, *tags), tag_class=self.tag_class(), filters=filters) def get_tags_dict_by_suffix(self, suffix, **kw): - """Return the tags with the given suffix of a tag file as a dict""" + """ + Return the tags with the given suffix of a tag file as a dict. + """ filters = kw.get('filters', []) return parse_tag_lines(self.search_by_suffix(suffix), tag_class=self.tag_class(), filters=filters) diff --git a/ctagsplugin.py b/ctagsplugin.py index 2900360..d3e918d 100644 --- a/ctagsplugin.py +++ b/ctagsplugin.py @@ -1,10 +1,12 @@ -#!/usr/bin/env python - -"""A ctags plugin for Sublime Text 2/3""" +""" +A ctags plugin for Sublime Text 2/3. +""" import functools +from functools import reduce import codecs import locale +import sys import os import pprint import re @@ -20,29 +22,29 @@ import sublime import sublime_plugin from sublime import status_message, error_message -except ImportError: # running tests - import sys + # hack the system path to prevent the following issue in ST3 + # ImportError: No module named 'ctags' + sys.path.append(os.path.dirname(os.path.realpath(__file__))) +except ImportError: # running tests from tests.sublime_fake import sublime from tests.sublime_fake import sublime_plugin sys.modules['sublime'] = sublime sys.modules['sublime_plugin'] = sublime_plugin -if sublime.version().startswith('2'): - import ctags - from ctags import (FILENAME, parse_tag_lines, PATH_ORDER, SYMBOL, - TagElements, TagFile) - from helpers.edit import Edit -else: # safe to assume if not ST2 then ST3 - from CTags import ctags - from CTags.ctags import (FILENAME, parse_tag_lines, PATH_ORDER, SYMBOL, - TagElements, TagFile) - from CTags.helpers.edit import Edit +import ctags +from ctags import (FILENAME, parse_tag_lines, PATH_ORDER, SYMBOL, + TagElements, TagFile) +from helpers.edit import Edit -""" -Contants -""" +from helpers.common import * +from ranking.rank import RankMgr +from ranking.parse import Parser + +# +# Contants +# OBJECT_PUNCTUATORS = { 'class': '.', @@ -52,44 +54,14 @@ ENTITY_SCOPE = 'entity.name.function, entity.name.type, meta.toc-list' -RUBY_SPECIAL_ENDINGS = '\?|!' +RUBY_SPECIAL_ENDINGS = r'\?|!' ON_LOAD = sublime_plugin.all_callbacks['on_load'] -RE_SPECIAL_CHARS = re.compile( - '(\\\\|\\*|\\+|\\?|\\||\\{|\\}|\\[|\\]|\\(|\\)|\\^|\\$|\\.|\\#|\\ )') - -""" -Functions -""" - -"""Helper functions""" - - -def get_settings(): - """Load settings. - - :returns: dictionary containing settings - """ - return sublime.load_settings("CTags.sublime-settings") - - -def get_setting(key, default=None): - """Load individual setting. - - :param key: setting key to get value for - :param default: default value to return if no value found - - :returns: value for ``key`` if ``key`` exists, else ``default`` - """ - return get_settings().get(key, default) - -setting = get_setting - - -def escape_regex(s): - return RE_SPECIAL_CHARS.sub(lambda m: '\\%s' % m.group(1), s) +# +# Functions +# def select(view, region): @@ -106,8 +78,9 @@ def done_in_main(*args, **kw): return done_in_main - # TODO: allow thread per tag file. That makes more sense. + + def threaded(finish=None, msg='Thread already running'): def decorator(func): func.running = 0 @@ -143,7 +116,8 @@ def run(): def on_load(path=None, window=None, encoded_row_col=True, begin_edit=False): - """Decorator to open or switch to a file. + """ + Decorator to open or switch to a file. Opens and calls the "decorated function" for the file specified by path, or the current file if no path is specified. In the case of the former, if @@ -179,7 +153,6 @@ def wrapped(): # if buffer is still loading, wait for it to complete then proceed if view.is_loading(): - class set_on_load(): callbacks = ON_LOAD @@ -208,7 +181,8 @@ def on_load(self, view): def find_tags_relative_to(path, tag_file): - """Find the tagfile relative to a file path. + """ + Find the tagfile relative to a file path. :param path: path to a file :param tag_file: name of tag file @@ -232,7 +206,8 @@ def find_tags_relative_to(path, tag_file): def get_alternate_tags_paths(view, tags_file): - """Search for additional tag files. + """ + Search for additional tag files. Search for additional tag files to use, including those define by a ``search_paths`` file, the ``extra_tag_path`` setting and the @@ -257,7 +232,9 @@ def get_alternate_tags_paths(view, tags_file): for (selector, platform), path in setting('extra_tag_paths'): if view.match_selector(view.sel()[0].begin(), selector): if sublime.platform() == platform: - search_paths.append(os.path.join(path, setting('tag_file'))) + search_paths.append( + os.path.join( + path, setting('tag_file'))) except Exception as e: print(e) @@ -280,14 +257,15 @@ def get_alternate_tags_paths(view, tags_file): # use list instead of set for keep order ret = [] - for p in search_paths: - if p and (p not in ret) and os.path.exists(p): - ret.append(p) + for path in search_paths: + if path and (path not in ret) and os.path.exists(path): + ret.append(path) return ret def get_common_ancestor_folder(path, folders): - """Get common ancestor for a file and a list of folders. + """ + Get common ancestor for a file and a list of folders. :param path: path to file :param folders: list of folder paths @@ -308,16 +286,15 @@ def get_common_ancestor_folder(path, folders): return path # return the root directory - -"""Scrolling functions""" +# Scrolling functions def find_with_scope(view, pattern, scope, start_pos=0, cond=True, flags=0): max_pos = view.size() - while start_pos < max_pos: - estrs = pattern.split('\ufffd') - if(len(estrs)>1):pattern = estrs[0] + estrs = pattern.split(r'\ufffd') + if(len(estrs) > 1): + pattern = estrs[0] f = view.find(pattern, start_pos, flags) if not f or view.match_selector(f.begin(), scope) is cond: @@ -365,15 +342,16 @@ def and_then(view): do_find = True if tag.ex_command.isdigit(): - look_from = view.text_point(int(tag.ex_command)-1, 0) + look_from = view.text_point(int(tag.ex_command) - 1, 0) else: look_from = follow_tag_path(view, tag.tag_path, tag.ex_command) if not look_from: do_find = False if do_find: + search_symbol = tag.get('def_symbol', tag.symbol) symbol_region = view.find( - escape_regex(tag.symbol) + r"(?:[^_]|$)", look_from, 0) + escape_regex(search_symbol) + r"(?:[^_]|$)", look_from, 0) if do_find and symbol_region: # Using reversed symbol_region so cursor stays in front of the @@ -389,19 +367,19 @@ def and_then(view): if hook: hook(view) - -"""Formatting helper functions""" +# Formatting helper functions def format_tag_for_quickopen(tag, show_path=True): - """Format a tag for use in quickopen panel. + """ + Format a tag for use in quickopen panel. :param tag: tag to display in quickopen :param show_path: show path to file containing tag in quickopen :returns: formatted tag """ - format = [] + format_ = [] tag = ctags.TagElements(tag) f = '' @@ -411,17 +389,18 @@ def format_tag_for_quickopen(tag, show_path=True): f += string.Template( ' %($field)s$punct%(symbol)s').substitute(locals()) - format = [f % tag if f else tag.symbol, tag.ex_command] - format[1] = format[1].strip() + format_ = [f % tag if f else tag.symbol, tag.ex_command] + format_[1] = format_[1].strip() if show_path: - format.insert(1, tag.filename) + format_.insert(1, tag.filename) - return format + return format_ def prepare_for_quickpanel(formatter=format_tag_for_quickopen): - """Prepare list of matching ctags for the quickpanel. + """ + Prepare list of matching ctags for the quickpanel. :param formatter: formatter function to apply to tag @@ -438,12 +417,12 @@ def compile_lists(sorter): return compile_lists - -"""File collection helper functions""" +# File collection helper functions def get_rel_path_to_source(path, tag_file, multiple=True): - """Get relative path from tag_file to source file. + """ + Get relative path from tag_file to source file. :param path: path to a source file :param tag_file: path to a tag file @@ -462,26 +441,27 @@ def get_rel_path_to_source(path, tag_file, multiple=True): def get_current_file_suffix(path): - """Get file extension + """ + Get file extension :param path: path to a source file :returns: file extension for file """ - file_prefix, file_suffix = os.path.splitext(path) + _, file_suffix = os.path.splitext(path) return file_suffix +# +# Sublime Commands +# -""" -Sublime Commands -""" - -"""JumpPrev Commands""" +# JumpPrev Commands class JumpPrev(sublime_plugin.WindowCommand): - """Provide ``jump_back`` command. + """ + Provide ``jump_back`` command. Command "jumps back" to the previous code point before a tag was navigated or "jumped" to. @@ -505,25 +485,25 @@ def run(self): file_name, sel = self.buf.pop() self.jump(file_name, sel) - def jump(self, fn, sel): - @on_load(fn, begin_edit=True) + def jump(self, path, sel): + @on_load(path, begin_edit=True) def and_then(view): select(view, sel) @classmethod def append(cls, view): """Append a code point to the list""" - fn = view.file_name() - if fn: + name = view.file_name() + if name: sel = [s for s in view.sel()][0] - cls.buf.append((fn, sel)) - + cls.buf.append((name, sel)) -"""CTags commands""" +# CTags commands def show_build_panel(view): - """Handle build ctags command. + """ + Handle build ctags command. Allows user to select whether tags should be built for the current file, a given directory or all open directories. @@ -543,7 +523,7 @@ def show_build_panel(view): ['All Open Folders', '; '.join( ['\'{0}\''.format(os.path.split(x)[1]) for x in view.window().folders()])]) - # append options to build for each open folder + # Append options to build for each open folder display.extend( [[os.path.split(x)[1], x] for x in view.window().folders()]) @@ -566,7 +546,8 @@ def on_select(i): def show_tag_panel(view, result, jump_directly): - """Handle tag navigation command. + """ + Handle tag navigation command. Jump directly to a tag entry, or show a quick panel with a list of matching tags @@ -592,11 +573,12 @@ def on_select(i): def ctags_goto_command(jump_directly=False): - """Decorator to goto a ctag entry. + """ + Decorator to goto a ctag entry. Allow jump to a ctags entry, directly or otherwise """ - def wrapper(f): + def wrapper(func): def command(self, edit, **args): view = self.view tags_file = find_tags_relative_to( @@ -606,7 +588,7 @@ def command(self, edit, **args): status_message('Can\'t find any relevant tags file') return - result = f(self, self.view, args, tags_file) + result = func(self, self.view, args, tags_file) show_tag_panel(self.view, result, jump_directly) return command @@ -614,38 +596,27 @@ def command(self, edit, **args): def check_if_building(self, **args): - """Check if ctags are currently being built""" + """ + Check if ctags are currently being built. + """ if RebuildTags.build_ctags.func.running: - error_message('Please wait while tags are built') + status_message('Tags not available until built') + if setting('display_rebuilding_message'): + error_message('Please wait while tags are built') return False return True -def compile_filters(view): - filters = [] - for selector, regexes in list(setting('filters', {}).items()): - if view.match_selector(view.sel() and view.sel()[0].begin() or 0, - selector): - filters.append(regexes) - return filters - - -def compile_definition_filters(view): - filters = [] - for selector, regexes in list(setting('definition_filters', {}).items()): - if view.match_selector(view.sel() and view.sel()[0].begin() or 0, - selector): - filters.append(regexes) - return filters - - -"""Goto definition under cursor commands""" - +# Goto definition under cursor commands class JumpToDefinition: - """Provider for NavigateToDefinition and SearchForDefinition commands""" + """ + Provider for NavigateToDefinition and SearchForDefinition commands. + """ @staticmethod - def run(symbol, view, tags_file): + def run(symbol, region, sym_line, mbrParts, view, tags_file): + # print('JumpToDefinition') + tags = {} for tags_file in get_alternate_tags_paths(view, tags_file): with TagFile(tags_file, SYMBOL) as tagfile: @@ -657,29 +628,22 @@ def run(symbol, view, tags_file): if not tags: return status_message('Can\'t find "%s"' % symbol) - def_filters = compile_definition_filters(view) - - def pass_def_filter(o): - for f in def_filters: - for k, v in list(f.items()): - if k in o: - if re.match(v, o[k]): - return False - return True + rankmgr = RankMgr(region, mbrParts, view, symbol, sym_line) @prepare_for_quickpanel() def sorted_tags(): - p_tags = list(filter(pass_def_filter, tags.get(symbol, []))) + taglist = tags.get(symbol, []) + p_tags = rankmgr.sort_tags(taglist) if not p_tags: status_message('Can\'t find "%s"' % symbol) - p_tags = sorted(p_tags, key=iget('tag_path')) return p_tags return sorted_tags class NavigateToDefinition(sublime_plugin.TextCommand): - """Provider for the ``navigate_to_definition`` command. + """ + Provider for the ``navigate_to_definition`` command. Command navigates to the definition for a symbol in the open file(s) or folder(s). @@ -701,17 +665,33 @@ def run(self, view, args, tags_file): # handle special line endings for Ruby language = view.settings().get('syntax') - endings = view.substr(sublime.Region(region.end(), region.end()+1)) + endings = view.substr( + sublime.Region( + region.end(), + region.end() + 1)) if 'Ruby' in language and self.endings.match(endings): - region = sublime.Region(region.begin(), region.end()+1) + region = sublime.Region(region.begin(), region.end() + 1) symbol = view.substr(region) - return JumpToDefinition.run(symbol, view, tags_file) + sym_line = view.substr(view.line(region)) + (row, col) = view.rowcol(region.begin()) + line_to_symbol = sym_line[:col] + #print ("line_to_symbol %s" % line_to_symbol) + source = get_source(view) + arrMbrParts = Parser.extract_member_exp(line_to_symbol, source) + return JumpToDefinition.run( + symbol, + region, + sym_line, + arrMbrParts, + view, + tags_file) class SearchForDefinition(sublime_plugin.WindowCommand): - """Provider for the ``search_for_definition`` command. + """ + Provider for the ``search_for_definition`` command. Command searches for definition for a symbol in the open file(s) or folder(s). @@ -734,7 +714,7 @@ def on_done(self, symbol): status_message('Can\'t find any relevant tags file') return - result = JumpToDefinition.run(symbol, view, tags_file) + result = JumpToDefinition.run(symbol, None, "", [], view, tags_file) show_tag_panel(view, result, True) def on_change(self, text): @@ -743,14 +723,14 @@ def on_change(self, text): def on_cancel(self): pass - -"""Show Symbol commands""" +# Show Symbol commands tags_cache = defaultdict(dict) class ShowSymbols(sublime_plugin.TextCommand): - """Provider for the ``show_symbols`` command. + """ + Provider for the ``show_symbols`` command. Command shows all symbols for the open file(s) or folder(s). """ @@ -822,16 +802,17 @@ def sorted_tags(): return sorted_tags - -"""Rebuild CTags commands""" +# Rebuild CTags commands class RebuildTags(sublime_plugin.TextCommand): - """Provider for the ``rebuild_tags`` command. + """ + Provider for the ``rebuild_tags`` command. Command (re)builds tag files for the open file(s) or folder(s), reading relevant settings from the settings file. """ + def run(self, edit, **args): """Handler for ``rebuild_tags`` command""" paths = [] @@ -858,7 +839,8 @@ def run(self, edit, **args): @threaded(msg='Already running CTags!') def build_ctags(self, paths, command, tag_file, recursive, opts): - """Build tags for the open file or folder(s) + """ + Build tags for the open file or folder(s). :param paths: paths to build ctags for :param command: ctags command @@ -898,31 +880,35 @@ def tags_built(tag_file): str_err = ' '.join( e.output.decode('windows-1252').splitlines()) else: - str_err = e.output.decode(locale.getpreferredencoding()).rstrip() + str_err = e.output.decode( + locale.getpreferredencoding()).rstrip() error_message(str_err) return except Exception as e: - error_message("An unknown error occured.\nCheck the console for info.") + error_message( + "An unknown error occured.\nCheck the console for info.") raise e tags_built(result) GetAllCTagsList.ctags_list = [] # clear the cached ctags list - -"""Autocomplete commands""" +# Autocomplete commands class GetAllCTagsList(): + """ + Cache all the ctags list. + """ ctags_list = [] - """cache all the ctags list""" def __init__(self, list): self.ctags_list = list class CTagsAutoComplete(sublime_plugin.EventListener): + def on_query_completions(self, view, prefix, locations): if setting('autocomplete'): prefix = prefix.strip().lower() @@ -951,7 +937,8 @@ def on_query_completions(self, view, prefix, locations): else: prefix = "\\" - f = os.popen("awk \"{ print "+prefix+"$1 }\" \"" + tags_path + "\"") + f = os.popen( + "awk \"{ print " + prefix + "$1 }\" \"" + tags_path + "\"") for i in f.readlines(): tags.append([i.strip()]) @@ -962,13 +949,11 @@ def on_query_completions(self, view, prefix, locations): GetAllCTagsList.ctags_list = tags results = [sublist for sublist in GetAllCTagsList.ctags_list if sublist[0].lower().startswith(prefix)] - results = list(set(results).union(set(sub_results))) - results.sort() + results = sorted(set(results).union(set(sub_results))) return results - -"""Test CTags commands""" +# Test CTags commands class TestCtags(sublime_plugin.TextCommand): diff --git a/helpers/check_output.py b/helpers/check_output.py index dd20835..8a7828b 100644 --- a/helpers/check_output.py +++ b/helpers/check_output.py @@ -1,27 +1,28 @@ -#!/usr/bin/env python +""" +Backport version of 'subprocess.check_output' for Python 2.6.x -# Based on source from here: https://gist.github.com/edufelipe/1027906 - -"""Backport version of 'subprocess.check_output' for Python 2.6.x""" +Based on source from here: https://gist.github.com/edufelipe/1027906 +""" import subprocess def check_output(*popenargs, **kwargs): - r"""Run command with arguments and return its output as a byte string. + r""" + Run command with arguments and return its output as a byte string. - Backported from Python 2.7 as it's implemented as pure python on stdlib. + Backported from Python 2.7 as it's implemented as pure python on stdlib. - >>> check_output(['/usr/bin/python', '--version']) - Python 2.6.2 - """ - process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - error = subprocess.CalledProcessError(retcode, cmd) - error.output = output - raise error - return output + >>> check_output(['/usr/bin/python', '--version']) + Python 2.6.2 + """ + process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) + output, _ = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + error = subprocess.CalledProcessError(retcode, cmd) + error.output = output + raise error + return output diff --git a/helpers/common.py b/helpers/common.py new file mode 100644 index 0000000..1587e68 --- /dev/null +++ b/helpers/common.py @@ -0,0 +1,135 @@ +""" +common utilities used by all ctags modules +""" +import re +import sys +import os + +# Helper functions + +try: + import sublime + import sublime_plugin + from sublime import status_message, error_message + + # hack the system path to prevent the following issue in ST3 + # ImportError: No module named 'ctags' + sys.path.append(os.path.dirname(os.path.realpath(__file__))) +except ImportError: # running tests + from tests.sublime_fake import sublime + from tests.sublime_fake import sublime_plugin + + sys.modules['sublime'] = sublime + sys.modules['sublime_plugin'] = sublime_plugin + + +def get_settings(): + """ + Load settings. + + :returns: dictionary containing settings + """ + return sublime.load_settings("CTags.sublime-settings") + + +def get_setting(key, default=None): + """ + Load individual setting. + + :param key: setting key to get value for + :param default: default value to return if no value found + + :returns: value for ``key`` if ``key`` exists, else ``default`` + """ + return get_settings().get(key, default) + +setting = get_setting + + +def concat_re(reList, escape=False, wrapCapture=False): + """ + concat list of regex into a single regex, used by re.split + wrapCapture - if true --> adds () around the result regex --> split will keep the splitters in its output array. + """ + ret = "|".join((re.escape(spl) if escape else spl) for spl in reList) + if (wrapCapture): + ret = "(" + ret + ")" + return ret + + +def dict_extend(dct, base): + if not dct: + dct = {} + if base: + deriv = base + deriv = merge_two_dicts_deep(deriv, dct) + else: + deriv = dct + return deriv + + +def merge_two_dicts_shallow(x, y): + """ + Given two dicts, merge them into a new dict as a shallow copy. + y members overwrite x members with the same keys. + """ + z = x.copy() + z.update(y) + return z + + +def merge_two_dicts_deep(a, b, path=None): + "Merges b into a including sub-dictionaries - recursive" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + merge_two_dicts_deep(a[key], b[key], path + [str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] + else: + a[key] = b[key] + return a + +RE_SPECIAL_CHARS = re.compile( + '(\\\\|\\*|\\+|\\?|\\||\\{|\\}|\\[|\\]|\\(|\\)|\\^|\\$|\\.|\\#|\\ )') + + +def escape_regex(s): + return RE_SPECIAL_CHARS.sub(lambda m: '\\%s' % m.group(1), s) + + +def get_source(view): + """ + return the language used in current caret or selection location + """ + scope_name = view.scope_name( + view.sel()[0].begin()) # ex: 'source.python meta.function-call.python ' + source = re.split(' ', scope_name)[0] # ex: 'source.python' + return source + + +def get_lang_setting(source): + """ + given source (ex: 'source.python') --> return its language_syntax settings. + A language can inherit its settings from another language, overidding as needed. + """ + lang = setting('language_syntax').get(source) + if lang is not None: + base = setting('language_syntax').get(lang.get('inherit')) + lang = dict_extend(lang, base) + else: + lang = {} + return lang + + +def compile_filters(view): + filters = [] + for selector, regexes in list(setting('filters', {}).items()): + if view.match_selector(view.sel() and view.sel()[0].begin() or 0, + selector): + filters.append(regexes) + return filters diff --git a/helpers/edit.py b/helpers/edit.py index e30c62f..436395d 100644 --- a/helpers/edit.py +++ b/helpers/edit.py @@ -1,8 +1,8 @@ -#!/usr/bin/env python +""" +Buffer editing for both ST2 and ST3 that 'just works'. -# Copyright, SublimeXiki project - -"""Buffer editing for both ST2 and ST3 that 'just works'""" +Copyright, SublimeXiki project +""" import inspect import sublime @@ -13,7 +13,6 @@ except AttributeError: sublime.edit_storage = {} - def run_callback(func, *args, **kwargs): spec = inspect.getfullargspec(func) if spec.args or spec.varargs: @@ -21,7 +20,6 @@ def run_callback(func, *args, **kwargs): else: func() - class EditFuture: def __init__(self, func): self.func = func @@ -29,7 +27,6 @@ def __init__(self, func): def resolve(self, view, edit): return self.func(view, edit) - class EditStep: def __init__(self, cmd, *args): self.cmd = cmd @@ -57,7 +54,6 @@ def resolve_args(self, view, edit): args.append(arg) return args - class Edit: def __init__(self, view): self.view = view @@ -67,7 +63,7 @@ def __nonzero__(self): return bool(self.steps) @classmethod - def future(self, func): + def future(cls, func): return EditFuture(func) def step(self, cmd, *args): @@ -98,7 +94,7 @@ def run(self, view, edit): def __enter__(self): return self - def __exit__(self, type, value, traceback): + def __exit__(self, type_, value, traceback): view = self.view if sublime.version().startswith('2'): edit = view.begin_edit() @@ -109,7 +105,6 @@ def __exit__(self, type, value, traceback): sublime.edit_storage[key] = self.run view.run_command('apply_edit', {'key': key}) - class apply_edit(sublime_plugin.TextCommand): def run(self, edit, key): sublime.edit_storage.pop(key)(self.view, edit) diff --git a/messages.json b/messages.json index e256dd2..6aafefd 100644 --- a/messages.json +++ b/messages.json @@ -10,4 +10,5 @@ "0.3.7": "messages/0.3.7.md", "0.3.8": "messages/0.3.8.md", "0.3.9": "messages/0.3.9.md" + "0.4.0": "messages/0.4.0.md" } diff --git a/messages/0.3.9.md b/messages/0.3.9.md index 9aa99d1..7c28c2c 100644 --- a/messages/0.3.9.md +++ b/messages/0.3.9.md @@ -1,4 +1,4 @@ -Changes in 0.3.8 +Changes in 0.3.9 ================ - Add build ctags options to sidebar diff --git a/messages/0.4.0.md b/messages/0.4.0.md new file mode 100644 index 0000000..9f4b626 --- /dev/null +++ b/messages/0.4.0.md @@ -0,0 +1,21 @@ +Changes in 0.4.0 +================ + +- Add ranking manager +- Documentation improvements in settings +- Bug Fixes + +Fixes +===== + +N/A + +Resolves +======== + +N/A + +******************************************************************************* + +For more detailed information about these changes, run ``git v0.3.9..v0.4.0`` +on the Git repository found [here](https://github.com/SublimeText/CTags). diff --git a/ranking/__init__.py b/ranking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ranking/parse.py b/ranking/parse.py new file mode 100644 index 0000000..f4b26d0 --- /dev/null +++ b/ranking/parse.py @@ -0,0 +1,94 @@ +import re +from helpers.common import * +#import spdb +# spdb.start() + + +class Parser: + """ + Parses tag references and tag definitions. Used for ranking + """ + @staticmethod + def extract_member_exp(line_to_symbol, source): + """ + Extract receiver object e.g. receiver.mtd() + Strip away brackets and operators. + TODO:HIGH: Add base lang defs + Python/Ruby/C++/Java/C#/PHP overrides (should be very similar) + TODO: comment and string support (eat as may contain brackets. add them to context - js['prop1']['prop-of-prop1']) + """ + lang = get_lang_setting(source) + if not lang: + return [line_to_symbol] + + # Get per-language syntax regex of brackets, splitters etc. + mbr_exp = lang.get('member_exp') + if mbr_exp is None: + return [line_to_symbol] + + lstStop = mbr_exp.get('stop', []) + if (not lstStop): + print('warning!: language has member_exp setting but it is ineffective: Must have "stop" key with array of regex to stop search backward from identifier') + return [line_to_symbol] + + lstClose = mbr_exp.get('close', []) + reClose = concat_re(lstClose) + lstOpen = mbr_exp.get('open', []) + reOpen = concat_re(lstOpen) + lstIgnore = mbr_exp.get('ignore', []) + reIgnore = concat_re(lstIgnore) + if len(lstOpen) != len(lstClose): + print('warning!: extract_member_exp: settings lstOpen must match lstClose') + matchOpenClose = dict(zip(lstOpen, lstClose)) + # Construct | regex from all open and close strings with capture (..) + splex = concat_re(lstOpen + lstClose + lstIgnore + lstStop) + + reStop = concat_re(lstStop) + splex = "({0}|{1})".format(splex, reIgnore) + splat = re.split(splex, line_to_symbol) + #print('splat=%s' % splat) + # Stack iter reverse(splat) for detecting unbalanced e.g 'func(obj.yyy' + # while skipping balanced brackets in getSlow(a && b).mtd() + stack = [] + lstMbr = [] + insideExp = False + for cur in reversed(splat): + # Scan backwards from the symbol: If alpha-numeric - keep it. If + # Closing bracket e.g ] or ) or } --> push into stack + if re.match(reClose, cur): + stack.append(cur) + insideExp = True + # If opening bracket --> match it from top-of-stack: If stack empty + # - stop else If match pop-and-continue else stop scanning + + # warning + elif re.match(reOpen, cur): + # '(' with no matching ')' --> func(obj.yyy case --> return obj.yyy + if len(stack) == 0: + break + tokClose = stack.pop() + tokCloseCur = matchOpenClose.get(cur) + if tokClose != tokCloseCur: + print( + 'non-matching brackets at the same nesting level: %s %s' % + (tokCloseCur, tokClose)) + break + insideExp = False + # If white space --> stop. Do not stop for whitespace inside + # open-close brackets nested expression + elif re.match(reStop, cur): + if not insideExp: + break + elif re.match(reIgnore, cur): + pass + else: + lstMbr[0:0] = cur + + strMbrExp = "".join(lstMbr) + + lstSplit = mbr_exp.get('splitters', []) + reSplit = concat_re(lstSplit) + # Split member deref per-lang (-> and :: in PHP and C++) - use base if + # not found + arrMbrParts = list(filter(None, re.split(reSplit, strMbrExp))) + # print('arrMbrParts=%s' % arrMbrParts) + + return arrMbrParts diff --git a/ranking/rank.py b/ranking/rank.py new file mode 100644 index 0000000..df7ef58 --- /dev/null +++ b/ranking/rank.py @@ -0,0 +1,231 @@ +""" +Rank and Filter support for ctags plugin for Sublime Text 2/3. +""" + +from functools import reduce +import sys +import os +import re +import string + + +from helpers.common import * + + +def compile_definition_filters(view): + filters = [] + for selector, regexes in list( + get_setting('definition_filters', {}).items()): + if view.match_selector(view.sel() and view.sel()[0].begin() or 0, + selector): + filters.append(regexes) + return filters + + +def get_grams(str): + """ + Return a set of tri-grams (each tri-gram is a tuple) given a string: + Ex: 'Dekel' --> {('d', 'e', 'k'), ('k', 'e', 'l'), ('e', 'k', 'e')} + """ + lstr = str.lower() + return set(zip(lstr, lstr[1:], lstr[2:])) + + +class RankMgr: + """ + For each matched Tag, calculates the rank score or filter it out. The remaining matches are sorted by decending score. + """ + + def __init__(self, region, mbrParts, view, symbol, sym_line): + self.region = region + self.mbrParts = mbrParts + self.view = view + # Used by Rank by Definition Types + self.symbol = symbol + self.sym_line = sym_line + + self.lang = get_lang_setting(get_source(view)) + self.mbr_exp = self.lang.get('member_exp', {}) + + self.def_filters = compile_definition_filters(view) + + self.fname_abs = view.file_name().lower() if not( + view.file_name() is None) else None + + mbrGrams = [get_grams(part) for part in mbrParts] + self.setMbrGrams = ( + reduce( + lambda s, + t: s.union(t), + mbrGrams) if mbrGrams else set()) + + def pass_def_filter(self, o): + for f in self.def_filters: + for k, v in list(f.items()): + if k in o: + if re.match(v, o[k]): + return False + return True + + def eq_filename(self, rel_path): + if self.fname_abs is None or rel_path is None: + return False + return self.fname_abs.endswith(rel_path.lstrip('.').lower()) + + def scope_filter(self, taglist): + """ + Given optional scope extended field tag.scope = 'startline:startcol-endline:endcol' - def-scope. + Return: Tuple of 2 Lists: + in_scope: Tags with matching scope: current cursor / caret position is contained in their start-end scope range. + no_scope: Tags without scope or with global scope + Usage: locals, local parameters Tags have scope (ex: in estr.js tag generator for JavaScript) + """ + in_scope = [] + no_scope = [] + for tag in taglist: + if self.region is None or tag.get( + 'scope') is None or tag.scope is None or tag.scope == 'global': + no_scope.append(tag) + continue + + if not self.eq_filename(tag.filename): + continue + + mch = re.search(get_setting('scope_re'), tag.scope) + + if mch: + # .tags file is 1 based and region.begin() is 0 based + beginLine = int(mch.group(1)) - 1 + beginCol = int(mch.group(2)) - 1 + endLine = int(mch.group(3)) - 1 + endCol = int(mch.group(4)) - 1 + beginPoint = self.view.text_point(beginLine, beginCol) + endPoint = self.view.text_point(endLine, endCol) + if self.region.begin() >= beginPoint and self.region.end() <= endPoint: + in_scope.append(tag) + + return (in_scope, no_scope) + + RANK_MATCH_TYPE = 30 + tag_types = None + + def get_type_rank(self, tag): + """ + Rank by Definition Types: Rank Higher matching definitions with types matching to the GotoDef + Use regex to identify the type + """ + # First time - compare current symbol line to the per-language list of regex: Each regex is mapped to 1 or more tag types + # Try all regex to build a list of preferred / higher rank tag types + if self.tag_types is None: + self.tag_types = set() + reference_types = self.lang.get('reference_types', {}) + for re_ref, lstTypes in reference_types.items(): + # replace special keyword __symbol__ with our reference symbol + cur_re = re_ref.replace('__symbol__', self.symbol) + if (re.search(cur_re, self.sym_line)): + self.tag_types = self.tag_types.union(lstTypes) + + return self.RANK_MATCH_TYPE if tag.type in self.tag_types else 0 + + RANK_EQ_FILENAME_RANK = 10 + reThis = None + + def get_samefile_rank(self, rel_path, mbrParts): + """ + If both reference and definition (tag) are in the same file --> Rank this tag higher. + Tag from same file as reference --> Boost rank + Tag from same file as reference and this|self.method() --> Double boost rank + Note: Inheritence model (base class in different file) is not yet supported. + """ + if self.reThis is None: + lstThis = self.mbr_exp.get('this') + if lstThis: + self.reThis = re.compile(concat_re(lstThis), re.IGNORECASE) + elif self.mbr_exp: + print( + 'Warning! Language that has syntax settings is expected to define this|self expression syntax') + + rank = 0 + if self.eq_filename(rel_path): + rank += self.RANK_EQ_FILENAME_RANK + if len(mbrParts) == 1 and self.reThis and self.reThis.match( + mbrParts[-1]): + # this.mtd() - rank candidate from current file very high. + rank += self.RANK_EQ_FILENAME_RANK + return rank + + RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME = 20 + WEIGHT_RIGHTMOST_MBR_PART = 2 + MAX_WEIGHT_GRAM = 3 + WEIGHT_DECAY = 1.5 + + def get_mbr_exp_match_tagfile_rank(self, rel_path, mbrParts): + """ + Object Member Expression File Ranking: Rank higher candiates tags path names that fuzzy match the .method() + Rules: + 1) youtube.fetch() --> mbrPaths = ['youtube'] --> get_rank of tag 'fetch' with rel_path a/b/Youtube.js ---> RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME + 2) vidtube.fetch() --> tag 'fetch' with rel_path google/video/youtube.js ---> fuzzy match of tri-grams of vidtube (vid,idt,dtu,tub,ube) with tri-grams from the path + """ + rank = 0 + if len(mbrParts) == 0: + return rank + + rel_path_no_ext = rel_path.lstrip('.' + os.sep) + rel_path_no_ext = os.path.splitext(rel_path_no_ext)[0] + pathParts = rel_path_no_ext.split(os.sep) + if len(pathParts) >= 1 and len( + mbrParts) >= 1 and pathParts[-1].lower() == mbrParts[-1].lower(): + rank += self.RANK_EXACT_MATCH_RIGHTMOST_MBR_PART_TO_FILENAME + + # Prepare dict of , where weight decays are we move + # further away from the method call (to the left) + pathGrams = [get_grams(part) for part in pathParts] + wt = self.MAX_WEIGHT_GRAM + dctPathGram = {} + for setPathGram in reversed(pathGrams): + dctPathPart = dict.fromkeys(setPathGram, wt) + dctPathGram = merge_two_dicts_shallow(dctPathPart, dctPathGram) + wt /= self.WEIGHT_DECAY + + for mbrGrm in self.setMbrGrams: + rank += dctPathGram.get(mbrGrm, 0) + + return rank + + def get_combined_rank(self, tag, mbrParts): + """ + Calculate rank score per tag, combining several heuristics + """ + rank = 0 + + # Type definition Rank + rank += self.get_type_rank(tag) + + rel_path = tag.tag_path[0] + # Same file and this.method() ranking + rank += self.get_samefile_rank(rel_path, mbrParts) + + # Object Member Expression File Ranking + rank += self.get_mbr_exp_match_tagfile_rank(rel_path, mbrParts) + +# print('rank = %d' % rank); + return rank + + def sort_tags(self, taglist): + # Scope Filter: If symbol matches at least 1 local scope tag - assume they hides non-scope and global scope tags. + # If no local-scope (in_scope) matches --> keep the global / no scope matches (see in sorted_tags) and discard + # the local-scope - because they are not locals of the current position + # If object-receiver (someobj.symbol) --> refer to as global tag --> + # filter out local-scope tags + (in_scope, no_scope) = self.scope_filter(taglist) + if (len(self.setMbrGrams) == 0 and len(in_scope) > + 0): # TODO:Config: @symbol - in Ruby instance var (therefore never local var) + p_tags = in_scope + else: + p_tags = no_scope + + p_tags = list(filter(lambda tag: self.pass_def_filter(tag), p_tags)) + p_tags = sorted( + p_tags, key=lambda tag: self.get_combined_rank( + tag, self.mbrParts), reverse=True) + return p_tags diff --git a/test_ctags.py b/test_ctags.py index 862c91d..c472cc3 100644 --- a/test_ctags.py +++ b/test_ctags.py @@ -1,6 +1,8 @@ #!/usr/bin/env python -"""Unit tests for ctags.py""" +""" +Unit tests for 'ctags.py'. +""" import os import sys @@ -13,27 +15,17 @@ else: import unittest -try: - import sublime - - if int(sublime.version()) > 3000: - from . import ctags - else: - import ctags -except: - import ctags - +import ctags class CTagsTest(unittest.TestCase): - """ - Helper functions - """ - + # + # Helper functions + # def build_python_file(self): - """Build a simple Python "program" that ctags can use. + """ + Build a simple Python "program" that ctags can use. - :Returns: - Path to a constructed, valid Python source file + :returns: Path to a constructed, valid Python source file """ path = '' @@ -52,7 +44,8 @@ def build_python_file(self): return path def build_python_file__extended(self): - """Build a Python "program" demonstrating all common CTag types + """ + Build a Python "program" demonstrating all common CTag types Build a Python program that demonstrates the following CTag types: - ``f`` - function definitions @@ -63,8 +56,7 @@ def build_python_file__extended(self): This is mainly intended to regression test for issue #209. - :Returns: - Path to a constructed, valid Python source file + :returns: Path to a constructed, valid Python source file """ path = '' @@ -94,13 +86,13 @@ def build_python_file__extended(self): return path def build_java_file(self): - """Build a slightly detailed Java "program" that ctags can use. + """ + Build a slightly detailed Java "program" that ctags can use. Build a slightly more detailed program that 'build_python_file' does, in order to test more advanced functionality of ctags.py, or ctags.exe - :Returns: - Path to a constructed, valid Java source file + :returns: Path to a constructed, valid Java source file """ path = '' @@ -128,12 +120,12 @@ def build_java_file(self): return path def build_c_file(self): - """Build a simple C "program" that ctags can use. + """ + Build a simple C "program" that ctags can use. This is mainly intended to regression test for issue #213. - :Returns: - Path to a constructed, valid Java source file + :returns: Path to a constructed, valid C source file """ path = '' @@ -158,24 +150,26 @@ def build_c_file(self): return path - - """ - Test functions - """ + # + # Test functions + # def setUp(self): - """Set up test environment. + """ + Set up test environment. Ensures the ``ctags_not_on_path`` test is run first, and all other tests are skipped if this fails. If ctags is not installed, no test - will pass + will pass. """ self.test_build_ctags__ctags_on_path() - """build ctags""" + # build ctags def test_build_ctags__ctags_on_path(self): - """Checks that ``ctags`` is in ``PATH``""" + """ + Checks that ``ctags`` is in ``PATH``. + """ # build_ctags requires a real path, so we create a temporary file as a # cross-platform way to get the temp directory with tempfile.NamedTemporaryFile() as temp: @@ -186,7 +180,9 @@ def test_build_ctags__ctags_on_path(self): ' on path') def test_build_ctags__custom_command(self): - """Checks for support of simple custom command to execute ctags""" + """ + Checks for support of simple custom command to execute ctags. + """ # build_ctags requires a real path, so we create a temporary file as a # cross-platform way to get the temp directory with tempfile.NamedTemporaryFile() as temp: @@ -197,7 +193,9 @@ def test_build_ctags__custom_command(self): ' on path') def test_build_ctags__invalid_custom_command(self): - """Checks for failure for invalid custom command to execute ctags""" + """ + Checks for failure for invalid custom command to execute ctags. + """ # build_ctags requires a real path, so we create a temporary file as a # cross-platform way to get the temp directory with tempfile.NamedTemporaryFile() as temp: @@ -205,7 +203,9 @@ def test_build_ctags__invalid_custom_command(self): ctags.build_ctags(path=temp.name, cmd='ccttaaggss') def test_build_ctags__single_file(self): - """Test execution of ctags using a single temporary file""" + """ + Test execution of ctags using a single temporary file. + """ path = self.build_python_file() tag_file = ctags.build_ctags(path=path) @@ -224,7 +224,9 @@ def test_build_ctags__single_file(self): os.remove(tag_file) def test_build_ctags__custom_tag_file(self): - """Test execution of ctags using a custom tag file""" + """ + Test execution of ctags using a custom tag file. + """ path = self.build_python_file() tag_file = ctags.build_ctags(path=path, tag_file='my_tag_file') @@ -243,7 +245,9 @@ def test_build_ctags__custom_tag_file(self): os.remove(tag_file) def test_build_ctags__additional_options(self): - """Test execution of ctags using additional ctags options""" + """ + Test execution of ctags using additional ctags options. + """ path = self.build_python_file() tag_file = ctags.build_ctags(path=path, tag_file='my_tag_file', @@ -262,10 +266,11 @@ def test_build_ctags__additional_options(self): os.remove(path) # clean up os.remove(tag_file) - """post_process_tag""" + # post_process_tag def test_post_process_tag__line_numbers(self): - """Test ``post_process_tag`` with a line number ``excmd`` variable. + """ + Test ``post_process_tag`` with a line number ``excmd`` variable. Test function with an sample tag from a Python file. This in turn tests the supporting functions. @@ -290,7 +295,8 @@ def test_post_process_tag__line_numbers(self): self.assertEqual(result, expected_output) def test_post_process_tag__regex_no_fields(self): - """Test ``post_process_tag`` with a regex ``excmd`` variable. + """ + Test ``post_process_tag`` with a regex ``excmd`` variable. Test function with an sample tag from a Python file. This in turn tests the supporting functions. @@ -315,7 +321,8 @@ def test_post_process_tag__regex_no_fields(self): self.assertEqual(result, expected_output) def test_post_process_tag__fields(self): - """Test ``post_process_tag`` with a number of ``field`` variables. + """ + Test ``post_process_tag`` with a number of ``field`` variables. Test function with an sample tag from a Java file. This in turn tests the supporting functions. @@ -342,11 +349,12 @@ def test_post_process_tag__fields(self): self.assertEqual(result, expected_output) - - """Tag class""" + # Tag class def test_parse_tag_lines__python(self): - """Test ``parse_tag_lines`` with a sample Python file""" + """ + Test ``parse_tag_lines`` with a sample Python file. + """ path = self.build_python_file__extended() tag_file = ctags.build_ctags(path=path, opts=['--python-kinds=-i']) @@ -355,7 +363,7 @@ def test_parse_tag_lines__python(self): try: content = output.readlines() filename = os.path.basename(path) - except: + except IOError: self.fail("Setup of files for test failed") finally: output.close() @@ -422,7 +430,9 @@ def test_parse_tag_lines__python(self): self.assertEqual(expected_outputs[key], result[key]) def test_parse_tag_lines__c(self): - """Test ``parse_tag_lines`` with a sample C file""" + """ + Test ``parse_tag_lines`` with a sample C file. + """ path = self.build_c_file() tag_file = ctags.build_ctags(path=path) @@ -474,6 +484,5 @@ def test_parse_tag_lines__c(self): for key in result: # don't forget - we might have missed something! self.assertEqual(expected_outputs[key], result[key]) - if __name__ == '__main__': unittest.main() diff --git a/test_ctagsplugin.py b/test_ctagsplugin.py index ad74d31..394b458 100644 --- a/test_ctagsplugin.py +++ b/test_ctagsplugin.py @@ -1,6 +1,8 @@ #!/usr/bin/env python -"""Unit tests for ctagsplugin.py""" +""" +Unit tests for 'ctagsplugin.py'. +""" import os import sys @@ -12,28 +14,17 @@ else: import unittest -try: - import sublime - - if int(sublime.version()) > 3000: - from . import ctagsplugin - from . import ctags - else: - import ctagsplugin - import ctags -except: - import ctagsplugin - import ctags - +import ctags +import ctagsplugin class CTagsPluginTest(unittest.TestCase): - - """ - Helper functions - """ + # + # Helper functions. + # def make_tmp_directory(self, pwd=None): - """Make a temporary directory to place files in + """ + Make a temporary directory to place files in. :returns: Path to the temporary directory """ @@ -41,10 +32,10 @@ def make_tmp_directory(self, pwd=None): return tmp_dir def build_python_file(self, pwd=None): - """Build a simple Python "program" that ctags can use. + """ + Build a simple Python "program" that ctags can use. - :Returns: - Path to a constructed, valid Java source file + :returns: Path to a constructed, valid Java source file """ path = '' @@ -64,13 +55,13 @@ def build_python_file(self, pwd=None): return path def build_java_file(self, pwd=None): - """Build a slightly detailed Java "program" that ctags can use. + """ + Build a slightly detailed Java "program" that ctags can use. Build a slightly more detailed program that 'build_python_file' does, in order to test more advanced functionality of ctags.py, or ctags.exe - :Returns: - Path to a constructed, valid Java source file + :returns: Path to a constructed, valid Java source file """ path = '' @@ -99,7 +90,8 @@ def build_java_file(self, pwd=None): return path def remove_tmp_directory(self, path): - """Remove a temporary directory made by ``make_tmp_directory`` + """ + Remove a temporary directory made by ``make_tmp_directory`` :param path: Path to directory @@ -108,7 +100,8 @@ def remove_tmp_directory(self, path): shutil.rmtree(path) def remove_tmp_files(self, paths): - """Remove temporary files made by ``make_x_file`` + """ + Remove temporary files made by ``make_x_file`` :param paths: Path to file @@ -117,45 +110,45 @@ def remove_tmp_files(self, paths): for path in paths: os.remove(path) - """ - Test functions - """ + # + # Test functions + # - """find_tags_relative_to""" + # find_tags_relative_to def test_find_tags_relative_to__find_tags_in_current_directory(self): - TAG_FILE = 'example_tags' + tag_file = 'example_tags' current_path = self.build_python_file() - tag_file = ctags.build_ctags(path=current_path, tag_file=TAG_FILE) + tag_file_ = ctags.build_ctags(path=current_path, tag_file=tag_file) # should find tag file in current directory self.assertEqual( - ctagsplugin.find_tags_relative_to(current_path, TAG_FILE), - tag_file) + ctagsplugin.find_tags_relative_to(current_path, tag_file), + tag_file_) # cleanup - self.remove_tmp_files([current_path, tag_file]) + self.remove_tmp_files([current_path, tag_file_]) def test_find_tags_relative_to__find_tags_in_parent_directory(self): - TAG_FILE = 'example_tags' + tag_file = 'example_tags' parent_path = self.build_python_file() parent_tag_file = ctags.build_ctags(path=parent_path, - tag_file=TAG_FILE) + tag_file=tag_file) child_dir = self.make_tmp_directory() child_path = self.build_python_file(pwd=child_dir) # should find tag file in parent directory self.assertEqual( - ctagsplugin.find_tags_relative_to(child_path, TAG_FILE), + ctagsplugin.find_tags_relative_to(child_path, tag_file), parent_tag_file) # cleanup self.remove_tmp_files([parent_path, parent_tag_file]) self.remove_tmp_directory(child_dir) - """get_common_ancestor_folder""" + # get_common_ancestor_folder def test_get_common_ancestor_folder__current_folder_open(self): parent_dir = '/c/users' @@ -206,7 +199,7 @@ def test_get_common_ancestor_folder__single_child_folder_open(self): # should return child directory as the deepest common folder self.assertEqual(path, child_dir) - """get_rel_path_to_source""" + # get_rel_path_to_source def test_get_rel_path_to_source__source_file_in_sibling_directory(self): temp = '/c/users/temporary_file' @@ -229,9 +222,7 @@ def test_get_rel_path_to_source__source_file_in_child_directory(self): # handle [windows, unix] paths relative_paths = ['folder\\temporary_file', 'folder/temporary_file'] - #self.assertEquals([relative_path], result) self.assertIn(result[0], relative_paths) - if __name__ == '__main__': unittest.main() diff --git a/tests/data/Main.java b/tests/data/Main.java new file mode 100644 index 0000000..ee0c1e4 --- /dev/null +++ b/tests/data/Main.java @@ -0,0 +1,6 @@ +class Main { + public static void main(String[] args) { + //get_settings + } + +} \ No newline at end of file diff --git a/tests/sublime_fake.py b/tests/sublime_fake.py index e50ad4f..bf2ee89 100644 --- a/tests/sublime_fake.py +++ b/tests/sublime_fake.py @@ -1,12 +1,10 @@ class sublime(object): - - '''Constants''' - + """ + Mock object for ``sublime`` class in Sublime Text. + """ LITERAL = '' VERSION = '2.0' - '''Functions''' - def load_settings(self, **kargs): pass @@ -14,17 +12,14 @@ def load_settings(self, **kargs): def version(): return sublime.VERSION - class sublime_plugin(object): - - '''Constants''' - + """ + Mock object for ``sublime_plugin`` class in Sublime Text. + """ all_callbacks = { 'on_load': [] } - '''Classes''' - class WindowCommand(object): pass