/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
7397.3.1 by Jelmer Vernooij
Add really basic zsh completion plugin.
1
#!/usr/bin/env python
2
3
# Copyright (C) 2009, 2010 Canonical Ltd
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
19
from __future__ import absolute_import
20
21
from ... import (
22
    cmdline,
23
    commands,
24
    config,
25
    help_topics,
26
    option,
27
    plugin,
28
)
29
from ...sixish import (
30
    text_type,
31
    )
32
import breezy
33
import re
34
import sys
35
36
37
class ZshCodeGen(object):
38
    """Generate a zsh script for given completion data."""
39
40
    def __init__(self, data, function_name='_brz', debug=False):
41
        self.data = data
42
        self.function_name = function_name
43
        self.debug = debug
44
45
    def script(self):
46
        return ("""\
47
#compdef brz bzr
48
49
%(function_name)s ()
50
{
51
    local ret=1
52
    local -a args
53
    args+=(
54
%(global-options)s
55
    )
56
57
    _arguments $args[@] && ret=0
58
59
    return ret
60
}
61
62
%(function_name)s
63
""" % {
64
            'global-options': self.global_options(),
65
            'function_name': self.function_name})
66
67
    def global_options(self):
68
        lines = []
69
        for (long, short, help) in self.data.global_options:
70
            lines.append(
71
                '      \'(%s%s)%s[%s]\'' % (
72
                    (short + ' ') if short else '',
73
                    long,
74
                    long,
75
                    help))
76
77
        return "\n".join(lines)
78
79
80
class CompletionData(object):
81
82
    def __init__(self):
83
        self.plugins = {}
84
        self.global_options = []
85
        self.commands = []
86
87
    def all_command_aliases(self):
88
        for c in self.commands:
89
            for a in c.aliases:
90
                yield a
91
92
93
class CommandData(object):
94
95
    def __init__(self, name):
96
        self.name = name
97
        self.aliases = [name]
98
        self.plugin = None
99
        self.options = []
100
        self.fixed_words = None
101
102
103
class PluginData(object):
104
105
    def __init__(self, name, version=None):
106
        if version is None:
107
            try:
108
                version = breezy.plugin.plugins()[name].__version__
109
            except:
110
                version = 'unknown'
111
        self.name = name
112
        self.version = version
113
114
    def __str__(self):
115
        if self.version == 'unknown':
116
            return self.name
117
        return '%s %s' % (self.name, self.version)
118
119
120
class OptionData(object):
121
122
    def __init__(self, name):
123
        self.name = name
124
        self.registry_keys = None
125
        self.error_messages = []
126
127
    def __str__(self):
128
        return self.name
129
130
    def __cmp__(self, other):
131
        return cmp(self.name, other.name)
132
133
    def __lt__(self, other):
134
        return self.name < other.name
135
136
137
class DataCollector(object):
138
139
    def __init__(self, no_plugins=False, selected_plugins=None):
140
        self.data = CompletionData()
141
        self.user_aliases = {}
142
        if no_plugins:
143
            self.selected_plugins = set()
144
        elif selected_plugins is None:
145
            self.selected_plugins = None
146
        else:
147
            self.selected_plugins = {x.replace('-', '_')
148
                                     for x in selected_plugins}
149
150
    def collect(self):
151
        self.global_options()
152
        self.aliases()
153
        self.commands()
154
        return self.data
155
156
    def global_options(self):
157
        for name, item in option.Option.OPTIONS.items():
158
            self.data.global_options.append(
159
                ('--' + item.name,
160
                 '-' + item.short_name() if item.short_name() else None,
161
                 item.help.rstrip()))
162
163
    def aliases(self):
164
        for alias, expansion in config.GlobalConfig().get_aliases().items():
165
            for token in cmdline.split(expansion):
166
                if not token.startswith("-"):
167
                    self.user_aliases.setdefault(token, set()).add(alias)
168
                    break
169
170
    def commands(self):
171
        for name in sorted(commands.all_command_names()):
172
            self.command(name)
173
174
    def command(self, name):
175
        cmd = commands.get_cmd_object(name)
176
        cmd_data = CommandData(name)
177
178
        plugin_name = cmd.plugin_name()
179
        if plugin_name is not None:
180
            if (self.selected_plugins is not None and
181
                    plugin not in self.selected_plugins):
182
                return None
183
            plugin_data = self.data.plugins.get(plugin_name)
184
            if plugin_data is None:
185
                plugin_data = PluginData(plugin_name)
186
                self.data.plugins[plugin_name] = plugin_data
187
            cmd_data.plugin = plugin_data
188
        self.data.commands.append(cmd_data)
189
190
        # Find all aliases to the command; both cmd-defined and user-defined.
191
        # We assume a user won't override one command with a different one,
192
        # but will choose completely new names or add options to existing
193
        # ones while maintaining the actual command name unchanged.
194
        cmd_data.aliases.extend(cmd.aliases)
195
        cmd_data.aliases.extend(sorted([useralias
196
                                        for cmdalias in cmd_data.aliases
197
                                        if cmdalias in self.user_aliases
198
                                        for useralias in self.user_aliases[cmdalias]
199
                                        if useralias not in cmd_data.aliases]))
200
201
        opts = cmd.options()
202
        for optname, opt in sorted(opts.items()):
203
            cmd_data.options.extend(self.option(opt))
204
205
        if 'help' == name or 'help' in cmd.aliases:
206
            cmd_data.fixed_words = ('($cmds %s)' %
207
                                    " ".join(sorted(help_topics.topic_registry.keys())))
208
209
        return cmd_data
210
211
    def option(self, opt):
212
        optswitches = {}
213
        parser = option.get_optparser([opt])
214
        parser = self.wrap_parser(optswitches, parser)
215
        optswitches.clear()
216
        opt.add_option(parser, opt.short_name())
217
        if isinstance(opt, option.RegistryOption) and opt.enum_switch:
218
            enum_switch = '--%s' % opt.name
219
            enum_data = optswitches.get(enum_switch)
220
            if enum_data:
221
                try:
222
                    enum_data.registry_keys = opt.registry.keys()
223
                except ImportError as e:
224
                    enum_data.error_messages.append(
225
                        "ERROR getting registry keys for '--%s': %s"
226
                        % (opt.name, str(e).split('\n')[0]))
227
        return sorted(optswitches.values())
228
229
    def wrap_container(self, optswitches, parser):
230
        def tweaked_add_option(*opts, **attrs):
231
            for name in opts:
232
                optswitches[name] = OptionData(name)
233
        parser.add_option = tweaked_add_option
234
        return parser
235
236
    def wrap_parser(self, optswitches, parser):
237
        orig_add_option_group = parser.add_option_group
238
239
        def tweaked_add_option_group(*opts, **attrs):
240
            return self.wrap_container(optswitches,
241
                                       orig_add_option_group(*opts, **attrs))
242
        parser.add_option_group = tweaked_add_option_group
243
        return self.wrap_container(optswitches, parser)
244
245
246
def zsh_completion_function(out, function_name="_brz",
247
                            debug=False,
248
                            no_plugins=False, selected_plugins=None):
249
    dc = DataCollector(no_plugins=no_plugins,
250
                       selected_plugins=selected_plugins)
251
    data = dc.collect()
252
    cg = ZshCodeGen(data, function_name=function_name, debug=debug)
253
    res = cg.script()
254
    out.write(res)
255
256
257
class cmd_zsh_completion(commands.Command):
258
    __doc__ = """Generate a shell function for zsh command line completion.
259
260
    This command generates a shell function which can be used by zsh to
261
    automatically complete the currently typed command when the user presses
262
    the completion key (usually tab).
263
264
    Commonly used like this:
265
        eval "`brz zsh -completion`"
266
    """
267
268
    takes_options = [
269
        option.Option("function-name", short_name="f", type=text_type, argname="name",
270
                      help="Name of the generated function (default: _brz)"),
271
        option.Option("debug", type=None, hidden=True,
272
                      help="Enable shell code useful for debugging"),
273
        option.ListOption("plugin", type=text_type, argname="name",
274
                          # param_name="selected_plugins", # doesn't work, bug #387117
275
                          help="Enable completions for the selected plugin"
276
                          + " (default: all plugins)"),
277
        ]
278
279
    def run(self, **kwargs):
280
        if 'plugin' in kwargs:
281
            # work around bug #387117 which prevents us from using param_name
282
            if len(kwargs['plugin']) > 0:
283
                kwargs['selected_plugins'] = kwargs['plugin']
284
            del kwargs['plugin']
285
        zsh_completion_function(sys.stdout, **kwargs)