from gosubl import gs from gosubl import gsq from gosubl import gsshell from gosubl import mg9 import os import re import sublime import sublime_plugin import threading import time DOMAIN = 'GsLint' CL_DOMAIN = 'GsCompLint' class FileRef(object): def __init__(self, view): self.view = view self.src = '' self.tm = 0.0 self.state = 0 self.reports = {} class Report(object): def __init__(self, row, col, msg): self.row = row self.col = col self.msg = msg class GsLintThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) self.daemon = True self.sem = threading.Semaphore() self.s = set() self.q = gs.queue.Queue() def putq(self, fn): with self.sem: if fn in self.s: return False self.s.add(fn) self.q.put(fn) return True def popq(self): fn = self.q.get() with self.sem: self.s.discard(fn) return fn def run(self): while True: fn = self.popq() fr = ref(fn, False) if fr: reports = {} res, _ = mg9.bcall('lint', { 'dir': (os.path.dirname(fn) if fn else ''), 'fn': fn, 'src': fr.src, 'filter': gs.setting('lint_filter', []), }) res = gs.dval(res, {}) for r in gs.dval(res.get('reports'), []): if fn and fn != '' and r.get('Fn') != fn: continue kind = r.get('Kind', '') row = r.get('Row', 0) col = r.get('Col', 0) msg = r.get('Message', '') msg = '%s: %s' % (kind, msg) if row >= 0 and msg: reports[row] = Report(row, col, msg) fr = ref(fn, False) if fr: with sem: fr.state = 1 fr.reports = reports file_refs[fn] = fr def highlight(fr): sel = gs.sel(fr.view).begin() row, _ = fr.view.rowcol(sel) if fr.state == 1: fr.state = 0 cleanup(fr.view) regions = [] regions0 = [] domain0 = DOMAIN+'-zero' for r in fr.reports.values(): line = fr.view.line(fr.view.text_point(r.row, 0)) pos = line.begin() + r.col if pos >= line.end(): pos = line.end() if pos == line.begin(): regions0.append(sublime.Region(pos, pos)) else: regions.append(sublime.Region(pos, pos)) if regions: fr.view.add_regions(DOMAIN, regions, 'comment', 'dot', sublime.DRAW_EMPTY_AS_OVERWRITE) else: fr.view.erase_regions(DOMAIN) if regions0: fr.view.add_regions(domain0, regions0, 'comment', 'dot', sublime.HIDDEN) else: fr.view.erase_regions(domain0) msg = '' reps = fr.reports.copy() l = len(reps) if l > 0: msg = '%s (%d)' % (DOMAIN, l) r = reps.get(row) if r and r.msg: msg = '%s: %s' % (msg, r.msg) if fr.state != 0: msg = u'\u231B %s' % msg fr.view.set_status(DOMAIN, msg) def cleanup(view): view.set_status(DOMAIN, '') view.erase_regions(DOMAIN) view.erase_regions(DOMAIN+'-zero') def watch(): global file_refs global th view = gs.active_valid_go_view() if view is not None and view.file_name(): fn = view.file_name() if fn.endswith(".gohtml"): return if view is not None and (view.file_name() and gs.setting('comp_lint_enabled') is True): fn = view.file_name() fr = ref(fn) with sem: if fr: fr.view = view highlight(fr) sublime.set_timeout(watch, 500) return if gs.setting('gslint_enabled') is not True: if view: with sem: for fn in file_refs: fr = file_refs[fn] cleanup(fr.view) file_refs = {} sublime.set_timeout(watch, 2000) return if view and not view.is_loading(): fn = view.file_name() fr = ref(fn) with sem: if fr: # always use the active view (e.g in split-panes) fr.view = view highlight(fr) else: fr = FileRef(view) file_refs[fn] = fr if fr.state == 0: src = view.substr(sublime.Region(0, view.size())) if src != fr.src: fr.src = src fr.tm = time.time() if fr.tm > 0.0: timeout = int(gs.setting('gslint_timeout', 500)) delta = int((time.time() - fr.tm) * 1000.0) if delta >= timeout: fr.tm = 0.0 fr.state = -1 if not th: th = GsLintThread() th.start() th.putq(fn) sublime.set_timeout(watch, 500) def ref(fn, validate=True): with sem: if validate: for fn in list(file_refs.keys()): fr = file_refs[fn] if not fr.view.window() or fn != fr.view.file_name(): del file_refs[fn] return file_refs.get(fn) def delref(fn): with sem: if fn in file_refs: del file_refs[fn] def do_comp_lint(dirname, fn): fr = ref(fn, False) reports = {} if not fr: return fn = gs.apath(fn, dirname) bindir, _ = gs.temp_dir('bin') local_env = { 'GOBIN': bindir, } pat = r'%s:(\d+)(?:[:](\d+))?\W+(.+)\s*' % re.escape(os.path.basename(fn)) pat = re.compile(pat, re.IGNORECASE) for c in gs.setting('comp_lint_commands'): try: cmd = c.get('cmd') if not cmd: continue cmd_domain = ' '.join(cmd) shell = c.get('shell') is True env = {} if c.get('global') is True else local_env out, err, _ = gsshell.run(cmd=cmd, shell=shell, cwd=dirname, env=env) if err: gs.notice(DOMAIN, err) out = out.replace('\r', '').replace('\n ', '\\n ').replace('\n\t', '\\n\t') for m in pat.findall(out): try: row, col, msg = m row = int(row)-1 col = int(col)-1 if col else 0 msg = msg.replace('\\n', '\n').strip() if row >= 0 and msg: msg = '%s: %s' % (cmd_domain, msg) if reports.get(row): reports[row].msg = '%s\n%s' % (reports[row].msg, msg) reports[row].col = max(reports[row].col, col) else: reports[row] = Report(row, col, msg) except: pass except: gs.notice(DOMAIN, gs.traceback()) def cb(): fr.reports = reports fr.state = 1 highlight(fr) sublime.set_timeout(cb, 0) class GsCompLintCommand(sublime_plugin.TextCommand): def run(self, edit): if gs.setting('comp_lint_enabled') is not True: return fn = self.view.file_name() fn = os.path.abspath(fn) if fn: dirname = gs.basedir_or_cwd(fn) file_refs[fn] = FileRef(self.view) gsq.dispatch(CL_DOMAIN, lambda: do_comp_lint(dirname, fn), '') try: th except: th = None sem = threading.Semaphore() file_refs = {} watch()