PageRenderTime 92ms CodeModel.GetById 26ms app.highlight 61ms RepoModel.GetById 1ms app.codeStats 1ms

/tortoisehg/hgqt/revset.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 399 lines | 334 code | 47 blank | 18 comment | 53 complexity | 8c772d1fe0fbda0e151791fd9bb7dd5d MD5 | raw file
  1# revset.py - revision set query dialog
  2#
  3# Copyright 2010 Steve Borho <steve@borho.org>
  4#
  5# This software may be used and distributed according to the terms of the
  6# GNU General Public License version 2, incorporated herein by reference.
  7
  8import os
  9
 10from mercurial import revset, hg, error
 11
 12from tortoisehg.hgqt import qtlib, cmdui
 13from tortoisehg.util import hglib
 14from tortoisehg.hgqt.i18n import _
 15
 16from PyQt4.Qsci import QsciScintilla, QsciAPIs, QsciLexerPython
 17from PyQt4.QtCore import *
 18from PyQt4.QtGui import *
 19
 20# TODO:
 21#  Connect to repoview revisionClicked events
 22#  Shift-Click rev range -> revision range X:Y
 23#  Ctrl-Click two revs -> DAG range X::Y
 24#  QFontMetrics.elidedText for help label
 25
 26_common = (
 27    ('user(string)',
 28     _('Changesets where username contains string.')),
 29    ('keyword(string)',
 30     _('Search commit message, user name, and names of changed '
 31       'files for string.')),
 32    ('grep(regex)',
 33     _('Like "keyword(string)" but accepts a regex.')),
 34    ('outgoing([path])',
 35     _('Changesets not found in the specified destination repository,'
 36       ' or the default push location.')),
 37    ('tagged()',
 38     _('Changeset is tagged.')),
 39    ('head()',
 40     _('Changeset is a named branch head.')),
 41    ('merge()',
 42     _('Changeset is a merge changeset.')),
 43    ('closed()',
 44     _('Changeset is closed.')),
 45    ('date(interval)',
 46     _('Changesets within the interval, see <a href="http://www.selenic.com/'
 47       'mercurial/hg.1.html#dates">help dates</a>')),
 48    ('ancestor(single, single)',
 49     _('Greatest common ancestor of the two changesets.')),
 50)
 51
 52_filepatterns = (
 53    ('file(pattern)',
 54     _('Changesets affecting files matched by pattern.  '
 55       'See <a href="http://www.selenic.com/mercurial/hg.1.html#patterns">'
 56       'help patterns</a>')),
 57    ('modifies(pattern)',
 58     _('Changesets which modify files matched by pattern.')),
 59    ('adds(pattern)',
 60     _('Changesets which add files matched by pattern.')),
 61    ('removes(pattern)',
 62     _('Changesets which remove files matched by pattern.')),
 63    ('contains(pattern)',
 64     _('Changesets containing files matched by pattern.')),
 65)
 66
 67_ancestry = (
 68    ('branch(set)',
 69     _('All changesets belonging to the branches of changesets in set.')),
 70    ('heads(set)',
 71     _('Members of a set with no children in set.')),
 72    ('descendants(set)',
 73     _('Changesets which are descendants of changesets in set.')),
 74    ('ancestors(set)',
 75     _('Changesets that are ancestors of a changeset in set.')),
 76    ('children(set)',
 77     _('Child changesets of changesets in set.')),
 78    ('parents(set)',
 79     _('The set of all parents for all changesets in set.')),
 80    ('p1(set)',
 81     _('First parent for all changesets in set.')),
 82    ('p2(set)',
 83     _('Second parent for all changesets in set.')),
 84    ('roots(set)',
 85     _('Changesets whith no parent changeset in set.')),
 86    ('present(set)',
 87     _('An empty set, if any revision in set isn\'t found; otherwise, '
 88       'all revisions in set.')),
 89)
 90
 91_logical = (
 92    ('min(set)',
 93     _('Changeset with lowest revision number in set.')),
 94    ('max(set)',
 95     _('Changeset with highest revision number in set.')),
 96    ('limit(set, n)',
 97     _('First n members of a set.')),
 98    ('sort(set[, [-]key...])',
 99     _('Sort set by keys.  The default sort order is ascending, specify a '
100       'key as "-key" to sort in descending order.')),
101    ('follow()',
102     _('An alias for "::." (ancestors of the working copy\'s first parent).')),
103    ('all()',
104     _('All changesets, the same as 0:tip.')),
105)
106
107class RevisionSetQuery(QDialog):
108    # Emit query string and resulting revision set
109    queryIssued = pyqtSignal(QString, object)
110    showMessage = pyqtSignal(QString)
111    progress = pyqtSignal(QString, object, QString, QString, object)
112
113    def __init__(self, repo, parent=None):
114        QDialog.__init__(self, parent)
115
116        self.repo = repo
117        self.setWindowTitle(_('Revision Set Query'))
118        self.setWindowFlags(Qt.Tool)
119
120        layout = QVBoxLayout()
121        layout.setMargin(0)
122        layout.setContentsMargins(*(4,)*4)
123        self.setLayout(layout)
124
125        if 'hgsubversion' in repo.extensions():
126            global _logical, _ancestry
127            _logical = list(_logical) + [('fromsvn()',
128                    _('all revisions converted from subversion')),]
129            _ancestry = list(_ancestry) + [('svnrev(rev)',
130                    _('changeset which represents converted svn revision')),]
131        if 'bookmarks' in repo.extensions():
132            global _common
133            _common = list(_common) + [('bookmark([name])',
134                    _('The named bookmark or all bookmarks.')),]
135
136        self.stbar = cmdui.ThgStatusBar(self)
137        self.stbar.setSizeGripEnabled(False)
138        self.stbar.lbl.setOpenExternalLinks(True)
139        self.showMessage.connect(self.stbar.showMessage)
140        self.progress.connect(self.stbar.progress)
141
142        hbox = QHBoxLayout()
143        hbox.setContentsMargins(*(0,)*4)
144
145        cgb = QGroupBox(_('Common sets'))
146        cgb.setLayout(QVBoxLayout())
147        cgb.layout().setContentsMargins(*(2,)*4)
148        def setCommonHelp(item):
149            self.stbar.showMessage(self.clw._help[self.clw.row(item)])
150        self.clw = QListWidget(self)
151        self.clw.addItems([x for x, y in _common])
152        self.clw._help = [y for x, y in _common]
153        self.clw.itemClicked.connect(setCommonHelp)
154        cgb.layout().addWidget(self.clw)
155        hbox.addWidget(cgb)
156
157        fgb = QGroupBox(_('File pattern sets'))
158        fgb.setLayout(QVBoxLayout())
159        fgb.layout().setContentsMargins(*(2,)*4)
160        def setFileHelp(item):
161            self.stbar.showMessage(self.flw._help[self.flw.row(item)])
162        self.flw = QListWidget(self)
163        self.flw.addItems([x for x, y in _filepatterns])
164        self.flw._help = [y for x, y in _filepatterns]
165        self.flw.itemClicked.connect(setFileHelp)
166        fgb.layout().addWidget(self.flw)
167        hbox.addWidget(fgb)
168
169        agb = QGroupBox(_('Set Ancestry'))
170        agb.setLayout(QVBoxLayout())
171        agb.layout().setContentsMargins(*(2,)*4)
172        def setAncHelp(item):
173            self.stbar.showMessage(self.alw._help[self.alw.row(item)])
174        self.alw = QListWidget(self)
175        self.alw.addItems([x for x, y in _ancestry])
176        self.alw._help = [y for x, y in _ancestry]
177        self.alw.itemClicked.connect(setAncHelp)
178        agb.layout().addWidget(self.alw)
179        hbox.addWidget(agb)
180
181        lgb = QGroupBox(_('Set Logic'))
182        lgb.setLayout(QVBoxLayout())
183        lgb.layout().setContentsMargins(*(2,)*4)
184        def setManipHelp(item):
185            self.stbar.showMessage(self.llw._help[self.llw.row(item)])
186        self.llw = QListWidget(self)
187        self.llw.addItems([x for x, y in _logical])
188        self.llw._help = [y for x, y in _logical]
189        self.llw.itemClicked.connect(setManipHelp)
190        lgb.layout().addWidget(self.llw)
191        hbox.addWidget(lgb)
192
193        # Clicking on one listwidget should clear selection of the others
194        listwidgets = (self.clw, self.flw, self.alw, self.llw)
195        for w in listwidgets:
196            w.itemClicked.connect(self.itemClicked)
197            #w.itemActivated.connect(self.returnPressed)
198            for w2 in listwidgets:
199                if w is not w2:
200                    w.itemClicked.connect(w2.clearSelection)
201
202        layout.addLayout(hbox, 1)
203
204        self.entry = RevsetEntry(self)
205        self.entry.addCompletions(_logical, _ancestry, _filepatterns, _common)
206        layout.addWidget(self.entry, 0)
207
208        txt = _('<a href="http://www.selenic.com/mercurial/hg.1.html#revsets">'
209                'help revsets</a>')
210        helpLabel = QLabel(txt)
211        helpLabel.setOpenExternalLinks(True)
212        self.stbar.addPermanentWidget(helpLabel)
213        layout.addWidget(self.stbar, 0)
214
215    def runQuery(self):
216        self.entry.setEnabled(False)
217        self.showMessage.emit(_('Searching...'))
218        self.progress.emit(*cmdui.startProgress(_('Running'), _('query')))
219
220        self.refreshing = RevsetThread(self.repo, self.entry.text(), self)
221        self.refreshing.showMessage.connect(self.showMessage)
222        self.refreshing.queryIssued.connect(self.queryIssued)
223        self.refreshing.finished.connect(self.queryFinished)
224        self.refreshing.setCursorPosition.connect(self.entry.setCursorPosition)
225        self.refreshing.start()
226
227    def queryFinished(self):
228        self.refreshing.wait()
229        self.entry.setEnabled(True)
230        self.progress.emit(*cmdui.stopProgress(_('Running')))
231
232    def returnPressed(self):
233        text = self.entry.text()
234        if self.entry.hasSelectedText():
235            lineFrom, indexFrom, lineTo, indexTo = self.entry.getSelection()
236            start = self.entry.positionFromLineIndex(lineFrom, indexFrom)
237            end = self.entry.positionFromLineIndex(lineTo, indexTo)
238            sel = self.entry.selectedText()
239            if sel.count('(') and sel.contains(')'):
240                bopen = sel.indexOf('(')
241                bclose = sel.lastIndexOf(')')
242                if bopen < bclose:
243                    self.entry.setSelection(lineFrom, start+bopen+1, 
244                                            lineFrom, start+bclose)
245                    self.entry.setFocus()
246                    return
247            self.entry.setSelection(lineTo, indexTo,
248                                    lineTo, indexTo)
249        else:
250            self.runQuery()
251        self.entry.setFocus()
252
253
254    def itemClicked(self, item):
255        self.entry.beginUndoAction()
256        text = self.entry.text()
257        itext, ilen = item.text(), len(item.text())
258        if self.entry.hasSelectedText():
259            # replace selection
260            lineFrom, indexFrom, lineTo, indexTo = self.entry.getSelection()
261            start = self.entry.positionFromLineIndex(lineFrom, indexFrom)
262            end = self.entry.positionFromLineIndex(lineTo, indexTo)
263            newtext = text[:start] + itext + text[end:]
264            self.entry.setText(newtext)
265            self.entry.setSelection(lineFrom, indexFrom,
266                                    lineFrom, indexFrom+ilen)
267        else:
268            line, index = self.entry.getCursorPosition()
269            pos = self.entry.positionFromLineIndex(line, index)
270            if len(text) <= pos:
271                # cursor at end of text, append
272                if text and text[-1] != u' ':
273                    text = text + u' '
274                newtext = text + itext
275                self.entry.setText(newtext)
276                self.entry.setSelection(line, len(text), line, len(newtext))
277            elif text[pos] == u' ':
278                # cursor is at a space, insert item
279                newtext = text[:pos] + itext + text[pos:]
280                self.entry.setText(newtext)
281                self.entry.setSelection(line, pos, line, pos+ilen)
282            else:
283                # cursor is on text, wrap current word
284                start, end = pos, pos
285                while start and text[start-1] != u' ':
286                    start = start-1
287                while end < len(text) and text[end] != u' ':
288                    end = end+1
289                bopen = itext.indexOf('(')
290                newtext = text[:start] + itext[:bopen+1] + text[start:end] + \
291                          ')' + text[end:] 
292                self.entry.setText(newtext)
293                self.entry.setSelection(line, start, line, end+bopen+2)
294        self.entry.endUndoAction()
295
296    def keyPressEvent(self, event):
297        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
298            self.returnPressed()
299            return
300        super(RevisionSetQuery, self).keyPressEvent(event)
301
302    def accept(self):
303        self.hide()
304
305    def reject(self):
306        self.accept()
307
308class RevsetEntry(QsciScintilla):
309    def __init__(self, parent=None):
310        super(RevsetEntry, self).__init__(parent)
311        self.setMarginWidth(1, 0)
312        self.setReadOnly(False)
313        self.setUtf8(True)
314        self.setCaretWidth(10)
315        self.setCaretLineBackgroundColor(QColor("#e6fff0"))
316        self.setCaretLineVisible(True)
317        self.setAutoIndent(True)
318        self.setMatchedBraceBackgroundColor(Qt.yellow)
319        self.setIndentationsUseTabs(False)
320        self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
321
322        self.setWrapMode(QsciScintilla.WrapWord)
323        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
324        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
325
326        sp = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
327        sp.setHorizontalStretch(1)
328        sp.setVerticalStretch(0)
329        self.setSizePolicy(sp)
330
331        self.setAutoCompletionThreshold(2)
332        self.setAutoCompletionSource(QsciScintilla.AcsAPIs)
333        self.setAutoCompletionFillupsEnabled(True)
334        self.setLexer(QsciLexerPython(self))
335        self.lexer().setFont(qtlib.getfont('fontcomment').font())
336        self.apis = QsciAPIs(self.lexer())
337
338    def addCompletions(self, *lists):
339        for list in lists:
340            for x, y in list:
341                self.apis.add(x)
342        self.apis.prepare()
343
344    def keyPressEvent(self, event):
345        if event.key() == Qt.Key_Escape:
346            event.ignore()
347            return
348        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
349            if not self.isListActive():
350                event.ignore()
351                return
352        super(RevsetEntry, self).keyPressEvent(event)
353
354    def sizeHint(self):
355        return QSize(10, self.fontMetrics().height())
356
357
358
359class RevsetThread(QThread):
360    queryIssued = pyqtSignal(QString, object)
361    showMessage = pyqtSignal(QString)
362    setCursorPosition = pyqtSignal(int, int)
363
364    def __init__(self, repo, query, parent):
365        super(RevsetThread, self).__init__(parent)
366        self.repo = hg.repository(repo.ui, repo.root)
367        self.text = hglib.fromunicode(query)
368        self.query = query
369
370    def run(self):
371        cwd = os.getcwd()
372        try:
373            os.chdir(self.repo.root)
374            func = revset.match(self.text)
375            l = []
376            for c in func(self.repo, range(len(self.repo))):
377                l.append(c)
378            if l:
379                self.showMessage.emit(_('%d matches found') % len(l))
380                self.queryIssued.emit(self.query, l)
381            else:
382                self.showMessage.emit(_('No matches'))
383        except error.ParseError, e:
384            if len(e.args) == 2:
385                msg, pos = e.args
386                self.setCursorPosition.emit(0, pos)
387            else:
388                msg = e.args[0]
389            self.showMessage.emit(_('Parse Error: ') + hglib.tounicode(msg))
390        except Exception, e:
391            self.showMessage.emit(_('Invalid query: ')+hglib.tounicode(str(e)))
392
393        os.chdir(cwd)
394
395def run(ui, *pats, **opts):
396    from tortoisehg.util import paths
397    from tortoisehg.hgqt import thgrepo
398    repo = thgrepo.repository(ui, path=paths.find_root())
399    return RevisionSetQuery(repo)