# 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)