]> git.cworth.org Git - notmuch/blobdiff - doc/elisp.py
doc: copy elisp.py from flycheck, make available
[notmuch] / doc / elisp.py
diff --git a/doc/elisp.py b/doc/elisp.py
new file mode 100644 (file)
index 0000000..1a96b12
--- /dev/null
@@ -0,0 +1,433 @@
+# Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors
+
+# This file is not part of GNU Emacs.
+
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+# details.
+
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from collections import namedtuple
+from sphinx import addnodes
+from sphinx.util import ws_re
+from sphinx.roles import XRefRole
+from sphinx.domains import Domain, ObjType
+from sphinx.util.nodes import make_refnode
+from sphinx.directives import ObjectDescription
+
+
+def make_target(cell, name):
+    """Create a target name from ``cell`` and ``name``.
+
+    ``cell`` is the name of a symbol cell, and ``name`` is a symbol name, both
+    as strings.
+
+    The target names are used as cross-reference targets for Sphinx.
+
+    """
+    return '{cell}-{name}'.format(cell=cell, name=name)
+
+
+def to_mode_name(symbol_name):
+    """Convert ``symbol_name`` to a mode name.
+
+    Split at ``-`` and titlecase each part.
+
+    """
+    return ' '.join(p.title() for p in symbol_name.split('-'))
+
+
+class Cell(namedtuple('Cell', 'objtype docname')):
+    """A cell in a symbol.
+
+    A cell holds the object type and the document name of the description for
+    the cell.
+
+    Cell objects are used within symbol entries in the domain data.
+
+    """
+
+    pass
+
+
+class KeySequence(namedtuple('KeySequence', 'keys')):
+    """A key sequence."""
+
+    PREFIX_KEYS = {'C-u'}
+    PREFIX_KEYS.update('M-{}'.format(n) for n in range(10))
+
+    @classmethod
+    def fromstring(cls, s):
+        return cls(s.split())
+
+    @property
+    def command_name(self):
+        """The command name in this key sequence.
+
+        Return ``None`` for key sequences that are no command invocations with
+        ``M-x``.
+
+        """
+        try:
+            return self.keys[self.keys.index('M-x') + 1]
+        except ValueError:
+            return None
+
+    @property
+    def has_prefix(self):
+        """Whether this key sequence has a prefix."""
+        return self.keys[0] in self.PREFIX_KEYS
+
+    def __str__(self):
+        return ' '.join(self.keys)
+
+
+class EmacsLispSymbol(ObjectDescription):
+    """An abstract base class for directives documenting symbols.
+
+    Provide target and index generation and registration of documented symbols
+    within the domain data.
+
+    Deriving classes must have a ``cell`` attribute which refers to the cell
+    the documentation goes in, and a ``label`` attribute which provides a
+    human-readable name for what is documented, used in the index entry.
+
+    """
+
+    cell_for_objtype = {
+        'defcustom': 'variable',
+        'defconst': 'variable',
+        'defvar': 'variable',
+        'defface': 'face'
+    }
+
+    @property
+    def cell(self):
+        """The cell in which to store symbol metadata."""
+        return self.cell_for_objtype[self.objtype]
+
+    @property
+    def label(self):
+        """The label for the documented object type."""
+        return self.objtype
+
+    def handle_signature(self, signature, signode):
+        """Create nodes in ``signode`` for the ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+
+        Add the object type label before the symbol name and return
+        ``signature``.
+
+        """
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(signature, signature)
+        return signature
+
+    def _add_index(self, name, target):
+        index_text = '{name}; {label}'.format(
+            name=name, label=self.label)
+        self.indexnode['entries'].append(
+            ('pair', index_text, target, '', None))
+
+    def _add_target(self, name, sig, signode):
+        target = make_target(self.cell, name)
+        if target not in self.state.document.ids:
+            signode['names'].append(name)
+            signode['ids'].append(target)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+
+            obarray = self.env.domaindata['el']['obarray']
+            symbol = obarray.setdefault(name, {})
+            if self.cell in symbol:
+                self.state_machine.reporter.warning(
+                    'duplicate description of %s %s, ' % (self.objtype, name)
+                    + 'other instance in '
+                    + self.env.doc2path(symbol[self.cell].docname),
+                    line=self.lineno)
+            symbol[self.cell] = Cell(self.objtype, self.env.docname)
+
+        return target
+
+    def add_target_and_index(self, name, sig, signode):
+        target = self._add_target(name, sig, signode)
+        self._add_index(name, target)
+
+
+class EmacsLispMinorMode(EmacsLispSymbol):
+    cell = 'function'
+    label = 'Minor Mode'
+
+    def handle_signature(self, signature, signode):
+        """Create nodes in ``signode`` for the ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+
+        Add the object type label before the symbol name and return
+        ``signature``.
+
+        """
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(signature, to_mode_name(signature))
+        return signature
+
+    def _add_index(self, name, target):
+        return super()._add_index(to_mode_name(name), target)
+
+
+class EmacsLispFunction(EmacsLispSymbol):
+    """A directive to document Emacs Lisp functions."""
+
+    cell_for_objtype = {
+        'defun': 'function',
+        'defmacro': 'function'
+    }
+
+    def handle_signature(self, signature, signode):
+        function_name, *args = ws_re.split(signature)
+        label = self.label + ' '
+        signode += addnodes.desc_annotation(label, label)
+        signode += addnodes.desc_name(function_name, function_name)
+        for arg in args:
+            is_keyword = arg.startswith('&')
+            node = (addnodes.desc_annotation
+                    if is_keyword
+                    else addnodes.desc_addname)
+            signode += node(' ' + arg, ' ' + arg)
+
+        return function_name
+
+
+class EmacsLispKey(ObjectDescription):
+    """A directive to document interactive commands via their bindings."""
+
+    label = 'Interactive command'
+
+    def handle_signature(self, signature, signode):
+        """Create nodes to ``signode`` for ``signature``.
+
+        ``signode`` is a docutils node to which to add the nodes, and
+        ``signature`` is the symbol name.
+        """
+        key_sequence = KeySequence.fromstring(signature)
+        signode += addnodes.desc_name(signature, str(key_sequence))
+        return str(key_sequence)
+
+    def _add_command_target_and_index(self, name, sig, signode):
+        target_name = make_target('function', name)
+        if target_name not in self.state.document.ids:
+            signode['names'].append(name)
+            signode['ids'].append(target_name)
+            self.state.document.note_explicit_target(signode)
+
+            obarray = self.env.domaindata['el']['obarray']
+            symbol = obarray.setdefault(name, {})
+            if 'function' in symbol:
+                self.state_machine.reporter.warning(
+                    'duplicate description of %s %s, ' % (self.objtype, name)
+                    + 'other instance in '
+                    + self.env.doc2path(symbol['function'].docname),
+                    line=self.lineno)
+            symbol['function'] = Cell(self.objtype, self.env.docname)
+
+        index_text = '{name}; {label}'.format(name=name, label=self.label)
+        self.indexnode['entries'].append(
+            ('pair', index_text, target_name, '', None))
+
+    def _add_binding_target_and_index(self, binding, sig, signode):
+        reftarget = make_target('key', binding)
+
+        if reftarget not in self.state.document.ids:
+            signode['names'].append(reftarget)
+            signode['ids'].append(reftarget)
+            signode['first'] = (not self.names)
+            self.state.document.note_explicit_target(signode)
+
+            keymap = self.env.domaindata['el']['keymap']
+            if binding in keymap:
+                self.state_machine.reporter.warning(
+                    'duplicate description of binding %s, ' % binding
+                    + 'other instance in '
+                    + self.env.doc2path(keymap[binding]),
+                    line=self.lineno)
+            keymap[binding] = self.env.docname
+
+        index_text = '{name}; key binding'.format(name=binding)
+        self.indexnode['entries'].append(
+            ('pair', index_text, reftarget, '', None))
+
+    def add_target_and_index(self, name, sig, signode):
+        # If unprefixed M-x command index as function and not as key binding
+        sequence = KeySequence.fromstring(name)
+        if sequence.command_name and not sequence.has_prefix:
+            self._add_command_target_and_index(sequence.command_name,
+                                               sig, signode)
+        else:
+            self._add_binding_target_and_index(name, sig, signode)
+
+
+class XRefModeRole(XRefRole):
+    """A role to cross-reference a minor mode.
+
+    Like a normal cross-reference role but appends ``-mode`` to the reference
+    target and title-cases the symbol name like Emacs does when referring to
+    modes.
+
+    """
+
+    fix_parens = False
+    lowercase = False
+
+    def process_link(self, env, refnode, has_explicit_title, title, target):
+        refnode['reftype'] = 'minor-mode'
+        target = target + '-mode'
+        return (title if has_explicit_title else to_mode_name(target), target)
+
+
+class EmacsLispDomain(Domain):
+    """A domain to document Emacs Lisp code."""
+
+    name = 'el'
+    label = 'Emacs Lisp'
+
+    object_types = {
+        # TODO: Set search prio for object types
+        # Types for user-facing options and commands
+        'minor-mode': ObjType('minor-mode', 'function', 'mode',
+                              cell='function'),
+        'define-key': ObjType('key binding', cell='interactive'),
+        'defcustom': ObjType('defcustom', 'defcustom', cell='variable'),
+        'defface': ObjType('defface', 'defface', cell='face'),
+        # Object types for code
+        'defun': ObjType('defun', 'defun', cell='function'),
+        'defmacro': ObjType('defmacro', 'defmacro', cell='function'),
+        'defvar': ObjType('defvar', 'defvar', cell='variable'),
+        'defconst': ObjType('defconst', 'defconst', cell='variable')
+    }
+    directives = {
+        'minor-mode': EmacsLispMinorMode,
+        'define-key': EmacsLispKey,
+        'defcustom': EmacsLispSymbol,
+        'defvar': EmacsLispSymbol,
+        'defconst': EmacsLispSymbol,
+        'defface': EmacsLispSymbol,
+        'defun': EmacsLispFunction,
+        'defmacro': EmacsLispFunction
+    }
+    roles = {
+        'mode': XRefModeRole(),
+        'defvar': XRefRole(),
+        'defconst': XRefRole(),
+        'defcustom': XRefRole(),
+        'defface': XRefRole(),
+        'defun': XRefRole(),
+        'defmacro': XRefRole()
+    }
+
+    data_version = 1
+    initial_data = {
+        # Our domain data attempts to somewhat mirror the semantics of Emacs
+        # Lisp, so we have an obarray which holds symbols which in turn have
+        # function, variable, face, etc. cells, and a keymap which holds the
+        # documentation for key bindings.
+        'obarray': {},
+        'keymap': {}
+    }
+
+    def clear_doc(self, docname):
+        """Clear all cells documented ``docname``."""
+        for symbol in self.data['obarray'].values():
+            for cell in list(symbol.keys()):
+                if docname == symbol[cell].docname:
+                    del symbol[cell]
+        for binding in list(self.data['keymap']):
+            if self.data['keymap'][binding] == docname:
+                del self.data['keymap'][binding]
+
+    def resolve_xref(self, env, fromdocname, builder,
+                     objtype, target, node, contnode):
+        """Resolve a cross reference to ``target``."""
+        if objtype == 'key':
+            todocname = self.data['keymap'].get(target)
+            if not todocname:
+                return None
+            reftarget = make_target('key', target)
+        else:
+            cell = self.object_types[objtype].attrs['cell']
+            symbol = self.data['obarray'].get(target, {})
+            if cell not in symbol:
+                return None
+            reftarget = make_target(cell, target)
+            todocname = symbol[cell].docname
+
+        return make_refnode(builder, fromdocname, todocname,
+                            reftarget, contnode, target)
+
+    def resolve_any_xref(self, env, fromdocname, builder,
+                         target, node, contnode):
+        """Return all possible cross references for ``target``."""
+        nodes = ((objtype, self.resolve_xref(env, fromdocname, builder,
+                                             objtype, target, node, contnode))
+                 for objtype in ['key', 'defun', 'defvar', 'defface'])
+        return [('el:{}'.format(objtype), node) for (objtype, node) in nodes
+                if node is not None]
+
+    def merge_warn_duplicate(self, objname, our_docname, their_docname):
+        self.env.warn(
+            their_docname,
+            "Duplicate declaration: '{}' also defined in '{}'.\n".format(
+                objname, their_docname))
+
+    def merge_keymapdata(self, docnames, our_keymap, their_keymap):
+        for key, docname in their_keymap.items():
+            if docname in docnames:
+                if key in our_keymap:
+                    our_docname = our_keymap[key]
+                    self.merge_warn_duplicate(key, our_docname, docname)
+                else:
+                    our_keymap[key] = docname
+
+    def merge_obarraydata(self, docnames, our_obarray, their_obarray):
+        for objname, their_cells in their_obarray.items():
+            our_cells = our_obarray.setdefault(objname, dict())
+            for cellname, their_cell in their_cells.items():
+                if their_cell.docname in docnames:
+                    our_cell = our_cells.get(cellname)
+                    if our_cell:
+                        self.merge_warn_duplicate(objname, our_cell.docname,
+                                                  their_cell.docname)
+                    else:
+                        our_cells[cellname] = their_cell
+
+    def merge_domaindata(self, docnames, otherdata):
+        self.merge_keymapdata(docnames, self.data['keymap'],
+                              otherdata['keymap'])
+        self.merge_obarraydata(docnames, self.data['obarray'],
+                               otherdata['obarray'])
+
+    def get_objects(self):
+        """Get all documented symbols for use in the search index."""
+        for name, symbol in self.data['obarray'].items():
+            for cellname, cell in symbol.items():
+                yield (name, name, cell.objtype, cell.docname,
+                       make_target(cellname, name),
+                       self.object_types[cell.objtype].attrs['searchprio'])
+
+
+def setup(app):
+    app.add_domain(EmacsLispDomain)
+    return {'version': '0.1', 'parallel_read_safe': True}