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

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 7399
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
lp:~jelmer/brz/zsh-completion updated
7399. By Jelmer Vernooij

Drop dead code.

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

Subscribers

People subscribed via source and target branches