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, _readonly
=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
)
328 self
.menu_actions
.append(self
.goto_action
)
331 goto_grep(self
.context
, self
.selected_line())
334 class PreviewTask(qtutils
.Task
):
335 """Asynchronous task for loading file content"""
337 def __init__(self
, filename
, line_number
):
338 qtutils
.Task
.__init
__(self
)
341 self
.filename
= filename
342 self
.line_number
= line_number
346 self
.content
= core
.read(self
.filename
, errors
='ignore')
349 return (self
.filename
, self
.content
, self
.line_number
)
352 # pylint: disable=too-many-ancestors
353 class PreviewTextView(VimTextBrowser
):
354 """Preview window for file contents"""
356 def __init__(self
, context
, parent
):
357 VimTextBrowser
.__init
__(self
, context
, parent
)
360 self
.runtask
= qtutils
.RunTask(parent
=self
)
362 def preview(self
, filename
, line_number
):
363 """Preview the a file at the specified line number"""
365 if filename
!= self
.filename
:
366 request
= PreviewTask(filename
, line_number
)
367 self
.runtask
.start(request
, finish
=self
.show_preview
)
369 self
.scroll_to_line(line_number
)
374 super(PreviewTextView
, self
).clear()
376 def show_preview(self
, task
):
377 """Show the results of the asynchronous file read"""
379 filename
= task
.filename
380 content
= task
.content
381 line_number
= task
.line_number
383 if filename
!= self
.filename
:
384 self
.filename
= filename
385 self
.content
= content
386 self
.set_value(content
)
388 self
.scroll_to_line(line_number
)
390 def scroll_to_line(self
, line_number
):
391 """Scroll to the specified line number"""
393 line_num
= int(line_number
) - 1
397 self
.numbers
.set_highlighted(line_num
)
398 cursor
= self
.textCursor()
399 cursor
.setPosition(0)
400 self
.setTextCursor(cursor
)
401 self
.ensureCursorVisible()
403 cursor
.movePosition(cursor
.Down
, cursor
.MoveAnchor
, line_num
)
404 cursor
.movePosition(cursor
.EndOfLine
, cursor
.KeepAnchor
)
405 self
.setTextCursor(cursor
)
406 self
.ensureCursorVisible()
408 scrollbar
= self
.verticalScrollBar()
409 step
= scrollbar
.pageStep() // 2
410 position
= scrollbar
.sliderPosition() + step
411 scrollbar
.setSliderPosition(position
)
412 self
.ensureCursorVisible()