diff -r 000000000000 -r ca70ae20a155 src/extras/pyrepl/reader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/extras/pyrepl/reader.py Tue Feb 16 10:07:05 2010 +0530 @@ -0,0 +1,582 @@ +# Copyright 2000-2004 Michael Hudson mwh@python.net +# +# All Rights Reserved +# +# Portions Copyright (c) 2005 Nokia Corporation +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import types +try: + import unicodedata +except ImportError: + import dumbunicodedata as unicodedata +from pyrepl import commands +import ascii +from pyrepl import input + +def _make_unctrl_map(): + uc_map = {} + for c in map(unichr, range(256)): + if unicodedata.category(c)[0] <> 'C': + uc_map[c] = c + for i in range(32): + c = unichr(i) + uc_map[c] = u'^' + unichr(ord('A') + i - 1) + uc_map['\177'] = u'^?' + for i in range(256): + c = unichr(i) + if not uc_map.has_key(c): + uc_map[c] = u'\\%03o'%i + return uc_map + +# disp_str proved to be a bottleneck for large inputs, so it's been +# rewritten in C; it's not required though. +try: + raise ImportError # currently it's borked by the unicode support + + from _pyrepl_utils import disp_str, init_unctrl_map + + init_unctrl_map(_make_unctrl_map()) + + del init_unctrl_map +except ImportError: + def _my_unctrl(c, u=_make_unctrl_map()): +# import unicodedata + if c in u: + return u[c] + else: + if unicodedata.category(c).startswith('C'): + return '\u%04x'%(ord(c),) + else: + return c + + def disp_str(buffer, join=''.join, uc=_my_unctrl): + """ disp_str(buffer:string) -> (string, [int]) + + Return the string that should be the printed represenation of + |buffer| and a list detailing where the characters of |buffer| + get used up. E.g.: + + >>> disp_str(chr(3)) + ('^C', [1, 0]) + + the list always contains 0s or 1s at present; it could conceivably + go higher as and when unicode support happens.""" + s = map(uc, buffer) + return (join(s), + map(ord, join(map(lambda x:'\001'+(len(x)-1)*'\000', s)))) + + del _my_unctrl + +del _make_unctrl_map + +# syntax classes: + +[SYNTAX_WHITESPACE, + SYNTAX_WORD, + SYNTAX_SYMBOL] = range(3) + +def make_default_syntax_table(): + # XXX perhaps should use some unicodedata here? + st = {} + for c in map(unichr, range(256)): + st[c] = SYNTAX_SYMBOL + for c in [a for a in map(unichr, range(256)) if a.isalpha()]: + st[c] = SYNTAX_WORD + st[u'\n'] = st[u' '] = SYNTAX_WHITESPACE + return st + +default_keymap = tuple( + [(r'\C-a', 'beginning-of-line'), + (r'\C-b', 'left'), + (r'\C-c', 'interrupt'), + (r'\C-d', 'delete'), + (r'\C-e', 'end-of-line'), + (r'\C-f', 'right'), + (r'\C-g', 'cancel'), + (r'\C-h', 'backspace'), + (r'\C-j', 'accept'), + (r'\', 'accept'), + (r'\C-k', 'kill-line'), + (r'\C-l', 'clear-screen'), + (r'\C-m', 'accept'), + (r'\C-q', 'quoted-insert'), + (r'\C-t', 'transpose-characters'), + (r'\C-u', 'unix-line-discard'), + (r'\C-v', 'quoted-insert'), + (r'\C-w', 'unix-word-rubout'), + (r'\C-x\C-s', 'save-history'), + (r'\C-x\C-u', 'upcase-region'), + (r'\C-y', 'yank'), + (r'\C-z', 'suspend'), + + (r'\M-b', 'backward-word'), + (r'\M-c', 'capitalize-word'), + (r'\M-d', 'kill-word'), + (r'\M-f', 'forward-word'), + (r'\M-l', 'downcase-word'), + (r'\M-t', 'transpose-words'), + (r'\M-u', 'upcase-word'), + (r'\M-y', 'yank-pop'), + (r'\M--', 'digit-arg'), + (r'\M-0', 'digit-arg'), + (r'\M-1', 'digit-arg'), + (r'\M-2', 'digit-arg'), + (r'\M-3', 'digit-arg'), + (r'\M-4', 'digit-arg'), + (r'\M-5', 'digit-arg'), + (r'\M-6', 'digit-arg'), + (r'\M-7', 'digit-arg'), + (r'\M-8', 'digit-arg'), + (r'\M-9', 'digit-arg'), + (r'\M-\n', 'insert-nl'), + ('\\\\', 'self-insert')] + \ + [(c, 'self-insert') + for c in map(chr, range(32, 127)) if c <> '\\'] + \ + [(c, 'self-insert') + for c in map(chr, range(128, 256)) if c.isalpha()] + \ + [(r'\', 'up'), + (r'\', 'down'), + (r'\', 'left'), + (r'\', 'right'), + (r'\', 'quoted-insert'), + (r'\', 'delete'), + (r'\', 'backspace'), + (r'\M-\', 'backward-kill-word'), + (r'\', 'end'), + (r'\', 'home'), + (r'\', 'help'), + (r'\EOF', 'end'), # the entries in the terminfo database for xterms + (r'\EOH', 'home'), # seem to be wrong. this is a less than ideal + # workaround + ]) + +del c # from the listcomps + +class Reader(object): + """The Reader class implements the bare bones of a command reader, + handling such details as editing and cursor motion. What it does + not support are such things as completion or history support - + these are implemented elsewhere. + + Instance variables of note include: + + * buffer: + A *list* (*not* a string at the moment :-) containing all the + characters that have been entered. + * console: + Hopefully encapsulates the OS dependent stuff. + * pos: + A 0-based index into `buffer' for where the insertion point + is. + * screeninfo: + Ahem. This list contains some info needed to move the + insertion point around reasonably efficiently. I'd like to + get rid of it, because its contents are obtuse (to put it + mildly) but I haven't worked out if that is possible yet. + * cxy, lxy: + the position of the insertion point in screen ... XXX + * syntax_table: + Dictionary mapping characters to `syntax class'; read the + emacs docs to see what this means :-) + * commands: + Dictionary mapping command names to command classes. + * arg: + The emacs-style prefix argument. It will be None if no such + argument has been provided. + * dirty: + True if we need to refresh the display. + * kill_ring: + The emacs-style kill-ring; manipulated with yank & yank-pop + * ps1, ps2, ps3, ps4: + prompts. ps1 is the prompt for a one-line input; for a + multiline input it looks like: + ps2> first line of input goes here + ps3> second and further + ps3> lines get ps3 + ... + ps4> and the last one gets ps4 + As with the usual top-level, you can set these to instances if + you like; str() will be called on them (once) at the beginning + of each command. Don't put really long or newline containing + strings here, please! + This is just the default policy; you can change it freely by + overriding get_prompt() (and indeed some standard subclasses + do). + * finished: + handle1 will set this to a true value if a command signals + that we're done. + """ + + help_text = """\ +This is pyrepl. Hear my roar. + +Helpful text may appear here at some point in the future when I'm +feeling more loquacious than I am now.""" + + def __init__(self, console): + self.buffer = [] + self.ps1 = "->> " + self.ps2 = "/>> " + self.ps3 = "|.. " + self.ps4 = "\__ " + self.kill_ring = [] + self.arg = None + self.finished = 0 + self.console = console + self.commands = {} + self.msg = '' + for v in vars(commands).values(): + if ( isinstance(v, type) + and issubclass(v, commands.Command) + and v.__name__[0].islower() ): + self.commands[v.__name__] = v + self.commands[v.__name__.replace('_', '-')] = v + self.syntax_table = make_default_syntax_table() + self.input_trans_stack = [] + self.keymap = self.collect_keymap() + self.input_trans = input.KeymapTranslator( + self.keymap, + invalid_cls='invalid-key', + character_cls='self-insert') + + def collect_keymap(self): + return default_keymap + + def calc_screen(self): + """The purpose of this method is to translate changes in + self.buffer into changes in self.screen. Currently it rips + everything down and starts from scratch, which whilst not + especially efficient is certainly simple(r). + """ + lines = self.get_unicode().split("\n") + screen = [] + screeninfo = [] + w = self.console.width - 1 + p = self.pos + for ln, line in zip(range(len(lines)), lines): + ll = len(line) + if 0 <= p <= ll: + if self.msg: + for mline in self.msg.split("\n"): + screen.append(mline) + screeninfo.append((0, [])) + self.lxy = p, ln + prompt = self.get_prompt(ln, ll >= p >= 0) + p -= ll + 1 + lp = len(prompt) + l, l2 = disp_str(line) + wrapcount = (len(l) + lp) / w + if wrapcount == 0: + screen.append(prompt + l) + screeninfo.append((lp, l2+[1])) + else: + screen.append(prompt + l[:w-lp] + "\\") + screeninfo.append((lp, l2[:w-lp])) + for i in range(-lp + w, -lp + wrapcount*w, w): + screen.append(l[i:i+w] + "\\") + screeninfo.append((0, l2[i:i + w])) + screen.append(l[wrapcount*w - lp:]) + screeninfo.append((0, l2[wrapcount*w - lp:]+[1])) + self.screeninfo = screeninfo + self.cxy = self.pos2xy(self.pos) + return screen + + def bow(self, p=None): + """Return the 0-based index of the word break preceding p most + immediately. + + p defaults to self.pos; word boundaries are determined using + self.syntax_table.""" + if p is None: + p = self.pos + st = self.syntax_table + b = self.buffer + p -= 1 + while p >= 0 and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD: + p -= 1 + while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD: + p -= 1 + return p + 1 + + def eow(self, p=None): + """Return the 0-based index of the word break following p most + immediately. + + p defaults to self.pos; word boundaries are determined using + self.syntax_table.""" + if p is None: + p = self.pos + st = self.syntax_table + b = self.buffer + while p < len(b) and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD: + p += 1 + while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD: + p += 1 + return p + + def bol(self, p=None): + """Return the 0-based index of the line break preceding p most + immediately. + + p defaults to self.pos.""" + # XXX there are problems here. + if p is None: + p = self.pos + b = self.buffer + p -= 1 + while p >= 0 and b[p] <> '\n': + p -= 1 + return p + 1 + + def eol(self, p=None): + """Return the 0-based index of the line break following p most + immediately. + + p defaults to self.pos.""" + if p is None: + p = self.pos + b = self.buffer + while p < len(b) and b[p] <> '\n': + p += 1 + return p + + def get_arg(self, default=1): + """Return any prefix argument that the user has supplied, + returning `default' if there is None. `default' defaults + (groan) to 1.""" + if self.arg is None: + return default + else: + return self.arg + + def get_prompt(self, lineno, cursor_on_line): + """Return what should be in the left-hand margin for line + `lineno'.""" + if self.arg is not None and cursor_on_line: + return "(arg: %s) "%self.arg + if "\n" in self.buffer: + if lineno == 0: + return self._ps2 + elif lineno == self.buffer.count("\n"): + return self._ps4 + else: + return self._ps3 + else: + return self._ps1 + + def push_input_trans(self, itrans): + self.input_trans_stack.append(self.input_trans) + self.input_trans = itrans + + def pop_input_trans(self): + self.input_trans = self.input_trans_stack.pop() + + def pos2xy(self, pos): + """Return the x, y coordinates of position 'pos'.""" + # this *is* incomprehensible, yes. + y = 0 + assert 0 <= pos <= len(self.buffer) + if pos == len(self.buffer): + y = len(self.screeninfo) - 1 + p, l2 = self.screeninfo[y] + return p + len(l2) - 1, y + else: + for p, l2 in self.screeninfo: + l = l2.count(1) + if l > pos: + break + else: + pos -= l + y += 1 + c = 0 + i = 0 + while c < pos: + c += l2[i] + i += 1 + while l2[i] == 0: + i += 1 + return p + i, y + + def insert(self, text): + """Insert 'text' at the insertion point.""" + self.buffer[self.pos:self.pos] = list(text) + self.pos += len(text) + self.dirty = 1 + + def update_cursor(self): + """Move the cursor to reflect changes in self.pos""" + self.cxy = self.pos2xy(self.pos) + self.console.move_cursor(*self.cxy) + + def after_command(self, cmd): + """This function is called to allow post command cleanup.""" + if getattr(cmd, "kills_digit_arg", 1): + if self.arg is not None: + self.dirty = 1 + self.arg = None + + def prepare(self): + """Get ready to run. Call restore when finished. You must not + write to the console in between the calls to prepare and + restore.""" + try: + self.console.prepare() + self.arg = None + self.screeninfo = [] + self.finished = 0 + del self.buffer[:] + self.pos = 0 + self.dirty = 1 + self.last_command = None + self._ps1, self._ps2, self._ps3, self._ps4 = \ + map(str, [self.ps1, self.ps2, self.ps3, self.ps4]) + except: + self.restore() + raise + + def last_command_is(self, klass): + if not self.last_command: + return 0 + return issubclass(klass, self.last_command) + + def restore(self): + """Clean up after a run.""" + self.console.restore() + + def finish(self): + """Called when a command signals that we're finished.""" + pass + + def message(self, msg="none"): + self.msg = "[ "+msg+" ] " + self.dirty = 1 + + def error(self, msg="none"): + self.msg = "! " + msg + " " + self.dirty = 1 + self.console.beep() + + def refresh(self): + """Recalculate and refresh the screen.""" + if self.console.isbusy(): + return + # this call sets up self.cxy, so call it first. + screen = self.calc_screen() + self.console.refresh(screen, self.cxy) + self.dirty = 0 # forgot this for a while (blush) + + def do_cmd(self, cmd): + if isinstance(cmd[0], str): + cmd = self.commands.get(cmd[0], + commands.invalid_command)(self, cmd) + elif isinstance(cmd[0], type): + cmd = cmd[0](self, cmd) + else: + cmd=cmd[0] + + cmd.do() + + self.after_command(cmd) + + if self.dirty: + self.refresh() + else: + self.update_cursor() + + if not isinstance(cmd, commands.digit_arg): + self.last_command = cmd.__class__ + + self.finished = cmd.finish + if self.finished: + self.console.finish() + self.finish() + + def handle1(self, block=1): + """Handle a single event. Wait as long as it takes if block + is true (the default), otherwise return None if no event is + pending.""" + + if self.msg: + self.msg = '' + self.dirty = 1 + + while 1: + event = self.console.get_event(block) + if not event: # can only happen if we're not blocking + return None + + if event.evt == 'key': + self.input_trans.push(event) + elif event.evt == 'scroll': + self.refresh() + elif event.evt == 'resize': + self.refresh() + else: + pass + + cmd = self.input_trans.get() + + if cmd is None: + if block: + continue + else: + return None + + self.do_cmd(cmd) + return 1 + + def readline(self): + """Read a line. The implementation of this method also shows + how to drive Reader if you want more control over the event + loop.""" + self.prepare() + try: + self.refresh() + while not self.finished: + self.handle1() + return self.get_buffer() + finally: + self.restore() + + def bind(self, spec, command): + self.keymap = self.keymap + ((spec, command),) + self.input_trans = input.KeymapTranslator( + self.keymap, + invalid_cls='invalid-key', + character_cls='self-insert') + + def get_buffer(self, encoding=None): + if encoding is None: + encoding = self.console.encoding + return u''.join(self.buffer).encode(self.console.encoding) + + def get_unicode(self): + """Return the current buffer as a unicode string.""" + return u''.join(self.buffer) + +def test(): + from pyrepl.unix_console import UnixConsole + reader = Reader(UnixConsole()) + reader.ps1 = "**> " + reader.ps2 = "/*> " + reader.ps3 = "|*> " + reader.ps4 = "\*> " + while reader.readline(): + pass + +if __name__=='__main__': + test()