diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index afbc59dd773..f74458e89b4 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -1249,6 +1249,7 @@ struct ProjectListComparator {
 	}
 };
 
+const char *ProjectList::SIGNAL_LIST_CHANGED = "list_changed";
 const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
 const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
 
@@ -1356,7 +1357,7 @@ ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_fa
 	return Item(project_name, description, tags, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
 }
 
-void ProjectList::migrate_config() {
+void ProjectList::_migrate_config() {
 	// Proposal #1637 moved the project list from editor settings to a separate config file
 	// If the new config file doesn't exist, populate it from EditorSettings
 	if (FileAccess::exists(_config_path)) {
@@ -1388,9 +1389,10 @@ void ProjectList::migrate_config() {
 	save_config();
 }
 
-void ProjectList::load_projects() {
+void ProjectList::update_project_list() {
 	// This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
 	// If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
+	// FIXME: Does it really have to be a full, hard reload? Runtime updates should be made much cheaper.
 
 	// Clear whole list
 	for (int i = 0; i < _projects.size(); ++i) {
@@ -1421,6 +1423,7 @@ void ProjectList::load_projects() {
 	update_dock_menu();
 
 	set_v_scroll(0);
+	emit_signal(SNAME(SIGNAL_LIST_CHANGED));
 }
 
 void ProjectList::update_dock_menu() {
@@ -1678,6 +1681,48 @@ void ProjectList::erase_missing_projects() {
 	save_config();
 }
 
+void ProjectList::_scan_folder_recursive(const String &p_path, List<String> *r_projects) {
+	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
+	Error error = da->change_dir(p_path);
+	ERR_FAIL_COND_MSG(error != OK, vformat("Failed to open the path \"%s\" for scanning (code %d).", p_path, error));
+
+	da->list_dir_begin();
+	String n = da->get_next();
+	while (!n.is_empty()) {
+		if (da->current_is_dir() && n[0] != '.') {
+			_scan_folder_recursive(da->get_current_dir().path_join(n), r_projects);
+		} else if (n == "project.godot") {
+			r_projects->push_back(da->get_current_dir());
+		}
+		n = da->get_next();
+	}
+	da->list_dir_end();
+}
+
+void ProjectList::find_projects(const String &p_path) {
+	PackedStringArray paths = { p_path };
+	find_projects_multiple(paths);
+}
+
+void ProjectList::find_projects_multiple(const PackedStringArray &p_paths) {
+	List<String> projects;
+
+	for (int i = 0; i < p_paths.size(); i++) {
+		const String &base_path = p_paths.get(i);
+		print_verbose(vformat("Scanning for projects in \"%s\".", base_path));
+
+		_scan_folder_recursive(base_path, &projects);
+		print_verbose(vformat("Found %d project(s).", projects.size()));
+	}
+
+	for (const String &E : projects) {
+		add_project(E, false);
+	}
+
+	save_config();
+	update_project_list();
+}
+
 int ProjectList::refresh_project(const String &dir_path) {
 	// Reloads information about a specific project.
 	// If it wasn't loaded and should be in the list, it is added (i.e new project).
@@ -1916,6 +1961,7 @@ void ProjectList::_show_project(const String &p_path) {
 }
 
 void ProjectList::_bind_methods() {
+	ADD_SIGNAL(MethodInfo(SIGNAL_LIST_CHANGED));
 	ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
 	ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
 }
@@ -1926,6 +1972,7 @@ ProjectList::ProjectList() {
 	add_child(_scroll_children);
 
 	_config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
+	_migrate_config();
 }
 
 /// Project Manager.
@@ -2213,15 +2260,6 @@ void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
 	}
 }
 
-void ProjectManager::_load_recent_projects() {
-	_project_list->set_search_term(search_box->get_text().strip_edges());
-	_project_list->load_projects();
-
-	_update_project_buttons();
-
-	tabs->set_current_tab(0);
-}
-
 void ProjectManager::_on_projects_updated() {
 	Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
 	int index = 0;
@@ -2459,30 +2497,6 @@ void ProjectManager::_run_project() {
 	}
 }
 
-void ProjectManager::_scan_dir(const String &path) {
-	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
-	Error error = da->change_dir(path);
-	ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path);
-	da->list_dir_begin();
-	String n = da->get_next();
-	while (!n.is_empty()) {
-		if (da->current_is_dir() && !n.begins_with(".")) {
-			_scan_dir(da->get_current_dir().path_join(n));
-		} else if (n == "project.godot") {
-			_project_list->add_project(da->get_current_dir(), false);
-		}
-		n = da->get_next();
-	}
-	da->list_dir_end();
-}
-
-void ProjectManager::_scan_begin(const String &p_base) {
-	print_line("Scanning projects at: " + p_base);
-	_scan_dir(p_base);
-	_project_list->save_config();
-	_load_recent_projects();
-}
-
 void ProjectManager::_scan_projects() {
 	scan_dir->popup_file_dialog();
 }
@@ -2682,54 +2696,26 @@ void ProjectManager::_install_project(const String &p_zip_path, const String &p_
 }
 
 void ProjectManager::_files_dropped(PackedStringArray p_files) {
+	// TODO: Support installing multiple ZIPs at the same time?
 	if (p_files.size() == 1 && p_files[0].ends_with(".zip")) {
-		const String file = p_files[0].get_file();
-		_install_project(p_files[0], file.substr(0, file.length() - 4).capitalize());
+		const String &file = p_files[0];
+		_install_project(file, file.get_file().get_basename().capitalize());
 		return;
 	}
+
 	HashSet<String> folders_set;
 	Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
 	for (int i = 0; i < p_files.size(); i++) {
-		String file = p_files[i];
+		const String &file = p_files[i];
 		folders_set.insert(da->dir_exists(file) ? file : file.get_base_dir());
 	}
-	if (folders_set.size() > 0) {
-		PackedStringArray folders;
-		for (const String &E : folders_set) {
-			folders.push_back(E);
-		}
+	ERR_FAIL_COND(folders_set.size() == 0); // This can't really happen, we consume every dropped file path above.
 
-		bool confirm = true;
-		if (folders.size() == 1) {
-			Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
-			if (dir->change_dir(folders[0]) == OK) {
-				dir->list_dir_begin();
-				String file = dir->get_next();
-				while (confirm && !file.is_empty()) {
-					if (!dir->current_is_dir() && file.ends_with("project.godot")) {
-						confirm = false;
-					}
-					file = dir->get_next();
-				}
-				dir->list_dir_end();
-			}
-		}
-		if (confirm) {
-			multi_scan_ask->get_ok_button()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders));
-			multi_scan_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders).bind(folders));
-			multi_scan_ask->set_text(
-					vformat(TTR("Are you sure to scan %s folders for existing Godot projects?\nThis could take a while."), folders.size()));
-			multi_scan_ask->popup_centered();
-		} else {
-			_scan_multiple_folders(folders);
-		}
-	}
-}
-
-void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) {
-	for (int i = 0; i < p_files.size(); i++) {
-		_scan_begin(p_files.get(i));
+	PackedStringArray folders;
+	for (const String &E : folders_set) {
+		folders.push_back(E);
 	}
+	_project_list->find_projects_multiple(folders);
 }
 
 void ProjectManager::_on_order_option_changed(int p_idx) {
@@ -2770,11 +2756,6 @@ void ProjectManager::_on_search_term_submitted(const String &p_text) {
 	_open_selected_projects_ask();
 }
 
-void ProjectManager::_bind_methods() {
-	ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons);
-	ClassDB::bind_method("_version_button_pressed", &ProjectManager::_version_button_pressed);
-}
-
 void ProjectManager::_open_asset_library() {
 	asset_library->disable_community_support();
 	tabs->set_current_tab(1);
@@ -2900,8 +2881,8 @@ ProjectManager::ProjectManager() {
 	vb->add_child(center_box);
 
 	tabs = memnew(TabContainer);
-	center_box->add_child(tabs);
 	tabs->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
+	center_box->add_child(tabs);
 	tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
 
 	local_projects_vb = memnew(VBoxContainer);
@@ -2980,10 +2961,11 @@ ProjectManager::ProjectManager() {
 		search_tree_hb->add_child(search_panel);
 
 		_project_list = memnew(ProjectList);
-		_project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
-		_project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
 		_project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
 		search_panel->add_child(_project_list);
+		_project_list->connect(ProjectList::SIGNAL_LIST_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+		_project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
+		_project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
 
 		// The side bar with the edit, run, rename, etc. buttons.
 		VBoxContainer *tree_vb = memnew(VBoxContainer);
@@ -3129,7 +3111,7 @@ ProjectManager::ProjectManager() {
 		scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden
 		scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path"));
 		add_child(scan_dir);
-		scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin));
+		scan_dir->connect("dir_selected", callable_mp(_project_list, &ProjectList::find_projects));
 
 		erase_missing_ask = memnew(ConfirmationDialog);
 		erase_missing_ask->set_ok_button_text(TTR("Remove All"));
@@ -3163,10 +3145,6 @@ ProjectManager::ProjectManager() {
 		multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm));
 		add_child(multi_run_ask);
 
-		multi_scan_ask = memnew(ConfirmationDialog);
-		multi_scan_ask->set_ok_button_text(TTR("Scan"));
-		add_child(multi_scan_ask);
-
 		ask_update_settings = memnew(ConfirmationDialog);
 		ask_update_settings->set_autowrap(true);
 		ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
@@ -3275,29 +3253,36 @@ ProjectManager::ProjectManager() {
 		create_tag_btn->connect("pressed", callable_mp((Window *)create_tag_dialog, &Window::popup_centered).bind(Vector2i(500, 0) * EDSCALE));
 	}
 
-	_project_list->migrate_config();
-	_load_recent_projects();
+	// Initialize project list.
+	{
+		Ref<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM);
 
-	Ref<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM);
-
-	String default_project_path = EDITOR_GET("filesystem/directories/default_project_path");
-	if (!dir_access->dir_exists(default_project_path)) {
-		Error error = dir_access->make_dir_recursive(default_project_path);
-		if (error != OK) {
-			ERR_PRINT("Could not create default project directory at: " + default_project_path);
-		}
-	}
-
-	String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path");
-	if (!autoscan_path.is_empty()) {
-		if (dir_access->dir_exists(autoscan_path)) {
-			_scan_begin(autoscan_path);
-		} else {
-			Error error = dir_access->make_dir_recursive(autoscan_path);
+		String default_project_path = EDITOR_GET("filesystem/directories/default_project_path");
+		if (!default_project_path.is_empty() && !dir_access->dir_exists(default_project_path)) {
+			Error error = dir_access->make_dir_recursive(default_project_path);
 			if (error != OK) {
-				ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path);
+				ERR_PRINT("Could not create default project directory at: " + default_project_path);
 			}
 		}
+
+		bool scanned_for_projects = false; // Scanning will update the list automatically.
+
+		String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path");
+		if (!autoscan_path.is_empty()) {
+			if (dir_access->dir_exists(autoscan_path)) {
+				_project_list->find_projects(autoscan_path);
+				scanned_for_projects = true;
+			} else {
+				Error error = dir_access->make_dir_recursive(autoscan_path);
+				if (error != OK) {
+					ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path);
+				}
+			}
+		}
+
+		if (!scanned_for_projects) {
+			_project_list->update_project_list();
+		}
 	}
 
 	SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
diff --git a/editor/project_manager.h b/editor/project_manager.h
index d41207d7a29..8a8b2ff99d4 100644
--- a/editor/project_manager.h
+++ b/editor/project_manager.h
@@ -267,6 +267,9 @@ private:
 	void _favorite_pressed(Node *p_hb);
 	void _show_project(const String &p_path);
 
+	void _migrate_config();
+	void _scan_folder_recursive(const String &p_path, List<String> *r_projects);
+
 	void _clear_project_selection();
 	void _toggle_project(int p_index);
 	void _select_project_nocheck(int p_index);
@@ -288,12 +291,15 @@ protected:
 	static void _bind_methods();
 
 public:
+	static const char *SIGNAL_LIST_CHANGED;
 	static const char *SIGNAL_SELECTION_CHANGED;
 	static const char *SIGNAL_PROJECT_ASK_OPEN;
 
-	void load_projects();
+	void update_project_list();
 	int get_project_count() const;
 
+	void find_projects(const String &p_path);
+	void find_projects_multiple(const PackedStringArray &p_paths);
 	void sort_projects();
 
 	void add_project(const String &dir_path, bool favorite);
@@ -316,7 +322,6 @@ public:
 	void set_order_option(int p_option);
 
 	void update_dock_menu();
-	void migrate_config();
 	void save_config();
 
 	ProjectList();
@@ -369,7 +374,6 @@ class ProjectManager : public Control {
 	ConfirmationDialog *erase_missing_ask = nullptr;
 	ConfirmationDialog *multi_open_ask = nullptr;
 	ConfirmationDialog *multi_run_ask = nullptr;
-	ConfirmationDialog *multi_scan_ask = nullptr;
 	ConfirmationDialog *ask_full_convert_dialog = nullptr;
 	ConfirmationDialog *ask_update_settings = nullptr;
 	ConfirmationDialog *open_templates = nullptr;
@@ -425,12 +429,8 @@ class ProjectManager : public Control {
 	void _set_new_tag_name(const String p_name);
 	void _create_new_tag();
 
-	void _load_recent_projects();
 	void _on_project_created(const String &dir);
 	void _on_projects_updated();
-	void _scan_multiple_folders(PackedStringArray p_files);
-	void _scan_begin(const String &p_base);
-	void _scan_dir(const String &path);
 
 	void _install_project(const String &p_zip_path, const String &p_title);
 
@@ -449,7 +449,6 @@ class ProjectManager : public Control {
 
 protected:
 	void _notification(int p_what);
-	static void _bind_methods();
 
 public:
 	static ProjectManager *get_singleton() { return singleton; }