# HG changeset patch
# User Alain Leufroy <alain.leufroyATgmailMYDOTcom>
# Date 1315835868 -7200
# Mon Sep 12 15:57:48 2011 +0200
# Node ID 33d334a26e40dec8eb95fddc69aa1518a9deaf1f
# Parent b3a3bf6de071dc49f43b290b2a1ed1a98bdf488f
[console] add history and completion for command (closes #84733)
* tab: completion of the command. It takes into account the text in the edit area
until the cursor.
The completion system allows to use widcard. So "*context" will be
completed by (hide-context, maximize-context, show-context)
* up|down|ctrl p|ctrl n: command history
* starting or endind the command name with '?' displays the command help.
So, "quit?" == "?quit" == "help quit"
:Note:
* I've tried to connect cmd.Cmd to urwid => poor result because only a few part
of Cmd may be usefull, output coloring is not really possible and I've to
refactorize the actual commands storage.
* Input and Output are not standard, so readline is not really usefull here.
# User Alain Leufroy <alain.leufroyATgmailMYDOTcom>
# Date 1315835868 -7200
# Mon Sep 12 15:57:48 2011 +0200
# Node ID 33d334a26e40dec8eb95fddc69aa1518a9deaf1f
# Parent b3a3bf6de071dc49f43b290b2a1ed1a98bdf488f
[console] add history and completion for command (closes #84733)
* tab: completion of the command. It takes into account the text in the edit area
until the cursor.
The completion system allows to use widcard. So "*context" will be
completed by (hide-context, maximize-context, show-context)
* up|down|ctrl p|ctrl n: command history
* starting or endind the command name with '?' displays the command help.
So, "quit?" == "?quit" == "help quit"
:Note:
* I've tried to connect cmd.Cmd to urwid => poor result because only a few part
of Cmd may be usefull, output coloring is not really possible and I've to
refactorize the actual commands storage.
* Input and Output are not standard, so readline is not really usefull here.
@@ -43,12 +43,12 @@
1 from urwid.signals import connect_signal, emit_signal 2 3 from hgviewlib.curses import helpviewer 4 from hgviewlib.curses import (CommandArg as CA, help_command, 5 register_command, unregister_command, 6 - emit_command, connect_command, 7 - hg_command_map) 8 + emit_command, connect_command, complete_command, 9 + hg_command_map, History) 10 11 def quitall(): 12 """ 13 usage: quall 14
@@ -75,11 +75,11 @@
15 footer = Footer() 16 self._bodies = {name:body} 17 self._visible = name 18 super(MainFrame, self).__init__(body=body, header=None, footer=footer, 19 *args, **kwargs) 20 - connect_signal(footer, 'end command', 21 + connect_signal(footer, 'end command', 22 lambda status: self.set_focus('body')) 23 24 def register_commands(self): 25 """Register specific command""" 26 register_command(('quit','q'), 'Close the current pane.')
@@ -186,29 +186,68 @@
27 def __init__(self, *args, **kwargs): 28 super(Footer, self).__init__( 29 urwid.Edit('type ":help<Enter>" for information'), 30 'INFO', *args, **kwargs) 31 connect_signal(self, 'start command', self.start_command) 32 + self.previous_keypress = None 33 + self._history = History() 34 + self._complete = History() 35 36 def start_command(self, key): 37 """start looking for user's command""" 38 # just for fun 39 label = {'f5':'command: ', ':':':', 'meta x':'M-x '}[key] 40 self.set('default', label, '') 41 42 - 43 def keypress(self, size, key): 44 "allow subclasses to intercept keystrokes" 45 if hg_command_map[key] == 'validate': 46 self.set('default') 47 - status = self.call_command() 48 - emit_signal(self, 'end command', not status) 49 + cmdline = self.call_command() 50 + self._history.append(cmdline) 51 + emit_signal(self, 'end command', bool(cmdline)) 52 elif hg_command_map[key] == 'escape': 53 self.set('default', '', '') 54 emit_signal(self, 'end command', False) 55 + elif key == 'tab': # hard coded :/ 56 + self.complete() 57 + elif key == 'up' or key == 'ctrl p': # hard coded :/ 58 + self.history(False) 59 + elif key == 'down' or key == 'ctrl n': # hard coded :/ 60 + self.history(True) 61 else: 62 + self.previous_keypress = key 63 return super(Footer, self).keypress(size, key) 64 + self.previous_keypress = key 65 + 66 + def complete(self): 67 + """ 68 + Lookup for text in the edit area (until the cursor) and complete with 69 + available command names (one per call). Calling mutiple times 70 + consequently will loop over all candidates. 71 + """ 72 + if self.previous_keypress != 'tab': # hard coded :/ 73 + line = self.get_edit_text()[:self.edit_pos] 74 + self._complete[:] = History(complete_command(line), line) 75 + self._complete.reset_position() 76 + if self.complete: 77 + self.set_edit_text(self._complete.get_next()) 78 + if len(self._complete) == 1: 79 + self.set_edit_pos(len(self.edit_text)) 80 + 81 + def history(self, next=True): 82 + """ 83 + Remind the commands history to the edit area. Calling mutiple times 84 + consequently will loop over all history entries. 85 + 86 + """ 87 + # key are hard coded :/ 88 + if self.previous_keypress not in ('up', 'down', 'ctrl p', 'ctrl n'): 89 + self._history[0] = self.get_edit_text() 90 + self._history.reset_position() 91 + text = self._history.get_next() if next else self._history.get_prev() 92 + self.set_edit_text(text) 93 94 def set(self, style=None, caption=None, edit=None): 95 '''Set the footer content. 96 97 :param style: a string that corresponds to a palette entry name
@@ -228,14 +267,21 @@
98 ''' 99 cmdline = self.get_edit_text() 100 if not cmdline: 101 self.footer.set('default', '', '') 102 return 103 + cmdline = cmdline.strip() 104 + if cmdline.endswith('?'): 105 + cmdline = 'help %s' % cmdline[:-1].split(None, 1)[0] 106 + elif cmdline.startswith('?'): 107 + cmdline = 'help %s' % cmdline[1:].split(None, 1)[0] 108 try: 109 emit_command(cmdline) 110 self.set('INFO') 111 except urwid.ExitMainLoop: # exit, so do not catch this 112 raise 113 except Exception, err: 114 logging.warn(err.__class__.__name__ + ': %s', str(err)) 115 logging.debug('Exception on: "%s"', cmdline, exc_info=True) 116 + else: 117 + return cmdline 118
@@ -18,18 +18,20 @@
119 """ 120 A Module that contains usefull utilities. 121 """ 122 123 import shlex 124 +import fnmatch 125 from collections import namedtuple 126 127 from urwid.command_map import CommandMap 128 from hgviewlib.curses.exceptions import UnknownCommand, RegisterCommandError 129 130 __all__ = ['register_command', 'unregister_command', 'connect_command', 131 - 'disconnect_command', 'emit_command', 'help_command', 'CommandArg', 132 - 'hg_command_map', 133 + 'disconnect_command', 'emit_command', 'help_command', 134 + 'complete_command', 'CommandArg', 'hg_command_map', 135 + 'History', 136 ] 137 138 139 # ____________________________________________________________________ commands 140 CommandEntry = namedtuple('CommandEntry', ('func', 'args', 'kwargs'))
@@ -219,22 +221,71 @@
141 def helps(self): 142 """Return a generator that gives (name, help) for each command""" 143 for name in sorted(self._helps.keys()): 144 yield name, self.help(name) 145 146 + def complete(self, line): 147 + """ 148 + Return command name candidates that complete ``line``. 149 + It uses fnmatch to match candidates, so ``line`` may contains 150 + wildcards. 151 + """ 152 + if not line: 153 + return self._helps.keys() 154 + line = tuple(line.split(None, 1)) 155 + out = line 156 + if len(line) == 1: 157 + cmd = line[0] + '*' 158 + return tuple(sorted(fnmatch.filter(self._args, cmd))) 159 + 160 # Instanciate a Commands object to handle command from a global scope. 161 #pylint: disable-msg=C0103 162 _commands = Commands() 163 register_command = _commands.register 164 unregister_command = _commands.unregister 165 connect_command = _commands.connect 166 disconnect_command = _commands.disconnect 167 emit_command = _commands.emit 168 help_command = _commands.help 169 help_commands = _commands.helps 170 +complete_command = _commands.complete 171 #pylint: enable-msg=C0103 172 173 +class History(list): 174 + def __init__(self, list=None, current=None): 175 + super(History, self).__init__(list or ()) 176 + self.insert(0, current) 177 + self.position = 0 178 + 179 + def get(self, position, default=None): 180 + """ 181 + Return the history entry at `position` or `default` if not found. 182 + """ 183 + try: 184 + return self[position] 185 + except IndexError: 186 + return default 187 + 188 + def get_next(self, default=None): 189 + """Return the next entry of the history""" 190 + self.position += 1 191 + self.position %= len(self) 192 + return self.get(self.position, default) 193 + 194 + def get_prev(self, default=None): 195 + """Return the previous entry of the history""" 196 + self.position -= 1 197 + self.position %= len(self) 198 + return self.get(self.position, default) 199 + 200 + def reset_position(self): 201 + """reset the position of the history pointer""" 202 + self.position = 0 203 + 204 + def set_current(self, current): 205 + self[0] = current 206 + 207 # _________________________________________________________________ command map 208 209 210 class HgCommandMap(object): 211 """Map keys to more expliite action names."""