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 |