# Klammertext.py
#
# Sublime Text plugin for klammer APPLICATION (@) delimiters.  Two features,
# both counterparts of doc/edit/emacs/klammertext-mode.el, both reusing the
# one context-sensitive matcher in the shared core:
#
#   1. Jump between an opening and its close — the Sublime equivalent of the
#      Emacs mode's `klammertext-jump-to-match' (bound C-c C-j).  Command name
#      klammertext_jump_to_match; keybinding in Default.sublime-keymap.
#
#   2. Live highlighting of the matching delimiter as the caret sits on one —
#      the equivalent of the Emacs mode's show-paren support.  Implemented as a
#      ViewEventListener (see KlammertextMatchHighlighter at the bottom).  A
#      mismatched named close or an unbalanced delimiter is highlighted in red
#      with a status-bar message, mirroring the Emacs mode's
#      klammertext-mismatch-face + minibuffer report.
#
# This is the companion to Klammertext.sublime-syntax.  The syntax file only
# colors tokens; a tokenizer cannot match context-dependent delimiters, so the
# jump is implemented here as a TextCommand.  The keybinding lives in the
# companion Default.sublime-keymap.
#
# The matcher itself — on-or-just-after caret rule, literal klammers matched
# BY NAME with opaque content (@code <-> code@), everything else by depth,
# @@/@@@ runs and removed text stepped over — lives in the shared core,
# doc/edit/shared/klammertext_edit.py, together with the LITERAL_KLAMMERS
# policy list.  This file is only the Sublime wrapper.
#
# The shared core is located next to this file (a vendored copy — the
# installed-package layout produced by doc/make_editing_zip.sh), or in
# ../shared (the repository layout), or under $KLAMMERTEXT_HOME.
#
# SYNC: the literal-klammer set in the shared core must agree with the @code
# rule + literal_code context in Klammertext.sublime-syntax (a static syntax
# file cannot read Python; both are seeded with just 'code').

import os
import sys

try:
    import sublime
    import sublime_plugin
    _IN_SUBLIME = True
except ImportError:          # standalone import outside Sublime Text
    _IN_SUBLIME = False


def _import_shared():
    here = os.path.dirname(os.path.abspath(__file__))
    candidates = [here, os.path.join(os.path.dirname(here), 'shared')]
    kh = os.environ.get('KLAMMERTEXT_HOME')
    if kh:
        candidates.append(os.path.join(kh, 'doc', 'edit', 'shared'))
    for d in candidates:
        if os.path.isfile(os.path.join(d, 'klammertext_edit.py')):
            if d not in sys.path:
                sys.path.insert(0, d)
            break
    import klammertext_edit
    return klammertext_edit


KE = _import_shared()


if _IN_SUBLIME:

    class KlammertextJumpToMatchCommand(sublime_plugin.TextCommand):
        """Jump between a klammer application's opening and closing delimiter.
        Sublime equivalent of the Emacs mode's C-c C-j."""

        def run(self, edit):
            view = self.view
            s = view.substr(sublime.Region(0, view.size()))
            new_regions = []
            moved = False
            message = None

            for region in view.sel():
                m = KE.match_at(s, region.b)
                if m is None:
                    new_regions.append(region)
                    message = ("point is not on a klammer application "
                               "delimiter (@)")
                    continue
                if m['match'] is None:
                    new_regions.append(region)
                    message = ("no matching delimiter for this %s klammer"
                               % ("opening" if m['kind'] == 'open'
                                  else "closing"))
                    continue
                new_regions.append(sublime.Region(m['match'], m['match']))
                moved = True

            view.sel().clear()
            for r in new_regions:
                view.sel().add(r)

            if moved:
                view.show(view.sel()[0].b)
            elif message:
                sublime.status_message("Klammertext: " + message)

        def is_enabled(self):
            # Only meaningful in Klammertext buffers.
            return self.view.match_selector(0, "text.klammertext")

    # --- live matched-delimiter highlighting (show-paren equivalent) -------

    class KlammertextMatchHighlighter(sublime_plugin.ViewEventListener):
        """Highlight the matching klammer application delimiter as the caret
        sits on one.  The Sublime equivalent of the Emacs mode's show-paren
        support — driven by cursor movement, reusing the shared matcher.

        A matched pair is boxed (region.bluish); a mismatch or unbalanced
        delimiter is boxed in red (region.redish) with a status-bar message.
        Both the token under the caret and its match are boxed; the Emacs mode
        highlights only the single @ character, but boxing the whole
        @name / name@ reads better here.  To highlight only the far delimiter,
        drop the first region in _update()."""

        MATCH_KEY = 'klammertext_paren_match'
        MISMATCH_KEY = 'klammertext_paren_mismatch'

        @classmethod
        def is_applicable(cls, settings):
            return str(settings.get('syntax', '')).endswith(
                'Klammertext.sublime-syntax')

        def __init__(self, view):
            super().__init__(view)
            self._change_count = -1
            self._text = ''

        def _buffer(self):
            # Re-read the buffer only when it has actually changed, so plain
            # cursor movement over a large file does not re-copy the document.
            cc = self.view.change_count()
            if cc != self._change_count:
                self._text = self.view.substr(
                    sublime.Region(0, self.view.size()))
                self._change_count = cc
            return self._text

        def on_selection_modified_async(self):
            self._update()

        def on_activated_async(self):
            self._update()

        def _clear(self):
            self.view.erase_regions(self.MATCH_KEY)
            self.view.erase_regions(self.MISMATCH_KEY)

        def _update(self):
            view = self.view
            sel = view.sel()
            if len(sel) == 0:
                self._clear()
                return
            s = self._buffer()
            m = KE.match_at(s, sel[0].b)
            if m is None:
                self._clear()
                return

            regions = [sublime.Region(*m['token'])]
            if m['match_token'] is not None:
                regions.append(sublime.Region(*m['match_token']))

            flags = sublime.DRAW_NO_FILL
            if m['mismatch']:
                view.erase_regions(self.MATCH_KEY)
                view.add_regions(self.MISMATCH_KEY, regions,
                                 'region.redish', '', flags)
                if m['message']:
                    sublime.status_message("Klammertext: " + m['message'])
            else:
                view.erase_regions(self.MISMATCH_KEY)
                view.add_regions(self.MATCH_KEY, regions,
                                 'region.bluish', '', flags)
