mirror of
https://github.com/sstent/sublime-text-3.git
synced 2026-01-25 14:41:38 +00:00
466 lines
15 KiB
Python
466 lines
15 KiB
Python
#
|
|
# 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))
|