Merge lp:~jelmer/brz/zsh-completion into lp:brz
- zsh-completion
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Review via email: mp+373701@code.launchpad.net |
Commit message
Description of the change
Add really basic zsh completion plugin.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'breezy/plugins/zsh_completion' | |||
2 | === added file 'breezy/plugins/zsh_completion/__init__.py' | |||
3 | --- breezy/plugins/zsh_completion/__init__.py 1970-01-01 00:00:00 +0000 | |||
4 | +++ breezy/plugins/zsh_completion/__init__.py 2019-10-13 16:26:28 +0000 | |||
5 | @@ -0,0 +1,38 @@ | |||
6 | 1 | # Copyright (C) 2019 Jelmer Vernooij <jelmer@jelmer.uk> | ||
7 | 2 | # | ||
8 | 3 | # This program is free software; you can redistribute it and/or modify | ||
9 | 4 | # it under the terms of the GNU General Public License as published by | ||
10 | 5 | # the Free Software Foundation; either version 2 of the License, or | ||
11 | 6 | # (at your option) any later version. | ||
12 | 7 | # | ||
13 | 8 | # This program is distributed in the hope that it will be useful, | ||
14 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
15 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
16 | 11 | # GNU General Public License for more details. | ||
17 | 12 | # | ||
18 | 13 | # You should have received a copy of the GNU General Public License | ||
19 | 14 | # along with this program; if not, write to the Free Software | ||
20 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
21 | 16 | |||
22 | 17 | from __future__ import absolute_import | ||
23 | 18 | |||
24 | 19 | __doc__ = """Generate a shell function for zsh command line completion. | ||
25 | 20 | """ | ||
26 | 21 | |||
27 | 22 | from ... import commands, version_info # noqa: F401 | ||
28 | 23 | |||
29 | 24 | |||
30 | 25 | bzr_plugin_name = 'zsh_completion' | ||
31 | 26 | bzr_commands = ['zsh-completion'] | ||
32 | 27 | |||
33 | 28 | commands.plugin_cmds.register_lazy('cmd_zsh_completion', [], | ||
34 | 29 | __name__ + '.zshcomp') | ||
35 | 30 | |||
36 | 31 | |||
37 | 32 | def load_tests(loader, basic_tests, pattern): | ||
38 | 33 | testmod_names = [ | ||
39 | 34 | 'tests', | ||
40 | 35 | ] | ||
41 | 36 | basic_tests.addTest(loader.loadTestsFromModuleNames( | ||
42 | 37 | ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) | ||
43 | 38 | return basic_tests | ||
44 | 0 | 39 | ||
45 | === added directory 'breezy/plugins/zsh_completion/tests' | |||
46 | === added file 'breezy/plugins/zsh_completion/tests/__init__.py' | |||
47 | --- breezy/plugins/zsh_completion/tests/__init__.py 1970-01-01 00:00:00 +0000 | |||
48 | +++ breezy/plugins/zsh_completion/tests/__init__.py 2019-10-13 16:26:28 +0000 | |||
49 | @@ -0,0 +1,24 @@ | |||
50 | 1 | # Copyright (C) 2010 by Canonical Ltd | ||
51 | 2 | # | ||
52 | 3 | # This program is free software; you can redistribute it and/or modify | ||
53 | 4 | # it under the terms of the GNU General Public License as published by | ||
54 | 5 | # the Free Software Foundation; either version 2 of the License, or | ||
55 | 6 | # (at your option) any later version. | ||
56 | 7 | # | ||
57 | 8 | # This program is distributed in the hope that it will be useful, | ||
58 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
59 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
60 | 11 | # GNU General Public License for more details. | ||
61 | 12 | # | ||
62 | 13 | # You should have received a copy of the GNU General Public License | ||
63 | 14 | # along with this program; if not, write to the Free Software | ||
64 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
65 | 16 | |||
66 | 17 | |||
67 | 18 | def load_tests(loader, basic_tests, pattern): | ||
68 | 19 | testmod_names = [ | ||
69 | 20 | 'test_zshcomp', | ||
70 | 21 | ] | ||
71 | 22 | basic_tests.addTest(loader.loadTestsFromModuleNames( | ||
72 | 23 | ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) | ||
73 | 24 | return basic_tests | ||
74 | 0 | 25 | ||
75 | === added file 'breezy/plugins/zsh_completion/tests/test_zshcomp.py' | |||
76 | --- breezy/plugins/zsh_completion/tests/test_zshcomp.py 1970-01-01 00:00:00 +0000 | |||
77 | +++ breezy/plugins/zsh_completion/tests/test_zshcomp.py 2019-10-13 16:26:28 +0000 | |||
78 | @@ -0,0 +1,26 @@ | |||
79 | 1 | # Copyright (C) 2010 by Canonical Ltd | ||
80 | 2 | # | ||
81 | 3 | # This program is free software; you can redistribute it and/or modify | ||
82 | 4 | # it under the terms of the GNU General Public License as published by | ||
83 | 5 | # the Free Software Foundation; either version 2 of the License, or | ||
84 | 6 | # (at your option) any later version. | ||
85 | 7 | # | ||
86 | 8 | # This program is distributed in the hope that it will be useful, | ||
87 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
88 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
89 | 11 | # GNU General Public License for more details. | ||
90 | 12 | # | ||
91 | 13 | # You should have received a copy of the GNU General Public License | ||
92 | 14 | # along with this program; if not, write to the Free Software | ||
93 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
94 | 16 | |||
95 | 17 | import sys | ||
96 | 18 | |||
97 | 19 | import breezy | ||
98 | 20 | from breezy import tests | ||
99 | 21 | |||
100 | 22 | |||
101 | 23 | class BlackboxTests(tests.TestCaseWithMemoryTransport): | ||
102 | 24 | |||
103 | 25 | def test_zsh_completion(self): | ||
104 | 26 | self.run_bzr("zsh-completion", encoding="utf-8") | ||
105 | 0 | 27 | ||
106 | === added file 'breezy/plugins/zsh_completion/zshcomp.py' | |||
107 | --- breezy/plugins/zsh_completion/zshcomp.py 1970-01-01 00:00:00 +0000 | |||
108 | +++ breezy/plugins/zsh_completion/zshcomp.py 2019-10-13 16:26:28 +0000 | |||
109 | @@ -0,0 +1,285 @@ | |||
110 | 1 | #!/usr/bin/env python | ||
111 | 2 | |||
112 | 3 | # Copyright (C) 2009, 2010 Canonical Ltd | ||
113 | 4 | # | ||
114 | 5 | # This program is free software; you can redistribute it and/or modify | ||
115 | 6 | # it under the terms of the GNU General Public License as published by | ||
116 | 7 | # the Free Software Foundation; either version 2 of the License, or | ||
117 | 8 | # (at your option) any later version. | ||
118 | 9 | # | ||
119 | 10 | # This program is distributed in the hope that it will be useful, | ||
120 | 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
121 | 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
122 | 13 | # GNU General Public License for more details. | ||
123 | 14 | # | ||
124 | 15 | # You should have received a copy of the GNU General Public License | ||
125 | 16 | # along with this program; if not, write to the Free Software | ||
126 | 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
127 | 18 | |||
128 | 19 | from __future__ import absolute_import | ||
129 | 20 | |||
130 | 21 | from ... import ( | ||
131 | 22 | cmdline, | ||
132 | 23 | commands, | ||
133 | 24 | config, | ||
134 | 25 | help_topics, | ||
135 | 26 | option, | ||
136 | 27 | plugin, | ||
137 | 28 | ) | ||
138 | 29 | from ...sixish import ( | ||
139 | 30 | text_type, | ||
140 | 31 | ) | ||
141 | 32 | import breezy | ||
142 | 33 | import re | ||
143 | 34 | import sys | ||
144 | 35 | |||
145 | 36 | |||
146 | 37 | class ZshCodeGen(object): | ||
147 | 38 | """Generate a zsh script for given completion data.""" | ||
148 | 39 | |||
149 | 40 | def __init__(self, data, function_name='_brz', debug=False): | ||
150 | 41 | self.data = data | ||
151 | 42 | self.function_name = function_name | ||
152 | 43 | self.debug = debug | ||
153 | 44 | |||
154 | 45 | def script(self): | ||
155 | 46 | return ("""\ | ||
156 | 47 | #compdef brz bzr | ||
157 | 48 | |||
158 | 49 | %(function_name)s () | ||
159 | 50 | { | ||
160 | 51 | local ret=1 | ||
161 | 52 | local -a args | ||
162 | 53 | args+=( | ||
163 | 54 | %(global-options)s | ||
164 | 55 | ) | ||
165 | 56 | |||
166 | 57 | _arguments $args[@] && ret=0 | ||
167 | 58 | |||
168 | 59 | return ret | ||
169 | 60 | } | ||
170 | 61 | |||
171 | 62 | %(function_name)s | ||
172 | 63 | """ % { | ||
173 | 64 | 'global-options': self.global_options(), | ||
174 | 65 | 'function_name': self.function_name}) | ||
175 | 66 | |||
176 | 67 | def global_options(self): | ||
177 | 68 | lines = [] | ||
178 | 69 | for (long, short, help) in self.data.global_options: | ||
179 | 70 | lines.append( | ||
180 | 71 | ' \'(%s%s)%s[%s]\'' % ( | ||
181 | 72 | (short + ' ') if short else '', | ||
182 | 73 | long, | ||
183 | 74 | long, | ||
184 | 75 | help)) | ||
185 | 76 | |||
186 | 77 | return "\n".join(lines) | ||
187 | 78 | |||
188 | 79 | |||
189 | 80 | class CompletionData(object): | ||
190 | 81 | |||
191 | 82 | def __init__(self): | ||
192 | 83 | self.plugins = {} | ||
193 | 84 | self.global_options = [] | ||
194 | 85 | self.commands = [] | ||
195 | 86 | |||
196 | 87 | def all_command_aliases(self): | ||
197 | 88 | for c in self.commands: | ||
198 | 89 | for a in c.aliases: | ||
199 | 90 | yield a | ||
200 | 91 | |||
201 | 92 | |||
202 | 93 | class CommandData(object): | ||
203 | 94 | |||
204 | 95 | def __init__(self, name): | ||
205 | 96 | self.name = name | ||
206 | 97 | self.aliases = [name] | ||
207 | 98 | self.plugin = None | ||
208 | 99 | self.options = [] | ||
209 | 100 | self.fixed_words = None | ||
210 | 101 | |||
211 | 102 | |||
212 | 103 | class PluginData(object): | ||
213 | 104 | |||
214 | 105 | def __init__(self, name, version=None): | ||
215 | 106 | if version is None: | ||
216 | 107 | try: | ||
217 | 108 | version = breezy.plugin.plugins()[name].__version__ | ||
218 | 109 | except: | ||
219 | 110 | version = 'unknown' | ||
220 | 111 | self.name = name | ||
221 | 112 | self.version = version | ||
222 | 113 | |||
223 | 114 | def __str__(self): | ||
224 | 115 | if self.version == 'unknown': | ||
225 | 116 | return self.name | ||
226 | 117 | return '%s %s' % (self.name, self.version) | ||
227 | 118 | |||
228 | 119 | |||
229 | 120 | class OptionData(object): | ||
230 | 121 | |||
231 | 122 | def __init__(self, name): | ||
232 | 123 | self.name = name | ||
233 | 124 | self.registry_keys = None | ||
234 | 125 | self.error_messages = [] | ||
235 | 126 | |||
236 | 127 | def __str__(self): | ||
237 | 128 | return self.name | ||
238 | 129 | |||
239 | 130 | def __cmp__(self, other): | ||
240 | 131 | return cmp(self.name, other.name) | ||
241 | 132 | |||
242 | 133 | def __lt__(self, other): | ||
243 | 134 | return self.name < other.name | ||
244 | 135 | |||
245 | 136 | |||
246 | 137 | class DataCollector(object): | ||
247 | 138 | |||
248 | 139 | def __init__(self, no_plugins=False, selected_plugins=None): | ||
249 | 140 | self.data = CompletionData() | ||
250 | 141 | self.user_aliases = {} | ||
251 | 142 | if no_plugins: | ||
252 | 143 | self.selected_plugins = set() | ||
253 | 144 | elif selected_plugins is None: | ||
254 | 145 | self.selected_plugins = None | ||
255 | 146 | else: | ||
256 | 147 | self.selected_plugins = {x.replace('-', '_') | ||
257 | 148 | for x in selected_plugins} | ||
258 | 149 | |||
259 | 150 | def collect(self): | ||
260 | 151 | self.global_options() | ||
261 | 152 | self.aliases() | ||
262 | 153 | self.commands() | ||
263 | 154 | return self.data | ||
264 | 155 | |||
265 | 156 | def global_options(self): | ||
266 | 157 | for name, item in option.Option.OPTIONS.items(): | ||
267 | 158 | self.data.global_options.append( | ||
268 | 159 | ('--' + item.name, | ||
269 | 160 | '-' + item.short_name() if item.short_name() else None, | ||
270 | 161 | item.help.rstrip())) | ||
271 | 162 | |||
272 | 163 | def aliases(self): | ||
273 | 164 | for alias, expansion in config.GlobalConfig().get_aliases().items(): | ||
274 | 165 | for token in cmdline.split(expansion): | ||
275 | 166 | if not token.startswith("-"): | ||
276 | 167 | self.user_aliases.setdefault(token, set()).add(alias) | ||
277 | 168 | break | ||
278 | 169 | |||
279 | 170 | def commands(self): | ||
280 | 171 | for name in sorted(commands.all_command_names()): | ||
281 | 172 | self.command(name) | ||
282 | 173 | |||
283 | 174 | def command(self, name): | ||
284 | 175 | cmd = commands.get_cmd_object(name) | ||
285 | 176 | cmd_data = CommandData(name) | ||
286 | 177 | |||
287 | 178 | plugin_name = cmd.plugin_name() | ||
288 | 179 | if plugin_name is not None: | ||
289 | 180 | if (self.selected_plugins is not None and | ||
290 | 181 | plugin not in self.selected_plugins): | ||
291 | 182 | return None | ||
292 | 183 | plugin_data = self.data.plugins.get(plugin_name) | ||
293 | 184 | if plugin_data is None: | ||
294 | 185 | plugin_data = PluginData(plugin_name) | ||
295 | 186 | self.data.plugins[plugin_name] = plugin_data | ||
296 | 187 | cmd_data.plugin = plugin_data | ||
297 | 188 | self.data.commands.append(cmd_data) | ||
298 | 189 | |||
299 | 190 | # Find all aliases to the command; both cmd-defined and user-defined. | ||
300 | 191 | # We assume a user won't override one command with a different one, | ||
301 | 192 | # but will choose completely new names or add options to existing | ||
302 | 193 | # ones while maintaining the actual command name unchanged. | ||
303 | 194 | cmd_data.aliases.extend(cmd.aliases) | ||
304 | 195 | cmd_data.aliases.extend(sorted([useralias | ||
305 | 196 | for cmdalias in cmd_data.aliases | ||
306 | 197 | if cmdalias in self.user_aliases | ||
307 | 198 | for useralias in self.user_aliases[cmdalias] | ||
308 | 199 | if useralias not in cmd_data.aliases])) | ||
309 | 200 | |||
310 | 201 | opts = cmd.options() | ||
311 | 202 | for optname, opt in sorted(opts.items()): | ||
312 | 203 | cmd_data.options.extend(self.option(opt)) | ||
313 | 204 | |||
314 | 205 | if 'help' == name or 'help' in cmd.aliases: | ||
315 | 206 | cmd_data.fixed_words = ('($cmds %s)' % | ||
316 | 207 | " ".join(sorted(help_topics.topic_registry.keys()))) | ||
317 | 208 | |||
318 | 209 | return cmd_data | ||
319 | 210 | |||
320 | 211 | def option(self, opt): | ||
321 | 212 | optswitches = {} | ||
322 | 213 | parser = option.get_optparser([opt]) | ||
323 | 214 | parser = self.wrap_parser(optswitches, parser) | ||
324 | 215 | optswitches.clear() | ||
325 | 216 | opt.add_option(parser, opt.short_name()) | ||
326 | 217 | if isinstance(opt, option.RegistryOption) and opt.enum_switch: | ||
327 | 218 | enum_switch = '--%s' % opt.name | ||
328 | 219 | enum_data = optswitches.get(enum_switch) | ||
329 | 220 | if enum_data: | ||
330 | 221 | try: | ||
331 | 222 | enum_data.registry_keys = opt.registry.keys() | ||
332 | 223 | except ImportError as e: | ||
333 | 224 | enum_data.error_messages.append( | ||
334 | 225 | "ERROR getting registry keys for '--%s': %s" | ||
335 | 226 | % (opt.name, str(e).split('\n')[0])) | ||
336 | 227 | return sorted(optswitches.values()) | ||
337 | 228 | |||
338 | 229 | def wrap_container(self, optswitches, parser): | ||
339 | 230 | def tweaked_add_option(*opts, **attrs): | ||
340 | 231 | for name in opts: | ||
341 | 232 | optswitches[name] = OptionData(name) | ||
342 | 233 | parser.add_option = tweaked_add_option | ||
343 | 234 | return parser | ||
344 | 235 | |||
345 | 236 | def wrap_parser(self, optswitches, parser): | ||
346 | 237 | orig_add_option_group = parser.add_option_group | ||
347 | 238 | |||
348 | 239 | def tweaked_add_option_group(*opts, **attrs): | ||
349 | 240 | return self.wrap_container(optswitches, | ||
350 | 241 | orig_add_option_group(*opts, **attrs)) | ||
351 | 242 | parser.add_option_group = tweaked_add_option_group | ||
352 | 243 | return self.wrap_container(optswitches, parser) | ||
353 | 244 | |||
354 | 245 | |||
355 | 246 | def zsh_completion_function(out, function_name="_brz", | ||
356 | 247 | debug=False, | ||
357 | 248 | no_plugins=False, selected_plugins=None): | ||
358 | 249 | dc = DataCollector(no_plugins=no_plugins, | ||
359 | 250 | selected_plugins=selected_plugins) | ||
360 | 251 | data = dc.collect() | ||
361 | 252 | cg = ZshCodeGen(data, function_name=function_name, debug=debug) | ||
362 | 253 | res = cg.script() | ||
363 | 254 | out.write(res) | ||
364 | 255 | |||
365 | 256 | |||
366 | 257 | class cmd_zsh_completion(commands.Command): | ||
367 | 258 | __doc__ = """Generate a shell function for zsh command line completion. | ||
368 | 259 | |||
369 | 260 | This command generates a shell function which can be used by zsh to | ||
370 | 261 | automatically complete the currently typed command when the user presses | ||
371 | 262 | the completion key (usually tab). | ||
372 | 263 | |||
373 | 264 | Commonly used like this: | ||
374 | 265 | eval "`brz zsh -completion`" | ||
375 | 266 | """ | ||
376 | 267 | |||
377 | 268 | takes_options = [ | ||
378 | 269 | option.Option("function-name", short_name="f", type=text_type, argname="name", | ||
379 | 270 | help="Name of the generated function (default: _brz)"), | ||
380 | 271 | option.Option("debug", type=None, hidden=True, | ||
381 | 272 | help="Enable shell code useful for debugging"), | ||
382 | 273 | option.ListOption("plugin", type=text_type, argname="name", | ||
383 | 274 | # param_name="selected_plugins", # doesn't work, bug #387117 | ||
384 | 275 | help="Enable completions for the selected plugin" | ||
385 | 276 | + " (default: all plugins)"), | ||
386 | 277 | ] | ||
387 | 278 | |||
388 | 279 | def run(self, **kwargs): | ||
389 | 280 | if 'plugin' in kwargs: | ||
390 | 281 | # work around bug #387117 which prevents us from using param_name | ||
391 | 282 | if len(kwargs['plugin']) > 0: | ||
392 | 283 | kwargs['selected_plugins'] = kwargs['plugin'] | ||
393 | 284 | del kwargs['plugin'] | ||
394 | 285 | zsh_completion_function(sys.stdout, **kwargs) |
Okay, makes sense to include the code. See one inline query.