mirror of
https://github.com/sstent/sublime-text-3.git
synced 2026-01-25 22:51:41 +00:00
backing up sublime settings
This commit is contained in:
32
Packages/SublimeLinter/lint/__init__.py
Normal file
32
Packages/SublimeLinter/lint/__init__.py
Normal 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',
|
||||
]
|
||||
449
Packages/SublimeLinter/lint/highlight.py
Normal file
449
Packages/SublimeLinter/lint/highlight.py
Normal 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
|
||||
1673
Packages/SublimeLinter/lint/linter.py
Normal file
1673
Packages/SublimeLinter/lint/linter.py
Normal file
File diff suppressed because it is too large
Load Diff
465
Packages/SublimeLinter/lint/persist.py
Normal file
465
Packages/SublimeLinter/lint/persist.py
Normal 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))
|
||||
325
Packages/SublimeLinter/lint/python_linter.py
Normal file
325
Packages/SublimeLinter/lint/python_linter.py
Normal 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 ''
|
||||
134
Packages/SublimeLinter/lint/queue.py
Normal file
134
Packages/SublimeLinter/lint/queue.py
Normal 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()
|
||||
144
Packages/SublimeLinter/lint/ruby_linter.py
Normal file
144
Packages/SublimeLinter/lint/ruby_linter.py
Normal 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
|
||||
1416
Packages/SublimeLinter/lint/util.py
Normal file
1416
Packages/SublimeLinter/lint/util.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user