src/extras/pyrepl/reader.py
changeset 0 ca70ae20a155
equal deleted inserted replaced
-1:000000000000 0:ca70ae20a155
       
     1 #   Copyright 2000-2004 Michael Hudson mwh@python.net
       
     2 #
       
     3 #                        All Rights Reserved
       
     4 #
       
     5 # Portions Copyright (c) 2005 Nokia Corporation 
       
     6 #
       
     7 # Permission to use, copy, modify, and distribute this software and
       
     8 # its documentation for any purpose is hereby granted without fee,
       
     9 # provided that the above copyright notice appear in all copies and
       
    10 # that both that copyright notice and this permission notice appear in
       
    11 # supporting documentation.
       
    12 #
       
    13 # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
       
    14 # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
       
    15 # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
       
    16 # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
       
    17 # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
       
    18 # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
       
    19 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       
    20 
       
    21 import types
       
    22 try:
       
    23     import unicodedata
       
    24 except ImportError:
       
    25     import dumbunicodedata as unicodedata
       
    26 from pyrepl import commands
       
    27 import ascii
       
    28 from pyrepl import input
       
    29 
       
    30 def _make_unctrl_map():
       
    31     uc_map = {}
       
    32     for c in map(unichr, range(256)):
       
    33         if unicodedata.category(c)[0] <> 'C':
       
    34             uc_map[c] = c
       
    35     for i in range(32):
       
    36         c = unichr(i)
       
    37         uc_map[c] = u'^' + unichr(ord('A') + i - 1)
       
    38     uc_map['\177'] = u'^?'
       
    39     for i in range(256):
       
    40         c = unichr(i)
       
    41         if not uc_map.has_key(c):
       
    42             uc_map[c] = u'\\%03o'%i 
       
    43     return uc_map
       
    44 
       
    45 # disp_str proved to be a bottleneck for large inputs, so it's been
       
    46 # rewritten in C; it's not required though.
       
    47 try:
       
    48     raise ImportError # currently it's borked by the unicode support
       
    49     
       
    50     from _pyrepl_utils import disp_str, init_unctrl_map
       
    51     
       
    52     init_unctrl_map(_make_unctrl_map())
       
    53 
       
    54     del init_unctrl_map
       
    55 except ImportError:
       
    56     def _my_unctrl(c, u=_make_unctrl_map()):
       
    57 #        import unicodedata
       
    58         if c in u:
       
    59             return u[c]
       
    60         else:
       
    61             if unicodedata.category(c).startswith('C'):
       
    62                 return '\u%04x'%(ord(c),)
       
    63             else:
       
    64                 return c
       
    65 
       
    66     def disp_str(buffer, join=''.join, uc=_my_unctrl):
       
    67         """ disp_str(buffer:string) -> (string, [int])
       
    68 
       
    69         Return the string that should be the printed represenation of
       
    70         |buffer| and a list detailing where the characters of |buffer|
       
    71         get used up.  E.g.:
       
    72 
       
    73         >>> disp_str(chr(3))
       
    74         ('^C', [1, 0])
       
    75 
       
    76         the list always contains 0s or 1s at present; it could conceivably
       
    77         go higher as and when unicode support happens."""
       
    78         s = map(uc, buffer)
       
    79         return (join(s),
       
    80                 map(ord, join(map(lambda x:'\001'+(len(x)-1)*'\000', s))))
       
    81 
       
    82     del _my_unctrl
       
    83 
       
    84 del _make_unctrl_map
       
    85 
       
    86 # syntax classes:
       
    87 
       
    88 [SYNTAX_WHITESPACE,
       
    89  SYNTAX_WORD,
       
    90  SYNTAX_SYMBOL] = range(3)
       
    91 
       
    92 def make_default_syntax_table():
       
    93     # XXX perhaps should use some unicodedata here?
       
    94     st = {}
       
    95     for c in map(unichr, range(256)):
       
    96         st[c] = SYNTAX_SYMBOL
       
    97     for c in [a for a in map(unichr, range(256)) if a.isalpha()]:
       
    98         st[c] = SYNTAX_WORD
       
    99     st[u'\n'] = st[u' '] = SYNTAX_WHITESPACE
       
   100     return st
       
   101 
       
   102 default_keymap = tuple(
       
   103     [(r'\C-a', 'beginning-of-line'),
       
   104      (r'\C-b', 'left'),
       
   105      (r'\C-c', 'interrupt'),
       
   106      (r'\C-d', 'delete'),
       
   107      (r'\C-e', 'end-of-line'),
       
   108      (r'\C-f', 'right'),
       
   109      (r'\C-g', 'cancel'),
       
   110      (r'\C-h', 'backspace'),
       
   111      (r'\C-j', 'accept'),
       
   112      (r'\<return>', 'accept'),
       
   113      (r'\C-k', 'kill-line'),
       
   114      (r'\C-l', 'clear-screen'),
       
   115      (r'\C-m', 'accept'),
       
   116      (r'\C-q', 'quoted-insert'),
       
   117      (r'\C-t', 'transpose-characters'),
       
   118      (r'\C-u', 'unix-line-discard'),
       
   119      (r'\C-v', 'quoted-insert'),
       
   120      (r'\C-w', 'unix-word-rubout'),
       
   121      (r'\C-x\C-s', 'save-history'),
       
   122      (r'\C-x\C-u', 'upcase-region'),
       
   123      (r'\C-y', 'yank'),
       
   124      (r'\C-z', 'suspend'),
       
   125      
       
   126      (r'\M-b', 'backward-word'),
       
   127      (r'\M-c', 'capitalize-word'),
       
   128      (r'\M-d', 'kill-word'),
       
   129      (r'\M-f', 'forward-word'),
       
   130      (r'\M-l', 'downcase-word'),
       
   131      (r'\M-t', 'transpose-words'),
       
   132      (r'\M-u', 'upcase-word'),
       
   133      (r'\M-y', 'yank-pop'),
       
   134      (r'\M--', 'digit-arg'),
       
   135      (r'\M-0', 'digit-arg'),
       
   136      (r'\M-1', 'digit-arg'),
       
   137      (r'\M-2', 'digit-arg'),
       
   138      (r'\M-3', 'digit-arg'),
       
   139      (r'\M-4', 'digit-arg'),
       
   140      (r'\M-5', 'digit-arg'),
       
   141      (r'\M-6', 'digit-arg'),
       
   142      (r'\M-7', 'digit-arg'),
       
   143      (r'\M-8', 'digit-arg'),
       
   144      (r'\M-9', 'digit-arg'),
       
   145      (r'\M-\n', 'insert-nl'),
       
   146      ('\\\\', 'self-insert')] + \
       
   147     [(c, 'self-insert')
       
   148      for c in map(chr, range(32, 127)) if c <> '\\'] + \
       
   149     [(c, 'self-insert')
       
   150      for c in map(chr, range(128, 256)) if c.isalpha()] + \
       
   151     [(r'\<up>', 'up'),
       
   152      (r'\<down>', 'down'),
       
   153      (r'\<left>', 'left'),
       
   154      (r'\<right>', 'right'),
       
   155      (r'\<insert>', 'quoted-insert'),
       
   156      (r'\<delete>', 'delete'),
       
   157      (r'\<backspace>', 'backspace'),
       
   158      (r'\M-\<backspace>', 'backward-kill-word'),
       
   159      (r'\<end>', 'end'),
       
   160      (r'\<home>', 'home'),
       
   161      (r'\<f1>', 'help'),
       
   162      (r'\EOF', 'end'),  # the entries in the terminfo database for xterms
       
   163      (r'\EOH', 'home'), # seem to be wrong.  this is a less than ideal
       
   164                         # workaround
       
   165      ])
       
   166 
       
   167 del c # from the listcomps
       
   168 
       
   169 class Reader(object):
       
   170     """The Reader class implements the bare bones of a command reader,
       
   171     handling such details as editing and cursor motion.  What it does
       
   172     not support are such things as completion or history support -
       
   173     these are implemented elsewhere.
       
   174 
       
   175     Instance variables of note include:
       
   176 
       
   177       * buffer:
       
   178         A *list* (*not* a string at the moment :-) containing all the
       
   179         characters that have been entered.
       
   180       * console:
       
   181         Hopefully encapsulates the OS dependent stuff.
       
   182       * pos:
       
   183         A 0-based index into `buffer' for where the insertion point
       
   184         is.
       
   185       * screeninfo:
       
   186         Ahem.  This list contains some info needed to move the
       
   187         insertion point around reasonably efficiently.  I'd like to
       
   188         get rid of it, because its contents are obtuse (to put it
       
   189         mildly) but I haven't worked out if that is possible yet.
       
   190       * cxy, lxy:
       
   191         the position of the insertion point in screen ... XXX
       
   192       * syntax_table:
       
   193         Dictionary mapping characters to `syntax class'; read the
       
   194         emacs docs to see what this means :-)
       
   195       * commands:
       
   196         Dictionary mapping command names to command classes.
       
   197       * arg:
       
   198         The emacs-style prefix argument.  It will be None if no such
       
   199         argument has been provided.
       
   200       * dirty:
       
   201         True if we need to refresh the display.
       
   202       * kill_ring:
       
   203         The emacs-style kill-ring; manipulated with yank & yank-pop
       
   204       * ps1, ps2, ps3, ps4:
       
   205         prompts.  ps1 is the prompt for a one-line input; for a
       
   206         multiline input it looks like:
       
   207             ps2> first line of input goes here
       
   208             ps3> second and further
       
   209             ps3> lines get ps3
       
   210             ...
       
   211             ps4> and the last one gets ps4
       
   212         As with the usual top-level, you can set these to instances if
       
   213         you like; str() will be called on them (once) at the beginning
       
   214         of each command.  Don't put really long or newline containing
       
   215         strings here, please!
       
   216         This is just the default policy; you can change it freely by
       
   217         overriding get_prompt() (and indeed some standard subclasses
       
   218         do).
       
   219       * finished:
       
   220         handle1 will set this to a true value if a command signals
       
   221         that we're done.
       
   222     """
       
   223 
       
   224     help_text = """\
       
   225 This is pyrepl.  Hear my roar.
       
   226 
       
   227 Helpful text may appear here at some point in the future when I'm
       
   228 feeling more loquacious than I am now."""
       
   229 
       
   230     def __init__(self, console):
       
   231         self.buffer = []
       
   232         self.ps1 = "->> "
       
   233         self.ps2 = "/>> "
       
   234         self.ps3 = "|.. "
       
   235         self.ps4 = "\__ "
       
   236         self.kill_ring = []
       
   237         self.arg = None
       
   238         self.finished = 0
       
   239         self.console = console
       
   240         self.commands = {}
       
   241         self.msg = ''
       
   242         for v in vars(commands).values():
       
   243             if  ( isinstance(v, type)
       
   244                   and issubclass(v, commands.Command)
       
   245                   and v.__name__[0].islower() ):
       
   246                 self.commands[v.__name__] = v
       
   247                 self.commands[v.__name__.replace('_', '-')] = v
       
   248         self.syntax_table = make_default_syntax_table()
       
   249         self.input_trans_stack = []
       
   250         self.keymap = self.collect_keymap()
       
   251         self.input_trans = input.KeymapTranslator(
       
   252             self.keymap,
       
   253             invalid_cls='invalid-key',
       
   254             character_cls='self-insert')
       
   255 
       
   256     def collect_keymap(self):
       
   257         return default_keymap
       
   258 
       
   259     def calc_screen(self):
       
   260         """The purpose of this method is to translate changes in
       
   261         self.buffer into changes in self.screen.  Currently it rips
       
   262         everything down and starts from scratch, which whilst not
       
   263         especially efficient is certainly simple(r).
       
   264         """
       
   265         lines = self.get_unicode().split("\n")
       
   266         screen = []
       
   267         screeninfo = []
       
   268         w = self.console.width - 1
       
   269         p = self.pos
       
   270         for ln, line in zip(range(len(lines)), lines):
       
   271             ll = len(line)
       
   272             if 0 <= p <= ll:
       
   273                 if self.msg:
       
   274                     for mline in self.msg.split("\n"):
       
   275                         screen.append(mline)
       
   276                         screeninfo.append((0, []))
       
   277                 self.lxy = p, ln
       
   278             prompt = self.get_prompt(ln, ll >= p >= 0)
       
   279             p -= ll + 1
       
   280             lp = len(prompt)
       
   281             l, l2 = disp_str(line)
       
   282             wrapcount = (len(l) + lp) / w
       
   283             if wrapcount == 0:
       
   284                 screen.append(prompt + l)
       
   285                 screeninfo.append((lp, l2+[1]))
       
   286             else:
       
   287                 screen.append(prompt + l[:w-lp] + "\\")
       
   288                 screeninfo.append((lp, l2[:w-lp]))
       
   289                 for i in range(-lp + w, -lp + wrapcount*w, w):
       
   290                     screen.append(l[i:i+w] +  "\\")
       
   291                     screeninfo.append((0, l2[i:i + w]))
       
   292                 screen.append(l[wrapcount*w - lp:])
       
   293                 screeninfo.append((0, l2[wrapcount*w - lp:]+[1]))
       
   294         self.screeninfo = screeninfo
       
   295         self.cxy = self.pos2xy(self.pos)
       
   296         return screen
       
   297 
       
   298     def bow(self, p=None):
       
   299         """Return the 0-based index of the word break preceding p most
       
   300         immediately.
       
   301 
       
   302         p defaults to self.pos; word boundaries are determined using
       
   303         self.syntax_table."""
       
   304         if p is None:
       
   305             p = self.pos
       
   306         st = self.syntax_table
       
   307         b = self.buffer
       
   308         p -= 1
       
   309         while p >= 0 and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD:
       
   310             p -= 1
       
   311         while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
       
   312             p -= 1
       
   313         return p + 1
       
   314 
       
   315     def eow(self, p=None):
       
   316         """Return the 0-based index of the word break following p most
       
   317         immediately.
       
   318 
       
   319         p defaults to self.pos; word boundaries are determined using
       
   320         self.syntax_table."""
       
   321         if p is None:
       
   322             p = self.pos
       
   323         st = self.syntax_table
       
   324         b = self.buffer
       
   325         while p < len(b) and st.get(b[p], SYNTAX_WORD) <> SYNTAX_WORD:
       
   326             p += 1
       
   327         while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
       
   328             p += 1
       
   329         return p
       
   330 
       
   331     def bol(self, p=None):
       
   332         """Return the 0-based index of the line break preceding p most
       
   333         immediately.
       
   334 
       
   335         p defaults to self.pos."""
       
   336         # XXX there are problems here.
       
   337         if p is None:
       
   338             p = self.pos
       
   339         b = self.buffer
       
   340         p -= 1
       
   341         while p >= 0 and b[p] <> '\n':
       
   342             p -= 1
       
   343         return p + 1
       
   344     
       
   345     def eol(self, p=None):
       
   346         """Return the 0-based index of the line break following p most
       
   347         immediately.
       
   348 
       
   349         p defaults to self.pos."""
       
   350         if p is None:
       
   351             p = self.pos
       
   352         b = self.buffer
       
   353         while p < len(b) and b[p] <> '\n':
       
   354             p += 1
       
   355         return p
       
   356 
       
   357     def get_arg(self, default=1):
       
   358         """Return any prefix argument that the user has supplied,
       
   359         returning `default' if there is None.  `default' defaults
       
   360         (groan) to 1."""
       
   361         if self.arg is None:
       
   362             return default
       
   363         else:
       
   364             return self.arg
       
   365 
       
   366     def get_prompt(self, lineno, cursor_on_line):
       
   367         """Return what should be in the left-hand margin for line
       
   368         `lineno'."""
       
   369         if self.arg is not None and cursor_on_line:
       
   370             return "(arg: %s) "%self.arg
       
   371         if "\n" in self.buffer:
       
   372             if lineno == 0:
       
   373                 return self._ps2
       
   374             elif lineno == self.buffer.count("\n"):
       
   375                 return self._ps4
       
   376             else:
       
   377                 return self._ps3
       
   378         else:
       
   379             return self._ps1
       
   380 
       
   381     def push_input_trans(self, itrans):
       
   382         self.input_trans_stack.append(self.input_trans)
       
   383         self.input_trans = itrans
       
   384 
       
   385     def pop_input_trans(self):
       
   386         self.input_trans = self.input_trans_stack.pop()
       
   387 
       
   388     def pos2xy(self, pos):
       
   389         """Return the x, y coordinates of position 'pos'."""
       
   390         # this *is* incomprehensible, yes.
       
   391         y = 0
       
   392         assert 0 <= pos <= len(self.buffer)
       
   393         if pos == len(self.buffer):
       
   394             y = len(self.screeninfo) - 1
       
   395             p, l2 = self.screeninfo[y]
       
   396             return p + len(l2) - 1, y
       
   397         else:
       
   398             for p, l2 in self.screeninfo:
       
   399                 l = l2.count(1)
       
   400                 if l > pos:
       
   401                     break
       
   402                 else:
       
   403                     pos -= l
       
   404                     y += 1
       
   405             c = 0
       
   406             i = 0
       
   407             while c < pos:
       
   408                 c += l2[i]
       
   409                 i += 1
       
   410             while l2[i] == 0:
       
   411                 i += 1
       
   412             return p + i, y
       
   413 
       
   414     def insert(self, text):
       
   415         """Insert 'text' at the insertion point."""
       
   416         self.buffer[self.pos:self.pos] = list(text)
       
   417         self.pos += len(text)
       
   418         self.dirty = 1
       
   419 
       
   420     def update_cursor(self):
       
   421         """Move the cursor to reflect changes in self.pos"""
       
   422         self.cxy = self.pos2xy(self.pos)
       
   423         self.console.move_cursor(*self.cxy)
       
   424 
       
   425     def after_command(self, cmd):
       
   426         """This function is called to allow post command cleanup."""
       
   427         if getattr(cmd, "kills_digit_arg", 1):
       
   428             if self.arg is not None:
       
   429                 self.dirty = 1                
       
   430             self.arg = None
       
   431 
       
   432     def prepare(self):
       
   433         """Get ready to run.  Call restore when finished.  You must not
       
   434         write to the console in between the calls to prepare and
       
   435         restore."""
       
   436         try:
       
   437             self.console.prepare()
       
   438             self.arg = None
       
   439             self.screeninfo = []
       
   440             self.finished = 0
       
   441             del self.buffer[:]
       
   442             self.pos = 0
       
   443             self.dirty = 1
       
   444             self.last_command = None
       
   445             self._ps1, self._ps2, self._ps3, self._ps4 = \
       
   446                            map(str, [self.ps1, self.ps2, self.ps3, self.ps4])
       
   447         except:
       
   448             self.restore()
       
   449             raise
       
   450 
       
   451     def last_command_is(self, klass):
       
   452         if not self.last_command:
       
   453             return 0
       
   454         return issubclass(klass, self.last_command)
       
   455 
       
   456     def restore(self):
       
   457         """Clean up after a run."""
       
   458         self.console.restore()
       
   459 
       
   460     def finish(self):
       
   461         """Called when a command signals that we're finished."""
       
   462         pass
       
   463 
       
   464     def message(self, msg="none"):
       
   465         self.msg = "[ "+msg+" ] "
       
   466         self.dirty = 1
       
   467 
       
   468     def error(self, msg="none"):
       
   469         self.msg = "! " + msg + " "
       
   470         self.dirty = 1
       
   471         self.console.beep()
       
   472 
       
   473     def refresh(self):
       
   474         """Recalculate and refresh the screen."""
       
   475         if self.console.isbusy():
       
   476             return
       
   477         # this call sets up self.cxy, so call it first.
       
   478         screen = self.calc_screen()
       
   479         self.console.refresh(screen, self.cxy)
       
   480         self.dirty = 0 # forgot this for a while (blush)
       
   481 
       
   482     def do_cmd(self, cmd):
       
   483         if isinstance(cmd[0], str):
       
   484             cmd = self.commands.get(cmd[0],
       
   485                                     commands.invalid_command)(self, cmd)
       
   486         elif isinstance(cmd[0], type):
       
   487             cmd = cmd[0](self, cmd)
       
   488         else:
       
   489             cmd=cmd[0]
       
   490 
       
   491         cmd.do()
       
   492 
       
   493         self.after_command(cmd)
       
   494 
       
   495         if self.dirty:
       
   496             self.refresh()
       
   497         else:
       
   498             self.update_cursor()
       
   499 
       
   500         if not isinstance(cmd, commands.digit_arg):
       
   501             self.last_command = cmd.__class__
       
   502 
       
   503         self.finished = cmd.finish
       
   504         if self.finished:
       
   505             self.console.finish()
       
   506             self.finish()
       
   507 
       
   508     def handle1(self, block=1):
       
   509         """Handle a single event.  Wait as long as it takes if block
       
   510         is true (the default), otherwise return None if no event is
       
   511         pending."""
       
   512 
       
   513         if self.msg:
       
   514             self.msg = ''
       
   515             self.dirty = 1
       
   516 
       
   517         while 1:
       
   518             event = self.console.get_event(block)
       
   519             if not event: # can only happen if we're not blocking
       
   520                 return None
       
   521 
       
   522             if event.evt == 'key':
       
   523                 self.input_trans.push(event)
       
   524             elif event.evt == 'scroll':
       
   525                 self.refresh()
       
   526             elif event.evt == 'resize':
       
   527                 self.refresh()
       
   528             else:
       
   529                 pass
       
   530 
       
   531             cmd = self.input_trans.get()
       
   532 
       
   533             if cmd is None:
       
   534                 if block:
       
   535                     continue
       
   536                 else:
       
   537                     return None
       
   538 
       
   539             self.do_cmd(cmd)
       
   540             return 1
       
   541     
       
   542     def readline(self):
       
   543         """Read a line.  The implementation of this method also shows
       
   544         how to drive Reader if you want more control over the event
       
   545         loop."""
       
   546         self.prepare()
       
   547         try:
       
   548             self.refresh()
       
   549             while not self.finished:
       
   550                 self.handle1()
       
   551             return self.get_buffer()
       
   552         finally:
       
   553             self.restore()
       
   554 
       
   555     def bind(self, spec, command):
       
   556         self.keymap = self.keymap + ((spec, command),)
       
   557         self.input_trans = input.KeymapTranslator(
       
   558             self.keymap,
       
   559             invalid_cls='invalid-key',
       
   560             character_cls='self-insert')
       
   561 
       
   562     def get_buffer(self, encoding=None):
       
   563         if encoding is None:
       
   564             encoding = self.console.encoding
       
   565         return u''.join(self.buffer).encode(self.console.encoding)
       
   566 
       
   567     def get_unicode(self):
       
   568         """Return the current buffer as a unicode string."""
       
   569         return u''.join(self.buffer)
       
   570 
       
   571 def test():
       
   572     from pyrepl.unix_console import UnixConsole
       
   573     reader = Reader(UnixConsole())
       
   574     reader.ps1 = "**> "
       
   575     reader.ps2 = "/*> "
       
   576     reader.ps3 = "|*> "
       
   577     reader.ps4 = "\*> "
       
   578     while reader.readline():
       
   579         pass
       
   580 
       
   581 if __name__=='__main__':
       
   582     test()