Home | History | Annotate | Download | only in idlelib
      1 """CodeContext - Extension to display the block context above the edit window
      2 
      3 Once code has scrolled off the top of a window, it can be difficult to
      4 determine which block you are in.  This extension implements a pane at the top
      5 of each IDLE edit window which provides block structure hints.  These hints are
      6 the lines which contain the block opening keywords, e.g. 'if', for the
      7 enclosing block.  The number of hint lines is determined by the numlines
      8 variable in the CodeContext section of config-extensions.def. Lines which do
      9 not open blocks are not shown in the context hints pane.
     10 
     11 """
     12 import Tkinter
     13 from Tkconstants import TOP, LEFT, X, W, SUNKEN
     14 import re
     15 from sys import maxint as INFINITY
     16 from idlelib.configHandler import idleConf
     17 
     18 BLOCKOPENERS = set(["class", "def", "elif", "else", "except", "finally", "for",
     19                     "if", "try", "while", "with"])
     20 UPDATEINTERVAL = 100 # millisec
     21 FONTUPDATEINTERVAL = 1000 # millisec
     22 
     23 getspacesfirstword =\
     24                    lambda s, c=re.compile(r"^(\s*)(\w*)"): c.match(s).groups()
     25 
     26 class 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)
    177