import Tkinter from configHandler import idleConf from PyShell import PyShell from string import whitespace import re BLOCKOPENERS = dict([(x, None) for x in ("class", "def", "elif", "else", "except", "finally", "for", "if", "try", "while")]) INFINITY = 1 << 30 UPDATEINTERVAL = 100 #ms FONTUPDATEINTERVAL = 1000 #ms getspacesfirstword = lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups() class CodeContext: menudefs = [] numlines = idleConf.GetOption("extensions", "CodeContext", "numlines", type="int", default=3) bgcolor = idleConf.GetOption("extensions", "CodeContext", "bgcolor", type="str", default="LightGray") fgcolor = idleConf.GetOption("extensions", "CodeContext", "fgcolor", type="str", default="Black") def __init__(self, editwin): if isinstance(editwin, PyShell): return self.editwin = editwin self.text = editwin.text self.textfont = self.text["font"] self.label = Tkinter.Label(self.editwin.top, text="\n" * (self.numlines - 1), anchor="w", justify="left", font=self.textfont, bg=self.bgcolor, fg=self.fgcolor, relief="sunken", width=1, # Don't request more than we get ) self.label.pack(side="top", fill="x", expand=0, after=self.editwin.status_bar) # A list with the info on the context of the last first line. # It includes tuples with: index of line, indentation of line, # and the text of the line. It is initialized by a dummy line, # which starts the "block" of the whole document. self.info = list(self.interesting_lines(1)) # The line to which the info relates self.lastfirstline = 1 self.text.after(UPDATEINTERVAL, self.timer_event) self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) def getIndentAndTextAndOpener(self, linenum): """ Return a tuple with the indentation of the given line number, and its text, and does the line start with an opener saved word (False if not, ot the opener if it does) The indentation of empty lines (or comment lines) is INFINITY. There is a dummy block opener, with indentation -1 and text "". """ if linenum == 0: return -1, "", True t = self.text.get("%d.0" % linenum, "%d.end" % linenum) spaces, firstword = getspacesfirstword(t) opener = firstword in BLOCKOPENERS and firstword # False, or the opener # itself. if len(t) == len(spaces) or t[len(spaces)] == '#': indent = INFINITY else: indent = len(spaces) return indent, t, opener def interesting_lines(self, firstline): """ A generator which yields for each interesting line (those are the lines shown above the text), scanning from the given first line, a tuple with: the line index, and the line text. """ # The indentation level we are being in: lastindent = INFINITY # For a line to be interesting, it must begin with a block-opener # saved word, and have a smaller indentation than lastindent. for curline in xrange(firstline, -1, -1): indent, t, opener = self.getIndentAndTextAndOpener(curline) if indent < lastindent: lastindent = indent if opener == "else": # We want to show the if statement to. lastindent += 1 if opener and curline < firstline: yield curline, t def update_label(self): firstline = int(self.text.index("@0,0").split('.')[0]) if self.lastfirstline == firstline: return self.lastfirstline = firstline tmpstack = [] for curline, t in self.interesting_lines(firstline): # Remove irrelevant self.info items, and if we reached an item in # it (which must happen because of the dummy element), break. while self.info[-1][0] > curline: del self.info[-1] if self.info[-1][0] == curline: break # Add the block opener we found to tmpstack tmpstack.append((curline, t)) while tmpstack: self.info.append(tmpstack.pop()) lines = [""]*max(0, self.numlines - len(self.info)) + \ [x[1] for x in self.info[-self.numlines:]] self.label["text"] = '\n'.join(lines) def timer_event(self): self.update_label() self.text.after(UPDATEINTERVAL, self.timer_event) def font_timer_event(self): newtextfont = self.text["font"] if newtextfont != self.textfont: self.textfont = newtextfont self.label["font"] = self.textfont self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)