~ubuntu-branches/ubuntu/vivid/kate/vivid-updates

« back to all changes in this revision

Viewing changes to addons/pate/src/plugins/gid/__init__.py

  • Committer: Package Import Robot
  • Author(s): Jonathan Riddell
  • Date: 2014-12-04 16:49:41 UTC
  • mfrom: (1.6.6)
  • Revision ID: package-import@ubuntu.com-20141204164941-l3qbvsly83hhlw2v
Tags: 4:14.11.97-0ubuntu1
* New upstream release
* Update build-deps and use pkg-kde v3 for Qt 5 build
* kate-data now kate5-data for co-installability

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Browse the tokens in a GNU idutils ID file, and use it to navigate within a codebase.
 
2
 
 
3
The necessary parts of the ID file are held in memory to allow sufficient performance
 
4
for token completion, when looking up the usage of a token, or jumping to the
 
5
definition. etags(1) is used to locate the definition.
 
6
"""
 
7
#
 
8
# Copyright 2012, 2013, Shaheed Haque <srhaque@theiet.org>.
 
9
#
 
10
# This program is free software; you can redistribute it and/or
 
11
# modify it under the terms of the GNU General Public License as
 
12
# published by the Free Software Foundation; either version 2 of
 
13
# the License or (at your option) version 3 or any later version
 
14
# accepted by the membership of KDE e.V. (or its successor approved
 
15
# by the membership of KDE e.V.), which shall act as a proxy
 
16
# defined in Section 14 of version 3 of the license.
 
17
#
 
18
# This program is distributed in the hope that it will be useful,
 
19
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
20
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
21
# GNU General Public License for more details.
 
22
#
 
23
# You should have received a copy of the GNU General Public License
 
24
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
25
#
 
26
 
 
27
import kate
 
28
 
 
29
from PyQt4 import uic
 
30
from PyQt4.QtCore import *
 
31
from PyQt4.QtGui import *
 
32
from PyKDE4.kdecore import *
 
33
from PyKDE4.kdeui import *
 
34
from PyKDE4.ktexteditor import KTextEditor
 
35
 
 
36
from .idutils import Lookup
 
37
 
 
38
import codecs
 
39
import os.path
 
40
import re
 
41
import sip
 
42
import subprocess
 
43
import sys
 
44
import time
 
45
 
 
46
idDatabase = None
 
47
searchBar = None
 
48
completionModel = None
 
49
 
 
50
def toStr(_bytes):
 
51
    if sys.version_info.major >= 3:
 
52
        return _bytes.decode("utf-8")
 
53
    else:
 
54
        return unicode(_bytes)
 
55
 
 
56
class ConfigWidget(QWidget):
 
57
    """Configuration widget for this plugin."""
 
58
    def __init__(self, parent = None, name = None):
 
59
        super(ConfigWidget, self).__init__(parent)
 
60
        #
 
61
        # Location of ID file.
 
62
        #
 
63
        self.idFile = None
 
64
        #
 
65
        # Completion string minimum size.
 
66
        #
 
67
        self.keySize = None
 
68
        #
 
69
        # Original file prefix.
 
70
        #
 
71
        self.srcIn = None
 
72
        #
 
73
        # Replacement file prefix.
 
74
        #
 
75
        self.srcOut = None
 
76
 
 
77
        # Set up the user interface from Designer.
 
78
        uic.loadUi(os.path.join(os.path.dirname(__file__), "config.ui"), self)
 
79
 
 
80
        self.reset();
 
81
 
 
82
    def apply(self):
 
83
        kate.configuration["idFile"] = self.idFile.text()
 
84
        kate.configuration["keySize"] = self.keySize.value()
 
85
        kate.configuration["useEtags"] = self.useEtags.isChecked()
 
86
        kate.configuration["useSuffixes"] = self.useSuffixes.text()
 
87
        kate.configuration["srcIn"] = self.srcIn.text()
 
88
        kate.configuration["srcOut"] = self.srcOut.text()
 
89
        kate.configuration.save()
 
90
 
 
91
    def reset(self):
 
92
        self.defaults()
 
93
        if "idFile" in kate.configuration:
 
94
            self.idFile.setText(kate.configuration["idFile"])
 
95
        if "keySize" in kate.configuration:
 
96
            self.keySize.setValue(kate.configuration["keySize"])
 
97
        if "useEtags" in kate.configuration:
 
98
            self.useEtags.setChecked(kate.configuration["useEtags"])
 
99
        if "useSuffixes" in kate.configuration:
 
100
            self.useSuffixes.setText(kate.configuration["useSuffixes"])
 
101
        if "srcIn" in kate.configuration:
 
102
            self.srcIn.setText(kate.configuration["srcIn"])
 
103
        if "srcOut" in kate.configuration:
 
104
            self.srcOut.setText(kate.configuration["srcOut"])
 
105
 
 
106
    def defaults(self):
 
107
        self.idFile.setText("/view/myview/vob/ID")
 
108
        self.keySize.setValue(5)
 
109
        self.useEtags.setChecked(True)
 
110
        self.useSuffixes.setText(".h;.hxx")
 
111
        self.srcIn.setText("/vob")
 
112
        self.srcOut.setText("%{idPrefix}/vob")
 
113
 
 
114
class ConfigPage(kate.Kate.PluginConfigPage, QWidget):
 
115
    """Kate configuration page for this plugin."""
 
116
    def __init__(self, parent = None, name = None):
 
117
        super(ConfigPage, self).__init__(parent, name)
 
118
        self.widget = ConfigWidget(parent)
 
119
        lo = parent.layout()
 
120
        lo.addWidget(self.widget)
 
121
 
 
122
    def apply(self):
 
123
        self.widget.apply()
 
124
 
 
125
    def reset(self):
 
126
        self.widget.reset()
 
127
 
 
128
    def defaults(self):
 
129
        self.widget.defaults()
 
130
        self.changed.emit()
 
131
 
 
132
class ConfigDialog(KDialog):
 
133
    """Standalong configuration dialog for this plugin."""
 
134
    def __init__(self, parent = None):
 
135
        super(ConfigDialog, self).__init__(parent)
 
136
        self.widget = ConfigWidget(self)
 
137
        self.setMainWidget(self.widget)
 
138
        self.setButtons(KDialog.ButtonCode(KDialog.Default | KDialog.Reset | KDialog.Ok | KDialog.Cancel))
 
139
        self.applyClicked.connect(self.apply)
 
140
        self.resetClicked.connect(self.reset)
 
141
        self.defaultClicked.connect(self.defaults)
 
142
        self.resize(600, 200)
 
143
 
 
144
    @pyqtSlot()
 
145
    def apply(self):
 
146
        self.widget.apply()
 
147
 
 
148
    @pyqtSlot()
 
149
    def reset(self):
 
150
        self.widget.reset()
 
151
 
 
152
    @pyqtSlot()
 
153
    def defaults(self):
 
154
        self.widget.defaults()
 
155
 
 
156
def transform(file):
 
157
    """Return the transformed file name."""
 
158
    transformationKey = kate.configuration["srcIn"]
 
159
    if len(transformationKey):
 
160
        #
 
161
        # A transformation of the file name is requested.
 
162
        #
 
163
        try:
 
164
            left, right = file.split(transformationKey, 1)
 
165
        except ValueError:
 
166
            #
 
167
            # No transformation is applicable.
 
168
            #
 
169
            return file
 
170
        percentI = kate.configuration["srcOut"].find("%{idPrefix}")
 
171
        if percentI > -1:
 
172
            try:
 
173
                insertLeft, discard = kate.configuration["idFile"].split(transformationKey, 1)
 
174
            except ValueError:
 
175
                #
 
176
                # No transformation is possible.
 
177
                #
 
178
                return file
 
179
            discard, insertRight = kate.configuration["srcOut"].split("%{idPrefix}", 1)
 
180
            file = insertLeft + insertRight + right
 
181
        else:
 
182
            file = kate.configuration["srcOut"] + right
 
183
    return file
 
184
 
 
185
class HistoryModel(QStandardItemModel):
 
186
    """Support the display of a stack of navigation points.
 
187
    Each visible row comprises { basename(fileName);line, text }.
 
188
    On column 0, we show a tooltip with the full fileName, and any icon.
 
189
    On column 1, we store line and column.
 
190
    """
 
191
    def __init__(self):
 
192
        """Constructor.
 
193
        """
 
194
        super(HistoryModel, self).__init__()
 
195
        self.setHorizontalHeaderLabels((i18n("File"), i18n("Match")))
 
196
 
 
197
    def add(self, fileName, icon, text, line, column, fileAndLine):
 
198
        """Add a new entry to the top of the stack."""
 
199
        #
 
200
        # Ignore if the top of the stack has an identical entry.
 
201
        #
 
202
        column0 = self.invisibleRootItem().child(0, 0)
 
203
        if column0:
 
204
            if fileName == column0.data(Qt.ToolTipRole):
 
205
                column1 = self.invisibleRootItem().child(0, 1)
 
206
                if line == column1.data(Qt.UserRole + 1):
 
207
                    return column0.index()
 
208
        column0 = QStandardItem(fileAndLine)
 
209
        if icon:
 
210
            column0.setIcon(KIcon(icon))
 
211
        column0.setData(fileName, Qt.ToolTipRole)
 
212
        column1 = QStandardItem(text)
 
213
        column1.setData(line, Qt.UserRole + 1)
 
214
        column1.setData(column, Qt.UserRole + 2)
 
215
        resultRow = (column0, column1)
 
216
        self.invisibleRootItem().insertRow(0, resultRow)
 
217
        if self.rowCount() > 64:
 
218
            self.setRowCount(64)
 
219
        return resultRow[0].index()
 
220
 
 
221
    def read(self, index):
 
222
        """Extract a row."""
 
223
        row = index.row()
 
224
        column0 = index.sibling(row, 0)
 
225
        fileAndLine = column0.data()
 
226
        fileName = column0.data(Qt.ToolTipRole)
 
227
        icon = column0.data(Qt.DecorationRole)
 
228
        column1 = index.sibling(row, 1)
 
229
        text = column1.data()
 
230
        line = column1.data(Qt.UserRole + 1)
 
231
        column = column1.data(Qt.UserRole + 2)
 
232
        return (fileName, icon, text, line, column, fileAndLine)
 
233
 
 
234
class MatchesModel(HistoryModel):
 
235
    """Support the display of a list of entries matching a token.
 
236
    The display matches HistoryModel, but is a list not a stack.
 
237
    """
 
238
 
 
239
    _boredomInterval = 20
 
240
 
 
241
    def __init__(self, dataSource):
 
242
        """Constructor.
 
243
 
 
244
        @param dataSource    an instance of a Lookup().
 
245
        """
 
246
        super(MatchesModel, self).__init__()
 
247
        self.dataSource = dataSource
 
248
 
 
249
    def _etagSearch(self, token, fileName):
 
250
        """Use etags to find any definition in this file.
 
251
 
 
252
        Look for [ 0x7f, token, 0x1 ].
 
253
        """
 
254
        if not kate.configuration["useEtags"]:
 
255
            return None
 
256
        etagsCmd = ["etags", "-o", "-", fileName]
 
257
        etagBytes = subprocess.check_output(etagsCmd, stderr = subprocess.STDOUT)
 
258
        tokenBytes = bytearray(token.encode("utf-8"))
 
259
        tokenBytes.insert(0, 0x7f)
 
260
        tokenBytes.append(0x1)
 
261
        etagDefinition = etagBytes.find(tokenBytes)
 
262
        if etagDefinition > -1:
 
263
            #
 
264
            # The line number follows.
 
265
            #
 
266
            lineNumberStart = etagDefinition + len(tokenBytes)
 
267
            lineNumberEnd = toStr(etagBytes).find(",", lineNumberStart)
 
268
            return int(etagBytes[lineNumberStart:lineNumberEnd]) - 1
 
269
        if etagBytes.startswith(bytearray(etagsCmd[0], "latin-1")):
 
270
            #
 
271
            # An error message was printed starting with "etags".
 
272
            #
 
273
            raise IOError(toStr(etagBytes))
 
274
        return None
 
275
 
 
276
    def _scanFile(self, regexp, filterRe, token, fileName, isDeclaration):
 
277
        """Scan a file looking for interesting hits. Return the QModelIndex of the last of any definitions we find."""
 
278
        definitionIndex = None
 
279
        fileRow = None
 
280
        hits = 0
 
281
        line = 0
 
282
        try:
 
283
            definitionLine = self._etagSearch(token, fileName)
 
284
            #
 
285
            # Question: what encoding is this file? TODO A better approach
 
286
            # to this question.
 
287
            #
 
288
            for text in codecs.open(fileName, encoding="latin-1"):
 
289
                match = regexp.search(text)
 
290
                if match and filterRe:
 
291
                    match = filterRe.search(text)
 
292
                if match:
 
293
                    hits += 1
 
294
                    column = match.start()
 
295
                    if line == definitionLine:
 
296
                        #
 
297
                        # Mark the line and the file as being a definition.
 
298
                        #
 
299
                        definitionIndex = self.add(fileName, "go-jump-definition", text[:-1], line, column)
 
300
                    elif isDeclaration:
 
301
                        self.add(fileName, "text-x-chdr", text[:-1], line, column)
 
302
                    else:
 
303
                        self.add(fileName, None, text[:-1], line, column)
 
304
                #
 
305
                # FF and VT are line endings it seems...
 
306
                #
 
307
                if text[-1] != '\f' and text[-1] != '\v':
 
308
                    line += 1
 
309
            if not hits:
 
310
                #
 
311
                # This was in the index, but we found no hits. Assuming the file
 
312
                # content has changed, we still permit navigation to the top of
 
313
                # the file.
 
314
                #
 
315
                self.add(fileName, "task-reject", "", 0, 0)
 
316
        except IOError as e:
 
317
            self.add(fileName, "face-sad", str(e), None, None)
 
318
        return definitionIndex
 
319
 
 
320
    def add(self, fileName, icon, text, line, column):
 
321
        """Append a new entry."""
 
322
        if line:
 
323
            column0 = QStandardItem("{}:{}".format(QFileInfo(fileName).fileName(), line + 1))
 
324
        else:
 
325
            column0 = QStandardItem(QFileInfo(fileName).fileName())
 
326
        if icon:
 
327
            column0.setIcon(KIcon(icon))
 
328
        column0.setData(fileName, Qt.ToolTipRole)
 
329
        column1 = QStandardItem(text)
 
330
        column1.setData(line, Qt.UserRole + 1)
 
331
        column1.setData(column, Qt.UserRole + 2)
 
332
        resultRow = (column0, column1)
 
333
        self.invisibleRootItem().appendRow(resultRow)
 
334
        return resultRow[0].index()
 
335
 
 
336
    def literalTokenSearch(self, parent, token, filter):
 
337
        """Add the entries which match the token to the matches, and return the QModelIndex of the last of any definitions we find.
 
338
 
 
339
        Entries are grouped under the file in which the hits are searched. Each
 
340
        entry shows the matched text, the line and column of the match. If so
 
341
        enabled, entries which are defintions according to etags are highlighted.
 
342
 
 
343
        If the output takes a long time to generate, the user is given options
 
344
        to continue or abort.
 
345
        """
 
346
        self.setRowCount(0)
 
347
        self.definitionIndex = None
 
348
        try:
 
349
            tokenFlags, hitCount, files = self.dataSource.literalSearch(token)
 
350
        except IndexError:
 
351
            return None
 
352
        #
 
353
        # Compile the REs we need.
 
354
        #
 
355
        if len(filter):
 
356
            filter = filter.replace("%{token}", token)
 
357
            try:
 
358
                filterRe = re.compile(filter)
 
359
            except re.error:
 
360
                KMessageBox.error(parent.parent(), i18n("Filter '%1' is not a valid regular expression", filter), i18n("Invalid filter"))
 
361
                return None
 
362
        else:
 
363
            filterRe = None
 
364
        regexp = re.compile("\\b" + token + "\\b")
 
365
        if len(kate.configuration["useSuffixes"]) == 0:
 
366
            declarationRe = None
 
367
        else:
 
368
            declarationRe = kate.configuration["useSuffixes"].replace(";", "|")
 
369
            declarationRe = "(" + declarationRe.replace(".", "\.") + ")$"
 
370
            declarationRe = re.compile(declarationRe, re.IGNORECASE)
 
371
        startBoredomQuery = time.time()
 
372
        previousBoredomQuery = startBoredomQuery - MatchesModel._boredomInterval // 2
 
373
        #
 
374
        # For each file, list the lines where a match is found.
 
375
        #
 
376
        definitionIndex = None
 
377
        filesListed = 0
 
378
        for fileName, fileFlags in files:
 
379
            fileName = transform(fileName)
 
380
            isDeclaration = declarationRe and declarationRe.search(fileName)
 
381
            #
 
382
            # Update the definitionIndex when we get a good one.
 
383
            #
 
384
            newDefinitionIndex = self._scanFile(regexp, filterRe, token, fileName, isDeclaration)
 
385
            if newDefinitionIndex:
 
386
                definitionIndex = newDefinitionIndex
 
387
            filesListed += 1
 
388
            #
 
389
            # Time to query the user's boredom level?
 
390
            #
 
391
            if time.time() - previousBoredomQuery > MatchesModel._boredomInterval:
 
392
                r = KMessageBox.questionYesNoCancel(parent.parent(), i18n("Scanned %1 of %2 files in %3 seconds", filesListed, len(files), int(time.time() - startBoredomQuery)),
 
393
                        i18n("Scan more files?"), KGuiItem(i18n("All Files")), KGuiItem(i18n("More Files")), KStandardGuiItem.cancel())
 
394
                if r == KMessageBox.Yes:
 
395
                    previousBoredomQuery = time.time() + 10 * MatchesModel._boredomInterval
 
396
                elif r == KMessageBox.No:
 
397
                    previousBoredomQuery = time.time()
 
398
                else:
 
399
                    break
 
400
        #
 
401
        # Return the model index of the match.
 
402
        #
 
403
        return definitionIndex
 
404
 
 
405
class CompletionModel(KTextEditor.CodeCompletionModel):
 
406
    """Support Kate code completion.
 
407
    """
 
408
    def __init__(self, parent, dataSource):
 
409
        """Constructor.
 
410
 
 
411
        @param parent        Parent QObject.
 
412
        @param dataSource    An instance of a Lookup().
 
413
        """
 
414
        super(CompletionModel, self).__init__(parent)
 
415
        self.dataSource = dataSource
 
416
        self.completionObj = None
 
417
 
 
418
    def completionInvoked(self, view, range, invocationType):
 
419
        """Find matches for the range given.
 
420
        """
 
421
        token = view.document().text(range)
 
422
        if len(token) < kate.configuration["keySize"]:
 
423
            #
 
424
            # Don't try to match if the token is too short.
 
425
            #
 
426
            self.setRowCount(0)
 
427
            return
 
428
        self.completionObj = list()
 
429
        try:
 
430
            #
 
431
            # Add the first item if we find one, then any other matches.
 
432
            #
 
433
            lastToken = token
 
434
            lastOffset, lastName = self.dataSource.prefixSearchFirst(lastToken)
 
435
            while True:
 
436
                self.completionObj.append(lastName)
 
437
                lastOffset, lastName = self.dataSource.prefixSearchNext(lastOffset, lastToken)
 
438
        except IndexError:
 
439
            pass
 
440
        self.setRowCount(len(self.completionObj))
 
441
 
 
442
    def columnCount(self, index):
 
443
        return KTextEditor.CodeCompletionModel.Name
 
444
 
 
445
    def data(self, index, role):
 
446
        """Return the data defining the item.
 
447
        """
 
448
        column = index.column()
 
449
        if column == KTextEditor.CodeCompletionModel.Name:
 
450
            if role == Qt.DisplayRole:
 
451
                # The match itself.
 
452
                return self.completionObj[index.row()]
 
453
            elif role ==  KTextEditor.CodeCompletionModel.HighlightingMethod:
 
454
                # Default highlighting.
 
455
                QVariant.Invalid
 
456
            else:
 
457
                #print "data()", index.row(), "Name", role
 
458
                return None
 
459
        else:
 
460
            #print "data()", index.row(), column, role
 
461
            return None
 
462
 
 
463
class SearchBar(QObject):
 
464
    def __init__(self, dataSource):
 
465
        super(SearchBar, self).__init__(None)
 
466
        self.lastToken = None
 
467
        self.lastOffset = None
 
468
        self.lastName = None
 
469
        self.gotSettings = False
 
470
        self.dataSource = dataSource
 
471
        self.toolView = kate.mainInterfaceWindow().createToolView("idutils_gid_plugin", kate.Kate.MainWindow.Bottom, SmallIcon("edit-find"), i18n("gid Search"))
 
472
        # By default, the toolview has box layout, which is not easy to delete.
 
473
        # For now, just add an extra widget.
 
474
        top = QWidget(self.toolView)
 
475
 
 
476
        # Set up the user interface from Designer.
 
477
        uic.loadUi(os.path.join(os.path.dirname(__file__), "tool.ui"), top)
 
478
        self.token = top.token
 
479
        self.filter = top.filter
 
480
        self.settings = top.settings
 
481
 
 
482
        self.matchesModel = MatchesModel(self.dataSource)
 
483
        self.matchesWidget = top.matches
 
484
        self.matchesWidget.verticalHeader().setDefaultSectionSize(20)
 
485
        self.matchesWidget.setModel(self.matchesModel)
 
486
        self.matchesWidget.setColumnWidth(0, 200)
 
487
        self.matchesWidget.setColumnWidth(1, 400)
 
488
        self.matchesWidget.activated.connect(self.navigateToMatch)
 
489
 
 
490
        self.historyModel = HistoryModel()
 
491
        self.historyWidget = top.history
 
492
        self.historyWidget.verticalHeader().setDefaultSectionSize(20)
 
493
        self.historyWidget.setModel(self.historyModel)
 
494
        self.historyWidget.setColumnWidth(0, 200)
 
495
        self.historyWidget.setColumnWidth(1, 400)
 
496
        self.historyWidget.activated.connect(self.navigateToHistory)
 
497
 
 
498
        self.token.setCompletionMode(KGlobalSettings.CompletionPopupAuto)
 
499
        self.token.completion.connect(self._findCompletion)
 
500
        self.token.completionObject().clear();
 
501
        self.token.returnPressed.connect(self.literalSearch)
 
502
        self.filter.returnPressed.connect(self.literalSearch)
 
503
        self.settings.clicked.connect(self.getSettings)
 
504
 
 
505
    def __del__(self):
 
506
        """Plugins that use a toolview need to delete it for reloading to work."""
 
507
        assert(self.toolView is not None)
 
508
        mw = kate.mainInterfaceWindow()
 
509
        if mw:
 
510
            self.hide()
 
511
            mw.destroyToolView(self.toolView)
 
512
        self.toolView = None
 
513
 
 
514
    @pyqtSlot()
 
515
    def literalSearch(self):
 
516
        """Lookup a single token and return the modelIndex of any definition."""
 
517
        definitionIndex = self.matchesModel.literalTokenSearch(self.toolView, self.token.currentText(), self.filter.currentText())
 
518
        self.matchesWidget.resizeColumnsToContents()
 
519
        if definitionIndex:
 
520
            #
 
521
            # Set the navigation starting point to (the last of) any
 
522
            # definition we found.
 
523
            #
 
524
            self.matchesWidget.setCurrentIndex(definitionIndex)
 
525
        return definitionIndex
 
526
 
 
527
    def _findCompletion(self, token):
 
528
        """Fill the completion object with potential matches."""
 
529
        completionObj = self.token.completionObject()
 
530
        completionObj.clear()
 
531
        if len(token) < kate.configuration["keySize"]:
 
532
            #
 
533
            # Don't try to match if the token is too short.
 
534
            #
 
535
            return
 
536
        try:
 
537
            #
 
538
            # Add the first item if we find one, then any other matches.
 
539
            #
 
540
            lastToken = token
 
541
            lastOffset, lastName = self.dataSource.prefixSearchFirst(lastToken)
 
542
            while True:
 
543
                completionObj.addItem(lastName)
 
544
                lastOffset, lastName = self.dataSource.prefixSearchNext(lastOffset, lastToken)
 
545
        except IndexError:
 
546
            return
 
547
 
 
548
    @pyqtSlot("QModelIndex &")
 
549
    def navigateToMatch(self, index):
 
550
        """Jump to the selected entry."""
 
551
        (fileName, icon, text, nextLine, column, fileAndLine) = self.matchesModel.read(index)
 
552
        if not nextLine:
 
553
            return
 
554
        #
 
555
        # Add a history record for the current point.
 
556
        #
 
557
        cursor = kate.activeView().cursorPosition()
 
558
        currentLine = cursor.line()
 
559
        document = kate.activeDocument()
 
560
        self.historyModel.add(document.url().toLocalFile(), "arrow-right", document.line(currentLine), currentLine, cursor.column(), "{}:{}".format(document.documentName(), currentLine + 1))
 
561
        #
 
562
        # Navigate to the point in the file.
 
563
        #
 
564
        document = kate.documentManager.openUrl(KUrl.fromPath(fileName))
 
565
        kate.mainInterfaceWindow().activateView(document)
 
566
        point = KTextEditor.Cursor(nextLine, column)
 
567
        kate.activeView().setCursorPosition(point)
 
568
        #
 
569
        # Add this new point to the history.
 
570
        #
 
571
        self.historyModel.add(fileName, icon, text, nextLine, column, fileAndLine)
 
572
        self.historyWidget.resizeColumnsToContents()
 
573
 
 
574
    @pyqtSlot("QModelIndex &")
 
575
    def navigateToHistory(self, index):
 
576
        """Jump to the selected entry."""
 
577
        (fileName, icon, text, line, column, fileAndLine) = self.historyModel.read(index)
 
578
        #
 
579
        # Navigate to the original point in the file.
 
580
        #
 
581
        document = kate.documentManager.openUrl(KUrl.fromPath(fileName))
 
582
        kate.mainInterfaceWindow().activateView(document)
 
583
        point = KTextEditor.Cursor(line, column)
 
584
        kate.activeView().setCursorPosition(point)
 
585
 
 
586
    @pyqtSlot()
 
587
    def getSettings(self):
 
588
        """Show the settings dialog.
 
589
        Establish our configuration. Loop until we have a viable settings, or
 
590
        the user cancels.
 
591
        """
 
592
        fileSet = False
 
593
        transformOK = False
 
594
        while not fileSet or not transformOK:
 
595
            fileSet = False
 
596
            transformOK = False
 
597
            dialog = ConfigDialog(kate.mainWindow().centralWidget())
 
598
            status = dialog.exec_()
 
599
            if status == QDialog.Rejected:
 
600
                break
 
601
            dialog.widget.apply()
 
602
            #
 
603
            # Check the save file name.
 
604
            #
 
605
            try:
 
606
                #
 
607
                # Only re-read the file if it has changed.
 
608
                #
 
609
                if not searchBar.dataSource.file or (searchBar.dataSource.file.name != kate.configuration["idFile"]):
 
610
                    searchBar.dataSource.setFile(kate.configuration["idFile"])
 
611
                fileSet = True
 
612
            except IOError as detail:
 
613
                KMessageBox.error(self.parent(), toStr(detail), i18n("ID database error"))
 
614
            #
 
615
            # Check the transformation settings.
 
616
            #
 
617
            try:
 
618
                transformationKey = kate.configuration["srcIn"]
 
619
                percentI = kate.configuration["srcOut"].find("%i")
 
620
                if len(transformationKey) and (percentI > -1):
 
621
                    #
 
622
                    # A transformation of the file name is with the output
 
623
                    # using the %i placeholder. Ensure the idFile is usable for
 
624
                    # that purpose.
 
625
                    #
 
626
                    insertLeft, discard = kate.configuration["idFile"].split(transformationKey, 1)
 
627
                transformOK = True
 
628
            except ValueError as detail:
 
629
                # xgettext: no-python-format
 
630
                KMessageBox.error(self.parent(), i18n("'%1' does not contain '%2'", kate.configuration["idFile"], transformationKey), i18n("Cannot use %i"))
 
631
        return fileSet and transformOK
 
632
 
 
633
    def show(self):
 
634
        if not self.gotSettings:
 
635
            self.gotSettings = self.getSettings()
 
636
        if self.gotSettings:
 
637
            kate.mainInterfaceWindow().showToolView(self.toolView)
 
638
            self.token.setFocus(Qt.PopupFocusReason)
 
639
        return self.gotSettings
 
640
 
 
641
    def hide(self):
 
642
        kate.mainInterfaceWindow().hideToolView(self.toolView)
 
643
 
 
644
def wordAtCursorPosition(line, cursor):
 
645
    ''' Get the word under the active view's cursor in the given document.'''
 
646
    # Better to use word boundaries than to hardcode valid letters because
 
647
    # expansions should be able to be in any unicode character.
 
648
    wordBoundary = re.compile("\W", re.UNICODE)
 
649
    start = end = cursor.column()
 
650
    if start == len(line) or wordBoundary.match(line[start]):
 
651
        start -= 1
 
652
    while start >= 0 and not wordBoundary.match(line[start]):
 
653
        start -= 1
 
654
    start += 1
 
655
    while end < len(line) and not wordBoundary.match(line[end]):
 
656
        end += 1
 
657
    return start, end
 
658
 
 
659
def wordAtCursor(document, view):
 
660
    ''' Get the word under the active view's cursor in the given document.
 
661
    Stolen from the expand plugin!'''
 
662
    cursor = view.cursorPosition()
 
663
    line = document.line(cursor.line())
 
664
    start, end = wordAtCursorPosition(line, cursor)
 
665
    return line[start:end]
 
666
 
 
667
@kate.action
 
668
def show():
 
669
    """Browse the tokens in the ID file."""
 
670
    # Make all our config is initialised.
 
671
    ConfigWidget().apply()
 
672
    global idDatabase
 
673
    global searchBar
 
674
    if searchBar is None:
 
675
        idDatabase = Lookup()
 
676
        searchBar = SearchBar(idDatabase)
 
677
    global completionModel
 
678
    if completionModel is None:
 
679
        completionModel = CompletionModel(kate.mainWindow(), idDatabase)
 
680
    viewChanged()
 
681
    return searchBar.show()
 
682
 
 
683
@kate.action
 
684
def lookup():
 
685
    """Lookup the currently selected token.
 
686
    Find the token, filter the results.
 
687
    """
 
688
    global searchBar
 
689
    if show():
 
690
        if kate.activeView().selection():
 
691
            selectedText = kate.activeView().selectionText()
 
692
        else:
 
693
            selectedText = wordAtCursor(kate.activeDocument(), kate.activeView())
 
694
        searchBar.token.insertItem(0, selectedText)
 
695
        searchBar.token.setCurrentIndex(1)
 
696
        searchBar.token.setEditText(selectedText)
 
697
        return searchBar.literalSearch()
 
698
    return None
 
699
 
 
700
@kate.action
 
701
def gotoDefinition():
 
702
    """Go to the definition of the currently selected token.
 
703
    Find the token, search for definitions using etags, jump to the definition.
 
704
    """
 
705
    global searchBar
 
706
    definitionIndex = lookup()
 
707
    if definitionIndex:
 
708
        searchBar.navigateToMatch(definitionIndex)
 
709
 
 
710
@kate.configPage(
 
711
    i18nc("@item:inmenu", "GNU idutils")
 
712
  , i18nc("@title:group", "<command section='1'>gid</command> token Lookup and Navigation")
 
713
  , icon = "edit-find"
 
714
  )
 
715
def configPage(parent = None, name = None):
 
716
    return ConfigPage(parent, name)
 
717
 
 
718
@kate.unload
 
719
def destroy():
 
720
    """Plugins that use a toolview need to delete it for reloading to work."""
 
721
    global searchBar
 
722
    if searchBar:
 
723
        del searchBar
 
724
        #searchBar = None
 
725
    global completionModel
 
726
    if completionModel:
 
727
        del completionModel
 
728
        #completionModel = None
 
729
 
 
730
@kate.viewChanged
 
731
def viewChanged():
 
732
    ''' Calls the function when the view changes. To access the new active view,
 
733
    use kate.activeView() '''
 
734
    global completionModel
 
735
    if completionModel:
 
736
        view = kate.activeView()
 
737
        if view:
 
738
            completionInterface = view.codeCompletionInterface()
 
739
            completionInterface.registerCompletionModel(completionModel)