/tortoisehg/hgqt/revset.py

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