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 self
.input_txt
.textChanged
.connect(lambda s
: self
.search())
189 self
.regexp_combo
.currentIndexChanged
.connect(lambda x
: self
.search())
190 self
.result_txt
.leave
.connect(self
.input_txt
.setFocus
)
191 self
.result_txt
.cursorPositionChanged
.connect(self
.update_preview
)
200 qtutils
.add_action(self
, 'Focus Input', self
.focus_input
, hotkeys
.FOCUS
)
202 qtutils
.connect_toggle(self
.shell_checkbox
, lambda x
: self
.search())
203 qtutils
.connect_button(self
.close_button
, self
.close
)
204 qtutils
.add_close_action(self
)
206 self
.init_size(parent
=parent
)
208 def focus_input(self
):
209 """Focus the grep input field and select the text"""
210 self
.input_txt
.setFocus()
211 self
.input_txt
.selectAll()
213 def focus_results(self
):
214 """Give focus to the results window"""
215 self
.result_txt
.setFocus()
217 def regexp_mode(self
):
218 """Return the selected grep regex mode"""
219 idx
= self
.regexp_combo
.currentIndex()
220 return self
.regexp_combo
.itemData(idx
, Qt
.UserRole
)
223 """Initiate a search by starting the GrepThread"""
224 self
.edit_group
.setEnabled(False)
225 self
.refresh_group
.setEnabled(False)
227 query
= get(self
.input_txt
)
229 self
.result_txt
.clear()
230 self
.preview_txt
.clear()
232 self
.worker_thread
.query
= query
233 self
.worker_thread
.shell
= get(self
.shell_checkbox
)
234 self
.worker_thread
.regexp_mode
= self
.regexp_mode()
235 self
.worker_thread
.start()
237 def search_for(self
, txt
):
238 """Set the initial value of the input text"""
239 self
.input_txt
.set_value(txt
)
241 def text_scroll(self
):
242 """Return the scrollbar value for the results window"""
243 scrollbar
= self
.result_txt
.verticalScrollBar()
245 return get(scrollbar
)
248 def set_text_scroll(self
, scroll
):
249 """Set the scrollbar value for the results window"""
250 scrollbar
= self
.result_txt
.verticalScrollBar()
251 if scrollbar
and scroll
is not None:
252 scrollbar
.setValue(scroll
)
254 def text_offset(self
):
255 """Return the cursor's offset within the result text"""
256 return self
.result_txt
.textCursor().position()
258 def set_text_offset(self
, offset
):
259 """Set the text cursor from an offset"""
260 cursor
= self
.result_txt
.textCursor()
261 cursor
.setPosition(offset
)
262 self
.result_txt
.setTextCursor(cursor
)
264 def process_result(self
, status
, out
, err
):
265 """Apply the results from grep to the widgets"""
269 value
= 'git grep: ' + out
+ err
273 # save scrollbar and text cursor
274 scroll
= self
.text_scroll()
275 offset
= min(len(value
), self
.text_offset())
277 self
.grep_result
= value
278 self
.result_txt
.set_value(value
)
280 self
.set_text_scroll(scroll
)
281 self
.set_text_offset(offset
)
283 enabled
= status
== 0
284 self
.edit_group
.setEnabled(enabled
)
285 self
.refresh_group
.setEnabled(True)
287 self
.preview_txt
.clear()
289 def update_preview(self
):
290 """Update the file preview window"""
291 parsed_line
= parse_grep_line(self
.result_txt
.selected_line())
293 filename
, line_number
, _
= parsed_line
294 self
.preview_txt
.preview(filename
, line_number
)
297 """Launch an editor on the currently selected line"""
298 goto_grep(self
.context
, self
.result_txt
.selected_line())
300 def export_state(self
):
301 """Export persistent settings"""
302 state
= super().export_state()
303 state
['sizes'] = get(self
.splitter
)
306 def apply_state(self
, state
):
307 """Apply persistent settings"""
308 result
= super().apply_state(state
)
310 self
.splitter
.setSizes(state
['sizes'])
311 except (AttributeError, KeyError, ValueError, TypeError):
316 class GrepTextView(VimHintedPlainTextEdit
):
317 """A text view with hotkeys for launching editors"""
319 def __init__(self
, context
, hint
, parent
):
320 VimHintedPlainTextEdit
.__init
__(self
, context
, hint
, parent
=parent
)
321 self
.context
= context
322 self
.goto_action
= qtutils
.add_action(self
, 'Launch Editor', self
.edit
)
323 self
.goto_action
.setShortcut(hotkeys
.EDIT
)
324 self
.menu_actions
.append(self
.goto_action
)
327 goto_grep(self
.context
, self
.selected_line())
330 class PreviewTask(qtutils
.Task
):
331 """Asynchronous task for loading file content"""
333 def __init__(self
, filename
, line_number
):
334 qtutils
.Task
.__init
__(self
)
337 self
.filename
= filename
338 self
.line_number
= line_number
342 self
.content
= core
.read(self
.filename
, errors
='ignore')
345 return (self
.filename
, self
.content
, self
.line_number
)
348 class PreviewTextView(VimTextBrowser
):
349 """Preview window for file contents"""
351 def __init__(self
, context
, parent
):
352 VimTextBrowser
.__init
__(self
, context
, parent
)
355 self
.runtask
= qtutils
.RunTask(parent
=self
)
357 def preview(self
, filename
, line_number
):
358 """Preview a file at the specified line number"""
360 if filename
!= self
.filename
:
361 request
= PreviewTask(filename
, line_number
)
362 self
.runtask
.start(request
, finish
=self
.show_preview
)
364 self
.scroll_to_line(line_number
)
371 def show_preview(self
, task
):
372 """Show the results of the asynchronous file read"""
374 filename
= task
.filename
375 content
= task
.content
376 line_number
= task
.line_number
378 if filename
!= self
.filename
:
379 self
.filename
= filename
380 self
.content
= content
381 self
.set_value(content
)
383 self
.scroll_to_line(line_number
)
385 def scroll_to_line(self
, line_number
):
386 """Scroll to the specified line number"""
388 line_num
= int(line_number
) - 1
392 self
.numbers
.set_highlighted(line_num
)
393 cursor
= self
.textCursor()
394 cursor
.setPosition(0)
395 self
.setTextCursor(cursor
)
396 self
.ensureCursorVisible()
398 cursor
.movePosition(cursor
.Down
, cursor
.MoveAnchor
, line_num
)
399 cursor
.movePosition(cursor
.EndOfLine
, cursor
.KeepAnchor
)
400 self
.setTextCursor(cursor
)
401 self
.ensureCursorVisible()
403 scrollbar
= self
.verticalScrollBar()
404 step
= scrollbar
.pageStep() // 2
405 position
= scrollbar
.sliderPosition() + step
406 scrollbar
.setSliderPosition(position
)
407 self
.ensureCursorVisible()