1 # Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors
3 # This file is not part of GNU Emacs.
5 # This program is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15 # You should have received a copy of the GNU General Public License along with
16 # this program. If not, see <http://www.gnu.org/licenses/>.
19 from collections import namedtuple
20 from sphinx import addnodes
21 from sphinx.util import ws_re
22 from sphinx.roles import XRefRole
23 from sphinx.domains import Domain, ObjType
24 from sphinx.util.nodes import make_refnode
25 from sphinx.directives import ObjectDescription
28 def make_target(cell, name):
29 """Create a target name from ``cell`` and ``name``.
31 ``cell`` is the name of a symbol cell, and ``name`` is a symbol name, both
34 The target names are used as cross-reference targets for Sphinx.
37 return '{cell}-{name}'.format(cell=cell, name=name)
40 def to_mode_name(symbol_name):
41 """Convert ``symbol_name`` to a mode name.
43 Split at ``-`` and titlecase each part.
46 return ' '.join(p.title() for p in symbol_name.split('-'))
49 class Cell(namedtuple('Cell', 'objtype docname')):
50 """A cell in a symbol.
52 A cell holds the object type and the document name of the description for
55 Cell objects are used within symbol entries in the domain data.
62 class KeySequence(namedtuple('KeySequence', 'keys')):
66 PREFIX_KEYS.update('M-{}'.format(n) for n in range(10))
69 def fromstring(cls, s):
73 def command_name(self):
74 """The command name in this key sequence.
76 Return ``None`` for key sequences that are no command invocations with
81 return self.keys[self.keys.index('M-x') + 1]
87 """Whether this key sequence has a prefix."""
88 return self.keys[0] in self.PREFIX_KEYS
91 return ' '.join(self.keys)
94 class EmacsLispSymbol(ObjectDescription):
95 """An abstract base class for directives documenting symbols.
97 Provide target and index generation and registration of documented symbols
98 within the domain data.
100 Deriving classes must have a ``cell`` attribute which refers to the cell
101 the documentation goes in, and a ``label`` attribute which provides a
102 human-readable name for what is documented, used in the index entry.
107 'defcustom': 'variable',
108 'defconst': 'variable',
109 'defvar': 'variable',
115 """The cell in which to store symbol metadata."""
116 return self.cell_for_objtype[self.objtype]
120 """The label for the documented object type."""
123 def handle_signature(self, signature, signode):
124 """Create nodes in ``signode`` for the ``signature``.
126 ``signode`` is a docutils node to which to add the nodes, and
127 ``signature`` is the symbol name.
129 Add the object type label before the symbol name and return
133 label = self.label + ' '
134 signode += addnodes.desc_annotation(label, label)
135 signode += addnodes.desc_name(signature, signature)
138 def _add_index(self, name, target):
139 index_text = '{name}; {label}'.format(
140 name=name, label=self.label)
141 self.indexnode['entries'].append(
142 ('pair', index_text, target, '', None))
144 def _add_target(self, name, sig, signode):
145 target = make_target(self.cell, name)
146 if target not in self.state.document.ids:
147 signode['names'].append(name)
148 signode['ids'].append(target)
149 signode['first'] = (not self.names)
150 self.state.document.note_explicit_target(signode)
152 obarray = self.env.domaindata['el']['obarray']
153 symbol = obarray.setdefault(name, {})
154 if self.cell in symbol:
155 self.state_machine.reporter.warning(
156 'duplicate description of %s %s, ' % (self.objtype, name)
157 + 'other instance in '
158 + self.env.doc2path(symbol[self.cell].docname),
160 symbol[self.cell] = Cell(self.objtype, self.env.docname)
164 def add_target_and_index(self, name, sig, signode):
165 target = self._add_target(name, sig, signode)
166 self._add_index(name, target)
169 class EmacsLispMinorMode(EmacsLispSymbol):
173 def handle_signature(self, signature, signode):
174 """Create nodes in ``signode`` for the ``signature``.
176 ``signode`` is a docutils node to which to add the nodes, and
177 ``signature`` is the symbol name.
179 Add the object type label before the symbol name and return
183 label = self.label + ' '
184 signode += addnodes.desc_annotation(label, label)
185 signode += addnodes.desc_name(signature, to_mode_name(signature))
188 def _add_index(self, name, target):
189 return super()._add_index(to_mode_name(name), target)
192 class EmacsLispFunction(EmacsLispSymbol):
193 """A directive to document Emacs Lisp functions."""
197 'defmacro': 'function'
200 def handle_signature(self, signature, signode):
201 function_name, *args = ws_re.split(signature)
202 label = self.label + ' '
203 signode += addnodes.desc_annotation(label, label)
204 signode += addnodes.desc_name(function_name, function_name)
206 is_keyword = arg.startswith('&')
207 node = (addnodes.desc_annotation
209 else addnodes.desc_addname)
210 signode += node(' ' + arg, ' ' + arg)
215 class EmacsLispKey(ObjectDescription):
216 """A directive to document interactive commands via their bindings."""
218 label = 'Emacs command'
220 def handle_signature(self, signature, signode):
221 """Create nodes to ``signode`` for ``signature``.
223 ``signode`` is a docutils node to which to add the nodes, and
224 ``signature`` is the symbol name.
226 key_sequence = KeySequence.fromstring(signature)
227 signode += addnodes.desc_name(signature, str(key_sequence))
228 return str(key_sequence)
230 def _add_command_target_and_index(self, name, sig, signode):
231 target_name = make_target('function', name)
232 if target_name not in self.state.document.ids:
233 signode['names'].append(name)
234 signode['ids'].append(target_name)
235 self.state.document.note_explicit_target(signode)
237 obarray = self.env.domaindata['el']['obarray']
238 symbol = obarray.setdefault(name, {})
239 if 'function' in symbol:
240 self.state_machine.reporter.warning(
241 'duplicate description of %s %s, ' % (self.objtype, name)
242 + 'other instance in '
243 + self.env.doc2path(symbol['function'].docname),
245 symbol['function'] = Cell(self.objtype, self.env.docname)
247 index_text = '{name}; {label}'.format(name=name, label=self.label)
248 self.indexnode['entries'].append(
249 ('pair', index_text, target_name, '', None))
251 def _add_binding_target_and_index(self, binding, sig, signode):
252 reftarget = make_target('key', binding)
254 if reftarget not in self.state.document.ids:
255 signode['names'].append(reftarget)
256 signode['ids'].append(reftarget)
257 signode['first'] = (not self.names)
258 self.state.document.note_explicit_target(signode)
260 keymap = self.env.domaindata['el']['keymap']
261 if binding in keymap:
262 self.state_machine.reporter.warning(
263 'duplicate description of binding %s, ' % binding
264 + 'other instance in '
265 + self.env.doc2path(keymap[binding]),
267 keymap[binding] = self.env.docname
269 index_text = '{name}; Emacs key binding'.format(name=binding)
270 self.indexnode['entries'].append(
271 ('pair', index_text, reftarget, '', None))
273 def add_target_and_index(self, name, sig, signode):
274 # If unprefixed M-x command index as function and not as key binding
275 sequence = KeySequence.fromstring(name)
276 if sequence.command_name and not sequence.has_prefix:
277 self._add_command_target_and_index(sequence.command_name,
280 self._add_binding_target_and_index(name, sig, signode)
283 class XRefModeRole(XRefRole):
284 """A role to cross-reference a minor mode.
286 Like a normal cross-reference role but appends ``-mode`` to the reference
287 target and title-cases the symbol name like Emacs does when referring to
295 def process_link(self, env, refnode, has_explicit_title, title, target):
296 refnode['reftype'] = 'minor-mode'
297 target = target + '-mode'
298 return (title if has_explicit_title else to_mode_name(target), target)
301 class EmacsLispDomain(Domain):
302 """A domain to document Emacs Lisp code."""
308 # TODO: Set search prio for object types
309 # Types for user-facing options and commands
310 'minor-mode': ObjType('minor-mode', 'function', 'mode',
312 'define-key': ObjType('key binding', cell='interactive'),
313 'defcustom': ObjType('defcustom', 'defcustom', cell='variable'),
314 'defface': ObjType('defface', 'defface', cell='face'),
315 # Object types for code
316 'defun': ObjType('defun', 'defun', cell='function'),
317 'defmacro': ObjType('defmacro', 'defmacro', cell='function'),
318 'defvar': ObjType('defvar', 'defvar', cell='variable'),
319 'defconst': ObjType('defconst', 'defconst', cell='variable')
322 'minor-mode': EmacsLispMinorMode,
323 'define-key': EmacsLispKey,
324 'defcustom': EmacsLispSymbol,
325 'defvar': EmacsLispSymbol,
326 'defconst': EmacsLispSymbol,
327 'defface': EmacsLispSymbol,
328 'defun': EmacsLispFunction,
329 'defmacro': EmacsLispFunction
332 'mode': XRefModeRole(),
333 'defvar': XRefRole(),
334 'defconst': XRefRole(),
335 'defcustom': XRefRole(),
336 'defface': XRefRole(),
338 'defmacro': XRefRole()
343 # Our domain data attempts to somewhat mirror the semantics of Emacs
344 # Lisp, so we have an obarray which holds symbols which in turn have
345 # function, variable, face, etc. cells, and a keymap which holds the
346 # documentation for key bindings.
351 def clear_doc(self, docname):
352 """Clear all cells documented ``docname``."""
353 for symbol in self.data['obarray'].values():
354 for cell in list(symbol.keys()):
355 if docname == symbol[cell].docname:
357 for binding in list(self.data['keymap']):
358 if self.data['keymap'][binding] == docname:
359 del self.data['keymap'][binding]
361 def resolve_xref(self, env, fromdocname, builder,
362 objtype, target, node, contnode):
363 """Resolve a cross reference to ``target``."""
365 todocname = self.data['keymap'].get(target)
368 reftarget = make_target('key', target)
370 cell = self.object_types[objtype].attrs['cell']
371 symbol = self.data['obarray'].get(target, {})
372 if cell not in symbol:
374 reftarget = make_target(cell, target)
375 todocname = symbol[cell].docname
377 return make_refnode(builder, fromdocname, todocname,
378 reftarget, contnode, target)
380 def resolve_any_xref(self, env, fromdocname, builder,
381 target, node, contnode):
382 """Return all possible cross references for ``target``."""
383 nodes = ((objtype, self.resolve_xref(env, fromdocname, builder,
384 objtype, target, node, contnode))
385 for objtype in ['key', 'defun', 'defvar', 'defface'])
386 return [('el:{}'.format(objtype), node) for (objtype, node) in nodes
389 def merge_warn_duplicate(self, objname, our_docname, their_docname):
392 "Duplicate declaration: '{}' also defined in '{}'.\n".format(
393 objname, their_docname))
395 def merge_keymapdata(self, docnames, our_keymap, their_keymap):
396 for key, docname in their_keymap.items():
397 if docname in docnames:
398 if key in our_keymap:
399 our_docname = our_keymap[key]
400 self.merge_warn_duplicate(key, our_docname, docname)
402 our_keymap[key] = docname
404 def merge_obarraydata(self, docnames, our_obarray, their_obarray):
405 for objname, their_cells in their_obarray.items():
406 our_cells = our_obarray.setdefault(objname, dict())
407 for cellname, their_cell in their_cells.items():
408 if their_cell.docname in docnames:
409 our_cell = our_cells.get(cellname)
411 self.merge_warn_duplicate(objname, our_cell.docname,
414 our_cells[cellname] = their_cell
416 def merge_domaindata(self, docnames, otherdata):
417 self.merge_keymapdata(docnames, self.data['keymap'],
419 self.merge_obarraydata(docnames, self.data['obarray'],
420 otherdata['obarray'])
422 def get_objects(self):
423 """Get all documented symbols for use in the search index."""
424 for name, symbol in self.data['obarray'].items():
425 for cellname, cell in symbol.items():
426 yield (name, name, cell.objtype, cell.docname,
427 make_target(cellname, name),
428 self.object_types[cell.objtype].attrs['searchprio'])
432 app.add_domain(EmacsLispDomain)
433 return {'version': '0.1', 'parallel_read_safe': True}