]> git.cworth.org Git - notmuch/blob - doc/elisp.py
doc/elisp: replace label for Emacs commands and key bindings.
[notmuch] / doc / elisp.py
1 # Copyright (C) 2016 Sebastian Wiesner and Flycheck contributors
2
3 # This file is not part of GNU Emacs.
4
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
8 # version.
9
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
13 # details.
14
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/>.
17
18
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
26
27
28 def make_target(cell, name):
29     """Create a target name from ``cell`` and ``name``.
30
31     ``cell`` is the name of a symbol cell, and ``name`` is a symbol name, both
32     as strings.
33
34     The target names are used as cross-reference targets for Sphinx.
35
36     """
37     return '{cell}-{name}'.format(cell=cell, name=name)
38
39
40 def to_mode_name(symbol_name):
41     """Convert ``symbol_name`` to a mode name.
42
43     Split at ``-`` and titlecase each part.
44
45     """
46     return ' '.join(p.title() for p in symbol_name.split('-'))
47
48
49 class Cell(namedtuple('Cell', 'objtype docname')):
50     """A cell in a symbol.
51
52     A cell holds the object type and the document name of the description for
53     the cell.
54
55     Cell objects are used within symbol entries in the domain data.
56
57     """
58
59     pass
60
61
62 class KeySequence(namedtuple('KeySequence', 'keys')):
63     """A key sequence."""
64
65     PREFIX_KEYS = {'C-u'}
66     PREFIX_KEYS.update('M-{}'.format(n) for n in range(10))
67
68     @classmethod
69     def fromstring(cls, s):
70         return cls(s.split())
71
72     @property
73     def command_name(self):
74         """The command name in this key sequence.
75
76         Return ``None`` for key sequences that are no command invocations with
77         ``M-x``.
78
79         """
80         try:
81             return self.keys[self.keys.index('M-x') + 1]
82         except ValueError:
83             return None
84
85     @property
86     def has_prefix(self):
87         """Whether this key sequence has a prefix."""
88         return self.keys[0] in self.PREFIX_KEYS
89
90     def __str__(self):
91         return ' '.join(self.keys)
92
93
94 class EmacsLispSymbol(ObjectDescription):
95     """An abstract base class for directives documenting symbols.
96
97     Provide target and index generation and registration of documented symbols
98     within the domain data.
99
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.
103
104     """
105
106     cell_for_objtype = {
107         'defcustom': 'variable',
108         'defconst': 'variable',
109         'defvar': 'variable',
110         'defface': 'face'
111     }
112
113     @property
114     def cell(self):
115         """The cell in which to store symbol metadata."""
116         return self.cell_for_objtype[self.objtype]
117
118     @property
119     def label(self):
120         """The label for the documented object type."""
121         return self.objtype
122
123     def handle_signature(self, signature, signode):
124         """Create nodes in ``signode`` for the ``signature``.
125
126         ``signode`` is a docutils node to which to add the nodes, and
127         ``signature`` is the symbol name.
128
129         Add the object type label before the symbol name and return
130         ``signature``.
131
132         """
133         label = self.label + ' '
134         signode += addnodes.desc_annotation(label, label)
135         signode += addnodes.desc_name(signature, signature)
136         return signature
137
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))
143
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)
151
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),
159                     line=self.lineno)
160             symbol[self.cell] = Cell(self.objtype, self.env.docname)
161
162         return target
163
164     def add_target_and_index(self, name, sig, signode):
165         target = self._add_target(name, sig, signode)
166         self._add_index(name, target)
167
168
169 class EmacsLispMinorMode(EmacsLispSymbol):
170     cell = 'function'
171     label = 'Minor Mode'
172
173     def handle_signature(self, signature, signode):
174         """Create nodes in ``signode`` for the ``signature``.
175
176         ``signode`` is a docutils node to which to add the nodes, and
177         ``signature`` is the symbol name.
178
179         Add the object type label before the symbol name and return
180         ``signature``.
181
182         """
183         label = self.label + ' '
184         signode += addnodes.desc_annotation(label, label)
185         signode += addnodes.desc_name(signature, to_mode_name(signature))
186         return signature
187
188     def _add_index(self, name, target):
189         return super()._add_index(to_mode_name(name), target)
190
191
192 class EmacsLispFunction(EmacsLispSymbol):
193     """A directive to document Emacs Lisp functions."""
194
195     cell_for_objtype = {
196         'defun': 'function',
197         'defmacro': 'function'
198     }
199
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)
205         for arg in args:
206             is_keyword = arg.startswith('&')
207             node = (addnodes.desc_annotation
208                     if is_keyword
209                     else addnodes.desc_addname)
210             signode += node(' ' + arg, ' ' + arg)
211
212         return function_name
213
214
215 class EmacsLispKey(ObjectDescription):
216     """A directive to document interactive commands via their bindings."""
217
218     label = 'Emacs command'
219
220     def handle_signature(self, signature, signode):
221         """Create nodes to ``signode`` for ``signature``.
222
223         ``signode`` is a docutils node to which to add the nodes, and
224         ``signature`` is the symbol name.
225         """
226         key_sequence = KeySequence.fromstring(signature)
227         signode += addnodes.desc_name(signature, str(key_sequence))
228         return str(key_sequence)
229
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)
236
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),
244                     line=self.lineno)
245             symbol['function'] = Cell(self.objtype, self.env.docname)
246
247         index_text = '{name}; {label}'.format(name=name, label=self.label)
248         self.indexnode['entries'].append(
249             ('pair', index_text, target_name, '', None))
250
251     def _add_binding_target_and_index(self, binding, sig, signode):
252         reftarget = make_target('key', binding)
253
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)
259
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]),
266                     line=self.lineno)
267             keymap[binding] = self.env.docname
268
269         index_text = '{name}; Emacs key binding'.format(name=binding)
270         self.indexnode['entries'].append(
271             ('pair', index_text, reftarget, '', None))
272
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,
278                                                sig, signode)
279         else:
280             self._add_binding_target_and_index(name, sig, signode)
281
282
283 class XRefModeRole(XRefRole):
284     """A role to cross-reference a minor mode.
285
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
288     modes.
289
290     """
291
292     fix_parens = False
293     lowercase = False
294
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)
299
300
301 class EmacsLispDomain(Domain):
302     """A domain to document Emacs Lisp code."""
303
304     name = 'el'
305     label = 'Emacs Lisp'
306
307     object_types = {
308         # TODO: Set search prio for object types
309         # Types for user-facing options and commands
310         'minor-mode': ObjType('minor-mode', 'function', 'mode',
311                               cell='function'),
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')
320     }
321     directives = {
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
330     }
331     roles = {
332         'mode': XRefModeRole(),
333         'defvar': XRefRole(),
334         'defconst': XRefRole(),
335         'defcustom': XRefRole(),
336         'defface': XRefRole(),
337         'defun': XRefRole(),
338         'defmacro': XRefRole()
339     }
340
341     data_version = 1
342     initial_data = {
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.
347         'obarray': {},
348         'keymap': {}
349     }
350
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:
356                     del symbol[cell]
357         for binding in list(self.data['keymap']):
358             if self.data['keymap'][binding] == docname:
359                 del self.data['keymap'][binding]
360
361     def resolve_xref(self, env, fromdocname, builder,
362                      objtype, target, node, contnode):
363         """Resolve a cross reference to ``target``."""
364         if objtype == 'key':
365             todocname = self.data['keymap'].get(target)
366             if not todocname:
367                 return None
368             reftarget = make_target('key', target)
369         else:
370             cell = self.object_types[objtype].attrs['cell']
371             symbol = self.data['obarray'].get(target, {})
372             if cell not in symbol:
373                 return None
374             reftarget = make_target(cell, target)
375             todocname = symbol[cell].docname
376
377         return make_refnode(builder, fromdocname, todocname,
378                             reftarget, contnode, target)
379
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
387                 if node is not None]
388
389     def merge_warn_duplicate(self, objname, our_docname, their_docname):
390         self.env.warn(
391             their_docname,
392             "Duplicate declaration: '{}' also defined in '{}'.\n".format(
393                 objname, their_docname))
394
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)
401                 else:
402                     our_keymap[key] = docname
403
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)
410                     if our_cell:
411                         self.merge_warn_duplicate(objname, our_cell.docname,
412                                                   their_cell.docname)
413                     else:
414                         our_cells[cellname] = their_cell
415
416     def merge_domaindata(self, docnames, otherdata):
417         self.merge_keymapdata(docnames, self.data['keymap'],
418                               otherdata['keymap'])
419         self.merge_obarraydata(docnames, self.data['obarray'],
420                                otherdata['obarray'])
421
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'])
429
430
431 def setup(app):
432     app.add_domain(EmacsLispDomain)
433     return {'version': '0.1', 'parallel_read_safe': True}