backing up sublime settings

This commit is contained in:
2014-04-04 11:21:58 -04:00
commit 2cbece8593
274 changed files with 23793 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
#
# lint.__init__
# 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 exports the linter classes and the highlight, linter, persist and util submodules."""
from .linter import Linter
from .python_linter import PythonLinter
from .ruby_linter import RubyLinter
from . import (
highlight,
linter,
persist,
util,
)
__all__ = [
'highlight',
'Linter',
'PythonLinter',
'RubyLinter',
'linter',
'persist',
'util',
]

View File

@@ -0,0 +1,449 @@
#
# highlight.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 highlighting code with marks.
The following classes are exported:
HighlightSet
Highlight
The following constants are exported:
WARNING - name of warning type
ERROR - name of error type
MARK_KEY_FORMAT - format string for key used to mark code regions
GUTTER_MARK_KEY_FORMAT - format string for key used to mark gutter mark regions
MARK_SCOPE_FORMAT - format string used for color scheme scope names
"""
import re
import sublime
from . import persist
#
# Error types
#
WARNING = 'warning'
ERROR = 'error'
MARK_KEY_FORMAT = 'sublimelinter-{}-marks'
GUTTER_MARK_KEY_FORMAT = 'sublimelinter-{}-gutter-marks'
MARK_SCOPE_FORMAT = 'sublimelinter.mark.{}'
UNDERLINE_FLAGS = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_EMPTY_AS_OVERWRITE
MARK_STYLES = {
'outline': sublime.DRAW_NO_FILL,
'fill': sublime.DRAW_NO_OUTLINE,
'solid underline': sublime.DRAW_SOLID_UNDERLINE | UNDERLINE_FLAGS,
'squiggly underline': sublime.DRAW_SQUIGGLY_UNDERLINE | UNDERLINE_FLAGS,
'stippled underline': sublime.DRAW_STIPPLED_UNDERLINE | UNDERLINE_FLAGS,
'none': sublime.HIDDEN
}
WORD_RE = re.compile(r'^([-\w]+)')
NEAR_RE_TEMPLATE = r'(?<!"){}({}){}(?!")'
def mark_style_names():
"""Return the keys from MARK_STYLES, sorted and capitalized, with None at the end."""
names = list(MARK_STYLES)
names.remove('none')
names.sort()
names.append('none')
return [name.capitalize() for name in names]
class HighlightSet:
"""This class maintains a set of Highlight objects and performs bulk operations on them."""
def __init__(self):
self.all = set()
def add(self, highlight):
"""Add a Highlight to the set."""
self.all.add(highlight)
def draw(self, view):
"""
Draw all of the Highlight objects in our set.
Rather than draw each Highlight object individually, the marks in each
object are aggregated into a new Highlight object, and that object
is then drawn for the given view.
"""
if not self.all:
return
all = Highlight()
for highlight in self.all:
all.update(highlight)
all.draw(view)
@staticmethod
def clear(view):
"""Clear all marks in the given view."""
for error_type in (WARNING, ERROR):
view.erase_regions(MARK_KEY_FORMAT.format(error_type))
view.erase_regions(GUTTER_MARK_KEY_FORMAT.format(error_type))
def redraw(self, view):
"""Redraw all marks in the given view."""
self.clear(view)
self.draw(view)
def reset(self, view):
"""Clear all marks in the given view and reset the list of marks in our Highlights."""
self.clear(view)
for highlight in self.all:
highlight.reset()
class Highlight:
"""This class maintains error marks and knows how to draw them."""
def __init__(self, code=''):
self.code = code
self.marks = {WARNING: [], ERROR: []}
self.mark_style = 'outline'
self.mark_flags = MARK_STYLES[self.mark_style]
# Every line that has a mark is kept in this dict, so we know which
# lines to mark in the gutter.
self.lines = {}
# These are used when highlighting embedded code, for example JavaScript
# or CSS within an HTML file. The embedded code is linted as if it begins
# at (0, 0), but we need to keep track of where the actual start is within the source.
self.line_offset = 0
self.char_offset = 0
# Linting runs asynchronously on a snapshot of the code. Marks are added to the code
# during that asynchronous linting, and the markup code needs to calculate character
# positions given a line + column. By the time marks are added, the actual buffer
# may have changed, so we can't reliably use the plugin API to calculate character
# positions. The solution is to calculate and store the character positions for
# every line when this object is created, then reference that when needed.
self.newlines = newlines = [0]
last = -1
while True:
last = code.find('\n', last + 1)
if last == -1:
break
newlines.append(last + 1)
newlines.append(len(code))
@staticmethod
def strip_quotes(text):
"""Return text stripped of enclosing single/double quotes."""
first = text[0]
if first in ('\'', '"') and text[-1] == first:
text = text[1:-1]
return text
def full_line(self, line):
"""
Return the start/end character positions for the given line.
This returns *real* character positions (relative to the beginning of self.code)
base on the *virtual* line number (adjusted by the self.line_offset).
"""
# The first line of the code needs the character offset
if line == 0:
char_offset = self.char_offset
else:
char_offset = 0
line += self.line_offset
start = self.newlines[line] + char_offset
end = self.newlines[min(line + 1, len(self.newlines) - 1)]
return start, end
def range(self, line, pos, length=-1, near=None, error_type=ERROR, word_re=None):
"""
Mark a range of text.
line and pos should be zero-based. The pos and length argument can be used to control marking:
- If pos < 0, the entire line is marked and length is ignored.
- If near is not None, it is stripped of quotes and length = len(near)
- If length < 0, the nearest word starting at pos is marked, and if
no word is matched, the character at pos is marked.
- If length == 0, no text is marked, but a gutter mark will appear on that line.
error_type determines what type of error mark will be drawn (ERROR or WARNING).
When length < 0, this method attempts to mark the closest word at pos on the given line.
If you want to customize the word matching regex, pass it in word_re.
If the error_type is WARNING and an identical ERROR region exists, it is not added.
If the error_type is ERROR and an identical WARNING region exists, the warning region
is removed and the error region is added.
"""
start, end = self.full_line(line)
if pos < 0:
pos = 0
length = (end - start) - 1
elif near is not None:
near = self.strip_quotes(near)
length = len(near)
elif length < 0:
code = self.code[start:end][pos:]
match = (word_re or WORD_RE).search(code)
if match:
length = len(match.group())
else:
length = 1
pos += start
region = sublime.Region(pos, pos + length)
other_type = ERROR if error_type == WARNING else WARNING
i_offset = 0
for i, mark in enumerate(self.marks[other_type].copy()):
if mark.a == region.a and mark.b == region.b:
if error_type == WARNING:
return
else:
self.marks[other_type].pop(i - i_offset)
i_offset += 1
self.marks[error_type].append(region)
def regex(self, line, regex, error_type=ERROR,
line_match=None, word_match=None, word_re=None):
"""
Mark a range of text that matches a regex.
line, error_type and word_re are the same as in range().
line_match may be a string pattern or a compiled regex.
If provided, it must have a named group called 'match' that
determines which part of the source line will be considered
for marking.
word_match may be a string pattern or a compiled regex.
If provided, it must have a named group called 'mark' that
determines which part of the source line will actually be marked.
Multiple portions of the source line may match.
"""
offset = 0
start, end = self.full_line(line)
line_text = self.code[start:end]
if line_match:
match = re.match(line_match, line_text)
if match:
line_text = match.group('match')
offset = match.start('match')
else:
return
it = re.finditer(regex, line_text)
results = [
result.span('mark')
for result in it
if word_match is None or result.group('mark') == word_match
]
for start, end in results:
self.range(line, start + offset, end - start, error_type=error_type)
def near(self, line, near, error_type=ERROR, word_re=None):
"""
Mark a range of text near a given word.
line, error_type and word_re are the same as in range().
If near is enclosed by quotes, they are stripped. The first occurrence
of near in the given line of code is matched. If the first and last
characters of near are word characters, a match occurs only if near
is a complete word.
The position at which near is found is returned, or zero if there
is no match.
"""
if not near:
return
start, end = self.full_line(line)
text = self.code[start:end]
near = self.strip_quotes(near)
# Add \b fences around the text if it begins/ends with a word character
fence = ['', '']
for i, pos in enumerate((0, -1)):
if near[pos].isalnum() or near[pos] == '_':
fence[i] = r'\b'
pattern = NEAR_RE_TEMPLATE.format(fence[0], re.escape(near), fence[1])
match = re.search(pattern, text)
if match:
start = match.start(1)
else:
start = -1
if start != -1:
self.range(line, start, len(near), error_type=error_type, word_re=word_re)
return start
else:
return 0
def update(self, other):
"""
Update this object with another Highlight.
It is assumed that other.code == self.code.
other's marks and error positions are merged, and this
object takes the newlines array from other.
"""
for error_type in (WARNING, ERROR):
self.marks[error_type].extend(other.marks[error_type])
# Errors override warnings on the same line
for line, error_type in other.lines.items():
current_type = self.lines.get(line)
if current_type is None or current_type == WARNING:
self.lines[line] = error_type
self.newlines = other.newlines
def set_mark_style(self):
"""Setup the mark style and flags based on settings."""
self.mark_style = persist.settings.get('mark_style', 'outline')
self.mark_flags = MARK_STYLES[self.mark_style]
if not persist.settings.get('show_marks_in_minimap'):
self.mark_flags |= sublime.HIDE_ON_MINIMAP
def draw(self, view):
"""
Draw code and gutter marks in the given view.
Error, warning and gutter marks are drawn with separate regions,
since each one potentially needs a different color.
"""
self.set_mark_style()
gutter_regions = {WARNING: [], ERROR: []}
draw_gutter_marks = persist.settings.get('gutter_theme') != 'None'
if draw_gutter_marks:
# We use separate regions for the gutter marks so we can use
# a scope that will not colorize the gutter icon, and to ensure
# that errors will override warnings.
for line, error_type in self.lines.items():
region = sublime.Region(self.newlines[line], self.newlines[line])
gutter_regions[error_type].append(region)
for error_type in (WARNING, ERROR):
if self.marks[error_type]:
view.add_regions(
MARK_KEY_FORMAT.format(error_type),
self.marks[error_type],
MARK_SCOPE_FORMAT.format(error_type),
flags=self.mark_flags
)
if draw_gutter_marks and gutter_regions[error_type]:
if persist.gutter_marks['colorize']:
scope = MARK_SCOPE_FORMAT.format(error_type)
else:
scope = 'sublimelinter.gutter-mark'
view.add_regions(
GUTTER_MARK_KEY_FORMAT.format(error_type),
gutter_regions[error_type],
scope,
icon=persist.gutter_marks[error_type]
)
@staticmethod
def clear(view):
"""Clear all marks in the given view."""
for error_type in (WARNING, ERROR):
view.erase_regions(MARK_KEY_FORMAT.format(error_type))
view.erase_regions(GUTTER_MARK_KEY_FORMAT.format(error_type))
def reset(self):
"""
Clear the list of marks maintained by this object.
This method does not clear the marks, only the list.
The next time this object is used to draw, the marks will be cleared.
"""
for error_type in (WARNING, ERROR):
del self.marks[error_type][:]
self.lines.clear()
def line(self, line, error_type):
"""Record the given line as having the given error type."""
line += self.line_offset
# Errors override warnings, if it's already an error leave it
if self.lines.get(line) == ERROR:
return
self.lines[line] = error_type
def move_to(self, line, char_offset):
"""
Move the highlight to the given line and character offset.
The character offset is relative to the start of the line.
This method is used to create virtual line numbers
and character positions when linting embedded code.
"""
self.line_offset = line
self.char_offset = char_offset

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
#
# persist.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 provides persistent global storage for other modules."""
from collections import defaultdict
from copy import deepcopy
import json
import os
import re
import sublime
import sys
from . import util
PLUGIN_NAME = 'SublimeLinter'
# Get the name of the plugin directory, which is the parent of this file's directory
PLUGIN_DIRECTORY = os.path.basename(os.path.dirname(os.path.dirname(__file__)))
LINT_MODES = (
('background', 'Lint whenever the text is modified'),
('load/save', 'Lint only when a file is loaded or saved'),
('save only', 'Lint only when a file is saved'),
('manual', 'Lint only when requested')
)
SYNTAX_RE = re.compile(r'(?i)/([^/]+)\.tmLanguage$')
DEFAULT_GUTTER_THEME_PATH = 'Packages/SublimeLinter/gutter-themes/Default/Default.gutter-theme'
class Settings:
"""This class provides global access to and management of plugin settings."""
def __init__(self):
self.settings = {}
self.previous_settings = {}
self.changeset = set()
self.plugin_settings = None
self.on_update_callback = None
def load(self, force=False):
"""Load the plugin settings."""
if force or not self.settings:
self.observe()
self.on_update()
self.observe_prefs()
def has_setting(self, setting):
"""Return whether the given setting exists."""
return setting in self.settings
def get(self, setting, default=None):
"""Return a plugin setting, defaulting to default if not found."""
return self.settings.get(setting, default)
def set(self, setting, value, changed=False):
"""
Set a plugin setting to the given value.
Clients of this module should always call this method to set a value
instead of doing settings['foo'] = 'bar'.
If the caller knows for certain that the value has changed,
they should pass changed=True.
"""
self.copy()
self.settings[setting] = value
if changed:
self.changeset.add(setting)
def pop(self, setting, default=None):
"""
Remove a given setting and return default if it is not in self.settings.
Clients of this module should always call this method to pop a value
instead of doing settings.pop('foo').
"""
self.copy()
return self.settings.pop(setting, default)
def copy(self):
"""Save a copy of the plugin settings."""
self.previous_settings = deepcopy(self.settings)
def observe_prefs(self, observer=None):
"""Observe changes to the ST prefs."""
prefs = sublime.load_settings('Preferences.sublime-settings')
prefs.clear_on_change('sublimelinter-pref-settings')
prefs.add_on_change('sublimelinter-pref-settings', observer or self.on_prefs_update)
def observe(self, observer=None):
"""Observer changes to the plugin settings."""
self.plugin_settings = sublime.load_settings('SublimeLinter.sublime-settings')
self.plugin_settings.clear_on_change('sublimelinter-persist-settings')
self.plugin_settings.add_on_change('sublimelinter-persist-settings',
observer or self.on_update)
def on_update_call(self, callback):
"""Set a callback to call when user settings are updated."""
self.on_update_callback = callback
def on_update(self):
"""
Update state when the user settings change.
The settings before the change are compared with the new settings.
Depending on what changes, views will either be redrawn or relinted.
"""
settings = util.merge_user_settings(self.plugin_settings)
self.settings.clear()
self.settings.update(settings)
if (
'@disable' in self.changeset or
self.previous_settings.get('@disable', False) != self.settings.get('@disable', False)
):
need_relint = True
self.changeset.discard('@disable')
else:
need_relint = False
# Clear the path-related caches if the paths list has changed
if (
'paths' in self.changeset or
(self.previous_settings and
self.previous_settings.get('paths') != self.settings.get('paths'))
):
need_relint = True
util.clear_caches()
self.changeset.discard('paths')
# Add python paths if they changed
if (
'python_paths' in self.changeset or
(self.previous_settings and
self.previous_settings.get('python_paths') != self.settings.get('python_paths'))
):
need_relint = True
self.changeset.discard('python_paths')
python_paths = self.settings.get('python_paths', {}).get(sublime.platform(), [])
for path in python_paths:
if path not in sys.path:
sys.path.append(path)
# If the syntax map changed, reassign linters to all views
from .linter import Linter
if (
'syntax_map' in self.changeset or
(self.previous_settings and
self.previous_settings.get('syntax_map') != self.settings.get('syntax_map'))
):
need_relint = True
self.changeset.discard('syntax_map')
Linter.clear_all()
util.apply_to_all_views(lambda view: Linter.assign(view, reset=True))
if (
'no_column_highlights_line' in self.changeset or
self.previous_settings.get('no_column_highlights_line') != self.settings.get('no_column_highlights_line')
):
need_relint = True
self.changeset.discard('no_column_highlights_line')
if (
'gutter_theme' in self.changeset or
self.previous_settings.get('gutter_theme') != self.settings.get('gutter_theme')
):
self.changeset.discard('gutter_theme')
self.update_gutter_marks()
error_color = self.settings.get('error_color', '')
warning_color = self.settings.get('warning_color', '')
if (
('error_color' in self.changeset or 'warning_color' in self.changeset) or
(self.previous_settings and error_color and warning_color and
(self.previous_settings.get('error_color') != error_color or
self.previous_settings.get('warning_color') != warning_color))
):
self.changeset.discard('error_color')
self.changeset.discard('warning_color')
if (
sublime.ok_cancel_dialog(
'You changed the error and/or warning color. '
'Would you like to update the user color schemes '
'with the new colors?')
):
util.change_mark_colors(error_color, warning_color)
# If any other settings changed, relint
if (self.previous_settings or len(self.changeset) > 0):
need_relint = True
self.changeset.clear()
if need_relint:
Linter.reload()
if self.previous_settings and self.on_update_callback:
self.on_update_callback(need_relint)
def save(self, view=None):
"""
Regenerate and save the user settings.
User settings are updated with the default settings and the defaults
from every linter, and if the user settings are currently being edited,
the view is updated.
"""
self.load()
# Fill in default linter settings
settings = self.settings
linters = settings.pop('linters', {})
for name, linter in linter_classes.items():
default = linter.settings().copy()
default.update(linters.pop(name, {}))
for key, value in (('@disable', False), ('args', []), ('excludes', [])):
if key not in default:
default[key] = value
linters[name] = default
settings['linters'] = linters
filename = '{}.sublime-settings'.format(PLUGIN_NAME)
user_prefs_path = os.path.join(sublime.packages_path(), 'User', filename)
settings_views = []
if view is None:
# See if any open views are the user prefs
for window in sublime.windows():
for view in window.views():
if view.file_name() == user_prefs_path:
settings_views.append(view)
else:
settings_views = [view]
if settings_views:
def replace(edit):
if not view.is_dirty():
j = json.dumps({'user': settings}, indent=4, sort_keys=True)
j = j.replace(' \n', '\n')
view.replace(edit, sublime.Region(0, view.size()), j)
for view in settings_views:
edits[view.id()].append(replace)
view.run_command('sublimelinter_edit')
view.run_command('save')
else:
user_settings = sublime.load_settings('SublimeLinter.sublime-settings')
user_settings.set('user', settings)
sublime.save_settings('SublimeLinter.sublime-settings')
def on_prefs_update(self):
"""Perform maintenance when the ST prefs are updated."""
util.generate_color_scheme()
def update_gutter_marks(self):
"""Update the gutter mark info based on the the current "gutter_theme" setting."""
theme_path = self.settings.get('gutter_theme', DEFAULT_GUTTER_THEME_PATH)
theme = os.path.splitext(os.path.basename(theme_path))[0]
if theme_path.lower() == 'none':
gutter_marks['warning'] = gutter_marks['error'] = ''
return
info = None
for path in (theme_path, DEFAULT_GUTTER_THEME_PATH):
try:
info = sublime.load_resource(path)
break
except IOError:
pass
if info is not None:
if theme != 'Default' and os.path.basename(path) == 'Default.gutter-theme':
printf('cannot find the gutter theme \'{}\', using the default'.format(theme))
path = os.path.dirname(path)
for error_type in ('warning', 'error'):
icon_path = '{}/{}.png'.format(path, error_type)
gutter_marks[error_type] = icon_path
try:
info = json.loads(info)
colorize = info.get('colorize', False)
except ValueError:
colorize = False
gutter_marks['colorize'] = colorize
else:
sublime.error_message(
'SublimeLinter: cannot find the gutter theme "{}",'
' and the default is also not available. '
'No gutter marks will display.'.format(theme)
)
gutter_marks['warning'] = gutter_marks['error'] = ''
if not 'queue' in globals():
settings = Settings()
# A mapping between view ids and errors, which are line:(col, message) dicts
errors = {}
# A mapping between view ids and HighlightSets
highlights = {}
# A mapping between linter class names and linter classes
linter_classes = {}
# A mapping between view ids and a set of linter instances
view_linters = {}
# A mapping between view ids and views
views = {}
# Every time a view is modified, this is updated with a mapping between a view id
# and the time of the modification. This is checked at various stages of the linting
# process. If a view has been modified since the original modification, the
# linting process stops.
last_hit_times = {}
edits = defaultdict(list)
# Info about the gutter mark icons
gutter_marks = {'warning': 'Default', 'error': 'Default', 'colorize': True}
# Whether sys.path has been imported from the system.
sys_path_imported = False
# Set to true when the plugin is loaded at startup
plugin_is_loaded = False
def get_syntax(view):
"""Return the view's syntax or the syntax it is mapped to in the "syntax_map" setting."""
view_syntax = view.settings().get('syntax', '')
mapped_syntax = ''
if view_syntax:
match = SYNTAX_RE.search(view_syntax)
if match:
view_syntax = match.group(1).lower()
mapped_syntax = settings.get('syntax_map', {}).get(view_syntax, '').lower()
else:
view_syntax = ''
return mapped_syntax or view_syntax
def edit(vid, edit):
"""Perform an operation on a view with the given edit object."""
callbacks = edits.pop(vid, [])
for c in callbacks:
c(edit)
def view_did_close(vid):
"""Remove all references to the given view id in persistent storage."""
if vid in errors:
del errors[vid]
if vid in highlights:
del highlights[vid]
if vid in view_linters:
del view_linters[vid]
if vid in views:
del views[vid]
if vid in last_hit_times:
del last_hit_times[vid]
def debug_mode():
"""Return whether the "debug" setting is True."""
return settings.get('debug')
def debug(*args):
"""Print args to the console if the "debug" setting is True."""
if settings.get('debug'):
printf(*args)
def printf(*args):
"""Print args to the console, prefixed by the plugin name."""
print(PLUGIN_NAME + ': ', end='')
for arg in args:
print(arg, end=' ')
print()
def import_sys_path():
"""Import system python 3 sys.path into our sys.path."""
global sys_path_imported
if plugin_is_loaded and not sys_path_imported:
# Make sure the system python 3 paths are available to plugins.
# We do this here to ensure it is only done once.
sys.path.extend(util.get_python_paths())
sys_path_imported = True
def register_linter(linter_class, name, attrs):
"""Add a linter class to our mapping of class names <--> linter classes."""
if name:
name = name.lower()
linter_classes[name] = linter_class
# By setting the lint_settings to None, they will be set the next
# time linter_class.settings() is called.
linter_class.lint_settings = None
# The sublime plugin API is not available until plugin_loaded is executed
if plugin_is_loaded:
settings.load(force=True)
# If a linter is reloaded, we have to reassign that linter to all views
from . import linter
# If the linter had previously been loaded, just reassign that linter
if name in linter_classes:
linter_name = name
else:
linter_name = None
for view in views.values():
linter.Linter.assign(view, linter_name=linter_name)
printf('{} linter reloaded'.format(name))
else:
printf('{} linter loaded'.format(name))

View File

@@ -0,0 +1,325 @@
#
# python_linter.py
# Part of SublimeLinter3, a code checking framework for Sublime Text 3
#
# Written by Aparajita Fishman
#
# Project: https://github.com/SublimeLinter/SublimeLinter3
# License: MIT
#
"""This module exports the PythonLinter subclass of Linter."""
import importlib
import os
import re
from . import linter, persist, util
class PythonLinter(linter.Linter):
"""
This Linter subclass provides python-specific functionality.
Linters that check python should inherit from this class.
By doing so, they automatically get the following features:
- comment_re is defined correctly for python.
- A python shebang is returned as the @python:<version> meta setting.
- Execution directly via a module method or via an executable.
If the module attribute is defined and is successfully imported,
whether it is used depends on the following algorithm:
- If the cmd attribute specifies @python and ST's python
satisfies that version, the module will be used. Note that this
check is done during class construction.
- If the check_version attribute is False, the module will be used
because the module is not version-sensitive.
- If the "@python" setting is set and ST's python satisfies
that version, the module will be used.
- Otherwise the executable will be used with the python specified
in the "@python" setting, the cmd attribute, or the default system
python.
"""
SHEBANG_RE = re.compile(r'\s*#!(?:(?:/[^/]+)*[/ ])?python(?P<version>\d(?:\.\d)?)')
comment_re = r'\s*#'
# If the linter wants to import a module and run a method directly,
# it should set this attribute to the module name, suitable for passing
# to importlib.import_module. During class construction, the named module
# will be imported, and if successful, the attribute will be replaced
# with the imported module.
module = None
# Some python-based linters are version-sensitive, i.e. the python version
# they are run with has to match the version of the code they lint.
# If a linter is version-sensitive, this attribute should be set to True.
check_version = False
@staticmethod
def match_shebang(code):
"""Convert and return a python shebang as a @python:<version> setting."""
match = PythonLinter.SHEBANG_RE.match(code)
if match:
return '@python', match.group('version')
else:
return None
shebang_match = match_shebang
@classmethod
def initialize(cls):
"""Perform class-level initialization."""
super().initialize()
persist.import_sys_path()
cls.import_module()
@classmethod
def reinitialize(cls):
"""Perform class-level initialization after plugins have been loaded at startup."""
# Be sure to clear _cmd so that import_module will re-import.
if hasattr(cls, '_cmd'):
delattr(cls, '_cmd')
cls.initialize()
@classmethod
def import_module(cls):
"""
Attempt to import the configured module.
If it could not be imported, use the executable.
"""
if hasattr(cls, '_cmd'):
return
module = getattr(cls, 'module', None)
cls._cmd = None
cmd = cls.cmd
script = None
if isinstance(cls.cmd, (list, tuple)):
cmd = cls.cmd[0]
if module is not None:
try:
module = importlib.import_module(module)
persist.debug('{} imported {}'.format(cls.name, module))
# If the linter specifies a python version, check to see
# if ST's python satisfies that version.
if cmd and not callable(cmd):
match = util.PYTHON_CMD_RE.match(cmd)
if match and match.group('version'):
version, script = match.group('version', 'script')
version = util.find_python(version=version, script=script, module=module)
# If we cannot find a python or script of the right version,
# we cannot use the module.
if version[0] is None or script and version[1] is None:
module = None
except ImportError:
message = '{}import of {} module in {} failed'
if cls.check_version:
warning = 'WARNING: '
message += ', linter will not work with python 3 code'
else:
warning = ''
message += ', linter will not run using built in python'
persist.printf(message.format(warning, module, cls.name))
module = None
except Exception as ex:
persist.printf(
'ERROR: unknown exception in {}: {}'
.format(cls.name, str(ex))
)
module = None
# If no module was specified, or the module could not be imported,
# or ST's python does not satisfy the version specified, see if
# any version of python available satisfies the linter. If not,
# set the cmd to '' to disable the linter.
can_lint = True
if not module and cmd and not callable(cmd):
match = util.PYTHON_CMD_RE.match(cmd)
if match and match.group('version'):
can_lint = False
version, script = match.group('version', 'script')
version = util.find_python(version=version, script=script)
if version[0] is not None and (not script or version[1] is not None):
can_lint = True
if can_lint:
cls._cmd = cls.cmd
# If there is a module, setting cmd to None tells us to
# use the check method.
if module:
cls.cmd = None
else:
persist.printf(
'WARNING: {} deactivated, no available version of python{} satisfies {}'
.format(
cls.name,
' or {}'.format(script) if script else '',
cmd
))
cls.disabled = True
cls.module = module
def context_sensitive_executable_path(self, cmd):
"""
Calculate the context-sensitive executable path, using @python and check_version.
Return a tuple of (have_path, path).
Return have_path == False if not self.check_version.
Return have_path == True if cmd is in [script]@python[version] form.
Return None for path if the desired version of python/script cannot be found.
Return '<builtin>' for path if the built-in python should be used.
"""
if not self.check_version:
return False, None
# Check to see if we have a @python command
match = util.PYTHON_CMD_RE.match(cmd[0])
if match:
settings = self.get_view_settings()
if '@python' in settings:
script = match.group('script') or ''
which = '{}@python{}'.format(script, settings.get('@python'))
path = self.which(which)
if path:
if path[0] == '<builtin>':
return True, '<builtin>'
elif path[0] is None or script and path[1] is None:
return True, None
return True, path
return False, None
@classmethod
def get_module_version(cls):
"""
Return the string version of the imported module, without any prefix/suffix.
This method handles the common case where a module (or one of its parents)
defines a __version__ string. For other cases, subclasses should override
this method and return the version string.
"""
if cls.module:
module = cls.module
while True:
if isinstance(getattr(module, '__version__', None), str):
return module.__version__
if hasattr(module, '__package__'):
try:
module = importlib.import_module(module.__package__)
except ImportError:
return None
else:
return None
def run(self, cmd, code):
"""Run the module checker or executable on code and return the output."""
if self.module is not None:
use_module = False
if not self.check_version:
use_module = True
else:
settings = self.get_view_settings()
version = settings.get('@python')
if version is None:
use_module = cmd is None or cmd[0] == '<builtin>'
else:
version = util.find_python(version=version, module=self.module)
use_module = version[0] == '<builtin>'
if use_module:
if persist.debug_mode():
persist.printf(
'{}: {} <builtin>'.format(
self.name,
os.path.basename(self.filename or '<unsaved>')
)
)
try:
errors = self.check(code, os.path.basename(self.filename or '<unsaved>'))
except Exception as err:
persist.printf(
'ERROR: exception in {}.check: {}'
.format(self.name, str(err))
)
errors = ''
if isinstance(errors, (tuple, list)):
return '\n'.join([str(e) for e in errors])
else:
return errors
else:
cmd = self._cmd
else:
cmd = self.cmd or self._cmd
cmd = self.build_cmd(cmd=cmd)
if cmd:
return super().run(cmd, code)
else:
return ''
def check(self, code, filename):
"""
Run a built-in check of code, returning errors.
Subclasses that provide built in checking must override this method
and return a string with one more lines per error, an array of strings,
or an array of objects that can be converted to strings.
"""
persist.printf(
'{}: subclasses must override the PythonLinter.check method'
.format(self.name)
)
return ''

View File

@@ -0,0 +1,134 @@
#
# queue.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 provides a threaded queue for lint requests."""
from queue import Queue, Empty
import threading
import traceback
import time
from . import persist, util
class Daemon:
"""
This class provides a threaded queue that dispatches lints.
The following operations can be added to the queue:
hit - Queue a lint for a given view
delay - Queue a delay for a number of milliseconds
reload - Indicates the main plugin was reloaded
"""
MIN_DELAY = 0.1
running = False
callback = None
q = Queue()
last_runs = {}
def start(self, callback):
"""Start the daemon thread that runs loop."""
self.callback = callback
if self.running:
self.q.put('reload')
else:
self.running = True
threading.Thread(target=self.loop).start()
def loop(self):
"""Continually check the queue for new items and process them."""
last_runs = {}
while True:
try:
try:
item = self.q.get(block=True, timeout=self.MIN_DELAY)
except Empty:
for view_id, (timestamp, delay) in last_runs.copy().items():
# Lint the view if we have gone past the time
# at which the lint wants to run.
if time.monotonic() > timestamp + delay:
self.last_runs[view_id] = time.monotonic()
del last_runs[view_id]
self.lint(view_id, timestamp)
continue
if isinstance(item, tuple):
view_id, timestamp, delay = item
if view_id in self.last_runs and timestamp < self.last_runs[view_id]:
continue
last_runs[view_id] = timestamp, delay
elif isinstance(item, (int, float)):
time.sleep(item)
elif isinstance(item, str):
if item == 'reload':
persist.printf('daemon detected a reload')
self.last_runs.clear()
last_runs.clear()
else:
persist.printf('unknown message sent to daemon:', item)
except:
persist.printf('error in SublimeLinter daemon:')
persist.printf('-' * 20)
persist.printf(traceback.format_exc())
persist.printf('-' * 20)
def hit(self, view):
"""Add a lint request to the queue, return the time at which the request was enqueued."""
timestamp = time.monotonic()
self.q.put((view.id(), timestamp, self.get_delay(view)))
return timestamp
def delay(self, milliseconds=100):
"""Add a millisecond delay to the queue."""
self.q.put(milliseconds / 1000.0)
def lint(self, view_id, timestamp):
"""
Call back into the main plugin to lint the given view.
timestamp is used to determine if the view has been modified
since the lint was requested.
"""
self.callback(view_id, timestamp)
def get_delay(self, view):
"""
Return the delay between a lint request and when it will be processed.
If the lint mode is not background, there is no delay. Otherwise, if
a "delay" setting is not available in any of the settings, MIN_DELAY is used.
"""
if persist.settings.get('lint_mode') != 'background':
return 0
delay = (util.get_view_rc_settings(view) or {}).get('delay')
if delay is None:
delay = persist.settings.get('delay', self.MIN_DELAY)
return delay
queue = Daemon()

View File

@@ -0,0 +1,144 @@
#
# ruby_linter.py
# Part of SublimeLinter3, a code checking framework for Sublime Text 3
#
# Written by Aparajita Fishman
#
# Project: https://github.com/SublimeLinter/SublimeLinter3
# License: MIT
#
"""This module exports the RubyLinter subclass of Linter."""
import os
import re
import shlex
from . import linter, persist, util
CMD_RE = re.compile(r'(?P<gem>.+?)@ruby')
class RubyLinter(linter.Linter):
"""
This Linter subclass provides ruby-specific functionality.
Linters that check ruby using gems should inherit from this class.
By doing so, they automatically get the following features:
- comment_re is defined correctly for ruby.
- Support for rbenv and rvm (via rvm-auto-ruby).
"""
comment_re = r'\s*#'
@classmethod
def initialize(cls):
"""Perform class-level initialization."""
super().initialize()
if cls.executable_path is not None:
return
if not callable(cls.cmd) and cls.cmd:
cls.executable_path = cls.lookup_executables(cls.cmd)
elif cls.executable:
cls.executable_path = cls.lookup_executables(cls.executable)
if not cls.executable_path:
cls.disabled = True
@classmethod
def reinitialize(cls):
"""Perform class-level initialization after plugins have been loaded at startup."""
# Be sure to clear cls.executable_path so that lookup_executables will run.
cls.executable_path = None
cls.initialize()
@classmethod
def lookup_executables(cls, cmd):
"""
Attempt to locate the gem and ruby specified in cmd, return new cmd list.
The following forms are valid:
gem@ruby
gem
ruby
If rbenv is installed and the gem is also under rbenv control,
the gem will be executed directly. Otherwise [ruby <, gem>] will
be returned.
If rvm-auto-ruby is installed, [rvm-auto-ruby <, gem>] will be
returned.
Otherwise [ruby] or [gem] will be returned.
"""
ruby = None
rbenv = util.which('rbenv')
if not rbenv:
ruby = util.which('rvm-auto-ruby')
if not ruby:
ruby = util.which('ruby')
if not rbenv and not ruby:
persist.printf(
'WARNING: {} deactivated, cannot locate ruby, rbenv or rvm-auto-ruby'
.format(cls.name, cmd[0])
)
return []
if isinstance(cmd, str):
cmd = shlex.split(cmd)
match = CMD_RE.match(cmd[0])
if match:
gem = match.group('gem')
elif cmd[0] != 'ruby':
gem = cmd[0]
else:
gem = ''
if gem:
gem_path = util.which(gem)
if gem_path:
if (rbenv and
('{0}.rbenv{0}shims{0}'.format(os.sep) in gem_path or
(os.altsep and '{0}.rbenv{0}shims{0}'.format(os.altsep in gem_path)))):
ruby_cmd = [gem_path]
else:
ruby_cmd = [ruby, gem_path]
else:
persist.printf(
'WARNING: {} deactivated, cannot locate the gem \'{}\''
.format(cls.name, gem)
)
return []
else:
ruby_cmd = [ruby]
if cls.env is None:
# Don't use GEM_HOME with rbenv, it prevents it from using gem shims
if rbenv:
cls.env = {}
else:
gem_home = util.get_environment_variable('GEM_HOME')
if gem_home:
cls.env = {'GEM_HOME': gem_home}
else:
cls.env = {}
return ruby_cmd

File diff suppressed because it is too large Load Diff