|
| 1 | +"""Smart SelectThis module provides smart selection functionality for Tkinter text widgets.By analyzing the character at the cursor position and its context, itautomatically selects text units such as words, lines, code blocks, bracketpairs, strings, and more.Functions: DisableTkDouble1Binding: Disables the default double-click binding of Tkinter text widgets to prevent conflicts with custom selection logic. TextSelector: Text selection operations, providing methods to select characters, lines, and ranges. SelectBlock: Selects code blocks based on indentation, supporting multi-line selection. SmartSelect: An event handler function that triggers smart selection based on the current character (e.g., letters, spaces, quotes, brackets).Usage: Bind SmartSelect to an event of the text widget (such as <Double-Button-1>) to enable automatic selection. Suitable for Tkinter applications that require enhanced text selection experience."""import re |
| 2 | + |
| 3 | +sp = lambda c: eval(c.replace('.',',')) |
| 4 | +jn = lambda x,y: '%i.%i'%(x,y) |
| 5 | +lc = lambda s: jn(s.count('\n')+1,len(s)-s.rfind('\n')-1) |
| 6 | + |
| 7 | +is_code = lambda s: s.split('#')[0].strip() |
| 8 | +is_blank = lambda s: not s.strip() |
| 9 | +get_indent = lambda s: re.match(r' *', s.rstrip()).end() |
| 10 | + |
| 11 | + |
| 12 | +def DisableTkDouble1Binding(root): |
| 13 | + tk = root.tk |
| 14 | + tk.eval('bind Text <Double-1> {catch {%W mark set insert sel.first}}') # See at: "~\tcl\tk8.6\text.tcl" |
| 15 | + |
| 16 | + |
| 17 | +class TextSelector: |
| 18 | + def __init__(self, text): |
| 19 | + self.text = text |
| 20 | + |
| 21 | + def select(self, idx1, idx2): |
| 22 | + self.text.tag_remove('sel', '1.0', 'end') |
| 23 | + self.text.tag_add('sel', idx1, idx2) |
| 24 | + self.text.mark_set('insert', idx1) |
| 25 | + |
| 26 | + def select_chars(self, n=1, n2=0): |
| 27 | + n, n2 = sorted([n, n2]) |
| 28 | + self.select('current%+dc' % n, 'current%+dc' % n2) |
| 29 | + |
| 30 | + def select_from_line(self, n1, n2): |
| 31 | + n2 = 'start%+dc' % n2 if isinstance(n2, int) else n2 |
| 32 | + self.select('current linestart+%dc' % n1, 'current line' + n2) |
| 33 | + |
| 34 | + def select_lines(self, n=1, n2=0): |
| 35 | + n, n2 = sorted([n, n2]) |
| 36 | + self.select('current linestart%+dl' % n, 'current linestart%+dl' % n2) |
| 37 | + |
| 38 | + |
| 39 | +def FindParent(s, c1='(', c2=')'): |
| 40 | + n1 = n2 = 0 |
| 41 | + for n, c in enumerate(s): |
| 42 | + n1 += c in c1 |
| 43 | + n2 += c in c2 |
| 44 | + if n1 and n1 == n2: |
| 45 | + return n |
| 46 | + |
| 47 | + |
| 48 | +def MatchSpan(r, s, n): |
| 49 | + for m in re.finditer(r, s): |
| 50 | + if m.start() <= n <= m.end(): |
| 51 | + return m.span() |
| 52 | + |
| 53 | + |
| 54 | +def SelectBlock(text, first_line=True): |
| 55 | + patt = re.compile(r'([ \t]*)(.*?)([ \t]*)(#.*)?') |
| 56 | + |
| 57 | + idx1 = 'current linestart' if first_line else 'current linestart-1l' |
| 58 | + lines = text.get(idx1, 'end').split('\n') |
| 59 | + base_indent = patt.fullmatch(lines[0]).group(1) |
| 60 | + |
| 61 | + ln = -1 |
| 62 | + for i, line in enumerate(lines[1:]): |
| 63 | + indent, code, space, comment = patt.fullmatch(line).groups() |
| 64 | + if code or comment: |
| 65 | + if indent > base_indent: |
| 66 | + ln = i |
| 67 | + else: |
| 68 | + break |
| 69 | + |
| 70 | + ln = max(1, ln + 2 if first_line else ln + 1) # at least select one line |
| 71 | + text_sel = TextSelector(text) |
| 72 | + text_sel.select_lines(ln) |
| 73 | + |
| 74 | + |
| 75 | +def SmartSelect(event): |
| 76 | + text = event.widget |
| 77 | + text_sel = TextSelector(text) |
| 78 | + text.tag_remove('hit', '1.0', 'end') |
| 79 | + |
| 80 | + if 'STRING' in text.tag_names('current'): |
| 81 | + st, ed = text.tag_prevrange('STRING', 'current+1c') # See at: `idlelib.squeezer.Squeezer.squeeze_current_text_event` |
| 82 | + ed2 = text.index(ed + '-1c') |
| 83 | + cur = text.index('current') |
| 84 | + if cur in (st, ed2): |
| 85 | + return text_sel.select(st, ed) |
| 86 | + |
| 87 | + cur = text.index('current') # should not be 'insert', it will cause the cursor position at the beginning of the automatic selection area |
| 88 | + col = sp(cur)[1] |
| 89 | + line = text.get('current linestart', 'current lineend') |
| 90 | + |
| 91 | + c = text.get('current') # charset: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ |
| 92 | + if c == ':': |
| 93 | + if is_code(line[col+1:]): |
| 94 | + text_sel.select_chars() |
| 95 | + else: |
| 96 | + SelectBlock(text) |
| 97 | + |
| 98 | + elif c == ' ' or c == '\n' and col == 0: |
| 99 | + indent = get_indent(line) |
| 100 | + if col <= indent: |
| 101 | + |
| 102 | + # | prev line | current line | action | |
| 103 | + # | ----------- | ------------ | ---------------- | |
| 104 | + # | blank | blank | select line | |
| 105 | + # | blank | indent | select block | |
| 106 | + # | indent | blank | select sub block | |
| 107 | + # | indent | same indent | select block | |
| 108 | + # | less indent | more indent | select sub block | |
| 109 | + # | more indent | less indent | select block | |
| 110 | + |
| 111 | + prev_line = text.get('current-1l linestart', 'current-1l lineend') |
| 112 | + if not is_blank(prev_line) and (get_indent(prev_line) < indent or is_blank(line)): |
| 113 | + SelectBlock(text, False) |
| 114 | + elif not is_blank(line): |
| 115 | + SelectBlock(text) |
| 116 | + else: |
| 117 | + text_sel.select_lines() |
| 118 | + else: |
| 119 | + p1, p2 = MatchSpan(r' +', line, col) |
| 120 | + text_sel.select_from_line(p1, p2) |
| 121 | + |
| 122 | + elif re.match(r'\w', c): |
| 123 | + p1, p2 = MatchSpan(r'\w+', line, col) |
| 124 | + text_sel.select_from_line(p1, p2) |
| 125 | + |
| 126 | + # sometimes will cause performance issues, so return it. |
| 127 | + return |
| 128 | + |
| 129 | + word = line[p1:p2] |
| 130 | + s = text.get('1.0', 'end') |
| 131 | + for m in re.finditer(r'\b%s\b' % word, s): |
| 132 | + p1, p2 = m.span() |
| 133 | + text.tag_add('hit', lc(s[:p1]), lc(s[:p2])) # should not be '1.0+nc', it will cause offseting when exist Squeezer |
| 134 | + |
| 135 | + elif c == '\n': |
| 136 | + text_sel.select_lines() |
| 137 | + |
| 138 | + elif c == '#': |
| 139 | + if 'COMMENT' in text.tag_names('current') and \ |
| 140 | + text.index('current') == text.tag_prevrange('COMMENT', 'current+1c')[0]: |
| 141 | + p1, p2 = MatchSpan(r' *#', line, col) |
| 142 | + if p1 > 0: |
| 143 | + text_sel.select_from_line(p1, 'end') |
| 144 | + else: |
| 145 | + text_sel.select_lines() |
| 146 | + else: |
| 147 | + text_sel.select_chars() |
| 148 | + |
| 149 | + elif c in '\'"`': # quote in comment or string |
| 150 | + s = text.get('current+1c', 'end') |
| 151 | + n = s.find(c) |
| 152 | + text_sel.select_chars(n + 2) |
| 153 | + |
| 154 | + elif c in '([{': |
| 155 | + c1 = c |
| 156 | + c2 = ')]}'['([{'.index(c1)] |
| 157 | + s = text.get('current', 'end') |
| 158 | + n = FindParent(s, c1, c2) |
| 159 | + text_sel.select_chars(n + 1) |
| 160 | + |
| 161 | + elif c in ')]}': |
| 162 | + c1 = c |
| 163 | + c2 = '([{'[')]}'.index(c1)] |
| 164 | + s = text.get('1.0', 'current+1c') |
| 165 | + n = FindParent(reversed(s), c1, c2) |
| 166 | + text_sel.select_chars(-n, 1) |
| 167 | + |
| 168 | + elif c == '\\': # e.g. \n \xhh \uxxxx \Uxxxxxxxx |
| 169 | + c2 = text.get('current+1c') |
| 170 | + n = {'x': 4, 'u': 6, 'U': 10}.get(c2, 2) |
| 171 | + text_sel.select_chars(n) |
| 172 | + |
| 173 | + elif c == '.': |
| 174 | + s = text.get('current+1c', 'current lineend') |
| 175 | + n = re.match(r'\s*\w*', s).end() |
| 176 | + text_sel.select_chars(n + 1) |
| 177 | + |
| 178 | + elif c == ',': # e.g. foo(a, b(c, d=e), f) |
| 179 | + s = text.get('current+1c', 'end') |
| 180 | + n1 = n2 = 0 |
| 181 | + for n, c1 in enumerate(s): |
| 182 | + if n1 == n2 and c1 in ',)]}': |
| 183 | + break |
| 184 | + n1 += c1 in '([{' |
| 185 | + n2 += c1 in ')]}' |
| 186 | + text_sel.select_chars(n + 1) |
| 187 | + |
| 188 | + else: # charset: !$%&*+,-/;<=>?@^|~ |
| 189 | + p1, p2 = MatchSpan(r'[^\w()[\]{}\'" ]+', line, col) # do not concatenate to brackets, spaces, or quotes |
| 190 | + text_sel.select_from_line(p1, p2) |
|
0 commit comments