964 lines
30 KiB
Python
964 lines
30 KiB
Python
# -*- coding: utf-8 -*-
|
|
from functools import partial
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
import threading
|
|
|
|
import sublime
|
|
import sublime_plugin
|
|
|
|
from sublimelinter.loader import Loader
|
|
from sublimelinter.modules.base_linter import INPUT_METHOD_FILE
|
|
|
|
LINTERS = {} # mapping of language name to linter module
|
|
QUEUE = {} # views waiting to be processed by linter
|
|
ERRORS = {} # error messages on given line obtained from linter; they are
|
|
# displayed in the status bar when cursor is on line with error
|
|
VIOLATIONS = {} # violation messages, they are displayed in the status bar
|
|
WARNINGS = {} # warning messages, they are displayed in the status bar
|
|
UNDERLINES = {} # underline regions related to each lint message
|
|
TIMES = {} # collects how long it took the linting to complete
|
|
MOD_LOAD = Loader(os.getcwdu(), LINTERS) # utility to load (and reload
|
|
# if necessary) linter modules [useful when working on plugin]
|
|
|
|
|
|
# For snappier linting, different delays are used for different linting times:
|
|
# (linting time, delays)
|
|
DELAYS = (
|
|
(50, (50, 100)),
|
|
(100, (100, 300)),
|
|
(200, (200, 500)),
|
|
(400, (400, 1000)),
|
|
(800, (800, 2000)),
|
|
(1600, (1600, 3000)),
|
|
)
|
|
|
|
MARKS = {
|
|
'violation': ('', 'dot'),
|
|
'warning': ('', 'dot'),
|
|
'illegal': ('', 'circle'),
|
|
}
|
|
|
|
# All available settings for SublimeLinter;
|
|
# only these are inherited from SublimeLinter.sublime-settings
|
|
ALL_SETTINGS = [
|
|
'annotations',
|
|
'csslint_options',
|
|
'gjslint_ignore',
|
|
'gjslint_options',
|
|
'javascript_linter',
|
|
'jshint_options',
|
|
'jslint_options',
|
|
'pep8',
|
|
'pep8_ignore',
|
|
'perl_linter',
|
|
'pyflakes_ignore',
|
|
'pyflakes_ignore_import_*',
|
|
'coffeelint_options',
|
|
'sublimelinter',
|
|
'sublimelinter_delay',
|
|
'sublimelinter_disable',
|
|
'sublimelinter_executable_map',
|
|
'sublimelinter_fill_outlines',
|
|
'sublimelinter_gutter_marks',
|
|
'sublimelinter_mark_style',
|
|
'sublimelinter_notes',
|
|
'sublimelinter_objj_check_ascii',
|
|
'sublimelinter_popup_errors_on_save',
|
|
'sublimelinter_syntax_map',
|
|
'sublimelinter_wrap_find',
|
|
]
|
|
|
|
WHITESPACE_RE = re.compile(r'\s+')
|
|
|
|
|
|
def get_delay(t, view):
|
|
delay = 0
|
|
|
|
for _t, d in DELAYS:
|
|
if _t <= t:
|
|
delay = d
|
|
|
|
delay = delay or DELAYS[0][1]
|
|
|
|
# If the user specifies a delay greater than the built in delay,
|
|
# figure they only want to see marks when idle.
|
|
minDelay = int(view.settings().get('sublimelinter_delay', 0) * 1000)
|
|
|
|
if minDelay > delay[1]:
|
|
erase_lint_marks(view)
|
|
|
|
return (minDelay, minDelay) if minDelay > delay[1] else delay
|
|
|
|
|
|
def last_selected_lineno(view):
|
|
viewSel = view.sel()
|
|
if not viewSel:
|
|
return None
|
|
return view.rowcol(viewSel[0].end())[0]
|
|
|
|
|
|
def update_statusbar(view):
|
|
vid = view.id()
|
|
lineno = last_selected_lineno(view)
|
|
errors = []
|
|
|
|
if lineno is not None:
|
|
if vid in ERRORS and lineno in ERRORS[vid]:
|
|
errors.extend(ERRORS[vid][lineno])
|
|
|
|
if vid in VIOLATIONS and lineno in VIOLATIONS[vid]:
|
|
errors.extend(VIOLATIONS[vid][lineno])
|
|
|
|
if vid in WARNINGS and lineno in WARNINGS[vid]:
|
|
errors.extend(WARNINGS[vid][lineno])
|
|
|
|
if errors:
|
|
view.set_status('Linter', '; '.join(errors))
|
|
else:
|
|
view.erase_status('Linter')
|
|
|
|
|
|
def run_once(linter, view, **kwargs):
|
|
'''run a linter on a given view regardless of user setting'''
|
|
if not linter:
|
|
return
|
|
|
|
vid = view.id()
|
|
ERRORS[vid] = {}
|
|
VIOLATIONS[vid] = {}
|
|
WARNINGS[vid] = {}
|
|
start = time.time()
|
|
text = view.substr(sublime.Region(0, view.size())).encode('utf-8')
|
|
lines, error_underlines, violation_underlines, warning_underlines, ERRORS[vid], VIOLATIONS[vid], WARNINGS[vid] = linter.run(view, text, view.file_name().encode('utf-8') or '')
|
|
|
|
UNDERLINES[vid] = error_underlines[:]
|
|
UNDERLINES[vid].extend(violation_underlines)
|
|
UNDERLINES[vid].extend(warning_underlines)
|
|
|
|
add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines)
|
|
|
|
if view.settings().get('sublimelinter_notes'):
|
|
highlight_notes(view)
|
|
|
|
update_statusbar(view)
|
|
end = time.time()
|
|
TIMES[vid] = (end - start) * 1000 # Keep how long it took to lint
|
|
|
|
if kwargs.get('event', None) == 'on_post_save' and view.settings().get('sublimelinter_popup_errors_on_save'):
|
|
popup_error_list(view)
|
|
|
|
|
|
def popup_error_list(view):
|
|
vid = view.id()
|
|
errors = ERRORS[vid].copy()
|
|
|
|
for message_map in [VIOLATIONS[vid], WARNINGS[vid]]:
|
|
for line, messages in message_map.items():
|
|
if line in errors:
|
|
errors[line].extend(messages)
|
|
else:
|
|
errors[line] = messages
|
|
|
|
# Flatten the errors into a list
|
|
error_list = []
|
|
|
|
for line in sorted(errors.keys()):
|
|
for index, message in enumerate(errors[line]):
|
|
error_list.append({'line': line, 'message': message})
|
|
|
|
panel_items = []
|
|
|
|
for error in error_list:
|
|
line_text = view.substr(view.full_line(view.text_point(error['line'], 0)))
|
|
item = [error['message'], u'{0}: {1}'.format(error['line'] + 1, line_text.strip())]
|
|
panel_items.append(item)
|
|
|
|
def on_done(selected_item):
|
|
if selected_item == -1:
|
|
return
|
|
|
|
selected = view.sel()
|
|
selected.clear()
|
|
|
|
error = error_list[selected_item]
|
|
region_begin = view.text_point(error['line'], 0)
|
|
|
|
# Go to the first non-whitespace character of the line
|
|
line_text = view.substr(view.full_line(region_begin))
|
|
match = WHITESPACE_RE.match(line_text)
|
|
|
|
if (match):
|
|
region_begin += len(match.group(0))
|
|
|
|
selected.add(sublime.Region(region_begin, region_begin))
|
|
# We have to force a move to update the cursor position
|
|
view.run_command('move', {'by': 'characters', 'forward': True})
|
|
view.run_command('move', {'by': 'characters', 'forward': False})
|
|
view.show_at_center(region_begin)
|
|
|
|
view.window().show_quick_panel(panel_items, on_done)
|
|
|
|
|
|
def add_lint_marks(view, lines, error_underlines, violation_underlines, warning_underlines):
|
|
'''Adds lint marks to view.'''
|
|
vid = view.id()
|
|
erase_lint_marks(view)
|
|
types = {'warning': warning_underlines, 'violation': violation_underlines, 'illegal': error_underlines}
|
|
|
|
for type_name, underlines in types.items():
|
|
if underlines:
|
|
view.add_regions('lint-underline-' + type_name, underlines, 'sublimelinter.underline.' + type_name, sublime.DRAW_EMPTY_AS_OVERWRITE)
|
|
|
|
if lines:
|
|
outline_style = view.settings().get('sublimelinter_mark_style', 'outline')
|
|
|
|
# This test is for the legacy "fill" setting; it will be removed
|
|
# in a future version (likely v1.7).
|
|
if view.settings().get('sublimelinter_fill_outlines', False):
|
|
outline_style = 'fill'
|
|
|
|
gutter_mark_enabled = True if view.settings().get('sublimelinter_gutter_marks', False) else False
|
|
|
|
outlines = {'warning': [], 'violation': [], 'illegal': []}
|
|
|
|
for line in ERRORS[vid]:
|
|
outlines['illegal'].append(view.full_line(view.text_point(line, 0)))
|
|
|
|
for line in WARNINGS[vid]:
|
|
outlines['warning'].append(view.full_line(view.text_point(line, 0)))
|
|
|
|
for line in VIOLATIONS[vid]:
|
|
outlines['violation'].append(view.full_line(view.text_point(line, 0)))
|
|
|
|
for lint_type in outlines:
|
|
if outlines[lint_type]:
|
|
args = [
|
|
'lint-outlines-{0}'.format(lint_type),
|
|
outlines[lint_type],
|
|
'sublimelinter.outline.{0}'.format(lint_type),
|
|
MARKS[lint_type][gutter_mark_enabled]
|
|
]
|
|
|
|
if outline_style == 'none':
|
|
args.append(sublime.HIDDEN)
|
|
elif outline_style == 'fill':
|
|
pass # outlines are filled by default
|
|
else:
|
|
args.append(sublime.DRAW_OUTLINED)
|
|
view.add_regions(*args)
|
|
|
|
|
|
def erase_lint_marks(view):
|
|
'''erase all "lint" error marks from view'''
|
|
view.erase_regions('lint-underline-illegal')
|
|
view.erase_regions('lint-underline-violation')
|
|
view.erase_regions('lint-underline-warning')
|
|
view.erase_regions('lint-outlines-illegal')
|
|
view.erase_regions('lint-outlines-violation')
|
|
view.erase_regions('lint-outlines-warning')
|
|
view.erase_regions('lint-annotations')
|
|
|
|
|
|
def get_lint_regions(view, reverse=False, coalesce=False):
|
|
vid = view.id()
|
|
underlines = UNDERLINES.get(vid, [])[:]
|
|
|
|
if (coalesce):
|
|
# Each of these regions is one character, so transform it into the character points
|
|
points = sorted([region.begin() for region in underlines])
|
|
|
|
# Now coalesce adjacent characters into a single region
|
|
underlines = []
|
|
last_point = -999
|
|
|
|
for point in points:
|
|
if point != last_point + 1:
|
|
underlines.append(sublime.Region(point, point))
|
|
else:
|
|
region = underlines[-1]
|
|
underlines[-1] = sublime.Region(region.begin(), point)
|
|
|
|
last_point = point
|
|
|
|
# Now get all outlines, which includes the entire line where underlines are
|
|
outlines = view.get_regions('lint-outlines-illegal')
|
|
outlines.extend(view.get_regions('lint-outlines-violation'))
|
|
outlines.extend(view.get_regions('lint-outlines-warning'))
|
|
outlines.extend(view.get_regions('lint-annotations'))
|
|
|
|
# If an outline region contains an underline region, use only the underline
|
|
regions = underlines
|
|
|
|
for outline in outlines:
|
|
contains_underlines = False
|
|
|
|
for underline in underlines:
|
|
if outline.contains(underline):
|
|
contains_underlines = True
|
|
break
|
|
|
|
if not contains_underlines:
|
|
regions.append(outline)
|
|
|
|
return sorted(regions, key=lambda x: x.begin(), reverse=reverse)
|
|
|
|
|
|
def select_lint_region(view, region):
|
|
selected = view.sel()
|
|
selected.clear()
|
|
|
|
# Find the first underline region within the region to select.
|
|
# If there are none, put the cursor at the beginning of the line.
|
|
underlineRegion = find_underline_within(view, region)
|
|
|
|
if underlineRegion is None:
|
|
underlineRegion = sublime.Region(region.begin(), region.begin())
|
|
|
|
selected.add(underlineRegion)
|
|
view.show(underlineRegion, True)
|
|
|
|
|
|
def find_underline_within(view, region):
|
|
underlines = view.get_regions('lint-underline-illegal')
|
|
underlines.extend(view.get_regions('lint-underline-violation'))
|
|
underlines.extend(view.get_regions('lint-underline-warning'))
|
|
underlines.sort(key=lambda x: x.begin())
|
|
|
|
for underline in underlines:
|
|
if region.contains(underline):
|
|
return underline
|
|
|
|
return None
|
|
|
|
|
|
def syntax_name(view):
|
|
syntax = os.path.basename(view.settings().get('syntax'))
|
|
syntax = os.path.splitext(syntax)[0]
|
|
return syntax
|
|
|
|
|
|
def select_linter(view, ignore_disabled=False):
|
|
'''selects the appropriate linter to use based on language in current view'''
|
|
syntax = syntax_name(view)
|
|
lc_syntax = syntax.lower()
|
|
language = None
|
|
linter = None
|
|
syntaxMap = view.settings().get('sublimelinter_syntax_map', {})
|
|
|
|
if syntax in syntaxMap:
|
|
language = syntaxMap[syntax].lower()
|
|
elif lc_syntax in syntaxMap:
|
|
language = syntaxMap[lc_syntax].lower()
|
|
elif lc_syntax in LINTERS:
|
|
language = lc_syntax
|
|
|
|
if language:
|
|
if ignore_disabled:
|
|
disabled = []
|
|
else:
|
|
disabled = view.settings().get('sublimelinter_disable', [])
|
|
|
|
if language not in disabled:
|
|
linter = LINTERS[language]
|
|
|
|
# If the enabled state is False, it must be checked.
|
|
# Enabled checking has to be deferred to first view use because
|
|
# user settings cannot be loaded during plugin startup.
|
|
if not linter.enabled:
|
|
enabled, message = linter.check_enabled(view)
|
|
print 'SublimeLinter: {0} {1} ({2})'.format(language, 'enabled' if enabled else 'disabled', message)
|
|
|
|
if not enabled:
|
|
del LINTERS[language]
|
|
linter = None
|
|
|
|
return linter
|
|
|
|
|
|
def highlight_notes(view):
|
|
'''highlight user-specified annotations in a file'''
|
|
view.erase_regions('lint-annotations')
|
|
text = view.substr(sublime.Region(0, view.size()))
|
|
regions = LINTERS['annotations'].built_in_check(view, text, '')
|
|
|
|
if regions:
|
|
view.add_regions('lint-annotations', regions, 'sublimelinter.annotations', sublime.DRAW_EMPTY_AS_OVERWRITE)
|
|
|
|
|
|
def _update_view(view, filename, **kwargs):
|
|
# It is possible that by the time the queue is run,
|
|
# the original file is no longer being displayed in the view,
|
|
# or the view may be gone. This happens especially when
|
|
# viewing files temporarily by single-clicking on a filename
|
|
# in the sidebar or when selecting a file through the choose file palette.
|
|
valid_view = False
|
|
view_id = view.id()
|
|
|
|
for window in sublime.windows():
|
|
for v in window.views():
|
|
if v.id() == view_id:
|
|
valid_view = True
|
|
break
|
|
|
|
if not valid_view or view.is_loading() or view.file_name().encode('utf-8') != filename:
|
|
return
|
|
|
|
try:
|
|
run_once(select_linter(view), view, **kwargs)
|
|
except RuntimeError, ex:
|
|
print ex
|
|
|
|
|
|
def queue_linter(linter, view, timeout=-1, preemptive=False, event=None):
|
|
'''Put the current view in a queue to be examined by a linter'''
|
|
if linter is None:
|
|
erase_lint_marks(view) # may have changed file type and left marks behind
|
|
|
|
# No point in queuing anything if no linters will run
|
|
if not view.settings().get('sublimelinter_notes'):
|
|
return
|
|
|
|
if preemptive:
|
|
timeout = busy_timeout = 0
|
|
elif timeout == -1:
|
|
timeout, busy_timeout = get_delay(TIMES.get(view.id(), 100), view)
|
|
else:
|
|
busy_timeout = timeout
|
|
|
|
kwargs = {'timeout': timeout, 'busy_timeout': busy_timeout, 'preemptive': preemptive, 'event': event}
|
|
queue(view, partial(_update_view, view, view.file_name().encode('utf-8'), **kwargs), kwargs)
|
|
|
|
|
|
def _callback(view, filename, kwargs):
|
|
kwargs['callback'](view, filename, **kwargs)
|
|
|
|
|
|
def background_linter():
|
|
__lock_.acquire()
|
|
|
|
try:
|
|
callbacks = QUEUE.values()
|
|
QUEUE.clear()
|
|
finally:
|
|
__lock_.release()
|
|
|
|
for callback in callbacks:
|
|
sublime.set_timeout(callback, 0)
|
|
|
|
################################################################################
|
|
# Queue dispatcher system:
|
|
|
|
queue_dispatcher = background_linter
|
|
queue_thread_name = 'background linter'
|
|
MAX_DELAY = 10
|
|
|
|
|
|
def queue_loop():
|
|
'''An infinite loop running the linter in a background thread meant to
|
|
update the view after user modifies it and then does no further
|
|
modifications for some time as to not slow down the UI with linting.'''
|
|
global __signaled_, __signaled_first_
|
|
|
|
while __loop_:
|
|
#print 'acquire...'
|
|
__semaphore_.acquire()
|
|
__signaled_first_ = 0
|
|
__signaled_ = 0
|
|
#print 'DISPATCHING!', len(QUEUE)
|
|
queue_dispatcher()
|
|
|
|
|
|
def queue(view, callback, kwargs):
|
|
global __signaled_, __signaled_first_
|
|
now = time.time()
|
|
__lock_.acquire()
|
|
|
|
try:
|
|
QUEUE[view.id()] = callback
|
|
timeout = kwargs['timeout']
|
|
busy_timeout = kwargs['busy_timeout']
|
|
|
|
if now < __signaled_ + timeout * 4:
|
|
timeout = busy_timeout or timeout
|
|
|
|
__signaled_ = now
|
|
_delay_queue(timeout, kwargs['preemptive'])
|
|
|
|
if not __signaled_first_:
|
|
__signaled_first_ = __signaled_
|
|
#print 'first',
|
|
#print 'queued in', (__signaled_ - now)
|
|
finally:
|
|
__lock_.release()
|
|
|
|
|
|
def _delay_queue(timeout, preemptive):
|
|
global __signaled_, __queued_
|
|
now = time.time()
|
|
|
|
if not preemptive and now <= __queued_ + 0.01:
|
|
return # never delay queues too fast (except preemptively)
|
|
|
|
__queued_ = now
|
|
_timeout = float(timeout) / 1000
|
|
|
|
if __signaled_first_:
|
|
if MAX_DELAY > 0 and now - __signaled_first_ + _timeout > MAX_DELAY:
|
|
_timeout -= now - __signaled_first_
|
|
if _timeout < 0:
|
|
_timeout = 0
|
|
timeout = int(round(_timeout * 1000, 0))
|
|
|
|
new__signaled_ = now + _timeout - 0.01
|
|
|
|
if __signaled_ >= now - 0.01 and (preemptive or new__signaled_ >= __signaled_ - 0.01):
|
|
__signaled_ = new__signaled_
|
|
#print 'delayed to', (preemptive, __signaled_ - now)
|
|
|
|
def _signal():
|
|
if time.time() < __signaled_:
|
|
return
|
|
__semaphore_.release()
|
|
|
|
sublime.set_timeout(_signal, timeout)
|
|
|
|
|
|
def delay_queue(timeout):
|
|
__lock_.acquire()
|
|
try:
|
|
_delay_queue(timeout, False)
|
|
finally:
|
|
__lock_.release()
|
|
|
|
|
|
# only start the thread once - otherwise the plugin will get laggy
|
|
# when saving it often.
|
|
__semaphore_ = threading.Semaphore(0)
|
|
__lock_ = threading.Lock()
|
|
__queued_ = 0
|
|
__signaled_ = 0
|
|
__signaled_first_ = 0
|
|
|
|
# First finalize old standing threads:
|
|
__loop_ = False
|
|
__pre_initialized_ = False
|
|
|
|
|
|
def queue_finalize(timeout=None):
|
|
global __pre_initialized_
|
|
|
|
for thread in threading.enumerate():
|
|
if thread.isAlive() and thread.name == queue_thread_name:
|
|
__pre_initialized_ = True
|
|
thread.__semaphore_.release()
|
|
thread.join(timeout)
|
|
queue_finalize()
|
|
|
|
# Initialize background thread:
|
|
__loop_ = True
|
|
__active_linter_thread = threading.Thread(target=queue_loop, name=queue_thread_name)
|
|
__active_linter_thread.__semaphore_ = __semaphore_
|
|
__active_linter_thread.start()
|
|
|
|
################################################################################
|
|
|
|
UNRECOGNIZED = '''
|
|
* Unrecognized option * : %s
|
|
==============================================
|
|
|
|
'''
|
|
|
|
|
|
def view_in_tab(view, title, text, file_type):
|
|
'''Helper function to display information in a tab.
|
|
'''
|
|
tab = view.window().new_file()
|
|
tab.set_name(title)
|
|
_id = tab.buffer_id()
|
|
tab.set_scratch(_id)
|
|
tab.settings().set('gutter', True)
|
|
tab.settings().set('line_numbers', False)
|
|
tab.set_syntax_file(file_type)
|
|
ed = tab.begin_edit()
|
|
tab.insert(ed, 0, text)
|
|
tab.end_edit(ed)
|
|
return tab, _id
|
|
|
|
|
|
def lint_views(linter):
|
|
if not linter:
|
|
return
|
|
|
|
viewsToLint = []
|
|
|
|
for window in sublime.windows():
|
|
for view in window.views():
|
|
viewLinter = select_linter(view)
|
|
|
|
if viewLinter == linter:
|
|
viewsToLint.append(view)
|
|
|
|
for view in viewsToLint:
|
|
queue_linter(linter, view, preemptive=True)
|
|
|
|
|
|
def reload_view_module(view):
|
|
for name, linter in LINTERS.items():
|
|
module = sys.modules[linter.__module__]
|
|
|
|
if module.__file__.encode('utf-8') == view.file_name().encode('utf-8'):
|
|
print 'SublimeLinter: reloading language:', linter.language
|
|
MOD_LOAD.reload_module(module)
|
|
lint_views(linter)
|
|
break
|
|
|
|
|
|
def settings_changed():
|
|
for window in sublime.windows():
|
|
for view in window.views():
|
|
linter = select_linter(view)
|
|
|
|
if (linter):
|
|
reload_settings(view)
|
|
|
|
|
|
def reload_settings(view):
|
|
'''Restores user settings.'''
|
|
settings = sublime.load_settings(__name__ + '.sublime-settings')
|
|
settings.clear_on_change(__name__)
|
|
settings.add_on_change(__name__, settings_changed)
|
|
|
|
for setting in ALL_SETTINGS:
|
|
if settings.get(setting) != None:
|
|
view.settings().set(setting, settings.get(setting))
|
|
|
|
if view.settings().get('sublimelinter') == None:
|
|
view.settings().set('sublimelinter', True)
|
|
|
|
|
|
class LintCommand(sublime_plugin.TextCommand):
|
|
'''command to interact with linters'''
|
|
|
|
def __init__(self, view):
|
|
self.view = view
|
|
self.help_called = False
|
|
|
|
def run_(self, action):
|
|
'''method called by default via view.run_command;
|
|
used to dispatch to appropriate method'''
|
|
if not action:
|
|
return
|
|
|
|
try:
|
|
lc_action = action.lower()
|
|
except AttributeError:
|
|
return
|
|
|
|
if lc_action == 'reset':
|
|
self.reset()
|
|
elif lc_action == 'on':
|
|
self.on()
|
|
elif lc_action == 'load-save':
|
|
self.enable_load_save()
|
|
elif lc_action == 'save-only':
|
|
self.enable_save_only()
|
|
elif lc_action == 'off':
|
|
self.off()
|
|
elif action.lower() in LINTERS:
|
|
self._run(lc_action)
|
|
|
|
def reset(self):
|
|
'''Removes existing lint marks and restores user settings.'''
|
|
erase_lint_marks(self.view)
|
|
reload_settings(self.view)
|
|
|
|
def on(self):
|
|
'''Turns background linting on.'''
|
|
self.view.settings().set('sublimelinter', True)
|
|
queue_linter(select_linter(self.view), self.view, preemptive=True)
|
|
|
|
def enable_load_save(self):
|
|
'''Turns load-save linting on.'''
|
|
self.view.settings().set('sublimelinter', 'load-save')
|
|
erase_lint_marks(self.view)
|
|
|
|
def enable_save_only(self):
|
|
'''Turns save-only linting on.'''
|
|
self.view.settings().set('sublimelinter', 'save-only')
|
|
erase_lint_marks(self.view)
|
|
|
|
def off(self):
|
|
'''Turns background linting off.'''
|
|
self.view.settings().set('sublimelinter', False)
|
|
erase_lint_marks(self.view)
|
|
|
|
def _run(self, name):
|
|
'''runs an existing linter'''
|
|
run_once(LINTERS[name.lower()], self.view)
|
|
|
|
|
|
class BackgroundLinter(sublime_plugin.EventListener):
|
|
'''This plugin controls a linter meant to work in the background
|
|
to provide interactive feedback as a file is edited. It can be
|
|
turned off via a setting.
|
|
'''
|
|
|
|
def __init__(self):
|
|
super(BackgroundLinter, self).__init__()
|
|
self.lastSelectedLineNo = -1
|
|
|
|
def on_modified(self, view):
|
|
if view.is_scratch():
|
|
return
|
|
|
|
if view.settings().get('sublimelinter') != True:
|
|
erase_lint_marks(view)
|
|
return
|
|
|
|
linter = select_linter(view)
|
|
|
|
# File-based linters are not invoked during a modify
|
|
if linter and linter.input_method == INPUT_METHOD_FILE:
|
|
erase_lint_marks(view)
|
|
return
|
|
|
|
# Reset the last selected line number so that the current line will show error messages
|
|
# when update_statusbar is called.
|
|
self.lastSelectedLineNo = -1
|
|
queue_linter(linter, view)
|
|
|
|
def on_load(self, view):
|
|
reload_settings(view)
|
|
|
|
if view.is_scratch() or view.settings().get('sublimelinter') == False or view.settings().get('sublimelinter') == 'save-only':
|
|
return
|
|
|
|
queue_linter(select_linter(view), view, event='on_load')
|
|
|
|
def on_post_save(self, view):
|
|
if view.is_scratch() or view.settings().get('sublimelinter') == False:
|
|
return
|
|
|
|
reload_view_module(view)
|
|
queue_linter(select_linter(view), view, preemptive=True, event='on_post_save')
|
|
|
|
def on_selection_modified(self, view):
|
|
if view.is_scratch():
|
|
return
|
|
delay_queue(1000) # on movement, delay queue (to make movement responsive)
|
|
|
|
# We only display errors in the status bar for the last line in the current selection.
|
|
# If that line number has not changed, there is no point in updating the status bar.
|
|
lastSelectedLineNo = last_selected_lineno(view)
|
|
|
|
if lastSelectedLineNo != self.lastSelectedLineNo:
|
|
self.lastSelectedLineNo = lastSelectedLineNo
|
|
update_statusbar(view)
|
|
|
|
|
|
class FindLintErrorCommand(sublime_plugin.TextCommand):
|
|
'''This command is just a superclass for other commands, it is never enabled.'''
|
|
def is_enabled(self):
|
|
return select_linter(self.view) is not None
|
|
|
|
def find_lint_error(self, forward):
|
|
linter = select_linter(self.view, ignore_disabled=True)
|
|
|
|
if not linter:
|
|
return
|
|
|
|
self.view.run_command('lint', linter.language)
|
|
regions = get_lint_regions(self.view, reverse=not forward, coalesce=True)
|
|
|
|
if len(regions) == 0:
|
|
sublime.error_message('No lint errors.')
|
|
return
|
|
|
|
selected = self.view.sel()
|
|
point = selected[0].begin() if forward else selected[-1].end()
|
|
regionToSelect = 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 forward:
|
|
for index, region in enumerate(regions):
|
|
if point < region.begin():
|
|
regionToSelect = region
|
|
break
|
|
else:
|
|
for index, region in enumerate(regions):
|
|
if point > region.end():
|
|
regionToSelect = 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 regionToSelect is None and (len(regions) > 1 or not regions[0].contains(point)):
|
|
if self.view.settings().get('sublimelinter_wrap_find', True):
|
|
regionToSelect = regions[0]
|
|
|
|
if regionToSelect is not None:
|
|
select_lint_region(self.view, regionToSelect)
|
|
else:
|
|
sublime.error_message('No {0} lint errors.'.format('next' if forward else 'previous'))
|
|
|
|
return regionToSelect
|
|
|
|
|
|
class FindNextLintErrorCommand(FindLintErrorCommand):
|
|
def run(self, edit):
|
|
'''
|
|
Move the cursor to the next lint error in the current view.
|
|
The search will wrap to the top unless the sublimelinter_wrap_find
|
|
setting is set to false.
|
|
'''
|
|
self.find_lint_error(forward=True)
|
|
|
|
|
|
class FindPreviousLintErrorCommand(FindLintErrorCommand):
|
|
def run(self, edit):
|
|
'''
|
|
Move the cursor to the previous lint error in the current view.
|
|
The search will wrap to the bottom unless the sublimelinter_wrap_find
|
|
setting is set to false.
|
|
'''
|
|
self.find_lint_error(forward=False)
|
|
|
|
|
|
class SublimelinterWindowCommand(sublime_plugin.WindowCommand):
|
|
def is_enabled(self):
|
|
view = self.window.active_view()
|
|
|
|
if view:
|
|
if view.is_scratch():
|
|
return False
|
|
else:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def run_(self, args):
|
|
pass
|
|
|
|
|
|
class SublimelinterAnnotationsCommand(SublimelinterWindowCommand):
|
|
'''Commands to extract annotations and display them in
|
|
a file
|
|
'''
|
|
def run_(self, args):
|
|
linter = LINTERS.get('annotations', None)
|
|
|
|
if linter is None:
|
|
return
|
|
|
|
view = self.window.active_view()
|
|
|
|
if not view:
|
|
return
|
|
|
|
text = view.substr(sublime.Region(0, view.size())).encode('utf-8')
|
|
filename = view.file_name().encode('utf-8')
|
|
notes = linter.extract_annotations(text, view, filename)
|
|
_, filename = os.path.split(filename)
|
|
annotations_view, _id = view_in_tab(view, 'Annotations from {0}'.format(filename), notes, '')
|
|
|
|
|
|
class SublimelinterCommand(SublimelinterWindowCommand):
|
|
def is_enabled(self):
|
|
enabled = super(SublimelinterCommand, self).is_enabled()
|
|
|
|
if not enabled:
|
|
return False
|
|
|
|
linter = select_linter(self.window.active_view(), ignore_disabled=True)
|
|
return linter is not None
|
|
|
|
def run_(self, args={}):
|
|
view = self.window.active_view()
|
|
action = args.get('action', '')
|
|
|
|
if view and action:
|
|
if action == 'lint':
|
|
self.lint_view(view, show_popup_list=args.get('show_popup', False))
|
|
else:
|
|
view.run_command('lint', action)
|
|
|
|
def lint_view(self, view, show_popup_list):
|
|
linter = select_linter(view, ignore_disabled=True)
|
|
|
|
if linter:
|
|
view.run_command('lint', linter.language)
|
|
regions = get_lint_regions(view, coalesce=True)
|
|
|
|
if regions:
|
|
if show_popup_list:
|
|
popup_error_list(view)
|
|
else:
|
|
sublime.error_message('{0} lint error{1}.'.format(len(regions), 's' if len(regions) != 1 else ''))
|
|
else:
|
|
sublime.error_message('No lint errors.')
|
|
else:
|
|
syntax = syntax_name(view)
|
|
sublime.error_message('No linter for the syntax "{0}"'.format(syntax))
|
|
|
|
|
|
class SublimelinterLintCommand(SublimelinterCommand):
|
|
def is_enabled(self):
|
|
enabled = super(SublimelinterLintCommand, self).is_enabled()
|
|
|
|
if enabled:
|
|
view = self.window.active_view()
|
|
|
|
if view and view.settings().get('sublimelinter') == True:
|
|
return False
|
|
|
|
return enabled
|
|
|
|
|
|
class SublimelinterShowErrorsCommand(SublimelinterCommand):
|
|
def is_enabled(self):
|
|
return super(SublimelinterShowErrorsCommand, self).is_enabled()
|
|
|
|
|
|
class SublimelinterEnableLoadSaveCommand(SublimelinterCommand):
|
|
def is_enabled(self):
|
|
enabled = super(SublimelinterEnableLoadSaveCommand, self).is_enabled()
|
|
|
|
if enabled:
|
|
view = self.window.active_view()
|
|
|
|
if view and view.settings().get('sublimelinter') == 'load-save':
|
|
return False
|
|
|
|
return enabled
|
|
|
|
|
|
class SublimelinterEnableSaveOnlyCommand(SublimelinterCommand):
|
|
def is_enabled(self):
|
|
enabled = super(SublimelinterEnableSaveOnlyCommand, self).is_enabled()
|
|
|
|
if enabled:
|
|
view = self.window.active_view()
|
|
|
|
if view and view.settings().get('sublimelinter') == 'save-only':
|
|
return False
|
|
|
|
return enabled
|
|
|
|
|
|
class SublimelinterDisableCommand(SublimelinterCommand):
|
|
def is_enabled(self):
|
|
enabled = super(SublimelinterDisableCommand, self).is_enabled()
|
|
|
|
if enabled:
|
|
view = self.window.active_view()
|
|
|
|
if view and view.settings().get('sublimelinter') == False:
|
|
return False
|
|
|
|
return enabled
|