]> git.cworth.org Git - notmuch/blob - doc/elisp.py
ruby: tags: return string array directly
[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     category_for_objtype = {
114         'defcustom': 'Emacs variable (customizable)',
115         'defconst': 'Emacs constant',
116         'defvar': 'Emacs variable',
117         'defface': 'Emacs face'
118     }
119
120     @property
121     def cell(self):
122         """The cell in which to store symbol metadata."""
123         return self.cell_for_objtype[self.objtype]
124
125     @property
126     def label(self):
127         """The label for the documented object type."""
128         return self.objtype
129
130     @property
131     def category(self):
132         """Index category"""
133         return self.category_for_objtype[self.objtype]
134
135     def handle_signature(self, signature, signode):
136         """Create nodes in ``signode`` for the ``signature``.
137
138         ``signode`` is a docutils node to which to add the nodes, and
139         ``signature`` is the symbol name.
140
141         Add the object type label before the symbol name and return
142         ``signature``.
143
144         """
145         label = self.label + ' '
146         signode += addnodes.desc_annotation(label, label)
147         signode += addnodes.desc_name(signature, signature)
148         return signature
149
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))
155
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)
163
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),
171                     line=self.lineno)
172             symbol[self.cell] = Cell(self.objtype, self.env.docname)
173
174         return target
175
176     def add_target_and_index(self, name, sig, signode):
177         target = self._add_target(name, sig, signode)
178         self._add_index(name, target)
179
180
181 class EmacsLispMinorMode(EmacsLispSymbol):
182     cell = 'function'
183     label = 'Minor Mode'
184
185     def handle_signature(self, signature, signode):
186         """Create nodes in ``signode`` for the ``signature``.
187
188         ``signode`` is a docutils node to which to add the nodes, and
189         ``signature`` is the symbol name.
190
191         Add the object type label before the symbol name and return
192         ``signature``.
193
194         """
195         label = self.label + ' '
196         signode += addnodes.desc_annotation(label, label)
197         signode += addnodes.desc_name(signature, to_mode_name(signature))
198         return signature
199
200     def _add_index(self, name, target):
201         return super()._add_index(to_mode_name(name), target)
202
203
204 class EmacsLispFunction(EmacsLispSymbol):
205     """A directive to document Emacs Lisp functions."""
206
207     cell_for_objtype = {
208         'defun': 'function',
209         'defmacro': 'function'
210     }
211
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)
217         for arg in args:
218             is_keyword = arg.startswith('&')
219             node = (addnodes.desc_annotation
220                     if is_keyword
221                     else addnodes.desc_addname)
222             signode += node(' ' + arg, ' ' + arg)
223
224         return function_name
225
226
227 class EmacsLispKey(ObjectDescription):
228     """A directive to document interactive commands via their bindings."""
229
230     label = 'Emacs command'
231
232     def handle_signature(self, signature, signode):
233         """Create nodes to ``signode`` for ``signature``.
234
235         ``signode`` is a docutils node to which to add the nodes, and
236         ``signature`` is the symbol name.
237         """
238         key_sequence = KeySequence.fromstring(signature)
239         signode += addnodes.desc_name(signature, str(key_sequence))
240         return str(key_sequence)
241
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)
248
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),
256                     line=self.lineno)
257             symbol['function'] = Cell(self.objtype, self.env.docname)
258
259         index_text = '{name}; {label}'.format(name=name, label=self.label)
260         self.indexnode['entries'].append(
261             ('pair', index_text, target_name, '', None))
262
263     def _add_binding_target_and_index(self, binding, sig, signode):
264         reftarget = make_target('key', binding)
265
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)
271
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]),
278                     line=self.lineno)
279             keymap[binding] = self.env.docname
280
281         index_text = '{name}; Emacs key binding'.format(name=binding)
282         self.indexnode['entries'].append(
283             ('pair', index_text, reftarget, '', None))
284
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,
290                                                sig, signode)
291         else:
292             self._add_binding_target_and_index(name, sig, signode)
293
294
295 class XRefModeRole(XRefRole):
296     """A role to cross-reference a minor mode.
297
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
300     modes.
301
302     """
303
304     fix_parens = False
305     lowercase = False
306
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)
311
312
313 class EmacsLispDomain(Domain):
314     """A domain to document Emacs Lisp code."""
315
316     name = 'el'
317     label = ''
318
319     object_types = {
320         # TODO: Set search prio for object types
321         # Types for user-facing options and commands
322         'minor-mode': ObjType('minor-mode', 'function', 'mode',
323                               cell='function'),
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')
332     }
333     directives = {
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
342     }
343     roles = {
344         'mode': XRefModeRole(),
345         'defvar': XRefRole(),
346         'defconst': XRefRole(),
347         'defcustom': XRefRole(),
348         'defface': XRefRole(),
349         'defun': XRefRole(),
350         'defmacro': XRefRole()
351     }
352
353     data_version = 1
354     initial_data = {
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.
359         'obarray': {},
360         'keymap': {}
361     }
362
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:
368                     del symbol[cell]
369         for binding in list(self.data['keymap']):
370             if self.data['keymap'][binding] == docname:
371                 del self.data['keymap'][binding]
372
373     def resolve_xref(self, env, fromdocname, builder,
374                      objtype, target, node, contnode):
375         """Resolve a cross reference to ``target``."""
376         if objtype == 'key':
377             todocname = self.data['keymap'].get(target)
378             if not todocname:
379                 return None
380             reftarget = make_target('key', target)
381         else:
382             cell = self.object_types[objtype].attrs['cell']
383             symbol = self.data['obarray'].get(target, {})
384             if cell not in symbol:
385                 return None
386             reftarget = make_target(cell, target)
387             todocname = symbol[cell].docname
388
389         return make_refnode(builder, fromdocname, todocname,
390                             reftarget, contnode, target)
391
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
399                 if node is not None]
400
401     def merge_warn_duplicate(self, objname, our_docname, their_docname):
402         self.env.warn(
403             their_docname,
404             "Duplicate declaration: '{}' also defined in '{}'.\n".format(
405                 objname, their_docname))
406
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)
413                 else:
414                     our_keymap[key] = docname
415
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)
422                     if our_cell:
423                         self.merge_warn_duplicate(objname, our_cell.docname,
424                                                   their_cell.docname)
425                     else:
426                         our_cells[cellname] = their_cell
427
428     def merge_domaindata(self, docnames, otherdata):
429         self.merge_keymapdata(docnames, self.data['keymap'],
430                               otherdata['keymap'])
431         self.merge_obarraydata(docnames, self.data['obarray'],
432                                otherdata['obarray'])
433
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'])
441
442
443 def setup(app):
444     app.add_domain(EmacsLispDomain)
445     return {'version': '0.1', 'parallel_read_safe': True}