""" (c) Craig Emery 2017-2022 AutoTag.py """ from __future__ import print_function import sys import os import fileinput import logging from collections import defaultdict import subprocess from traceback import format_exc import multiprocessing as mp from glob import glob import vim # pylint: disable=import-error __all__ = ["autotag"] # global vim config variables used (all are g:autotag): # name purpose # ExcludeSuffixes suffixes to not ctags on # VerbosityLevel logging verbosity (as in Python logging module) # CtagsCmd name of ctags command # TagsFile name of tags file to look for # Disabled Disable autotag (enable by setting to any non-blank value) # StopAt stop looking for a tags file (and make one) at this directory (defaults to $HOME) GLOBALS_DEFAULTS = dict(ExcludeSuffixes="tml.xml.text.txt", VerbosityLevel=logging.WARNING, CtagsCmd="ctags", TagsFile=".tags", TagsDir="", Disabled=0, StopAt=0, StartMethod="") def do_cmd(cmd, cwd): """ Abstract subprocess """ with subprocess.Popen(cmd, cwd=cwd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) as proc: stdout = proc.communicate()[0] return stdout.split("\n") def vim_global(name, kind=str): """ Get global variable from vim, cast it appropriately """ ret = GLOBALS_DEFAULTS.get(name, None) try: vname = "autotag" + name v_buffer = "b:" + vname exists_buffer = (vim.eval(f"exists('{v_buffer}')") == "1") v_global = "g:" + vname exists_global = (vim.eval(f"exists('{v_global}')") == "1") if exists_buffer: ret = vim.eval(v_buffer) elif exists_global: ret = vim.eval(v_global) else: if isinstance(ret, int): vim.command(f"let {v_global}={ret}") else: vim.command(f"let {v_global}=\"{ret}\"") finally: if kind == bool: ret = (ret in [1, "1", "true", "yes"]) elif kind == int: try: val = int(ret) except TypeError: val = ret except ValueError: val = ret ret = val elif kind == str: ret = str(ret) return ret def init_multiprocessing(): """ Init multiprocessing, set_executable() & get the context we'll use """ wanted_start_method = vim_global("StartMethod") or None used_start_method = mp.get_start_method() if wanted_start_method in mp.get_all_start_methods(): used_start_method = wanted_start_method else: wanted_start_method = None # here wanted_start_method is either a valid method or None # used_start_method is what the module has as the default or our overriden value ret = mp.get_context(wanted_start_method) # wanted_start_method might be None try: mp.set_executable except AttributeError: return ret if used_start_method == 'spawn': suff = os.path.splitext(sys.executable)[1] pat1 = f"python*{suff}" pat2 = os.path.join("bin", pat1) exes = glob(os.path.join(sys.exec_prefix, pat1)) + glob(os.path.join(sys.exec_prefix, pat2)) if exes: win = [exe for exe in exes if exe.endswith(f"w{suff}")] if win: # In Windows pythonw.exe is best ret.set_executable(win[0]) else: # This isn't great, for now pick the first one ret.set_executable(exes[0]) return ret CTX = init_multiprocessing() class VimAppendHandler(logging.Handler): """ Logger handler that finds a buffer and appends the log message as a new line """ def __init__(self, name): logging.Handler.__init__(self) self.__name = name self.__formatter = logging.Formatter() def __find_buffer(self): """ Look for the named buffer """ for buff in vim.buffers: if buff and buff.name and buff.name.endswith(self.__name): yield buff def emit(self, record): """ Emit the logging message """ for buff in self.__find_buffer(): buff.append(self.__formatter.format(record)) def set_logger_verbosity(): """ Set the verbosity of the logger """ level = vim_global("VerbosityLevel", kind=int) LOGGER.setLevel(level) def make_and_add_handler(logger, name): """ Make the handler and add it to the standard logger """ ret = VimAppendHandler(name) logger.addHandler(ret) return ret try: LOGGER except NameError: DEBUG_NAME = "autotag_debug" LOGGER = logging.getLogger(DEBUG_NAME) HANDLER = make_and_add_handler(LOGGER, DEBUG_NAME) set_logger_verbosity() class AutoTag(): # pylint: disable=too-many-instance-attributes """ Class that does auto ctags updating """ LOG = LOGGER AUTOFILETYPES = ["python"] FILETYPES = {} def __init__(self): self.locks = {} self.tags = defaultdict(list) self.excludesuffix = ["." + s for s in vim_global("ExcludeSuffixes").split(".")] self.excludefiletype = vim_global("ExcludeFiletypes").split(",") set_logger_verbosity() self.sep_used_by_ctags = '/' self.ctags_cmd = vim_global("CtagsCmd") self.tags_file = str(vim_global("TagsFile")) self.tags_dir = str(vim_global("TagsDir")) self.parents = os.pardir * (len(os.path.split(self.tags_dir)) - 1) self.count = 0 self.stop_at = vim_global("StopAt") def find_tag_file(self, source): """ Find the tag file that belongs to the source file """ AutoTag.LOG.info('source = "%s"', source) (drive, fname) = os.path.splitdrive(source) ret = None while ret is None: fname = os.path.dirname(fname) AutoTag.LOG.info('drive = "%s", file = "%s"', drive, fname) tags_dir = os.path.join(drive, fname) tags_file = os.path.join(tags_dir, self.tags_dir, self.tags_file) AutoTag.LOG.info('testing tags_file "%s"', tags_file) if os.path.isfile(tags_file): stinf = os.stat(tags_file) if stinf: size = getattr(stinf, 'st_size', None) if size is None: AutoTag.LOG.warning("Could not stat tags file %s", tags_file) ret = "" ret = (fname, tags_file) elif tags_dir and tags_dir == self.stop_at: AutoTag.LOG.info("Reached %s. Making one %s", self.stop_at, tags_file) open(tags_file, 'wb').close() ret = (fname, tags_file) ret = "" elif not fname or fname == os.sep or fname == "//" or fname == "\\\\": AutoTag.LOG.info('bail (file = "%s")', fname) ret = "" return ret or None def add_source(self, source, filetype): """ Make a note of the source file, ignoring some etc """ if not source: AutoTag.LOG.warning('No source') return if os.path.basename(source) == self.tags_file: AutoTag.LOG.info("Ignoring tags file %s", self.tags_file) return suff = os.path.splitext(source)[1] if suff: AutoTag.LOG.info("Source %s has suffix %s, so filetype doesn't count!", source, suff) filetype = None else: AutoTag.LOG.info("Source %s has no suffix, so filetype counts!", source) if suff in self.excludesuffix: AutoTag.LOG.info("Ignoring excluded suffix %s for file %s", suff, source) return if filetype in self.excludefiletype: AutoTag.LOG.info("Ignoring excluded filetype %s for file %s", filetype, source) return found = self.find_tag_file(source) if found: (tags_dir, tags_file) = found relative_source = os.path.splitdrive(source)[1][len(tags_dir):] if relative_source[0] == os.sep: relative_source = relative_source[1:] if os.sep != self.sep_used_by_ctags: relative_source = relative_source.replace(os.sep, self.sep_used_by_ctags) key = (tags_dir, tags_file, filetype) self.tags[key].append(relative_source) if key not in self.locks: self.locks[key] = CTX.Lock() @staticmethod def good_tag(line, excluded): """ Filter method for stripping tags """ if line[0] == '!': return True fields = line.split('\t') AutoTag.LOG.log(1, "read tags line:%s", str(fields)) if len(fields) > 3 and fields[1] not in excluded: return True return False def strip_tags(self, tags_file, sources): """ Strip all tags for a given source file """ AutoTag.LOG.info("Stripping tags for %s from tags file %s", ",".join(sources), tags_file) backup = ".SAFE" try: with fileinput.FileInput(files=tags_file, inplace=True, backup=backup) as source: for line in source: line = line.strip() if self.good_tag(line, sources): print(line) finally: try: os.unlink(tags_file + backup) except IOError: pass def _vim_ft_to_ctags_ft(self, name): """ convert vim filetype strings to ctags strings """ if name in AutoTag.AUTOFILETYPES: return name return self.FILETYPES.get(name, None) def update_tags_file(self, key, sources): """ Strip all tags for the source file, then re-run ctags in append mode """ (tags_dir, tags_file, filetype) = key lock = self.locks[key] if self.tags_dir: sources = [os.path.join(self.parents + s) for s in sources] cmd = [self.ctags_cmd] if self.tags_file: cmd += ["-f", self.tags_file] if filetype: ctags_filetype = self._vim_ft_to_ctags_ft(filetype) if ctags_filetype: cmd += [f"--language-force={ctags_filetype}"] cmd += ["-a"] def is_file(src): """ inner """ return os.path.isfile(os.path.join(tags_dir, self.tags_dir, src)) srcs = list(filter(is_file, sources)) if not srcs: return cmd += srcs cmd = " ".join(cmd) with lock: self.strip_tags(tags_file, sources) AutoTag.LOG.log(1, "%s: %s", tags_dir, cmd) for line in do_cmd(cmd, self.tags_dir or tags_dir): AutoTag.LOG.log(10, line) def rebuild_tag_files(self): """ rebuild the tags file thread worker """ for (key, sources) in self.tags.items(): AutoTag.LOG.info('Process(%s, %s)', key, ",".join(sources)) proc = CTX.Process(target=self.update_tags_file, args=(key, sources)) proc.daemon = True proc.start() def autotag(): """ Do the work """ try: if not vim_global("Disabled", bool): runner = AutoTag() runner.add_source(vim.eval("expand(\"%:p\")"), vim.eval("&ft")) runner.rebuild_tag_files() except Exception: # pylint: disable=broad-except logging.warning(format_exc())