Ejemplo n.º 1
0
def test_truncate_result_with_different_input(findinfiles, qtbot, line_input):
    """
    Issue: 6218 - checking if truncate_result raise UnicodeDecodeError
    """

    # with
    slice_start = 1
    slice_end = 2

    line_input_expected = line_input

    expected_result = (
        f'<span style="color:{QStylePalette.COLOR_TEXT_1}">'
        f'{line_input_expected[:slice_start]}'
        f'<span style="background-color:{SpyderPalette.COLOR_OCCURRENCE_4}">'
        f'{line_input_expected[slice_start:slice_end]}</span>'
        f'{line_input_expected[slice_end:]}</span>'
    )

    # when
    thread = SearchThread(None, '', text_color=QStylePalette.COLOR_TEXT_1)
    truncated_line = thread.truncate_result(line_input, slice_start,
                                            slice_end)
    # then
    assert truncated_line == expected_result
Ejemplo n.º 2
0
    def start(self):
        """Start find thread."""
        options = self._get_options()
        if options is None:
            return

        self._stop_and_reset_thread(ignore_results=True)
        search_text = self.search_text_edit.currentText()

        # Update and set options
        self._update_options()

        # Set path in result_browser
        self.result_browser.set_path(options[0])

        # Start
        self.running = True
        self.start_spinner()
        self.search_thread = SearchThread(self, search_text, self.text_color)
        self.search_thread.sig_finished.connect(self._handle_search_complete)
        self.search_thread.sig_file_match.connect(
            self.result_browser.append_file_result)
        self.search_thread.sig_line_match.connect(
            self.result_browser.append_result)
        self.result_browser.clear_title(search_text)
        self.search_thread.initialize(*self._get_options())
        self.search_thread.start()
        self.update_actions()
Ejemplo n.º 3
0
class FindInFilesWidget(PluginMainWidget):
    """
    Find in files main widget.
    """

    ENABLE_SPINNER = True
    REGEX_INVALID = f"background-color:{SpyderPalette.COLOR_ERROR_2};"
    REGEX_ERROR = _("Regular expression error")

    # Signals
    sig_edit_goto_requested = Signal(str, int, str, int, int)
    """
    This signal will request to open a file in a given row and column
    using a code editor.

    Parameters
    ----------
    path: str
        Path to file.
    row: int
        Cursor starting row position.
    word: str
        Word to select on given row.
    start_column: int
        Starting column of found word.
    end_column:
        Ending column of found word.
    """

    sig_finished = Signal()
    """
    This signal is emitted to inform the search process has finished.
    """

    sig_max_results_reached = Signal()
    """
    This signal is emitted to inform the search process has finished due
    to reaching the maximum number of results.
    """
    def __init__(self, name=None, plugin=None, parent=None):
        super().__init__(name, plugin, parent=parent)
        self.set_conf('text_color', MAIN_TEXT_COLOR)
        self.set_conf('hist_limit', MAX_PATH_HISTORY)

        # Attributes
        self.text_color = self.get_conf('text_color')
        self.supported_encodings = self.get_conf('supported_encodings')
        self.search_thread = None
        self.running = False
        self.more_options_action = None
        self.extras_toolbar = None

        search_text = self.get_conf('search_text', '')
        path_history = self.get_conf('path_history', [])
        exclude = self.get_conf('exclude')

        if not isinstance(search_text, (list, tuple)):
            search_text = [search_text]

        if not isinstance(exclude, (list, tuple)):
            exclude = [exclude]

        if not isinstance(path_history, (list, tuple)):
            path_history = [path_history]

        # Widgets
        self.search_text_edit = PatternComboBox(
            self,
            search_text,
            id_=FindInFilesWidgetToolbarItems.SearchPatternCombo)

        self.search_text_edit.lineEdit().setPlaceholderText(
            _('Write text to search'))

        self.search_in_label = QLabel(_('Search in:'))
        self.search_in_label.ID = FindInFilesWidgetToolbarItems.SearchInLabel

        self.exclude_label = QLabel(_('Exclude:'))
        self.exclude_label.ID = FindInFilesWidgetToolbarItems.ExcludeLabel

        self.path_selection_combo = SearchInComboBox(
            path_history,
            self,
            id_=FindInFilesWidgetToolbarItems.SearchInCombo)

        self.exclude_pattern_edit = PatternComboBox(
            self,
            exclude,
            _("Exclude pattern"),
            id_=FindInFilesWidgetToolbarItems.ExcludePatternCombo)

        self.result_browser = ResultsBrowser(
            self,
            text_color=self.text_color,
            max_results=self.get_conf('max_results'),
        )

        # Setup
        self.exclude_label.setBuddy(self.exclude_pattern_edit)
        exclude_idx = self.get_conf('exclude_index', None)
        if (exclude_idx is not None and exclude_idx >= 0
                and exclude_idx < self.exclude_pattern_edit.count()):
            self.exclude_pattern_edit.setCurrentIndex(exclude_idx)

        search_in_index = self.get_conf('search_in_index', None)
        self.path_selection_combo.set_current_searchpath_index(search_in_index)

        # Layout
        layout = QHBoxLayout()
        layout.addWidget(self.result_browser)
        self.setLayout(layout)

        # Signals
        self.path_selection_combo.sig_redirect_stdio_requested.connect(
            self.sig_redirect_stdio_requested)
        self.search_text_edit.valid.connect(lambda valid: self.find())
        self.exclude_pattern_edit.valid.connect(lambda valid: self.find())
        self.result_browser.sig_edit_goto_requested.connect(
            self.sig_edit_goto_requested)
        self.result_browser.sig_max_results_reached.connect(
            self.sig_max_results_reached)
        self.result_browser.sig_max_results_reached.connect(
            self._stop_and_reset_thread)
        self.search_text_edit.sig_resized.connect(self._update_size)

    # --- PluginMainWidget API
    # ------------------------------------------------------------------------
    def get_title(self):
        return _("Find")

    def get_focus_widget(self):
        return self.search_text_edit

    def setup(self):
        self.search_regexp_action = self.create_action(
            FindInFilesWidgetActions.ToggleSearchRegex,
            text=_('Regular expression'),
            tip=_('Use regular expressions'),
            icon=self.create_icon('regex'),
            toggled=True,
            initial=self.get_conf('search_text_regexp'),
            option='search_text_regexp')
        self.case_action = self.create_action(
            FindInFilesWidgetActions.ToggleExcludeCase,
            text=_("Case sensitive"),
            tip=_("Case sensitive search"),
            icon=self.create_icon("format_letter_case"),
            toggled=True,
            initial=self.get_conf('case_sensitive'),
            option='case_sensitive')
        self.find_action = self.create_action(
            FindInFilesWidgetActions.Find,
            text=_("&Find in files"),
            tip=_("Search text"),
            icon=self.create_icon('find'),
            triggered=self.find,
            register_shortcut=False,
        )
        self.exclude_regexp_action = self.create_action(
            FindInFilesWidgetActions.ToggleExcludeRegex,
            text=_('Regular expression'),
            tip=_('Use regular expressions'),
            icon=self.create_icon('regex'),
            toggled=True,
            initial=self.get_conf('exclude_regexp'),
            option='exclude_regexp')
        self.exclude_case_action = self.create_action(
            FindInFilesWidgetActions.ToggleCase,
            text=_("Exclude case sensitive"),
            tip=_("Exclude case sensitive"),
            icon=self.create_icon("format_letter_case"),
            toggled=True,
            initial=self.get_conf('exclude_case_sensitive'),
            option='exclude_case_sensitive')
        self.more_options_action = self.create_action(
            FindInFilesWidgetActions.ToggleMoreOptions,
            text=_('Show advanced options'),
            tip=_('Show advanced options'),
            icon=self.create_icon("options_more"),
            toggled=True,
            initial=self.get_conf('more_options'),
            option='more_options')
        self.set_max_results_action = self.create_action(
            FindInFilesWidgetActions.MaxResults,
            text=_('Set maximum number of results'),
            tip=_('Set maximum number of results'),
            triggered=lambda x=None: self.set_max_results(),
        )

        # Toolbar
        toolbar = self.get_main_toolbar()
        for item in [
                self.search_text_edit, self.find_action,
                self.search_regexp_action, self.case_action,
                self.more_options_action
        ]:
            self.add_item_to_toolbar(
                item,
                toolbar=toolbar,
                section=FindInFilesWidgetMainToolbarSections.Main,
            )

        # Exclude Toolbar
        self.extras_toolbar = self.create_toolbar(
            FindInFilesWidgetToolbars.Exclude)

        stretcher = self.create_stretcher()
        stretcher.ID = FindInFilesWidgetToolbarItems.Stretcher1
        for item in [
                self.exclude_label, self.exclude_pattern_edit,
                self.exclude_regexp_action, stretcher
        ]:
            self.add_item_to_toolbar(
                item,
                toolbar=self.extras_toolbar,
                section=FindInFilesWidgetExcludeToolbarSections.Main,
            )

        # Location toolbar
        location_toolbar = self.create_toolbar(
            FindInFilesWidgetToolbars.Location)
        for item in [self.search_in_label, self.path_selection_combo]:
            self.add_item_to_toolbar(
                item,
                toolbar=location_toolbar,
                section=FindInFilesWidgetLocationToolbarSections.Main,
            )

        menu = self.get_options_menu()
        self.add_item_to_menu(
            self.set_max_results_action,
            menu=menu,
        )

    def update_actions(self):
        self.find_action.setIcon(
            self.create_icon('stop' if self.running else 'find'))

        if self.extras_toolbar and self.more_options_action:
            self.extras_toolbar.setVisible(
                self.more_options_action.isChecked())

    @on_conf_change(option='more_options')
    def on_more_options_update(self, value):
        self.exclude_pattern_edit.setMinimumWidth(
            self.search_text_edit.width())

        if value:
            icon = self.create_icon('options_less')
            tip = _('Hide advanced options')
        else:
            icon = self.create_icon('options_more')
            tip = _('Show advanced options')

        if self.extras_toolbar:
            self.extras_toolbar.setVisible(value)

        if self.more_options_action:
            self.more_options_action.setIcon(icon)
            self.more_options_action.setToolTip(tip)

    @on_conf_change(option='max_results')
    def on_max_results_update(self, value):
        self.result_browser.set_max_results(value)

    # --- Private API
    # ------------------------------------------------------------------------
    def _update_size(self, size, old_size):
        self.exclude_pattern_edit.setMinimumWidth(size.width())

    def _get_options(self):
        """
        Get search options.
        """
        text_re = self.search_regexp_action.isChecked()
        exclude_re = self.exclude_regexp_action.isChecked()
        case_sensitive = self.case_action.isChecked()

        # Clear fields
        self.search_text_edit.lineEdit().setStyleSheet("")
        self.exclude_pattern_edit.lineEdit().setStyleSheet("")
        self.exclude_pattern_edit.setToolTip("")
        self.search_text_edit.setToolTip("")

        utext = str(self.search_text_edit.currentText())
        if not utext:
            return

        try:
            texts = [(utext.encode('utf-8'), 'utf-8')]
        except UnicodeEncodeError:
            texts = []
            for enc in self.supported_encodings:
                try:
                    texts.append((utext.encode(enc), enc))
                except UnicodeDecodeError:
                    pass

        exclude = str(self.exclude_pattern_edit.currentText())

        if not case_sensitive:
            texts = [(text[0].lower(), text[1]) for text in texts]

        file_search = self.path_selection_combo.is_file_search()
        path = self.path_selection_combo.get_current_searchpath()

        if not exclude_re:
            items = [
                fnmatch.translate(item.strip()) for item in exclude.split(",")
                if item.strip() != ''
            ]
            exclude = '|'.join(items)

        # Validate exclude regular expression
        if exclude:
            error_msg = regexp_error_msg(exclude)
            if error_msg:
                exclude_edit = self.exclude_pattern_edit.lineEdit()
                exclude_edit.setStyleSheet(self.REGEX_INVALID)
                tooltip = self.REGEX_ERROR + ': ' + str(error_msg)
                self.exclude_pattern_edit.setToolTip(tooltip)
                return None
            else:
                exclude = re.compile(exclude)

        # Validate text regular expression
        if text_re:
            error_msg = regexp_error_msg(texts[0][0])
            if error_msg:
                self.search_text_edit.lineEdit().setStyleSheet(
                    self.REGEX_INVALID)
                tooltip = self.REGEX_ERROR + ': ' + str(error_msg)
                self.search_text_edit.setToolTip(tooltip)
                return None
            else:
                texts = [(re.compile(x[0]), x[1]) for x in texts]

        return (path, file_search, exclude, texts, text_re, case_sensitive)

    def _update_options(self):
        """
        Extract search options from widgets and set the corresponding option.
        """
        hist_limit = self.get_conf('hist_limit')
        search_texts = [
            str(self.search_text_edit.itemText(index))
            for index in range(self.search_text_edit.count())
        ]
        excludes = [
            str(self.exclude_pattern_edit.itemText(index))
            for index in range(self.exclude_pattern_edit.count())
        ]
        path_history = self.path_selection_combo.get_external_paths()

        self.set_conf('path_history', path_history)
        self.set_conf('search_text', search_texts[:hist_limit])
        self.set_conf('exclude', excludes[:hist_limit])
        self.set_conf('path_history', path_history[-hist_limit:])
        self.set_conf('exclude_index',
                      self.exclude_pattern_edit.currentIndex())
        self.set_conf('search_in_index',
                      self.path_selection_combo.currentIndex())

    def _handle_search_complete(self, completed):
        """
        Current search thread has finished.
        """
        self.result_browser.set_sorting(ON)
        self.result_browser.expandAll()
        if self.search_thread is None:
            return

        self.sig_finished.emit()
        found = self.search_thread.get_results()
        self._stop_and_reset_thread()
        if found is not None:
            self.result_browser.show()

        self.stop_spinner()
        self.update_actions()

    def _stop_and_reset_thread(self, ignore_results=False):
        """Stop current search thread and clean-up."""
        if self.search_thread is not None:
            if self.search_thread.isRunning():
                if ignore_results:
                    self.search_thread.sig_finished.disconnect(
                        self.search_complete)
                self.search_thread.stop()
                self.search_thread.wait()

            self.search_thread.setParent(None)
            self.search_thread = None

        self.running = False
        self.stop_spinner()
        self.update_actions()

    # --- Public API
    # ------------------------------------------------------------------------
    @property
    def path(self):
        """Return the current path."""
        return self.path_selection_combo.path

    @property
    def project_path(self):
        """Return the current project path."""
        return self.path_selection_combo.project_path

    @property
    def file_path(self):
        """Return the current file path."""
        return self.path_selection_combo.file_path

    def set_directory(self, directory):
        """
        Set directory as current path.

        Parameters
        ----------
        directory: str
            Directory path string.
        """
        self.path_selection_combo.path = osp.abspath(directory)

    def set_project_path(self, path):
        """
        Set path as current project path.

        Parameters
        ----------
        path: str
            Project path string.
        """
        self.path_selection_combo.set_project_path(path)

    def disable_project_search(self):
        """Disable project search path in combobox."""
        self.path_selection_combo.set_project_path(None)

    def set_file_path(self, path):
        """
        Set path as current file path.

        Parameters
        ----------
        path: str
            File path string.
        """
        self.path_selection_combo.file_path = path

    def set_search_text(self, text):
        """
        Set current search text.

        Parameters
        ----------
        text: str
            Search string.

        Notes
        -----
        If `text` is empty, focus will be given to the search lineedit and no
        search will be performed.
        """
        if text:
            self.search_text_edit.add_text(text)
            self.search_text_edit.lineEdit().selectAll()

        self.search_text_edit.setFocus()

    def find(self):
        """
        Start/stop find action.

        Notes
        -----
        If there is no search running, this will start the search. If there is
        a search running, this will stop it.
        """
        if self.running:
            self.stop()
        else:
            self.start()

    def stop(self):
        """Stop find thread."""
        self._stop_and_reset_thread()

    def start(self):
        """Start find thread."""
        options = self._get_options()
        if options is None:
            return

        self._stop_and_reset_thread(ignore_results=True)
        search_text = self.search_text_edit.currentText()

        # Update and set options
        self._update_options()

        # Set path in result_browser
        self.result_browser.set_path(options[0])

        # Start
        self.running = True
        self.start_spinner()
        self.search_thread = SearchThread(self, search_text, self.text_color)
        self.search_thread.sig_finished.connect(self._handle_search_complete)
        self.search_thread.sig_file_match.connect(
            self.result_browser.append_file_result)
        self.search_thread.sig_line_match.connect(
            self.result_browser.append_result)
        self.result_browser.clear_title(search_text)
        self.search_thread.initialize(*self._get_options())
        self.search_thread.start()
        self.update_actions()

    def add_external_path(self, path):
        """
        Parameters
        ----------
        path: str
            Path to add to combobox.
        """
        self.path_selection_combo.add_external_path(path)

    def set_max_results(self, value=None):
        """
        Set maximum amount of results to add to the result browser.

        Parameters
        ----------
        value: int, optional
            Number of results. If None an input dialog will be used.
            Default is None.
        """
        if value is None:
            # Create dialog
            dialog = QInputDialog(self)

            # Set dialog properties
            dialog.setModal(False)
            dialog.setWindowTitle(_('Max results'))
            dialog.setLabelText(_('Set maximum number of results: '))
            dialog.setInputMode(QInputDialog.IntInput)
            dialog.setIntRange(1, 10000)
            dialog.setIntStep(1)
            dialog.setIntValue(self.get_conf('max_results'))

            # Connect slot
            dialog.intValueSelected.connect(
                lambda value: self.set_conf('max_results', value))

            dialog.show()
        else:
            self.set_conf('max_results', value)