| Total Complexity | 52 |
| Total Lines | 239 |
| Duplicated Lines | 100 % |
| Changes | 12 | ||
| Bugs | 0 | Features | 1 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like NvimTk often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | """Neovim TKinter UI.""" |
||
| 26 | View Code Duplication | class NvimTk(object): |
|
|
|
|||
| 27 | |||
| 28 | """Wraps all nvim/tk event handling.""" |
||
| 29 | |||
| 30 | def __init__(self, nvim): |
||
| 31 | """Initialize with a Nvim instance.""" |
||
| 32 | self._nvim = nvim |
||
| 33 | self._attrs = {} |
||
| 34 | self._nvim_updates = deque() |
||
| 35 | self._canvas = None |
||
| 36 | self._fg = '#000000' |
||
| 37 | self._bg = '#ffffff' |
||
| 38 | |||
| 39 | def run(self): |
||
| 40 | """Start the UI.""" |
||
| 41 | self._tk_setup() |
||
| 42 | t = Thread(target=self._nvim_event_loop) |
||
| 43 | t.daemon = True |
||
| 44 | t.start() |
||
| 45 | self._root.mainloop() |
||
| 46 | |||
| 47 | def _tk_setup(self): |
||
| 48 | self._root = Tk() |
||
| 49 | self._root.bind('<<nvim_redraw>>', self._tk_nvim_redraw) |
||
| 50 | self._root.bind('<<nvim_detach>>', self._tk_nvim_detach) |
||
| 51 | self._root.bind('<Key>', self._tk_key) |
||
| 52 | |||
| 53 | def _tk_nvim_redraw(self, *args): |
||
| 54 | update = self._nvim_updates.popleft() |
||
| 55 | for update in update: |
||
| 56 | handler = getattr(self, '_tk_nvim_' + update[0]) |
||
| 57 | for args in update[1:]: |
||
| 58 | handler(*args) |
||
| 59 | |||
| 60 | def _tk_nvim_detach(self, *args): |
||
| 61 | self._root.destroy() |
||
| 62 | |||
| 63 | def _tk_nvim_resize(self, width, height): |
||
| 64 | self._tk_redraw_canvas(width, height) |
||
| 65 | |||
| 66 | def _tk_nvim_clear(self): |
||
| 67 | self._tk_clear_region(0, self._height - 1, 0, self._width - 1) |
||
| 68 | |||
| 69 | def _tk_nvim_eol_clear(self): |
||
| 70 | row, col = (self._cursor_row, self._cursor_col,) |
||
| 71 | self._tk_clear_region(row, row, col, self._scroll_right) |
||
| 72 | |||
| 73 | def _tk_nvim_cursor_goto(self, row, col): |
||
| 74 | self._cursor_row = row |
||
| 75 | self._cursor_col = col |
||
| 76 | |||
| 77 | def _tk_nvim_cursor_on(self): |
||
| 78 | pass |
||
| 79 | |||
| 80 | def _tk_nvim_cursor_off(self): |
||
| 81 | pass |
||
| 82 | |||
| 83 | def _tk_nvim_mouse_on(self): |
||
| 84 | pass |
||
| 85 | |||
| 86 | def _tk_nvim_mouse_off(self): |
||
| 87 | pass |
||
| 88 | |||
| 89 | def _tk_nvim_insert_mode(self): |
||
| 90 | pass |
||
| 91 | |||
| 92 | def _tk_nvim_normal_mode(self): |
||
| 93 | pass |
||
| 94 | |||
| 95 | def _tk_nvim_set_scroll_region(self, top, bot, left, right): |
||
| 96 | self._scroll_top = top |
||
| 97 | self._scroll_bot = bot |
||
| 98 | self._scroll_left = left |
||
| 99 | self._scroll_right = right |
||
| 100 | |||
| 101 | def _tk_nvim_scroll(self, count): |
||
| 102 | top, bot = (self._scroll_top, self._scroll_bot,) |
||
| 103 | left, right = (self._scroll_left, self._scroll_right,) |
||
| 104 | |||
| 105 | if count > 0: |
||
| 106 | destroy_top = top |
||
| 107 | destroy_bot = top + count - 1 |
||
| 108 | move_top = destroy_bot + 1 |
||
| 109 | move_bot = bot |
||
| 110 | fill_top = move_bot + 1 |
||
| 111 | fill_bot = fill_top + count - 1 |
||
| 112 | else: |
||
| 113 | destroy_top = bot + count + 1 |
||
| 114 | destroy_bot = bot |
||
| 115 | move_top = top |
||
| 116 | move_bot = destroy_top - 1 |
||
| 117 | fill_bot = move_top - 1 |
||
| 118 | fill_top = fill_bot + count + 1 |
||
| 119 | |||
| 120 | # destroy items that would be moved outside the scroll region after |
||
| 121 | # scrolling |
||
| 122 | # self._tk_clear_region(destroy_top, destroy_bot, left, right) |
||
| 123 | # self._tk_clear_region(move_top, move_bot, left, right) |
||
| 124 | self._tk_destroy_region(destroy_top, destroy_bot, left, right) |
||
| 125 | self._tk_tag_region('move', move_top, move_bot, left, right) |
||
| 126 | self._canvas.move('move', 0, -count * self._rowsize) |
||
| 127 | self._canvas.dtag('move', 'move') |
||
| 128 | # self._tk_fill_region(fill_top, fill_bot, left, right) |
||
| 129 | |||
| 130 | def _tk_nvim_highlight_set(self, attrs): |
||
| 131 | self._attrs = attrs |
||
| 132 | |||
| 133 | def _tk_nvim_put(self, data): |
||
| 134 | # choose a Font instance |
||
| 135 | font = self._fnormal |
||
| 136 | if self._attrs.get('bold', False): |
||
| 137 | font = self._fbold |
||
| 138 | if self._attrs.get('italic', False): |
||
| 139 | font = self._fbolditalic if font == self._fbold else self._fitalic |
||
| 140 | # colors |
||
| 141 | fg = "#{0:0{1}x}".format(self._attrs.get('foreground', self._fg), 6) |
||
| 142 | bg = "#{0:0{1}x}".format(self._attrs.get('background', self._bg), 6) |
||
| 143 | # get the "text" and "rect" which correspond to the current cell |
||
| 144 | x, y = self._tk_get_coords(self._cursor_row, self._cursor_col) |
||
| 145 | items = self._canvas.find_overlapping(x, y, x + 1, y + 1) |
||
| 146 | if len(items) != 2: |
||
| 147 | # caught part the double-width character in the cell to the left, |
||
| 148 | # filter items which dont have the same horizontal coordinate as |
||
| 149 | # "x" |
||
| 150 | predicate = lambda item: self._canvas.coords(item)[0] == x |
||
| 151 | items = filter(predicate, items) |
||
| 152 | # rect has lower id than text, sort to unpack correctly |
||
| 153 | rect, text = sorted(items) |
||
| 154 | self._canvas.itemconfig(text, fill=fg, font=font, text=data or ' ') |
||
| 155 | self._canvas.itemconfig(rect, fill=bg) |
||
| 156 | self._tk_nvim_cursor_goto(self._cursor_row, self._cursor_col + 1) |
||
| 157 | |||
| 158 | def _tk_nvim_bell(self): |
||
| 159 | self._root.bell() |
||
| 160 | |||
| 161 | def _tk_nvim_update_fg(self, fg): |
||
| 162 | self._fg = "#{0:0{1}x}".format(fg, 6) |
||
| 163 | |||
| 164 | def _tk_nvim_update_bg(self, bg): |
||
| 165 | self._bg = "#{0:0{1}x}".format(bg, 6) |
||
| 166 | |||
| 167 | def _tk_redraw_canvas(self, width, height): |
||
| 168 | if self._canvas: |
||
| 169 | self._canvas.destroy() |
||
| 170 | self._fnormal = Font(family='Monospace', size=13) |
||
| 171 | self._fbold = Font(family='Monospace', weight='bold', size=13) |
||
| 172 | self._fitalic = Font(family='Monospace', slant='italic', size=13) |
||
| 173 | self._fbolditalic = Font(family='Monospace', weight='bold', |
||
| 174 | slant='italic', size=13) |
||
| 175 | self._colsize = self._fnormal.measure('A') |
||
| 176 | self._rowsize = self._fnormal.metrics('linespace') |
||
| 177 | self._canvas = Canvas(self._root, width=self._colsize * width, |
||
| 178 | height=self._rowsize * height) |
||
| 179 | self._tk_fill_region(0, height - 1, 0, width - 1) |
||
| 180 | self._cursor_row = 0 |
||
| 181 | self._cursor_col = 0 |
||
| 182 | self._scroll_top = 0 |
||
| 183 | self._scroll_bot = height - 1 |
||
| 184 | self._scroll_left = 0 |
||
| 185 | self._scroll_right = width - 1 |
||
| 186 | self._width, self._height = (width, height,) |
||
| 187 | self._canvas.pack() |
||
| 188 | |||
| 189 | def _tk_fill_region(self, top, bot, left, right): |
||
| 190 | # create columns from right to left so the left columns have a |
||
| 191 | # higher z-index than the right columns. This is required to |
||
| 192 | # properly display characters that cross cell boundary |
||
| 193 | for rownum in range(bot, top - 1, -1): |
||
| 194 | for colnum in range(right, left - 1, -1): |
||
| 195 | x1 = colnum * self._colsize |
||
| 196 | y1 = rownum * self._rowsize |
||
| 197 | x2 = (colnum + 1) * self._colsize |
||
| 198 | y2 = (rownum + 1) * self._rowsize |
||
| 199 | # for each cell, create two items: The rectangle is used for |
||
| 200 | # filling background and the text is for cell contents. |
||
| 201 | self._canvas.create_rectangle(x1, y1, x2, y2, |
||
| 202 | fill=self._background, width=0) |
||
| 203 | self._canvas.create_text(x1, y1, anchor='nw', |
||
| 204 | font=self._fnormal, width=1, |
||
| 205 | fill=self._foreground, text=' ') |
||
| 206 | |||
| 207 | def _tk_clear_region(self, top, bot, left, right): |
||
| 208 | self._tk_tag_region('clear', top, bot, left, right) |
||
| 209 | self._canvas.itemconfig('clear', fill=self._bg) |
||
| 210 | self._canvas.dtag('clear', 'clear') |
||
| 211 | |||
| 212 | def _tk_destroy_region(self, top, bot, left, right): |
||
| 213 | self._tk_tag_region('destroy', top, bot, left, right) |
||
| 214 | self._canvas.delete('destroy') |
||
| 215 | self._canvas.dtag('destroy', 'destroy') |
||
| 216 | |||
| 217 | def _tk_tag_region(self, tag, top, bot, left, right): |
||
| 218 | x1, y1 = self._tk_get_coords(top, left) |
||
| 219 | x2, y2 = self._tk_get_coords(bot, right) |
||
| 220 | self._canvas.addtag_overlapping(tag, x1, y1, x2 + 1, y2 + 1) |
||
| 221 | |||
| 222 | def _tk_get_coords(self, row, col): |
||
| 223 | x = col * self._colsize |
||
| 224 | y = row * self._rowsize |
||
| 225 | return x, y |
||
| 226 | |||
| 227 | def _tk_key(self, event): |
||
| 228 | if 0xffe1 <= event.keysym_num <= 0xffee: |
||
| 229 | # this is a modifier key, ignore. Source: |
||
| 230 | # https://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm |
||
| 231 | return |
||
| 232 | # Translate to Nvim representation of keys |
||
| 233 | send = [] |
||
| 234 | if event.state & 0x1: |
||
| 235 | send.append('S') |
||
| 236 | if event.state & 0x4: |
||
| 237 | send.append('C') |
||
| 238 | if event.state & (0x8 | 0x80): |
||
| 239 | send.append('A') |
||
| 240 | special = len(send) > 0 |
||
| 241 | key = event.char |
||
| 242 | if _is_invalid_key(key): |
||
| 243 | special = True |
||
| 244 | key = event.keysym |
||
| 245 | send.append(SPECIAL_KEYS.get(key, key)) |
||
| 246 | send = '-'.join(send) |
||
| 247 | if special: |
||
| 248 | send = '<' + send + '>' |
||
| 249 | nvim = self._nvim |
||
| 250 | nvim.session.threadsafe_call(lambda: nvim.input(send)) |
||
| 251 | |||
| 252 | def _nvim_event_loop(self): |
||
| 253 | self._nvim.session.run(self._nvim_request, |
||
| 254 | self._nvim_notification, |
||
| 255 | lambda: self._nvim.attach_ui(80, 24)) |
||
| 256 | self._root.event_generate('<<nvim_detach>>', when='tail') |
||
| 257 | |||
| 258 | def _nvim_request(self, method, args): |
||
| 259 | raise Exception('This UI does not implement any methods') |
||
| 260 | |||
| 261 | def _nvim_notification(self, method, args): |
||
| 262 | if method == 'redraw': |
||
| 263 | self._nvim_updates.append(args) |
||
| 264 | self._root.event_generate('<<nvim_redraw>>', when='tail') |
||
| 265 | |||
| 285 |