Merge lp:~jelmer/brz/zsh-completion into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/zsh-completion
Merge into: lp:brz
Diff against target: 394 lines (+373/-0)
4 files modified
breezy/plugins/zsh_completion/__init__.py (+38/-0)
breezy/plugins/zsh_completion/tests/__init__.py (+24/-0)
breezy/plugins/zsh_completion/tests/test_zshcomp.py (+26/-0)
breezy/plugins/zsh_completion/zshcomp.py (+285/-0)
To merge this branch: bzr merge lp:~jelmer/brz/zsh-completion
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+373701@code.launchpad.net

Description of the change

Add really basic zsh completion plugin.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Okay, makes sense to include the code. See one inline query.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'breezy/plugins/zsh_completion'
=== added file 'breezy/plugins/zsh_completion/__init__.py'
--- breezy/plugins/zsh_completion/__init__.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/zsh_completion/__init__.py 2019-10-13 16:26:28 +0000
@@ -0,0 +1,38 @@
1# Copyright (C) 2019 Jelmer Vernooij <jelmer@jelmer.uk>
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
17from __future__ import absolute_import
18
19__doc__ = """Generate a shell function for zsh command line completion.
20"""
21
22from ... import commands, version_info # noqa: F401
23
24
25bzr_plugin_name = 'zsh_completion'
26bzr_commands = ['zsh-completion']
27
28commands.plugin_cmds.register_lazy('cmd_zsh_completion', [],
29 __name__ + '.zshcomp')
30
31
32def load_tests(loader, basic_tests, pattern):
33 testmod_names = [
34 'tests',
35 ]
36 basic_tests.addTest(loader.loadTestsFromModuleNames(
37 ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
38 return basic_tests
039
=== added directory 'breezy/plugins/zsh_completion/tests'
=== added file 'breezy/plugins/zsh_completion/tests/__init__.py'
--- breezy/plugins/zsh_completion/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/zsh_completion/tests/__init__.py 2019-10-13 16:26:28 +0000
@@ -0,0 +1,24 @@
1# Copyright (C) 2010 by 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
18def load_tests(loader, basic_tests, pattern):
19 testmod_names = [
20 'test_zshcomp',
21 ]
22 basic_tests.addTest(loader.loadTestsFromModuleNames(
23 ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
24 return basic_tests
025
=== added file 'breezy/plugins/zsh_completion/tests/test_zshcomp.py'
--- breezy/plugins/zsh_completion/tests/test_zshcomp.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/zsh_completion/tests/test_zshcomp.py 2019-10-13 16:26:28 +0000
@@ -0,0 +1,26 @@
1# Copyright (C) 2010 by 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
17import sys
18
19import breezy
20from breezy import tests
21
22
23class BlackboxTests(tests.TestCaseWithMemoryTransport):
24
25 def test_zsh_completion(self):
26 self.run_bzr("zsh-completion", encoding="utf-8")
027
=== added file 'breezy/plugins/zsh_completion/zshcomp.py'
--- breezy/plugins/zsh_completion/zshcomp.py 1970-01-01 00:00:00 +0000
+++ breezy/plugins/zsh_completion/zshcomp.py 2019-10-13 16:26:28 +0000
@@ -0,0 +1,285 @@
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
19from __future__ import absolute_import
20
21from ... import (
22 cmdline,
23 commands,
24 config,
25 help_topics,
26 option,
27 plugin,
28)
29from ...sixish import (
30 text_type,
31 )
32import breezy
33import re
34import sys
35
36
37class 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
80class 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
93class 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
103class 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
120class 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
137class 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
246def 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
257class 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)

Subscribers

People subscribed via source and target branches