Coverage for parenmatch.py: 28%

87 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1"""ParenMatch -- for parenthesis matching. 

2 

3When you hit a right paren, the cursor should move briefly to the left 

4paren. Paren here is used generically; the matching applies to 

5parentheses, square brackets, and curly braces. 

6""" 

7from idlelib.hyperparser import HyperParser 

8from idlelib.config import idleConf 

9 

10_openers = {')':'(',']':'[','}':'{'} 

11CHECK_DELAY = 100 # milliseconds 

12 

13class ParenMatch: 

14 """Highlight matching openers and closers, (), [], and {}. 

15 

16 There are three supported styles of paren matching. When a right 

17 paren (opener) is typed: 

18 

19 opener -- highlight the matching left paren (closer); 

20 parens -- highlight the left and right parens (opener and closer); 

21 expression -- highlight the entire expression from opener to closer. 

22 (For back compatibility, 'default' is a synonym for 'opener'). 

23 

24 Flash-delay is the maximum milliseconds the highlighting remains. 

25 Any cursor movement (key press or click) before that removes the 

26 highlight. If flash-delay is 0, there is no maximum. 

27 

28 TODO: 

29 - Augment bell() with mismatch warning in status window. 

30 - Highlight when cursor is moved to the right of a closer. 

31 This might be too expensive to check. 

32 """ 

33 

34 RESTORE_VIRTUAL_EVENT_NAME = "<<parenmatch-check-restore>>" 

35 # We want the restore event be called before the usual return and 

36 # backspace events. 

37 RESTORE_SEQUENCES = ("<KeyPress>", "<ButtonPress>", 

38 "<Key-Return>", "<Key-BackSpace>") 

39 

40 def __init__(self, editwin): 

41 self.editwin = editwin 

42 self.text = editwin.text 

43 # Bind the check-restore event to the function restore_event, 

44 # so that we can then use activate_restore (which calls event_add) 

45 # and deactivate_restore (which calls event_delete). 

46 editwin.text.bind(self.RESTORE_VIRTUAL_EVENT_NAME, 

47 self.restore_event) 

48 self.counter = 0 

49 self.is_restore_active = 0 

50 

51 @classmethod 

52 def reload(cls): 

53 cls.STYLE = idleConf.GetOption( 

54 'extensions','ParenMatch','style', default='opener') 

55 cls.FLASH_DELAY = idleConf.GetOption( 

56 'extensions','ParenMatch','flash-delay', type='int',default=500) 

57 cls.BELL = idleConf.GetOption( 

58 'extensions','ParenMatch','bell', type='bool', default=1) 

59 cls.HILITE_CONFIG = idleConf.GetHighlight(idleConf.CurrentTheme(), 

60 'hilite') 

61 

62 def activate_restore(self): 

63 "Activate mechanism to restore text from highlighting." 

64 if not self.is_restore_active: 

65 for seq in self.RESTORE_SEQUENCES: 

66 self.text.event_add(self.RESTORE_VIRTUAL_EVENT_NAME, seq) 

67 self.is_restore_active = True 

68 

69 def deactivate_restore(self): 

70 "Remove restore event bindings." 

71 if self.is_restore_active: 

72 for seq in self.RESTORE_SEQUENCES: 

73 self.text.event_delete(self.RESTORE_VIRTUAL_EVENT_NAME, seq) 

74 self.is_restore_active = False 

75 

76 def flash_paren_event(self, event): 

77 "Handle editor 'show surrounding parens' event (menu or shortcut)." 

78 indices = (HyperParser(self.editwin, "insert") 

79 .get_surrounding_brackets()) 

80 self.finish_paren_event(indices) 

81 return "break" 

82 

83 def paren_closed_event(self, event): 

84 "Handle user input of closer." 

85 # If user bound non-closer to <<paren-closed>>, quit. 

86 closer = self.text.get("insert-1c") 

87 if closer not in _openers: 

88 return 

89 hp = HyperParser(self.editwin, "insert-1c") 

90 if not hp.is_in_code(): 

91 return 

92 indices = hp.get_surrounding_brackets(_openers[closer], True) 

93 self.finish_paren_event(indices) 

94 return # Allow calltips to see ')' 

95 

96 def finish_paren_event(self, indices): 

97 if indices is None and self.BELL: 

98 self.text.bell() 

99 return 

100 self.activate_restore() 

101 # self.create_tag(indices) 

102 self.tagfuncs.get(self.STYLE, self.create_tag_expression)(self, indices) 

103 # self.set_timeout() 

104 (self.set_timeout_last if self.FLASH_DELAY else 

105 self.set_timeout_none)() 

106 

107 def restore_event(self, event=None): 

108 "Remove effect of doing match." 

109 self.text.tag_delete("paren") 

110 self.deactivate_restore() 

111 self.counter += 1 # disable the last timer, if there is one. 

112 

113 def handle_restore_timer(self, timer_count): 

114 if timer_count == self.counter: 

115 self.restore_event() 

116 

117 # any one of the create_tag_XXX methods can be used depending on 

118 # the style 

119 

120 def create_tag_opener(self, indices): 

121 """Highlight the single paren that matches""" 

122 self.text.tag_add("paren", indices[0]) 

123 self.text.tag_config("paren", self.HILITE_CONFIG) 

124 

125 def create_tag_parens(self, indices): 

126 """Highlight the left and right parens""" 

127 if self.text.get(indices[1]) in (')', ']', '}'): 

128 rightindex = indices[1]+"+1c" 

129 else: 

130 rightindex = indices[1] 

131 self.text.tag_add("paren", indices[0], indices[0]+"+1c", rightindex+"-1c", rightindex) 

132 self.text.tag_config("paren", self.HILITE_CONFIG) 

133 

134 def create_tag_expression(self, indices): 

135 """Highlight the entire expression""" 

136 if self.text.get(indices[1]) in (')', ']', '}'): 

137 rightindex = indices[1]+"+1c" 

138 else: 

139 rightindex = indices[1] 

140 self.text.tag_add("paren", indices[0], rightindex) 

141 self.text.tag_config("paren", self.HILITE_CONFIG) 

142 

143 tagfuncs = { 

144 'opener': create_tag_opener, 

145 'default': create_tag_opener, 

146 'parens': create_tag_parens, 

147 'expression': create_tag_expression, 

148 } 

149 

150 # any one of the set_timeout_XXX methods can be used depending on 

151 # the style 

152 

153 def set_timeout_none(self): 

154 """Highlight will remain until user input turns it off 

155 or the insert has moved""" 

156 # After CHECK_DELAY, call a function which disables the "paren" tag 

157 # if the event is for the most recent timer and the insert has changed, 

158 # or schedules another call for itself. 

159 self.counter += 1 

160 def callme(callme, self=self, c=self.counter, 

161 index=self.text.index("insert")): 

162 if index != self.text.index("insert"): 

163 self.handle_restore_timer(c) 

164 else: 

165 self.editwin.text_frame.after(CHECK_DELAY, callme, callme) 

166 self.editwin.text_frame.after(CHECK_DELAY, callme, callme) 

167 

168 def set_timeout_last(self): 

169 """The last highlight created will be removed after FLASH_DELAY millisecs""" 

170 # associate a counter with an event; only disable the "paren" 

171 # tag if the event is for the most recent timer. 

172 self.counter += 1 

173 self.editwin.text_frame.after( 

174 self.FLASH_DELAY, 

175 lambda self=self, c=self.counter: self.handle_restore_timer(c)) 

176 

177 

178ParenMatch.reload() 

179 

180 

181if __name__ == '__main__': 181 ↛ 182line 181 didn't jump to line 182, because the condition on line 181 was never true

182 from unittest import main 

183 main('idlelib.idle_test.test_parenmatch', verbosity=2)