public
Description: My Gedit plugins ;)
Homepage:
Clone URL: git://github.com/caironoleto/my-gedit-plugins.git
my-gedit-plugins / completion.py
100644 382 lines (316 sloc) 15.389 kb
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
# Copyright (C) 2006-2008 Osmo Salomaa
#
# 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.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
"""Complete words with the tab key.
 
This plugin provides a 'stupid' word completion plugin, one that is aware of
all words in all open documents, but knows nothing of any context or syntax.
This plugin can be used to speed up writing and to avoid spelling errors in
either regular text documents or in programming documents if no programming
language -aware completion is available.
 
Words are automatically scanned at regular intervals. Once you have typed a
word and the interval has passed, the word is available in the completion
system. A completion window listing possible completions is shown and updated
as you type. You can complete to the topmost word in the window with the Tab
key, or choose another completion with the arrow keys and complete with the Tab
key. The keybindinds are configurable only by editing the source code.
"""
 
import gedit
import gobject
import gtk
import pango
import re
 
 
class CompletionWindow(gtk.Window):
 
    """Window for displaying a list of words to complete to.
 
This is a popup window merely to display words. This window is not meant
to receive or handle input from the user, rather the various methods should
be called to chang the list of words and which one of them is selected.
"""
 
    def __init__(self, parent):
 
        gtk.Window.__init__(self, gtk.WINDOW_POPUP)
        self._store = None
        self._view = None
        self.set_transient_for(parent)
        self._init_view()
        self._init_containers()
 
    def _init_containers(self):
        """Initialize the frame and the scrolled window."""
 
        scroller = gtk.ScrolledWindow()
        scroller.set_policy(*((gtk.POLICY_NEVER,) * 2))
        scroller.add(self._view)
        frame = gtk.Frame()
        frame.set_shadow_type(gtk.SHADOW_OUT)
        frame.add(scroller)
        self.add(frame)
 
    def _init_view(self):
        """Initialize the tree view listing the complete words."""
 
        self._store = gtk.ListStore(gobject.TYPE_STRING)
        self._view = gtk.TreeView(self._store)
        renderer = gtk.CellRendererText()
        renderer.xpad = renderer.ypad = 6
        column = gtk.TreeViewColumn("", renderer, text=0)
        self._view.append_column(column)
        self._view.set_enable_search(False)
        self._view.set_headers_visible(False)
        self._view.set_rules_hint(True)
        selection = self._view.get_selection()
        selection.set_mode(gtk.SELECTION_SINGLE)
 
    def get_selected(self):
        """Return the index of the selected row."""
 
        selection = self._view.get_selection()
        return selection.get_selected_rows()[1][0][0]
 
    def select_next(self):
        """Select the next complete word."""
 
        row = min(self.get_selected() + 1, len(self._store) - 1)
        selection = self._view.get_selection()
        selection.unselect_all()
        selection.select_path(row)
        self._view.scroll_to_cell(row)
 
    def select_previous(self):
        """Select the previous complete word."""
 
        row = max(self.get_selected() - 1, 0)
        selection = self._view.get_selection()
        selection.unselect_all()
        selection.select_path(row)
        self._view.scroll_to_cell(row)
 
    def set_completions(self, completions):
        """Set the completions to display."""
 
        # 'gtk.Window.resize' followed later by 'gtk.TreeView.columns_autosize'
        # will allow the window to either grow or shrink to fit the new data.
        self.resize(1, 1)
        self._store.clear()
        for word in completions:
            self._store.append((word,))
        self._view.columns_autosize()
        self._view.get_selection().select_path(0)
 
    def set_font_description(self, font_desc):
        """Set the font description used in the view."""
 
        self._view.modify_font(font_desc)
 
 
class CompletionPlugin(gedit.Plugin):
 
    """Complete words with the tab key.
 
Instance variables are as follows. '_completion_windows' is a dictionary
mapping 'gedit.Windows' to 'CompletionWindows'.
 
'_all_words' is a dictionary mapping documents to a frozen set containing
all words in the document. '_favorite_words' is a dictionary mapping
documents to a set of words that the user has completed to. Favorites are
thus always document-specific and there are no degrees to favoritism. These
favorites will be displayed at the top of the completion window. As
'_all_words' and '_favorite_words' are both sets, the exact order in which
the words are listed in the completion window is unpredictable.
 
'_completions' is a list of the currently active complete words, shown in
the completion window, that the user can complete to. Similarly '_remains'
is a list of the untyped parts the _completions, i.e. the part that will be
inserted when the user presses the Tab key. '_completions' and '_remains'
always contain words for the gedit window, document and text view that has
input focus.
 
'_font_ascent' is the ascent of the font used in gedit's text view as
reported by pango. It is needed to be able to properly place the completion
window right below the caret regardless of the font and font size used.
"""
 
    # Unlike gedit itself, consider underscores alphanumeric characters
    # allowing completion of identifier names in many programming languages.
    _re_alpha = re.compile(r"\w+", re.UNICODE | re.MULTILINE)
    _re_non_alpha = re.compile(r"\W+", re.UNICODE | re.MULTILINE)
 
    # TODO: Are these sane defaults? Do we need a configuration dialog?
    _scan_frequency = 10000 # ms
    _max_completions_to_show = 6
 
    def __init__(self):
 
        gedit.Plugin.__init__(self)
        self._all_words = {}
        self._completion_windows = {}
        self._completions = []
        self._favorite_words = {}
        self._font_ascent = 0
        self._remains = []
 
    def _complete_current(self):
        """Complete the current word."""
 
        window = gedit.app_get_default().get_active_window()
        doc = window.get_active_document()
        index = self._completion_windows[window].get_selected()
        doc.insert_at_cursor(self._remains[index])
        words = self._favorite_words.setdefault(doc, set(()))
        words.add(self._completions[index])
        self._terminate_completion()
 
    def _connect_document(self, doc):
        """Connect to document's 'loaded' signal."""
 
        callback = lambda doc, x, self: self._scan_document(doc)
        handler_id = doc.connect("loaded", callback, self)
        doc.set_data(self.__class__.__name__, (handler_id,))
 
    def _connect_view(self, view, window):
        """Connect to view's editing signals."""
 
        callback = lambda x, y, self: self._terminate_completion()
        id_1 = view.connect("focus-out-event", callback, self)
        callback = self._on_view_key_press_event
        id_2 = view.connect("key-press-event", callback, window)
        view.set_data(self.__class__.__name__, (id_1, id_2))
 
    def _display_completions(self, view, event):
        """Find completions and display them in the completion window."""
 
        doc = view.get_buffer()
        insert = doc.get_iter_at_mark(doc.get_insert())
        start = insert.copy()
        while start.backward_char():
            char = unicode(start.get_char())
            if not self._re_alpha.match(char):
                start.forward_char()
                break
        incomplete = unicode(doc.get_text(start, insert))
        incomplete += unicode(event.string)
        if incomplete.isdigit():
            # Usually completing numbers is not a good idea.
            return self._terminate_completion()
        self._find_completions(doc, incomplete)
        if not self._completions:
            return self._terminate_completion()
        self._show_completion_window(view, insert)
 
    def _find_completions(self, doc, incomplete):
        """Find completions for incomplete word and save them."""
 
        self._completions = []
        self._remains = []
        favorites = self._favorite_words.get(doc, ())
        _all_words = set(())
        for words in self._all_words.itervalues():
            _all_words.update(words)
        limit = self._max_completions_to_show
        for sequence in (favorites, _all_words):
            for word in sequence:
                if not word.startswith(incomplete): continue
                if word == incomplete: continue
                if word in self._completions: continue
                self._completions.append(word)
                self._remains.append(word[len(incomplete):])
                if len(self._remains) >= limit: break
 
    def _on_view_key_press_event(self, view, event, window):
        """Manage actions for completions and the completion window."""
 
        if event.state & gtk.gdk.CONTROL_MASK:
            return self._terminate_completion()
        if event.state & gtk.gdk.MOD1_MASK:
            return self._terminate_completion()
        if (event.keyval == gtk.keysyms.Tab) and self._remains:
            return not self._complete_current()
        completion_window = self._completion_windows[window]
        if (event.keyval == gtk.keysyms.Up) and self._remains:
            return not completion_window.select_previous()
        if (event.keyval == gtk.keysyms.Down) and self._remains:
            return not completion_window.select_next()
        string = unicode(event.string)
        if len(string) != 1:
            # Do not suggest completions after pasting text.
            return self._terminate_completion()
        if self._re_alpha.match(string) is None:
            return self._terminate_completion()
        doc = view.get_buffer()
        insert = doc.get_iter_at_mark(doc.get_insert())
        if self._re_alpha.match(unicode(insert.get_char())):
            # Do not suggest completions in the middle of a word.
            return self._terminate_completion()
        return self._display_completions(view, event)
 
    def _on_window_tab_added(self, window, tab):
        """Connect to signals of the document and view in tab."""
 
        self._update_fonts(tab.get_view())
        name = self.__class__.__name__
        doc = tab.get_document()
        handler_id = doc.get_data(name)
        if handler_id is None:
            self._connect_document(doc)
        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):
        """Remove closed document's word and favorite sets."""
 
        doc = tab.get_document()
        self._all_words.pop(doc, None)
        self._favorite_words.pop(doc, None)
 
    def _scan_active_document(self, window):
        """Scan all the words in the active document in window."""
 
        # Return False to not scan again.
        if window is None: return False
        doc = window.get_active_document()
        if doc is not None:
            self._scan_document(doc)
        return True
 
    def _scan_document(self, doc):
        """Scan and save all words in document."""
 
        text = unicode(doc.get_text(*doc.get_bounds()))
        self._all_words[doc] = frozenset(self._re_non_alpha.split(text))
 
    def _show_completion_window(self, view, itr):
        """Show the completion window below the caret."""
 
        text_window = gtk.TEXT_WINDOW_WIDGET
        rect = view.get_iter_location(itr)
        x, y = view.buffer_to_window_coords(text_window, rect.x, rect.y)
        window = gedit.app_get_default().get_active_window()
        x, y = view.translate_coordinates(window, x, y)
        x += window.get_position()[0] + self._font_ascent
        # Use 24 pixels as an estimate height for window title bar.
        # TODO: There must be a better way than a hardcoded pixel value.
        y += window.get_position()[1] + 24 + (2 * self._font_ascent)
        completion_window = self._completion_windows[window]
        completion_window.set_completions(self._completions)
        completion_window.move(int(x), int(y))
        completion_window.show_all()
 
    def _terminate_completion(self):
        """Hide the completion window and cancel completions."""
 
        window = gedit.app_get_default().get_active_window()
        self._completion_windows[window].hide()
        self._completions = []
        self._remains = []
 
    def _update_fonts(self, view):
        """Update font descriptions and ascent metrics."""
 
        context = view.get_pango_context()
        font_desc = context.get_font_description()
        if self._font_ascent == 0:
            # Acquiring pango metrics is a bit slow,
            # so do this only when absolutely needed.
            metrics = context.get_metrics(font_desc, None)
            self._font_ascent = metrics.get_ascent() / pango.SCALE
        for completion_window in self._completion_windows.itervalues():
            completion_window.set_font_description(font_desc)
 
    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))
        for doc in window.get_documents():
            self._connect_document(doc)
            self._scan_document(doc)
        views = window.get_views()
        for view in views:
            self._connect_view(view, window)
        if views: self._update_fonts(views[0])
        self._completion_windows[window] = CompletionWindow(window)
        # Scan the active document in window if it has input focus
        # for new words at constant intervals.
        def scan(self, window):
            if not window.is_active(): return True
            return self._scan_active_document(window)
        freq = self._scan_frequency
        priority = gobject.PRIORITY_LOW
        gobject.timeout_add(freq, scan, self, window, priority=priority)
 
    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)
        self._terminate_completion()
        self._completion_windows.pop(window)
        for doc in window.get_documents():
            self._all_words.pop(doc, None)
            self._favorite_words.pop(doc, None)