Repository: spyder-ide/spyder

Issue:
I'm not able to search by context in the shortcuts table
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [ ] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [ ] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [ ] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ ] Tried basic troubleshooting (if a bug/error)
    * [ ] Restarted Spyder
    * [ ] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
The find widget inside the shortcuts table is not searching by context, just by the name of the shortcut


### What steps reproduce the problem?

1. Open the preference menu
2. Go to Keyboard shortcuts 
3. Try to find all the shortcuts in a given context

### What is the expected output? What do you see instead?
See the shortcuts of the typed context





Discussion:

Resolving patch:
diff --git a/spyder/preferences/shortcuts.py b/spyder/preferences/shortcuts.py
--- a/spyder/preferences/shortcuts.py
+++ b/spyder/preferences/shortcuts.py
@@ -13,7 +13,8 @@
 # Third party imports
 from qtpy import PYQT5
 from qtpy.compat import from_qvariant, to_qvariant
-from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Slot, QEvent
+from qtpy.QtCore import (QAbstractTableModel, QModelIndex, Qt, Slot, QEvent,
+                         QSortFilterProxyModel)
 from qtpy.QtGui import QKeySequence, QIcon
 from qtpy.QtWidgets import (QAbstractItemView, QApplication, QDialog,
                             QGridLayout, QHBoxLayout, QLabel,
@@ -26,9 +27,8 @@
 from spyder.preferences.configdialog import GeneralConfigPage
 from spyder.utils import icon_manager as ima
 from spyder.utils.qthelpers import get_std_icon, create_toolbutton
-from spyder.utils.stringmatching import get_search_scores
-from spyder.widgets.helperwidgets import (CustomSortFilterProxy,
-                                          FinderLineEdit, HelperToolButton,
+from spyder.utils.stringmatching import get_search_scores, get_search_regex
+from spyder.widgets.helperwidgets import (FinderLineEdit, HelperToolButton,
                                           HTMLDelegate, VALID_FINDER_CHARS)
 
 
@@ -494,6 +494,7 @@ def __init__(self, parent, text_color=None, text_color_highlight=None):
         self.scores = []
         self.rich_text = []
         self.normal_text = []
+        self.context_rich_text = []
         self.letters = ''
         self.label = QLabel()
         self.widths = []
@@ -539,15 +540,20 @@ def data(self, index, role=Qt.DisplayRole):
         column = index.column()
 
         if role == Qt.DisplayRole:
+            color = self.text_color
+            if self._parent == QApplication.focusWidget():
+                if self.current_index().row() == row:
+                    color = self.text_color_highlight
+                else:
+                    color = self.text_color
             if column == CONTEXT:
-                return to_qvariant(shortcut.context)
+                if len(self.context_rich_text) > 0:
+                    text = self.context_rich_text[row]
+                else:
+                    text = shortcut.context
+                text = '<p style="color:{0}">{1}</p>'.format(color, text)
+                return to_qvariant(text)
             elif column == NAME:
-                color = self.text_color
-                if self._parent == QApplication.focusWidget():
-                    if self.current_index().row() == row:
-                        color = self.text_color_highlight
-                    else:
-                        color = self.text_color
                 text = self.rich_text[row]
                 text = '<p style="color:{0}">{1}</p>'.format(color, text)
                 return to_qvariant(text)
@@ -606,9 +612,15 @@ def setData(self, index, value, role=Qt.EditRole):
     def update_search_letters(self, text):
         """Update search letters with text input in search box."""
         self.letters = text
+        contexts = [shortcut.context for shortcut in self.shortcuts]
         names = [shortcut.name for shortcut in self.shortcuts]
+        context_results = get_search_scores(
+            text, contexts, template='<b>{0}</b>')
         results = get_search_scores(text, names, template='<b>{0}</b>')
+        __, self.context_rich_text, context_scores = (
+            zip(*context_results))
         self.normal_text, self.rich_text, self.scores = zip(*results)
+        self.scores = [x + y for x, y in zip(self.scores, context_scores)]
         self.reset()
 
     def update_active_row(self):
@@ -636,17 +648,19 @@ def __init__(self,
                                     self,
                                     text_color=text_color,
                                     text_color_highlight=text_color_highlight)
-        self.proxy_model = CustomSortFilterProxy(self)
+        self.proxy_model = ShortcutsSortFilterProxy(self)
         self.last_regex = ''
 
         self.proxy_model.setSourceModel(self.source_model)
         self.proxy_model.setDynamicSortFilter(True)
-        self.proxy_model.setFilterKeyColumn(NAME)
+        self.proxy_model.setFilterByColumn(CONTEXT)
+        self.proxy_model.setFilterByColumn(NAME)
         self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
         self.setModel(self.proxy_model)
 
         self.hideColumn(SEARCH_SCORE)
         self.setItemDelegateForColumn(NAME, HTMLDelegate(self, margin=9))
+        self.setItemDelegateForColumn(CONTEXT, HTMLDelegate(self, margin=9))
         self.setSelectionBehavior(QAbstractItemView.SelectRows)
         self.setSelectionMode(QAbstractItemView.SingleSelection)
         self.setSortingEnabled(True)
@@ -893,6 +907,65 @@ def apply_settings(self, options):
         self.main.apply_shortcuts()
 
 
+class ShortcutsSortFilterProxy(QSortFilterProxyModel):
+    """Custom proxy for supporting shortcuts multifiltering."""
+
+    def __init__(self, parent=None):
+        """Initialize the multiple sort filter proxy."""
+        super().__init__(parent)
+        self._parent = parent
+        self.pattern = re.compile(r'')
+        self.filters = {}
+
+    def setFilterByColumn(self, column):
+        """Set regular expression in the given column."""
+        self.filters[column] = self.pattern
+        self.invalidateFilter()
+
+    def set_filter(self, text):
+        """Set regular expression for filter."""
+        for key, __ in self.filters.items():
+            self.pattern = get_search_regex(text)
+            if self.pattern and text:
+                self._parent.setSortingEnabled(False)
+            else:
+                self._parent.setSortingEnabled(True)
+            self.filters[key] = self.pattern
+            self.invalidateFilter()
+
+    def clearFilter(self, column):
+        """Clear the filter of the given column."""
+        self.filters.pop(column)
+        self.invalidateFilter()
+
+    def clearFilters(self):
+        """Clear all the filters."""
+        self.filters = {}
+        self.invalidateFilter()
+
+    def filterAcceptsRow(self, row_num, parent):
+        """Qt override.
+
+        Reimplemented to allow filtering in multiple columns.
+        """
+        results = []
+        for key, regex in self.filters.items():
+            model = self.sourceModel()
+            idx = model.index(row_num, key, parent)
+            if idx.isValid():
+                name = model.row(row_num).name
+                r_name = re.search(regex, name)
+                if r_name is None:
+                    r_name = ''
+                context = model.row(row_num).context
+                r_context = re.search(regex, context)
+                if r_context is None:
+                    r_context = ''
+                results.append(r_name)
+                results.append(r_context)
+        return any(results)
+
+
 def load_shortcuts(shortcut_table):
     """
     Load shortcuts from CONF for testing.


Test patch:
diff --git a/spyder/preferences/tests/test_shorcuts.py b/spyder/preferences/tests/test_shorcuts.py
--- a/spyder/preferences/tests/test_shorcuts.py
+++ b/spyder/preferences/tests/test_shorcuts.py
@@ -114,6 +114,25 @@ def test_shortcuts_filtering(shortcut_table):
     assert shortcut_table.model().rowCount() == row_count
 
 
+def test_shortcut_filtering_context(shortcut_table):
+    """Test multifiltering by context and name in the table."""
+    # Verify that the model is filtering by two columns
+    assert len(shortcut_table.model().filters) == 2
+    # Filter by "console"
+    shortcut_table.finder = FilterTextMock('console')
+    shortcut_table.set_regex()
+    # Verify the number of entries after the regex are 7
+    # If a new shortcut is added to console, this needs to be changed
+    assert shortcut_table.model().rowCount() == 7
+
+    # Filter by "pylint"
+    shortcut_table.finder = FilterTextMock('pylint')
+    shortcut_table.set_regex()
+    # Verify the number of entries after the regex is 1
+    # If a new shortcut is added to pylint, this needs to be changed
+    assert shortcut_table.model().rowCount() == 1
+
+
 # ---- Tests ShortcutEditor
 def test_clear_shortcut(create_shortcut_editor, qtbot):
     """