pyreadline refactor: Refactored line_buffer and history_buffer

This commit is contained in:
jstenar
2006-02-14 20:50:16 +00:00
parent 0b8a260ac4
commit d581263f4b
11 changed files with 1006 additions and 283 deletions
+7
View File
@@ -1,3 +1,10 @@
2006-02-14 Jörgen Stenarson <jorgen.stenarson -at- bostream.nu>
* Started Refactor branch
* Refactored line_buffer to separate class
* Refactored history buffer to separate class
*
2006-01-31 Jörgen Stenarson <jorgen.stenarson -at- bostream.nu>
* Added experimental mulitline paste. Thís version ALWAYS adds
+5 -2
View File
@@ -12,7 +12,7 @@ This was modeled after the C extension of the same name by Fredrik Lundh.
'''
# primitive debug printing that won't interfere with the screen
if 0:
if 1:
fp = open('debug.txt', 'w')
def log(s):
print >>fp, s
@@ -468,6 +468,7 @@ class Console(object):
'''Return next key press event from the queue, ignoring others.'''
while 1:
e = self.get()
log("getKeypress:%s,%s,%s"%(e.keyinfo,e.keycode,e.type))
if e.type == 'KeyPress' and e.keycode not in key_modifiers:
log(e)
if e.keysym == 'Next':
@@ -476,7 +477,9 @@ class Console(object):
self.scroll_window(-12)
else:
return e
elif e.type == 'KeyRelease' and e.keyinfo==(True, False, False, 83):
return e
def getchar(self):
'''Get next character from queue.'''
+16
View File
@@ -0,0 +1,16 @@
FOREGROUND_BLUE = 0x0001
FOREGROUND_GREEN = 0x0002
FOREGROUND_RED = 0x0004
FOREGROUND_INTENSITY = 0x0008
BACKGROUND_BLUE = 0x0010
BACKGROUND_GREEN = 0x0020
BACKGROUND_RED = 0x0040
BACKGROUND_INTENSITY = 0x0080
COMMON_LVB_LEADING_BYTE = 0x0100
COMMON_LVB_TRAILING_BYTE = 0x0200
COMMON_LVB_GRID_HORIZONTAL= 0x0400
COMMON_LVB_GRID_LVERTICAL = 0x0800
COMMON_LVB_GRID_RVERTICAL = 0x1000
COMMON_LVB_REVERSE_VIDEO = 0x2000
COMMON_LVB_UNDERSCORE = 0x4000
+18
View File
@@ -0,0 +1,18 @@
import sys,textwrap
rlmain=sys.modules["pyreadline.rlmain"]
rl=rlmain.rl
def get_doc(rl):
methods=[(x,getattr(rl,x)) for x in dir(rl) if callable(getattr(rl,x))]
return [ (x,m.__doc__ )for x,m in methods if m.__doc__]
def get_rest(rl):
q=get_doc(rl)
out=[]
for funcname,doc in q:
out.append(funcname)
out.append("\n".join(textwrap.wrap(doc,80,initial_indent=" ")))
out.append("")
return out
View File
+190
View File
@@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
#*****************************************************************************
# Copyright (C) 2006 Jorgen Stenarson. <jorgen.stenarson@bostream.nu>
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#*****************************************************************************
import re,operator,string,sys
#import wordmatcher
#import pyreadline.clipboard as clipboard
if "pyreadline" in sys.modules:
pyreadline= sys.modules["pyreadline"]
else:
import pyreadline
import lineobj
import exceptions
class EscapeHistory(exceptions.Exception):
pass
class LineHistory(object):
def __init__(self):
self.history=[]
self.history_length=-1
self.history_cursor=0
def get_history_length(self):
return self.history_length
def set_history_length(self,value):
self.history_length=value
def read_history_file(self, filename):
'''Load a readline history file.'''
try:
for line in open(filename, 'rt'):
self.add_history(lineobj.ReadLineTextBuffer(line.rstrip()))
except IOError:
self.history = []
self.history_cursor = 0
raise IOError
def write_history_file(self, filename):
'''Save a readline history file.'''
fp = open(filename, 'wb')
for line in self.history:
fp.write(line.get_line_text())
fp.write('\n')
fp.close()
def add_history(self, line):
'''Append a line to the history buffer, as if it was the last line typed.'''
if not line.get_line_text():
pass
elif len(self.history) > 0 and self.history[-1].get_line_text() == line.get_line_text():
pass
else:
self.history.append(line)
if self.history_length > 0 and len(self.history) > self.history_length:
self.history = self.history[-self.history_length:]
self.history_cursor = len(self.history)
def previous_history(self,current): # (C-p)
'''Move back through the history list, fetching the previous command. '''
if self.history_cursor==len(self.history):
self.history.append(current.copy()) #do not use add_history since we do not want to increment cursor
if self.history_cursor > 0:
self.history_cursor -= 1
current.set_line(self.history[self.history_cursor].get_line_text())
def next_history(self,current): # (C-n)
'''Move forward through the history list, fetching the next command. '''
if self.history_cursor < len(self.history)-1:
self.history_cursor += 1
current.set_line(self.history[self.history_cursor].get_line_text())
def beginning_of_history(self): # (M-<)
'''Move to the first line in the history.'''
self.history_cursor = 0
if len(self.history) > 0:
self.l_buffer = self.history[0]
def end_of_history(self,current): # (M->)
'''Move to the end of the input history, i.e., the line currently
being entered.'''
self.history_cursor=len(self.history)
current.set_line(self.history[-1].get_line_text())
def reverse_search_history(self,searchfor):
res=[(idx,line) for idx,line in enumerate(self.history[self.history_cursor:0:-1]) if searchfor in line]
if res:
self.history_cursor-=res[0][0]
return res[0][1].get_line_text()
return ""
def forward_search_history(self,searchfor):
res=[(idx,line) for idx,line in enumerate(self.history[self.history_cursor:]) if searchfor in line]
if res:
self.history_cursor+=res[0][0]
return res[0][1].get_line_text()
return ""
def _non_i_search(self, direction, current):
c = pyreadline.rl.console
line = current.get_line_text()
query = ''
while 1:
c.pos(*pyreadline.rl.prompt_end_pos)
scroll = c.write_scrolling(":%s" % query)
pyreadline.rl._update_prompt_pos(scroll)
pyreadline.rl._clear_after()
event = c.getkeypress()
if event.keysym == 'BackSpace':
if len(query) > 0:
query = query[:-1]
else:
break
elif event.char in string.letters + string.digits + string.punctuation + ' ':
query += event.char
elif event.keysym == 'Return':
break
else:
pyreadline.rl._bell()
if query:
hc = self.history_cursor - 1
while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)):
if self.history[hc].startswith(query) >= 0:
current=self.history[hc]
self.history_cursor = hc
return
hc += direction
else:
pyreadline.rl._bell()
def non_incremental_reverse_search_history(self,current): # (M-p)
'''Search backward starting at the current line and moving up
through the history as necessary using a non-incremental search for
a string supplied by the user.'''
self._non_i_search(-1,current)
def non_incremental_forward_search_history(self,current): # (M-n)
'''Search forward starting at the current line and moving down
through the the history as necessary using a non-incremental search
for a string supplied by the user.'''
self._non_i_search(1,current)
def _search(self, direction,partial):
query = partial[0:partial.point].get_line_text()
hc = self.history_cursor + direction
while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)):
h = self.history[hc]
if not query:
self.history_cursor = hc
result=lineobj.ReadLineTextBuffer(h,point=partial.point)
return result
elif h.get_line_text().startswith(query) and h != partial.get_line_text():
self.history_cursor = hc
result=lineobj.ReadLineTextBuffer(h,point=partial.point)
return result
hc += direction
else:
return lineobj.ReadLineTextBuffer(query,point=partial.point)
def history_search_forward(self,partial): # ()
'''Search forward through the history for the string of characters
between the start of the current line and the point. This is a
non-incremental search. By default, this command is unbound.'''
return self._search(1,partial)
def history_search_backward(self,partial): # ()
'''Search backward through the history for the string of characters
between the start of the current line and the point. This is a
non-incremental search. By default, this command is unbound.'''
return self._search(-1,partial)
if __name__=="__main__":
q=LineHistory()
RL=lineobj.ReadLineTextBuffer
q.add_history(RL("apan"))
q.add_history(RL("apbn"))
q.add_history(RL("apcn"))
q.add_history(RL("apdn"))
q.add_history(RL("apen"))
+526
View File
@@ -0,0 +1,526 @@
# -*- coding: utf-8 -*-
#*****************************************************************************
# Copyright (C) 2006 Jorgen Stenarson. <jorgen.stenarson@bostream.nu>
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#*****************************************************************************
import re,operator
import wordmatcher
import pyreadline.clipboard as clipboard
class NotAWordError(IndexError):
pass
def quote_char(c):
if ord(c)>0:
return c
############## Line positioner ########################
class LinePositioner(object):
def __call__(self,line):
NotImplementedError("Base class !!!")
class NextChar(LinePositioner):
def __call__(self,line):
if line.point<len(line.line_buffer):
return line.point+1
else:
return line.point
NextChar=NextChar()
class PrevChar(LinePositioner):
def __call__(self,line):
if line.point>0:
return line.point-1
else:
return line.point
PrevChar=PrevChar()
class NextWordStart(LinePositioner):
def __call__(self,line):
return line.next_start_segment(line.line_buffer,line.is_word_token)[line.point]
NextWordStart=NextWordStart()
class NextWordEnd(LinePositioner):
def __call__(self,line):
return line.next_end_segment(line.line_buffer,line.is_word_token)[line.point]
NextWordEnd=NextWordEnd()
class PrevWordStart(LinePositioner):
def __call__(self,line):
return line.prev_start_segment(line.line_buffer,line.is_word_token)[line.point]
PrevWordStart=PrevWordStart()
class WordStart(LinePositioner):
def __call__(self,line):
if line.is_word_token(line.get_line_text()[Point(line):Point(line)+1]):
if Point(line)>0 and line.is_word_token(line.get_line_text()[Point(line)-1:Point(line)]):
return PrevWordStart(line)
else:
return line.point
else:
raise NotInWord
WordStart=WordStart()
class WordEnd(LinePositioner):
def __call__(self,line):
if line.is_word_token(line.get_line_text()[Point(line):Point(line)+1]):
if line.is_word_token(line.get_line_text()[Point(line)+1:Point(line)+2]):
return NextWordEnd(line)
else:
return line.point
else:
raise NotAWordError("Point is not in a word")
WordEnd=WordEnd()
class PrevWordEnd(LinePositioner):
def __call__(self,line):
return line.prev_end_segment(line.line_buffer,line.is_word_token)[line.point]
PrevWordEnd=PrevWordEnd()
class StartOfLine(LinePositioner):
def __call__(self,line):
return 0
StartOfLine=StartOfLine()
class EndOfLine(LinePositioner):
def __call__(self,line):
return len(line.line_buffer)
EndOfLine=EndOfLine()
class Point(LinePositioner):
def __call__(self,line):
return line.point
Point=Point()
class Mark(LinePositioner):
def __call__(self,line):
return line.mark
Mark=Mark()
all_positioners=[(value.__class__.__name__,value) for key,value in globals().items() if isinstance(value,LinePositioner)]
all_positioners.sort()
############### LineSlice #################
class LineSlice(object):
def __call__(self,line):
NotImplementedError("Base class !!!")
class CurrentWord(LineSlice):
def __call__(self,line):
return slice(WordStart(line),WordEnd(line),None)
CurrentWord=CurrentWord()
class NextWord(LineSlice):
def __call__(self,line):
work=TextLine(line)
work.point=NextWordStart
start=work.point
stop=NextWordEnd(work)
return slice(start,stop)
NextWord=NextWord()
class PrevWord(LineSlice):
def __call__(self,line):
work=TextLine(line)
work.point=PrevWordEnd
stop=work.point
start=PrevWordStart(work)
return slice(start,stop)
PrevWord=PrevWord()
class PointSlice(LineSlice):
def __call__(self,line):
return slice(Point(line),Point(line)+1,None)
PointSlice=PointSlice()
############### TextLine ######################
class TextLine(object):
def __init__(self,txtstr,point=None,mark=None):
self.line_buffer=[]
self._point=0
self.mark=-1
self.undo_stack=[]
if isinstance(txtstr,TextLine): #copy
if point is None:
self.point=txtstr.point
else:
self.point=point
if mark is None:
self.mark=txtstr.mark
else:
self.mark=mark
self.line_buffer=txtstr.line_buffer[:]
else:
self._insert_text(txtstr)
if point is None:
self.point=0
else:
self.point=point
if mark is None:
self.mark=-1
else:
self.mark=mark
self.is_word_token=wordmatcher.is_word_token
self.next_start_segment=wordmatcher.next_start_segment
self.next_end_segment=wordmatcher.next_end_segment
self.prev_start_segment=wordmatcher.prev_start_segment
self.prev_end_segment=wordmatcher.prev_end_segment
def push_undo(self):
ltext = self.get_line_text()
if self.undo_stack and ltext == self.undo_stack[-1].get_line_text():
self.undo_stack[-1].point = self.point
else:
self.undo_stack.append(self.copy())
def pop_undo(self):
if len(self.undo_stack) >= 2:
self.undo_stack.pop()
self.set_top_undo()
self.undo_stack.pop()
else:
self.reset_line()
self.undo_stack = []
def set_top_undo(self):
if self.undo_stack:
undo=self.undo_stack[-1]
self.line_buffer=undo.line_buffer
self.point=undo.point
self.mark=undo.mark
else:
pass
def __repr__(self):
return 'TextLine("%s",point=%s,mark=%s)'%(self.line_buffer,self.point,self.mark)
def copy(self):
return self.__class__(self)
def set_point(self,value):
if isinstance(value,LinePositioner):
self._point=value(self)
else:
self._point=value
def get_point(self):
return self._point
point=property(get_point,set_point)
def visible_line_width(self,position=Point):
"""Return the visible width of the text in line buffer up to position."""
return len(self[:position].quoted_text())
def quoted_text(self):
quoted = [ quote_char(c) for c in self.line_buffer ]
self.line_char_width = [ len(c) for c in quoted ]
return ''.join(quoted)
def get_line_text(self):
return ''.join(self.line_buffer)
def set_line(self, text, cursor=None):
self.line_buffer = [ c for c in str(text) ]
if cursor is None:
self.point = len(self.line_buffer)
else:
self.point = cursor
def reset_line(self):
self.line_buffer = []
self.point = 0
def _insert_text(self, text):
for c in text:
self.line_buffer.insert(self.point, c)
self.point += 1
def __getitem__(self,key):
#Check if key is LineSlice, convert to regular slice
#and continue processing
if isinstance(key,LineSlice):
key=key(self)
if isinstance(key,slice):
if key.step is None:
pass
else:
raise Error
if key.start is None:
start=StartOfLine(self)
elif isinstance(key.start,LinePositioner):
start=key.start(self)
else:
start=key.start
if key.stop is None:
stop=EndOfLine(self)
elif isinstance(key.stop,LinePositioner):
stop=key.stop(self)
else:
stop=key.stop
return TextLine(self.line_buffer[start:stop])
elif isinstance(key,LinePositioner):
return self.line_buffer[key(self)]
elif isinstance(key,tuple):
raise IndexError("Cannot use step in line buffer indexing") #Multiple slice not allowed
else:
return TextLine(self.line_buffer[key])
def __delitem__(self,key):
if isinstance(key,LineSlice):
key=key(self)
if isinstance(key,slice):
start=key.start
stop=key.stop
if isinstance(start,LinePositioner):
start=start(self)
if isinstance(stop,LinePositioner):
stop=stop(self)
elif isinstance(key,LinePositioner):
start=key(self)
stop=start+1
else:
start=key
stop=key+1
if self.point>stop:
self.point=self.point-(stop-start)
elif self.point>=start and self.point <=stop:
self.point=start
prev=self.line_buffer[:start]
rest=self.line_buffer[stop:]
self.line_buffer=prev+rest
def __setitem__(self,key,value):
if isinstance(key,LineSlice):
key=key(self)
if isinstance(key,slice):
start=key.start
stop=key.stop
prev=self.line_buffer[:start]
rest=self.line_buffer[stop:]
elif isinstance(key,LinePositioner):
start=key(self)
stop=start+1
else:
start=key
stop=key+1
value=TextLine(value).line_buffer
self.line_buffer=prev+value+rest
def upper(self):
self.line_buffer=self.line_buffer.upper()
def lower(self):
self.line_buffer=self.line_buffer.lower()
def startswith(self,txt):
return self.get_line_text().startswith(txt)
def endswith(self,txt):
return self.get_line_text().endswith(txt)
def __contains__(self,txt):
return txt in self.get_line_text()
lines=[TextLine("abc"),
TextLine("abc def"),
TextLine("abc def ghi"),
TextLine(" abc def "),
]
l=lines[2]
l.point=5
class ReadLineTextBuffer(TextLine):
def __init__(self,txtstr,point=None,mark=None):
super(ReadLineTextBuffer,self).__init__(txtstr,point,mark)
self.enable_win32_clipboard=True
def insert_text(self,char):
self._insert_text(char)
######### Movement
def beginning_of_line(self):
self.point=StartOfLine
def end_of_line(self):
self.point=EndOfLine
def forward_char(self):
self.point=NextChar
def backward_char(self):
self.point=PrevChar
def forward_word(self):
self.point=NextWordStart
def backward_word(self):
self.point=PrevWordStart
######### delete
def delete_char(self):
del self[Point]
def backward_delete_char(self):
self.backward_char()
self.delete_char()
def delete_current_word(self):
del self[CurrentWord]
def delete_horizontal_space(self):
pass
######### Case
def upcase_word(self):
try:
self[CurrentWord]=self[CurrentWord].line_buffer.upper()
except NotAWordError:
pass
def downcase_word(self):
try:
self[CurrentWord]=self[CurrentWord].line_buffer.lower()
except NotAWordError:
pass
def capitalize_word(self):
try:
self[CurrentWord]=self[CurrentWord].line_buffer.capitalize()
except NotAWordError:
pass
########### Transpose
def transpose_chars(self):
pass
def transpose_words(self):
pass
############ Kill
def kill_line(self):
if self.enable_win32_clipboard:
toclipboard="".join(self.line_buffer[self.point:])
clipboard.set_clipboard_text(toclipboard)
self.line_buffer[self.point:] = []
def backward_kill_line(self):
del self[StartOfLine:Point]
def unix_line_discard(self):
pass
def kill_word(self):
"""Kills to next word ending"""
del self[Point:NextWordEnd]
def backward_kill_word(self):
"""Kills to next word ending"""
pass
def unix_word_rubout(self):
pass
def kill_region(self):
pass
def copy_region_as_kill(self):
pass
def copy_backward_word(self):
pass
def copy_forward_word(self):
pass
def yank(self):
pass
def yank_pop(self):
pass
############## Mark
def set_mark(self):
self.mark=self.point
def exchange_point_and_mark(self):
pass
def copy_region_to_clipboard(self): # ()
'''Copy the text in the region to the windows clipboard.'''
if self.enable_win32_clipboard:
mark=min(self.mark,len(self.line_buffer))
cursor=min(self.point,len(self.line_buffer))
if self.mark==-1:
return
begin=min(cursor,mark)
end=max(cursor,mark)
toclipboard="".join(self.line_buffer[begin:end])
clipboard.SetClipboardText(str(toclipboard))
############## Paste
##################################################################
q=ReadLineTextBuffer("asff asFArw ewrWErhg",point=8)
q=TextLine("asff asFArw ewrWErhg",point=8)
def show_pos(buff,pos,chr="."):
l=len(buff.line_buffer)
def choice(bool):
if bool:
return chr
else:
return " "
return "".join([choice(pos==idx) for idx in range(l+1)])
def test_positioner(buff,points,positioner):
print (" %s "%positioner.__class__.__name__).center(40,"-")
buffstr=buff.line_buffer
print '"%s"'%(buffstr)
for point in points:
b=TextLine(buff,point=point)
out=[" "]*(len(buffstr)+1)
pos=positioner(b)
if pos==point:
out[pos]="&"
else:
out[point]="."
out[pos]="^"
print '"%s"'%("".join(out))
if __name__=="__main__":
print '%-15s "%s"'%("Position",q.get_line_text())
print '%-15s "%s"'%("Point",show_pos(q,q.point))
for name,positioner in all_positioners:
pos=positioner(q)
[]
print '%-15s "%s"'%(name,show_pos(q,pos,"^"))
+102
View File
@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
#*****************************************************************************
# Copyright (C) 2006 Jorgen Stenarson. <jorgen.stenarson@bostream.nu>
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#*****************************************************************************
import re,operator
def str_find_all(str,ch):
result=[]
index=0
while index>=0:
index=str.find(ch,index)
if index>=0:
result.append(index)
index+=1
return result
word_pattern=re.compile("(x*)")
def markwords(str,iswordfun):
markers={True:"x",False:"o"}
return "".join([markers[iswordfun(ch)] for ch in str])
def split_words(str,iswordfun):
return [x for x in word_pattern.split(markwords(str,iswordfun)) if x !=""]
def mark_start_segment(str,is_segment):
def mark_start(s):
if s[0:1]=="x":
return "s"+s[1:]
else:
return s
return "".join(map(mark_start,split_words(str,is_segment)))
def mark_end_segment(str,is_segment):
def mark_start(s):
if s[0:1]=="x":
return s[:-1]+"s"
else:
return s
return "".join(map(mark_start,split_words(str,is_segment)))
def mark_start_segment_index(str,is_segment):
return str_find_all(mark_start_segment(str,is_segment),"s")
def mark_end_segment_index(str,is_segment):
return [x+1 for x in str_find_all(mark_end_segment(str,is_segment),"s")]
################ Following are used in lineobj ###########################
def is_word_token(str):
return not is_non_word_token(str)
def is_non_word_token(str):
assert(len(str)==1)
if str in " \t\n":
return True
def next_start_segment(str,is_segment):
str="".join(str)
result=[]
for start in mark_start_segment_index(str,is_segment):
result[len(result):start]=[start for x in range(start-len(result))]
result[len(result):len(str)]=[len(str) for x in range(len(str)-len(result)+1)]
return result
def next_end_segment(str,is_segment):
str="".join(str)
result=[]
for start in mark_end_segment_index(str,is_segment):
result[len(result):start]=[start for x in range(start-len(result))]
result[len(result):len(str)]=[len(str) for x in range(len(str)-len(result)+1)]
return result
def prev_start_segment(str,is_segment):
str="".join(str)
result=[]
prev=0
for start in mark_start_segment_index(str,is_segment):
result[len(result):start+1]=[prev for x in range(start-len(result)+1)]
prev=start
result[len(result):len(str)]=[prev for x in range(len(str)-len(result)+1)]
return result
def prev_end_segment(str,is_segment):
str="".join(str)
result=[]
prev=0
for start in mark_end_segment_index(str,is_segment):
result[len(result):start+1]=[prev for x in range(start-len(result)+1)]
prev=start
result[len(result):len(str)]=[len(str) for x in range(len(str)-len(result)+1)]
return result
+4 -1
View File
@@ -34,6 +34,9 @@ It is not complete and does not necessarily strive for complete
compatibilty but rather convenience in use on the windows platform. It has been
tested for use with windows 2000 and windows xp.
There are currently no released version, only subversion access is possible.
use: easy_install pyreadline==dev
features:
- Copy and paste using the clipboard
- Smart paste for convenient use with ipython. Converting tab separated data to
@@ -44,7 +47,7 @@ features:
The latest development version is always available at the IPython subversion
repository_.
.. _repository: http://ipython.scipy.org/svn/ipython/pyreadline/trunk#egg=ipython-dev
.. _repository: http://ipython.scipy.org/svn/ipython/pyreadline/trunk#egg=pyreadline-dev
"""
license = 'BSD'
+114 -280
View File
@@ -25,7 +25,8 @@ import clipboard
from console import log
from keysyms import key_text_to_keyinfo
import lineeditor.lineobj as lineobj
import lineeditor.history as history
def quote_char(c):
@@ -38,19 +39,16 @@ class ReadlineError(exceptions.Exception):
def inword(buffer,point):
return buffer[point:point+1] in [A-Za-z0-9]
class GetSetError(ReadlineError):
pass
class Readline:
class Readline(object):
def __init__(self):
self.startup_hook = None
self.pre_input_hook = None
self.completer = None
self.completer_delims = " \t\n\"\\'`@$><=;|&{("
self.history_length = -1
self.history = [] # strings for previous commands
self.history_cursor = 0
self.undo_stack = [] # each entry is a tuple with cursor_position and line_text
self.line_buffer = []
self.line_cursor = 0
self.console = console.Console()
self.size = self.console.size()
self.prompt_color = None
@@ -85,6 +83,24 @@ class Readline:
self.paste_line_buffer=[]
# self.line_buffer = []
# self.line_cursor = 0
self.l_buffer=lineobj.ReadLineTextBuffer("")
self._history=history.LineHistory()
def _g(x):
def g(self):
raise GetSetError("GET %s"%x)
def s(self,q):
raise GetSetError("SET %s"%x)
return g,s
line_buffer=property(*_g("line_buffer"))
line_cursor=property(*_g("line_buffer"))
undo_stack =property(*_g("undo_stack")) # each entry is a tuple with cursor_position and line_text
history_length =property(*_g("history_length")) # each entry is a tuple with cursor_position and line_text
history =property(*_g("history")) # each entry is a tuple with cursor_position and line_text
history_cursor =property(*_g("history_cursor")) # each entry is a tuple with cursor_position and line_text
def rl_settings_to_string(self):
out=["%-20s: %s"%("show all if ambigous",self.show_all_if_ambiguous)]
@@ -110,26 +126,6 @@ class Readline:
else:
raise ReadlineError("Bellstyle %s unknown."%self.bell_style)
def _quoted_text(self):
quoted = [ quote_char(c) for c in self.line_buffer ]
self.line_char_width = [ len(c) for c in quoted ]
return ''.join(quoted)
def _line_text(self):
return ''.join(self.line_buffer)
def _set_line(self, text, cursor=None):
self.line_buffer = [ c for c in str(text) ]
if cursor is None:
self.line_cursor = len(self.line_buffer)
else:
self.line_cursor = cursor
def _reset_line(self):
self.line_buffer = []
self.line_cursor = 0
self.undo_stack = []
def _clear_after(self):
c = self.console
x, y = c.pos()
@@ -141,7 +137,7 @@ class Readline:
c = self.console
xc, yc = self.prompt_end_pos
w, h = c.size()
xc += reduce(operator.add, self.line_char_width[0:self.line_cursor], 0)
xc += self.l_buffer.visible_line_width()
while(xc > w):
xc -= w
yc += 1
@@ -166,7 +162,7 @@ class Readline:
def _update_line(self):
c=self.console
c.pos(*self.prompt_end_pos)
ltext = self._quoted_text()
ltext = self.l_buffer.quoted_text()
n = c.write_scrolling(ltext, self.command_color)
self._update_prompt_pos(n)
self._clear_after()
@@ -185,35 +181,23 @@ class Readline:
#Process exit keys. Only exit on empty line
if event.keyinfo in self.exit_dispatch:
if len(self.line_buffer) == 0:
if lineobj.EndOfLine(self.l_buffer) == 0:
raise EOFError
try:
dispatch_func = self.key_dispatch[event.keyinfo]
except KeyError:
# unknown? try printing it anyway
#if event.keyinfo[0]!=True:
self.self_insert(event) #insert only if ctrl is not pressed
#c.bell()
continue
dispatch_func = self.key_dispatch.get(event.keyinfo,self.self_insert)
log("readline from keyboard:%s"%(event.keyinfo,))
r = None
if dispatch_func:
r = dispatch_func(event)
ltext = self._line_text()
if self.undo_stack and ltext == self.undo_stack[-1][1]:
self.undo_stack[-1][0] = self.line_cursor
else:
self.undo_stack.append([self.line_cursor, ltext])
self.l_buffer.push_undo()
self.previous_func = dispatch_func
if r:
self._update_line()
break
def readline(self, prompt=''):
'''Try to act like GNU readline.'''
# handle startup_hook
if self.first_prompt:
self.first_prompt = False
@@ -225,7 +209,7 @@ class Readline:
traceback.print_exc()
c = self.console
self._reset_line()
self.l_buffer.reset_line()
self.prompt = prompt
self._print_prompt()
@@ -239,25 +223,18 @@ class Readline:
log("in readline: %s"%self.paste_line_buffer)
if len(self.paste_line_buffer)>0:
#self.console.bell()
self._set_line(self.paste_line_buffer[0])
c.pos(*self.prompt_end_pos)
ltext = self._quoted_text()
n = c.write_scrolling(ltext, self.command_color)
self._update_prompt_pos(n)
self._clear_after()
self._set_cursor()
self.l_buffer=lineobj.ReadlineTextBuffer(self.paste_line_buffer[0])
self._update_line()
self.paste_line_buffer=self.paste_line_buffer[1:]
c.write('\r\n')
else:
self._readline_from_keyboard()
c.write('\r\n')
rtext = self._line_text()
self.add_history(rtext)
self.add_history(self.l_buffer.copy())
log('returning(%s)' % rtext)
return rtext + '\n'
log('returning(%s)' % self.l_buffer.get_line_text())
return self.l_buffer.get_line_text() + '\n'
def parse_and_bind(self, string):
'''Parse and execute single line of a readline init file.'''
@@ -295,41 +272,29 @@ class Readline:
def get_line_buffer(self):
'''Return the current contents of the line buffer.'''
return "".join(self.line_buffer)
return self.l_buffer.get_line_text()
def insert_text(self, string):
'''Insert text into the command line.'''
for c in string:
self.line_buffer.insert(self.line_cursor, c)
self.line_cursor += 1
self.l_buffer.insert_text(string)
def read_init_file(self, filename=None):
'''Parse a readline initialization file. The default filename is the last filename used.'''
log('read_init_file("%s")' % filename)
def read_history_file(self, filename=os.path.expanduser('~/.history')):
'''Load a readline history file. The default filename is ~/.history.'''
try:
for line in open(filename, 'rt'):
self.add_history(line.rstrip())
except IOError:
self.history = []
self.history_cursor = 0
raise IOError
self._history.read_history_file(filename)
def write_history_file(self, filename=os.path.expanduser('~/.history')):
'''Save a readline history file. The default filename is ~/.history.'''
fp = open(filename, 'wb')
for line in self.history:
fp.write(line)
fp.write('\n')
fp.close()
self._history.write_history_file(filename)
def get_history_length(self, ):
'''Return the desired length of the history file.
Negative values imply unlimited history file size.'''
return self.history_length
return self._history.get_history_length()
def set_history_length(self, length):
'''Set the number of lines to save in the history file.
@@ -337,7 +302,7 @@ class Readline:
write_history_file() uses this value to truncate the history file
when saving. Negative values imply unlimited history file size.
'''
self.history_length = length
self._history.set_history_length(length)
def set_startup_hook(self, function=None):
'''Set or remove the startup_hook function.
@@ -400,58 +365,35 @@ class Readline:
def add_history(self, line):
'''Append a line to the history buffer, as if it was the last line typed.'''
if not line:
pass
elif len(self.history) > 0 and self.history[-1] == line:
pass
else:
self.history.append(line)
if self.history_length > 0 and len(self.history) > self.history_length:
self.history = self.history[-self.history_length:]
self.history_cursor = len(self.history)
self._history.add_history(line)
### Methods below here are bindable functions
def beginning_of_line(self, e): # (C-a)
'''Move to the start of the current line. '''
self.line_cursor = 0
self.l_buffer.beginning_of_line()
def end_of_line(self, e): # (C-e)
'''Move to the end of the line. '''
self.line_cursor = len(self.line_buffer)
self.l_buffer.end_of_line()
def forward_char(self, e): # (C-f)
'''Move forward a character. '''
if self.line_cursor < len(self.line_buffer):
self.line_cursor += 1
else:
self._bell()
self.l_buffer.forward_char()
def backward_char(self, e): # (C-b)
'''Move back a character. '''
if self.line_cursor > 0:
self.line_cursor -= 1
else:
self._bell()
self.l_buffer.backward_char()
def forward_word(self, e): # (M-f)
'''Move forward to the end of the next word. Words are composed of
letters and digits.'''
L = len(self.line_buffer)
while self.line_cursor < L:
self.line_cursor += 1
if self.line_cursor == L:
break
if self.line_buffer[self.line_cursor] not in string.letters + string.digits:
break
self.l_buffer.forward_word()
def backward_word(self, e): # (M-b)
'''Move back to the start of the current or previous word. Words are
composed of letters and digits.'''
while self.line_cursor > 0:
self.line_cursor -= 1
if self.line_buffer[self.line_cursor] not in string.letters + string.digits:
break
self.l_buffer.backward_word()
def clear_screen(self, e): # (C-l)
'''Clear the screen and redraw the current line, leaving the current
@@ -468,51 +410,29 @@ class Readline:
with add_history(). If this line is a modified history line, the
history line is restored to its original state.'''
return True
######### History commands
def previous_history(self, e): # (C-p)
'''Move back through the history list, fetching the previous command. '''
if self.history_cursor > 0:
self.history_cursor -= 1
line = self.history[self.history_cursor]
self._set_line(line)
else:
self._bell()
self._history.previous_history(self.l_buffer)
def next_history(self, e): # (C-n)
'''Move forward through the history list, fetching the next command. '''
if self.history_cursor < len(self.history) - 1:
self.history_cursor += 1
line = self.history[self.history_cursor]
self._set_line(line)
elif self.undo_stack:
cursor, text = self.undo_stack[-1]
self._set_line(text, cursor)
else:
self._bell()
self._history.next_history(self.l_buffer)
def beginning_of_history(self, e): # (M-<)
'''Move to the first line in the history.'''
self.history_cursor = 0
if len(self.history) > 0:
self._set_line(self.history[0])
else:
self._bell()
self._history.beginning_of_history()
def end_of_history(self, e): # (M->)
'''Move to the end of the input history, i.e., the line currently
being entered.'''
if self.undo_stack:
cursor, text = self.undo_stack[-1]
self._set_line(text, cursor)
else:
self._bell()
self._history.end_of_history(self.l_buffer)
def _i_search(self, direction, init_event):
def _i_search(self, searchfun, direction, init_event):
c = self.console
line = self._line_text()
line = self.get_line_buffer()
query = ''
hc_start = self.history_cursor + direction
hc = hc_start
hc_start = self._history.history_cursor #+ direction
while 1:
x, y = self.prompt_end_pos
c.pos(0, y)
@@ -529,123 +449,66 @@ class Readline:
if event.keysym == 'BackSpace':
if len(query) > 0:
query = query[:-1]
hc = hc_start
self._history.history_cursor = hc_start
else:
self._bell()
elif event.char in string.letters + string.digits + string.punctuation + ' ':
self._history.history_cursor = hc_start
query += event.char
hc = hc_start
elif event.keyinfo == init_event.keyinfo:
hc += direction
self._history.history_cursor += direction
line=searchfun(query)
pass
else:
if event.keysym != 'Return':
self._bell()
break
while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)):
if self.history[hc].find(query) >= 0:
break
hc += direction
else:
self._bell()
continue
line = self.history[hc]
line=searchfun(query)
px, py = self.prompt_begin_pos
c.pos(0, py)
self._set_line(line)
self.l_buffer.set_line(line)
self._print_prompt()
self._history.history_cursor=len(self._history.history)
def reverse_search_history(self, e): # (C-r)
'''Search backward starting at the current line and moving up
through the history as necessary. This is an incremental search.'''
self._i_search(-1, e)
# print "HEJ"
# self.console.bell()
self._i_search(self._history.reverse_search_history, -1, e)
def forward_search_history(self, e): # (C-s)
'''Search forward starting at the current line and moving down
through the the history as necessary. This is an incremental search.'''
self._i_search(1, e)
def _non_i_search(self, direction):
c = self.console
line = self._line_text()
query = ''
while 1:
c.pos(*self.prompt_end_pos)
scroll = c.write_scrolling(":%s" % query)
self._update_prompt_pos(scroll)
self._clear_after()
event = c.getkeypress()
if event.keysym == 'BackSpace':
if len(query) > 0:
query = query[:-1]
else:
break
elif event.char in string.letters + string.digits + string.punctuation + ' ':
query += event.char
elif event.keysym == 'Return':
break
else:
self._bell()
if query:
hc = self.history_cursor - 1
while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)):
if self.history[hc].find(query) >= 0:
self._set_line(self.history[hc])
self.history_cursor = hc
return
hc += direction
else:
self._bell()
# print "HEJ"
# self.console.bell()
self._i_search(self._history.forward_search_history, 1, e)
def non_incremental_reverse_search_history(self, e): # (M-p)
'''Search backward starting at the current line and moving up
through the history as necessary using a non-incremental search for
a string supplied by the user.'''
self._non_i_search(-1)
self._history.non_incremental_reverse_search_history(self.l_buffer)
def non_incremental_forward_search_history(self, e): # (M-n)
'''Search forward starting at the current line and moving down
through the the history as necessary using a non-incremental search
for a string supplied by the user.'''
self._non_i_search(1)
def _search(self, direction):
c = self.console
if (self.previous_func != self.history_search_forward and
self.previous_func != self.history_search_backward):
self.query = ''.join(self.line_buffer[0:self.line_cursor])
hc = self.history_cursor + direction
while (direction < 0 and hc >= 0) or (direction > 0 and hc < len(self.history)):
h = self.history[hc]
if not self.query:
self._set_line(h)
self.history_cursor = hc
return
elif h.startswith(self.query) and h != self._line_text:
self._set_line(h, len(self.query))
self.history_cursor = hc
return
hc += direction
else:
self._set_line(self.query)
self._bell()
self._history.non_incremental_reverse_search_history(self.l_buffer)
def history_search_forward(self, e): # ()
'''Search forward through the history for the string of characters
between the start of the current line and the point. This is a
non-incremental search. By default, this command is unbound.'''
self._search(1)
self.l_buffer=self._history.history_search_forward(self.l_buffer)
def history_search_backward(self, e): # ()
'''Search backward through the history for the string of characters
between the start of the current line and the point. This is a
non-incremental search. By default, this command is unbound.'''
self._search(-1)
self.l_buffer=self._history.history_search_backward(self.l_buffer)
def yank_nth_arg(self, e): # (M-C-y)
'''Insert the first argument to the previous command (usually the
@@ -666,17 +529,12 @@ class Readline:
'''Delete the character at point. If point is at the beginning of
the line, there are no characters in the line, and the last
character typed was not bound to delete-char, then return EOF.'''
if self.line_cursor < len(self.line_buffer):
del self.line_buffer[self.line_cursor]
else:
self._bell()
self.l_buffer.delete_char()
def backward_delete_char(self, e): # (Rubout)
'''Delete the character behind the cursor. A numeric argument means
to kill the characters instead of deleting them.'''
if self.line_cursor > 0:
del self.line_buffer[self.line_cursor-1]
self.line_cursor -= 1
self.l_buffer.backward_delete_char()
def forward_backward_delete_char(self, e): # ()
'''Delete the character under the cursor, unless the cursor is at
@@ -688,8 +546,7 @@ class Readline:
'''Add the next character typed to the line verbatim. This is how to
insert key sequences like C-q, for example.'''
e = self.console.getkeypress()
self.line_buffer.insert(self.line_cursor, e.char)
self.line_cursor += 1
self.insert_text(e.char)
def tab_insert(self, e): # (M-TAB)
'''Insert a tab character. '''
@@ -699,36 +556,35 @@ class Readline:
def self_insert(self, e): # (a, b, A, 1, !, ...)
'''Insert yourself. '''
if ord(e.char)!=0: #don't insert null character in buffer, can happen with dead keys.
self.line_buffer.insert(self.line_cursor, e.char)
self.line_cursor += 1
self.insert_text(e.char)
def transpose_chars(self, e): # (C-t)
'''Drag the character before the cursor forward over the character
at the cursor, moving the cursor forward as well. If the insertion
point is at the end of the line, then this transposes the last two
characters of the line. Negative arguments have no effect.'''
pass
self.l_buffer.transpose_chars()
def transpose_words(self, e): # (M-t)
'''Drag the word before point past the word after point, moving
point past that word as well. If the insertion point is at the end
of the line, this transposes the last two words on the line.'''
pass
self.l_buffer.transpose_words()
def upcase_word(self, e): # (M-u)
'''Uppercase the current (or following) word. With a negative
argument, uppercase the previous word, but do not move the cursor.'''
pass
self.l_buffer.upcase_word()
def downcase_word(self, e): # (M-l)
'''Lowercase the current (or following) word. With a negative
argument, lowercase the previous word, but do not move the cursor.'''
pass
self.l_buffer.downcase_word()
def capitalize_word(self, e): # (M-c)
'''Capitalize the current (or following) word. With a negative
argument, capitalize the previous word, but do not move the cursor.'''
pass
self.l_buffer.capitalize_word()
def overwrite_mode(self, e): # ()
'''Toggle overwrite mode. With an explicit positive numeric
@@ -740,56 +596,40 @@ class Readline:
the text to the right. Characters bound to backward-delete-char
replace the character before point with a space.'''
pass
def kill_line(self, e): # (C-k)
'''Kill the text from point to the end of the line. '''
if self.enable_win32_clipboard:
toclipboard="".join(self.line_buffer[self.line_cursor:])
clipboard.set_clipboard_text(toclipboard)
self.line_buffer[self.line_cursor:] = []
self.l_buffer.kill_line()
def backward_kill_line(self, e): # (C-x Rubout)
'''Kill backward to the beginning of the line. '''
self.line_buffer[:self.line_cursor] = []
self.line_cursor = 0
self.l_buffer.backward_kill_line()
def unix_line_discard(self, e): # (C-u)
'''Kill backward from the cursor to the beginning of the current line. '''
# how is this different from backward_kill_line?
self.line_buffer[:self.line_cursor] = []
self.line_cursor = 0
self.l_buffer.unix_line_discard()
def kill_whole_line(self, e): # ()
'''Kill all characters on the current line, no matter where point
is. By default, this is unbound.'''
self.line_buffer=self.line_buffer[:0]
self.line_cursor=0
self.l_buffer.kill_whole_line()
def kill_word(self, e): # (M-d)
'''Kill from point to the end of the current word, or if between
words, to the end of the next word. Word boundaries are the same as
forward-word.'''
begin = self.line_cursor
self.forward_word(e)
self.line_buffer[begin:self.line_cursor] = []
self.line_cursor = begin
self.l_buffer.kill_word()
def backward_kill_word(self, e): # (M-DEL)
'''Kill the word behind point. Word boundaries are the same as
backward-word. '''
begin = self.line_cursor
self.backward_word(e)
self.line_buffer[self.line_cursor:begin] = []
self.l_buffer.backward_kill_word()
def unix_word_rubout(self, e): # (C-w)
'''Kill the word behind point, using white space as a word
boundary. The killed text is saved on the kill-ring.'''
begin = self.line_cursor
while self.line_cursor > 0:
self.line_cursor -= 1
if self.line_buffer[self.line_cursor] == ' ':
break
self.line_buffer[self.line_cursor:begin] = []
self.l_buffer.unix_word_rubout()
def delete_horizontal_space(self, e): # ()
'''Delete all spaces and tabs around point. By default, this is unbound. '''
@@ -807,13 +647,13 @@ class Readline:
def copy_region_to_clipboard(self, e): # ()
'''Copy the text in the region to the windows clipboard.'''
if self.enable_win32_clipboard:
mark=min(self.mark,len(self.line_buffer))
cursor=min(self.line_cursor,len(self.line_buffer))
if self.mark==-1:
mark=min(self.l_buffer.mark,len(self.l_buffer.line_buffer))
cursor=min(self.l_buffer.point,len(self.l_buffer.line_buffer))
if self.l_buffer.mark==-1:
return
begin=min(cursor,mark)
end=max(cursor,mark)
toclipboard="".join(self.line_buffer[begin:end])
toclipboard="".join(self.l_buffer.line_buffer[begin:end])
clipboard.SetClipboardText(str(toclipboard))
def copy_backward_word(self, e): # ()
@@ -843,6 +683,7 @@ class Readline:
t=[row for row in t if row.strip()!=""] #remove empty lines
if t!=[""]:
self.insert_text(t[0])
self.add_history(self.l_buffer.copy())
self.paste_line_buffer=t[1:]
log("multi: %s"%self.paste_line_buffer)
return True
@@ -896,16 +737,17 @@ class Readline:
Also set begidx and endidx in the process.'''
completions = []
self.begidx = self.line_cursor
self.endidx = self.line_cursor
self.begidx = self.l_buffer.point
self.endidx = self.l_buffer.point
buf=self.l_buffer.line_buffer
if self.completer:
# get the string to complete
while self.begidx > 0:
self.begidx -= 1
if self.line_buffer[self.begidx] in self.completer_delims:
if buf[self.begidx] in self.completer_delims:
self.begidx += 1
break
text = ''.join(self.line_buffer[self.begidx:self.endidx])
text = ''.join(buf[self.begidx:self.endidx])
log('complete text="%s"' % text)
i = 0
while 1:
@@ -923,10 +765,10 @@ class Readline:
# get the filename to complete
while self.begidx > 0:
self.begidx -= 1
if self.line_buffer[self.begidx] in ' \t\n':
if buf[self.begidx] in ' \t\n':
self.begidx += 1
break
text = ''.join(self.line_buffer[self.begidx:self.endidx])
text = ''.join(buf[self.begidx:self.endidx])
log('file complete text="%s"' % text)
completions = glob(os.path.expanduser(text) + '*')
if self.mark_directories == 'on':
@@ -965,8 +807,8 @@ class Readline:
if completions:
cprefix = commonprefix(completions)
rep = [ c for c in cprefix ]
self.line_buffer[self.begidx:self.endidx] = rep
self.line_cursor += len(rep) - (self.endidx - self.begidx)
self.l_buffer[self.begidx:self.endidx] = rep
self.l_buffer.point += len(rep) - (self.endidx - self.begidx)
if len(completions) > 1:
if self.show_all_if_ambiguous == 'on':
self._display_completions(completions)
@@ -989,10 +831,10 @@ class Readline:
for comp in completions:
rep = [ c for c in comp ]
rep.append(' ')
self.line_buffer[b:e] = rep
self.l_buffer[b:e] = rep
b += len(rep)
e = b
self.line_cursor = b
self.line_cursor = b
def menu_complete(self, e): # ()
'''Similar to complete, but replaces the word to be completed with a
@@ -1049,15 +891,7 @@ class Readline:
def undo(self, e): # (C-_ or C-x C-u)
'''Incremental undo, separately remembered for each line.'''
log(self.undo_stack)
if len(self.undo_stack) >= 2:
self.undo_stack.pop()
cursor, text = self.undo_stack.pop()
else:
cursor = 0
text = ''
self.undo_stack = []
self._set_line(text, cursor)
self.l_buffer.pop_undo()
def revert_line(self, e): # (M-r)
'''Undo all changes made to this line. This is like executing the
@@ -1071,7 +905,7 @@ class Readline:
def set_mark(self, e): # (C-@)
'''Set the mark to the point. If a numeric argument is supplied, the
mark is set to that position.'''
self.mark=self.line_cursor
self.l_buffer.set_mark()
def exchange_point_and_mark(self, e): # (C-x C-x)
'''Swap the point with the mark. The current cursor position is set
@@ -1284,4 +1118,4 @@ if __name__ == '__main__':
else:
#import wingdbstub
console.install_readline(rl.readline)
pass
+24
View File
@@ -0,0 +1,24 @@
This is the refactor branch of pyreadline.
Goals:
- Decouple console from readline. In order to simplify future handling of terminals onother platforms than win32
- Decouple keyboard shortcut logic from lineditor functions
- Add unittests
- Improve cut paste logic to be more like other windows apps
- Make all subfunctionality like console, clipboard etc pluggable to simplify moving to other platforms.
pyreadline
clipboard.py - clipboard functions
console.py - terminal functions, read keyboard write to screen
keysyms.py - translate descriptions of keypresses to actual keyinfo
release.py - release info, description, copyright
rlmain.py - contains readline class, this is the entry point to readline functionality
lineditor
history.py - history buffer
lineobj.py - Contains linebuffer object, contains all editing functionality
wordmatcher - Contains functions that match word and non-words