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