diff --git a/editor/editor_paths.cpp b/editor/editor_paths.cpp index 801c81efa848..b31a22453090 100644 --- a/editor/editor_paths.cpp +++ b/editor/editor_paths.cpp @@ -97,6 +97,7 @@ void EditorPaths::create() { void EditorPaths::free() { ERR_FAIL_NULL(singleton); memdelete(singleton); + singleton = nullptr; } void EditorPaths::_bind_methods() { diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index a0329eb8d231..c6df7636f6b7 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -268,7 +268,7 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) { while (!next.is_empty()) { if (dir->current_is_dir()) { - if (next == "." || next == ".." || next == "completion" || next == "lsp") { + if (next == "." || next == ".." || next == "completion" || next == "highlighter" || next == "lsp") { next = dir->get_next(); continue; } @@ -345,7 +345,7 @@ static bool generate_class_index_recursive(const String &p_dir) { StringName gdscript_name = GDScriptLanguage::get_singleton()->get_name(); while (!next.is_empty()) { if (dir->current_is_dir()) { - if (next == "." || next == ".." || next == "completion" || next == "lsp") { + if (next == "." || next == ".." || next == "completion" || next == "highlighter" || next == "lsp") { next = dir->get_next(); continue; } diff --git a/modules/gdscript/tests/scripts/highlighter/type_hints.gd b/modules/gdscript/tests/scripts/highlighter/type_hints.gd new file mode 100644 index 000000000000..297edb356376 --- /dev/null +++ b/modules/gdscript/tests/scripts/highlighter/type_hints.gd @@ -0,0 +1,15 @@ +class_name MyClass + +class InnerClass extends RefCounted: pass +enum MyEnum {A = 123, B} + +var variant: Variant +var variant_type: Variant.Type +var builtin: int +var typed_array: Array[Node] +var native: Node +var global: MyClass +var inner: InnerClass +var my_enum: MyEnum + +var a : int diff --git a/modules/gdscript/tests/scripts/highlighter/type_hints.out b/modules/gdscript/tests/scripts/highlighter/type_hints.out new file mode 100644 index 000000000000..6b4baef29cd0 --- /dev/null +++ b/modules/gdscript/tests/scripts/highlighter/type_hints.out @@ -0,0 +1,32 @@ +0001 class_name MyClass +>>>> Kw SN +0002 +>>>> +0003 class InnerClass extends RefCounted: pass +>>>> Kw SN SKw SEt S Cf +0004 enum MyEnum {A = 123, B} +>>>> Kw SN S NS NumS NS +0005 +>>>> +0006 var variant: Variant +>>>> Kw SN S Bt +0007 var variant_type: Variant.Type +>>>> Kw SN S Bt SBt +0008 var builtin: int +>>>> Kw SN S Bt +0009 var typed_array: Array[Node] +>>>> Kw SN S Bt SEt S +0010 var native: Node +>>>> Kw SN S Et +0011 var global: MyClass +>>>> Kw SN S Bt +0012 var inner: InnerClass +>>>> Kw SN S Bt +0013 var my_enum: MyEnum +>>>> Kw SN S Bt +0014 +>>>> +0015 var a : int +>>>> Kw SNS Bt +0016 +>>>> diff --git a/modules/gdscript/tests/test_highlighter.h b/modules/gdscript/tests/test_highlighter.h new file mode 100644 index 000000000000..483d9ed419c6 --- /dev/null +++ b/modules/gdscript/tests/test_highlighter.h @@ -0,0 +1,217 @@ +/**************************************************************************/ +/* test_highlighter.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. */ +/**************************************************************************/ + +#ifndef TEST_HIGHLIGHTER_H +#define TEST_HIGHLIGHTER_H + +#ifdef TOOLS_ENABLED + +#include "../editor/gdscript_highlighter.h" +#include "../gdscript.h" +#include "gdscript_test_runner.h" + +#include "core/io/dir_access.h" +#include "core/io/file_access.h" +#include "core/templates/hash_map.h" +#include "core/variant/variant.h" +#include "editor/editor_settings.h" +#include "scene/gui/text_edit.h" +#include "tests/test_macros.h" + +namespace GDScriptTests { + +class TestHighlighter { + static HashMap color_map; + static TextEdit *text_edit; + static Ref highlighter; + + static void make_output(int p_line_idx, const String &p_line_str, const Dictionary &p_dict, String &r_output) { + String symbols; + String error; + + const Variant *key = nullptr; + while ((key = p_dict.next(key)) != nullptr) { + const int column = *key; + const Color color = p_dict[*key].operator Dictionary().get("color", Color()); + + const HashMap::ConstIterator E = color_map.find(color); + if (!E) { + error = vformat("Invalid color %s at column %d.", color, column + 1); + break; + } + const String symbol = E->value; + + if (symbols.length() < column) { + symbols += String(" ").repeat(column - symbols.length()); + } else if (symbols.length() > column) { + error = vformat("Symbol %s does not fit in column %d.", symbol, column + 1); + break; + } + + symbols += symbol; + } + +#define ADD_LINE(m_text) r_output += (m_text).strip_edges(false, true) + "\n"; + + ADD_LINE(vformat("%04d %s", p_line_idx + 1, p_line_str.replace("\t", " "))); + ADD_LINE(vformat(">>>> %s", symbols)); + if (!error.is_empty()) { + ADD_LINE(vformat(">>>> ERROR: %s", error)); + } + +#undef ADD_LINE + } + + static void test_directory(const String &p_dir_path) { + Error err = OK; + Ref dir = DirAccess::open(p_dir_path, &err); + if (err != OK) { + FAIL(vformat(R"(Cannot open directory "%s".)", p_dir_path)); + return; + } + + dir->list_dir_begin(); + while (true) { + const String item_name = dir->get_next(); + if (item_name.is_empty()) { + break; + } + + const String path = dir->get_current_dir().path_join(item_name); + + if (dir->current_is_dir()) { + if (item_name != "." && item_name != "..") { + test_directory(path); + } + } else if (item_name.get_extension() == "gd") { + Ref gdscript; + gdscript.instantiate(); + gdscript->set_path(path); + gdscript->load_source_code(path); + + text_edit->set_text(gdscript->get_source_code()); + highlighter->_set_edited_resource(gdscript); + highlighter->_update_cache(); + + String output; + for (int i = 0; i < text_edit->get_line_count(); i++) { + Dictionary dict = highlighter->_get_line_syntax_highlighting_impl(i); + make_output(i, text_edit->get_line(i), dict, output); + } + + const String out_path = path.get_basename() + ".out"; + const Ref out_file = FileAccess::open(out_path, FileAccess::READ, &err); + if (err != OK) { + FAIL(vformat(R"(Cannot open file "%s".)", out_path)); + return; + } + + const String expected = out_file->get_as_utf8_string(); + const bool result = output == expected; + CHECK_MESSAGE(result, vformat("%s\n\n%s", path, output)); + } + } + } + +public: + static void test() { + text_edit = memnew(TextEdit); + highlighter.instantiate(); + highlighter->set_text_edit(text_edit); + + struct ColorInfo { + const char *symbol; + const char *setting; + }; + + constexpr ColorInfo COLOR_INFO[] = { + /* clang-format off */ + { "N", "" }, + { "S", "text_editor/theme/highlighting/symbol_color" }, + { "Kw", "text_editor/theme/highlighting/keyword_color" }, + { "Cf", "text_editor/theme/highlighting/control_flow_keyword_color" }, + { "Bt", "text_editor/theme/highlighting/base_type_color" }, + { "Et", "text_editor/theme/highlighting/engine_type_color" }, + { "Ut", "text_editor/theme/highlighting/user_type_color" }, + { "Com", "text_editor/theme/highlighting/comment_color" }, + { "Doc", "text_editor/theme/highlighting/doc_comment_color" }, + { "Str", "text_editor/theme/highlighting/string_color" }, + { "Num", "text_editor/theme/highlighting/number_color" }, + { "Fn", "text_editor/theme/highlighting/function_color" }, + { "Mem", "text_editor/theme/highlighting/member_variable_color" }, + { "Rgn", "text_editor/theme/highlighting/folded_code_region_color" }, + { "Fndef", "text_editor/theme/highlighting/gdscript/function_definition_color" }, + { "Gfn", "text_editor/theme/highlighting/gdscript/global_function_color" }, + { "Npath", "text_editor/theme/highlighting/gdscript/node_path_color" }, + { "Nref", "text_editor/theme/highlighting/gdscript/node_reference_color" }, + { "Annot", "text_editor/theme/highlighting/gdscript/annotation_color" }, + { "Sname", "text_editor/theme/highlighting/gdscript/string_name_color" }, + { "Mcrit", "text_editor/theme/highlighting/comment_markers/critical_color" }, + { "Mwarn", "text_editor/theme/highlighting/comment_markers/warning_color" }, + { "Mnote", "text_editor/theme/highlighting/comment_markers/notice_color" }, + /* clang-format on */ + }; + + for (long unsigned int i = 0; i < sizeof(COLOR_INFO) / sizeof(COLOR_INFO[0]); i++) { + Color color(0, 0, 0.01 * (i + 1)); + color_map[color] = COLOR_INFO[i].symbol; + if (i == 0) { + text_edit->add_theme_color_override("font_color", color); + } else { + EditorSettings::get_singleton()->set_setting(COLOR_INFO[i].setting, color); + } + } + + init_language("modules/gdscript/tests/scripts"); + + test_directory("modules/gdscript/tests/scripts/highlighter"); + + color_map.clear(); + memdelete(text_edit); + text_edit = nullptr; + highlighter.unref(); + } +}; + +HashMap TestHighlighter::color_map; +TextEdit *TestHighlighter::text_edit = nullptr; +Ref TestHighlighter::highlighter; + +TEST_SUITE("[Modules][GDScript][Highlighter]") { + TEST_CASE("[Editor] Check GDScript highlighter") { + TestHighlighter::test(); + } +} + +} // namespace GDScriptTests + +#endif // TOOLS_ENABLED + +#endif // TEST_HIGHLIGHTER_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index f40f5629730a..4f5c2febc875 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -320,6 +320,9 @@ struct GodotTestCaseListener : public doctest::IReporter { if (EditorSettings::get_singleton()) { EditorSettings::destroy(); } + if (EditorPaths::get_singleton()) { + EditorPaths::free(); + } #endif // TOOLS_ENABLED Engine::get_singleton()->set_editor_hint(false);