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',
113 category_for_objtype = {
114 'defcustom': 'Emacs variable (customizable)',
115 'defconst': 'Emacs constant',
116 'defvar': 'Emacs variable',
117 'defface': 'Emacs face'
122 """The cell in which to store symbol metadata."""
123 return self.cell_for_objtype[self.objtype]
127 """The label for the documented object type."""
133 return self.category_for_objtype[self.objtype]
135 def handle_signature(self, signature, signode):
136 """Create nodes in ``signode`` for the ``signature``.
138 ``signode`` is a docutils node to which to add the nodes, and
139 ``signature`` is the symbol name.
141 Add the object type label before the symbol name and return
145 label = self.label + ' '
146 signode += addnodes.desc_annotation(label, label)
147 signode += addnodes.desc_name(signature, signature)
150 def _add_index(self, name, target):
151 index_text = '{name}; {label}'.format(
152 name=name, label=self.category)
153 self.indexnode['entries'].append(
154 ('pair', index_text, target, '', None))
156 def _add_target(self, name, sig, signode):
157 target = make_target(self.cell, name)
158 if target not in self.state.document.ids:
159 signode['names'].append(name)
160 signode['ids'].append(target)
161 signode['first'] = (not self.names)
162 self.state.document.note_explicit_target(signode)
164 obarray = self.env.domaindata['el']['obarray']
165 symbol = obarray.setdefault(name, {})
166 if self.cell in symbol:
167 self.state_machine.reporter.warning(
168 'duplicate description of %s %s, ' % (self.objtype, name)
169 + 'other instance in '
170 + self.env.doc2path(symbol[self.cell].docname),
172 symbol[self.cell] = Cell(self.objtype, self.env.docname)
176 def add_target_and_index(self, name, sig, signode):
177 target = self._add_target(name, sig, signode)
178 self._add_index(name, target)
181 class EmacsLispMinorMode(EmacsLispSymbol):
185 def handle_signature(self, signature, signode):
186 """Create nodes in ``signode`` for the ``signature``.
188 ``signode`` is a docutils node to which to add the nodes, and
189 ``signature`` is the symbol name.
191 Add the object type label before the symbol name and return
195 label = self.label + ' '
196 signode += addnodes.desc_annotation(label, label)
197 signode += addnodes.desc_name(signature, to_mode_name(signature))
200 def _add_index(self, name, target):
201 return super()._add_index(to_mode_name(name), target)
204 class EmacsLispFunction(EmacsLispSymbol):
205 """A directive to document Emacs Lisp functions."""
209 'defmacro': 'function'
212 def handle_signature(self, signature, signode):
213 function_name, *args = ws_re.split(signature)
214 label = self.label + ' '
215 signode += addnodes.desc_annotation(label, label)
216 signode += addnodes.desc_name(function_name, function_name)
218 is_keyword = arg.startswith('&')
219 node = (addnodes.desc_annotation
221 else addnodes.desc_addname)
222 signode += node(' ' + arg, ' ' + arg)
227 class EmacsLispKey(ObjectDescription):
228 """A directive to document interactive commands via their bindings."""
230 label = 'Emacs command'
232 def handle_signature(self, signature, signode):
233 """Create nodes to ``signode`` for ``signature``.
235 ``signode`` is a docutils node to which to add the nodes, and
236 ``signature`` is the symbol name.
238 key_sequence = KeySequence.fromstring(signature)
239 signode += addnodes.desc_name(signature, str(key_sequence))
240 return str(key_sequence)
242 def _add_command_target_and_index(self, name, sig, signode):
243 target_name = make_target('function', name)
244 if target_name not in self.state.document.ids:
245 signode['names'].append(name)
246 signode['ids'].append(target_name)
247 self.state.document.note_explicit_target(signode)
249 obarray = self.env.domaindata['el']['obarray']
250 symbol = obarray.setdefault(name, {})
251 if 'function' in symbol:
252 self.state_machine.reporter.warning(
253 'duplicate description of %s %s, ' % (self.objtype, name)
254 + 'other instance in '
255 + self.env.doc2path(symbol['function'].docname),
257 symbol['function'] = Cell(self.objtype, self.env.docname)
259 index_text = '{name}; {label}'.format(name=name, label=self.label)
260 self.indexnode['entries'].append(
261 ('pair', index_text, target_name, '', None))
263 def _add_binding_target_and_index(self, binding, sig, signode):
264 reftarget = make_target('key', binding)
266 if reftarget not in self.state.document.ids:
267 signode['names'].append(reftarget)
268 signode['ids'].append(reftarget)
269 signode['first'] = (not self.names)
270 self.state.document.note_explicit_target(signode)
272 keymap = self.env.domaindata['el']['keymap']
273 if binding in keymap:
274 self.state_machine.reporter.warning(
275 'duplicate description of binding %s, ' % binding
276 + 'other instance in '
277 + self.env.doc2path(keymap[binding]),
279 keymap[binding] = self.env.docname
281 index_text = '{name}; Emacs key binding'.format(name=binding)
282 self.indexnode['entries'].append(
283 ('pair', index_text, reftarget, '', None))
285 def add_target_and_index(self, name, sig, signode):
286 # If unprefixed M-x command index as function and not as key binding
287 sequence = KeySequence.fromstring(name)
288 if sequence.command_name and not sequence.has_prefix:
289 self._add_command_target_and_index(sequence.command_name,
292 self._add_binding_target_and_index(name, sig, signode)
295 class XRefModeRole(XRefRole):
296 """A role to cross-reference a minor mode.
298 Like a normal cross-reference role but appends ``-mode`` to the reference
299 target and title-cases the symbol name like Emacs does when referring to
307 def process_link(self, env, refnode, has_explicit_title, title, target):
308 refnode['reftype'] = 'minor-mode'
309 target = target + '-mode'
310 return (title if has_explicit_title else to_mode_name(target), target)
313 class EmacsLispDomain(Domain):
314 """A domain to document Emacs Lisp code."""
320 # TODO: Set search prio for object types
321 # Types for user-facing options and commands
322 'minor-mode': ObjType('minor-mode', 'function', 'mode',
324 'define-key': ObjType('key binding', cell='interactive'),
325 'defcustom': ObjType('defcustom', 'defcustom', cell='variable'),
326 'defface': ObjType('defface', 'defface', cell='face'),
327 # Object types for code
328 'defun': ObjType('defun', 'defun', cell='function'),
329 'defmacro': ObjType('defmacro', 'defmacro', cell='function'),
330 'defvar': ObjType('defvar', 'defvar', cell='variable'),
331 'defconst': ObjType('defconst', 'defconst', cell='variable')
334 'minor-mode': EmacsLispMinorMode,
335 'define-key': EmacsLispKey,
336 'defcustom': EmacsLispSymbol,
337 'defvar': EmacsLispSymbol,
338 'defconst': EmacsLispSymbol,
339 'defface': EmacsLispSymbol,
340 'defun': EmacsLispFunction,
341 'defmacro': EmacsLispFunction
344 'mode': XRefModeRole(),
345 'defvar': XRefRole(),
346 'defconst': XRefRole(),
347 'defcustom': XRefRole(),
348 'defface': XRefRole(),
350 'defmacro': XRefRole()
355 # Our domain data attempts to somewhat mirror the semantics of Emacs
356 # Lisp, so we have an obarray which holds symbols which in turn have
357 # function, variable, face, etc. cells, and a keymap which holds the
358 # documentation for key bindings.
363 def clear_doc(self, docname):
364 """Clear all cells documented ``docname``."""
365 for symbol in self.data['obarray'].values():
366 for cell in list(symbol.keys()):
367 if docname == symbol[cell].docname:
369 for binding in list(self.data['keymap']):
370 if self.data['keymap'][binding] == docname:
371 del self.data['keymap'][binding]
373 def resolve_xref(self, env, fromdocname, builder,
374 objtype, target, node, contnode):
375 """Resolve a cross reference to ``target``."""
377 todocname = self.data['keymap'].get(target)
380 reftarget = make_target('key', target)
382 cell = self.object_types[objtype].attrs['cell']
383 symbol = self.data['obarray'].get(target, {})
384 if cell not in symbol:
386 reftarget = make_target(cell, target)
387 todocname = symbol[cell].docname
389 return make_refnode(builder, fromdocname, todocname,
390 reftarget, contnode, target)
392 def resolve_any_xref(self, env, fromdocname, builder,
393 target, node, contnode):
394 """Return all possible cross references for ``target``."""
395 nodes = ((objtype, self.resolve_xref(env, fromdocname, builder,
396 objtype, target, node, contnode))
397 for objtype in ['key', 'defun', 'defvar', 'defface'])
398 return [('el:{}'.format(objtype), node) for (objtype, node) in nodes
401 def merge_warn_duplicate(self, objname, our_docname, their_docname):
404 "Duplicate declaration: '{}' also defined in '{}'.\n".format(
405 objname, their_docname))
407 def merge_keymapdata(self, docnames, our_keymap, their_keymap):
408 for key, docname in their_keymap.items():
409 if docname in docnames:
410 if key in our_keymap:
411 our_docname = our_keymap[key]
412 self.merge_warn_duplicate(key, our_docname, docname)
414 our_keymap[key] = docname
416 def merge_obarraydata(self, docnames, our_obarray, their_obarray):
417 for objname, their_cells in their_obarray.items():
418 our_cells = our_obarray.setdefault(objname, dict())
419 for cellname, their_cell in their_cells.items():
420 if their_cell.docname in docnames:
421 our_cell = our_cells.get(cellname)
423 self.merge_warn_duplicate(objname, our_cell.docname,
426 our_cells[cellname] = their_cell
428 def merge_domaindata(self, docnames, otherdata):
429 self.merge_keymapdata(docnames, self.data['keymap'],
431 self.merge_obarraydata(docnames, self.data['obarray'],
432 otherdata['obarray'])
434 def get_objects(self):
435 """Get all documented symbols for use in the search index."""
436 for name, symbol in self.data['obarray'].items():
437 for cellname, cell in symbol.items():
438 yield (name, name, cell.objtype, cell.docname,
439 make_target(cellname, name),
440 self.object_types[cell.objtype].attrs['searchprio'])
444 app.add_domain(EmacsLispDomain)
445 return {'version': '0.1', 'parallel_read_safe': True}