# coding: utf-8 # # commands.py # Part of SublimeLinter3, a code checking framework for Sublime Text 3 # # Written by Ryan Hileman and Aparajita Fishman # # Project: https://github.com/SublimeLinter/SublimeLinter3 # License: MIT # """This module implements the Sublime Text commands provided by SublimeLinter.""" import datetime from fnmatch import fnmatch import json import os import re import shutil import subprocess import tempfile from textwrap import TextWrapper from threading import Thread import time import sublime import sublime_plugin from .lint import highlight, linter, persist, util def error_command(method): """ A decorator that executes method only if the current view has errors. This decorator is meant to be used only with the run method of sublime_plugin.TextCommand subclasses. A wrapped version of method is returned. """ def run(self, edit, **kwargs): vid = self.view.id() if vid in persist.errors and persist.errors[vid]: method(self, self.view, persist.errors[vid], **kwargs) else: sublime.message_dialog('No lint errors.') return run def select_line(view, line): """Change view's selection to be the given line.""" point = view.text_point(line, 0) sel = view.sel() sel.clear() sel.add(view.line(point)) class SublimelinterLintCommand(sublime_plugin.TextCommand): """A command that lints the current view if it has a linter.""" def is_enabled(self): """ Return True if the current view can be linted. If the view has *only* file-only linters, it can be linted only if the view is not dirty. Otherwise it can be linted. """ has_non_file_only_linter = False vid = self.view.id() linters = persist.view_linters.get(vid, []) for lint in linters: if lint.tempfile_suffix != '-': has_non_file_only_linter = True break if not has_non_file_only_linter: return not self.view.is_dirty() return True def run(self, edit): """Lint the current view.""" from .sublimelinter import SublimeLinter SublimeLinter.shared_plugin().lint(self.view.id()) class HasErrorsCommand: """ A mixin class for sublime_plugin.TextCommand subclasses. Inheriting from this class will enable the command only if the current view has errors. """ def is_enabled(self): """Return True if the current view has errors.""" vid = self.view.id() return vid in persist.errors and len(persist.errors[vid]) > 0 class GotoErrorCommand(sublime_plugin.TextCommand): """A superclass for commands that go to the next/previous error.""" def goto_error(self, view, errors, direction='next'): """Go to the next/previous error in view.""" sel = view.sel() if len(sel) == 0: sel.add(sublime.Region(0, 0)) saved_sel = tuple(sel) empty_selection = len(sel) == 1 and sel[0].empty() # sublime.Selection() changes the view's selection, get the point first point = sel[0].begin() if direction == 'next' else sel[-1].end() regions = sublime.Selection(view.id()) regions.clear() for error_type in (highlight.WARNING, highlight.ERROR): regions.add_all(view.get_regions(highlight.MARK_KEY_FORMAT.format(error_type))) region_to_select = None # If going forward, find the first region beginning after the point. # If going backward, find the first region ending before the point. # If nothing is found in the given direction, wrap to the first/last region. if direction == 'next': for region in regions: if ( (point == region.begin() and empty_selection and not region.empty()) or (point < region.begin()) ): region_to_select = region break else: for region in reversed(regions): if ( (point == region.end() and empty_selection and not region.empty()) or (point > region.end()) ): region_to_select = region break # If there is only one error line and the cursor is in that line, we cannot move. # Otherwise wrap to the first/last error line unless settings disallow that. if region_to_select is None and ((len(regions) > 1 or not regions[0].contains(point))): if persist.settings.get('wrap_find', True): region_to_select = regions[0] if direction == 'next' else regions[-1] if region_to_select is not None: self.select_lint_region(self.view, region_to_select) else: sel.clear() sel.add_all(saved_sel) sublime.message_dialog('No {0} lint error.'.format(direction)) @classmethod def select_lint_region(cls, view, region): """ Select and scroll to the first marked region that contains region. If none are found, the beginning of region is used. The view is centered on the calculated region and the region is selected. """ marked_region = cls.find_mark_within(view, region) if marked_region is None: marked_region = sublime.Region(region.begin(), region.begin()) sel = view.sel() sel.clear() sel.add(marked_region) # There is a bug in ST3 that prevents the selection from changing # when a quick panel is open and the viewport does not change position, # so we call our own custom method that works around that. util.center_region_in_view(marked_region, view) @classmethod def find_mark_within(cls, view, region): """Return the nearest marked region that contains region, or None if none found.""" marks = view.get_regions(highlight.MARK_KEY_FORMAT.format(highlight.WARNING)) marks.extend(view.get_regions(highlight.MARK_KEY_FORMAT.format(highlight.ERROR))) marks.sort(key=sublime.Region.begin) for mark in marks: if mark.contains(region): return mark return None class SublimelinterGotoErrorCommand(GotoErrorCommand): """A command that selects the next/previous error.""" @error_command def run(self, view, errors, **kwargs): """Run the command.""" self.goto_error(view, errors, **kwargs) class SublimelinterShowAllErrors(sublime_plugin.TextCommand): """A command that shows a quick panel with all of the errors in the current view.""" @error_command def run(self, view, errors): """Run the command.""" self.errors = errors self.points = [] options = [] for lineno, line_errors in sorted(errors.items()): line = view.substr(view.full_line(view.text_point(lineno, 0))).rstrip('\n\r') # Strip whitespace from the front of the line, but keep track of how much was # stripped so we can adjust the column. diff = len(line) line = line.lstrip() diff -= len(line) max_prefix_len = 40 for column, message in sorted(line_errors): # Keep track of the line and column point = view.text_point(lineno, column) self.points.append(point) # If there are more than max_prefix_len characters before the adjusted column, # lop off the excess and insert an ellipsis. column = max(column - diff, 0) if column > max_prefix_len: visible_line = '...' + line[column - max_prefix_len:] column = max_prefix_len + 3 # 3 for ... else: visible_line = line # Insert an arrow at the column in the stripped line code = visible_line[:column] + '➜' + visible_line[column:] options.append(['{} {}'.format(lineno + 1, message), code]) self.viewport_pos = view.viewport_position() self.selection = list(view.sel()) view.window().show_quick_panel( options, on_select=self.select_error, on_highlight=self.select_error ) def select_error(self, index): """Completion handler for the quick panel. Selects the indexed error.""" if index != -1: point = self.points[index] GotoErrorCommand.select_lint_region(self.view, sublime.Region(point, point)) else: self.view.set_viewport_position(self.viewport_pos) self.view.sel().clear() self.view.sel().add_all(self.selection) class SublimelinterToggleSettingCommand(sublime_plugin.WindowCommand): """Command that toggles a setting.""" def __init__(self, window): super().__init__(window) def is_visible(self, **args): """Return True if the opposite of the setting is True.""" if args.get('checked', False): return True if persist.settings.has_setting(args['setting']): setting = persist.settings.get(args['setting'], None) return setting is not None and setting is not args['value'] else: return args['value'] is not None def is_checked(self, **args): """Return True if the setting should be checked.""" if args.get('checked', False): setting = persist.settings.get(args['setting'], False) return setting is True else: return False def run(self, **args): """Toggle the setting if value is boolean, or remove it if None.""" if 'value' in args: if args['value'] is None: persist.settings.pop(args['setting']) else: persist.settings.set(args['setting'], args['value'], changed=True) else: setting = persist.settings.get(args['setting'], False) persist.settings.set(args['setting'], not setting, changed=True) persist.settings.save() class ChooseSettingCommand(sublime_plugin.WindowCommand): """An abstract base class for commands that choose a setting from a list.""" def __init__(self, window, setting=None, preview=False): super().__init__(window) self.setting = setting self._settings = None self.preview = preview def description(self, **args): """Return the visible description of the command, used in menus.""" return args.get('value', None) def is_checked(self, **args): """Return whether this command should be checked in a menu.""" if 'value' not in args: return False item = self.transform_setting(args['value'], matching=True) setting = self.setting_value(matching=True) return item == setting def _get_settings(self): """Return the list of settings.""" if self._settings is None: self._settings = self.get_settings() return self._settings settings = property(_get_settings) def get_settings(self): """Return the list of settings. Subclasses must override this.""" raise NotImplementedError def transform_setting(self, setting, matching=False): """ Transform the display text for setting to the form it is stored in. By default, returns a lowercased copy of setting. """ return setting.lower() def setting_value(self, matching=False): """Return the current value of the setting.""" return self.transform_setting(persist.settings.get(self.setting, ''), matching=matching) def on_highlight(self, index): """If preview is on, set the selected setting.""" if self.preview: self.set(index) def choose(self, **kwargs): """ Choose or set the setting. If 'value' is in kwargs, the setting is set to the corresponding value. Otherwise the list of available settings is built via get_settings and is displayed in a quick panel. The current value of the setting is initially selected in the quick panel. """ if 'value' in kwargs: setting = self.transform_setting(kwargs['value']) else: setting = self.setting_value(matching=True) index = 0 for i, s in enumerate(self.settings): if isinstance(s, (tuple, list)): s = self.transform_setting(s[0]) else: s = self.transform_setting(s) if s == setting: index = i break if 'value' in kwargs: self.set(index) else: self.previous_setting = self.setting_value() self.window.show_quick_panel( self.settings, on_select=self.set, selected_index=index, on_highlight=self.on_highlight) def set(self, index): """Set the value of the setting.""" if index == -1: if self.settings_differ(self.previous_setting, self.setting_value()): self.update_setting(self.previous_setting) return setting = self.selected_setting(index) if isinstance(setting, (tuple, list)): setting = setting[0] setting = self.transform_setting(setting) if not self.settings_differ(persist.settings.get(self.setting, ''), setting): return self.update_setting(setting) def update_setting(self, value): """Update the setting with the given value.""" persist.settings.set(self.setting, value, changed=True) self.setting_was_changed(value) persist.settings.save() def settings_differ(self, old_setting, new_setting): """Return whether two setting values differ.""" if isinstance(new_setting, (tuple, list)): new_setting = new_setting[0] new_setting = self.transform_setting(new_setting) return new_setting != old_setting def selected_setting(self, index): """ Return the selected setting by index. Subclasses may override this if they want to return something other than the indexed value from self.settings. """ return self.settings[index] def setting_was_changed(self, setting): """ Do something after the setting value is changed but before settings are saved. Subclasses may override this if further action is necessary after the setting's value is changed. """ pass def choose_setting_command(setting, preview): """Return a decorator that provides common methods for concrete subclasses of ChooseSettingCommand.""" def decorator(cls): def init(self, window): super(cls, self).__init__(window, setting, preview) def run(self, **kwargs): """Run the command.""" self.choose(**kwargs) cls.setting = setting cls.__init__ = init cls.run = run return cls return decorator @choose_setting_command('lint_mode', preview=False) class SublimelinterChooseLintModeCommand(ChooseSettingCommand): """A command that selects a lint mode from a list.""" def get_settings(self): """Return a list of the lint modes.""" return [[name.capitalize(), description] for name, description in persist.LINT_MODES] def setting_was_changed(self, setting): """Update all views when the lint mode changes.""" if setting == 'background': from .sublimelinter import SublimeLinter SublimeLinter.lint_all_views() else: linter.Linter.clear_all() @choose_setting_command('mark_style', preview=True) class SublimelinterChooseMarkStyleCommand(ChooseSettingCommand): """A command that selects a mark style from a list.""" def get_settings(self): """Return a list of the mark styles.""" return highlight.mark_style_names() @choose_setting_command('gutter_theme', preview=True) class SublimelinterChooseGutterThemeCommand(ChooseSettingCommand): """A command that selects a gutter theme from a list.""" def get_settings(self): """ Return a list of all available gutter themes, with 'None' at the end. Whether the theme is colorized and is a SublimeLinter or user theme is indicated below the theme name. """ settings = self.find_gutter_themes() settings.append(['None', 'Do not display gutter marks']) self.themes.append('none') return settings def find_gutter_themes(self): """ Find all SublimeLinter.gutter-theme resources. For each found resource, if it doesn't match one of the patterns from the "gutter_theme_excludes" setting, return the base name of resource and info on whether the theme is a standard theme or a user theme, as well as whether it is colorized. The list of paths to the resources is appended to self.themes. """ self.themes = [] settings = [] gutter_themes = sublime.find_resources('*.gutter-theme') excludes = persist.settings.get('gutter_theme_excludes', []) pngs = sublime.find_resources('*.png') for theme in gutter_themes: # Make sure the theme has error.png and warning.png exclude = False parent = os.path.dirname(theme) for name in ('error', 'warning'): if '{}/{}.png'.format(parent, name) not in pngs: exclude = True if exclude: continue # Now see if the theme name is in gutter_theme_excludes name = os.path.splitext(os.path.basename(theme))[0] for pattern in excludes: if fnmatch(name, pattern): exclude = True break if exclude: continue self.themes.append(theme) try: info = json.loads(sublime.load_resource(theme)) colorize = info.get('colorize', False) except ValueError: colorize = False std_theme = theme.startswith('Packages/SublimeLinter/gutter-themes/') settings.append([ name, '{}{}'.format( 'SublimeLinter theme' if std_theme else 'User theme', ' (colorized)' if colorize else '' ) ]) # Sort self.themes and settings in parallel using the zip trick settings, self.themes = zip(*sorted(zip(settings, self.themes))) # zip returns tuples, convert back to lists settings = list(settings) self.themes = list(self.themes) return settings def selected_setting(self, index): """Return the theme name with the given index.""" return self.themes[index] def transform_setting(self, setting, matching=False): """ Return a transformed version of setting. For gutter themes, setting is a Packages-relative path to a .gutter-theme file. If matching == False, return the original setting text, gutter theme settings are not lowercased. If matching == True, return the base name of the filename without the .gutter-theme extension. """ if matching: return os.path.splitext(os.path.basename(setting))[0] else: return setting class SublimelinterToggleLinterCommand(sublime_plugin.WindowCommand): """A command that toggles, enables, or disables linter plugins.""" def __init__(self, window): super().__init__(window) self.linters = {} def is_visible(self, **args): """Return True if the command would show any linters.""" which = args['which'] if self.linters.get(which) is None: linters = [] settings = persist.settings.get('linters', {}) for linter in persist.linter_classes: linter_settings = settings.get(linter, {}) disabled = linter_settings.get('@disable') if which == 'all': include = True linter = [linter, 'disabled' if disabled else 'enabled'] else: include = ( which == 'enabled' and not disabled or which == 'disabled' and disabled ) if include: linters.append(linter) linters.sort() self.linters[which] = linters return len(self.linters[which]) > 0 def run(self, **args): """Run the command.""" self.which = args['which'] if self.linters[self.which]: self.window.show_quick_panel(self.linters[self.which], self.on_done) def on_done(self, index): """Completion handler for quick panel, toggle the enabled state of the chosen linter.""" if index != -1: linter = self.linters[self.which][index] if isinstance(linter, list): linter = linter[0] settings = persist.settings.get('linters', {}) linter_settings = settings.get(linter, {}) linter_settings['@disable'] = not linter_settings.get('@disable', False) persist.settings.set('linters', settings, changed=True) persist.settings.save() self.linters = {} class SublimelinterCreateLinterPluginCommand(sublime_plugin.WindowCommand): """A command that creates a new linter plugin.""" def run(self): """Run the command.""" if not sublime.ok_cancel_dialog( 'You will be asked for the linter name. Please enter the name ' 'of the linter binary (including dashes), NOT the name of the language being linted. ' 'For example, to lint CSS with csslint, the linter name is ' '“csslint”, NOT “css”.', 'I understand' ): return self.window.show_input_panel( 'Linter name:', '', on_done=self.copy_linter, on_change=None, on_cancel=None) def copy_linter(self, name): """Copy the template linter to a new linter with the given name.""" self.name = name self.fullname = 'SublimeLinter-contrib-{}'.format(name) self.dest = os.path.join(sublime.packages_path(), self.fullname) if os.path.exists(self.dest): sublime.error_message('The plugin “{}” already exists.'.format(self.fullname)) return src = os.path.join(sublime.packages_path(), persist.PLUGIN_DIRECTORY, 'linter-plugin-template') self.temp_dir = None try: self.temp_dir = tempfile.mkdtemp() self.temp_dest = os.path.join(self.temp_dir, self.fullname) shutil.copytree(src, self.temp_dest) self.get_linter_language(name, self.configure_linter) except Exception as ex: if self.temp_dir and os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir) sublime.error_message('An error occurred while copying the template plugin: {}'.format(str(ex))) def configure_linter(self, language): """Fill out the template and move the linter into Packages.""" try: if language is None: return if not self.fill_template(self.temp_dir, self.name, self.fullname, language): return git = util.which('git') if git: subprocess.call((git, 'init', self.temp_dest)) shutil.move(self.temp_dest, self.dest) util.open_directory(self.dest) self.wait_for_open(self.dest) except Exception as ex: sublime.error_message('An error occurred while configuring the plugin: {}'.format(str(ex))) finally: if self.temp_dir and os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir) def get_linter_language(self, name, callback): """Get the language (python, node, etc.) on which the linter is based.""" languages = ['javascript', 'python', 'ruby', 'other'] items = ['Select the language on which the linter is based:'] for language in languages: items.append(' ' + language.capitalize()) def on_done(index): language = languages[index - 1] if index > 0 else None callback(language) self.window.show_quick_panel(items, on_done) def fill_template(self, template_dir, name, fullname, language): """Replace placeholders and fill template files in template_dir, return success.""" # Read per-language info path = os.path.join(os.path.dirname(__file__), 'create_linter_info.json') with open(path, mode='r', encoding='utf-8') as f: try: info = json.load(f) except Exception as err: persist.printf(err) sublime.error_message('A configuration file could not be opened, the linter cannot be created.') return False info = info.get(language, {}) extra_attributes = [] comment_re = info.get('comment_re', 'None') extra_attributes.append('comment_re = ' + comment_re) attributes = info.get('attributes', []) for attr in attributes: extra_attributes.append(attr.format(name)) extra_attributes = '\n '.join(extra_attributes) if extra_attributes: extra_attributes += '\n' extra_steps = info.get('extra_steps', '') if isinstance(extra_steps, list): extra_steps = '\n\n'.join(extra_steps) if extra_steps: extra_steps = '\n' + extra_steps + '\n' platform = info.get('platform', language.capitalize()) # Replace placeholders placeholders = { '__linter__': name, '__user__': util.get_user_fullname(), '__year__': str(datetime.date.today().year), '__class__': self.camel_case(name), '__superclass__': info.get('superclass', 'Linter'), '__cmd__': '{}@python'.format(name) if language == 'python' else name, '__extra_attributes__': extra_attributes, '__platform__': platform, '__install__': info['installer'].format(name), '__extra_install_steps__': extra_steps } for root, dirs, files in os.walk(template_dir): for filename in files: extension = os.path.splitext(filename)[1] if extension in ('.py', '.md', '.txt'): path = os.path.join(root, filename) with open(path, encoding='utf-8') as f: text = f.read() for placeholder, value in placeholders.items(): text = text.replace(placeholder, value) with open(path, mode='w', encoding='utf-8') as f: f.write(text) return True def camel_case(self, name): """Convert and return a name in the form foo-bar to FooBar.""" camel_name = name[0].capitalize() i = 1 while i < len(name): if name[i] == '-' and i < len(name) - 1: camel_name += name[i + 1].capitalize() i += 1 else: camel_name += name[i] i += 1 return camel_name def wait_for_open(self, dest): """Wait for new linter window to open in another thread.""" def open_linter_py(): """Wait until the new linter window has opened and open linter.py.""" start = datetime.datetime.now() while True: time.sleep(0.25) delta = datetime.datetime.now() - start # Wait a maximum of 5 seconds if delta.seconds > 5: break window = sublime.active_window() folders = window.folders() if folders and folders[0] == dest: window.open_file(os.path.join(dest, 'linter.py')) break sublime.set_timeout_async(open_linter_py, 0) class SublimelinterPackageControlCommand(sublime_plugin.WindowCommand): """ Abstract superclass for Package Control utility commands. Only works if git is installed. """ TAG_RE = re.compile(r'(?P\d+)\.(?P\d+)\.(?P\d+)(?:\+\d+)?') def __init__(self, window): super().__init__(window) self.git = '' def is_visible(self, paths=[]): """Return True if any eligible plugin directories are selected.""" if self.git == '': self.git = util.which('git') if self.git: for path in paths: if self.is_eligible_path(path): return True return False def is_eligible_path(self, path): """ Return True if path is an eligible directory. A directory is eligible if it is a direct child of Packages, has a messages subdirectory, and has messages.json. """ packages_path = sublime.packages_path() return ( os.path.isdir(path) and os.path.dirname(path) == packages_path and os.path.isdir(os.path.join(path, 'messages')) and os.path.isfile(os.path.join(path, 'messages.json')) ) def get_current_tag(self): """ Return the most recent tag components. A tuple of (major, minor, release) is returned, or (1, 0, 0) if there are no tags. If the most recent tag does not conform to semver, return (None, None, None). """ tag = util.communicate(['git', 'describe', '--tags', '--abbrev=0']).strip() if not tag: return (1, 0, 0) match = self.TAG_RE.match(tag) if match: return (int(match.group('major')), int(match.group('minor')), int(match.group('release'))) else: return None class SublimelinterNewPackageControlMessageCommand(SublimelinterPackageControlCommand): """ This command automates the process of creating new Package Control release messages. It creates a new entry in messages.json for the next version and creates a new file named messages/.txt. """ COMMIT_MSG_RE = re.compile(r'{{{{(.+?)}}}}') def __init__(self, window): super().__init__(window) def run(self, paths=[]): """Run the command.""" for path in paths: if self.is_eligible_path(path): self.make_new_version_message(path) def make_new_version_message(self, path): """Make a new version message for the repo at the given path.""" try: cwd = os.getcwd() os.chdir(path) version = self.get_current_tag() if version[0] is None: return messages_path = os.path.join(path, 'messages.json') message_path = self.rewrite_messages_json(messages_path, version) if os.path.exists(message_path): os.remove(message_path) with open(message_path, mode='w', encoding='utf-8') as f: header = '{} {}'.format( os.path.basename(path), os.path.splitext(os.path.basename(message_path))[0]) f.write('{}\n{}\n'.format(header, '-' * (len(header) + 1))) f.write(self.get_commit_messages_since(version)) self.window.run_command('open_file', args={'file': message_path}) except Exception: import traceback traceback.print_exc() finally: os.chdir(cwd) def rewrite_messages_json(self, messages_path, tag): """Add an entry in messages.json for tag, return relative path to the file.""" with open(messages_path, encoding='utf-8') as f: messages = json.load(f) major, minor, release = tag release += 1 tag = '{}.{}.{}'.format(major, minor, release) message_path = os.path.join('messages', '{}.txt'.format(tag)) messages[tag] = message_path message_path = os.path.join(os.path.dirname(messages_path), message_path) with open(messages_path, mode='w', encoding='utf-8') as f: messages_json = '{\n' sorted_messages = [] if 'install' in messages: install_message = messages.pop('install') sorted_messages.append(' "install": "{}"'.format(install_message)) keys = sorted(map(self.sortable_tag, messages.keys())) for _, key in keys: sorted_messages.append(' "{}": "{}"'.format(key, messages[key])) messages_json += ',\n'.join(sorted_messages) messages_json += '\n}\n' f.write(messages_json) return message_path def sortable_tag(self, tag): """Return a version tag in a sortable form.""" if tag == 'install': return (tag, tag) major, minor, release = tag.split('.') if '+' in release: release, update = release.split('+') update = '+{:04}'.format(int(update)) else: update = '' return ('{:04}.{:04}.{:04}{}'.format(int(major), int(minor), int(release), update), tag) def get_commit_messages_since(self, version): """Return a formatted list of commit messages since the given tagged version.""" tag = '{}.{}.{}'.format(*version) output = util.communicate([ 'git', 'log', '--pretty=format:{{{{%w(0,0,0)%s %b}}}}', '--reverse', tag + '..' ]) # Split the messages, they are bounded by {{{{ }}}} messages = [] for match in self.COMMIT_MSG_RE.finditer(output): messages.append(match.group(1).strip()) # Wrap the messages wrapper = TextWrapper(initial_indent='- ', subsequent_indent=' ') messages = list(map(lambda msg: '\n'.join(wrapper.wrap(msg)), messages)) return '\n\n'.join(messages) + '\n' class SublimelinterReportCommand(sublime_plugin.WindowCommand): """ A command that displays a report of all errors. The scope of the report is all open files in the current window, all files in all folders in the current window, or both. """ def run(self, on='files'): """Run the command. on determines the scope of the report.""" output = self.window.new_file() output.set_name('{} Error Report'.format(persist.PLUGIN_NAME)) output.set_scratch(True) from .sublimelinter import SublimeLinter self.plugin = SublimeLinter.shared_plugin() if on == 'files' or on == 'both': for view in self.window.views(): self.report(output, view) if on == 'folders' or on == 'both': for folder in self.window.folders(): self.folder(output, folder) def folder(self, output, folder): """Report on all files in a folder.""" for root, dirs, files in os.walk(folder): for name in files: path = os.path.join(root, name) # Ignore files over 256K to speed things up a bit if os.stat(path).st_size < 256 * 1024: # TODO: not implemented pass def report(self, output, view): """Write a report on the given view to output.""" def finish_lint(view, linters, hit_time): if not linters: return def insert(edit): if not any(l.errors for l in linters): return filename = os.path.basename(linters[0].filename or 'untitled') out = '\n{}:\n'.format(filename) for linter in sorted(linters, key=lambda linter: linter.name): if linter.errors: out += '\n {}:\n'.format(linter.name) items = sorted(linter.errors.items()) # Get the highest line number so we know how much padding numbers need highest_line = items[-1][0] width = 1 while highest_line >= 10: highest_line /= 10 width += 1 for line, messages in items: for col, message in messages: out += ' {:>{width}}: {}\n'.format(line, message, width=width) output.insert(edit, output.size(), out) persist.edits[output.id()].append(insert) output.run_command('sublimelinter_edit') kwargs = {'self': self.plugin, 'view_id': view.id(), 'callback': finish_lint} from .sublimelinter import SublimeLinter Thread(target=SublimeLinter.lint, kwargs=kwargs).start()