1 from qtpy
import QtCore
2 from qtpy
import QtWidgets
3 from qtpy
.QtCore
import Qt
4 from qtpy
.QtCore
import Signal
7 from ..utils
import Group
10 from .. import hotkeys
12 from .. import qtutils
13 from ..qtutils
import get
14 from .standard
import Dialog
15 from .text
import HintedLineEdit
16 from .text
import VimHintedPlainTextEdit
17 from .text
import VimTextBrowser
22 """Prompt and use 'git grep' to find the content."""
23 widget
= new_grep(context
, parent
=qtutils
.active_window())
28 def new_grep(context
, text
=None, parent
=None):
29 """Construct a new Grep dialog"""
30 widget
= Grep(context
, parent
=parent
)
32 widget
.search_for(text
)
36 def parse_grep_line(line
):
37 """Parse a grep result line into (filename, line_number, content)"""
39 filename
, line_number
, contents
= line
.split(':', 2)
40 result
= (filename
, line_number
, contents
)
46 def goto_grep(context
, line
):
47 """Called when Search -> Grep's right-click 'goto' action."""
48 parsed_line
= parse_grep_line(line
)
50 filename
, line_number
, _
= parsed_line
55 line_number
=line_number
,
56 background_editor
=True,
60 class GrepThread(QtCore
.QThread
):
61 """Gather `git grep` results in a background thread"""
63 result
= Signal(object, object, object)
65 def __init__(self
, context
, parent
):
66 QtCore
.QThread
.__init
__(self
, parent
)
67 self
.context
= context
70 self
.regexp_mode
= '--basic-regexp'
73 if self
.query
is None:
75 git
= self
.context
.git
78 args
= utils
.shell_split(query
)
81 status
, out
, err
= git
.grep(self
.regexp_mode
, n
=True, _readonly
=True, *args
)
82 if query
== self
.query
:
83 self
.result
.emit(status
, out
, err
)
89 """A dialog for searching content using `git grep`"""
91 def __init__(self
, context
, parent
=None):
92 Dialog
.__init
__(self
, parent
)
93 self
.context
= context
96 self
.setWindowTitle(N_('Search'))
97 if parent
is not None:
98 self
.setWindowModality(Qt
.WindowModal
)
100 self
.edit_action
= qtutils
.add_action(self
, N_('Edit'), self
.edit
, hotkeys
.EDIT
)
102 self
.refresh_action
= qtutils
.add_action(
103 self
, N_('Refresh'), self
.search
, *hotkeys
.REFRESH_HOTKEYS
106 self
.input_label
= QtWidgets
.QLabel('git grep')
107 self
.input_label
.setFont(qtutils
.diff_font(context
))
109 self
.input_txt
= HintedLineEdit(
110 context
, N_('command-line arguments'), parent
=self
113 self
.regexp_combo
= combo
= QtWidgets
.QComboBox()
114 combo
.setToolTip(N_('Choose the "git grep" regular expression mode'))
115 items
= [N_('Basic Regexp'), N_('Extended Regexp'), N_('Fixed String')]
116 combo
.addItems(items
)
117 combo
.setCurrentIndex(0)
118 combo
.setEditable(False)
120 tooltip0
= N_('Search using a POSIX basic regular expression')
121 tooltip1
= N_('Search using a POSIX extended regular expression')
122 tooltip2
= N_('Search for a fixed string')
123 combo
.setItemData(0, tooltip0
, Qt
.ToolTipRole
)
124 combo
.setItemData(1, tooltip1
, Qt
.ToolTipRole
)
125 combo
.setItemData(2, tooltip2
, Qt
.ToolTipRole
)
126 combo
.setItemData(0, '--basic-regexp', Qt
.UserRole
)
127 combo
.setItemData(1, '--extended-regexp', Qt
.UserRole
)
128 combo
.setItemData(2, '--fixed-strings', Qt
.UserRole
)
130 self
.result_txt
= GrepTextView(context
, N_('grep result...'), self
)
131 self
.preview_txt
= PreviewTextView(context
, self
)
132 self
.preview_txt
.setFocusProxy(self
.result_txt
)
134 self
.edit_button
= qtutils
.edit_button(default
=True)
135 qtutils
.button_action(self
.edit_button
, self
.edit_action
)
137 self
.refresh_button
= qtutils
.refresh_button()
138 qtutils
.button_action(self
.refresh_button
, self
.refresh_action
)
140 text
= N_('Shell arguments')
142 'Parse arguments using a shell.\n'
143 'Queries with spaces will require "double quotes".'
145 self
.shell_checkbox
= qtutils
.checkbox(
146 text
=text
, tooltip
=tooltip
, checked
=False
148 self
.close_button
= qtutils
.close_button()
150 self
.refresh_group
= Group(self
.refresh_action
, self
.refresh_button
)
151 self
.refresh_group
.setEnabled(False)
153 self
.edit_group
= Group(self
.edit_action
, self
.edit_button
)
154 self
.edit_group
.setEnabled(False)
156 self
.input_layout
= qtutils
.hbox(
164 self
.bottom_layout
= qtutils
.hbox(
174 self
.splitter
= qtutils
.splitter(Qt
.Vertical
, self
.result_txt
, self
.preview_txt
)
176 self
.mainlayout
= qtutils
.vbox(
183 self
.setLayout(self
.mainlayout
)
185 thread
= self
.worker_thread
= GrepThread(context
, self
)
186 thread
.result
.connect(self
.process_result
, type=Qt
.QueuedConnection
)
188 # pylint: disable=no-member
189 self
.input_txt
.textChanged
.connect(lambda s
: self
.search())
190 self
.regexp_combo
.currentIndexChanged
.connect(lambda x
: self
.search())
191 self
.result_txt
.leave
.connect(self
.input_txt
.setFocus
)
192 self
.result_txt
.cursorPositionChanged
.connect(self
.update_preview
)
201 qtutils
.add_action(self
, 'Focus Input', self
.focus_input
, hotkeys
.FOCUS
)
203 qtutils
.connect_toggle(self
.shell_checkbox
, lambda x
: self
.search())
204 qtutils
.connect_button(self
.close_button
, self
.close
)
205 qtutils
.add_close_action(self
)
207 self
.init_size(parent
=parent
)
209 def focus_input(self
):
210 """Focus the grep input field and select the text"""
211 self
.input_txt
.setFocus()
212 self
.input_txt
.selectAll()
214 def focus_results(self
):
215 """Give focus to the results window"""
216 self
.result_txt
.setFocus()
218 def regexp_mode(self
):
219 """Return the selected grep regex mode"""
220 idx
= self
.regexp_combo
.currentIndex()
221 return self
.regexp_combo
.itemData(idx
, Qt
.UserRole
)
224 """Initiate a search by starting the GrepThread"""
225 self
.edit_group
.setEnabled(False)
226 self
.refresh_group
.setEnabled(False)
228 query
= get(self
.input_txt
)
230 self
.result_txt
.clear()
231 self
.preview_txt
.clear()
233 self
.worker_thread
.query
= query
234 self
.worker_thread
.shell
= get(self
.shell_checkbox
)
235 self
.worker_thread
.regexp_mode
= self
.regexp_mode()
236 self
.worker_thread
.start()
238 def search_for(self
, txt
):
239 """Set the initial value of the input text"""
240 self
.input_txt
.set_value(txt
)
242 def text_scroll(self
):
243 """Return the scrollbar value for the results window"""
244 scrollbar
= self
.result_txt
.verticalScrollBar()
246 return get(scrollbar
)
249 def set_text_scroll(self
, scroll
):
250 """Set the scrollbar value for the results window"""
251 scrollbar
= self
.result_txt
.verticalScrollBar()
252 if scrollbar
and scroll
is not None:
253 scrollbar
.setValue(scroll
)
255 def text_offset(self
):
256 """Return the cursor's offset within the result text"""
257 return self
.result_txt
.textCursor().position()
259 def set_text_offset(self
, offset
):
260 """Set the text cursor from an offset"""
261 cursor
= self
.result_txt
.textCursor()
262 cursor
.setPosition(offset
)
263 self
.result_txt
.setTextCursor(cursor
)
265 def process_result(self
, status
, out
, err
):
266 """Apply the results from grep to the widgets"""
270 value
= 'git grep: ' + out
+ err
274 # save scrollbar and text cursor
275 scroll
= self
.text_scroll()
276 offset
= min(len(value
), self
.text_offset())
278 self
.grep_result
= value
279 self
.result_txt
.set_value(value
)
281 self
.set_text_scroll(scroll
)
282 self
.set_text_offset(offset
)
284 enabled
= status
== 0
285 self
.edit_group
.setEnabled(enabled
)
286 self
.refresh_group
.setEnabled(True)
288 self
.preview_txt
.clear()
290 def update_preview(self
):
291 """Update the file preview window"""
292 parsed_line
= parse_grep_line(self
.result_txt
.selected_line())
294 filename
, line_number
, _
= parsed_line
295 self
.preview_txt
.preview(filename
, line_number
)
298 """Launch an editor on the currently selected line"""
299 goto_grep(self
.context
, self
.result_txt
.selected_line())
301 def export_state(self
):
302 """Export persistent settings"""
303 state
= super().export_state()
304 state
['sizes'] = get(self
.splitter
)
307 def apply_state(self
, state
):
308 """Apply persistent settings"""
309 result
= super().apply_state(state
)
311 self
.splitter
.setSizes(state
['sizes'])
312 except (AttributeError, KeyError, ValueError, TypeError):
317 # pylint: disable=too-many-ancestors
318 class GrepTextView(VimHintedPlainTextEdit
):
319 """A text view with hotkeys for launching editors"""
321 def __init__(self
, context
, hint
, parent
):
322 VimHintedPlainTextEdit
.__init
__(self
, context
, hint
, parent
=parent
)
323 self
.context
= context
324 self
.goto_action
= qtutils
.add_action(self
, 'Launch Editor', self
.edit
)
325 self
.goto_action
.setShortcut(hotkeys
.EDIT
)
326 self
.menu_actions
.append(self
.goto_action
)
329 goto_grep(self
.context
, self
.selected_line())
332 class PreviewTask(qtutils
.Task
):
333 """Asynchronous task for loading file content"""
335 def __init__(self
, filename
, line_number
):
336 qtutils
.Task
.__init
__(self
)
339 self
.filename
= filename
340 self
.line_number
= line_number
344 self
.content
= core
.read(self
.filename
, errors
='ignore')
347 return (self
.filename
, self
.content
, self
.line_number
)
350 # pylint: disable=too-many-ancestors
351 class PreviewTextView(VimTextBrowser
):
352 """Preview window for file contents"""
354 def __init__(self
, context
, parent
):
355 VimTextBrowser
.__init
__(self
, context
, parent
)
358 self
.runtask
= qtutils
.RunTask(parent
=self
)
360 def preview(self
, filename
, line_number
):
361 """Preview the a file at the specified line number"""
363 if filename
!= self
.filename
:
364 request
= PreviewTask(filename
, line_number
)
365 self
.runtask
.start(request
, finish
=self
.show_preview
)
367 self
.scroll_to_line(line_number
)
374 def show_preview(self
, task
):
375 """Show the results of the asynchronous file read"""
377 filename
= task
.filename
378 content
= task
.content
379 line_number
= task
.line_number
381 if filename
!= self
.filename
:
382 self
.filename
= filename
383 self
.content
= content
384 self
.set_value(content
)
386 self
.scroll_to_line(line_number
)
388 def scroll_to_line(self
, line_number
):
389 """Scroll to the specified line number"""
391 line_num
= int(line_number
) - 1
395 self
.numbers
.set_highlighted(line_num
)
396 cursor
= self
.textCursor()
397 cursor
.setPosition(0)
398 self
.setTextCursor(cursor
)
399 self
.ensureCursorVisible()
401 cursor
.movePosition(cursor
.Down
, cursor
.MoveAnchor
, line_num
)
402 cursor
.movePosition(cursor
.EndOfLine
, cursor
.KeepAnchor
)
403 self
.setTextCursor(cursor
)
404 self
.ensureCursorVisible()
406 scrollbar
= self
.verticalScrollBar()
407 step
= scrollbar
.pageStep() // 2
408 position
= scrollbar
.sliderPosition() + step
409 scrollbar
.setSliderPosition(position
)
410 self
.ensureCursorVisible()