mirror of
https://git.notmuchmail.org/git/notmuch
synced 2024-11-27 13:17:56 +01:00
174221b179
A more verbose category is needed for the index, but not the description in the manual.
445 lines
16 KiB
Python
445 lines
16 KiB
Python
# 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'
|
|
}
|
|
|
|
category_for_objtype = {
|
|
'defcustom': 'Emacs variable (customizable)',
|
|
'defconst': 'Emacs constant',
|
|
'defvar': 'Emacs variable',
|
|
'defface': 'Emacs 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
|
|
|
|
@property
|
|
def category(self):
|
|
"""Index category"""
|
|
return self.category_for_objtype[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.category)
|
|
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 = 'Emacs 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}; Emacs 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 = ''
|
|
|
|
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}
|