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
9 from ..utils
import Group
12 from .. import hotkeys
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
24 """Prompt and use 'git grep' to find the content."""
25 widget
= new_grep(context
, parent
=qtutils
.active_window())
30 def new_grep(context
, text
=None, parent
=None):
31 """Construct a new Grep dialog"""
32 widget
= Grep(context
, parent
=parent
)
34 widget
.search_for(text
)
38 def parse_grep_line(line
):
39 """Parse a grep result line into (filename, line_number, content)"""
41 filename
, line_number
, contents
= line
.split(':', 2)
42 result
= (filename
, line_number
, contents
)
48 def goto_grep(context
, line
):
49 """Called when Search -> Grep's right-click 'goto' action."""
50 parsed_line
= parse_grep_line(line
)
52 filename
, line_number
, _
= parsed_line
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
72 self
.regexp_mode
= '--basic-regexp'
75 if self
.query
is None:
77 git
= self
.context
.git
80 args
= utils
.shell_split(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
)
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
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')
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(
166 self
.bottom_layout
= qtutils
.hbox(
176 self
.splitter
= qtutils
.splitter(Qt
.Vertical
, self
.result_txt
, self
.preview_txt
)
178 self
.mainlayout
= qtutils
.vbox(
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
)
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
)
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
)
232 self
.result_txt
.clear()
233 self
.preview_txt
.clear()
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()
248 return get(scrollbar
)
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"""
272 value
= 'git grep: ' + out
+ err
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
)
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)
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())
296 filename
, line_number
, _
= parsed_line
297 self
.preview_txt
.preview(filename
, line_number
)
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
)
309 def apply_state(self
, state
):
310 """Apply persistent settings"""
311 result
= super(Grep
, self
).apply_state(state
)
313 self
.splitter
.setSizes(state
['sizes'])
314 except (AttributeError, KeyError, ValueError, TypeError):
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())
332 menu
.addAction(self
.goto_action
)
333 menu
.exec_(self
.mapToGlobal(event
.pos()))
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
)
346 self
.filename
= filename
347 self
.line_number
= line_number
351 self
.content
= core
.read(self
.filename
, errors
='ignore')
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
)
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
)
374 self
.scroll_to_line(line_number
)
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"""
398 line_num
= int(line_number
) - 1
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()