-
Notifications
You must be signed in to change notification settings - Fork 1
/
intelligent_text_completion.py
437 lines (382 loc) · 17.3 KB
/
intelligent_text_completion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# Copyright (C) 2010 - Jens Nyman ([email protected])
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
import gedit
import gobject
import gtk
import pango
import re
import traceback
import gconf
class IntelligentTextCompletionPlugin(gedit.Plugin):
def __init__(self):
gedit.Plugin.__init__(self)
def create_configure_dialog(self):
return options_singleton().create_configure_dialog()
def _connect_view(self, view, window):
"""Connect to view's editing signals."""
callback = self._on_view_key_press_event
id = view.connect("key-press-event", callback, window)
view.set_data(self.__class__.__name__, (id))
def _on_window_tab_added(self, window, tab):
"""Connect to signals of the document and view in tab."""
name = self.__class__.__name__
view = tab.get_view()
handler_id = view.get_data(name)
if handler_id is None:
self._connect_view(view, window)
def _on_window_tab_removed(self, window, tab):
pass
def activate(self, window):
"""Activate plugin."""
callback = self._on_window_tab_added
id_1 = window.connect("tab-added", callback)
callback = self._on_window_tab_removed
id_2 = window.connect("tab-removed", callback)
window.set_data(self.__class__.__name__, (id_1, id_2))
views = window.get_views()
for view in views:
self._connect_view(view, window)
def deactivate(self, window):
"""Deactivate plugin."""
widgets = [window]
widgets.extend(window.get_views())
widgets.extend(window.get_documents())
name = self.__class__.__name__
for widget in widgets:
for handler_id in widget.get_data(name):
widget.disconnect(handler_id)
widget.set_data(name, None)
def _on_view_key_press_event(self, view, event, window):
window = gedit.app_get_default().get_active_window()
doc = window.get_active_document()
try:
return self._handle_event(view, event, window)
except:
err = "Exception\n"
err += traceback.format_exc()
doc.set_text(err)
############ plugin core functions ############
def _handle_event(self, view, event, window):
"""Key press event"""
### get vars ###
# constants
ignore_whitespace = '\t '
# get document
window = gedit.app_get_default().get_active_window()
doc = window.get_active_document()
# get cursor
cursor = doc.get_iter_at_mark(doc.get_insert())
# get typed string
typed_string = unicode(event.string)
# get previous char
prev_char = None
if not cursor.get_line_offset() == 0:
prev_char_pos = cursor.copy()
prev_char_pos.set_line_offset(cursor.get_line_offset() - 1)
prev_char = doc.get_text(prev_char_pos, cursor)
# get next char
next_char = None
if not cursor.ends_line():
next_char_pos = cursor.copy()
next_char_pos.set_line_offset(cursor.get_line_offset() + 1)
next_char = doc.get_text(cursor, next_char_pos)
# get line before cursor
line_start = cursor.copy()
line_start.set_line_offset(0)
preceding_line = doc.get_text(line_start, cursor)
# get line after cursor
line_end = cursor.copy()
if not cursor.ends_line():
line_end.forward_to_line_end()
line_after = doc.get_text(cursor, line_end)
# get whitespace in front of line
whitespace_pos = 0
whitespace = ""
while len(preceding_line) > whitespace_pos and preceding_line[whitespace_pos] in ignore_whitespace:
whitespace += preceding_line[whitespace_pos]
whitespace_pos += 1
# get options
options = options_singleton()
# Do not complete text after pasting text.
if len(typed_string) > 1:
return False
typed_char = typed_string
# do not complete if text is selected
bounds = doc.get_selection_bounds()
if len(bounds):
return False
################### auto-close brackets and quotes ###################
if options.closeBracketsAndQuotes:
open_close = {
'"': '"',
"'": "'",
'(': ')',
'{': '}',
'[': ']',
}
for check_char, add_char in open_close.items():
# if character user is adding is the same as the one that
# is auto-generated, remove the auto generated char
if typed_char == add_char:
if not cursor.ends_line():
if next_char == add_char:
doc.delete(cursor, next_char_pos)
continue
# typed_char equals char we're looking for
if typed_char == check_char:
# check for unlogical adding
if check_char == add_char:
# uneven number of add_char's in front
if len(re.findall(check_char, preceding_line)) % 2 == 1:
continue
# uneven number of add_char's in back
if len(re.findall(check_char, line_after)) % 2 == 1:
continue
# don't add add_char if it is used around text
non_text_left = ' \t\n\r,=+*/:;.?!$&@%~<>\\(){}[]-"\''
non_text_right = ' \t\n\r,=+*/:;.?!$&@%~<>\\)}]'
if not next_char and not check_char == "'":
# if we're just typing with nothing on the right,
# adding is OK as long as it isn't a "'".
pass
elif (not prev_char or prev_char in non_text_left) and (not next_char or next_char in non_text_right):
# this char is surrounded by nothing or non-text, therefore, we can add autotext
pass
elif check_char != add_char and (not next_char or next_char in non_text_right):
# this opening char has non-text on the right, therefore, we can add autotext
pass
else:
continue
# insert add_char
return self._insert_at_cursor(typed_char, add_char)
# check backspace
if event.keyval == gtk.keysyms.BackSpace:
if prev_char == check_char and next_char == add_char:
doc.delete(cursor, next_char_pos)
################### auto-complete XML tags ###################
if options.completeXML:
if prev_char == "<" and typed_char == "/":
start = doc.get_start_iter()
preceding_document = doc.get_text(start, cursor)
# analyse previous XML code
closing_tag = get_closing_xml_tag(preceding_document)
# insert code
if closing_tag:
return self._insert_at_cursor(typed_char + closing_tag + ">")
else:
return False # do nothing
################### detect lists ###################
if options.detectLists:
if event.keyval == gtk.keysyms.Return:
# constants
list_bullets = ['* ', '- ', '$ ', '> ', '+ ']
# cycle through all bullets
for bullet in list_bullets:
if len(preceding_line) >= whitespace_pos + len(bullet):
if preceding_line[whitespace_pos:whitespace_pos + len(bullet)] == bullet:
# endlist function by double enter
if preceding_line == whitespace + bullet and bullet != '* ':
start = cursor.copy()
start.set_line_offset(len(whitespace))
doc.delete(start, cursor)
return True
return self._insert_at_cursor(typed_char + whitespace + bullet)
################### detect java-like comment ###################
if event.keyval == gtk.keysyms.Return:
# constants
comments = {
'/**' : (' * ', ' */'),
'/*' : (' * ', ' */'),
}
# cycle through all types of comment
for comment_start, (comment_middle, comment_end) in comments.items():
if preceding_line[whitespace_pos:] == comment_start:
add_middle = typed_char + whitespace + comment_middle
add_end = typed_char + whitespace + comment_end
return self._insert_at_cursor(add_middle, add_end)
################### auto-indent after function/list ###################
if options.autoindentAfterFunctionOrList:
if event.keyval == gtk.keysyms.Return:
indent_triggers = {
'(': ')',
'{': '}',
'[': ']',
':': '',
}
for indent_trigger, ending_char in indent_triggers.items():
if prev_char == indent_trigger:
if line_after:
# text between begin and ending brackets should come
# in the middle row
if ending_char != '' and ending_char in line_after:
ending_pos = line_after.find(ending_char)
else:
ending_pos = len(line_after)
end = cursor.copy()
end.set_line_offset(end.get_line_offset() + ending_pos)
ending_text = doc.get_text(cursor, end).strip()
doc.delete(cursor, end)
add_middle = typed_char + whitespace + get_tab_string(view)
add_end = ending_text + typed_char + whitespace
else:
add_middle = typed_char + whitespace + get_tab_string(view)
add_end = ""
return self._insert_at_cursor(add_middle, add_end)
def _insert_at_cursor(self, middle, end = ""):
window = gedit.app_get_default().get_active_window()
doc = window.get_active_document()
doc.insert_at_cursor(middle + end)
# refresh cursor and move it to the middle
cursor = doc.get_iter_at_mark(doc.get_insert())
cursor.set_offset(cursor.get_offset() - len(end))
doc.place_cursor(cursor)
return True
##### regular functions #####
def get_tab_string(view):
tab_width = view.get_tab_width()
tab_spaces = view.get_insert_spaces_instead_of_tabs()
tab_code = ""
if tab_spaces:
for x in range(tab_width):
tab_code += " "
else:
tab_code = "\t"
return tab_code
def get_closing_xml_tag(document):
tags = re.findall(r'<.*?>', document)
tags.reverse()
closed = []
for tag in tags:
# neutral tag
if re.match(r'<.*?/>', tag):
continue
# closing tag
m = re.match(r'</ *([^ ]*).*?>', tag)
if m:
closed.append(m.group(1))
continue
# opening tag
m = re.match(r'< *([^/][^ ]*).*?>', tag)
if m:
openedtag = m.group(1)
while True:
if len(closed) == 0:
return openedtag
close_tag = closed.pop()
if close_tag.lower() == openedtag.lower():
break
continue
return None
################## OPTIONS DIALOG ##################
def options_singleton():
if Options.singleton is None:
Options.singleton = Options()
return Options.singleton
class Options(gobject.GObject):
__gsignals__ = {
'options-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
}
singleton = None
def __init__(self):
gobject.GObject.__init__(self)
self.__gconfDir = "/apps/gedit-2/plugins/intelligent_text_completion"
# default values
self.closeBracketsAndQuotes = True
self.completeXML = True
self.detectLists = True
self.autoindentAfterFunctionOrList = True
# create gconf directory if not set yet
client = gconf.client_get_default()
if not client.dir_exists(self.__gconfDir):
client.add_dir(self.__gconfDir,gconf.CLIENT_PRELOAD_NONE)
# get the gconf keys, or stay with default if key not set
try:
self.closeBracketsAndQuotes = client.get_bool(self.__gconfDir+"/closeBracketsAndQuotes")
self.completeXML = client.get_bool(self.__gconfDir+"/completeXML")
self.detectLists = client.get_bool(self.__gconfDir+"/detectLists")
self.autoindentAfterFunctionOrList = client.get_bool(self.__gconfDir+"/autoindentAfterFunctionOrList")
except Exception, e: # catch, just in case
print e
def __del__(self):
# write changes to gconf
client = gconf.client_get_default()
client.set_bool(self.__gconfDir+"/closeBracketsAndQuotes", self.closeBracketsAndQuotes)
client.set_bool(self.__gconfDir+"/completeXML", self.completeXML)
client.set_bool(self.__gconfDir+"/detectLists", self.detectLists)
client.set_bool(self.__gconfDir+"/autoindentAfterFunctionOrList", self.autoindentAfterFunctionOrList)
def create_configure_dialog(self):
win = gtk.Window()
win.connect("delete-event",lambda w,e: w.destroy())
win.set_title("Preferences")
win.set_position(gtk.WIN_POS_CENTER)
vbox = gtk.VBox()
#--------------------------------
# disable tabs
#notebook = gtk.Notebook()
#notebook.set_border_width(6)
#vbox.pack_start(notebook)
vbox2 = gtk.VBox()
vbox2.set_border_width(6)
box = gtk.HBox()
closeBracketsAndQuotes = gtk.CheckButton("Auto-close brackets and quotes")
closeBracketsAndQuotes.set_active(self.closeBracketsAndQuotes)
box.pack_start(closeBracketsAndQuotes,False,False,6)
vbox2.pack_start(box,False)
box = gtk.HBox()
completeXML = gtk.CheckButton("Auto-complete XML tags")
completeXML.set_active(self.completeXML)
box.pack_start(completeXML,False,False,6)
vbox2.pack_start(box,False)
box = gtk.HBox()
detectLists = gtk.CheckButton("Detect lists")
detectLists.set_active(self.detectLists)
box.pack_start(detectLists,False,False,6)
vbox2.pack_start(box,False)
box = gtk.HBox()
autoindentAfterFunctionOrList = gtk.CheckButton("Auto-indent after function or list")
autoindentAfterFunctionOrList.set_active(self.autoindentAfterFunctionOrList)
box.pack_start(autoindentAfterFunctionOrList,False,False,6)
vbox2.pack_start(box,False)
# disable tabs
#notebook.append_page(vbox2,gtk.Label("General"))
vbox.pack_start(vbox2, False)
#--------------------------------
vbox2 = gtk.VBox()
vbox2.set_border_width(6)
button = {}
def setValues(w):
# set class attributes
self.closeBracketsAndQuotes = closeBracketsAndQuotes.get_active()
self.completeXML = completeXML.get_active()
self.detectLists = detectLists.get_active()
self.autoindentAfterFunctionOrList = autoindentAfterFunctionOrList.get_active()
# write changes to gconf
client = gconf.client_get_default()
client.set_bool(self.__gconfDir+"/closeBracketsAndQuotes", self.closeBracketsAndQuotes)
client.set_bool(self.__gconfDir+"/completeXML", self.completeXML)
client.set_bool(self.__gconfDir+"/detectLists", self.detectLists)
client.set_bool(self.__gconfDir+"/autoindentAfterFunctionOrList", self.autoindentAfterFunctionOrList)
# commit changes and quit dialog
self.emit("options-changed")
win.destroy()
box = gtk.HBox()
b = gtk.Button(None,gtk.STOCK_OK)
b.connect("clicked",setValues)
box.pack_end(b,False)
b = gtk.Button(None,gtk.STOCK_CANCEL)
b.connect("clicked",lambda w,win: win.destroy(),win)
box.pack_end(b,False)
vbox.pack_start(box,False)
win.add(vbox)
win.show_all()
return win
gobject.type_register(Options)