| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | /**************************************************************************/ | 
					
						
							|  |  |  | /*  test_completion.h                                                     */ | 
					
						
							|  |  |  | /**************************************************************************/ | 
					
						
							|  |  |  | /*                         This file is part of:                          */ | 
					
						
							|  |  |  | /*                             GODOT ENGINE                               */ | 
					
						
							|  |  |  | /*                        https://godotengine.org                         */ | 
					
						
							|  |  |  | /**************************************************************************/ | 
					
						
							|  |  |  | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ | 
					
						
							|  |  |  | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */ | 
					
						
							|  |  |  | /*                                                                        */ | 
					
						
							|  |  |  | /* Permission is hereby granted, free of charge, to any person obtaining  */ | 
					
						
							|  |  |  | /* a copy of this software and associated documentation files (the        */ | 
					
						
							|  |  |  | /* "Software"), to deal in the Software without restriction, including    */ | 
					
						
							|  |  |  | /* without limitation the rights to use, copy, modify, merge, publish,    */ | 
					
						
							|  |  |  | /* distribute, sublicense, and/or sell copies of the Software, and to     */ | 
					
						
							|  |  |  | /* permit persons to whom the Software is furnished to do so, subject to  */ | 
					
						
							|  |  |  | /* the following conditions:                                              */ | 
					
						
							|  |  |  | /*                                                                        */ | 
					
						
							|  |  |  | /* The above copyright notice and this permission notice shall be         */ | 
					
						
							|  |  |  | /* included in all copies or substantial portions of the Software.        */ | 
					
						
							|  |  |  | /*                                                                        */ | 
					
						
							|  |  |  | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */ | 
					
						
							|  |  |  | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */ | 
					
						
							|  |  |  | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ | 
					
						
							|  |  |  | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */ | 
					
						
							|  |  |  | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */ | 
					
						
							|  |  |  | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */ | 
					
						
							|  |  |  | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */ | 
					
						
							|  |  |  | /**************************************************************************/ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #pragma once
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #ifdef TOOLS_ENABLED
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-23 03:29:22 +08:00
										 |  |  | #include "tests/test_macros.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include "../gdscript.h"
 | 
					
						
							|  |  |  | #include "gdscript_test_runner.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | #include "core/config/project_settings.h"
 | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | #include "core/io/config_file.h"
 | 
					
						
							|  |  |  | #include "core/io/dir_access.h"
 | 
					
						
							|  |  |  | #include "core/io/file_access.h"
 | 
					
						
							|  |  |  | #include "core/object/script_language.h"
 | 
					
						
							|  |  |  | #include "core/variant/dictionary.h"
 | 
					
						
							|  |  |  | #include "core/variant/variant.h"
 | 
					
						
							| 
									
										
										
										
											2025-06-10 16:47:26 +02:00
										 |  |  | #include "editor/settings/editor_settings.h"
 | 
					
						
							| 
									
										
										
										
											2024-12-23 03:29:22 +08:00
										 |  |  | #include "scene/resources/packed_scene.h"
 | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | #include "scene/theme/theme_db.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-23 03:29:22 +08:00
										 |  |  | #include "modules/modules_enabled.gen.h" // For mono.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | namespace GDScriptTests { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) { | 
					
						
							|  |  |  | 	if (p_expected.get("display", p_got.display) != p_got.display) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (p_expected.get("location", p_got.location) != Variant(p_got.location)) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return true; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) { | 
					
						
							|  |  |  | 	ERR_FAIL_COND(!p_variant.is_array()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Array arr = p_variant; | 
					
						
							|  |  |  | 	for (int i = 0; i < arr.size(); i++) { | 
					
						
							|  |  |  | 		if (arr[i].get_type() == Variant::DICTIONARY) { | 
					
						
							|  |  |  | 			p_list.push_back(arr[i]); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static void test_directory(const String &p_dir) { | 
					
						
							|  |  |  | 	Error err = OK; | 
					
						
							|  |  |  | 	Ref<DirAccess> dir = DirAccess::open(p_dir, &err); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (err != OK) { | 
					
						
							|  |  |  | 		FAIL("Invalid test directory."); | 
					
						
							|  |  |  | 		return; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	String path = dir->get_current_dir(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	dir->list_dir_begin(); | 
					
						
							|  |  |  | 	String next = dir->get_next(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	while (!next.is_empty()) { | 
					
						
							|  |  |  | 		if (dir->current_is_dir()) { | 
					
						
							|  |  |  | 			if (next == "." || next == "..") { | 
					
						
							|  |  |  | 				next = dir->get_next(); | 
					
						
							|  |  |  | 				continue; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			test_directory(path.path_join(next)); | 
					
						
							|  |  |  | 		} else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) { | 
					
						
							|  |  |  | 			Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (err != OK) { | 
					
						
							|  |  |  | 				next = dir->get_next(); | 
					
						
							|  |  |  | 				continue; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			String code = acc->get_as_utf8_string(); | 
					
						
							|  |  |  | 			// For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.
 | 
					
						
							|  |  |  | 			code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF)); | 
					
						
							|  |  |  | 			// Require pointer sentinel char in scripts.
 | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 			int location = code.find_char(0xFFFF); | 
					
						
							|  |  |  | 			CHECK(location != -1); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			String res_path = ProjectSettings::get_singleton()->localize_path(path.path_join(next)); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			ConfigFile conf; | 
					
						
							|  |  |  | 			if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) { | 
					
						
							|  |  |  | 				FAIL("No config file found."); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #ifndef MODULE_MONO_ENABLED
 | 
					
						
							|  |  |  | 			if (conf.get_value("input", "cs", false)) { | 
					
						
							|  |  |  | 				next = dir->get_next(); | 
					
						
							|  |  |  | 				continue; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | #endif
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false)); | 
					
						
							| 
									
										
										
										
											2024-06-11 22:28:18 +02:00
										 |  |  | 			EditorSettings::get_singleton()->set_setting("text_editor/completion/add_node_path_literals", conf.get_value("input", "add_node_path_literals", false)); | 
					
						
							|  |  |  | 			EditorSettings::get_singleton()->set_setting("text_editor/completion/add_string_name_literals", conf.get_value("input", "add_string_name_literals", false)); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			List<Dictionary> include; | 
					
						
							| 
									
										
										
										
											2024-01-08 16:03:57 +01:00
										 |  |  | 			to_dict_list(conf.get_value("output", "include", Array()), include); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			List<Dictionary> exclude; | 
					
						
							| 
									
										
										
										
											2024-01-08 16:03:57 +01:00
										 |  |  | 			to_dict_list(conf.get_value("output", "exclude", Array()), exclude); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			List<ScriptLanguage::CodeCompletionOption> options; | 
					
						
							|  |  |  | 			String call_hint; | 
					
						
							|  |  |  | 			bool forced; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 			Node *scene = nullptr; | 
					
						
							| 
									
										
										
										
											2024-01-08 16:03:57 +01:00
										 |  |  | 			if (conf.has_section_key("input", "scene")) { | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 				Ref<PackedScene> packed_scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP); | 
					
						
							|  |  |  | 				if (packed_scene.is_valid()) { | 
					
						
							|  |  |  | 					scene = packed_scene->instantiate(); | 
					
						
							| 
									
										
										
										
											2024-01-08 16:03:57 +01:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} else if (dir->file_exists(next.get_basename() + ".tscn")) { | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 				Ref<PackedScene> packed_scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene"); | 
					
						
							|  |  |  | 				if (packed_scene.is_valid()) { | 
					
						
							|  |  |  | 					scene = packed_scene->instantiate(); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			Node *owner = nullptr; | 
					
						
							|  |  |  | 			if (scene != nullptr) { | 
					
						
							|  |  |  | 				owner = scene->get_node(conf.get_value("input", "node_path", ".")); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-06 00:40:35 +01:00
										 |  |  | 			// The only requirement is for the script to be parsable, warnings and errors from the analyzer might happen and completion should still work.
 | 
					
						
							|  |  |  | 			ERR_PRINT_OFF; | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 			if (owner != nullptr) { | 
					
						
							|  |  |  | 				// Remove the line which contains the sentinel char, to get a valid script.
 | 
					
						
							|  |  |  | 				Ref<GDScript> scr; | 
					
						
							|  |  |  | 				scr.instantiate(); | 
					
						
							|  |  |  | 				int start = location; | 
					
						
							|  |  |  | 				int end = location; | 
					
						
							|  |  |  | 				for (; start >= 0; --start) { | 
					
						
							|  |  |  | 					if (code.get(start) == '\n') { | 
					
						
							|  |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				for (; end < code.size(); ++end) { | 
					
						
							|  |  |  | 					if (code.get(end) == '\n') { | 
					
						
							|  |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 				} | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 				scr->set_source_code(code.erase(start, end - start)); | 
					
						
							|  |  |  | 				scr->reload(); | 
					
						
							|  |  |  | 				scr->set_path(res_path); | 
					
						
							|  |  |  | 				owner->set_script(scr); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 			} | 
					
						
							| 
									
										
										
										
											2024-03-01 12:01:14 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 			GDScriptLanguage::get_singleton()->complete_code(code, res_path, owner, &options, forced, call_hint); | 
					
						
							| 
									
										
										
										
											2025-03-06 00:40:35 +01:00
										 |  |  | 			ERR_PRINT_ON; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 			String contains_excluded; | 
					
						
							|  |  |  | 			for (ScriptLanguage::CodeCompletionOption &option : options) { | 
					
						
							|  |  |  | 				for (const Dictionary &E : exclude) { | 
					
						
							|  |  |  | 					if (match_option(E, option)) { | 
					
						
							|  |  |  | 						contains_excluded = option.display; | 
					
						
							|  |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				if (!contains_excluded.is_empty()) { | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				for (const Dictionary &E : include) { | 
					
						
							|  |  |  | 					if (match_option(E, option)) { | 
					
						
							|  |  |  | 						include.erase(E); | 
					
						
							|  |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'."); | 
					
						
							|  |  |  | 			CHECK(include.is_empty()); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 16:03:57 +01:00
										 |  |  | 			String expected_call_hint = conf.get_value("output", "call_hint", call_hint); | 
					
						
							|  |  |  | 			bool expected_forced = conf.get_value("output", "forced", forced); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			CHECK(expected_call_hint == call_hint); | 
					
						
							|  |  |  | 			CHECK(expected_forced == forced); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-25 16:11:44 +01:00
										 |  |  | 			if (scene) { | 
					
						
							|  |  |  | 				memdelete(scene); | 
					
						
							| 
									
										
										
										
											2023-11-21 16:06:43 +01:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		next = dir->get_next(); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | TEST_SUITE("[Modules][GDScript][Completion]") { | 
					
						
							|  |  |  | 	TEST_CASE("[Editor] Check suggestion list") { | 
					
						
							|  |  |  | 		// Set all editor settings that code completion relies on.
 | 
					
						
							|  |  |  | 		EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false); | 
					
						
							|  |  |  | 		init_language("modules/gdscript/tests/scripts"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		test_directory("modules/gdscript/tests/scripts/completion"); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | } // namespace GDScriptTests
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endif
 |