extras: pylint fixes
[git-cola.git] / cola / widgets / grep.py
blob0c1a1828ec69e94f31f083d09e93b7ce36e07e6c
1 from __future__ import absolute_import, division, print_function, unicode_literals
3 from qtpy import QtCore
4 from qtpy import QtWidgets
5 from qtpy.QtCore import Qt
6 from qtpy.QtCore import Signal
8 from ..i18n import N_
9 from ..utils import Group
10 from .. import cmds
11 from .. import core
12 from .. import hotkeys
13 from .. import utils
14 from .. import qtutils
15 from ..qtutils import get
16 from .standard import Dialog
17 from .text import HintedLineEdit
18 from .text import VimHintedPlainTextEdit
19 from .text import VimTextBrowser
20 from . import defs
23 def grep(context):
24 """Prompt and use 'git grep' to find the content."""
25 widget = new_grep(context, parent=qtutils.active_window())
26 widget.show()
27 return widget
30 def new_grep(context, text=None, parent=None):
31 """Construct a new Grep dialog"""
32 widget = Grep(context, parent=parent)
33 if text:
34 widget.search_for(text)
35 return widget
38 def parse_grep_line(line):
39 """Parse a grep result line into (filename, line_number, content)"""
40 try:
41 filename, line_number, contents = line.split(':', 2)
42 result = (filename, line_number, contents)
43 except ValueError:
44 result = None
45 return result
48 def goto_grep(context, line):
49 """Called when Search -> Grep's right-click 'goto' action."""
50 parsed_line = parse_grep_line(line)
51 if parsed_line:
52 filename, line_number, _ = parsed_line
53 cmds.do(
54 cmds.Edit,
55 context,
56 [filename],
57 line_number=line_number,
58 background_editor=True,
62 class GrepThread(QtCore.QThread):
63 """Gather `git grep` results in a background thread"""
65 result = Signal(object, object, object)
67 def __init__(self, context, parent):
68 QtCore.QThread.__init__(self, parent)
69 self.context = context
70 self.query = None
71 self.shell = False
72 self.regexp_mode = '--basic-regexp'
74 def run(self):
75 if self.query is None:
76 return
77 git = self.context.git
78 query = self.query
79 if self.shell:
80 args = utils.shell_split(query)
81 else:
82 args = [query]
83 status, out, err = git.grep(self.regexp_mode, n=True, *args)
84 if query == self.query:
85 self.result.emit(status, out, err)
86 else:
87 self.run()
90 class Grep(Dialog):
91 """A dialog for searching content using `git grep`"""
93 def __init__(self, context, parent=None):
94 Dialog.__init__(self, parent)
95 self.context = context
96 self.grep_result = ''
98 self.setWindowTitle(N_('Search'))
99 if parent is not None:
100 self.setWindowModality(Qt.WindowModal)
102 self.edit_action = qtutils.add_action(self, N_('Edit'), self.edit, hotkeys.EDIT)
104 self.refresh_action = qtutils.add_action(
105 self, N_('Refresh'), self.search, *hotkeys.REFRESH_HOTKEYS
108 self.input_label = QtWidgets.QLabel('git grep')
109 self.input_label.setFont(qtutils.diff_font(context))
111 self.input_txt = HintedLineEdit(
112 context, N_('command-line arguments'), parent=self
115 self.regexp_combo = combo = QtWidgets.QComboBox()
116 combo.setToolTip(N_('Choose the "git grep" regular expression mode'))
117 items = [N_('Basic Regexp'), N_('Extended Regexp'), N_('Fixed String')]
118 combo.addItems(items)
119 combo.setCurrentIndex(0)
120 combo.setEditable(False)
122 tooltip0 = N_('Search using a POSIX basic regular expression')
123 tooltip1 = N_('Search using a POSIX extended regular expression')
124 tooltip2 = N_('Search for a fixed string')
125 combo.setItemData(0, tooltip0, Qt.ToolTipRole)
126 combo.setItemData(1, tooltip1, Qt.ToolTipRole)
127 combo.setItemData(2, tooltip2, Qt.ToolTipRole)
128 combo.setItemData(0, '--basic-regexp', Qt.UserRole)
129 combo.setItemData(1, '--extended-regexp', Qt.UserRole)
130 combo.setItemData(2, '--fixed-strings', Qt.UserRole)
132 self.result_txt = GrepTextView(context, N_('grep result...'), self)
133 self.preview_txt = PreviewTextView(context, self)
134 self.preview_txt.setFocusProxy(self.result_txt)
136 self.edit_button = qtutils.edit_button(default=True)
137 qtutils.button_action(self.edit_button, self.edit_action)
139 self.refresh_button = qtutils.refresh_button()
140 qtutils.button_action(self.refresh_button, self.refresh_action)
142 text = N_('Shell arguments')
143 tooltip = N_(
144 'Parse arguments using a shell.\n'
145 'Queries with spaces will require "double quotes".'
147 self.shell_checkbox = qtutils.checkbox(
148 text=text, tooltip=tooltip, checked=False
150 self.close_button = qtutils.close_button()
152 self.refresh_group = Group(self.refresh_action, self.refresh_button)
153 self.refresh_group.setEnabled(False)
155 self.edit_group = Group(self.edit_action, self.edit_button)
156 self.edit_group.setEnabled(False)
158 self.input_layout = qtutils.hbox(
159 defs.no_margin,
160 defs.button_spacing,
161 self.input_label,
162 self.input_txt,
163 self.regexp_combo,
166 self.bottom_layout = qtutils.hbox(
167 defs.no_margin,
168 defs.button_spacing,
169 self.close_button,
170 qtutils.STRETCH,
171 self.shell_checkbox,
172 self.refresh_button,
173 self.edit_button,
176 self.splitter = qtutils.splitter(Qt.Vertical, self.result_txt, self.preview_txt)
178 self.mainlayout = qtutils.vbox(
179 defs.margin,
180 defs.no_spacing,
181 self.input_layout,
182 self.splitter,
183 self.bottom_layout,
185 self.setLayout(self.mainlayout)
187 thread = self.worker_thread = GrepThread(context, self)
188 thread.result.connect(self.process_result, type=Qt.QueuedConnection)
190 # pylint: disable=no-member
191 self.input_txt.textChanged.connect(lambda s: self.search())
192 self.regexp_combo.currentIndexChanged.connect(lambda x: self.search())
193 self.result_txt.leave.connect(self.input_txt.setFocus)
194 self.result_txt.cursorPositionChanged.connect(self.update_preview)
196 qtutils.add_action(
197 self.input_txt,
198 'Focus Results',
199 self.focus_results,
200 hotkeys.DOWN,
201 *hotkeys.ACCEPT
203 qtutils.add_action(self, 'Focus Input', self.focus_input, hotkeys.FOCUS)
205 qtutils.connect_toggle(self.shell_checkbox, lambda x: self.search())
206 qtutils.connect_button(self.close_button, self.close)
207 qtutils.add_close_action(self)
209 self.init_size(parent=parent)
211 def focus_input(self):
212 """Focus the grep input field and select the text"""
213 self.input_txt.setFocus()
214 self.input_txt.selectAll()
216 def focus_results(self):
217 """Give focus to the results window"""
218 self.result_txt.setFocus()
220 def regexp_mode(self):
221 """Return the selected grep regex mode"""
222 idx = self.regexp_combo.currentIndex()
223 return self.regexp_combo.itemData(idx, Qt.UserRole)
225 def search(self):
226 """Initiate a search by starting the GrepThread"""
227 self.edit_group.setEnabled(False)
228 self.refresh_group.setEnabled(False)
230 query = get(self.input_txt)
231 if len(query) < 2:
232 self.result_txt.clear()
233 self.preview_txt.clear()
234 return
235 self.worker_thread.query = query
236 self.worker_thread.shell = get(self.shell_checkbox)
237 self.worker_thread.regexp_mode = self.regexp_mode()
238 self.worker_thread.start()
240 def search_for(self, txt):
241 """Set the initial value of the input text"""
242 self.input_txt.set_value(txt)
244 def text_scroll(self):
245 """Return the scrollbar value for the results window"""
246 scrollbar = self.result_txt.verticalScrollBar()
247 if scrollbar:
248 return get(scrollbar)
249 return None
251 def set_text_scroll(self, scroll):
252 """Set the scrollbar value for the results window"""
253 scrollbar = self.result_txt.verticalScrollBar()
254 if scrollbar and scroll is not None:
255 scrollbar.setValue(scroll)
257 def text_offset(self):
258 """Return the cursor's offset within the result text"""
259 return self.result_txt.textCursor().position()
261 def set_text_offset(self, offset):
262 """Set the text cursor from an offset"""
263 cursor = self.result_txt.textCursor()
264 cursor.setPosition(offset)
265 self.result_txt.setTextCursor(cursor)
267 def process_result(self, status, out, err):
268 """Apply the results from grep to the widgets"""
269 if status == 0:
270 value = out + err
271 elif out + err:
272 value = 'git grep: ' + out + err
273 else:
274 value = ''
276 # save scrollbar and text cursor
277 scroll = self.text_scroll()
278 offset = min(len(value), self.text_offset())
280 self.grep_result = value
281 self.result_txt.set_value(value)
282 # restore
283 self.set_text_scroll(scroll)
284 self.set_text_offset(offset)
286 enabled = status == 0
287 self.edit_group.setEnabled(enabled)
288 self.refresh_group.setEnabled(True)
289 if not value:
290 self.preview_txt.clear()
292 def update_preview(self):
293 """Update the file preview window"""
294 parsed_line = parse_grep_line(self.result_txt.selected_line())
295 if parsed_line:
296 filename, line_number, _ = parsed_line
297 self.preview_txt.preview(filename, line_number)
299 def edit(self):
300 """Launch an editor on the currently selected line"""
301 goto_grep(self.context, self.result_txt.selected_line())
303 def export_state(self):
304 """Export persistent settings"""
305 state = super(Grep, self).export_state()
306 state['sizes'] = get(self.splitter)
307 return state
309 def apply_state(self, state):
310 """Apply persistent settings"""
311 result = super(Grep, self).apply_state(state)
312 try:
313 self.splitter.setSizes(state['sizes'])
314 except (AttributeError, KeyError, ValueError, TypeError):
315 result = False
316 return result
319 # pylint: disable=too-many-ancestors
320 class GrepTextView(VimHintedPlainTextEdit):
321 """A text view with hotkeys for launching editors"""
323 def __init__(self, context, hint, parent):
324 VimHintedPlainTextEdit.__init__(self, context, hint, parent=parent)
325 self.context = context
326 self.goto_action = qtutils.add_action(self, 'Launch Editor', self.edit)
327 self.goto_action.setShortcut(hotkeys.EDIT)
329 def contextMenuEvent(self, event):
330 menu = self.createStandardContextMenu(event.pos())
331 menu.addSeparator()
332 menu.addAction(self.goto_action)
333 menu.exec_(self.mapToGlobal(event.pos()))
335 def edit(self):
336 goto_grep(self.context, self.selected_line())
339 class PreviewTask(qtutils.Task):
340 """Asynchronous task for loading file content"""
342 def __init__(self, filename, line_number):
343 qtutils.Task.__init__(self)
345 self.content = ''
346 self.filename = filename
347 self.line_number = line_number
349 def task(self):
350 try:
351 self.content = core.read(self.filename, errors='ignore')
352 except IOError:
353 pass
354 return (self.filename, self.content, self.line_number)
357 # pylint: disable=too-many-ancestors
358 class PreviewTextView(VimTextBrowser):
359 """Preview window for file contents"""
361 def __init__(self, context, parent):
362 VimTextBrowser.__init__(self, context, parent)
363 self.filename = None
364 self.content = None
365 self.runtask = qtutils.RunTask(parent=self)
367 def preview(self, filename, line_number):
368 """Preview the a file at the specified line number"""
370 if filename != self.filename:
371 request = PreviewTask(filename, line_number)
372 self.runtask.start(request, finish=self.show_preview)
373 else:
374 self.scroll_to_line(line_number)
376 def clear(self):
377 self.filename = ''
378 self.content = ''
379 super(PreviewTextView, self).clear()
381 def show_preview(self, task):
382 """Show the results of the asynchronous file read"""
384 filename = task.filename
385 content = task.content
386 line_number = task.line_number
388 if filename != self.filename:
389 self.filename = filename
390 self.content = content
391 self.set_value(content)
393 self.scroll_to_line(line_number)
395 def scroll_to_line(self, line_number):
396 """Scroll to the specified line number"""
397 try:
398 line_num = int(line_number) - 1
399 except ValueError:
400 return
402 self.numbers.set_highlighted(line_num)
403 cursor = self.textCursor()
404 cursor.setPosition(0)
405 self.setTextCursor(cursor)
406 self.ensureCursorVisible()
408 cursor.movePosition(cursor.Down, cursor.MoveAnchor, line_num)
409 cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor)
410 self.setTextCursor(cursor)
411 self.ensureCursorVisible()
413 scrollbar = self.verticalScrollBar()
414 step = scrollbar.pageStep() // 2
415 position = scrollbar.sliderPosition() + step
416 scrollbar.setSliderPosition(position)
417 self.ensureCursorVisible()