Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | """CodeContext - Display the block context of code at top of edit window |
2 | ||
3 | Once code has scrolled off the top of the screen, it can be difficult | |
4 | to determine which block you are in. This extension implements a pane | |
5 | at the top of each IDLE edit window which provides block structure | |
6 | hints. These hints are the lines which contain the block opening | |
7 | keywords, e.g. 'if', for the enclosing block. The number of hint lines | |
8 | is determined by the numlines variable in the CodeContext section of | |
9 | config-extensions.def. Lines which do not open blocks are not shown in | |
10 | the context hints pane. | |
11 | ||
12 | """ | |
13 | import Tkinter | |
14 | from configHandler import idleConf | |
15 | from sets import Set | |
16 | import re | |
17 | ||
18 | BLOCKOPENERS = Set(["class", "def", "elif", "else", "except", "finally", "for", | |
19 | "if", "try", "while"]) | |
20 | INFINITY = 1 << 30 | |
21 | UPDATEINTERVAL = 100 # millisec | |
22 | FONTUPDATEINTERVAL = 1000 # millisec | |
23 | ||
24 | getspacesfirstword = 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 | ||
29 | numlines = idleConf.GetOption("extensions", "CodeContext", | |
30 | "numlines", type="int", default=3) | |
31 | bgcolor = idleConf.GetOption("extensions", "CodeContext", | |
32 | "bgcolor", type="str", default="LightGray") | |
33 | fgcolor = idleConf.GetOption("extensions", "CodeContext", | |
34 | "fgcolor", type="str", default="Black") | |
35 | def __init__(self, editwin): | |
36 | self.editwin = editwin | |
37 | self.text = editwin.text | |
38 | self.textfont = self.text["font"] | |
39 | self.label = None | |
40 | # Dummy line, which starts the "block" of the whole document: | |
41 | self.info = list(self.interesting_lines(1)) | |
42 | self.lastfirstline = 1 | |
43 | visible = idleConf.GetOption("extensions", "CodeContext", | |
44 | "visible", type="bool", default=False) | |
45 | if visible: | |
46 | self.toggle_code_context_event() | |
47 | self.editwin.setvar('<<toggle-code-context>>', True) | |
48 | # Start two update cycles, one for context lines, one for font changes. | |
49 | self.text.after(UPDATEINTERVAL, self.timer_event) | |
50 | self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) | |
51 | ||
52 | def toggle_code_context_event(self, event=None): | |
53 | if not self.label: | |
54 | self.label = Tkinter.Label(self.editwin.top, | |
55 | text="\n" * (self.numlines - 1), | |
56 | anchor="w", justify="left", | |
57 | font=self.textfont, | |
58 | bg=self.bgcolor, fg=self.fgcolor, | |
59 | relief="sunken", | |
60 | width=1, # Don't request more than we get | |
61 | ) | |
62 | self.label.pack(side="top", fill="x", expand=0, | |
63 | after=self.editwin.status_bar) | |
64 | else: | |
65 | self.label.destroy() | |
66 | self.label = None | |
67 | idleConf.SetOption("extensions", "CodeContext", "visible", | |
68 | str(self.label is not None)) | |
69 | idleConf.SaveUserCfgFiles() | |
70 | ||
71 | def get_line_info(self, linenum): | |
72 | """Get the line indent value, text, and any block start keyword | |
73 | ||
74 | If the line does not start a block, the keyword value is False. | |
75 | The indentation of empty lines (or comment lines) is INFINITY. | |
76 | There is a dummy block start, with indentation -1 and text "". | |
77 | ||
78 | Return the indent level, text (including leading whitespace), | |
79 | and the block opening keyword. | |
80 | ||
81 | """ | |
82 | if linenum == 0: | |
83 | return -1, "", True | |
84 | text = self.text.get("%d.0" % linenum, "%d.end" % linenum) | |
85 | spaces, firstword = getspacesfirstword(text) | |
86 | opener = firstword in BLOCKOPENERS and firstword | |
87 | if len(text) == len(spaces) or text[len(spaces)] == '#': | |
88 | indent = INFINITY | |
89 | else: | |
90 | indent = len(spaces) | |
91 | return indent, text, opener | |
92 | ||
93 | def interesting_lines(self, firstline): | |
94 | """Generator which yields context lines, starting at firstline.""" | |
95 | # The indentation level we are currently in: | |
96 | lastindent = INFINITY | |
97 | # For a line to be interesting, it must begin with a block opening | |
98 | # keyword, and have less indentation than lastindent. | |
99 | for line_index in xrange(firstline, -1, -1): | |
100 | indent, text, opener = self.get_line_info(line_index) | |
101 | if indent < lastindent: | |
102 | lastindent = indent | |
103 | if opener in ("else", "elif"): | |
104 | # We also show the if statement | |
105 | lastindent += 1 | |
106 | if opener and line_index < firstline: | |
107 | yield line_index, text | |
108 | ||
109 | def update_label(self): | |
110 | firstline = int(self.text.index("@0,0").split('.')[0]) | |
111 | if self.lastfirstline == firstline: | |
112 | return | |
113 | self.lastfirstline = firstline | |
114 | tmpstack = [] | |
115 | for line_index, text in self.interesting_lines(firstline): | |
116 | # Remove irrelevant self.info items, and when we reach a relevant | |
117 | # item (which must happen because of the dummy element), break. | |
118 | while self.info[-1][0] > line_index: | |
119 | del self.info[-1] | |
120 | if self.info[-1][0] == line_index: | |
121 | break | |
122 | tmpstack.append((line_index, text)) | |
123 | while tmpstack: | |
124 | self.info.append(tmpstack.pop()) | |
125 | lines = [""] * max(0, self.numlines - len(self.info)) + \ | |
126 | [x[1] for x in self.info[-self.numlines:]] | |
127 | self.label["text"] = '\n'.join(lines) | |
128 | ||
129 | def timer_event(self): | |
130 | if self.label: | |
131 | self.update_label() | |
132 | self.text.after(UPDATEINTERVAL, self.timer_event) | |
133 | ||
134 | def font_timer_event(self): | |
135 | newtextfont = self.text["font"] | |
136 | if self.label and newtextfont != self.textfont: | |
137 | self.textfont = newtextfont | |
138 | self.label["font"] = self.textfont | |
139 | self.text.after(FONTUPDATEINTERVAL, self.font_timer_event) |