<reponame>oleg-vinted/ApplySyntax
"""Apply Syntax."""
import sublime
import sublime_plugin
import os
import re
import imp
import sys
from wcmatch import glob

GLOB_FLAGS = glob.G | glob.B | glob.E | glob.N | glob.D

DEFAULT_SETTINGS = '''
{
    // If you want exceptions reraised so you can see them in the console, change this to true.
    "reraise_exceptions": false,

    // If you want to have a syntax applied when new files are created, set new_file_syntax to the name of the syntax
    // to use.  The format is exactly the same as "syntax" in the rules below. For example, if you want to have a new
    // file use JavaScript syntax, set new_file_syntax to 'JavaScript'.
    "new_file_syntax": false,

    // Auto add extensions to language settings file in User folder.
    // Do not manually remove "apply_syntax_extensions" from the settings file.
    // "extenstions" are ignored by "match": "all" setting.
    "add_exts_to_lang_settings": true,

    // Control level of logging in the console.
    // (true|false|"verbose")
    "debug": true,

    // Put your custom syntax rules here:
    "syntaxes": [
    ]
}
'''

USE_ST_SYNTAX = int(sublime.version()) >= 3084
PLUGIN_NAME = 'ApplySyntax'
PLUGIN_DIR = "Packages/%s" % PLUGIN_NAME
PLUGIN_SETTINGS = PLUGIN_NAME + '.sublime-settings'
EXT_SETTINGS = PLUGIN_NAME + ".ext-list"
ST_LANGUAGES = ('.sublime-syntax', '.tmLanguage') if USE_ST_SYNTAX else ('.tmLanguage',)
SETTINGS = {}
LANG_HASH = 0

# Call back for whether view(s) have been touched
on_touched_callback = None


def ensure_user_settings():
    """Create a default 'User' settings file for ApplySyntax if it doesn't exist."""

    user_settings_file = os.path.join(sublime.packages_path(), 'User', PLUGIN_SETTINGS)
    if os.path.exists(user_settings_file):
        return

    # file doesn't exist, let's create a bare one
    with open(user_settings_file, 'w') as f:
        f.write(DEFAULT_SETTINGS)


def get_all_syntax_files():
    """Find all `sublime-syntax` and `tmLanguage` files."""

    syntax_files = []
    if USE_ST_SYNTAX:
        syntax_files += sublime.find_resources("*.sublime-syntax")
    syntax_files += sublime.find_resources("*.tmLanguage")
    return syntax_files


def sublime_format_path(pth):
    """Format the path for the sublime API."""

    m = re.match(r"^([A-Za-z]{1}):(?:/|\\)(.*)", pth)
    if sublime.platform() == "windows" and m is not None:
        pth = m.group(1) + "/" + m.group(2)
    return pth.replace("\\", "/")


def get_lang_hash():
    """
    Return the hash of the loaded languages in Sublime.

    Return the actual language `frozenset` (hashable) as well.
    """

    def was_seen(item, item_set):
        """Check if item was seen already."""

        seen_item = True
        base = os.path.splitext(item)[0]
        if base is not item_set:
            item_set.add(base)
            seen_item = False
        return seen_item

    # Strip off `tmlanguage` so we don't have to worry about case of extension
    seen = set()
    lst = frozenset(
        [os.path.splitext(x)[0] for x in get_all_syntax_files() if not was_seen(x, seen)]
    )
    hsh = hash(lst)
    devlog("Language Hash - '%s'" % str(hsh))
    return hsh, lst


def update_language_extensions(ext_added):
    """Process the extensions for each language."""

    for lang, exts in ext_added.items():
        updated = False
        settings_file = lang + ".sublime-settings"
        lang_settings = sublime.load_settings(settings_file)
        extension_file = sublime.load_settings(EXT_SETTINGS)
        lang_ext = set(lang_settings.get("extensions", []))
        apsy_ext = set(extension_file.get(lang, []))

        for ext in list(exts):
            if ext not in lang_ext:
                # Append extension to current sublime extension list
                lang_ext.add(ext)
                # Track which extensions were specifically added by apply syntax
                apsy_ext.add(ext)
                updated = True

        if updated:
            devlog("============" + settings_file + "============")
            devlog("Updated Extensions: %s" % str(lang_ext))
            lang_settings.set("extensions", list(lang_ext))
            if len(apsy_ext):
                extension_file.set(lang, sorted(list(apsy_ext)))
            else:
                extension_file.erase(sorted(lang))
            sublime.save_settings(settings_file)
            sublime.save_settings(EXT_SETTINGS)


def map_extensions(ext, lst, names, ext_map, ext_added):
    """Create mappings to help with updating and pruning extensions."""

    # Always deal with language names as a series of names
    if not isinstance(names, list):
        names = [names]

    # For each language name that currently exists and is loaded,
    # append the extensions to the corresponding settings file
    # if the extension is not already there

    for n in names:
        # updated = False
        path = os.path.dirname(n)
        name = os.path.basename(n)
        syntax_file = sublime_format_path('/'.join(['Packages', path, name]))
        if syntax_file in lst:
            for e in ext:
                if e in ext_map:
                    ext_map[e].append(name)
                else:
                    ext_map[e] = [name]

        for ext, languages in ext_map.items():
            lang = languages[-1]
            ext_map[ext] = [lang]
            ext_added[lang].add(ext)


def prune_language_extensions(ext_map, ext_added):
    """
    Prune dead extensions that were added by ApplySyntax (AS), but are no longer defined in AS.

    `exts`        - Sublime's extension list for the given language
    `old_ext`     - The current saved list of AS added extension
    `new_ext`     - The current tracked list of extensions found defined by AS
    `bad_ext`     - (`old_ext` - `new_ext`) Extensions that were added by AS but are no longer defined
    `updated_ext` - (`ext` - `bad_ext`) Sublime's adjusted extension list minus the obsolete ones
    """

    devlog("Pruning Extensions")
    for name, new_ext in ext_added.items():
        updated = False
        settings_file = name + '.sublime-settings'
        lang_settings = sublime.load_settings(settings_file)
        extension_file = sublime.load_settings(EXT_SETTINGS)
        exts = set(lang_settings.get("extensions", []))
        old_ext = set(extension_file.get(name, []))

        # Calculate the correct extension list
        bad_ext = old_ext.difference(new_ext)
        updated_ext = exts.difference(bad_ext)

        # Remove extension from file it ApplySyntax added
        # it to a different file
        for ext in list(updated_ext):
            try:
                if ext_map[ext][-1] != name:
                    updated_ext.remove(ext)
            except Exception:
                pass

        # Update settings file if necessary
        if len(updated_ext) != len(exts):
            # Update pruned list
            lang_settings.set("extensions", sorted(list(updated_ext)))
            if len(new_ext) == 0:
                # No currently added extensions by AS
                extension_file.erase(name)
            else:
                # Updated with relevant AS extensions
                extension_file.set(name, sorted(list(new_ext)))
            updated = True

        if updated:
            devlog("============" + settings_file + "============")
            devlog("Pruned Extensions: %s" % str(bad_ext))
            sublime.save_settings(settings_file)
            sublime.save_settings(EXT_SETTINGS)


def update_extenstions(lst):
    """Walk through all syntax rules updating the extensions in the corresponding language settings."""

    devlog("Updating Extensions")

    # Prepare a list to track extensions defined by ApplySyntax
    ext_added = dict([(os.path.splitext(os.path.basename(l))[0], set()) for l in lst])
    ext_map = {}

    # Walk the entries
    for entry in SETTINGS.get("syntaxes", []) + SETTINGS.get("default_syntaxes", []):
        # Grab the extensions from each relevant rule
        ext = []
        if "extensions" in entry:
            ext += entry.get("extensions", [])

        # Add the extensions to the relevant language settings file
        if len(ext):
            syntax = entry.get("syntax", entry.get("name"))
            if isinstance(syntax, list):
                for s in syntax:
                    name = os.path.splitext(s)[0]
                    devlog("Found Extensions: %s - %s" % (name, str(ext)))
                    map_extensions(ext, lst, name, ext_map, ext_added)
            else:
                name = os.path.splitext(syntax)[0]
                devlog("Found Extensions: %s - %s" % (name, str(ext)))
                map_extensions(ext, lst, name, ext_map, ext_added)

    update_language_extensions(ext_added)

    prune_language_extensions(ext_map, ext_added)


def log(msg):
    """Print the ApplySyntax log message in console."""

    print("ApplySyntax: %s" % msg)


def debug(msg):
    """Print the debug ApplySyntax log message in console (debug mode only)."""

    if SETTINGS.get("debug", True) in (True, 'verbose'):
        log(msg)


def devlog(msg):
    """Print the developer mode ApplySyntax log message in console (developer mode only)."""

    if SETTINGS.get("debug", True) == 'verbose':
        log(msg)


class ApplySyntaxBrowseCommand(sublime_plugin.WindowCommand):
    """Browse syntaxes."""

    def run(self):
        """Run command."""

        self.syntaxes = [
            os.path.splitext(x)[0].replace('Packages/', '', 1)
            for x in glob.globfilter(
                sublime.find_resources('*'), '**/*.@(sublime-syntax|tmLanguage)',
                flags=GLOB_FLAGS | glob.I
            )
        ]

        if self.syntaxes:
            self.window.show_quick_panel(self.syntaxes, self.check_selection)

    def check_selection(self, value):
        """Check the user's selection."""

        if value > -1:
            syntax = self.syntaxes[value]
            sublime.set_clipboard(syntax)
            self.window.status_message('Copied syntax: {}'.format(syntax))


class ApplySyntaxCurrentCommand(sublime_plugin.TextCommand):
    """Browse syntaxes."""

    def run(self, edit):
        """Run command."""

        syntax = self.view.settings().get('syntax')
        if syntax:
            syntax = os.path.splitext(syntax)[0].replace('Packages/', '', 1)
            sublime.set_clipboard(syntax)
            self.view.window().status_message('Copied syntax: {}'.format(syntax))


class ApplySyntaxCommand(sublime_plugin.EventListener):
    """ApplySyntax command."""

    def __init__(self):
        """Initialization."""

        global on_touched_callback
        self.first_line = None
        self.file_name = None
        self.orig_file_name = None
        self.norm_file_name = None
        self.entire_file = None
        self.view = None
        self.syntaxes = []
        self.reraise_exceptions = False
        self.seen_deprecation_warnings = set()
        self.is_win = sublime.platform() == "windows"

        on_touched_callback = self.on_touched

    def print_deprecation_warning(self, keyword, rule=None):
        """Print the deprecation warnings."""

        if rule is not None:
            warn_key = "%s,%s" % (keyword, rule)
        else:
            warn_key = keyword

        if warn_key not in self.seen_deprecation_warnings:
            self.seen_deprecation_warnings.add(warn_key)
            if rule is None:
                log(
                    "Warning: '%s' keyword is deprecated and will be removed in the future."
                    % keyword
                )
            else:
                log(
                    "Warning: '%s' keyword in '%s' rule is deprecated and will be removed in the future."
                    % (keyword, rule)
                )

    def touch(self, view):
        """Touch the view."""

        view.settings().set("apply_syntax_touched", True)

    def update_extenstions(self):
        """Only update extensions if desired."""

        if not SETTINGS.get("add_exts_to_lang_settings", False):
            devlog("Skipping Extension Update")
            return

        global LANG_HASH
        hsh, lst = get_lang_hash()
        if LANG_HASH != hsh:
            LANG_HASH = hsh
            update_extenstions(lst)

    def get_setting(self, name, default=None):
        """Get the settings."""

        active_settings = self.view.settings() if self.view else {}
        key_exist = name in active_settings if isinstance(active_settings, dict) else active_settings.has(name)
        return active_settings.get(name) if key_exist else SETTINGS.get(name, default)

    def on_new(self, view):
        """Apply syntax on new file."""
        self.touch(view)
        self.update_extenstions()
        name = self.get_setting("new_file_syntax")
        if name:
            self.view = view
            self.set_syntax(name)

    def on_load(self, view):
        """Apply syntax on file load."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def on_post_save(self, view):
        """Apply syntax on save."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def on_touched(self, view):
        """Apply syntax to untouched views."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def detect_syntax(self, view):
        """Detect the syntax."""

        self.plugins = {}
        if view.is_scratch() or not view.file_name():  # buffer has never been saved
            return

        self.reset_cache_variables(view)
        self.load_syntaxes()

        if not self.syntaxes:
            return

        found = False
        for syntax in self.syntaxes:
            # stop on the first syntax that matches
            if self.syntax_matches(syntax):
                found = True
                self.set_syntax(syntax.get("syntax"))
                break

        # If pattern matches extension trim pattern, try again after trimming the extension
        if not found:
            for ext_trim in self.ext_trim:
                try:
                    match = False
                    pattern = ext_trim.get('file_path')
                    if pattern:
                        match = re.match(pattern, self.norm_file_name) is not None
                    if not match:
                        pattern = ext_trim.get('globmatch')
                        case = glob.C if ext_trim.get('case', False) else glob.I
                        match = glob.globmatch(self.file_name, pattern, flags=GLOB_FLAGS | case)
                except Exception:
                    if self.reraise_exceptions:
                        raise
                    else:
                        match = False

                if match:
                    parts = os.path.splitext(self.orig_file_name)
                    # If there is no extension, then there is nothing to do
                    if parts[1]:
                        self.file_name = parts[0]
                        self.norm_file_name = self.file_name.replace('\\', '/') if self.is_win else self.file_name
                        for syntax in self.syntaxes:
                            if self.syntax_matches(syntax):
                                self.set_syntax(syntax.get("syntax", syntax.get("name")))
                                break
                    break

        self.plugins = {}

    def reset_cache_variables(self, view):
        """Reset variables."""

        self.view = view
        self.file_name = view.file_name()
        self.norm_file_name = self.file_name.replace('\\', '/') if self.is_win else self.file_name
        self.orig_file_name = self.file_name
        self.first_line = None  # We read the first line only when needed
        self.entire_file = None  # We read the contents of the entire file only when needed
        self.syntaxes = []
        self.reraise_exceptions = False

    def fetch_first_line(self):
        """Get the first line."""

        self.first_line = self.view.substr(self.view.line(0))  # load the first line only when needed

    def fetch_entire_file(self):
        """Get the entire file content."""

        self.entire_file = self.view.substr(sublime.Region(0, self.view.size()))  # load file only when needed

    def set_syntax(self, name):
        """
        Set the syntax.

        The default settings file uses / to separate the syntax name parts, but if the user
        is on windows, that might not work right. And if the user happens to be on Mac/Linux but
        is using rules that were written on windows, the same thing will happen. So let's
        be intelligent about this and replace forward slashes and back slashes with `os.path.sep` to get
        a reasonable starting point.
        """

        if not isinstance(name, list):
            names = [name]
        else:
            names = name

        for n in names:
            if not n:
                continue

            path = os.path.dirname(n)
            if not path:
                debug('Syntax file for ' + n + ' does not exist')
                continue

            for ext in ST_LANGUAGES:
                lang_ext = os.path.splitext(n)[1]
                if lang_ext and lang_ext in ST_LANGUAGES:
                    if lang_ext != ext:
                        continue

                name = os.path.splitext(os.path.basename(n))[0]
                file_name = name + ext
                new_syntax = sublime_format_path('/'.join(['Packages', path, file_name]))

                current_syntax = self.view.settings().get('syntax')

                # only set the syntax if it's different
                if new_syntax != current_syntax:
                    # let's make sure it exists first!
                    try:
                        sublime.load_resource(new_syntax)
                        self.view.set_syntax_file(new_syntax)
                        debug('Syntax set to ' + name + ' using ' + new_syntax)
                        return
                    except Exception:
                        debug('Syntax file for ' + name + ' does not exist at ' + new_syntax)
                else:
                    debug('Syntax already set to ' + new_syntax)
                    return

    def create_extension_rule(self, syntaxes):
        """Create a rules for the defined extensions."""

        for syntax in syntaxes:
            if 'extensions' in syntax:
                if 'rules' not in syntax:
                    syntax['rules'] = []
                syntax['rules'].insert(0, {'extensions': syntax['extensions']})
        return syntaxes

    def load_syntaxes(self):
        """Load syntax rules."""

        self.reraise_exceptions = SETTINGS.get("reraise_exceptions")
        # load the default syntaxes
        default_syntaxes = self.create_extension_rule(
            self.get_setting("default_syntaxes", [])
        )
        # load any user-defined syntaxes
        user_syntaxes = self.create_extension_rule(
            self.get_setting("syntaxes", [])
        )
        # load any project-defined syntaxes
        project_syntaxes = self.create_extension_rule(
            self.get_setting("project_syntaxes", [])
        )

        self.syntaxes = project_syntaxes + user_syntaxes + default_syntaxes

        default_ext_trim = self.get_setting("default_ext_trim", [])
        user_ext_trim = self.get_setting("ext_trim", [])
        project_ext_trim = self.get_setting("project_ext_trim", [])

        self.ext_trim = default_ext_trim + user_ext_trim + project_ext_trim

    def syntax_matches(self, syntax):
        """Match syntax rules."""

        rules = syntax.get("rules", [])
        match_all = syntax.get("match") == 'all'

        for rule in rules:
            if 'extensions' in rule:
                # Do not let 'extensions' contribute to 'match_all'
                result = self.extension_matches(rule)
                if result:
                    return True
                else:
                    continue

            if 'globmatch' in rule:
                result = self.glob_matches(rule)
            elif 'function' in rule:
                result = self.function_matches(rule)
            else:
                result = self.regexp_matches(rule)

            if match_all:
                # can return on the first failure since they all
                # have to match
                if not result:
                    return False
            elif result:
                # return on first match. don't return if it doesn't
                # match or else the remaining rules won't be applied
                return True

        if match_all:
            # if we need to match all and we got here, then all of the
            # rules matched
            return True
        else:
            # if we needed to match just one and got here, none of the
            # rules matched
            return False

    def get_function(self, path_to_file):
        """Get the match function."""
        try:
            path_name = sublime_format_path(os.path.join("Packages", path_to_file))
            module_name = os.path.splitext(path_name)[0].replace('Packages/', '', 1).replace('/', '.')
            module = imp.new_module(module_name)
            sys.modules[module_name] = module
            exec(compile(sublime.load_resource(path_name), module_name, 'exec'), sys.modules[module_name].__dict__)
            function = getattr(module, "syntax_test")
        except Exception:
            if self.reraise_exceptions:
                raise
            else:
                function = None

        return function

    def extension_matches(self, rule):
        """Match extension."""

        match = False
        extensions = rule.get('extensions', [])
        file_name = os.path.basename(self.file_name).lower()
        for extension in extensions:
            ext = extension.lower()
            dot_file_match = ext.startswith('.') and ext == file_name
            if dot_file_match or file_name.endswith('.' + ext):
                match = True
                break
        return match

    def function_matches(self, rule):
        """Perform function match."""

        function_rule = rule.get("function")
        source = function_rule.get("source")
        args = function_rule.get("args", None)
        if source in self.plugins:
            function = self.plugins[source]
        else:
            if not source or source.lower().endswith(".py"):
                # Bad format
                return False

            path_to_file = source.replace('.', '/') + '.py'
            function = self.get_function(path_to_file)

            if function is None:
                # can't find it ... nothing more to do
                return False
            else:
                self.plugins[source] = function

        try:
            return function(self.file_name, **args) if args is not None else function(self.file_name)
        except Exception:
            if self.reraise_exceptions:
                raise
            else:
                return False

    def glob_matches(self, rule):
        """Perform glob matches."""

        result = False
        pattern = rule.get('globmatch')
        if pattern:
            case = glob.C if rule.get('case', False) else glob.I
            result = glob.globmatch(self.file_name, pattern, flags=GLOB_FLAGS | case)
        return result

    def regexp_matches(self, rule):
        """Perform regex matches."""

        from_beginning = True  # match only from the beginning or anywhere in the string

        if "first_line" in rule:
            if self.first_line is None:
                self.fetch_first_line()
            subject = self.first_line
            regexp = rule.get("first_line")
        elif "interpreter" in rule:
            if self.first_line is None:
                self.fetch_first_line()
            subject = self.first_line
            regexp = '^#\\!(?:.+)' + rule.get("interpreter")
        elif "file_path" in rule:
            subject = self.norm_file_name
            regexp = rule.get("file_path")
        elif "contains" in rule:
            if self.entire_file is None:
                self.fetch_entire_file()
            subject = self.entire_file
            regexp = rule.get("contains")
            from_beginning = False  # requires us to match anywhere in the file
        else:
            return False

        if regexp and subject:
            if from_beginning:
                result = re.match(regexp, subject)
            else:
                result = re.search(regexp, subject)  # matches anywhere, not only from the beginning
            return result is not None
        else:
            return False


def touch_untouched():
    """Touch the untouched views."""

    for window in sublime.windows():
        for view in window.views():
            if not view.settings().get("apply_syntax_touched", False):
                if on_touched_callback is not None:
                    on_touched_callback(view)
                else:
                    devlog("EventListener not loaded yet")


def on_reload():
    """
    Only update extensions if desired.

    Remove extensions added by ApplySyntax if disabled.
    """

    if not SETTINGS.get("add_exts_to_lang_settings", False):
        ext_added = dict(
            [(os.path.splitext(os.path.basename(l))[0], set()) for l in get_lang_hash()[1]]
        )
        ext_map = {}
        devlog("Skipping Extension Update")
        prune_language_extensions(ext_map, ext_added)
    else:
        global LANG_HASH
        LANG_HASH, lang_list = get_lang_hash()
        update_extenstions(lang_list)


def plugin_loaded():
    """Setup plugin."""

    global SETTINGS
    ensure_user_settings()
    SETTINGS = sublime.load_settings(PLUGIN_SETTINGS)
    SETTINGS.clear_on_change('reload')
    SETTINGS.add_on_change('reload', on_reload)
    on_reload()
    touch_untouched()

"""Apply Syntax."""
import sublime
import sublime_plugin
import os
import re
import imp
import sys
from wcmatch import glob

GLOB_FLAGS = glob.G | glob.B | glob.E | glob.N | glob.D

DEFAULT_SETTINGS = '''
{
    // If you want exceptions reraised so you can see them in the console, change this to true.
    "reraise_exceptions": false,

    // If you want to have a syntax applied when new files are created, set new_file_syntax to the name of the syntax
    // to use.  The format is exactly the same as "syntax" in the rules below. For example, if you want to have a new
    // file use JavaScript syntax, set new_file_syntax to 'JavaScript'.
    "new_file_syntax": false,

    // Auto add extensions to language settings file in User folder.
    // Do not manually remove "apply_syntax_extensions" from the settings file.
    // "extenstions" are ignored by "match": "all" setting.
    "add_exts_to_lang_settings": true,

    // Control level of logging in the console.
    // (true|false|"verbose")
    "debug": true,

    // Put your custom syntax rules here:
    "syntaxes": [
    ]
}
'''

USE_ST_SYNTAX = int(sublime.version()) >= 3084
PLUGIN_NAME = 'ApplySyntax'
PLUGIN_DIR = "Packages/%s" % PLUGIN_NAME
PLUGIN_SETTINGS = PLUGIN_NAME + '.sublime-settings'
EXT_SETTINGS = PLUGIN_NAME + ".ext-list"
ST_LANGUAGES = ('.sublime-syntax', '.tmLanguage') if USE_ST_SYNTAX else ('.tmLanguage',)
SETTINGS = {}
LANG_HASH = 0

# Call back for whether view(s) have been touched
on_touched_callback = None


def ensure_user_settings():
    """Create a default 'User' settings file for ApplySyntax if it doesn't exist."""

    user_settings_file = os.path.join(sublime.packages_path(), 'User', PLUGIN_SETTINGS)
    if os.path.exists(user_settings_file):
        return

    # file doesn't exist, let's create a bare one
    with open(user_settings_file, 'w') as f:
        f.write(DEFAULT_SETTINGS)


def get_all_syntax_files():
    """Find all `sublime-syntax` and `tmLanguage` files."""

    syntax_files = []
    if USE_ST_SYNTAX:
        syntax_files += sublime.find_resources("*.sublime-syntax")
    syntax_files += sublime.find_resources("*.tmLanguage")
    return syntax_files


def sublime_format_path(pth):
    """Format the path for the sublime API."""

    m = re.match(r"^([A-Za-z]{1}):(?:/|\\)(.*)", pth)
    if sublime.platform() == "windows" and m is not None:
        pth = m.group(1) + "/" + m.group(2)
    return pth.replace("\\", "/")


def get_lang_hash():
    """
    Return the hash of the loaded languages in Sublime.

    Return the actual language `frozenset` (hashable) as well.
    """

    def was_seen(item, item_set):
        """Check if item was seen already."""

        seen_item = True
        base = os.path.splitext(item)[0]
        if base is not item_set:
            item_set.add(base)
            seen_item = False
        return seen_item

    # Strip off `tmlanguage` so we don't have to worry about case of extension
    seen = set()
    lst = frozenset(
        [os.path.splitext(x)[0] for x in get_all_syntax_files() if not was_seen(x, seen)]
    )
    hsh = hash(lst)
    devlog("Language Hash - '%s'" % str(hsh))
    return hsh, lst


def update_language_extensions(ext_added):
    """Process the extensions for each language."""

    for lang, exts in ext_added.items():
        updated = False
        settings_file = lang + ".sublime-settings"
        lang_settings = sublime.load_settings(settings_file)
        extension_file = sublime.load_settings(EXT_SETTINGS)
        lang_ext = set(lang_settings.get("extensions", []))
        apsy_ext = set(extension_file.get(lang, []))

        for ext in list(exts):
            if ext not in lang_ext:
                # Append extension to current sublime extension list
                lang_ext.add(ext)
                # Track which extensions were specifically added by apply syntax
                apsy_ext.add(ext)
                updated = True

        if updated:
            devlog("============" + settings_file + "============")
            devlog("Updated Extensions: %s" % str(lang_ext))
            lang_settings.set("extensions", list(lang_ext))
            if len(apsy_ext):
                extension_file.set(lang, sorted(list(apsy_ext)))
            else:
                extension_file.erase(sorted(lang))
            sublime.save_settings(settings_file)
            sublime.save_settings(EXT_SETTINGS)


def map_extensions(ext, lst, names, ext_map, ext_added):
    """Create mappings to help with updating and pruning extensions."""

    # Always deal with language names as a series of names
    if not isinstance(names, list):
        names = [names]

    # For each language name that currently exists and is loaded,
    # append the extensions to the corresponding settings file
    # if the extension is not already there

    for n in names:
        # updated = False
        path = os.path.dirname(n)
        name = os.path.basename(n)
        syntax_file = sublime_format_path('/'.join(['Packages', path, name]))
        if syntax_file in lst:
            for e in ext:
                if e in ext_map:
                    ext_map[e].append(name)
                else:
                    ext_map[e] = [name]

        for ext, languages in ext_map.items():
            lang = languages[-1]
            ext_map[ext] = [lang]
            ext_added[lang].add(ext)


def prune_language_extensions(ext_map, ext_added):
    """
    Prune dead extensions that were added by ApplySyntax (AS), but are no longer defined in AS.

    `exts`        - Sublime's extension list for the given language
    `old_ext`     - The current saved list of AS added extension
    `new_ext`     - The current tracked list of extensions found defined by AS
    `bad_ext`     - (`old_ext` - `new_ext`) Extensions that were added by AS but are no longer defined
    `updated_ext` - (`ext` - `bad_ext`) Sublime's adjusted extension list minus the obsolete ones
    """

    devlog("Pruning Extensions")
    for name, new_ext in ext_added.items():
        updated = False
        settings_file = name + '.sublime-settings'
        lang_settings = sublime.load_settings(settings_file)
        extension_file = sublime.load_settings(EXT_SETTINGS)
        exts = set(lang_settings.get("extensions", []))
        old_ext = set(extension_file.get(name, []))

        # Calculate the correct extension list
        bad_ext = old_ext.difference(new_ext)
        updated_ext = exts.difference(bad_ext)

        # Remove extension from file it ApplySyntax added
        # it to a different file
        for ext in list(updated_ext):
            try:
                if ext_map[ext][-1] != name:
                    updated_ext.remove(ext)
            except Exception:
                pass

        # Update settings file if necessary
        if len(updated_ext) != len(exts):
            # Update pruned list
            lang_settings.set("extensions", sorted(list(updated_ext)))
            if len(new_ext) == 0:
                # No currently added extensions by AS
                extension_file.erase(name)
            else:
                # Updated with relevant AS extensions
                extension_file.set(name, sorted(list(new_ext)))
            updated = True

        if updated:
            devlog("============" + settings_file + "============")
            devlog("Pruned Extensions: %s" % str(bad_ext))
            sublime.save_settings(settings_file)
            sublime.save_settings(EXT_SETTINGS)


def update_extenstions(lst):
    """Walk through all syntax rules updating the extensions in the corresponding language settings."""

    devlog("Updating Extensions")

    # Prepare a list to track extensions defined by ApplySyntax
    ext_added = dict([(os.path.splitext(os.path.basename(l))[0], set()) for l in lst])
    ext_map = {}

    # Walk the entries
    for entry in SETTINGS.get("syntaxes", []) + SETTINGS.get("default_syntaxes", []):
        # Grab the extensions from each relevant rule
        ext = []
        if "extensions" in entry:
            ext += entry.get("extensions", [])

        # Add the extensions to the relevant language settings file
        if len(ext):
            syntax = entry.get("syntax", entry.get("name"))
            if isinstance(syntax, list):
                for s in syntax:
                    name = os.path.splitext(s)[0]
                    devlog("Found Extensions: %s - %s" % (name, str(ext)))
                    map_extensions(ext, lst, name, ext_map, ext_added)
            else:
                name = os.path.splitext(syntax)[0]
                devlog("Found Extensions: %s - %s" % (name, str(ext)))
                map_extensions(ext, lst, name, ext_map, ext_added)

    update_language_extensions(ext_added)

    prune_language_extensions(ext_map, ext_added)


def log(msg):
    """Print the ApplySyntax log message in console."""

    print("ApplySyntax: %s" % msg)


def debug(msg):
    """Print the debug ApplySyntax log message in console (debug mode only)."""

    if SETTINGS.get("debug", True) in (True, 'verbose'):
        log(msg)


def devlog(msg):
    """Print the developer mode ApplySyntax log message in console (developer mode only)."""

    if SETTINGS.get("debug", True) == 'verbose':
        log(msg)


class ApplySyntaxBrowseCommand(sublime_plugin.WindowCommand):
    """Browse syntaxes."""

    def run(self):
        """Run command."""

        self.syntaxes = [
            os.path.splitext(x)[0].replace('Packages/', '', 1)
            for x in glob.globfilter(
                sublime.find_resources('*'), '**/*.@(sublime-syntax|tmLanguage)',
                flags=GLOB_FLAGS | glob.I
            )
        ]

        if self.syntaxes:
            self.window.show_quick_panel(self.syntaxes, self.check_selection)

    def check_selection(self, value):
        """Check the user's selection."""

        if value > -1:
            syntax = self.syntaxes[value]
            sublime.set_clipboard(syntax)
            self.window.status_message('Copied syntax: {}'.format(syntax))


class ApplySyntaxCurrentCommand(sublime_plugin.TextCommand):
    """Browse syntaxes."""

    def run(self, edit):
        """Run command."""

        syntax = self.view.settings().get('syntax')
        if syntax:
            syntax = os.path.splitext(syntax)[0].replace('Packages/', '', 1)
            sublime.set_clipboard(syntax)
            self.view.window().status_message('Copied syntax: {}'.format(syntax))


class ApplySyntaxCommand(sublime_plugin.EventListener):
    """ApplySyntax command."""

    def __init__(self):
        """Initialization."""

        global on_touched_callback
        self.first_line = None
        self.file_name = None
        self.orig_file_name = None
        self.norm_file_name = None
        self.entire_file = None
        self.view = None
        self.syntaxes = []
        self.reraise_exceptions = False
        self.seen_deprecation_warnings = set()
        self.is_win = sublime.platform() == "windows"

        on_touched_callback = self.on_touched

    def print_deprecation_warning(self, keyword, rule=None):
        """Print the deprecation warnings."""

        if rule is not None:
            warn_key = "%s,%s" % (keyword, rule)
        else:
            warn_key = keyword

        if warn_key not in self.seen_deprecation_warnings:
            self.seen_deprecation_warnings.add(warn_key)
            if rule is None:
                log(
                    "Warning: '%s' keyword is deprecated and will be removed in the future."
                    % keyword
                )
            else:
                log(
                    "Warning: '%s' keyword in '%s' rule is deprecated and will be removed in the future."
                    % (keyword, rule)
                )

    def touch(self, view):
        """Touch the view."""

        view.settings().set("apply_syntax_touched", True)

    def update_extenstions(self):
        """Only update extensions if desired."""

        if not SETTINGS.get("add_exts_to_lang_settings", False):
            devlog("Skipping Extension Update")
            return

        global LANG_HASH
        hsh, lst = get_lang_hash()
        if LANG_HASH != hsh:
            LANG_HASH = hsh
            update_extenstions(lst)

    def get_setting(self, name, default=None):
        """Get the settings."""

        active_settings = self.view.settings() if self.view else {}
        key_exist = name in active_settings if isinstance(active_settings, dict) else active_settings.has(name)
        return active_settings.get(name) if key_exist else SETTINGS.get(name, default)

    def on_new(self, view):
        """Apply syntax on new file."""
        self.touch(view)
        self.update_extenstions()
        name = self.get_setting("new_file_syntax")
        if name:
            self.view = view
            self.set_syntax(name)

    def on_load(self, view):
        """Apply syntax on file load."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def on_post_save(self, view):
        """Apply syntax on save."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def on_touched(self, view):
        """Apply syntax to untouched views."""

        self.touch(view)
        self.update_extenstions()
        self.detect_syntax(view)

    def detect_syntax(self, view):
        """Detect the syntax."""

        self.plugins = {}
        if view.is_scratch() or not view.file_name():  # buffer has never been saved
            return

        self.reset_cache_variables(view)
        self.load_syntaxes()

        if not self.syntaxes:
            return

        found = False
        for syntax in self.syntaxes:
            # stop on the first syntax that matches
            if self.syntax_matches(syntax):
                found = True
                self.set_syntax(syntax.get("syntax"))
                break

        # If pattern matches extension trim pattern, try again after trimming the extension
        if not found:
            for ext_trim in self.ext_trim:
                try:
                    match = False
                    pattern = ext_trim.get('file_path')
                    if pattern:
                        match = re.match(pattern, self.norm_file_name) is not None
                    if not match:
                        pattern = ext_trim.get('globmatch')
                        case = glob.C if ext_trim.get('case', False) else glob.I
                        match = glob.globmatch(self.file_name, pattern, flags=GLOB_FLAGS | case)
                except Exception:
                    if self.reraise_exceptions:
                        raise
                    else:
                        match = False

                if match:
                    parts = os.path.splitext(self.orig_file_name)
                    # If there is no extension, then there is nothing to do
                    if parts[1]:
                        self.file_name = parts[0]
                        self.norm_file_name = self.file_name.replace('\\', '/') if self.is_win else self.file_name
                        for syntax in self.syntaxes:
                            if self.syntax_matches(syntax):
                                self.set_syntax(syntax.get("syntax", syntax.get("name")))
                                break
                    break

        self.plugins = {}

    def reset_cache_variables(self, view):
        """Reset variables."""

        self.view = view
        self.file_name = view.file_name()
        self.norm_file_name = self.file_name.replace('\\', '/') if self.is_win else self.file_name
        self.orig_file_name = self.file_name
        self.first_line = None  # We read the first line only when needed
        self.entire_file = None  # We read the contents of the entire file only when needed
        self.syntaxes = []
        self.reraise_exceptions = False

    def fetch_first_line(self):
        """Get the first line."""

        self.first_line = self.view.substr(self.view.line(0))  # load the first line only when needed

    def fetch_entire_file(self):
        """Get the entire file content."""

        self.entire_file = self.view.substr(sublime.Region(0, self.view.size()))  # load file only when needed

    def set_syntax(self, name):
        """
        Set the syntax.

        The default settings file uses / to separate the syntax name parts, but if the user
        is on windows, that might not work right. And if the user happens to be on Mac/Linux but
        is using rules that were written on windows, the same thing will happen. So let's
        be intelligent about this and replace forward slashes and back slashes with `os.path.sep` to get
        a reasonable starting point.
        """

        if not isinstance(name, list):
            names = [name]
        else:
            names = name

        for n in names:
            if not n:
                continue

            path = os.path.dirname(n)
            if not path:
                debug('Syntax file for ' + n + ' does not exist')
                continue

            for ext in ST_LANGUAGES:
                lang_ext = os.path.splitext(n)[1]
                if lang_ext and lang_ext in ST_LANGUAGES:
                    if lang_ext != ext:
                        continue

                name = os.path.splitext(os.path.basename(n))[0]
                file_name = name + ext
                new_syntax = sublime_format_path('/'.join(['Packages', path, file_name]))

                current_syntax = self.view.settings().get('syntax')

                # only set the syntax if it's different
                if new_syntax != current_syntax:
                    # let's make sure it exists first!
                    try:
                        sublime.load_resource(new_syntax)
                        self.view.set_syntax_file(new_syntax)
                        debug('Syntax set to ' + name + ' using ' + new_syntax)
                        return
                    except Exception:
                        debug('Syntax file for ' + name + ' does not exist at ' + new_syntax)
                else:
                    debug('Syntax already set to ' + new_syntax)
                    return

    def create_extension_rule(self, syntaxes):
        """Create a rules for the defined extensions."""

        for syntax in syntaxes:
            if 'extensions' in syntax:
                if 'rules' not in syntax:
                    syntax['rules'] = []
                syntax['rules'].insert(0, {'extensions': syntax['extensions']})
        return syntaxes

    def load_syntaxes(self):
        """Load syntax rules."""

        self.reraise_exceptions = SETTINGS.get("reraise_exceptions")
        # load the default syntaxes
        default_syntaxes = self.create_extension_rule(
            self.get_setting("default_syntaxes", [])
        )
        # load any user-defined syntaxes
        user_syntaxes = self.create_extension_rule(
            self.get_setting("syntaxes", [])
        )
        # load any project-defined syntaxes
        project_syntaxes = self.create_extension_rule(
            self.get_setting("project_syntaxes", [])
        )

        self.syntaxes = project_syntaxes + user_syntaxes + default_syntaxes

        default_ext_trim = self.get_setting("default_ext_trim", [])
        user_ext_trim = self.get_setting("ext_trim", [])
        project_ext_trim = self.get_setting("project_ext_trim", [])

        self.ext_trim = default_ext_trim + user_ext_trim + project_ext_trim

    def syntax_matches(self, syntax):
        """Match syntax rules."""

        rules = syntax.get("rules", [])
        match_all = syntax.get("match") == 'all'

        for rule in rules:
            if 'extensions' in rule:
                # Do not let 'extensions' contribute to 'match_all'
                result = self.extension_matches(rule)
                if result:
                    return True
                else:
                    continue

            if 'globmatch' in rule:
                result = self.glob_matches(rule)
            elif 'function' in rule:
                result = self.function_matches(rule)
            else:
                result = self.regexp_matches(rule)

            if match_all:
                # can return on the first failure since they all
                # have to match
                if not result:
                    return False
            elif result:
                # return on first match. don't return if it doesn't
                # match or else the remaining rules won't be applied
                return True

        if match_all:
            # if we need to match all and we got here, then all of the
            # rules matched
            return True
        else:
            # if we needed to match just one and got here, none of the
            # rules matched
            return False

    def get_function(self, path_to_file):
        """Get the match function."""
        try:
            path_name = sublime_format_path(os.path.join("Packages", path_to_file))
            module_name = os.path.splitext(path_name)[0].replace('Packages/', '', 1).replace('/', '.')
            module = imp.new_module(module_name)
            sys.modules[module_name] = module
            exec(compile(sublime.load_resource(path_name), module_name, 'exec'), sys.modules[module_name].__dict__)
            function = getattr(module, "syntax_test")
        except Exception:
            if self.reraise_exceptions:
                raise
            else:
                function = None

        return function

    def extension_matches(self, rule):
        """Match extension."""

        match = False
        extensions = rule.get('extensions', [])
        file_name = os.path.basename(self.file_name).lower()
        for extension in extensions:
            ext = extension.lower()
            dot_file_match = ext.startswith('.') and ext == file_name
            if dot_file_match or file_name.endswith('.' + ext):
                match = True
                break
        return match

    def function_matches(self, rule):
        """Perform function match."""

        function_rule = rule.get("function")
        source = function_rule.get("source")
        args = function_rule.get("args", None)
        if source in self.plugins:
            function = self.plugins[source]
        else:
            if not source or source.lower().endswith(".py"):
                # Bad format
                return False

            path_to_file = source.replace('.', '/') + '.py'
            function = self.get_function(path_to_file)

            if function is None:
                # can't find it ... nothing more to do
                return False
            else:
                self.plugins[source] = function

        try:
            return function(self.file_name, **args) if args is not None else function(self.file_name)
        except Exception:
            if self.reraise_exceptions:
                raise
            else:
                return False

    def glob_matches(self, rule):
        """Perform glob matches."""

        result = False
        pattern = rule.get('globmatch')
        if pattern:
            case = glob.C if rule.get('case', False) else glob.I
            result = glob.globmatch(self.file_name, pattern, flags=GLOB_FLAGS | case)
        return result

    def regexp_matches(self, rule):
        """Perform regex matches."""

        from_beginning = True  # match only from the beginning or anywhere in the string

        if "first_line" in rule:
            if self.first_line is None:
                self.fetch_first_line()
            subject = self.first_line
            regexp = rule.get("first_line")
        elif "interpreter" in rule:
            if self.first_line is None:
                self.fetch_first_line()
            subject = self.first_line
            regexp = '^#\\!(?:.+)' + rule.get("interpreter")
        elif "file_path" in rule:
            subject = self.norm_file_name
            regexp = rule.get("file_path")
        elif "contains" in rule:
            if self.entire_file is None:
                self.fetch_entire_file()
            subject = self.entire_file
            regexp = rule.get("contains")
            from_beginning = False  # requires us to match anywhere in the file
        else:
            return False

        if regexp and subject:
            if from_beginning:
                result = re.match(regexp, subject)
            else:
                result = re.search(regexp, subject)  # matches anywhere, not only from the beginning
            return result is not None
        else:
            return False


def touch_untouched():
    """Touch the untouched views."""

    for window in sublime.windows():
        for view in window.views():
            if not view.settings().get("apply_syntax_touched", False):
                if on_touched_callback is not None:
                    on_touched_callback(view)
                else:
                    devlog("EventListener not loaded yet")


def on_reload():
    """
    Only update extensions if desired.

    Remove extensions added by ApplySyntax if disabled.
    """

    if not SETTINGS.get("add_exts_to_lang_settings", False):
        ext_added = dict(
            [(os.path.splitext(os.path.basename(l))[0], set()) for l in get_lang_hash()[1]]
        )
        ext_map = {}
        devlog("Skipping Extension Update")
        prune_language_extensions(ext_map, ext_added)
    else:
        global LANG_HASH
        LANG_HASH, lang_list = get_lang_hash()
        update_extenstions(lang_list)


def plugin_loaded():
    """Setup plugin."""

    global SETTINGS
    ensure_user_settings()
    SETTINGS = sublime.load_settings(PLUGIN_SETTINGS)
    SETTINGS.clear_on_change('reload')
    SETTINGS.add_on_change('reload', on_reload)
    on_reload()
    touch_untouched()
Apply Syntax. {     // If you want exceptions reraised so you can see them in the console, change this to true.     "reraise_exceptions": false,      // If you want to have a syntax applied when new files are created, set new_file_syntax to the name of the syntax     // to use.  The format is exactly the same as "syntax" in the rules below. For example, if you want to have a new     // file use JavaScript syntax, set new_file_syntax to 'JavaScript'.     "new_file_syntax": false,      // Auto add extensions to language settings file in User folder.     // Do not manually remove "apply_syntax_extensions" from the settings file.     // "extenstions" are ignored by "match": "all" setting.     "add_exts_to_lang_settings": true,      // Control level of logging in the console.     // (true|false|"verbose")     "debug": true,      // Put your custom syntax rules here:     "syntaxes": [     ] } # Call back for whether view(s) have been touched Create a default 'User' settings file for ApplySyntax if it doesn't exist. # file doesn't exist, let's create a bare one Find all `sublime-syntax` and `tmLanguage` files. Format the path for the sublime API. Return the hash of the loaded languages in Sublime.      Return the actual language `frozenset` (hashable) as well. Check if item was seen already. # Strip off `tmlanguage` so we don't have to worry about case of extension Process the extensions for each language. # Append extension to current sublime extension list # Track which extensions were specifically added by apply syntax Create mappings to help with updating and pruning extensions. # Always deal with language names as a series of names # For each language name that currently exists and is loaded, # append the extensions to the corresponding settings file # if the extension is not already there # updated = False Prune dead extensions that were added by ApplySyntax (AS), but are no longer defined in AS.      `exts`        - Sublime's extension list for the given language     `old_ext`     - The current saved list of AS added extension     `new_ext`     - The current tracked list of extensions found defined by AS     `bad_ext`     - (`old_ext` - `new_ext`) Extensions that were added by AS but are no longer defined     `updated_ext` - (`ext` - `bad_ext`) Sublime's adjusted extension list minus the obsolete ones # Calculate the correct extension list # Remove extension from file it ApplySyntax added # it to a different file # Update settings file if necessary # Update pruned list # No currently added extensions by AS # Updated with relevant AS extensions Walk through all syntax rules updating the extensions in the corresponding language settings. # Prepare a list to track extensions defined by ApplySyntax # Walk the entries # Grab the extensions from each relevant rule # Add the extensions to the relevant language settings file Print the ApplySyntax log message in console. Print the debug ApplySyntax log message in console (debug mode only). Print the developer mode ApplySyntax log message in console (developer mode only). Browse syntaxes. Run command. Check the user's selection. Browse syntaxes. Run command. ApplySyntax command. Initialization. Print the deprecation warnings. Touch the view. Only update extensions if desired. Get the settings. Apply syntax on new file. Apply syntax on file load. Apply syntax on save. Apply syntax to untouched views. Detect the syntax. # buffer has never been saved # stop on the first syntax that matches # If pattern matches extension trim pattern, try again after trimming the extension # If there is no extension, then there is nothing to do Reset variables. # We read the first line only when needed # We read the contents of the entire file only when needed Get the first line. # load the first line only when needed Get the entire file content. # load file only when needed Set the syntax.          The default settings file uses / to separate the syntax name parts, but if the user         is on windows, that might not work right. And if the user happens to be on Mac/Linux but         is using rules that were written on windows, the same thing will happen. So let's         be intelligent about this and replace forward slashes and back slashes with `os.path.sep` to get         a reasonable starting point. # only set the syntax if it's different # let's make sure it exists first! Create a rules for the defined extensions. Load syntax rules. # load the default syntaxes # load any user-defined syntaxes # load any project-defined syntaxes Match syntax rules. # Do not let 'extensions' contribute to 'match_all' # can return on the first failure since they all # have to match # return on first match. don't return if it doesn't # match or else the remaining rules won't be applied # if we need to match all and we got here, then all of the # rules matched # if we needed to match just one and got here, none of the # rules matched Get the match function. Match extension. Perform function match. # Bad format # can't find it ... nothing more to do Perform glob matches. Perform regex matches. # match only from the beginning or anywhere in the string #\\!(?:.+)' + rule.get("interpreter") # requires us to match anywhere in the file # matches anywhere, not only from the beginning Touch the untouched views. Only update extensions if desired.      Remove extensions added by ApplySyntax if disabled. Setup plugin.