/**************************************************************************/ /* gdscript_highlighter.cpp */ /**************************************************************************/ /* 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. */ /**************************************************************************/ #include "gdscript_highlighter.h" #include "../gdscript.h" #include "../gdscript_tokenizer.h" #include "core/config/project_settings.h" #include "editor/editor_settings.h" #include "editor/themes/editor_theme_manager.h" Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_line) { Dictionary color_map; Type next_type = NONE; Type current_type = NONE; Type prev_type = NONE; String prev_text = ""; int prev_column = 0; bool prev_is_char = false; bool prev_is_digit = false; bool prev_is_binary_op = false; bool in_keyword = false; bool in_word = false; bool in_number = false; bool in_node_path = false; bool in_node_ref = false; bool in_annotation = false; bool in_string_name = false; bool is_hex_notation = false; bool is_bin_notation = false; bool in_member_variable = false; bool in_lambda = false; bool in_function_name = false; // Any call. bool in_function_declaration = false; // Only declaration. bool in_signal_declaration = false; bool is_after_func_signal_declaration = false; bool in_var_const_declaration = false; bool is_after_var_const_declaration = false; bool expect_type = false; int in_declaration_params = 0; // The number of opened `(` after func/signal name. int in_declaration_param_dicts = 0; // The number of opened `{` inside func params. int in_type_params = 0; // The number of opened `[` after type name. Color keyword_color; Color color; color_region_cache[p_line] = -1; int in_region = -1; if (p_line != 0) { int prev_region_line = p_line - 1; while (prev_region_line > 0 && !color_region_cache.has(prev_region_line)) { prev_region_line--; } for (int i = prev_region_line; i < p_line - 1; i++) { get_line_syntax_highlighting(i); } if (!color_region_cache.has(p_line - 1)) { get_line_syntax_highlighting(p_line - 1); } in_region = color_region_cache[p_line - 1]; } const String &str = text_edit->get_line(p_line); const int line_length = str.length(); Color prev_color; if (in_region != -1 && line_length == 0) { color_region_cache[p_line] = in_region; } for (int j = 0; j < line_length; j++) { Dictionary highlighter_info; color = font_color; bool is_char = !is_symbol(str[j]); bool is_a_symbol = is_symbol(str[j]); bool is_a_digit = is_digit(str[j]); bool is_binary_op = false; /* color regions */ if (is_a_symbol || in_region != -1) { int from = j; if (in_region == -1) { for (; from < line_length; from++) { if (str[from] == '\\') { from++; continue; } break; } } if (from != line_length) { // Check if we are in entering a region. if (in_region == -1) { const bool r_prefix = from > 0 && str[from - 1] == 'r'; for (int c = 0; c < color_regions.size(); c++) { // Check there is enough room. int chars_left = line_length - from; int start_key_length = color_regions[c].start_key.length(); int end_key_length = color_regions[c].end_key.length(); if (chars_left < start_key_length) { continue; } if (color_regions[c].is_string && color_regions[c].r_prefix != r_prefix) { continue; } // Search the line. bool match = true; const char32_t *start_key = color_regions[c].start_key.get_data(); for (int k = 0; k < start_key_length; k++) { if (start_key[k] != str[from + k]) { match = false; break; } } if (!match) { continue; } in_region = c; from += start_key_length; // Check if it's the whole line. if (end_key_length == 0 || color_regions[c].line_only || from + end_key_length > line_length) { // Don't skip comments, for highlighting markers. if (color_regions[in_region].is_comment) { break; } if (from + end_key_length > line_length) { // If it's key length and there is a '\', dont skip to highlight esc chars. if (str.find("\\", from) >= 0) { break; } } prev_color = color_regions[in_region].color; highlighter_info["color"] = color_regions[c].color; color_map[j] = highlighter_info; j = line_length; if (!color_regions[c].line_only) { color_region_cache[p_line] = c; } } break; } // Don't skip comments, for highlighting markers. if (j == line_length && !color_regions[in_region].is_comment) { continue; } } // If we are in one, find the end key. if (in_region != -1) { Color region_color = color_regions[in_region].color; if (in_node_path && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = node_path_color; } if (in_node_ref && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = node_ref_color; } if (in_string_name && color_regions[in_region].type == ColorRegion::TYPE_STRING) { region_color = string_name_color; } prev_color = region_color; highlighter_info["color"] = region_color; color_map[j] = highlighter_info; if (color_regions[in_region].is_comment) { int marker_start_pos = from; int marker_len = 0; while (from <= line_length) { if (from < line_length && is_unicode_identifier_continue(str[from])) { marker_len++; } else { if (marker_len > 0) { HashMap::ConstIterator E = comment_markers.find(str.substr(marker_start_pos, marker_len)); if (E) { Dictionary marker_highlighter_info; marker_highlighter_info["color"] = comment_marker_colors[E->value]; color_map[marker_start_pos] = marker_highlighter_info; Dictionary marker_continue_highlighter_info; marker_continue_highlighter_info["color"] = region_color; color_map[from] = marker_continue_highlighter_info; } } marker_start_pos = from + 1; marker_len = 0; } from++; } from = line_length - 1; j = from; } else { // Search the line. int region_end_index = -1; int end_key_length = color_regions[in_region].end_key.length(); const char32_t *end_key = color_regions[in_region].end_key.get_data(); for (; from < line_length; from++) { if (line_length - from < end_key_length) { // Don't break if '\' to highlight esc chars. if (str.find("\\", from) < 0) { break; } } if (!is_symbol(str[from])) { continue; } if (str[from] == '\\') { if (!color_regions[in_region].r_prefix) { Dictionary escape_char_highlighter_info; escape_char_highlighter_info["color"] = symbol_color; color_map[from] = escape_char_highlighter_info; } from++; if (!color_regions[in_region].r_prefix) { int esc_len = 0; if (str[from] == 'u') { esc_len = 4; } else if (str[from] == 'U') { esc_len = 6; } for (int k = 0; k < esc_len && from < line_length - 1; k++) { if (!is_hex_digit(str[from + 1])) { break; } from++; } Dictionary region_continue_highlighter_info; region_continue_highlighter_info["color"] = region_color; color_map[from + 1] = region_continue_highlighter_info; } continue; } region_end_index = from; for (int k = 0; k < end_key_length; k++) { if (end_key[k] != str[from + k]) { region_end_index = -1; break; } } if (region_end_index != -1) { break; } } j = from + (end_key_length - 1); if (region_end_index == -1) { color_region_cache[p_line] = in_region; } } prev_type = REGION; prev_text = ""; prev_column = j; in_region = -1; prev_is_char = false; prev_is_digit = false; prev_is_binary_op = false; continue; } } } // VERY hacky... but couldn't come up with anything better. if (j > 0 && (str[j] == '&' || str[j] == '^' || str[j] == '%' || str[j] == '+' || str[j] == '-' || str[j] == '~' || str[j] == '.')) { int to = j - 1; // Find what the last text was (prev_text won't work if there's no whitespace, so we need to do it manually). while (to > 0 && is_whitespace(str[to])) { to--; } int from = to; while (from > 0 && !is_symbol(str[from])) { from--; } String word = str.substr(from + 1, to - from); // Keywords need to be exceptions, except for keywords that represent a value. if (word == "true" || word == "false" || word == "null" || word == "PI" || word == "TAU" || word == "INF" || word == "NAN" || word == "self" || word == "super" || !reserved_keywords.has(word)) { if (!is_symbol(str[to]) || str[to] == '"' || str[to] == '\'' || str[to] == ')' || str[to] == ']' || str[to] == '}') { is_binary_op = true; } } } if (!is_char) { in_keyword = false; } // Allow ABCDEF in hex notation. if (is_hex_notation && (is_hex_digit(str[j]) || is_a_digit)) { is_a_digit = true; } else if (str[j] != '_') { is_hex_notation = false; } // Disallow anything not a 0 or 1 in binary notation. if (is_bin_notation && !is_binary_digit(str[j])) { is_a_digit = false; is_bin_notation = false; } if (!in_number && !in_word && is_a_digit) { in_number = true; } // Special cases for numbers. if (in_number && !is_a_digit) { if (str[j] == 'b' && str[j - 1] == '0') { is_bin_notation = true; } else if (str[j] == 'x' && str[j - 1] == '0') { is_hex_notation = true; } else if (!((str[j] == '-' || str[j] == '+') && str[j - 1] == 'e' && !prev_is_digit) && !(str[j] == '_' && (prev_is_digit || str[j - 1] == 'b' || str[j - 1] == 'x' || str[j - 1] == '.')) && !(str[j] == 'e' && (prev_is_digit || str[j - 1] == '_')) && !(str[j] == '.' && (prev_is_digit || (!prev_is_binary_op && (j > 0 && (str[j - 1] == '_' || str[j - 1] == '-' || str[j - 1] == '+' || str[j - 1] == '~'))))) && !((str[j] == '-' || str[j] == '+' || str[j] == '~') && !is_binary_op && !prev_is_binary_op && str[j - 1] != 'e')) { /* This condition continues number highlighting in special cases. 1st row: '+' or '-' after scientific notation (like 3e-4); 2nd row: '_' as a numeric separator; 3rd row: Scientific notation 'e' and floating points; 4th row: Floating points inside the number, or leading if after a unary mathematical operator; 5th row: Multiple unary mathematical operators (like ~-7) */ in_number = false; } } else if (str[j] == '.' && !is_binary_op && is_digit(str[j + 1]) && (j == 0 || (j > 0 && str[j - 1] != '.'))) { // Start number highlighting from leading decimal points (like .42) in_number = true; } else if ((str[j] == '-' || str[j] == '+' || str[j] == '~') && !is_binary_op) { // Only start number highlighting on unary operators if a digit follows them. int non_op = j + 1; while (str[non_op] == '-' || str[non_op] == '+' || str[non_op] == '~') { non_op++; } if (is_digit(str[non_op]) || (str[non_op] == '.' && non_op < line_length && is_digit(str[non_op + 1]))) { in_number = true; } } if (!in_word && is_unicode_identifier_start(str[j]) && !in_number) { in_word = true; } if (is_a_symbol && str[j] != '.' && in_word) { in_word = false; } if (!in_keyword && is_char && !prev_is_char) { int to = j; while (to < line_length && !is_symbol(str[to])) { to++; } String word = str.substr(j, to - j); Color col; if (global_functions.has(word)) { // "assert" and "preload" are reserved, so highlight even if not followed by a bracket. if (word == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::ASSERT) || word == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::PRELOAD)) { col = global_function_color; } else { // For other global functions, check if followed by bracket. int k = to; while (k < line_length && is_whitespace(str[k])) { k++; } if (str[k] == '(') { col = global_function_color; } } } else if (class_names.has(word)) { col = class_names[word]; } else if (reserved_keywords.has(word)) { col = reserved_keywords[word]; // Don't highlight `list` as a type in `for elem: Type in list`. expect_type = false; } else if (member_keywords.has(word)) { col = member_keywords[word]; } if (col != Color()) { for (int k = j - 1; k >= 0; k--) { if (str[k] == '.') { col = Color(); // Keyword, member & global func indexing not allowed. break; } else if (str[k] > 32) { break; } } if (col != Color()) { in_keyword = true; keyword_color = col; } } } if (!in_function_name && in_word && !in_keyword) { if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::SIGNAL)) { in_signal_declaration = true; } else { int k = j; while (k < line_length && !is_symbol(str[k]) && !is_whitespace(str[k])) { k++; } // Check for space between name and bracket. while (k < line_length && is_whitespace(str[k])) { k++; } if (str[k] == '(') { in_function_name = true; if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) { in_function_declaration = true; } } else if (prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::VAR) || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FOR) || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::CONST)) { in_var_const_declaration = true; } // Check for lambda. if (in_function_declaration) { k = j - 1; while (k > 0 && is_whitespace(str[k])) { k--; } if (str[k] == ':') { in_lambda = true; } } } } if (!in_function_name && !in_member_variable && !in_keyword && !in_number && in_word) { int k = j; while (k > 0 && !is_symbol(str[k]) && !is_whitespace(str[k])) { k--; } if (str[k] == '.') { in_member_variable = true; } } if (is_a_symbol) { if (in_function_declaration || in_signal_declaration) { is_after_func_signal_declaration = true; } if (in_var_const_declaration) { is_after_var_const_declaration = true; } if (in_declaration_params > 0) { switch (str[j]) { case '(': in_declaration_params += 1; break; case ')': in_declaration_params -= 1; break; case '{': in_declaration_param_dicts += 1; break; case '}': in_declaration_param_dicts -= 1; break; } } else if ((is_after_func_signal_declaration || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) && str[j] == '(') { in_declaration_params = 1; in_declaration_param_dicts = 0; } if (expect_type) { switch (str[j]) { case '[': in_type_params += 1; break; case ']': in_type_params -= 1; break; case ',': if (in_type_params <= 0) { expect_type = false; } break; case ' ': case '\t': case '.': break; default: expect_type = false; break; } } else { if (j > 0 && str[j - 1] == '-' && str[j] == '>') { expect_type = true; in_type_params = 0; } if ((is_after_var_const_declaration || (in_declaration_params == 1 && in_declaration_param_dicts == 0)) && str[j] == ':') { expect_type = true; in_type_params = 0; } } in_function_name = false; in_function_declaration = false; in_signal_declaration = false; in_var_const_declaration = false; in_lambda = false; in_member_variable = false; if (!is_whitespace(str[j])) { is_after_func_signal_declaration = false; is_after_var_const_declaration = false; } } // Keep symbol color for binary '&&'. In the case of '&&&' use StringName color for the last ampersand. if (!in_string_name && in_region == -1 && str[j] == '&' && !is_binary_op) { if (j >= 2 && str[j - 1] == '&' && str[j - 2] != '&' && prev_is_binary_op) { is_binary_op = true; } else if (j == 0 || (j > 0 && str[j - 1] != '&') || prev_is_binary_op) { in_string_name = true; } } else if (in_region != -1 || is_a_symbol) { in_string_name = false; } // '^^' has no special meaning, so unlike StringName, when binary, use NodePath color for the last caret. if (!in_node_path && in_region == -1 && str[j] == '^' && !is_binary_op && (j == 0 || (j > 0 && str[j - 1] != '^') || prev_is_binary_op)) { in_node_path = true; } else if (in_region != -1 || is_a_symbol) { in_node_path = false; } if (!in_node_ref && in_region == -1 && (str[j] == '$' || (str[j] == '%' && !is_binary_op))) { in_node_ref = true; } else if (in_region != -1 || (is_a_symbol && str[j] != '/' && str[j] != '%') || (is_a_digit && j > 0 && (str[j - 1] == '$' || str[j - 1] == '/' || str[j - 1] == '%'))) { // NodeRefs can't start with digits, so point out wrong syntax immediately. in_node_ref = false; } if (!in_annotation && in_region == -1 && str[j] == '@') { in_annotation = true; } else if (in_region != -1 || is_a_symbol) { in_annotation = false; } const bool in_raw_string_prefix = in_region == -1 && str[j] == 'r' && j + 1 < line_length && (str[j + 1] == '"' || str[j + 1] == '\''); if (in_raw_string_prefix) { color = string_color; } else if (in_node_ref) { next_type = NODE_REF; color = node_ref_color; } else if (in_annotation) { next_type = ANNOTATION; color = annotation_color; } else if (in_string_name) { next_type = STRING_NAME; color = string_name_color; } else if (in_node_path) { next_type = NODE_PATH; color = node_path_color; } else if (in_keyword) { next_type = KEYWORD; color = keyword_color; } else if (in_signal_declaration) { next_type = SIGNAL; color = member_color; } else if (in_function_name) { next_type = FUNCTION; if (!in_lambda && in_function_declaration) { color = function_definition_color; } else { color = function_color; } } else if (in_number) { next_type = NUMBER; color = number_color; } else if (is_a_symbol) { next_type = SYMBOL; color = symbol_color; } else if (expect_type) { next_type = TYPE; color = type_color; } else if (in_member_variable) { next_type = MEMBER; color = member_color; } else { next_type = IDENTIFIER; } if (next_type != current_type) { if (current_type == NONE) { current_type = next_type; } else { prev_type = current_type; current_type = next_type; // No need to store regions... if (prev_type == REGION) { prev_text = ""; prev_column = j; } else { String text = str.substr(prev_column, j - prev_column).strip_edges(); prev_column = j; // Ignore if just whitespace. if (!text.is_empty()) { prev_text = text; } } } } prev_is_char = is_char; prev_is_digit = is_a_digit; prev_is_binary_op = is_binary_op; if (color != prev_color) { prev_color = color; highlighter_info["color"] = color; color_map[j] = highlighter_info; } } return color_map; } String GDScriptSyntaxHighlighter::_get_name() const { return "GDScript"; } PackedStringArray GDScriptSyntaxHighlighter::_get_supported_languages() const { PackedStringArray languages; languages.push_back("GDScript"); return languages; } void GDScriptSyntaxHighlighter::_update_cache() { class_names.clear(); reserved_keywords.clear(); member_keywords.clear(); global_functions.clear(); color_regions.clear(); color_region_cache.clear(); font_color = text_edit->get_theme_color(SceneStringName(font_color)); symbol_color = EDITOR_GET("text_editor/theme/highlighting/symbol_color"); function_color = EDITOR_GET("text_editor/theme/highlighting/function_color"); number_color = EDITOR_GET("text_editor/theme/highlighting/number_color"); member_color = EDITOR_GET("text_editor/theme/highlighting/member_variable_color"); /* Engine types. */ const Color types_color = EDITOR_GET("text_editor/theme/highlighting/engine_type_color"); List types; ClassDB::get_class_list(&types); for (const StringName &E : types) { if (ClassDB::is_class_exposed(E)) { class_names[E] = types_color; } } /* User types. */ const Color usertype_color = EDITOR_GET("text_editor/theme/highlighting/user_type_color"); List global_classes; ScriptServer::get_global_class_list(&global_classes); for (const StringName &E : global_classes) { class_names[E] = usertype_color; } /* Autoloads. */ for (const KeyValue &E : ProjectSettings::get_singleton()->get_autoload_list()) { const ProjectSettings::AutoloadInfo &info = E.value; if (info.is_singleton) { class_names[info.name] = usertype_color; } } const GDScriptLanguage *gdscript = GDScriptLanguage::get_singleton(); /* Core types. */ const Color basetype_color = EDITOR_GET("text_editor/theme/highlighting/base_type_color"); List core_types; gdscript->get_core_type_words(&core_types); for (const String &E : core_types) { class_names[StringName(E)] = basetype_color; } class_names[SNAME("Variant")] = basetype_color; class_names[SNAME("void")] = basetype_color; // `get_core_type_words()` doesn't return primitive types. class_names[SNAME("bool")] = basetype_color; class_names[SNAME("int")] = basetype_color; class_names[SNAME("float")] = basetype_color; /* Reserved words. */ const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color"); const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color"); List keyword_list; gdscript->get_reserved_words(&keyword_list); for (const String &E : keyword_list) { if (gdscript->is_control_flow_keyword(E)) { reserved_keywords[StringName(E)] = control_flow_keyword_color; } else { reserved_keywords[StringName(E)] = keyword_color; } } // Highlight `set` and `get` as "keywords" with the function color to avoid conflicts with method calls. reserved_keywords[SNAME("set")] = function_color; reserved_keywords[SNAME("get")] = function_color; /* Global functions. */ List global_function_list; GDScriptUtilityFunctions::get_function_list(&global_function_list); Variant::get_utility_function_list(&global_function_list); // "assert" and "preload" are not utility functions, but are global nonetheless, so insert them. global_functions.insert(SNAME("assert")); global_functions.insert(SNAME("preload")); for (const StringName &E : global_function_list) { global_functions.insert(E); } /* Comments */ const Color comment_color = EDITOR_GET("text_editor/theme/highlighting/comment_color"); List comments; gdscript->get_comment_delimiters(&comments); for (const String &comment : comments) { String beg = comment.get_slice(" ", 0); String end = comment.get_slice_count(" ") > 1 ? comment.get_slice(" ", 1) : String(); add_color_region(ColorRegion::TYPE_COMMENT, beg, end, comment_color, end.is_empty()); } /* Doc comments */ const Color doc_comment_color = EDITOR_GET("text_editor/theme/highlighting/doc_comment_color"); List doc_comments; gdscript->get_doc_comment_delimiters(&doc_comments); for (const String &doc_comment : doc_comments) { String beg = doc_comment.get_slice(" ", 0); String end = doc_comment.get_slice_count(" ") > 1 ? doc_comment.get_slice(" ", 1) : String(); add_color_region(ColorRegion::TYPE_COMMENT, beg, end, doc_comment_color, end.is_empty()); } /* Code regions */ const Color code_region_color = Color(EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color").operator Color(), 1.0); add_color_region(ColorRegion::TYPE_CODE_REGION, "#region", "", code_region_color, true); add_color_region(ColorRegion::TYPE_CODE_REGION, "#endregion", "", code_region_color, true); /* Strings */ string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); add_color_region(ColorRegion::TYPE_STRING, "\"", "\"", string_color); add_color_region(ColorRegion::TYPE_STRING, "'", "'", string_color); add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "\"\"\"", "\"\"\"", string_color); add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "'''", "'''", string_color); add_color_region(ColorRegion::TYPE_STRING, "\"", "\"", string_color, false, true); add_color_region(ColorRegion::TYPE_STRING, "'", "'", string_color, false, true); add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "\"\"\"", "\"\"\"", string_color, false, true); add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "'''", "'''", string_color, false, true); const Ref