Skip to content

Commit

Permalink
Merge pull request #2590 from goanpeca/switcher
Browse files Browse the repository at this point in the history
Redesign file switcher (a la Sublime Text)
  • Loading branch information
goanpeca committed Aug 20, 2015
2 parents 46b2b31 + b41e5d5 commit b6532f0
Show file tree
Hide file tree
Showing 9 changed files with 705 additions and 170 deletions.
5 changes: 2 additions & 3 deletions spyderlib/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,9 @@ def is_ubuntu():
# -- In widgets/editor
'editor/inspect current object': 'Ctrl+I',
'editor/go to line': 'Ctrl+L',
'editor/file list management': 'Ctrl+E',
'editor/go to previous file': 'Ctrl+Tab',
'editor/go to next file': 'Ctrl+Shift+Tab',
'_/file switcher': 'Ctrl+P',
# -- In spyder.py
'editor/find text': "Ctrl+F",
'editor/find next': "F3",
Expand All @@ -557,7 +557,6 @@ def is_ubuntu():
'editor/save file': "Ctrl+S",
'editor/save all': "Ctrl+Alt+S",
'editor/save as': 'Ctrl+Shift+S',
'editor/print': "Ctrl+P",
'editor/close all': "Ctrl+Shift+W",
'editor/breakpoint': 'F12',
'editor/conditional breakpoint': 'Shift+F12',
Expand Down Expand Up @@ -742,7 +741,7 @@ def is_ubuntu():
# 2. If you want to *remove* options that are no longer needed in our codebase,
# you need to do a MAJOR update in version, e.g. from 3.0.0 to 4.0.0
# 3. You don't need to touch this value if you're just adding a new option
CONF_VERSION = '20.0.0'
CONF_VERSION = '21.0.0'

# XXX: Previously we had load=(not DEV) here but DEV was set to *False*.
# Check if it *really* needs to be updated or not
Expand Down
2 changes: 0 additions & 2 deletions spyderlib/plugins/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,8 +665,6 @@ def get_plugin_actions(self):
self.print_action = create_action(self, _("&Print..."),
icon=ima.icon('print'), tip=_("Print current file..."),
triggered=self.print_file)
self.register_shortcut(self.print_action, context="Editor",
name="Print")
# Shortcut for close_action is defined in widgets/editor.py
self.close_action = create_action(self, _("&Close"),
icon=ima.icon('fileclose'), tip=_("Close current file"),
Expand Down
3 changes: 2 additions & 1 deletion spyderlib/plugins/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ def __init__(self, parent):
self.shortcuts = []
self.scores = []
self.rich_text = []
self.normal_text = []
self.letters = ''
self.label = QLabel()
self.widths = []
Expand Down Expand Up @@ -545,7 +546,7 @@ def update_search_letters(self, text):
self.letters = text
names = [shortcut.name for shortcut in self.shortcuts]
results = get_search_scores(text, names, template='<b>{0}</b>')
self.rich_text, self.scores = zip(*results)
self.normal_text, self.rich_text, self.scores = zip(*results)
self.reset()

def update_active_row(self):
Expand Down
43 changes: 42 additions & 1 deletion spyderlib/py3compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@

from __future__ import print_function

import sys
import operator
import os
import sys

PY2 = sys.version[0] == '2'
PY3 = sys.version[0] == '3'
Expand Down Expand Up @@ -243,6 +244,46 @@ def qbytearray_to_str(qba):
"""Convert QByteArray object to str in a way compatible with Python 2/3"""
return str(bytes(qba.toHex().data()).decode())

# =============================================================================
# Dict funcs
# =============================================================================
if PY3:
def iterkeys(d, **kw):
return iter(d.keys(**kw))

def itervalues(d, **kw):
return iter(d.values(**kw))

def iteritems(d, **kw):
return iter(d.items(**kw))

def iterlists(d, **kw):
return iter(d.lists(**kw))

viewkeys = operator.methodcaller("keys")

viewvalues = operator.methodcaller("values")

viewitems = operator.methodcaller("items")
else:
def iterkeys(d, **kw):
return d.iterkeys(**kw)

def itervalues(d, **kw):
return d.itervalues(**kw)

def iteritems(d, **kw):
return d.iteritems(**kw)

def iterlists(d, **kw):
return d.iterlists(**kw)

viewkeys = operator.methodcaller("viewkeys")

viewvalues = operator.methodcaller("viewvalues")

viewitems = operator.methodcaller("viewitems")


if __name__ == '__main__':
pass
13 changes: 12 additions & 1 deletion spyderlib/spyder.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,14 @@ def setup(self):
context=Qt.WidgetShortcut)
self.register_shortcut(self.replace_action, "Editor",
"Replace text")

self.file_switcher_action = create_action(self, _('File switcher...'),
icon=ima.icon('filelist'),
tip=_('Fast switch between files'),
triggered=self.call_file_switcher,
context=Qt.ApplicationShortcut)
self.register_shortcut(self.file_switcher_action, "_",
"file switcher")
self.file_menu_actions.append(self.file_switcher_action)
def create_edit_action(text, tr_text, icon):
textseq = text.split(' ')
method_name = textseq[0].lower()+"".join(textseq[1:])
Expand Down Expand Up @@ -2459,6 +2466,10 @@ def global_callback(self):
if isinstance(widget, TextEditBaseWidget):
getattr(widget, callback)()

def call_file_switcher(self):
if self.editor.editorstacks:
self.editor.get_current_editorstack().open_fileswitcher_dlg()

def redirect_internalshell_stdio(self, state):
if state:
self.console.shell.interpreter.redirect_stds()
Expand Down
46 changes: 30 additions & 16 deletions spyderlib/utils/stringmatching.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@


NOT_FOUND_SCORE = -1
NO_SCORE = 0


def get_search_regex(query, ignore_case=True):
Expand Down Expand Up @@ -80,20 +81,22 @@ def get_search_score(query, choice, ignore_case=True, apply_regex=True,
- Letters in one word and no spaces with exact match.
Example: 'up' in 'up stroke'
- Letters in one word and no spaces with partial match.
Example: 'up' in 'upstream stroke'
Example: 'up' in 'upstream stroke'
- Letters in one word but with skip letters.
Example: 'cls' in 'close up'
- Letters in two or more words.
Example: 'cls' in 'car lost'
Example: 'cls' in 'close up'
- Letters in two or more words
Example: 'cls' in 'car lost'
"""
result = (choice, NOT_FOUND_SCORE)

original_choice = choice
result = (original_choice, NOT_FOUND_SCORE)

# Handle empty string case
if not query:
return result

if ignore_case:
query = query.lower()
choice = choice.lower()

if apply_regex:
pattern = get_search_regex(query, ignore_case=ignore_case)
Expand All @@ -109,9 +112,14 @@ def get_search_score(query, choice, ignore_case=True, apply_regex=True,
partial_words = [query in word for word in choice.split(u' ')]

if any(exact_words) or any(partial_words):
score += choice.find(query)
pos_start = choice.find(query)
pos_end = pos_start + len(query)
score += pos_start
text = choice.replace(query, sep*len(query), 1)
enriched_text = choice.replace(query, template.format(query), 1)

enriched_text = original_choice[:pos_start] +\
template.format(original_choice[pos_start:pos_end]) +\
original_choice[pos_end:]

if any(exact_words):
# Check if the query words exists in a word with exact match
Expand All @@ -121,17 +129,18 @@ def get_search_score(query, choice, ignore_case=True, apply_regex=True,
score += 100
else:
# Check letter by letter
text = [l for l in choice]
text = [l for l in original_choice]
if ignore_case:
temp_text = text[:]
temp_text = [l.lower() for l in original_choice]
else:
temp_text = [l.lower() for l in choice]
temp_text = text[:]

# Give points to start of string
score += temp_text.index(query[0])

# Find the query letters and replace them by `sep`, also apply
# template as needed for enricching the letters in the text
enriched_text = temp_text[:]
enriched_text = text[:]
for char in query:
if char != u'' and char in temp_text:
index = temp_text.index(char)
Expand Down Expand Up @@ -164,13 +173,13 @@ def get_search_score(query, choice, ignore_case=True, apply_regex=True,
score += pat.count(u' ')*10000
score += pat.count(let)*100

return enriched_text, score
return original_choice, enriched_text, score


def get_search_scores(query, choices, ignore_case=True, template='{}',
valid_only=False, sort=False):
"""Search for query inside choices and return a list of tuples.
Returns a list of tuples of text with the enriched text (if a template is
provided) and a score for the match. Lower scores imply a better match.
Expand All @@ -193,16 +202,21 @@ def get_search_scores(query, choices, ignore_case=True, template='{}',
List of tuples where the first item is the text (enriched if a
template was used) and a search score. Lower scores means better match.
"""
# First remove spaces from query
query = query.replace(' ', '')
pattern = get_search_regex(query, ignore_case)
results = []

for choice in choices:
r = re.search(pattern, choice)
if r:
if query and r:
result = get_search_score(query, choice, ignore_case=ignore_case,
apply_regex=False, template=template)
else:
result = (choice, NOT_FOUND_SCORE)
if query:
result = (choice, choice, NOT_FOUND_SCORE)
else:
result = (choice, choice, NO_SCORE)

if valid_only:
if result[-1] != NOT_FOUND_SCORE:
Expand Down
Loading

0 comments on commit b6532f0

Please sign in to comment.