PageRenderTime 285ms CodeModel.GetById 80ms app.highlight 101ms RepoModel.GetById 100ms app.codeStats 1ms

/Lib/idlelib/CodeContext.py

http://unladen-swallow.googlecode.com/
Python | 176 lines | 140 code | 3 blank | 33 comment | 22 complexity | a0fabc7e791a5837b5bdcc7f59d7ad26 MD5 | raw file
  1"""CodeContext - Extension to display the block context above the edit window
  2
  3Once code has scrolled off the top of a window, it can be difficult to
  4determine which block you are in.  This extension implements a pane at the top
  5of each IDLE edit window which provides block structure hints.  These hints are
  6the lines which contain the block opening keywords, e.g. 'if', for the
  7enclosing block.  The number of hint lines is determined by the numlines
  8variable in the CodeContext section of config-extensions.def. Lines which do
  9not open blocks are not shown in the context hints pane.
 10
 11"""
 12import Tkinter
 13from Tkconstants import TOP, LEFT, X, W, SUNKEN
 14from configHandler import idleConf
 15import re
 16from sys import maxint as INFINITY
 17
 18BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
 19                    "if", "try", "while", "with"])
 20UPDATEINTERVAL = 100 # millisec
 21FONTUPDATEINTERVAL = 1000 # millisec
 22
 23getspacesfirstword =\
 24                   lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
 25
 26class CodeContext:
 27    menudefs = [('options', [('!Code Conte_xt', '<<toggle-code-context>>')])]
 28    context_depth = idleConf.GetOption("extensions", "CodeContext",
 29                                       "numlines", type="int", default=3)
 30    bgcolor = idleConf.GetOption("extensions", "CodeContext",
 31                                 "bgcolor", type="str", default="LightGray")
 32    fgcolor = idleConf.GetOption("extensions", "CodeContext",
 33                                 "fgcolor", type="str", default="Black")
 34    def __init__(self, editwin):
 35        self.editwin = editwin
 36        self.text = editwin.text
 37        self.textfont = self.text["font"]
 38        self.label = None
 39        # self.info is a list of (line number, indent level, line text, block
 40        # keyword) tuples providing the block structure associated with
 41        # self.topvisible (the linenumber of the line displayed at the top of
 42        # the edit window). self.info[0] is initialized as a 'dummy' line which
 43        # starts the toplevel 'block' of the module.
 44        self.info = [(0, -1, "", False)]
 45        self.topvisible = 1
 46        visible = idleConf.GetOption("extensions", "CodeContext",
 47                                     "visible", type="bool", default=False)
 48        if visible:
 49            self.toggle_code_context_event()
 50            self.editwin.setvar('<<toggle-code-context>>', True)
 51        # Start two update cycles, one for context lines, one for font changes.
 52        self.text.after(UPDATEINTERVAL, self.timer_event)
 53        self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)
 54
 55    def toggle_code_context_event(self, event=None):
 56        if not self.label:
 57            # Calculate the border width and horizontal padding required to
 58            # align the context with the text in the main Text widget.
 59            #
 60            # All values are passed through int(str(<value>)), since some
 61            # values may be pixel objects, which can't simply be added to ints.
 62            widgets = self.editwin.text, self.editwin.text_frame
 63            # Calculate the required vertical padding
 64            padx = 0
 65            for widget in widgets:
 66                padx += int(str( widget.pack_info()['padx'] ))
 67                padx += int(str( widget.cget('padx') ))
 68            # Calculate the required border width
 69            border = 0
 70            for widget in widgets:
 71                border += int(str( widget.cget('border') ))
 72            self.label = Tkinter.Label(self.editwin.top,
 73                                       text="\n" * (self.context_depth - 1),
 74                                       anchor=W, justify=LEFT,
 75                                       font=self.textfont,
 76                                       bg=self.bgcolor, fg=self.fgcolor,
 77                                       width=1, #don't request more than we get
 78                                       padx=padx, border=border,
 79                                       relief=SUNKEN)
 80            # Pack the label widget before and above the text_frame widget,
 81            # thus ensuring that it will appear directly above text_frame
 82            self.label.pack(side=TOP, fill=X, expand=False,
 83                            before=self.editwin.text_frame)
 84        else:
 85            self.label.destroy()
 86            self.label = None
 87        idleConf.SetOption("extensions", "CodeContext", "visible",
 88                           str(self.label is not None))
 89        idleConf.SaveUserCfgFiles()
 90
 91    def get_line_info(self, linenum):
 92        """Get the line indent value, text, and any block start keyword
 93
 94        If the line does not start a block, the keyword value is False.
 95        The indentation of empty lines (or comment lines) is INFINITY.
 96
 97        """
 98        text = self.text.get("%d.0" % linenum, "%d.end" % linenum)
 99        spaces, firstword = getspacesfirstword(text)
100        opener = firstword in BLOCKOPENERS and firstword
101        if len(text) == len(spaces) or text[len(spaces)] == '#':
102            indent = INFINITY
103        else:
104            indent = len(spaces)
105        return indent, text, opener
106
107    def get_context(self, new_topvisible, stopline=1, stopindent=0):
108        """Get context lines, starting at new_topvisible and working backwards.
109
110        Stop when stopline or stopindent is reached. Return a tuple of context
111        data and the indent level at the top of the region inspected.
112
113        """
114        assert stopline > 0
115        lines = []
116        # The indentation level we are currently in:
117        lastindent = INFINITY
118        # For a line to be interesting, it must begin with a block opening
119        # keyword, and have less indentation than lastindent.
120        for linenum in xrange(new_topvisible, stopline-1, -1):
121            indent, text, opener = self.get_line_info(linenum)
122            if indent < lastindent:
123                lastindent = indent
124                if opener in ("else", "elif"):
125                    # We also show the if statement
126                    lastindent += 1
127                if opener and linenum < new_topvisible and indent >= stopindent:
128                    lines.append((linenum, indent, text, opener))
129                if lastindent <= stopindent:
130                    break
131        lines.reverse()
132        return lines, lastindent
133
134    def update_code_context(self):
135        """Update context information and lines visible in the context pane.
136
137        """
138        new_topvisible = int(self.text.index("@0,0").split('.')[0])
139        if self.topvisible == new_topvisible:      # haven't scrolled
140            return
141        if self.topvisible < new_topvisible:       # scroll down
142            lines, lastindent = self.get_context(new_topvisible,
143                                                 self.topvisible)
144            # retain only context info applicable to the region
145            # between topvisible and new_topvisible:
146            while self.info[-1][1] >= lastindent:
147                del self.info[-1]
148        elif self.topvisible > new_topvisible:     # scroll up
149            stopindent = self.info[-1][1] + 1
150            # retain only context info associated
151            # with lines above new_topvisible:
152            while self.info[-1][0] >= new_topvisible:
153                stopindent = self.info[-1][1]
154                del self.info[-1]
155            lines, lastindent = self.get_context(new_topvisible,
156                                                 self.info[-1][0]+1,
157                                                 stopindent)
158        self.info.extend(lines)
159        self.topvisible = new_topvisible
160        # empty lines in context pane:
161        context_strings = [""] * max(0, self.context_depth - len(self.info))
162        # followed by the context hint lines:
163        context_strings += [x[2] for x in self.info[-self.context_depth:]]
164        self.label["text"] = '\n'.join(context_strings)
165
166    def timer_event(self):
167        if self.label:
168            self.update_code_context()
169        self.text.after(UPDATEINTERVAL, self.timer_event)
170
171    def font_timer_event(self):
172        newtextfont = self.text["font"]
173        if self.label and newtextfont != self.textfont:
174            self.textfont = newtextfont
175            self.label["font"] = self.textfont
176        self.text.after(FONTUPDATEINTERVAL, self.font_timer_event)