Skip to content

Commit c5de2ed

Browse files
authored
Merge pull request #21275 from ccordoba12/issue-20940
PR: Compute Projects switcher results in a worker to avoid freezes
2 parents fcfc257 + d51c98d commit c5de2ed

File tree

12 files changed

+475
-287
lines changed

12 files changed

+475
-287
lines changed

spyder/app/tests/test_mainwindow.py

+89-22
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import gc
1515
import os
1616
import os.path as osp
17+
from pathlib import Path
1718
import random
1819
import re
1920
import shutil
@@ -2535,9 +2536,15 @@ def example_def_2():
25352536

25362537

25372538
@flaky(max_runs=3)
2538-
def test_switcher_project_files(main_window, qtbot, tmpdir):
2539-
"""Test the number of items in the switcher when a project is active."""
2540-
# Wait until the window is fully up
2539+
def test_switcher_projects_integration(main_window, pytestconfig, qtbot,
2540+
tmp_path):
2541+
"""Test integration between the Switcher and Projects plugins."""
2542+
# Disable pytest stdin capture to make calls to fzf work. Idea taken from:
2543+
# https://github.com/pytest-dev/pytest/issues/2189#issuecomment-449512764
2544+
capmanager = pytestconfig.pluginmanager.getplugin('capturemanager')
2545+
capmanager.suspend_global_capture(in_=True)
2546+
2547+
# Wait until the console is fully up
25412548
shell = main_window.ipyconsole.get_current_shellwidget()
25422549
qtbot.waitUntil(
25432550
lambda: shell.spyder_kernel_ready and shell._prompt_html is not None,
@@ -2550,47 +2557,107 @@ def test_switcher_project_files(main_window, qtbot, tmpdir):
25502557
editorstack = main_window.editor.get_current_editorstack()
25512558

25522559
# Create a temp project directory
2553-
project_dir = to_text_string(tmpdir.mkdir('test'))
2560+
project_dir = tmp_path / 'test-projects-switcher'
2561+
project_dir.mkdir()
2562+
2563+
# Create some empty files in the project dir
2564+
n_files_project = 3
2565+
for i in range(n_files_project):
2566+
fpath = project_dir / f"test_file{i}.py"
2567+
fpath.touch()
2568+
2569+
# Copy binary file from our source tree to the project to check it's not
2570+
# displayed in the switcher.
2571+
binary_file = Path(LOCATION).parents[1] / 'images' / 'windows_app_icon.ico'
2572+
binary_file_copy = project_dir / 'windows.ico'
2573+
shutil.copyfile(binary_file, binary_file_copy)
25542574

25552575
# Create project
25562576
with qtbot.waitSignal(projects.sig_project_loaded):
2557-
projects.create_project(project_dir)
2577+
projects.create_project(str(project_dir))
25582578

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

2585+
# Assert that the number of items in the switcher is correct
25632586
switcher.open_switcher()
2564-
n_files_project = len(projects.get_project_filenames())
25652587
n_files_open = editorstack.get_stack_count()
2588+
assert switcher.count() == n_files_open + n_files_project
2589+
switcher.on_close()
25662590

2567-
# Assert that the number of items in the switcher is correct
2568-
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
2591+
# Assert only two items have visible sections
2592+
switcher.open_switcher()
2593+
2594+
sections = []
2595+
for row in range(switcher.count()):
2596+
item = switcher_widget.model.item(row)
2597+
if item._section_visible:
2598+
sections.append(item.get_section())
2599+
2600+
assert len(sections) == 2
25692601
switcher.on_close()
25702602

2571-
# Close all files opened in editorstack
2572-
main_window.editor.close_all_files()
2603+
# Assert searching text in the switcher works as expected
2604+
switcher.open_switcher()
2605+
switcher.set_search_text('0')
2606+
qtbot.wait(500)
2607+
assert switcher.count() == 1
2608+
switcher.on_close()
25732609

2610+
# Assert searching for a non-existent file leaves the switcher empty
25742611
switcher.open_switcher()
2575-
n_files_project = len(projects.get_project_filenames())
2576-
n_files_open = editorstack.get_stack_count()
2577-
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
2612+
switcher.set_search_text('foo')
2613+
qtbot.wait(500)
2614+
assert switcher.count() == 0
25782615
switcher.on_close()
25792616

2580-
# Select file in the project explorer
2617+
# Assert searching for a binary file leaves the switcher empty
2618+
switcher.open_switcher()
2619+
switcher.set_search_text('windows')
2620+
qtbot.wait(500)
2621+
assert switcher.count() == 0
2622+
switcher.on_close()
2623+
2624+
# Remove project file and check the switcher is updated
2625+
n_files_project -= 1
2626+
os.remove(str(project_dir / 'test_file1.py'))
2627+
qtbot.wait(500)
2628+
switcher.open_switcher()
2629+
assert switcher.count() == n_files_open + n_files_project
2630+
switcher.on_close()
2631+
2632+
# Check that a project file opened in the editor is not shown twice in the
2633+
# switcher
25812634
idx = projects.get_widget().treewidget.get_index(
2582-
osp.join(project_dir, 'test_file0.py'))
2635+
str(project_dir / 'test_file0.py')
2636+
)
25832637
projects.get_widget().treewidget.setCurrentIndex(idx)
2584-
2585-
# Press Enter there
25862638
qtbot.keyClick(projects.get_widget().treewidget, Qt.Key_Enter)
25872639

25882640
switcher.open_switcher()
2589-
n_files_project = len(projects.get_project_filenames())
25902641
n_files_open = editorstack.get_stack_count()
2591-
assert switcher_widget.model.rowCount() == n_files_open + n_files_project
2642+
assert switcher.count() == n_files_open + n_files_project - 1
25922643
switcher.on_close()
25932644

2645+
# Check the switcher works without fzf
2646+
fzf = projects.get_widget()._fzf
2647+
projects.get_widget()._fzf = None
2648+
projects.get_widget()._default_switcher_paths = []
2649+
2650+
switcher.open_switcher()
2651+
switcher.set_search_text('0')
2652+
qtbot.wait(500)
2653+
assert switcher.count() == 1
2654+
switcher.on_close()
2655+
2656+
projects.get_widget()._fzf = fzf
2657+
2658+
# Resume capturing
2659+
capmanager.resume_global_capture()
2660+
25942661

25952662
@flaky(max_runs=3)
25962663
@pytest.mark.skipif(sys.platform == 'darwin',

spyder/app/utils.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,20 @@ def create_application():
275275
# The try/except is necessary to run the main window tests on their own.
276276
try:
277277
app.set_font()
278-
except AttributeError:
279-
pass
278+
except AttributeError as error:
279+
if running_under_pytest():
280+
# Set font options to avoid a ton of Qt warnings when running tests
281+
app_family = app.font().family()
282+
app_size = app.font().pointSize()
283+
CONF.set('appearance', 'app_font/family', app_family)
284+
CONF.set('appearance', 'app_font/size', app_size)
285+
286+
from spyder.config.fonts import MEDIUM, MONOSPACE
287+
CONF.set('appearance', 'monospace_app_font/family', MONOSPACE[0])
288+
CONF.set('appearance', 'monospace_app_font/size', MEDIUM)
289+
else:
290+
# Raise in case the error is valid
291+
raise error
280292

281293
# Required for correct icon on GNOME/Wayland:
282294
if hasattr(app, 'setDesktopFileName'):

spyder/config/utils.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ def get_filter(filetypes, ext):
116116
return ''
117117

118118

119-
def get_edit_filetypes():
119+
def get_edit_filetypes(ignore_pygments_extensions=True):
120120
"""Get all file types supported by the Editor"""
121-
# The filter details are not hidden on Windows, so we can't use
122-
# all Pygments extensions on that platform
123-
if os.name == 'nt':
121+
# The filter details are not hidden on Windows, so we can't use all
122+
# Pygments extensions on that platform.
123+
if os.name == 'nt' and ignore_pygments_extensions:
124124
supported_exts = []
125125
else:
126126
try:
@@ -154,8 +154,8 @@ def get_edit_extensions():
154154
Return extensions associated with the file types
155155
supported by the Editor
156156
"""
157-
edit_filetypes = get_edit_filetypes()
158-
return _get_extensions(edit_filetypes)+['']
157+
edit_filetypes = get_edit_filetypes(ignore_pygments_extensions=False)
158+
return _get_extensions(edit_filetypes) + ['']
159159

160160

161161
#==============================================================================

spyder/plugins/editor/utils/switcher_manager.py

+16-17
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def setup_switcher(self):
6161
self._switcher.sig_rejected.connect(self.handle_switcher_rejection)
6262
self._switcher.sig_item_changed.connect(
6363
self.handle_switcher_item_change)
64+
self._switcher.sig_search_text_available.connect(
65+
lambda text: self._switcher.setup()
66+
)
6467

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

8083
editorstack = self._editorstack()
81-
82-
# Since editor open files are inserted at position 0, the
83-
# list needs to be reversed so they're shown in order.
8484
editor_list = editorstack.data.copy()
85-
editor_list.reverse()
86-
87-
paths = [data.filename.lower()
88-
for data in editor_list]
89-
save_statuses = [data.newly_created
90-
for data in editor_list]
85+
paths = [data.filename for data in editor_list]
86+
save_statuses = [data.newly_created for data in editor_list]
9187
short_paths = shorten_paths(paths, save_statuses)
9288

9389
for idx, data in enumerate(editor_list):
@@ -99,15 +95,18 @@ def create_editor_switcher(self):
9995
if len(paths[idx]) > 75:
10096
path = short_paths[idx]
10197
else:
102-
path = osp.dirname(data.filename.lower())
98+
path = osp.dirname(data.filename)
10399
last_item = (idx + 1 == len(editor_list))
104-
self._switcher.add_item(title=title,
105-
description=path,
106-
icon=icon,
107-
section=self._section,
108-
data=data,
109-
last_item=last_item)
110-
self._switcher.set_current_row(0)
100+
101+
self._switcher.add_item(
102+
title=title,
103+
description=path,
104+
icon=icon,
105+
section=self._section,
106+
data=data,
107+
last_item=last_item,
108+
score=0 # To make these items appear above those from Projects
109+
)
111110

112111
def create_line_switcher(self):
113112
"""Populate switcher with line info."""

spyder/plugins/projects/plugin.py

+31-20
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ def on_switcher_available(self):
216216
self._switcher.sig_item_selected.connect(
217217
self._handle_switcher_selection)
218218
self._switcher.sig_search_text_available.connect(
219-
self._handle_switcher_results)
219+
self._handle_switcher_search)
220220

221221
@on_plugin_teardown(plugin=Plugins.Editor)
222222
def on_editor_teardown(self):
@@ -281,7 +281,7 @@ def on_switcher_teardown(self):
281281
self._switcher.sig_item_selected.disconnect(
282282
self._handle_switcher_selection)
283283
self._switcher.sig_search_text_available.disconnect(
284-
self._handle_switcher_results)
284+
self._handle_switcher_search)
285285
self._switcher = None
286286

287287
def on_close(self, cancelable=False):
@@ -508,17 +508,11 @@ def _handle_switcher_modes(self, mode):
508508
mode: str
509509
The selected mode (open files "", symbol "@" or line ":").
510510
"""
511-
items = self.get_widget().handle_switcher_modes()
512-
for (title, description, icon, section, path, is_last_item) in items:
513-
self._switcher.add_item(
514-
title=title,
515-
description=description,
516-
icon=icon,
517-
section=section,
518-
data=path,
519-
last_item=is_last_item
520-
)
521-
self._switcher.set_current_row(0)
511+
# Don't compute anything if we're not in files mode
512+
if mode != "":
513+
return
514+
515+
self.get_widget().display_default_switcher_items()
522516

523517
def _handle_switcher_selection(self, item, mode, search_text):
524518
"""
@@ -540,20 +534,33 @@ def _handle_switcher_selection(self, item, mode, search_text):
540534
self.get_widget().handle_switcher_selection(item, mode, search_text)
541535
self._switcher.hide()
542536

543-
def _handle_switcher_results(self, search_text, items_data):
537+
def _handle_switcher_search(self, search_text):
544538
"""
545539
Handle user typing in switcher to filter results.
546540
547-
Load switcher results when a search text is typed for projects.
548541
Parameters
549542
----------
550543
text: str
551544
The current search text in the switcher dialog box.
552-
items_data: list
553-
List of items shown in the switcher.
554545
"""
555-
items = self.get_widget().handle_switcher_results(search_text,
556-
items_data)
546+
self.get_widget().handle_switcher_search(search_text)
547+
548+
def _display_items_in_switcher(self, items, setup, clear_section):
549+
"""
550+
Display a list of items in the switcher.
551+
552+
Parameters
553+
----------
554+
items: list
555+
Items to display.
556+
setup: bool
557+
Call the switcher's setup after adding the items.
558+
clear_section: bool
559+
Clear Projects section before adding the items.
560+
"""
561+
if clear_section:
562+
self._switcher.remove_section(self.get_widget().get_title())
563+
557564
for (title, description, icon, section, path, is_last_item) in items:
558565
self._switcher.add_item(
559566
title=title,
@@ -562,5 +569,9 @@ def _handle_switcher_results(self, search_text, items_data):
562569
section=section,
563570
data=path,
564571
last_item=is_last_item,
565-
score=100
572+
score=1e10, # To make the editor results appear first
573+
use_score=False # Results come from fzf in the right order
566574
)
575+
576+
if setup:
577+
self._switcher.setup()

spyder/plugins/projects/tests/test_plugin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ def test_project_explorer_tree_root(projects, tmpdir, qtbot):
351351
# Open the projects.
352352
for ppath in [ppath1, ppath2]:
353353
projects.open_project(path=ppath)
354-
projects.get_widget()._update_explorer(None)
354+
projects.get_widget()._setup_project(ppath)
355355

356356
# Check that the root path of the project explorer tree widget is
357357
# set correctly.

0 commit comments

Comments
 (0)