[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.

authorAlain Leufroy <alain.leufroyATgmailMYDOTcom>
changeset33d334a26e40
branchdefault
phasepublic
hiddenno
parent revision#b3a3bf6de071 [cosmetic] remove superfluous hash-bangs and the entry point in ``hgviewlib/application.py`` (closes #78002)
child revision#29e1b3984822 [core] improve mq support (closes #19194)
files modified by this revision
hgviewlib/curses/mainframe.py
hgviewlib/curses/utils.py
# 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.

diff --git a/hgviewlib/curses/mainframe.py b/hgviewlib/curses/mainframe.py
@@ -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 
diff --git a/hgviewlib/curses/utils.py b/hgviewlib/curses/utils.py
@@ -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."""