class Linter: __metaclass__ = Tracker language = "" cmd = () regex = "" languages = {} linters = {} def __init__(self, view, syntax, filename="untitled"): self.view = view self.syntax = syntax self.filename = filename if self.regex: self.regex = re.compile(self.regex) @classmethod def add_subclass(cls, sub, name, attrs): if name: sub.name = name cls.languages[name] = sub @classmethod def assign(cls, view): """ find a linter for a specified view if possible, then add it to our mapping of view <--> lint class and return it each view has its own linter to make it feasible for linters to store persistent data about a view """ id = view.id() settings = view.settings() syn = settings.get("syntax") match = syntax_re.search(syn) if match: syntax, = match.groups() else: syntax = syn if syntax: if id in cls.linters and cls.linters[id]: if tuple(cls.linters[id])[0].syntax == syntax: return linters = set() for entry in cls.languages.values(): if entry.can_lint(syntax): linter = entry(view, syntax) linters.add(linter) if linters: cls.linters[id] = linters else: if id in cls.linters: del cls.linters[id] return linters @classmethod def reload(cls, mod): """ reload all linters originating from a specific module (follows a module reload) """ for id, linters in cls.linters.items(): for linter in linters: if linter.__module__ == mod: cls.linters[id].remove(linter) linter = cls.languages[linter.name](linter.view, linter.syntax) cls.linters[id].add(linter) return @classmethod def text(cls, view): return view.substr(sublime.Region(0, view.size())).encode("utf-8") @classmethod def lint_view(cls, view_id, code, callback): if view_id in cls.linters: linters = tuple(cls.linters[view_id]) for linter in linters: linter.lint(code) # merge our result back to the main thread sublime.set_timeout(lambda: callback(linters[0].view, linters), 0) @classmethod def get_view(self, view_id): if view_id in self.linters: return tuple(self.linters[view_id])[0].view def lint(self, code=None): if not (self.language and self.cmd and self.regex): raise NotImplementedError if code is None: code = Linter.text(self.view) self.highlight = Highlight(code) self.errors = errors = {} if not code: return output = self.communicate(self.cmd, code) print repr(output) for line in output.splitlines(): line = line.strip() match, row, col, message, near = self.match_error(self.regex, line) if match: if row or row is 0: if col or col is 0: self.highlight.range(row, col) elif near: self.highlight.near(row, near) else: self.highlight.line(row) if row in errors: errors[row].append(message) else: errors[row] = [message] def draw(self, prefix="lint"): self.highlight.draw(self.view, prefix) def clear(self, prefix="lint"): self.highlight.clear(self.view, prefix) # helper methods @classmethod def can_lint(cls, language): if language.lower() == cls.language: return True else: return False def error(self, line, error): self.highlight.line(line) error = str(error) if line in self.errors: self.errors[line].append(error) else: self.errors[line] = [error] def match_error(self, r, line): match = r.match(line) if match: items = {"row": None, "col": None, "error": "", "near": None} items.update(match.groupdict()) error, row, col, near = [items[k] for k in ("error", "line", "col", "near")] row = int(row) - 1 return match, row, col, error, near return match, None, None, "", None # popen methods def communicate(self, cmd, code): out = self.popen(cmd).communicate(code) return (out[0] or "") + (out[1] or "") def tmpfile(self, cmd, code, suffix=""): f = tempfile.NamedTemporaryFile(suffix=suffix) f.write(code) f.flush() cmd = tuple(cmd) + (f.name,) out = self.popen(cmd).communicate("") return (out[0] or "") + (out[1] or "") def popen(self, cmd): if isinstance(cmd, basestring): cmd = (cmd,) info = None if os.name == "nt": info = subprocess.STARTUPINFO() info.dwFlags |= subprocess.STARTF_USESHOWWINDOW info.wShowWindow = subprocess.SW_HIDE return subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=info )
class Linter: __metaclass__ = Tracker language = '' cmd = () regex = '' multiline = False flags = 0 tab_size = 1 scope = 'keyword' selector = None outline = True needs_api = False languages = {} linters = {} errors = None highlight = None def __init__(self, view, syntax, filename=None): self.view = view self.syntax = syntax self.filename = filename if self.regex: if self.multiline: self.flags |= re.MULTILINE self.regex = re.compile(self.regex, self.flags) self.highlight = Highlight(scope=self.scope) @classmethod def add_subclass(cls, sub, name, attrs): if name: sub.name = name cls.languages[name] = sub @classmethod def assign(cls, view): ''' find a linter for a specified view if possible, then add it to our mapping of view <--> lint class and return it each view has its own linter to make it feasible for linters to store persistent data about a view ''' try: vid = view.id() except RuntimeError: pass settings = view.settings() syn = settings.get('syntax') if not syn: cls.remove(vid) return match = syntax_re.search(syn) if match: syntax, = match.groups() else: syntax = syn if syntax: if vid in cls.linters and cls.linters[vid]: if tuple(cls.linters[vid])[0].syntax == syntax: return linters = set() for name, entry in cls.languages.items(): if entry.can_lint(syntax): linter = entry(view, syntax, view.file_name()) linters.add(linter) if linters: cls.linters[vid] = linters return linters cls.remove(vid) @classmethod def remove(cls, vid): if vid in cls.linters: for linter in cls.linters[vid]: linter.clear() del cls.linters[vid] @classmethod def reload(cls, mod): ''' reload all linters originating from a specific module (follows a module reload) ''' for id, linters in cls.linters.items(): for linter in linters: if linter.__module__ == mod: linter.clear() cls.linters[id].remove(linter) linter = cls.languages[linter.name](linter.view, linter.syntax, linter.filename) cls.linters[id].add(linter) linter.draw() return @classmethod def text(cls, view): return view.substr(sublime.Region(0, view.size())).encode('utf-8') @classmethod def lint_view(cls, view_id, filename, code, sections, callback): if view_id in cls.linters: selectors = Linter.get_selectors(view_id) linters = tuple(cls.linters[view_id]) for linter in linters: if not linter.selector: linter.filename = filename linter.pre_lint(code) for sel, linter in selectors: if sel in sections: highlight = Highlight(code, scope=linter.scope, outline=linter.outline) errors = {} for line_offset, left, right in sections[sel]: highlight.shift(line_offset, left) linter.pre_lint(code[left:right], highlight=highlight) for line, error in linter.errors.items(): errors[line+line_offset] = error linter.errors = errors # merge our result back to the main thread sublime.set_timeout(lambda: callback(linters[0].view, linters), 0) @classmethod def get_view(cls, view_id): if view_id in cls.linters: return tuple(cls.linters[view_id])[0].view @classmethod def get_linters(cls, view_id): if view_id in cls.linters: return tuple(cls.linters[view_id]) return () @classmethod def get_selectors(cls, view_id): return [(linter.selector, linter) for linter in cls.get_linters(view_id) if linter.selector] def pre_lint(self, code, highlight=None): self.errors = {} self.highlight = highlight or Highlight(code, scope=self.scope, outline=self.outline) if not code: return # if this linter needs the api, we want to merge back into the main thread # but stall this thread until it's done so we still have the return if self.needs_api: q = Queue() def callback(): q.get() self.lint(code) q.task_done() q.put(1) sublime.set_timeout(callback, 1) q.join() else: self.lint(code) def lint(self, code): if not (self.language and self.cmd and self.regex): raise NotImplementedError output = self.communicate(self.cmd, code) if output: persist.debug('Output:', repr(output)) for match, row, col, message, near in self.find_errors(output): if match: if row or row is 0: if col or col is 0: # adjust column numbers to match the linter's tabs if necessary if self.tab_size > 1: start, end = self.highlight.full_line(row) code_line = code[start:end] diff = 0 for i in xrange(len(code_line)): if code_line[i] == '\t': diff += (self.tab_size - 1) if col - diff <= i: col = i break self.highlight.range(row, col) elif near: self.highlight.near(row, near) else: self.highlight.line(row) self.error(row, message) def draw(self, prefix='lint'): self.highlight.draw(self.view, prefix) def clear(self, prefix='lint'): self.highlight.clear(self.view, prefix) # helper methods @classmethod def can_lint(cls, language): language = language.lower() if cls.language: if language == cls.language: return True elif isinstance(cls.language, (tuple, list)) and language in cls.language: return True else: return False def error(self, line, error): self.highlight.line(line) error = str(error) if line in self.errors: self.errors[line].append(error) else: self.errors[line] = [error] def find_errors(self, output): if self.multiline: errors = self.regex.finditer(output) if errors: for error in errors: yield self.split_match(error) else: yield self.split_match(None) else: for line in output.splitlines(): yield self.match_error(self.regex, line.strip()) def split_match(self, match): if match: items = {'row':None, 'col':None, 'error':'', 'near':None} items.update(match.groupdict()) error, row, col, near = [items[k] for k in ('error', 'line', 'col', 'near')] row = int(row) - 1 if col: col = int(col) - 1 return match, row, col, error, near return match, None, None, '', None def match_error(self, r, line): return self.split_match(r.match(line)) # popen wrappers def communicate(self, cmd, code): return util.communicate(cmd, code) def tmpfile(self, cmd, code, suffix=''): return util.tmpfile(cmd, code, suffix) def tmpdir(self, cmd, files, code): return util.tmpdir(cmd, files, self.filename, code) def popen(self, cmd, env=None): return util.popen(cmd, env)
class Linter: __metaclass__ = Tracker language = '' cmd = () regex = '' multiline = False flags = 0 tab_size = 1 scope = 'keyword' selector = None outline = True needs_api = False languages = {} linters = {} errors = None highlight = None def __init__(self, view, syntax, filename=None): self.view = view self.syntax = syntax self.filename = filename if self.regex: if self.multiline: self.flags |= re.MULTILINE self.regex = re.compile(self.regex, self.flags) self.highlight = Highlight(scope=self.scope) @classmethod def add_subclass(cls, sub, name, attrs): if name: sub.name = name cls.languages[name] = sub @classmethod def assign(cls, view): ''' find a linter for a specified view if possible, then add it to our mapping of view <--> lint class and return it each view has its own linter to make it feasible for linters to store persistent data about a view ''' try: vid = view.id() except RuntimeError: pass settings = view.settings() syn = settings.get('syntax') if not syn: cls.remove(vid) return match = syntax_re.search(syn) if match: syntax, = match.groups() else: syntax = syn if syntax: if vid in cls.linters and cls.linters[vid]: if tuple(cls.linters[vid])[0].syntax == syntax: return linters = set() for name, entry in cls.languages.items(): if entry.can_lint(syntax): linter = entry(view, syntax, view.file_name()) linters.add(linter) if linters: cls.linters[vid] = linters return linters cls.remove(vid) @classmethod def remove(cls, vid): if vid in cls.linters: for linter in cls.linters[vid]: linter.clear() del cls.linters[vid] @classmethod def reload(cls, mod): ''' reload all linters originating from a specific module (follows a module reload) ''' for id, linters in cls.linters.items(): for linter in linters: if linter.__module__ == mod: linter.clear() cls.linters[id].remove(linter) linter = cls.languages[linter.name](linter.view, linter.syntax, linter.filename) cls.linters[id].add(linter) linter.draw() return @classmethod def text(cls, view): return view.substr(sublime.Region(0, view.size())).encode('utf-8') @classmethod def lint_view(cls, view_id, filename, code, sections, callback): if view_id in cls.linters: selectors = Linter.get_selectors(view_id) linters = tuple(cls.linters[view_id]) for linter in linters: if not linter.selector: linter.filename = filename linter.pre_lint(code) for sel, linter in selectors: if sel in sections: highlight = Highlight(code, scope=linter.scope, outline=linter.outline) errors = {} for line_offset, left, right in sections[sel]: highlight.shift(line_offset, left) linter.pre_lint(code[left:right], highlight=highlight) for line, error in linter.errors.items(): errors[line + line_offset] = error linter.errors = errors # merge our result back to the main thread sublime.set_timeout(lambda: callback(linters[0].view, linters), 0) @classmethod def get_view(cls, view_id): if view_id in cls.linters: return tuple(cls.linters[view_id])[0].view @classmethod def get_linters(cls, view_id): if view_id in cls.linters: return tuple(cls.linters[view_id]) return () @classmethod def get_selectors(cls, view_id): return [(linter.selector, linter) for linter in cls.get_linters(view_id) if linter.selector] def pre_lint(self, code, highlight=None): self.errors = {} self.highlight = highlight or Highlight( code, scope=self.scope, outline=self.outline) if not code: return # if this linter needs the api, we want to merge back into the main thread # but stall this thread until it's done so we still have the return if self.needs_api: q = Queue() def callback(): q.get() self.lint(code) q.task_done() q.put(1) sublime.set_timeout(callback, 1) q.join() else: self.lint(code) def lint(self, code): if not (self.language and self.cmd and self.regex): raise NotImplementedError output = self.communicate(self.cmd, code) if output: persist.debug('Output:', repr(output)) for match, row, col, message, near in self.find_errors(output): if match: if row or row is 0: if col or col is 0: # adjust column numbers to match the linter's tabs if necessary if self.tab_size > 1: start, end = self.highlight.full_line(row) code_line = code[start:end] diff = 0 for i in xrange(len(code_line)): if code_line[i] == '\t': diff += (self.tab_size - 1) if col - diff <= i: col = i break self.highlight.range(row, col) elif near: self.highlight.near(row, near) else: self.highlight.line(row) self.error(row, message) def draw(self, prefix='lint'): self.highlight.draw(self.view, prefix) def clear(self, prefix='lint'): self.highlight.clear(self.view, prefix) # helper methods @classmethod def can_lint(cls, language): language = language.lower() if cls.language: if language == cls.language: return True elif isinstance(cls.language, (tuple, list)) and language in cls.language: return True else: return False def error(self, line, error): self.highlight.line(line) error = str(error) if line in self.errors: self.errors[line].append(error) else: self.errors[line] = [error] def find_errors(self, output): if self.multiline: errors = self.regex.finditer(output) if errors: for error in errors: yield self.split_match(error) else: yield self.split_match(None) else: for line in output.splitlines(): yield self.match_error(self.regex, line.strip()) def split_match(self, match): if match: items = {'row': None, 'col': None, 'error': '', 'near': None} items.update(match.groupdict()) error, row, col, near = [ items[k] for k in ('error', 'line', 'col', 'near') ] row = int(row) - 1 if col: col = int(col) - 1 return match, row, col, error, near return match, None, None, '', None def match_error(self, r, line): return self.split_match(r.match(line)) # popen wrappers def communicate(self, cmd, code): return util.communicate(cmd, code) def tmpfile(self, cmd, code, suffix=''): return util.tmpfile(cmd, code, suffix) def tmpdir(self, cmd, files, code): return util.tmpdir(cmd, files, self.filename, code) def popen(self, cmd, env=None): return util.popen(cmd, env)