Skip to content

Commit

Permalink
Merge pull request #21275 from ccordoba12/issue-20940
Browse files Browse the repository at this point in the history
PR: Compute Projects switcher results in a worker to avoid freezes
  • Loading branch information
ccordoba12 authored Aug 26, 2023
2 parents fcfc257 + d51c98d commit c5de2ed
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 287 deletions.
111 changes: 89 additions & 22 deletions spyder/app/tests/test_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import gc
import os
import os.path as osp
from pathlib import Path
import random
import re
import shutil
Expand Down Expand Up @@ -2535,9 +2536,15 @@ def example_def_2():


@flaky(max_runs=3)
def test_switcher_project_files(main_window, qtbot, tmpdir):
"""Test the number of items in the switcher when a project is active."""
# Wait until the window is fully up
def test_switcher_projects_integration(main_window, pytestconfig, qtbot,
tmp_path):
"""Test integration between the Switcher and Projects plugins."""
# Disable pytest stdin capture to make calls to fzf work. Idea taken from:
# https://github.com/pytest-dev/pytest/issues/2189#issuecomment-449512764
capmanager = pytestconfig.pluginmanager.getplugin('capturemanager')
capmanager.suspend_global_capture(in_=True)

# Wait until the console is fully up
shell = main_window.ipyconsole.get_current_shellwidget()
qtbot.waitUntil(
lambda: shell.spyder_kernel_ready and shell._prompt_html is not None,
Expand All @@ -2550,47 +2557,107 @@ def test_switcher_project_files(main_window, qtbot, tmpdir):
editorstack = main_window.editor.get_current_editorstack()

# Create a temp project directory
project_dir = to_text_string(tmpdir.mkdir('test'))
project_dir = tmp_path / 'test-projects-switcher'
project_dir.mkdir()

# Create some empty files in the project dir
n_files_project = 3
for i in range(n_files_project):
fpath = project_dir / f"test_file{i}.py"
fpath.touch()

# Copy binary file from our source tree to the project to check it's not
# displayed in the switcher.
binary_file = Path(LOCATION).parents[1] / 'images' / 'windows_app_icon.ico'
binary_file_copy = project_dir / 'windows.ico'
shutil.copyfile(binary_file, binary_file_copy)

# Create project
with qtbot.waitSignal(projects.sig_project_loaded):
projects.create_project(project_dir)
projects.create_project(str(project_dir))

# Create four empty files in the project dir
for i in range(3):
main_window.editor.new("test_file"+str(i)+".py")
# Check that the switcher has been populated in Projects
qtbot.waitUntil(
lambda: projects.get_widget()._default_switcher_paths != [],
timeout=1000
)

# Assert that the number of items in the switcher is correct
switcher.open_switcher()
n_files_project = len(projects.get_project_filenames())
n_files_open = editorstack.get_stack_count()
assert switcher.count() == n_files_open + n_files_project
switcher.on_close()

# Assert that the number of items in the switcher is correct
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
# Assert only two items have visible sections
switcher.open_switcher()

sections = []
for row in range(switcher.count()):
item = switcher_widget.model.item(row)
if item._section_visible:
sections.append(item.get_section())

assert len(sections) == 2
switcher.on_close()

# Close all files opened in editorstack
main_window.editor.close_all_files()
# Assert searching text in the switcher works as expected
switcher.open_switcher()
switcher.set_search_text('0')
qtbot.wait(500)
assert switcher.count() == 1
switcher.on_close()

# Assert searching for a non-existent file leaves the switcher empty
switcher.open_switcher()
n_files_project = len(projects.get_project_filenames())
n_files_open = editorstack.get_stack_count()
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
switcher.set_search_text('foo')
qtbot.wait(500)
assert switcher.count() == 0
switcher.on_close()

# Select file in the project explorer
# Assert searching for a binary file leaves the switcher empty
switcher.open_switcher()
switcher.set_search_text('windows')
qtbot.wait(500)
assert switcher.count() == 0
switcher.on_close()

# Remove project file and check the switcher is updated
n_files_project -= 1
os.remove(str(project_dir / 'test_file1.py'))
qtbot.wait(500)
switcher.open_switcher()
assert switcher.count() == n_files_open + n_files_project
switcher.on_close()

# Check that a project file opened in the editor is not shown twice in the
# switcher
idx = projects.get_widget().treewidget.get_index(
osp.join(project_dir, 'test_file0.py'))
str(project_dir / 'test_file0.py')
)
projects.get_widget().treewidget.setCurrentIndex(idx)

# Press Enter there
qtbot.keyClick(projects.get_widget().treewidget, Qt.Key_Enter)

switcher.open_switcher()
n_files_project = len(projects.get_project_filenames())
n_files_open = editorstack.get_stack_count()
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
assert switcher.count() == n_files_open + n_files_project - 1
switcher.on_close()

# Check the switcher works without fzf
fzf = projects.get_widget()._fzf
projects.get_widget()._fzf = None
projects.get_widget()._default_switcher_paths = []

switcher.open_switcher()
switcher.set_search_text('0')
qtbot.wait(500)
assert switcher.count() == 1
switcher.on_close()

projects.get_widget()._fzf = fzf

# Resume capturing
capmanager.resume_global_capture()


@flaky(max_runs=3)
@pytest.mark.skipif(sys.platform == 'darwin',
Expand Down
16 changes: 14 additions & 2 deletions spyder/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,20 @@ def create_application():
# The try/except is necessary to run the main window tests on their own.
try:
app.set_font()
except AttributeError:
pass
except AttributeError as error:
if running_under_pytest():
# Set font options to avoid a ton of Qt warnings when running tests
app_family = app.font().family()
app_size = app.font().pointSize()
CONF.set('appearance', 'app_font/family', app_family)
CONF.set('appearance', 'app_font/size', app_size)

from spyder.config.fonts import MEDIUM, MONOSPACE
CONF.set('appearance', 'monospace_app_font/family', MONOSPACE[0])
CONF.set('appearance', 'monospace_app_font/size', MEDIUM)
else:
# Raise in case the error is valid
raise error

# Required for correct icon on GNOME/Wayland:
if hasattr(app, 'setDesktopFileName'):
Expand Down
12 changes: 6 additions & 6 deletions spyder/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ def get_filter(filetypes, ext):
return ''


def get_edit_filetypes():
def get_edit_filetypes(ignore_pygments_extensions=True):
"""Get all file types supported by the Editor"""
# The filter details are not hidden on Windows, so we can't use
# all Pygments extensions on that platform
if os.name == 'nt':
# The filter details are not hidden on Windows, so we can't use all
# Pygments extensions on that platform.
if os.name == 'nt' and ignore_pygments_extensions:
supported_exts = []
else:
try:
Expand Down Expand Up @@ -154,8 +154,8 @@ def get_edit_extensions():
Return extensions associated with the file types
supported by the Editor
"""
edit_filetypes = get_edit_filetypes()
return _get_extensions(edit_filetypes)+['']
edit_filetypes = get_edit_filetypes(ignore_pygments_extensions=False)
return _get_extensions(edit_filetypes) + ['']


#==============================================================================
Expand Down
33 changes: 16 additions & 17 deletions spyder/plugins/editor/utils/switcher_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def setup_switcher(self):
self._switcher.sig_rejected.connect(self.handle_switcher_rejection)
self._switcher.sig_item_changed.connect(
self.handle_switcher_item_change)
self._switcher.sig_search_text_available.connect(
lambda text: self._switcher.setup()
)

def handle_switcher_modes(self, mode):
"""Handle switcher for registered modes."""
Expand All @@ -78,16 +81,9 @@ def create_editor_switcher(self):
_('Start typing the name of an open file'))

editorstack = self._editorstack()

# Since editor open files are inserted at position 0, the
# list needs to be reversed so they're shown in order.
editor_list = editorstack.data.copy()
editor_list.reverse()

paths = [data.filename.lower()
for data in editor_list]
save_statuses = [data.newly_created
for data in editor_list]
paths = [data.filename for data in editor_list]
save_statuses = [data.newly_created for data in editor_list]
short_paths = shorten_paths(paths, save_statuses)

for idx, data in enumerate(editor_list):
Expand All @@ -99,15 +95,18 @@ def create_editor_switcher(self):
if len(paths[idx]) > 75:
path = short_paths[idx]
else:
path = osp.dirname(data.filename.lower())
path = osp.dirname(data.filename)
last_item = (idx + 1 == len(editor_list))
self._switcher.add_item(title=title,
description=path,
icon=icon,
section=self._section,
data=data,
last_item=last_item)
self._switcher.set_current_row(0)

self._switcher.add_item(
title=title,
description=path,
icon=icon,
section=self._section,
data=data,
last_item=last_item,
score=0 # To make these items appear above those from Projects
)

def create_line_switcher(self):
"""Populate switcher with line info."""
Expand Down
51 changes: 31 additions & 20 deletions spyder/plugins/projects/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def on_switcher_available(self):
self._switcher.sig_item_selected.connect(
self._handle_switcher_selection)
self._switcher.sig_search_text_available.connect(
self._handle_switcher_results)
self._handle_switcher_search)

@on_plugin_teardown(plugin=Plugins.Editor)
def on_editor_teardown(self):
Expand Down Expand Up @@ -281,7 +281,7 @@ def on_switcher_teardown(self):
self._switcher.sig_item_selected.disconnect(
self._handle_switcher_selection)
self._switcher.sig_search_text_available.disconnect(
self._handle_switcher_results)
self._handle_switcher_search)
self._switcher = None

def on_close(self, cancelable=False):
Expand Down Expand Up @@ -508,17 +508,11 @@ def _handle_switcher_modes(self, mode):
mode: str
The selected mode (open files "", symbol "@" or line ":").
"""
items = self.get_widget().handle_switcher_modes()
for (title, description, icon, section, path, is_last_item) in items:
self._switcher.add_item(
title=title,
description=description,
icon=icon,
section=section,
data=path,
last_item=is_last_item
)
self._switcher.set_current_row(0)
# Don't compute anything if we're not in files mode
if mode != "":
return

self.get_widget().display_default_switcher_items()

def _handle_switcher_selection(self, item, mode, search_text):
"""
Expand All @@ -540,20 +534,33 @@ def _handle_switcher_selection(self, item, mode, search_text):
self.get_widget().handle_switcher_selection(item, mode, search_text)
self._switcher.hide()

def _handle_switcher_results(self, search_text, items_data):
def _handle_switcher_search(self, search_text):
"""
Handle user typing in switcher to filter results.
Load switcher results when a search text is typed for projects.
Parameters
----------
text: str
The current search text in the switcher dialog box.
items_data: list
List of items shown in the switcher.
"""
items = self.get_widget().handle_switcher_results(search_text,
items_data)
self.get_widget().handle_switcher_search(search_text)

def _display_items_in_switcher(self, items, setup, clear_section):
"""
Display a list of items in the switcher.
Parameters
----------
items: list
Items to display.
setup: bool
Call the switcher's setup after adding the items.
clear_section: bool
Clear Projects section before adding the items.
"""
if clear_section:
self._switcher.remove_section(self.get_widget().get_title())

for (title, description, icon, section, path, is_last_item) in items:
self._switcher.add_item(
title=title,
Expand All @@ -562,5 +569,9 @@ def _handle_switcher_results(self, search_text, items_data):
section=section,
data=path,
last_item=is_last_item,
score=100
score=1e10, # To make the editor results appear first
use_score=False # Results come from fzf in the right order
)

if setup:
self._switcher.setup()
2 changes: 1 addition & 1 deletion spyder/plugins/projects/tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def test_project_explorer_tree_root(projects, tmpdir, qtbot):
# Open the projects.
for ppath in [ppath1, ppath2]:
projects.open_project(path=ppath)
projects.get_widget()._update_explorer(None)
projects.get_widget()._setup_project(ppath)

# Check that the root path of the project explorer tree widget is
# set correctly.
Expand Down
Loading

0 comments on commit c5de2ed

Please sign in to comment.