diff options
74 files changed, 2677 insertions, 539 deletions
diff --git a/core/typedefs.h b/core/typedefs.h index 1dcba58188..24c247fd38 100644 --- a/core/typedefs.h +++ b/core/typedefs.h @@ -109,7 +109,7 @@ constexpr T ABS(T m_v) { template <typename T> constexpr const T SIGN(const T m_v) { - return m_v == 0 ? 0.0f : (m_v < 0 ? -1.0f : +1.0f); + return m_v > 0 ? +1.0f : (m_v < 0 ? -1.0f : 0.0f); } template <typename T, typename T2> diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 3173e46be8..1b43f13e37 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1168,11 +1168,13 @@ <return type="Variant" /> <param index="0" name="x" type="Variant" /> <description> - Returns the same type of [Variant] as [param x], with [code]-1[/code] for negative values, [code]1[/code] for positive values, and [code]0[/code] for zeros. Supported types: [int], [float], [Vector2], [Vector2i], [Vector3], [Vector3i], [Vector4], [Vector4i]. + Returns the same type of [Variant] as [param x], with [code]-1[/code] for negative values, [code]1[/code] for positive values, and [code]0[/code] for zeros. For [code]nan[/code] values it returns 0. + Supported types: [int], [float], [Vector2], [Vector2i], [Vector3], [Vector3i], [Vector4], [Vector4i]. [codeblock] sign(-6.0) # Returns -1 sign(0.0) # Returns 0 sign(6.0) # Returns 1 + sign(NAN) # Returns 0 sign(Vector3(-6.0, 0.0, 6.0)) # Returns (-1, 0, 1) [/codeblock] @@ -1183,11 +1185,12 @@ <return type="float" /> <param index="0" name="x" type="float" /> <description> - Returns [code]-1.0[/code] if [param x] is negative, [code]1.0[/code] if [param x] is positive, and [code]0.0[/code] if [param x] is zero. + Returns [code]-1.0[/code] if [param x] is negative, [code]1.0[/code] if [param x] is positive, and [code]0.0[/code] if [param x] is zero. For [code]nan[/code] values of [param x] it returns 0.0. [codeblock] signf(-6.5) # Returns -1.0 signf(0.0) # Returns 0.0 signf(6.5) # Returns 1.0 + signf(NAN) # Returns 0.0 [/codeblock] </description> </method> diff --git a/doc/classes/CodeEdit.xml b/doc/classes/CodeEdit.xml index 0e829127f2..d9146cf604 100644 --- a/doc/classes/CodeEdit.xml +++ b/doc/classes/CodeEdit.xml @@ -137,6 +137,15 @@ Values of [code]-1[/code] convert the entire text. </description> </method> + <method name="create_code_region"> + <return type="void" /> + <description> + Creates a new code region with the selection. At least one single line comment delimiter have to be defined (see [method add_comment_delimiter]). + A code region is a part of code that is highlighted when folded and can help organize your script. + Code region start and end tags can be customized (see [method set_code_region_tags]). + Code regions are delimited using start and end tags (respectively [code]region[/code] and [code]endregion[/code] by default) preceded by one line comment delimiter. (eg. [code]#region[/code] and [code]#endregion[/code]) + </description> + </method> <method name="do_indent"> <return type="void" /> <description> @@ -200,6 +209,18 @@ Gets the index of the current selected completion option. </description> </method> + <method name="get_code_region_end_tag" qualifiers="const"> + <return type="String" /> + <description> + Returns the code region end tag (without comment delimiter). + </description> + </method> + <method name="get_code_region_start_tag" qualifiers="const"> + <return type="String" /> + <description> + Returns the code region start tag (without comment delimiter). + </description> + </method> <method name="get_delimiter_end_key" qualifiers="const"> <return type="String" /> <param index="0" name="delimiter_index" type="int" /> @@ -326,6 +347,20 @@ Returns whether the line at the specified index is breakpointed or not. </description> </method> + <method name="is_line_code_region_end" qualifiers="const"> + <return type="bool" /> + <param index="0" name="line" type="int" /> + <description> + Returns whether the line at the specified index is a code region end. + </description> + </method> + <method name="is_line_code_region_start" qualifiers="const"> + <return type="bool" /> + <param index="0" name="line" type="int" /> + <description> + Returns whether the line at the specified index is a code region start. + </description> + </method> <method name="is_line_executing" qualifiers="const"> <return type="bool" /> <param index="0" name="line" type="int" /> @@ -382,6 +417,14 @@ Sets if the code hint should draw below the text. </description> </method> + <method name="set_code_region_tags"> + <return type="void" /> + <param index="0" name="start" type="String" default=""region"" /> + <param index="1" name="end" type="String" default=""endregion"" /> + <description> + Sets the code region start and end tags (without comment delimiter). + </description> + </method> <method name="set_line_as_bookmarked"> <return type="void" /> <param index="0" name="line" type="int" /> @@ -629,6 +672,9 @@ <theme_item name="executing_line_color" data_type="color" type="Color" default="Color(0.98, 0.89, 0.27, 1)"> [Color] of the executing icon for executing lines. </theme_item> + <theme_item name="folded_code_region_color" data_type="color" type="Color" default="Color(0.68, 0.46, 0.77, 0.2)"> + [Color] of background line highlight for folded code region. + </theme_item> <theme_item name="font_color" data_type="color" type="Color" default="Color(0.875, 0.875, 0.875, 1)"> Sets the font [Color]. </theme_item> @@ -693,12 +739,18 @@ <theme_item name="can_fold" data_type="icon" type="Texture2D"> Sets a custom [Texture2D] to draw in the line folding gutter when a line can be folded. </theme_item> + <theme_item name="can_fold_code_region" data_type="icon" type="Texture2D"> + Sets a custom [Texture2D] to draw in the line folding gutter when a code region can be folded. + </theme_item> <theme_item name="executing_line" data_type="icon" type="Texture2D"> Icon to draw in the executing gutter for executing lines. </theme_item> <theme_item name="folded" data_type="icon" type="Texture2D"> Sets a custom [Texture2D] to draw in the line folding gutter when a line is folded and can be unfolded. </theme_item> + <theme_item name="folded_code_region" data_type="icon" type="Texture2D"> + Sets a custom [Texture2D] to draw in the line folding gutter when a code region is folded and can be unfolded. + </theme_item> <theme_item name="folded_eol_icon" data_type="icon" type="Texture2D"> Sets a custom [Texture2D] to draw at the end of a folded line. </theme_item> diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 44b2d72ebe..e1035a55c8 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -1858,16 +1858,16 @@ [b]Note:[/b] This flag is implemented only on macOS. </constant> <constant name="VSYNC_DISABLED" value="0" enum="VSyncMode"> - No vertical synchronization, which means the engine will display frames as fast as possible (tearing may be visible). Framerate is unlimited (nonwithstanding [member Engine.max_fps]). + No vertical synchronization, which means the engine will display frames as fast as possible (tearing may be visible). Framerate is unlimited (notwithstanding [member Engine.max_fps]). </constant> <constant name="VSYNC_ENABLED" value="1" enum="VSyncMode"> - Default vertical synchronization mode, the image is displayed only on vertical blanking intervals (no tearing is visible). Framerate is limited by the monitor refresh rate (nonwithstanding [member Engine.max_fps]). + Default vertical synchronization mode, the image is displayed only on vertical blanking intervals (no tearing is visible). Framerate is limited by the monitor refresh rate (notwithstanding [member Engine.max_fps]). </constant> <constant name="VSYNC_ADAPTIVE" value="2" enum="VSyncMode"> - Behaves like [constant VSYNC_DISABLED] when the framerate drops below the screen's refresh rate to reduce stuttering (tearing may be visible). Otherwise, vertical synchronization is enabled to avoid tearing. Framerate is limited by the monitor refresh rate (nonwithstanding [member Engine.max_fps]). Behaves like [constant VSYNC_ENABLED] when using the Compatibility rendering method. + Behaves like [constant VSYNC_DISABLED] when the framerate drops below the screen's refresh rate to reduce stuttering (tearing may be visible). Otherwise, vertical synchronization is enabled to avoid tearing. Framerate is limited by the monitor refresh rate (notwithstanding [member Engine.max_fps]). Behaves like [constant VSYNC_ENABLED] when using the Compatibility rendering method. </constant> <constant name="VSYNC_MAILBOX" value="3" enum="VSyncMode"> - Displays the most recent image in the queue on vertical blanking intervals, while rendering to the other images (no tearing is visible). Framerate is unlimited (nonwithstanding [member Engine.max_fps]). + Displays the most recent image in the queue on vertical blanking intervals, while rendering to the other images (no tearing is visible). Framerate is unlimited (notwithstanding [member Engine.max_fps]). Although not guaranteed, the images can be rendered as fast as possible, which may reduce input lag (also called "Fast" V-Sync mode). [constant VSYNC_MAILBOX] works best when at least twice as many frames as the display refresh rate are rendered. Behaves like [constant VSYNC_ENABLED] when using the Compatibility rendering method. </constant> <constant name="DISPLAY_HANDLE" value="0" enum="HandleType"> diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index aa3bf3c535..5a0cb9fc5e 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -959,6 +959,9 @@ <member name="text_editor/theme/highlighting/executing_line_color" type="Color" setter="" getter=""> The script editor's color for the debugger's executing line icon (displayed in the gutter). </member> + <member name="text_editor/theme/highlighting/folded_code_region_color" type="Color" setter="" getter=""> + The script editor's background line highlighting color for folded code region. + </member> <member name="text_editor/theme/highlighting/function_color" type="Color" setter="" getter=""> The script editor's function call color. [b]Note:[/b] When using the GDScript syntax highlighter, this is replaced by the function definition color configured in the syntax theme for function definitions (e.g. [code]func _ready():[/code]). diff --git a/doc/classes/LightmapGI.xml b/doc/classes/LightmapGI.xml index a626c71377..2b2fca7d35 100644 --- a/doc/classes/LightmapGI.xml +++ b/doc/classes/LightmapGI.xml @@ -118,6 +118,9 @@ <constant name="BAKE_ERROR_USER_ABORTED" value="8" enum="BakeError"> The user aborted the lightmap baking operation (typically by clicking the [b]Cancel[/b] button in the progress dialog). </constant> + <constant name="BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL" value="9" enum="BakeError"> + Lightmap baking failed as the maximum texture size is too small to fit some of the meshes marked for baking. + </constant> <constant name="ENVIRONMENT_MODE_DISABLED" value="0" enum="EnvironmentMode"> Ignore environment lighting when baking lightmaps. </constant> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index e8a440b76f..d3b124dcde 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -549,6 +549,9 @@ <member name="debug/gdscript/warnings/unsafe_void_return" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when returning a call from a [code]void[/code] function when such call cannot be guaranteed to be also [code]void[/code]. </member> + <member name="debug/gdscript/warnings/untyped_declaration" type="int" setter="" getter="" default="0"> + When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a variable or parameter has no static type, or if a function has no static return type. + </member> <member name="debug/gdscript/warnings/unused_local_constant" type="int" setter="" getter="" default="1"> When set to [code]warn[/code] or [code]error[/code], produces a warning or an error respectively when a local constant is never used. </member> diff --git a/doc/classes/TileSetAtlasSource.xml b/doc/classes/TileSetAtlasSource.xml index 1623cd87ee..f478f37500 100644 --- a/doc/classes/TileSetAtlasSource.xml +++ b/doc/classes/TileSetAtlasSource.xml @@ -287,5 +287,20 @@ <constant name="TILE_ANIMATION_MODE_MAX" value="2" enum="TileAnimationMode"> Represents the size of the [enum TileAnimationMode] enum. </constant> + <constant name="TRANSFORM_FLIP_H" value="4096"> + Represents cell's horizontal flip flag. Should be used directly with [TileMap] to flip placed tiles by altering their alternative IDs. + [codeblock] + var alternate_id = $TileMap.get_cell_alternative_tile(0, Vector2i(2, 2)) + if not alternate_id & TileSetAtlasSource.TRANSFORM_FLIP_H: + # If tile is not already flipped, flip it. + $TileMap.set_cell(0, Vector2i(2, 2), source_id, atlas_coords, alternate_id | TileSetAtlasSource.TRANSFORM_FLIP_H) + [/codeblock] + </constant> + <constant name="TRANSFORM_FLIP_V" value="8192"> + Represents cell's vertical flip flag. See [constant TRANSFORM_FLIP_H] for usage. + </constant> + <constant name="TRANSFORM_TRANSPOSE" value="16384"> + Represents cell's transposed flag. See [constant TRANSFORM_FLIP_H] for usage. + </constant> </constants> </class> diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index e042d8570f..a1504d1ac0 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -531,7 +531,7 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) { _initial_set("docks/filesystem/textfile_extensions", "txt,md,cfg,ini,log,json,yml,yaml,toml"); // Property editor - _initial_set("docks/property_editor/auto_refresh_interval", 0.2); //update 5 times per second by default + EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "docks/property_editor/auto_refresh_interval", 0.2, "0.01,1,0.001"); // Update 5 times per second by default. EDITOR_SETTING(Variant::FLOAT, PROPERTY_HINT_RANGE, "docks/property_editor/subresource_hue_tint", 0.75, "0,1,0.01") /* Text editor */ @@ -848,6 +848,7 @@ void EditorSettings::_load_godot2_text_editor_theme() { _initial_set("text_editor/theme/highlighting/breakpoint_color", Color(0.9, 0.29, 0.3)); _initial_set("text_editor/theme/highlighting/executing_line_color", Color(0.98, 0.89, 0.27)); _initial_set("text_editor/theme/highlighting/code_folding_color", Color(0.8, 0.8, 0.8, 0.8)); + _initial_set("text_editor/theme/highlighting/folded_code_region_color", Color(0.68, 0.46, 0.77, 0.2)); _initial_set("text_editor/theme/highlighting/search_result_color", Color(0.05, 0.25, 0.05, 1)); _initial_set("text_editor/theme/highlighting/search_result_border_color", Color(0.41, 0.61, 0.91, 0.38)); } diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index 311e532e63..54fd8d63af 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -208,6 +208,8 @@ void EditorColorMap::create() { add_conversion_exception("GuiSpace"); add_conversion_exception("CodeFoldedRightArrow"); add_conversion_exception("CodeFoldDownArrow"); + add_conversion_exception("CodeRegionFoldedRightArrow"); + add_conversion_exception("CodeRegionFoldDownArrow"); add_conversion_exception("TextEditorPlay"); add_conversion_exception("Breakpoint"); } @@ -2088,6 +2090,7 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { const Color breakpoint_color = dark_theme ? error_color : Color(1, 0.27, 0.2, 1); const Color executing_line_color = Color(0.98, 0.89, 0.27); const Color code_folding_color = alpha3; + const Color folded_code_region_color = Color(0.68, 0.46, 0.77, 0.2); const Color search_result_color = alpha1; const Color search_result_border_color = dark_theme ? Color(0.41, 0.61, 0.91, 0.38) : Color(0, 0.4, 1, 0.38); @@ -2128,6 +2131,7 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { setting->set_initial_value("text_editor/theme/highlighting/breakpoint_color", breakpoint_color, true); setting->set_initial_value("text_editor/theme/highlighting/executing_line_color", executing_line_color, true); setting->set_initial_value("text_editor/theme/highlighting/code_folding_color", code_folding_color, true); + setting->set_initial_value("text_editor/theme/highlighting/folded_code_region_color", folded_code_region_color, true); setting->set_initial_value("text_editor/theme/highlighting/search_result_color", search_result_color, true); setting->set_initial_value("text_editor/theme/highlighting/search_result_border_color", search_result_border_color, true); } else if (text_editor_color_theme == "Godot 2") { @@ -2147,6 +2151,8 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_icon("space", "CodeEdit", theme->get_icon(SNAME("GuiSpace"), EditorStringName(EditorIcons))); theme->set_icon("folded", "CodeEdit", theme->get_icon(SNAME("CodeFoldedRightArrow"), EditorStringName(EditorIcons))); theme->set_icon("can_fold", "CodeEdit", theme->get_icon(SNAME("CodeFoldDownArrow"), EditorStringName(EditorIcons))); + theme->set_icon("folded_code_region", "CodeEdit", theme->get_icon(SNAME("CodeRegionFoldedRightArrow"), EditorStringName(EditorIcons))); + theme->set_icon("can_fold_code_region", "CodeEdit", theme->get_icon(SNAME("CodeRegionFoldDownArrow"), EditorStringName(EditorIcons))); theme->set_icon("executing_line", "CodeEdit", theme->get_icon(SNAME("TextEditorPlay"), EditorStringName(EditorIcons))); theme->set_icon("breakpoint", "CodeEdit", theme->get_icon(SNAME("Breakpoint"), EditorStringName(EditorIcons))); @@ -2172,6 +2178,7 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) { theme->set_color("breakpoint_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/breakpoint_color")); theme->set_color("executing_line_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/executing_line_color")); theme->set_color("code_folding_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/code_folding_color")); + theme->set_color("folded_code_region_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color")); theme->set_color("search_result_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_color")); theme->set_color("search_result_border_color", "CodeEdit", EDITOR_GET("text_editor/theme/highlighting/search_result_border_color")); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index cef5cdb4a9..22750d6d81 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -1818,7 +1818,6 @@ void FileSystemDock::_rename_operation_confirm() { _rescan(); print_verbose("FileSystem: saving moved scenes."); - _save_scenes_after_move(file_renames); current_path = new_path; current_path_line_edit->set_text(current_path); diff --git a/editor/icons/CodeRegionFoldDownArrow.svg b/editor/icons/CodeRegionFoldDownArrow.svg new file mode 100644 index 0000000000..3bc4f3f73b --- /dev/null +++ b/editor/icons/CodeRegionFoldDownArrow.svg @@ -0,0 +1 @@ +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm1 5a1 1 0 0 1 1.414-1.414L6 6.172l1.586-1.586A1 1 0 0 1 9 6L6.707 8.293a1 1 0 0 1-1.414 0Z" fill="#fff"/></svg>
\ No newline at end of file diff --git a/editor/icons/CodeRegionFoldedRightArrow.svg b/editor/icons/CodeRegionFoldedRightArrow.svg new file mode 100644 index 0000000000..a9b81d54f3 --- /dev/null +++ b/editor/icons/CodeRegionFoldedRightArrow.svg @@ -0,0 +1 @@ +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm3.5 8a1 1 0 0 1-1.414-1.414L5.672 6 4.086 4.414A1 1 0 0 1 5.5 3l2.293 2.293a1 1 0 0 1 0 1.414Z" fill="#fff"/></svg>
\ No newline at end of file diff --git a/editor/plugins/canvas_item_editor_plugin.cpp b/editor/plugins/canvas_item_editor_plugin.cpp index 703cd7ef81..e544ff325a 100644 --- a/editor/plugins/canvas_item_editor_plugin.cpp +++ b/editor/plugins/canvas_item_editor_plugin.cpp @@ -78,7 +78,8 @@ class SnapDialog : public ConfirmationDialog { SpinBox *grid_offset_y; SpinBox *grid_step_x; SpinBox *grid_step_y; - SpinBox *primary_grid_steps; + SpinBox *primary_grid_step_x; + SpinBox *primary_grid_step_y; SpinBox *rotation_offset; SpinBox *rotation_step; SpinBox *scale_step; @@ -151,24 +152,30 @@ public: grid_step_y->set_select_all_on_focus(true); child_container->add_child(grid_step_y); - child_container = memnew(GridContainer); - child_container->set_columns(2); - container->add_child(child_container); - label = memnew(Label); label->set_text(TTR("Primary Line Every:")); label->set_h_size_flags(Control::SIZE_EXPAND_FILL); child_container->add_child(label); - primary_grid_steps = memnew(SpinBox); - primary_grid_steps->set_min(0); - primary_grid_steps->set_step(1); - primary_grid_steps->set_max(100); - primary_grid_steps->set_allow_greater(true); - primary_grid_steps->set_suffix(TTR("steps")); - primary_grid_steps->set_h_size_flags(Control::SIZE_EXPAND_FILL); - primary_grid_steps->set_select_all_on_focus(true); - child_container->add_child(primary_grid_steps); + primary_grid_step_x = memnew(SpinBox); + primary_grid_step_x->set_min(1); + primary_grid_step_x->set_step(1); + primary_grid_step_x->set_max(SPIN_BOX_GRID_RANGE); + primary_grid_step_x->set_allow_greater(true); + primary_grid_step_x->set_suffix("steps"); + primary_grid_step_x->set_h_size_flags(Control::SIZE_EXPAND_FILL); + primary_grid_step_x->set_select_all_on_focus(true); + child_container->add_child(primary_grid_step_x); + + primary_grid_step_y = memnew(SpinBox); + primary_grid_step_y->set_min(1); + primary_grid_step_y->set_step(1); + primary_grid_step_y->set_max(SPIN_BOX_GRID_RANGE); + primary_grid_step_y->set_allow_greater(true); + primary_grid_step_y->set_suffix("steps"); + primary_grid_step_y->set_h_size_flags(Control::SIZE_EXPAND_FILL); + primary_grid_step_y->set_select_all_on_focus(true); + child_container->add_child(primary_grid_step_y); container->add_child(memnew(HSeparator)); @@ -224,21 +231,22 @@ public: child_container->add_child(scale_step); } - void set_fields(const Point2 p_grid_offset, const Point2 p_grid_step, const int p_primary_grid_steps, const real_t p_rotation_offset, const real_t p_rotation_step, const real_t p_scale_step) { + void set_fields(const Point2 p_grid_offset, const Point2 p_grid_step, const Vector2i p_primary_grid_step, const real_t p_rotation_offset, const real_t p_rotation_step, const real_t p_scale_step) { grid_offset_x->set_value(p_grid_offset.x); grid_offset_y->set_value(p_grid_offset.y); grid_step_x->set_value(p_grid_step.x); grid_step_y->set_value(p_grid_step.y); - primary_grid_steps->set_value(p_primary_grid_steps); + primary_grid_step_x->set_value(p_primary_grid_step.x); + primary_grid_step_y->set_value(p_primary_grid_step.y); rotation_offset->set_value(Math::rad_to_deg(p_rotation_offset)); rotation_step->set_value(Math::rad_to_deg(p_rotation_step)); scale_step->set_value(p_scale_step); } - void get_fields(Point2 &p_grid_offset, Point2 &p_grid_step, int &p_primary_grid_steps, real_t &p_rotation_offset, real_t &p_rotation_step, real_t &p_scale_step) { + void get_fields(Point2 &p_grid_offset, Point2 &p_grid_step, Vector2i &p_primary_grid_step, real_t &p_rotation_offset, real_t &p_rotation_step, real_t &p_scale_step) { p_grid_offset = Point2(grid_offset_x->get_value(), grid_offset_y->get_value()); p_grid_step = Point2(grid_step_x->get_value(), grid_step_y->get_value()); - p_primary_grid_steps = int(primary_grid_steps->get_value()); + p_primary_grid_step = Vector2i(primary_grid_step_x->get_value(), primary_grid_step_y->get_value()); p_rotation_offset = Math::deg_to_rad(rotation_offset->get_value()); p_rotation_step = Math::deg_to_rad(rotation_step->get_value()); p_scale_step = scale_step->get_value(); @@ -894,11 +902,11 @@ void CanvasItemEditor::_commit_canvas_item_state(List<CanvasItem *> p_canvas_ite } void CanvasItemEditor::_snap_changed() { - static_cast<SnapDialog *>(snap_dialog)->get_fields(grid_offset, grid_step, primary_grid_steps, snap_rotation_offset, snap_rotation_step, snap_scale_step); + static_cast<SnapDialog *>(snap_dialog)->get_fields(grid_offset, grid_step, primary_grid_step, snap_rotation_offset, snap_rotation_step, snap_scale_step); EditorSettings::get_singleton()->set_project_metadata("2d_editor", "grid_offset", grid_offset); EditorSettings::get_singleton()->set_project_metadata("2d_editor", "grid_step", grid_step); - EditorSettings::get_singleton()->set_project_metadata("2d_editor", "primary_grid_steps", primary_grid_steps); + EditorSettings::get_singleton()->set_project_metadata("2d_editor", "primary_grid_step", primary_grid_step); EditorSettings::get_singleton()->set_project_metadata("2d_editor", "snap_rotation_offset", snap_rotation_offset); EditorSettings::get_singleton()->set_project_metadata("2d_editor", "snap_rotation_step", snap_rotation_step); EditorSettings::get_singleton()->set_project_metadata("2d_editor", "snap_scale_step", snap_scale_step); @@ -2925,10 +2933,10 @@ void CanvasItemEditor::_draw_grid() { if (last_cell != cell) { Color grid_color; - if (primary_grid_steps == 0) { + if (primary_grid_step.x <= 1) { grid_color = secondary_grid_color; } else { - grid_color = cell % primary_grid_steps == 0 ? primary_grid_color : secondary_grid_color; + grid_color = cell % primary_grid_step.x == 0 ? primary_grid_color : secondary_grid_color; } viewport->draw_line(Point2(i, 0), Point2(i, viewport_size.height), grid_color, Math::round(EDSCALE)); @@ -2948,10 +2956,10 @@ void CanvasItemEditor::_draw_grid() { if (last_cell != cell) { Color grid_color; - if (primary_grid_steps == 0) { + if (primary_grid_step.y <= 1) { grid_color = secondary_grid_color; } else { - grid_color = cell % primary_grid_steps == 0 ? primary_grid_color : secondary_grid_color; + grid_color = cell % primary_grid_step.y == 0 ? primary_grid_color : secondary_grid_color; } viewport->draw_line(Point2(0, i), Point2(viewport_size.width, i), grid_color, Math::round(EDSCALE)); @@ -4341,8 +4349,8 @@ void CanvasItemEditor::_popup_callback(int p_op) { snap_config_menu->get_popup()->set_item_checked(idx, snap_pixel); } break; case SNAP_CONFIGURE: { - static_cast<SnapDialog *>(snap_dialog)->set_fields(grid_offset, grid_step, primary_grid_steps, snap_rotation_offset, snap_rotation_step, snap_scale_step); - snap_dialog->popup_centered(Size2(220, 160) * EDSCALE); + static_cast<SnapDialog *>(snap_dialog)->set_fields(grid_offset, grid_step, primary_grid_step, snap_rotation_offset, snap_rotation_step, snap_scale_step); + snap_dialog->popup_centered(Size2(320, 160) * EDSCALE); } break; case SKELETON_SHOW_BONES: { List<Node *> selection = editor_selection->get_selected_node_list(); @@ -4728,7 +4736,7 @@ Dictionary CanvasItemEditor::get_state() const { state["ofs"] = view_offset; state["grid_offset"] = grid_offset; state["grid_step"] = grid_step; - state["primary_grid_steps"] = primary_grid_steps; + state["primary_grid_step"] = primary_grid_step; state["snap_rotation_offset"] = snap_rotation_offset; state["snap_rotation_step"] = snap_rotation_step; state["snap_scale_step"] = snap_scale_step; @@ -4780,8 +4788,15 @@ void CanvasItemEditor::set_state(const Dictionary &p_state) { grid_step = state["grid_step"]; } +#ifndef DISABLE_DEPRECATED if (state.has("primary_grid_steps")) { - primary_grid_steps = state["primary_grid_steps"]; + primary_grid_step.x = state["primary_grid_steps"]; + primary_grid_step.y = state["primary_grid_steps"]; + } +#endif // DISABLE_DEPRECATED + + if (state.has("primary_grid_step")) { + primary_grid_step = state["primary_grid_step"]; } if (state.has("snap_rotation_step")) { @@ -4934,7 +4949,7 @@ void CanvasItemEditor::clear() { grid_offset = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "grid_offset", Vector2()); grid_step = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "grid_step", Vector2(8, 8)); - primary_grid_steps = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "primary_grid_steps", 8); + primary_grid_step = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "primary_grid_step", Vector2i(8, 8)); snap_rotation_step = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "snap_rotation_step", Math::deg_to_rad(15.0)); snap_rotation_offset = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "snap_rotation_offset", 0.0); snap_scale_step = EditorSettings::get_singleton()->get_project_metadata("2d_editor", "snap_scale_step", 0.1); diff --git a/editor/plugins/canvas_item_editor_plugin.h b/editor/plugins/canvas_item_editor_plugin.h index 674f38c8c0..06f91be081 100644 --- a/editor/plugins/canvas_item_editor_plugin.h +++ b/editor/plugins/canvas_item_editor_plugin.h @@ -216,7 +216,7 @@ private: // Defaults are defined in clear(). Point2 grid_offset; Point2 grid_step; - int primary_grid_steps = 0; + Vector2i primary_grid_step; int grid_step_multiplier = 0; real_t snap_rotation_step = 0.0; diff --git a/editor/plugins/lightmap_gi_editor_plugin.cpp b/editor/plugins/lightmap_gi_editor_plugin.cpp index db5593a132..efd11cfab9 100644 --- a/editor/plugins/lightmap_gi_editor_plugin.cpp +++ b/editor/plugins/lightmap_gi_editor_plugin.cpp @@ -103,6 +103,9 @@ void LightmapGIEditorPlugin::_bake_select_file(const String &p_file) { case LightmapGI::BAKE_ERROR_FOREIGN_DATA: { EditorNode::get_singleton()->show_warning(TTR("Lightmap data is not local to the scene.")); } break; + case LightmapGI::BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL: { + EditorNode::get_singleton()->show_warning(TTR("Maximum texture size is too small for the lightmap images.")); + } break; default: { } break; } diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index 17c9a097ba..ca13a061bb 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -4957,7 +4957,7 @@ void Node3DEditorViewport::update_transform_numeric() { apply_transform(motion, extra); } -// Perform cleanup after a transform operation is committed or cancelled. +// Perform cleanup after a transform operation is committed or canceled. void Node3DEditorViewport::finish_transform() { _edit.mode = TRANSFORM_NONE; _edit.instant = false; diff --git a/editor/plugins/script_text_editor.cpp b/editor/plugins/script_text_editor.cpp index 511e4dfd15..8132fd8e9b 100644 --- a/editor/plugins/script_text_editor.cpp +++ b/editor/plugins/script_text_editor.cpp @@ -181,10 +181,12 @@ void ScriptTextEditor::_load_theme_settings() { Color updated_marked_line_color = EDITOR_GET("text_editor/theme/highlighting/mark_color"); Color updated_safe_line_number_color = EDITOR_GET("text_editor/theme/highlighting/safe_line_number_color"); + Color updated_folded_code_region_color = EDITOR_GET("text_editor/theme/highlighting/folded_code_region_color"); bool safe_line_number_color_updated = updated_safe_line_number_color != safe_line_number_color; bool marked_line_color_updated = updated_marked_line_color != marked_line_color; - if (safe_line_number_color_updated || marked_line_color_updated) { + bool folded_code_region_color_updated = updated_folded_code_region_color != folded_code_region_color; + if (safe_line_number_color_updated || marked_line_color_updated || folded_code_region_color_updated) { safe_line_number_color = updated_safe_line_number_color; for (int i = 0; i < text_edit->get_line_count(); i++) { if (marked_line_color_updated && text_edit->get_line_background_color(i) == marked_line_color) { @@ -194,8 +196,13 @@ void ScriptTextEditor::_load_theme_settings() { if (safe_line_number_color_updated && text_edit->get_line_gutter_item_color(i, line_number_gutter) != default_line_number_color) { text_edit->set_line_gutter_item_color(i, line_number_gutter, safe_line_number_color); } + + if (folded_code_region_color_updated && text_edit->get_line_background_color(i) == folded_code_region_color) { + text_edit->set_line_background_color(i, updated_folded_code_region_color); + } } marked_line_color = updated_marked_line_color; + folded_code_region_color = updated_folded_code_region_color; } theme_loaded = true; @@ -647,7 +654,8 @@ void ScriptTextEditor::_update_errors() { bool last_is_safe = false; for (int i = 0; i < te->get_line_count(); i++) { if (errors.is_empty()) { - te->set_line_background_color(i, Color(0, 0, 0, 0)); + bool is_folded_code_region = te->is_line_code_region_start(i) && te->is_line_folded(i); + te->set_line_background_color(i, is_folded_code_region ? folded_code_region_color : Color(0, 0, 0, 0)); } else { for (const ScriptLanguage::ScriptError &E : errors) { bool error_line = i == E.line - 1; @@ -1312,6 +1320,9 @@ void ScriptTextEditor::_edit_option(int p_op) { tx->unfold_all_lines(); tx->queue_redraw(); } break; + case EDIT_CREATE_CODE_REGION: { + tx->create_code_region(); + } break; case EDIT_TOGGLE_COMMENT: { _edit_option_toggle_inline_comment(); } break; @@ -2064,6 +2075,7 @@ void ScriptTextEditor::_make_context_menu(bool p_selection, bool p_color, bool p context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_uppercase"), EDIT_TO_UPPERCASE); context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/convert_to_lowercase"), EDIT_TO_LOWERCASE); context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/evaluate_selection"), EDIT_EVALUATE); + context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION); } if (p_foldable) { context_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE); @@ -2178,6 +2190,7 @@ void ScriptTextEditor::_enable_code_editor() { sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/toggle_fold_line"), EDIT_TOGGLE_FOLD_LINE); sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/fold_all_lines"), EDIT_FOLD_ALL_LINES); sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/unfold_all_lines"), EDIT_UNFOLD_ALL_LINES); + sub_menu->add_shortcut(ED_GET_SHORTCUT("script_text_editor/create_code_region"), EDIT_CREATE_CODE_REGION); sub_menu->connect("id_pressed", callable_mp(this, &ScriptTextEditor::_edit_option)); edit_menu->get_popup()->add_child(sub_menu); edit_menu->get_popup()->add_submenu_item(TTR("Folding"), "folding_menu"); @@ -2373,6 +2386,7 @@ void ScriptTextEditor::register_editor() { ED_SHORTCUT("script_text_editor/toggle_fold_line", TTR("Fold/Unfold Line"), KeyModifierMask::ALT | Key::F); ED_SHORTCUT_OVERRIDE("script_text_editor/toggle_fold_line", "macos", KeyModifierMask::CTRL | KeyModifierMask::META | Key::F); ED_SHORTCUT("script_text_editor/fold_all_lines", TTR("Fold All Lines"), Key::NONE); + ED_SHORTCUT("script_text_editor/create_code_region", TTR("Create Code Region"), KeyModifierMask::ALT | Key::R); ED_SHORTCUT("script_text_editor/unfold_all_lines", TTR("Unfold All Lines"), Key::NONE); ED_SHORTCUT("script_text_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::SHIFT | KeyModifierMask::CTRL | Key::D); ED_SHORTCUT_OVERRIDE("script_text_editor/duplicate_selection", "macos", KeyModifierMask::SHIFT | KeyModifierMask::META | Key::C); diff --git a/editor/plugins/script_text_editor.h b/editor/plugins/script_text_editor.h index d275013b91..0efe7d54e3 100644 --- a/editor/plugins/script_text_editor.h +++ b/editor/plugins/script_text_editor.h @@ -98,6 +98,7 @@ class ScriptTextEditor : public ScriptEditorBase { Color safe_line_number_color = Color(1, 1, 1); Color marked_line_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); PopupPanel *color_panel = nullptr; ColorPicker *color_picker = nullptr; @@ -133,6 +134,7 @@ class ScriptTextEditor : public ScriptEditorBase { EDIT_TOGGLE_WORD_WRAP, EDIT_TOGGLE_FOLD_LINE, EDIT_FOLD_ALL_LINES, + EDIT_CREATE_CODE_REGION, EDIT_UNFOLD_ALL_LINES, SEARCH_FIND, SEARCH_FIND_NEXT, diff --git a/editor/plugins/tiles/tile_map_editor.cpp b/editor/plugins/tiles/tile_map_editor.cpp index fa35a03a22..21e532b22a 100644 --- a/editor/plugins/tiles/tile_map_editor.cpp +++ b/editor/plugins/tiles/tile_map_editor.cpp @@ -73,34 +73,21 @@ void TileMapEditorTilesPlugin::_update_toolbar() { // Show only the correct settings. if (tool_buttons_group->get_pressed_button() == select_tool_button) { - } else if (tool_buttons_group->get_pressed_button() == paint_tool_button) { + transform_toolbar->show(); + } else if (tool_buttons_group->get_pressed_button() != bucket_tool_button) { tools_settings_vsep->show(); picker_button->show(); erase_button->show(); + transform_toolbar->show(); tools_settings_vsep_2->show(); random_tile_toggle->show(); scatter_label->show(); scatter_spinbox->show(); - } else if (tool_buttons_group->get_pressed_button() == line_tool_button) { - tools_settings_vsep->show(); - picker_button->show(); - erase_button->show(); - tools_settings_vsep_2->show(); - random_tile_toggle->show(); - scatter_label->show(); - scatter_spinbox->show(); - } else if (tool_buttons_group->get_pressed_button() == rect_tool_button) { - tools_settings_vsep->show(); - picker_button->show(); - erase_button->show(); - tools_settings_vsep_2->show(); - random_tile_toggle->show(); - scatter_label->show(); - scatter_spinbox->show(); - } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + } else { tools_settings_vsep->show(); picker_button->show(); erase_button->show(); + transform_toolbar->show(); tools_settings_vsep_2->show(); bucket_contiguous_checkbox->show(); random_tile_toggle->show(); @@ -109,6 +96,31 @@ void TileMapEditorTilesPlugin::_update_toolbar() { } } +void TileMapEditorTilesPlugin::_update_transform_buttons() { + TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); + if (!tile_map) { + return; + } + + Ref<TileSet> tile_set = tile_map->get_tileset(); + if (tile_set.is_null() || selection_pattern.is_null()) { + return; + } + + if (tile_set->get_tile_shape() == TileSet::TILE_SHAPE_SQUARE || selection_pattern->get_size() == Vector2i(1, 1)) { + transform_button_rotate_left->set_disabled(false); + transform_button_rotate_left->set_tooltip_text(""); + transform_button_rotate_right->set_disabled(false); + transform_button_rotate_right->set_tooltip_text(""); + } else { + const String tooltip_text = TTR("Can't rotate patterns when using non-square tile grid."); + transform_button_rotate_left->set_disabled(true); + transform_button_rotate_left->set_tooltip_text(tooltip_text); + transform_button_rotate_right->set_disabled(true); + transform_button_rotate_right->set_tooltip_text(tooltip_text); + } +} + Vector<TileMapSubEditorPlugin::TabData> TileMapEditorTilesPlugin::get_tabs() const { Vector<TileMapSubEditorPlugin::TabData> tabs; tabs.push_back({ toolbar, tiles_bottom_panel }); @@ -480,6 +492,11 @@ void TileMapEditorTilesPlugin::_update_theme() { erase_button->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("Eraser"))); random_tile_toggle->set_icon(tiles_bottom_panel->get_editor_theme_icon(SNAME("RandomNumberGenerator"))); + transform_button_rotate_left->set_icon(tiles_bottom_panel->get_editor_theme_icon("RotateLeft")); + transform_button_rotate_right->set_icon(tiles_bottom_panel->get_editor_theme_icon("RotateRight")); + transform_button_flip_h->set_icon(tiles_bottom_panel->get_editor_theme_icon("MirrorX")); + transform_button_flip_v->set_icon(tiles_bottom_panel->get_editor_theme_icon("MirrorY")); + missing_atlas_texture_icon = tiles_bottom_panel->get_editor_theme_icon(SNAME("TileSet")); _update_tile_set_sources_list(); } @@ -573,8 +590,17 @@ bool TileMapEditorTilesPlugin::forward_canvas_gui_input(const Ref<InputEvent> &p Ref<InputEventKey> k = p_event; if (k.is_valid() && k->is_pressed() && !k->is_echo()) { for (BaseButton *b : viewport_shortcut_buttons) { + if (b->is_disabled()) { + continue; + } + if (b->get_shortcut().is_valid() && b->get_shortcut()->matches_event(p_event)) { - b->set_pressed(b->get_button_group().is_valid() || !b->is_pressed()); + if (b->is_toggle_mode()) { + b->set_pressed(b->get_button_group().is_valid() || !b->is_pressed()); + } else { + // Can't press a button without toggle mode, so just emit the signal directly. + b->emit_signal(SNAME("pressed")); + } return true; } } @@ -924,18 +950,18 @@ void TileMapEditorTilesPlugin::forward_canvas_draw_over_viewport(Control *p_over Rect2 dest_rect; dest_rect.size = source_rect.size; - bool transpose = tile_data->get_transpose(); + bool transpose = tile_data->get_transpose() ^ bool(E.value.alternative_tile & TileSetAtlasSource::TRANSFORM_TRANSPOSE); if (transpose) { dest_rect.position = (tile_map->map_to_local(E.key) - Vector2(dest_rect.size.y, dest_rect.size.x) / 2 - tile_offset); } else { dest_rect.position = (tile_map->map_to_local(E.key) - dest_rect.size / 2 - tile_offset); } - if (tile_data->get_flip_h()) { + if (tile_data->get_flip_h() ^ bool(E.value.alternative_tile & TileSetAtlasSource::TRANSFORM_FLIP_H)) { dest_rect.size.x = -dest_rect.size.x; } - if (tile_data->get_flip_v()) { + if (tile_data->get_flip_v() ^ bool(E.value.alternative_tile & TileSetAtlasSource::TRANSFORM_FLIP_V)) { dest_rect.size.y = -dest_rect.size.y; } @@ -1475,6 +1501,94 @@ void TileMapEditorTilesPlugin::_stop_dragging() { drag_type = DRAG_TYPE_NONE; } +void TileMapEditorTilesPlugin::_apply_transform(int p_type) { + if (selection_pattern.is_null() || selection_pattern->is_empty()) { + return; + } + + Ref<TileMapPattern> transformed_pattern; + transformed_pattern.instantiate(); + bool keep_shape = selection_pattern->get_size() == Vector2i(1, 1); + + Vector2i size = selection_pattern->get_size(); + for (int y = 0; y < size.y; y++) { + for (int x = 0; x < size.x; x++) { + Vector2i src_coords = Vector2i(x, y); + if (!selection_pattern->has_cell(src_coords)) { + continue; + } + + Vector2i dst_coords; + + if (keep_shape) { + dst_coords = src_coords; + } else if (p_type == TRANSFORM_ROTATE_LEFT) { + dst_coords = Vector2i(y, size.x - x - 1); + } else if (p_type == TRANSFORM_ROTATE_RIGHT) { + dst_coords = Vector2i(size.y - y - 1, x); + } else if (p_type == TRANSFORM_FLIP_H) { + dst_coords = Vector2i(size.x - x - 1, y); + } else if (p_type == TRANSFORM_FLIP_V) { + dst_coords = Vector2i(x, size.y - y - 1); + } + + transformed_pattern->set_cell(dst_coords, + selection_pattern->get_cell_source_id(src_coords), selection_pattern->get_cell_atlas_coords(src_coords), + _get_transformed_alternative(selection_pattern->get_cell_alternative_tile(src_coords), p_type)); + } + } + selection_pattern = transformed_pattern; + CanvasItemEditor::get_singleton()->update_viewport(); +} + +int TileMapEditorTilesPlugin::_get_transformed_alternative(int p_alternative_id, int p_transform) { + bool transform_flip_h = p_alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H; + bool transform_flip_v = p_alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V; + bool transform_transpose = p_alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE; + + switch (p_transform) { + case TRANSFORM_ROTATE_LEFT: + case TRANSFORM_ROTATE_RIGHT: { + // A matrix with every possible flip/transpose combination, sorted by what comes next when you rotate. + const LocalVector<bool> rotation_matrix = { + 0, 0, 0, + 0, 1, 1, + 1, 1, 0, + 1, 0, 1, + 1, 0, 0, + 0, 0, 1, + 0, 1, 0, + 1, 1, 1 + }; + + for (int i = 0; i < 8; i++) { + if (transform_flip_h == rotation_matrix[i * 3] && transform_flip_v == rotation_matrix[i * 3 + 1] && transform_transpose == rotation_matrix[i * 3 + 2]) { + if (p_transform == TRANSFORM_ROTATE_LEFT) { + i = i / 4 * 4 + (i + 1) % 4; + } else { + i = i / 4 * 4 + Math::posmod(i - 1, 4); + } + transform_flip_h = rotation_matrix[i * 3]; + transform_flip_v = rotation_matrix[i * 3 + 1]; + transform_transpose = rotation_matrix[i * 3 + 2]; + break; + } + } + } break; + case TRANSFORM_FLIP_H: { + transform_flip_h = !transform_flip_h; + } break; + case TRANSFORM_FLIP_V: { + transform_flip_v = !transform_flip_v; + } break; + } + + return TileSetAtlasSource::alternative_no_transform(p_alternative_id) | + int(transform_flip_h) * TileSetAtlasSource::TRANSFORM_FLIP_H | + int(transform_flip_v) * TileSetAtlasSource::TRANSFORM_FLIP_V | + int(transform_transpose) * TileSetAtlasSource::TRANSFORM_TRANSPOSE; +} + void TileMapEditorTilesPlugin::_update_fix_selected_and_hovered() { TileMap *tile_map = Object::cast_to<TileMap>(ObjectDB::get_instance(tile_map_id)); if (!tile_map) { @@ -1589,6 +1703,7 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tilemap_selection( coords_array.push_back(E); } selection_pattern = tile_map->get_pattern(tile_map_layer, coords_array); + _update_transform_buttons(); } void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_tiles_selection() { @@ -1662,6 +1777,7 @@ void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_tiles_sele vertical_offset += MAX(organized_size.y, 1); } CanvasItemEditor::get_singleton()->update_viewport(); + _update_transform_buttons(); } void TileMapEditorTilesPlugin::_update_selection_pattern_from_tileset_pattern_selection() { @@ -1700,6 +1816,7 @@ void TileMapEditorTilesPlugin::_update_tileset_selection_from_selection_pattern( _update_source_display(); tile_atlas_control->queue_redraw(); alternative_tiles_control->queue_redraw(); + _update_transform_buttons(); } void TileMapEditorTilesPlugin::_tile_atlas_control_draw() { @@ -2161,6 +2278,39 @@ TileMapEditorTilesPlugin::TileMapEditorTilesPlugin() { tools_settings->add_child(erase_button); viewport_shortcut_buttons.push_back(erase_button); + // Transform toolbar. + transform_toolbar = memnew(HBoxContainer); + tools_settings->add_child(transform_toolbar); + transform_toolbar->add_child(memnew(VSeparator)); + + transform_button_rotate_left = memnew(Button); + transform_button_rotate_left->set_flat(true); + transform_button_rotate_left->set_shortcut(ED_SHORTCUT("tiles_editor/rotate_tile_left", TTR("Rotate Tile Left"), Key::Z)); + transform_toolbar->add_child(transform_button_rotate_left); + transform_button_rotate_left->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_apply_transform).bind(TRANSFORM_ROTATE_LEFT)); + viewport_shortcut_buttons.push_back(transform_button_rotate_left); + + transform_button_rotate_right = memnew(Button); + transform_button_rotate_right->set_flat(true); + transform_button_rotate_right->set_shortcut(ED_SHORTCUT("tiles_editor/rotate_tile_right", TTR("Rotate Tile Right"), Key::X)); + transform_toolbar->add_child(transform_button_rotate_right); + transform_button_rotate_right->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_apply_transform).bind(TRANSFORM_ROTATE_RIGHT)); + viewport_shortcut_buttons.push_back(transform_button_rotate_right); + + transform_button_flip_h = memnew(Button); + transform_button_flip_h->set_flat(true); + transform_button_flip_h->set_shortcut(ED_SHORTCUT("tiles_editor/flip_tile_horizontal", TTR("Flip Tile Horizontally"), Key::C)); + transform_toolbar->add_child(transform_button_flip_h); + transform_button_flip_h->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_apply_transform).bind(TRANSFORM_FLIP_H)); + viewport_shortcut_buttons.push_back(transform_button_flip_h); + + transform_button_flip_v = memnew(Button); + transform_button_flip_v->set_flat(true); + transform_button_flip_v->set_shortcut(ED_SHORTCUT("tiles_editor/flip_tile_vertical", TTR("Flip Tile Vertically"), Key::V)); + transform_toolbar->add_child(transform_button_flip_v); + transform_button_flip_v->connect("pressed", callable_mp(this, &TileMapEditorTilesPlugin::_apply_transform).bind(TRANSFORM_FLIP_V)); + viewport_shortcut_buttons.push_back(transform_button_flip_v); + // Separator 2. tools_settings_vsep_2 = memnew(VSeparator); tools_settings->add_child(tools_settings_vsep_2); @@ -2352,25 +2502,11 @@ void TileMapEditorTerrainsPlugin::_update_toolbar() { } // Show only the correct settings. - if (tool_buttons_group->get_pressed_button() == paint_tool_button) { - tools_settings_vsep->show(); - picker_button->show(); - erase_button->show(); - tools_settings_vsep_2->hide(); - bucket_contiguous_checkbox->hide(); - } else if (tool_buttons_group->get_pressed_button() == line_tool_button) { + if (tool_buttons_group->get_pressed_button() != bucket_tool_button) { tools_settings_vsep->show(); picker_button->show(); erase_button->show(); - tools_settings_vsep_2->hide(); - bucket_contiguous_checkbox->hide(); - } else if (tool_buttons_group->get_pressed_button() == rect_tool_button) { - tools_settings_vsep->show(); - picker_button->show(); - erase_button->show(); - tools_settings_vsep_2->hide(); - bucket_contiguous_checkbox->hide(); - } else if (tool_buttons_group->get_pressed_button() == bucket_tool_button) { + } else { tools_settings_vsep->show(); picker_button->show(); erase_button->show(); @@ -3496,7 +3632,6 @@ TileMapEditorTerrainsPlugin::~TileMapEditorTerrainsPlugin() { void TileMapEditor::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_ENTER_TREE: case NOTIFICATION_THEME_CHANGED: { missing_tile_texture = get_editor_theme_icon(SNAME("StatusWarning")); warning_pattern_texture = get_editor_theme_icon(SNAME("WarningPattern")); diff --git a/editor/plugins/tiles/tile_map_editor.h b/editor/plugins/tiles/tile_map_editor.h index ab5787b78f..0b3d9813c3 100644 --- a/editor/plugins/tiles/tile_map_editor.h +++ b/editor/plugins/tiles/tile_map_editor.h @@ -48,6 +48,8 @@ #include "scene/gui/tab_bar.h" #include "scene/gui/tree.h" +class TileMapEditor; + class TileMapSubEditorPlugin : public Object { public: struct TabData { @@ -68,6 +70,14 @@ public: class TileMapEditorTilesPlugin : public TileMapSubEditorPlugin { GDCLASS(TileMapEditorTilesPlugin, TileMapSubEditorPlugin); +public: + enum { + TRANSFORM_ROTATE_LEFT, + TRANSFORM_ROTATE_RIGHT, + TRANSFORM_FLIP_H, + TRANSFORM_FLIP_V, + }; + private: ObjectID tile_map_id; int tile_map_layer = -1; @@ -89,6 +99,12 @@ private: Button *picker_button = nullptr; Button *erase_button = nullptr; + HBoxContainer *transform_toolbar = nullptr; + Button *transform_button_rotate_left = nullptr; + Button *transform_button_rotate_right = nullptr; + Button *transform_button_flip_h = nullptr; + Button *transform_button_flip_v = nullptr; + VSeparator *tools_settings_vsep_2 = nullptr; CheckBox *bucket_contiguous_checkbox = nullptr; Button *random_tile_toggle = nullptr; @@ -101,6 +117,7 @@ private: void _on_scattering_spinbox_changed(double p_value); void _update_toolbar(); + void _update_transform_buttons(); ///// Tilemap editing. ///// bool has_mouse = false; @@ -129,6 +146,9 @@ private: HashMap<Vector2i, TileMapCell> _draw_bucket_fill(Vector2i p_coords, bool p_contiguous, bool p_erase); void _stop_dragging(); + void _apply_transform(int p_type); + int _get_transformed_alternative(int p_alternative_id, int p_transform); + ///// Selection system. ///// RBSet<Vector2i> tile_map_selection; Ref<TileMapPattern> tile_map_clipboard; diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index 114c04a38f..62d2c14c17 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -1348,22 +1348,6 @@ void GDScript::_save_orphaned_subclasses(ClearData *p_clear_data) { } } -void GDScript::_init_rpc_methods_properties() { - // Copy the base rpc methods so we don't mask their IDs. - rpc_config.clear(); - if (base.is_valid()) { - rpc_config = base->rpc_config.duplicate(); - } - - // RPC Methods - for (KeyValue<StringName, GDScriptFunction *> &E : member_functions) { - Variant config = E.value->get_rpc_config(); - if (config.get_type() != Variant::NIL) { - rpc_config[E.value->get_name()] = config; - } - } -} - #ifdef DEBUG_ENABLED String GDScript::debug_get_script_name(const Ref<Script> &p_script) { if (p_script.is_valid()) { diff --git a/modules/gdscript/gdscript.h b/modules/gdscript/gdscript.h index 0eaaac8811..2fd2ec236a 100644 --- a/modules/gdscript/gdscript.h +++ b/modules/gdscript/gdscript.h @@ -168,7 +168,6 @@ class GDScript : public Script { bool _update_exports(bool *r_err = nullptr, bool p_recursive_call = false, PlaceHolderScriptInstance *p_instance_to_update = nullptr); void _save_orphaned_subclasses(GDScript::ClearData *p_clear_data); - void _init_rpc_methods_properties(); void _get_script_property_list(List<PropertyInfo> *r_list, bool p_include_base) const; void _get_script_method_list(List<MethodInfo> *r_list, bool p_include_base) const; diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp index 0eda7b2664..7b3d31f4a8 100644 --- a/modules/gdscript/gdscript_analyzer.cpp +++ b/modules/gdscript/gdscript_analyzer.cpp @@ -1000,6 +1000,11 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class, GDScriptParser::ParameterNode *param = member.signal->parameters[j]; GDScriptParser::DataType param_type = type_from_metatype(resolve_datatype(param->datatype_specifier)); param->set_datatype(param_type); +#ifdef DEBUG_ENABLED + if (param->datatype_specifier == nullptr) { + parser->push_warning(param, GDScriptWarning::UNTYPED_DECLARATION, "Parameter", param->identifier->name); + } +#endif mi.arguments.push_back(param_type.to_property_info(param->identifier->name)); // Signals do not support parameter default values. } @@ -1279,17 +1284,15 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co } else if (member.type == GDScriptParser::ClassNode::Member::VARIABLE && member.variable->property != GDScriptParser::VariableNode::PROP_NONE) { if (member.variable->property == GDScriptParser::VariableNode::PROP_INLINE) { if (member.variable->getter != nullptr) { - member.variable->getter->set_datatype(member.variable->datatype); + member.variable->getter->return_type = member.variable->datatype_specifier; + member.variable->getter->set_datatype(member.get_datatype()); resolve_function_body(member.variable->getter); } if (member.variable->setter != nullptr) { - resolve_function_signature(member.variable->setter); - - if (member.variable->setter->parameters.size() > 0) { - member.variable->setter->parameters[0]->datatype_specifier = member.variable->datatype_specifier; - member.variable->setter->parameters[0]->set_datatype(member.get_datatype()); - } + ERR_CONTINUE(member.variable->setter->parameters.is_empty()); + member.variable->setter->parameters[0]->datatype_specifier = member.variable->datatype_specifier; + member.variable->setter->parameters[0]->set_datatype(member.get_datatype()); resolve_function_body(member.variable->setter); } @@ -1593,15 +1596,18 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * int default_value_count = 0; #endif // TOOLS_ENABLED +#ifdef DEBUG_ENABLED + String function_visible_name = function_name; + if (function_name == StringName()) { + function_visible_name = p_is_lambda ? "<anonymous lambda>" : "<unknown function>"; + } +#endif + for (int i = 0; i < p_function->parameters.size(); i++) { resolve_parameter(p_function->parameters[i]); #ifdef DEBUG_ENABLED if (p_function->parameters[i]->usages == 0 && !String(p_function->parameters[i]->identifier->name).begins_with("_")) { - String visible_name = function_name; - if (function_name == StringName()) { - visible_name = p_is_lambda ? "<anonymous lambda>" : "<unknown function>"; - } - parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, visible_name, p_function->parameters[i]->identifier->name); + parser->push_warning(p_function->parameters[i]->identifier, GDScriptWarning::UNUSED_PARAMETER, function_visible_name, p_function->parameters[i]->identifier->name); } is_shadowing(p_function->parameters[i]->identifier, "function parameter", true); #endif // DEBUG_ENABLED @@ -1716,6 +1722,12 @@ void GDScriptAnalyzer::resolve_function_signature(GDScriptParser::FunctionNode * #endif // TOOLS_ENABLED } +#ifdef DEBUG_ENABLED + if (p_function->return_type == nullptr) { + parser->push_warning(p_function, GDScriptWarning::UNTYPED_DECLARATION, "Function", function_visible_name); + } +#endif + if (p_function->get_datatype().is_resolving()) { p_function->set_datatype(prev_datatype); } @@ -1919,6 +1931,13 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi } } +#ifdef DEBUG_ENABLED + if (!has_specified_type && !p_assignable->infer_datatype && !is_constant) { + const bool is_parameter = p_assignable->type == GDScriptParser::Node::PARAMETER; + parser->push_warning(p_assignable, GDScriptWarning::UNTYPED_DECLARATION, is_parameter ? "Parameter" : "Variable", p_assignable->identifier->name); + } +#endif + type.is_constant = is_constant; type.is_read_only = false; p_assignable->set_datatype(type); @@ -2129,13 +2148,18 @@ void GDScriptAnalyzer::resolve_for(GDScriptParser::ForNode *p_for) { #endif } #ifdef DEBUG_ENABLED - } else { + } else if (variable_type.is_hard_type()) { parser->push_warning(p_for->datatype_specifier, GDScriptWarning::REDUNDANT_FOR_VARIABLE_TYPE, p_for->variable->name, variable_type.to_string(), specified_type.to_string()); #endif } p_for->variable->set_datatype(specified_type); } else { p_for->variable->set_datatype(variable_type); +#ifdef DEBUG_ENABLED + if (!variable_type.is_hard_type()) { + parser->push_warning(p_for->variable, GDScriptWarning::UNTYPED_DECLARATION, R"("for" iterator variable)", p_for->variable->name); + } +#endif } } diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp index 70f95321be..fae7861539 100644 --- a/modules/gdscript/gdscript_compiler.cpp +++ b/modules/gdscript/gdscript_compiler.cpp @@ -2513,7 +2513,10 @@ Error GDScriptCompiler::_parse_setter_getter(GDScript *p_script, const GDScriptP return err; } -Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state) { +// Prepares given script, and inner class scripts, for compilation. It populates class members and initializes method +// RPC info for its base classes first, then for itself, then for inner classes. +// Warning: this function cannot initiate compilation of other classes, or it will result in cyclic dependency issues. +Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state) { if (parsed_classes.has(p_script)) { return OK; } @@ -2571,6 +2574,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri p_script->implicit_initializer = nullptr; p_script->implicit_ready = nullptr; p_script->static_initializer = nullptr; + p_script->rpc_config.clear(); p_script->clearing = false; @@ -2601,7 +2605,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri } if (main_script->has_class(base.ptr())) { - Error err = _populate_class_members(base.ptr(), p_class->base_type.class_type, p_keep_state); + Error err = _prepare_compilation(base.ptr(), p_class->base_type.class_type, p_keep_state); if (err) { return err; } @@ -2620,7 +2624,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri return ERR_COMPILATION_FAILED; } - err = _populate_class_members(base.ptr(), p_class->base_type.class_type, p_keep_state); + err = _prepare_compilation(base.ptr(), p_class->base_type.class_type, p_keep_state); if (err) { _set_error(vformat(R"(Could not populate class members of base class "%s" in "%s".)", base->fully_qualified_name, base->path), nullptr); return err; @@ -2637,6 +2641,12 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri } break; } + // Duplicate RPC information from base GDScript + // Base script isn't valid because it should not have been compiled yet, but the reference contains relevant info. + if (base_type.kind == GDScriptDataType::GDSCRIPT && p_script->base.is_valid()) { + p_script->rpc_config = p_script->base->rpc_config.duplicate(); + } + for (int i = 0; i < p_class->members.size(); i++) { const GDScriptParser::ClassNode::Member &member = p_class->members[i]; switch (member.type) { @@ -2748,6 +2758,14 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri p_script->members.insert(Variant()); } break; + case GDScriptParser::ClassNode::Member::FUNCTION: { + const GDScriptParser::FunctionNode *function_n = member.function; + + Variant config = function_n->rpc_config; + if (config.get_type() != Variant::NIL) { + p_script->rpc_config[function_n->identifier->name] = config; + } + } break; default: break; // Nothing to do here. } @@ -2758,7 +2776,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri parsed_classes.insert(p_script); parsing_classes.erase(p_script); - // Populate sub-classes. + // Populate inner classes. for (int i = 0; i < p_class->members.size(); i++) { const GDScriptParser::ClassNode::Member &member = p_class->members[i]; if (member.type != member.CLASS) { @@ -2771,7 +2789,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri // Subclass might still be parsing, just skip it if (!parsing_classes.has(subclass_ptr)) { - Error err = _populate_class_members(subclass_ptr, inner_class, p_keep_state); + Error err = _prepare_compilation(subclass_ptr, inner_class, p_keep_state); if (err) { return err; } @@ -2907,8 +2925,6 @@ Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser: has_static_data = has_static_data || inner_class->has_static_data; } - p_script->_init_rpc_methods_properties(); - p_script->valid = true; return OK; } @@ -2982,7 +2998,7 @@ Error GDScriptCompiler::compile(const GDScriptParser *p_parser, GDScript *p_scri make_scripts(p_script, root, p_keep_state); main_script->_owner = nullptr; - Error err = _populate_class_members(main_script, parser->get_tree(), p_keep_state); + Error err = _prepare_compilation(main_script, parser->get_tree(), p_keep_state); if (err) { return err; diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h index 2f522da4ea..1882471331 100644 --- a/modules/gdscript/gdscript_compiler.h +++ b/modules/gdscript/gdscript_compiler.h @@ -135,7 +135,7 @@ class GDScriptCompiler { GDScriptFunction *_parse_function(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::FunctionNode *p_func, bool p_for_ready = false, bool p_for_lambda = false); GDScriptFunction *_make_static_initializer(Error &r_error, GDScript *p_script, const GDScriptParser::ClassNode *p_class); Error _parse_setter_getter(GDScript *p_script, const GDScriptParser::ClassNode *p_class, const GDScriptParser::VariableNode *p_variable, bool p_is_setter); - Error _populate_class_members(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); + Error _prepare_compilation(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); Error _compile_class(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state); int err_line = 0; int err_column = 0; diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp index 6cad3b2b90..5e626f0520 100644 --- a/modules/gdscript/gdscript_editor.cpp +++ b/modules/gdscript/gdscript_editor.cpp @@ -1459,8 +1459,13 @@ static bool _guess_expression_type(GDScriptParser::CompletionContext &p_context, if (p_expression->is_constant) { // Already has a value, so just use that. r_type = _type_from_variant(p_expression->reduced_value); - if (p_expression->get_datatype().kind == GDScriptParser::DataType::ENUM) { - r_type.type = p_expression->get_datatype(); + switch (p_expression->get_datatype().kind) { + case GDScriptParser::DataType::ENUM: + case GDScriptParser::DataType::CLASS: + r_type.type = p_expression->get_datatype(); + break; + default: + break; } found = true; } else { diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp index 4fec445995..fcc6ea34de 100644 --- a/modules/gdscript/gdscript_warning.cpp +++ b/modules/gdscript/gdscript_warning.cpp @@ -88,6 +88,12 @@ String GDScriptWarning::get_message() const { case FUNCTION_USED_AS_PROPERTY: CHECK_SYMBOLS(2); return vformat(R"(The property "%s" was not found in base "%s" but there's a method with the same name. Did you mean to call it?)", symbols[0], symbols[1]); + case UNTYPED_DECLARATION: + CHECK_SYMBOLS(2); + if (symbols[0] == "Function") { + return vformat(R"*(%s "%s()" has no static return type.)*", symbols[0], symbols[1]); + } + return vformat(R"(%s "%s" has no static type.)", symbols[0], symbols[1]); case UNSAFE_PROPERTY_ACCESS: CHECK_SYMBOLS(2); return vformat(R"(The property "%s" is not present on the inferred type "%s" (but may be present on a subtype).)", symbols[0], symbols[1]); @@ -208,6 +214,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) { "PROPERTY_USED_AS_FUNCTION", "CONSTANT_USED_AS_FUNCTION", "FUNCTION_USED_AS_PROPERTY", + "UNTYPED_DECLARATION", "UNSAFE_PROPERTY_ACCESS", "UNSAFE_METHOD_ACCESS", "UNSAFE_CAST", diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h index 73e12eb20e..a26cfaf72c 100644 --- a/modules/gdscript/gdscript_warning.h +++ b/modules/gdscript/gdscript_warning.h @@ -64,6 +64,7 @@ public: PROPERTY_USED_AS_FUNCTION, // Function not found, but there's a property with the same name. CONSTANT_USED_AS_FUNCTION, // Function not found, but there's a constant with the same name. FUNCTION_USED_AS_PROPERTY, // Property not found, but there's a function with the same name. + UNTYPED_DECLARATION, // Variable/parameter/function has no static type, explicitly specified or inferred (`:=`). UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes). UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes). UNSAFE_CAST, // Cast used in an unknown type. @@ -112,6 +113,7 @@ public: WARN, // PROPERTY_USED_AS_FUNCTION WARN, // CONSTANT_USED_AS_FUNCTION WARN, // FUNCTION_USED_AS_PROPERTY + IGNORE, // UNTYPED_DECLARATION // Static typing is optional, we don't want to spam warnings. IGNORE, // UNSAFE_PROPERTY_ACCESS // Too common in untyped scenarios. IGNORE, // UNSAFE_METHOD_ACCESS // Too common in untyped scenarios. IGNORE, // UNSAFE_CAST // Too common in untyped scenarios. diff --git a/modules/gdscript/language_server/gdscript_extend_parser.cpp b/modules/gdscript/language_server/gdscript_extend_parser.cpp index 3a5a54e275..362f253a99 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.cpp +++ b/modules/gdscript/language_server/gdscript_extend_parser.cpp @@ -32,9 +32,94 @@ #include "../gdscript.h" #include "../gdscript_analyzer.h" +#include "editor/editor_settings.h" #include "gdscript_language_protocol.h" #include "gdscript_workspace.h" +int get_indent_size() { + if (EditorSettings::get_singleton()) { + return EditorSettings::get_singleton()->get_setting("text_editor/behavior/indent/size"); + } else { + return 4; + } +} + +lsp::Position GodotPosition::to_lsp(const Vector<String> &p_lines) const { + lsp::Position res; + + // Special case: `line = 0` -> root class (range covers everything). + if (this->line <= 0) { + return res; + } + // Special case: `line = p_lines.size() + 1` -> root class (range covers everything). + if (this->line >= p_lines.size() + 1) { + res.line = p_lines.size(); + return res; + } + res.line = this->line - 1; + // Note: character outside of `pos_line.length()-1` is valid. + res.character = this->column - 1; + + String pos_line = p_lines[res.line]; + if (pos_line.contains("\t")) { + int tab_size = get_indent_size(); + + int in_col = 1; + int res_char = 0; + + while (res_char < pos_line.size() && in_col < this->column) { + if (pos_line[res_char] == '\t') { + in_col += tab_size; + res_char++; + } else { + in_col++; + res_char++; + } + } + + res.character = res_char; + } + + return res; +} + +GodotPosition GodotPosition::from_lsp(const lsp::Position p_pos, const Vector<String> &p_lines) { + GodotPosition res(p_pos.line + 1, p_pos.character + 1); + + // Line outside of actual text is valid (-> pos/cursor at end of text). + if (res.line > p_lines.size()) { + return res; + } + + String line = p_lines[p_pos.line]; + int tabs_before_char = 0; + for (int i = 0; i < p_pos.character && i < line.length(); i++) { + if (line[i] == '\t') { + tabs_before_char++; + } + } + + if (tabs_before_char > 0) { + int tab_size = get_indent_size(); + res.column += tabs_before_char * (tab_size - 1); + } + + return res; +} + +lsp::Range GodotRange::to_lsp(const Vector<String> &p_lines) const { + lsp::Range res; + res.start = start.to_lsp(p_lines); + res.end = end.to_lsp(p_lines); + return res; +} + +GodotRange GodotRange::from_lsp(const lsp::Range &p_range, const Vector<String> &p_lines) { + GodotPosition start = GodotPosition::from_lsp(p_range.start, p_lines); + GodotPosition end = GodotPosition::from_lsp(p_range.end, p_lines); + return GodotRange(start, end); +} + void ExtendGDScriptParser::update_diagnostics() { diagnostics.clear(); @@ -90,7 +175,7 @@ void ExtendGDScriptParser::update_symbols() { const lsp::DocumentSymbol &symbol = class_symbol.children[i]; members.insert(symbol.name, &symbol); - // cache level one inner classes + // Cache level one inner classes. if (symbol.kind == lsp::SymbolKind::Class) { ClassMembers inner_class; for (int j = 0; j < symbol.children.size(); j++) { @@ -126,10 +211,7 @@ void ExtendGDScriptParser::update_document_links(const String &p_code) { String value = const_val; lsp::DocumentLink link; link.target = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(scr_path); - link.range.start.line = LINE_NUMBER_TO_INDEX(token.start_line); - link.range.end.line = LINE_NUMBER_TO_INDEX(token.end_line); - link.range.start.character = LINE_NUMBER_TO_INDEX(token.start_column); - link.range.end.character = LINE_NUMBER_TO_INDEX(token.end_column); + link.range = GodotRange(GodotPosition(token.start_line, token.start_column), GodotPosition(token.end_line, token.end_column)).to_lsp(this->lines); document_links.push_back(link); } } @@ -137,6 +219,12 @@ void ExtendGDScriptParser::update_document_links(const String &p_code) { } } +lsp::Range ExtendGDScriptParser::range_of_node(const GDScriptParser::Node *p_node) const { + GodotPosition start(p_node->start_line, p_node->start_column); + GodotPosition end(p_node->end_line, p_node->end_column); + return GodotRange(start, end).to_lsp(this->lines); +} + void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p_class, lsp::DocumentSymbol &r_symbol) { const String uri = get_uri(); @@ -149,13 +237,30 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p } r_symbol.kind = lsp::SymbolKind::Class; r_symbol.deprecated = false; - r_symbol.range.start.line = p_class->start_line; - r_symbol.range.start.character = p_class->start_column; - r_symbol.range.end.line = lines.size(); - r_symbol.selectionRange.start.line = r_symbol.range.start.line; + r_symbol.range = range_of_node(p_class); + r_symbol.range.start.line = MAX(r_symbol.range.start.line, 0); + if (p_class->identifier) { + r_symbol.selectionRange = range_of_node(p_class->identifier); + } r_symbol.detail = "class " + r_symbol.name; - bool is_root_class = &r_symbol == &class_symbol; - r_symbol.documentation = parse_documentation(is_root_class ? 0 : LINE_NUMBER_TO_INDEX(p_class->start_line), is_root_class); + { + String doc = p_class->doc_data.description; + if (!p_class->doc_data.description.is_empty()) { + doc += "\n\n" + p_class->doc_data.description; + } + + if (!p_class->doc_data.tutorials.is_empty()) { + doc += "\n"; + for (const Pair<String, String> &tutorial : p_class->doc_data.tutorials) { + if (tutorial.first.is_empty()) { + doc += vformat("\n@tutorial: %s", tutorial.second); + } else { + doc += vformat("\n@tutorial(%s): %s", tutorial.first, tutorial.second); + } + } + } + r_symbol.documentation = doc; + } for (int i = 0; i < p_class->members.size(); i++) { const ClassNode::Member &m = p_class->members[i]; @@ -166,11 +271,8 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p symbol.name = m.variable->identifier->name; symbol.kind = m.variable->property == VariableNode::PROP_NONE ? lsp::SymbolKind::Variable : lsp::SymbolKind::Property; symbol.deprecated = false; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.variable->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.variable->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.variable->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.variable->end_column); - symbol.selectionRange.start.line = symbol.range.start.line; + symbol.range = range_of_node(m.variable); + symbol.selectionRange = range_of_node(m.variable->identifier); if (m.variable->exported) { symbol.detail += "@export "; } @@ -182,10 +284,31 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p symbol.detail += " = " + m.variable->initializer->reduced_value.to_json_string(); } - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.variable->start_line)); + symbol.documentation = m.variable->doc_data.description; symbol.uri = uri; symbol.script_path = path; + if (m.variable->initializer && m.variable->initializer->type == GDScriptParser::Node::LAMBDA) { + GDScriptParser::LambdaNode *lambda_node = (GDScriptParser::LambdaNode *)m.variable->initializer; + lsp::DocumentSymbol lambda; + parse_function_symbol(lambda_node->function, lambda); + // Merge lambda into current variable. + symbol.children.append_array(lambda.children); + } + + if (m.variable->getter && m.variable->getter->type == GDScriptParser::Node::FUNCTION) { + lsp::DocumentSymbol get_symbol; + parse_function_symbol(m.variable->getter, get_symbol); + get_symbol.local = true; + symbol.children.push_back(get_symbol); + } + if (m.variable->setter && m.variable->setter->type == GDScriptParser::Node::FUNCTION) { + lsp::DocumentSymbol set_symbol; + parse_function_symbol(m.variable->setter, set_symbol); + set_symbol.local = true; + symbol.children.push_back(set_symbol); + } + r_symbol.children.push_back(symbol); } break; case ClassNode::Member::CONSTANT: { @@ -194,12 +317,9 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p symbol.name = m.constant->identifier->name; symbol.kind = lsp::SymbolKind::Constant; symbol.deprecated = false; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.constant->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.constant->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.constant->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.constant->start_column); - symbol.selectionRange.start.line = LINE_NUMBER_TO_INDEX(m.constant->start_line); - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.constant->start_line)); + symbol.range = range_of_node(m.constant); + symbol.selectionRange = range_of_node(m.constant->identifier); + symbol.documentation = m.constant->doc_data.description; symbol.uri = uri; symbol.script_path = path; @@ -231,36 +351,14 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p r_symbol.children.push_back(symbol); } break; - case ClassNode::Member::ENUM_VALUE: { - lsp::DocumentSymbol symbol; - - symbol.name = m.enum_value.identifier->name; - symbol.kind = lsp::SymbolKind::EnumMember; - symbol.deprecated = false; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.enum_value.leftmost_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.enum_value.rightmost_column); - symbol.selectionRange.start.line = LINE_NUMBER_TO_INDEX(m.enum_value.line); - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.enum_value.line)); - symbol.uri = uri; - symbol.script_path = path; - - symbol.detail = symbol.name + " = " + itos(m.enum_value.value); - - r_symbol.children.push_back(symbol); - } break; case ClassNode::Member::SIGNAL: { lsp::DocumentSymbol symbol; symbol.name = m.signal->identifier->name; symbol.kind = lsp::SymbolKind::Event; symbol.deprecated = false; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.signal->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.signal->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.signal->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.signal->end_column); - symbol.selectionRange.start.line = symbol.range.start.line; - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.signal->start_line)); + symbol.range = range_of_node(m.signal); + symbol.selectionRange = range_of_node(m.signal->identifier); + symbol.documentation = m.signal->doc_data.description; symbol.uri = uri; symbol.script_path = path; symbol.detail = "signal " + String(m.signal->identifier->name) + "("; @@ -272,17 +370,48 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p } symbol.detail += ")"; + for (GDScriptParser::ParameterNode *param : m.signal->parameters) { + lsp::DocumentSymbol param_symbol; + param_symbol.name = param->identifier->name; + param_symbol.kind = lsp::SymbolKind::Variable; + param_symbol.deprecated = false; + param_symbol.local = true; + param_symbol.range = range_of_node(param); + param_symbol.selectionRange = range_of_node(param->identifier); + param_symbol.uri = uri; + param_symbol.script_path = path; + param_symbol.detail = "var " + param_symbol.name; + if (param->get_datatype().is_hard_type()) { + param_symbol.detail += ": " + param->get_datatype().to_string(); + } + symbol.children.push_back(param_symbol); + } + r_symbol.children.push_back(symbol); + } break; + case ClassNode::Member::ENUM_VALUE: { + lsp::DocumentSymbol symbol; + + symbol.name = m.enum_value.identifier->name; + symbol.kind = lsp::SymbolKind::EnumMember; + symbol.deprecated = false; + symbol.range.start = GodotPosition(m.enum_value.line, m.enum_value.leftmost_column).to_lsp(this->lines); + symbol.range.end = GodotPosition(m.enum_value.line, m.enum_value.rightmost_column).to_lsp(this->lines); + symbol.selectionRange = range_of_node(m.enum_value.identifier); + symbol.documentation = m.enum_value.doc_data.description; + symbol.uri = uri; + symbol.script_path = path; + + symbol.detail = symbol.name + " = " + itos(m.enum_value.value); + r_symbol.children.push_back(symbol); } break; case ClassNode::Member::ENUM: { lsp::DocumentSymbol symbol; + symbol.name = m.m_enum->identifier->name; symbol.kind = lsp::SymbolKind::Enum; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(m.m_enum->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(m.m_enum->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(m.m_enum->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(m.m_enum->end_column); - symbol.selectionRange.start.line = symbol.range.start.line; - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(m.m_enum->start_line)); + symbol.range = range_of_node(m.m_enum); + symbol.selectionRange = range_of_node(m.m_enum->identifier); + symbol.documentation = m.m_enum->doc_data.description; symbol.uri = uri; symbol.script_path = path; @@ -294,6 +423,25 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p symbol.detail += String(m.m_enum->values[j].identifier->name) + " = " + itos(m.m_enum->values[j].value); } symbol.detail += "}"; + + for (GDScriptParser::EnumNode::Value value : m.m_enum->values) { + lsp::DocumentSymbol child; + + child.name = value.identifier->name; + child.kind = lsp::SymbolKind::EnumMember; + child.deprecated = false; + child.range.start = GodotPosition(value.line, value.leftmost_column).to_lsp(this->lines); + child.range.end = GodotPosition(value.line, value.rightmost_column).to_lsp(this->lines); + child.selectionRange = range_of_node(value.identifier); + child.documentation = value.doc_data.description; + child.uri = uri; + child.script_path = path; + + child.detail = child.name + " = " + itos(value.value); + + symbol.children.push_back(child); + } + r_symbol.children.push_back(symbol); } break; case ClassNode::Member::FUNCTION: { @@ -317,32 +465,29 @@ void ExtendGDScriptParser::parse_class_symbol(const GDScriptParser::ClassNode *p void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionNode *p_func, lsp::DocumentSymbol &r_symbol) { const String uri = get_uri(); - r_symbol.name = p_func->identifier->name; - r_symbol.kind = p_func->is_static ? lsp::SymbolKind::Function : lsp::SymbolKind::Method; - r_symbol.detail = "func " + String(p_func->identifier->name) + "("; + bool is_named = p_func->identifier != nullptr; + + r_symbol.name = is_named ? p_func->identifier->name : ""; + r_symbol.kind = (p_func->is_static || p_func->source_lambda != nullptr) ? lsp::SymbolKind::Function : lsp::SymbolKind::Method; + r_symbol.detail = "func"; + if (is_named) { + r_symbol.detail += " " + String(p_func->identifier->name); + } + r_symbol.detail += "("; r_symbol.deprecated = false; - r_symbol.range.start.line = LINE_NUMBER_TO_INDEX(p_func->start_line); - r_symbol.range.start.character = LINE_NUMBER_TO_INDEX(p_func->start_column); - r_symbol.range.end.line = LINE_NUMBER_TO_INDEX(p_func->start_line); - r_symbol.range.end.character = LINE_NUMBER_TO_INDEX(p_func->end_column); - r_symbol.selectionRange.start.line = r_symbol.range.start.line; - r_symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(p_func->start_line)); + r_symbol.range = range_of_node(p_func); + if (is_named) { + r_symbol.selectionRange = range_of_node(p_func->identifier); + } else { + r_symbol.selectionRange.start = r_symbol.selectionRange.end = r_symbol.range.start; + } + r_symbol.documentation = p_func->doc_data.description; r_symbol.uri = uri; r_symbol.script_path = path; String parameters; for (int i = 0; i < p_func->parameters.size(); i++) { const ParameterNode *parameter = p_func->parameters[i]; - lsp::DocumentSymbol symbol; - symbol.kind = lsp::SymbolKind::Variable; - symbol.name = parameter->identifier->name; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(parameter->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(parameter->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(parameter->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(parameter->end_column); - symbol.uri = uri; - symbol.script_path = path; - r_symbol.children.push_back(symbol); if (i > 0) { parameters += ", "; } @@ -387,6 +532,13 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN node_stack.push_back(while_node->loop); } break; + case GDScriptParser::TypeNode::MATCH: { + GDScriptParser::MatchNode *match_node = (GDScriptParser::MatchNode *)node; + for (GDScriptParser::MatchBranchNode *branch_node : match_node->branches) { + node_stack.push_back(branch_node); + } + } break; + case GDScriptParser::TypeNode::MATCH_BRANCH: { GDScriptParser::MatchBranchNode *match_node = (GDScriptParser::MatchBranchNode *)node; node_stack.push_back(match_node->block); @@ -400,20 +552,6 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN } } break; - case GDScriptParser::TypeNode::VARIABLE: { - GDScriptParser::VariableNode *variable_node = (GDScriptParser::VariableNode *)(node); - lsp::DocumentSymbol symbol; - symbol.kind = lsp::SymbolKind::Variable; - symbol.name = variable_node->identifier->name; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(variable_node->start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(variable_node->start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(variable_node->end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(variable_node->end_column); - symbol.uri = uri; - symbol.script_path = path; - r_symbol.children.push_back(symbol); - } break; - default: continue; } @@ -426,10 +564,40 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN lsp::DocumentSymbol symbol; symbol.name = local.name; symbol.kind = local.type == SuiteNode::Local::CONSTANT ? lsp::SymbolKind::Constant : lsp::SymbolKind::Variable; - symbol.range.start.line = LINE_NUMBER_TO_INDEX(local.start_line); - symbol.range.start.character = LINE_NUMBER_TO_INDEX(local.start_column); - symbol.range.end.line = LINE_NUMBER_TO_INDEX(local.end_line); - symbol.range.end.character = LINE_NUMBER_TO_INDEX(local.end_column); + switch (local.type) { + case SuiteNode::Local::CONSTANT: + symbol.range = range_of_node(local.constant); + symbol.selectionRange = range_of_node(local.constant->identifier); + break; + case SuiteNode::Local::VARIABLE: + symbol.range = range_of_node(local.variable); + symbol.selectionRange = range_of_node(local.variable->identifier); + if (local.variable->initializer && local.variable->initializer->type == GDScriptParser::Node::LAMBDA) { + GDScriptParser::LambdaNode *lambda_node = (GDScriptParser::LambdaNode *)local.variable->initializer; + lsp::DocumentSymbol lambda; + parse_function_symbol(lambda_node->function, lambda); + // Merge lambda into current variable. + // -> Only interested in new variables, not lambda itself. + symbol.children.append_array(lambda.children); + } + break; + case SuiteNode::Local::PARAMETER: + symbol.range = range_of_node(local.parameter); + symbol.selectionRange = range_of_node(local.parameter->identifier); + break; + case SuiteNode::Local::FOR_VARIABLE: + case SuiteNode::Local::PATTERN_BIND: + symbol.range = range_of_node(local.bind); + symbol.selectionRange = range_of_node(local.bind); + break; + default: + // Fallback. + symbol.range.start = GodotPosition(local.start_line, local.start_column).to_lsp(get_lines()); + symbol.range.end = GodotPosition(local.end_line, local.end_column).to_lsp(get_lines()); + symbol.selectionRange = symbol.range; + break; + } + symbol.local = true; symbol.uri = uri; symbol.script_path = path; symbol.detail = local.type == SuiteNode::Local::CONSTANT ? "const " : "var "; @@ -437,53 +605,19 @@ void ExtendGDScriptParser::parse_function_symbol(const GDScriptParser::FunctionN if (local.get_datatype().is_hard_type()) { symbol.detail += ": " + local.get_datatype().to_string(); } - symbol.documentation = parse_documentation(LINE_NUMBER_TO_INDEX(local.start_line)); - r_symbol.children.push_back(symbol); - } - } -} - -String ExtendGDScriptParser::parse_documentation(int p_line, bool p_docs_down) { - ERR_FAIL_INDEX_V(p_line, lines.size(), String()); - - List<String> doc_lines; - - if (!p_docs_down) { // inline comment - String inline_comment = lines[p_line]; - int comment_start = inline_comment.find("##"); - if (comment_start != -1) { - inline_comment = inline_comment.substr(comment_start, inline_comment.length()).strip_edges(); - if (inline_comment.length() > 1) { - doc_lines.push_back(inline_comment.substr(2, inline_comment.length())); + switch (local.type) { + case SuiteNode::Local::CONSTANT: + symbol.documentation = local.constant->doc_data.description; + break; + case SuiteNode::Local::VARIABLE: + symbol.documentation = local.variable->doc_data.description; + break; + default: + break; } + r_symbol.children.push_back(symbol); } } - - int step = p_docs_down ? 1 : -1; - int start_line = p_docs_down ? p_line : p_line - 1; - for (int i = start_line; true; i += step) { - if (i < 0 || i >= lines.size()) { - break; - } - - String line_comment = lines[i].strip_edges(true, false); - if (line_comment.begins_with("##")) { - line_comment = line_comment.substr(2, line_comment.length()); - if (p_docs_down) { - doc_lines.push_back(line_comment); - } else { - doc_lines.push_front(line_comment); - } - } else { - break; - } - } - - String doc; - for (const String &E : doc_lines) { - doc += E + "\n"; - } - return doc; } String ExtendGDScriptParser::get_text_for_completion(const lsp::Position &p_cursor) const { @@ -492,7 +626,7 @@ String ExtendGDScriptParser::get_text_for_completion(const lsp::Position &p_curs for (int i = 0; i < len; i++) { if (i == p_cursor.line) { longthing += lines[i].substr(0, p_cursor.character); - longthing += String::chr(0xFFFF); //not unicode, represents the cursor + longthing += String::chr(0xFFFF); // Not unicode, represents the cursor. longthing += lines[i].substr(p_cursor.character, lines[i].size()); } else { longthing += lines[i]; @@ -513,7 +647,7 @@ String ExtendGDScriptParser::get_text_for_lookup_symbol(const lsp::Position &p_c if (i == p_cursor.line) { String line = lines[i]; String first_part = line.substr(0, p_cursor.character); - String last_part = line.substr(p_cursor.character + 1, lines[i].length()); + String last_part = line.substr(p_cursor.character, lines[i].length()); if (!p_symbol.is_empty()) { String left_cursor_text; for (int c = p_cursor.character - 1; c >= 0; c--) { @@ -527,9 +661,9 @@ String ExtendGDScriptParser::get_text_for_lookup_symbol(const lsp::Position &p_c } longthing += first_part; - longthing += String::chr(0xFFFF); //not unicode, represents the cursor + longthing += String::chr(0xFFFF); // Not unicode, represents the cursor. if (p_func_required) { - longthing += "("; // tell the parser this is a function call + longthing += "("; // Tell the parser this is a function call. } longthing += last_part; } else { @@ -544,7 +678,7 @@ String ExtendGDScriptParser::get_text_for_lookup_symbol(const lsp::Position &p_c return longthing; } -String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position &p_position, Vector2i &p_offset) const { +String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position &p_position, lsp::Range &r_range) const { ERR_FAIL_INDEX_V(p_position.line, lines.size(), ""); String line = lines[p_position.line]; if (line.is_empty()) { @@ -552,8 +686,32 @@ String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position & } ERR_FAIL_INDEX_V(p_position.character, line.size(), ""); - int start_pos = p_position.character; - for (int c = p_position.character; c >= 0; c--) { + // `p_position` cursor is BETWEEN chars, not ON chars. + // -> + // ```gdscript + // var member| := some_func|(some_variable|) + // ^ ^ ^ + // | | | cursor on `some_variable, position on `)` + // | | + // | | cursor on `some_func`, pos on `(` + // | + // | cursor on `member`, pos on ` ` (space) + // ``` + // -> Move position to previous character if: + // * Position not on valid identifier char. + // * Prev position is valid identifier char. + lsp::Position pos = p_position; + if ( + pos.character >= line.length() // Cursor at end of line. + || (!is_ascii_identifier_char(line[pos.character]) // Not on valid identifier char. + && (pos.character > 0 // Not line start -> there is a prev char. + && is_ascii_identifier_char(line[pos.character - 1]) // Prev is valid identifier char. + ))) { + pos.character--; + } + + int start_pos = pos.character; + for (int c = pos.character; c >= 0; c--) { start_pos = c; char32_t ch = line[c]; bool valid_char = is_ascii_identifier_char(ch); @@ -562,8 +720,8 @@ String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position & } } - int end_pos = p_position.character; - for (int c = p_position.character; c < line.length(); c++) { + int end_pos = pos.character; + for (int c = pos.character; c < line.length(); c++) { char32_t ch = line[c]; bool valid_char = is_ascii_identifier_char(ch); if (!valid_char) { @@ -571,9 +729,11 @@ String ExtendGDScriptParser::get_identifier_under_position(const lsp::Position & } end_pos = c; } + if (start_pos < end_pos) { - p_offset.x = start_pos - p_position.character; - p_offset.y = end_pos - p_position.character; + r_range.start.line = r_range.end.line = pos.line; + r_range.start.character = start_pos + 1; + r_range.end.character = end_pos + 1; return line.substr(start_pos + 1, end_pos - start_pos); } @@ -584,15 +744,15 @@ String ExtendGDScriptParser::get_uri() const { return GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_uri(path); } -const lsp::DocumentSymbol *ExtendGDScriptParser::search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent) const { +const lsp::DocumentSymbol *ExtendGDScriptParser::search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent, const String &p_symbol_name) const { const lsp::DocumentSymbol *ret = nullptr; if (p_line < p_parent.range.start.line) { return ret; - } else if (p_parent.range.start.line == p_line) { + } else if (p_parent.range.start.line == p_line && (p_symbol_name.is_empty() || p_parent.name == p_symbol_name)) { return &p_parent; } else { for (int i = 0; i < p_parent.children.size(); i++) { - ret = search_symbol_defined_at_line(p_line, p_parent.children[i]); + ret = search_symbol_defined_at_line(p_line, p_parent.children[i], p_symbol_name); if (ret) { break; } @@ -645,11 +805,11 @@ Error ExtendGDScriptParser::get_left_function_call(const lsp::Position &p_positi return ERR_METHOD_NOT_FOUND; } -const lsp::DocumentSymbol *ExtendGDScriptParser::get_symbol_defined_at_line(int p_line) const { +const lsp::DocumentSymbol *ExtendGDScriptParser::get_symbol_defined_at_line(int p_line, const String &p_symbol_name) const { if (p_line <= 0) { return &class_symbol; } - return search_symbol_defined_at_line(p_line, class_symbol); + return search_symbol_defined_at_line(p_line, class_symbol, p_symbol_name); } const lsp::DocumentSymbol *ExtendGDScriptParser::get_member_symbol(const String &p_name, const String &p_subclass) const { diff --git a/modules/gdscript/language_server/gdscript_extend_parser.h b/modules/gdscript/language_server/gdscript_extend_parser.h index 4fd27de081..a808f19e5b 100644 --- a/modules/gdscript/language_server/gdscript_extend_parser.h +++ b/modules/gdscript/language_server/gdscript_extend_parser.h @@ -39,6 +39,9 @@ #ifndef LINE_NUMBER_TO_INDEX #define LINE_NUMBER_TO_INDEX(p_line) ((p_line)-1) #endif +#ifndef COLUMN_NUMBER_TO_INDEX +#define COLUMN_NUMBER_TO_INDEX(p_column) ((p_column)-1) +#endif #ifndef SYMBOL_SEPERATOR #define SYMBOL_SEPERATOR "::" @@ -50,6 +53,64 @@ typedef HashMap<String, const lsp::DocumentSymbol *> ClassMembers; +/** + * Represents a Position as used by GDScript Parser. Used for conversion to and from `lsp::Position`. + * + * Difference to `lsp::Position`: + * * Line & Char/column: 1-based + * * LSP: both 0-based + * * Tabs are expanded to columns using tab size (`text_editor/behavior/indent/size`). + * * LSP: tab is single char + * + * Example: + * ```gdscript + * →→var my_value = 42 + * ``` + * `_` is at: + * * Godot: `column=12` + * * using `indent/size=4` + * * Note: counting starts at `1` + * * LSP: `character=8` + * * Note: counting starts at `0` + */ +struct GodotPosition { + int line; + int column; + + GodotPosition(int p_line, int p_column) : + line(p_line), column(p_column) {} + + lsp::Position to_lsp(const Vector<String> &p_lines) const; + static GodotPosition from_lsp(const lsp::Position p_pos, const Vector<String> &p_lines); + + bool operator==(const GodotPosition &p_other) const { + return line == p_other.line && column == p_other.column; + } + + String to_string() const { + return vformat("(%d,%d)", line, column); + } +}; + +struct GodotRange { + GodotPosition start; + GodotPosition end; + + GodotRange(GodotPosition p_start, GodotPosition p_end) : + start(p_start), end(p_end) {} + + lsp::Range to_lsp(const Vector<String> &p_lines) const; + static GodotRange from_lsp(const lsp::Range &p_range, const Vector<String> &p_lines); + + bool operator==(const GodotRange &p_other) const { + return start == p_other.start && end == p_other.end; + } + + String to_string() const { + return vformat("[%s:%s]", start.to_string(), end.to_string()); + } +}; + class ExtendGDScriptParser : public GDScriptParser { String path; Vector<String> lines; @@ -60,6 +121,8 @@ class ExtendGDScriptParser : public GDScriptParser { ClassMembers members; HashMap<String, ClassMembers> inner_classes; + lsp::Range range_of_node(const GDScriptParser::Node *p_node) const; + void update_diagnostics(); void update_symbols(); @@ -70,8 +133,7 @@ class ExtendGDScriptParser : public GDScriptParser { Dictionary dump_function_api(const GDScriptParser::FunctionNode *p_func) const; Dictionary dump_class_api(const GDScriptParser::ClassNode *p_class) const; - String parse_documentation(int p_line, bool p_docs_down = false); - const lsp::DocumentSymbol *search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent) const; + const lsp::DocumentSymbol *search_symbol_defined_at_line(int p_line, const lsp::DocumentSymbol &p_parent, const String &p_symbol_name = "") const; Array member_completions; @@ -87,10 +149,18 @@ public: String get_text_for_completion(const lsp::Position &p_cursor) const; String get_text_for_lookup_symbol(const lsp::Position &p_cursor, const String &p_symbol = "", bool p_func_required = false) const; - String get_identifier_under_position(const lsp::Position &p_position, Vector2i &p_offset) const; + String get_identifier_under_position(const lsp::Position &p_position, lsp::Range &r_range) const; String get_uri() const; - const lsp::DocumentSymbol *get_symbol_defined_at_line(int p_line) const; + /** + * `p_symbol_name` gets ignored if empty. Otherwise symbol must match passed in named. + * + * Necessary when multiple symbols at same line for example with `func`: + * `func handle_arg(arg: int):` + * -> Without `p_symbol_name`: returns `handle_arg`. Even if parameter (`arg`) is wanted. + * With `p_symbol_name`: symbol name MUST match `p_symbol_name`: returns `arg`. + */ + const lsp::DocumentSymbol *get_symbol_defined_at_line(int p_line, const String &p_symbol_name = "") const; const lsp::DocumentSymbol *get_member_symbol(const String &p_name, const String &p_subclass = "") const; const List<lsp::DocumentLink> &get_document_links() const; diff --git a/modules/gdscript/language_server/gdscript_language_protocol.cpp b/modules/gdscript/language_server/gdscript_language_protocol.cpp index 112db4df3a..14fc21d7dc 100644 --- a/modules/gdscript/language_server/gdscript_language_protocol.cpp +++ b/modules/gdscript/language_server/gdscript_language_protocol.cpp @@ -278,6 +278,11 @@ void GDScriptLanguageProtocol::stop() { } void GDScriptLanguageProtocol::notify_client(const String &p_method, const Variant &p_params, int p_client_id) { +#ifdef TESTS_ENABLED + if (clients.is_empty()) { + return; + } +#endif if (p_client_id == -1) { ERR_FAIL_COND_MSG(latest_client_id == -1, "GDScript LSP: Can't notify client as none was connected."); @@ -294,6 +299,11 @@ void GDScriptLanguageProtocol::notify_client(const String &p_method, const Varia } void GDScriptLanguageProtocol::request_client(const String &p_method, const Variant &p_params, int p_client_id) { +#ifdef TESTS_ENABLED + if (clients.is_empty()) { + return; + } +#endif if (p_client_id == -1) { ERR_FAIL_COND_MSG(latest_client_id == -1, "GDScript LSP: Can't notify client as none was connected."); diff --git a/modules/gdscript/language_server/gdscript_text_document.cpp b/modules/gdscript/language_server/gdscript_text_document.cpp index 92a5f55978..1e927f9f6e 100644 --- a/modules/gdscript/language_server/gdscript_text_document.cpp +++ b/modules/gdscript/language_server/gdscript_text_document.cpp @@ -50,6 +50,8 @@ void GDScriptTextDocument::_bind_methods() { ClassDB::bind_method(D_METHOD("completion"), &GDScriptTextDocument::completion); ClassDB::bind_method(D_METHOD("resolve"), &GDScriptTextDocument::resolve); ClassDB::bind_method(D_METHOD("rename"), &GDScriptTextDocument::rename); + ClassDB::bind_method(D_METHOD("prepareRename"), &GDScriptTextDocument::prepareRename); + ClassDB::bind_method(D_METHOD("references"), &GDScriptTextDocument::references); ClassDB::bind_method(D_METHOD("foldingRange"), &GDScriptTextDocument::foldingRange); ClassDB::bind_method(D_METHOD("codeLens"), &GDScriptTextDocument::codeLens); ClassDB::bind_method(D_METHOD("documentLink"), &GDScriptTextDocument::documentLink); @@ -161,11 +163,8 @@ Array GDScriptTextDocument::documentSymbol(const Dictionary &p_params) { String path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(uri); Array arr; if (HashMap<String, ExtendGDScriptParser *>::ConstIterator parser = GDScriptLanguageProtocol::get_singleton()->get_workspace()->scripts.find(path)) { - Vector<lsp::DocumentedSymbolInformation> list; - parser->value->get_symbols().symbol_tree_as_list(uri, list); - for (int i = 0; i < list.size(); i++) { - arr.push_back(list[i].to_json()); - } + lsp::DocumentSymbol symbol = parser->value->get_symbols(); + arr.push_back(symbol.to_json(true)); } return arr; } @@ -253,6 +252,48 @@ Dictionary GDScriptTextDocument::rename(const Dictionary &p_params) { return GDScriptLanguageProtocol::get_singleton()->get_workspace()->rename(params, new_name); } +Variant GDScriptTextDocument::prepareRename(const Dictionary &p_params) { + lsp::TextDocumentPositionParams params; + params.load(p_params); + + lsp::DocumentSymbol symbol; + lsp::Range range; + if (GDScriptLanguageProtocol::get_singleton()->get_workspace()->can_rename(params, symbol, range)) { + return Variant(range.to_json()); + } + + // `null` -> rename not valid at current location. + return Variant(); +} + +Array GDScriptTextDocument::references(const Dictionary &p_params) { + Array res; + + lsp::ReferenceParams params; + params.load(p_params); + + const lsp::DocumentSymbol *symbol = GDScriptLanguageProtocol::get_singleton()->get_workspace()->resolve_symbol(params); + if (symbol) { + Vector<lsp::Location> usages = GDScriptLanguageProtocol::get_singleton()->get_workspace()->find_all_usages(*symbol); + res.resize(usages.size()); + int declaration_adjustment = 0; + for (int i = 0; i < usages.size(); i++) { + lsp::Location usage = usages[i]; + if (!params.context.includeDeclaration && usage.range == symbol->range) { + declaration_adjustment++; + continue; + } + res[i - declaration_adjustment] = usages[i].to_json(); + } + + if (declaration_adjustment > 0) { + res.resize(res.size() - declaration_adjustment); + } + } + + return res; +} + Dictionary GDScriptTextDocument::resolve(const Dictionary &p_params) { lsp::CompletionItem item; item.load(p_params); @@ -450,7 +491,7 @@ Array GDScriptTextDocument::find_symbols(const lsp::TextDocumentPositionParams & if (symbol) { lsp::Location location; location.uri = symbol->uri; - location.range = symbol->range; + location.range = symbol->selectionRange; const String &path = GDScriptLanguageProtocol::get_singleton()->get_workspace()->get_file_path(symbol->uri); if (file_checker->file_exists(path)) { arr.push_back(location.to_json()); @@ -464,7 +505,7 @@ Array GDScriptTextDocument::find_symbols(const lsp::TextDocumentPositionParams & if (!s->uri.is_empty()) { lsp::Location location; location.uri = s->uri; - location.range = s->range; + location.range = s->selectionRange; arr.push_back(location.to_json()); r_list.push_back(s); } diff --git a/modules/gdscript/language_server/gdscript_text_document.h b/modules/gdscript/language_server/gdscript_text_document.h index 0121101db2..cfd0490f0a 100644 --- a/modules/gdscript/language_server/gdscript_text_document.h +++ b/modules/gdscript/language_server/gdscript_text_document.h @@ -65,6 +65,8 @@ public: Array completion(const Dictionary &p_params); Dictionary resolve(const Dictionary &p_params); Dictionary rename(const Dictionary &p_params); + Variant prepareRename(const Dictionary &p_params); + Array references(const Dictionary &p_params); Array foldingRange(const Dictionary &p_params); Array codeLens(const Dictionary &p_params); Array documentLink(const Dictionary &p_params); diff --git a/modules/gdscript/language_server/gdscript_workspace.cpp b/modules/gdscript/language_server/gdscript_workspace.cpp index 9f848b02f5..81933c8c87 100644 --- a/modules/gdscript/language_server/gdscript_workspace.cpp +++ b/modules/gdscript/language_server/gdscript_workspace.cpp @@ -46,7 +46,6 @@ void GDScriptWorkspace::_bind_methods() { ClassDB::bind_method(D_METHOD("apply_new_signal"), &GDScriptWorkspace::apply_new_signal); ClassDB::bind_method(D_METHOD("didDeleteFiles"), &GDScriptWorkspace::did_delete_files); - ClassDB::bind_method(D_METHOD("symbol"), &GDScriptWorkspace::symbol); ClassDB::bind_method(D_METHOD("parse_script", "path", "content"), &GDScriptWorkspace::parse_script); ClassDB::bind_method(D_METHOD("parse_local_script", "path"), &GDScriptWorkspace::parse_local_script); ClassDB::bind_method(D_METHOD("get_file_path", "uri"), &GDScriptWorkspace::get_file_path); @@ -182,35 +181,33 @@ const lsp::DocumentSymbol *GDScriptWorkspace::get_parameter_symbol(const lsp::Do return nullptr; } -const lsp::DocumentSymbol *GDScriptWorkspace::get_local_symbol(const ExtendGDScriptParser *p_parser, const String &p_symbol_identifier) { - const lsp::DocumentSymbol *class_symbol = &p_parser->get_symbols(); +const lsp::DocumentSymbol *GDScriptWorkspace::get_local_symbol_at(const ExtendGDScriptParser *p_parser, const String &p_symbol_identifier, const lsp::Position p_position) { + // Go down and pick closest `DocumentSymbol` with `p_symbol_identifier`. - for (int i = 0; i < class_symbol->children.size(); ++i) { - int kind = class_symbol->children[i].kind; - switch (kind) { - case lsp::SymbolKind::Function: - case lsp::SymbolKind::Method: - case lsp::SymbolKind::Class: { - const lsp::DocumentSymbol *function_symbol = &class_symbol->children[i]; + const lsp::DocumentSymbol *current = &p_parser->get_symbols(); + const lsp::DocumentSymbol *best_match = nullptr; - for (int l = 0; l < function_symbol->children.size(); ++l) { - const lsp::DocumentSymbol *local = &function_symbol->children[l]; - if (!local->detail.is_empty() && local->name == p_symbol_identifier) { - return local; - } - } - } break; + while (current) { + if (current->name == p_symbol_identifier) { + if (current->selectionRange.contains(p_position)) { + // Exact match: pos is ON symbol decl identifier. + return current; + } - case lsp::SymbolKind::Variable: { - const lsp::DocumentSymbol *variable_symbol = &class_symbol->children[i]; - if (variable_symbol->name == p_symbol_identifier) { - return variable_symbol; - } - } break; + best_match = current; + } + + const lsp::DocumentSymbol *parent = current; + current = nullptr; + for (const lsp::DocumentSymbol &child : parent->children) { + if (child.range.contains(p_position)) { + current = &child; + break; + } } } - return nullptr; + return best_match; } void GDScriptWorkspace::reload_all_workspace_scripts() { @@ -275,25 +272,6 @@ ExtendGDScriptParser *GDScriptWorkspace::get_parse_result(const String &p_path) return nullptr; } -Array GDScriptWorkspace::symbol(const Dictionary &p_params) { - String query = p_params["query"]; - Array arr; - if (!query.is_empty()) { - for (const KeyValue<String, ExtendGDScriptParser *> &E : scripts) { - Vector<lsp::DocumentedSymbolInformation> script_symbols; - E.value->get_symbols().symbol_tree_as_list(E.key, script_symbols); - for (int i = 0; i < script_symbols.size(); ++i) { - if (query.is_subsequence_ofn(script_symbols[i].name)) { - lsp::DocumentedSymbolInformation symbol = script_symbols[i]; - symbol.location.uri = get_file_uri(symbol.location.uri); - arr.push_back(symbol.to_json()); - } - } - } - } - return arr; -} - Error GDScriptWorkspace::initialize() { if (initialized) { return OK; @@ -423,7 +401,7 @@ Error GDScriptWorkspace::initialize() { native_members.insert(E.key, members); } - // cache member completions + // Cache member completions. for (const KeyValue<String, ExtendGDScriptParser *> &S : scripts) { S.value->get_member_completions(); } @@ -458,48 +436,110 @@ Error GDScriptWorkspace::parse_script(const String &p_path, const String &p_cont return err; } -Dictionary GDScriptWorkspace::rename(const lsp::TextDocumentPositionParams &p_doc_pos, const String &new_name) { - Error err; - String path = get_file_path(p_doc_pos.textDocument.uri); +static bool is_valid_rename_target(const lsp::DocumentSymbol *p_symbol) { + // Must be valid symbol. + if (!p_symbol) { + return false; + } + + // Cannot rename builtin. + if (!p_symbol->native_class.is_empty()) { + return false; + } + + // Source must be available. + if (p_symbol->script_path.is_empty()) { + return false; + } + return true; +} + +Dictionary GDScriptWorkspace::rename(const lsp::TextDocumentPositionParams &p_doc_pos, const String &new_name) { lsp::WorkspaceEdit edit; - List<String> paths; - list_script_files("res://", paths); + const lsp::DocumentSymbol *reference_symbol = resolve_symbol(p_doc_pos); + if (is_valid_rename_target(reference_symbol)) { + Vector<lsp::Location> usages = find_all_usages(*reference_symbol); + for (int i = 0; i < usages.size(); ++i) { + lsp::Location loc = usages[i]; + + edit.add_change(loc.uri, loc.range.start.line, loc.range.start.character, loc.range.end.character, new_name); + } + } + + return edit.to_json(); +} +bool GDScriptWorkspace::can_rename(const lsp::TextDocumentPositionParams &p_doc_pos, lsp::DocumentSymbol &r_symbol, lsp::Range &r_range) { const lsp::DocumentSymbol *reference_symbol = resolve_symbol(p_doc_pos); - if (reference_symbol) { - String identifier = reference_symbol->name; + if (!is_valid_rename_target(reference_symbol)) { + return false; + } - for (List<String>::Element *PE = paths.front(); PE; PE = PE->next()) { - PackedStringArray content = FileAccess::get_file_as_string(PE->get(), &err).split("\n"); - for (int i = 0; i < content.size(); ++i) { - String line = content[i]; + String path = get_file_path(p_doc_pos.textDocument.uri); + if (const ExtendGDScriptParser *parser = get_parse_result(path)) { + parser->get_identifier_under_position(p_doc_pos.position, r_range); + r_symbol = *reference_symbol; + return true; + } - int character = line.find(identifier); - while (character > -1) { - lsp::TextDocumentPositionParams params; + return false; +} - lsp::TextDocumentIdentifier text_doc; - text_doc.uri = get_file_uri(PE->get()); +Vector<lsp::Location> GDScriptWorkspace::find_usages_in_file(const lsp::DocumentSymbol &p_symbol, const String &p_file_path) { + Vector<lsp::Location> usages; - params.textDocument = text_doc; - params.position.line = i; - params.position.character = character; + String identifier = p_symbol.name; + if (const ExtendGDScriptParser *parser = get_parse_result(p_file_path)) { + const PackedStringArray &content = parser->get_lines(); + for (int i = 0; i < content.size(); ++i) { + String line = content[i]; - const lsp::DocumentSymbol *other_symbol = resolve_symbol(params); + int character = line.find(identifier); + while (character > -1) { + lsp::TextDocumentPositionParams params; - if (other_symbol == reference_symbol) { - edit.add_change(text_doc.uri, i, character, character + identifier.length(), new_name); - } + lsp::TextDocumentIdentifier text_doc; + text_doc.uri = get_file_uri(p_file_path); - character = line.find(identifier, character + 1); + params.textDocument = text_doc; + params.position.line = i; + params.position.character = character; + + const lsp::DocumentSymbol *other_symbol = resolve_symbol(params); + + if (other_symbol == &p_symbol) { + lsp::Location loc; + loc.uri = text_doc.uri; + loc.range.start = params.position; + loc.range.end.line = params.position.line; + loc.range.end.character = params.position.character + identifier.length(); + usages.append(loc); } + + character = line.find(identifier, character + 1); } } } - return edit.to_json(); + return usages; +} + +Vector<lsp::Location> GDScriptWorkspace::find_all_usages(const lsp::DocumentSymbol &p_symbol) { + if (p_symbol.local) { + // Only search in current document. + return find_usages_in_file(p_symbol, p_symbol.script_path); + } + // Search in all documents. + List<String> paths; + list_script_files("res://", paths); + + Vector<lsp::Location> usages; + for (List<String>::Element *PE = paths.front(); PE; PE = PE->next()) { + usages.append_array(find_usages_in_file(p_symbol, PE->get())); + } + return usages; } Error GDScriptWorkspace::parse_local_script(const String &p_path) { @@ -636,9 +676,9 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu lsp::Position pos = p_doc_pos.position; if (symbol_identifier.is_empty()) { - Vector2i offset; - symbol_identifier = parser->get_identifier_under_position(p_doc_pos.position, offset); - pos.character += offset.y; + lsp::Range range; + symbol_identifier = parser->get_identifier_under_position(p_doc_pos.position, range); + pos.character = range.end.character; } if (!symbol_identifier.is_empty()) { @@ -661,7 +701,7 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu } if (const ExtendGDScriptParser *target_parser = get_parse_result(target_script_path)) { - symbol = target_parser->get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(ret.location)); + symbol = target_parser->get_symbol_defined_at_line(LINE_NUMBER_TO_INDEX(ret.location), symbol_identifier); if (symbol) { switch (symbol->kind) { @@ -670,10 +710,6 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu symbol = get_parameter_symbol(symbol, symbol_identifier); } } break; - - case lsp::SymbolKind::Variable: { - symbol = get_local_symbol(parser, symbol_identifier); - } break; } } } @@ -686,10 +722,9 @@ const lsp::DocumentSymbol *GDScriptWorkspace::resolve_symbol(const lsp::TextDocu symbol = get_native_symbol(ret.class_name, member); } } else { - symbol = parser->get_member_symbol(symbol_identifier); - + symbol = get_local_symbol_at(parser, symbol_identifier, p_doc_pos.position); if (!symbol) { - symbol = get_local_symbol(parser, symbol_identifier); + symbol = parser->get_member_symbol(symbol_identifier); } } } @@ -703,8 +738,8 @@ void GDScriptWorkspace::resolve_related_symbols(const lsp::TextDocumentPositionP String path = get_file_path(p_doc_pos.textDocument.uri); if (const ExtendGDScriptParser *parser = get_parse_result(path)) { String symbol_identifier; - Vector2i offset; - symbol_identifier = parser->get_identifier_under_position(p_doc_pos.position, offset); + lsp::Range range; + symbol_identifier = parser->get_identifier_under_position(p_doc_pos.position, range); for (const KeyValue<StringName, ClassMembers> &E : native_members) { const ClassMembers &members = native_members.get(E.key); diff --git a/modules/gdscript/language_server/gdscript_workspace.h b/modules/gdscript/language_server/gdscript_workspace.h index 80653778fb..0b2d43b817 100644 --- a/modules/gdscript/language_server/gdscript_workspace.h +++ b/modules/gdscript/language_server/gdscript_workspace.h @@ -54,7 +54,7 @@ protected: const lsp::DocumentSymbol *get_native_symbol(const String &p_class, const String &p_member = "") const; const lsp::DocumentSymbol *get_script_symbol(const String &p_path) const; const lsp::DocumentSymbol *get_parameter_symbol(const lsp::DocumentSymbol *p_parent, const String &symbol_identifier); - const lsp::DocumentSymbol *get_local_symbol(const ExtendGDScriptParser *p_parser, const String &p_symbol_identifier); + const lsp::DocumentSymbol *get_local_symbol_at(const ExtendGDScriptParser *p_parser, const String &p_symbol_identifier, const lsp::Position p_position); void reload_all_workspace_scripts(); @@ -74,9 +74,6 @@ public: HashMap<StringName, ClassMembers> native_members; public: - Array symbol(const Dictionary &p_params); - -public: Error initialize(); Error parse_script(const String &p_path, const String &p_content); @@ -96,6 +93,9 @@ public: Error resolve_signature(const lsp::TextDocumentPositionParams &p_doc_pos, lsp::SignatureHelp &r_signature); void did_delete_files(const Dictionary &p_params); Dictionary rename(const lsp::TextDocumentPositionParams &p_doc_pos, const String &new_name); + bool can_rename(const lsp::TextDocumentPositionParams &p_doc_pos, lsp::DocumentSymbol &r_symbol, lsp::Range &r_range); + Vector<lsp::Location> find_usages_in_file(const lsp::DocumentSymbol &p_symbol, const String &p_file_path); + Vector<lsp::Location> find_all_usages(const lsp::DocumentSymbol &p_symbol); GDScriptWorkspace(); ~GDScriptWorkspace(); diff --git a/modules/gdscript/language_server/godot_lsp.h b/modules/gdscript/language_server/godot_lsp.h index 3782945e07..1ac4267c7b 100644 --- a/modules/gdscript/language_server/godot_lsp.h +++ b/modules/gdscript/language_server/godot_lsp.h @@ -83,6 +83,14 @@ struct Position { */ int character = 0; + _FORCE_INLINE_ bool operator==(const Position &p_other) const { + return line == p_other.line && character == p_other.character; + } + + String to_string() const { + return vformat("(%d,%d)", line, character); + } + _FORCE_INLINE_ void load(const Dictionary &p_params) { line = p_params["line"]; character = p_params["character"]; @@ -112,6 +120,27 @@ struct Range { */ Position end; + _FORCE_INLINE_ bool operator==(const Range &p_other) const { + return start == p_other.start && end == p_other.end; + } + + bool contains(const Position &p_pos) const { + // Inside line range. + if (start.line <= p_pos.line && p_pos.line <= end.line) { + // If on start line: must come after start char. + bool start_ok = p_pos.line == start.line ? start.character <= p_pos.character : true; + // If on end line: must come before end char. + bool end_ok = p_pos.line == end.line ? p_pos.character <= end.character : true; + return start_ok && end_ok; + } else { + return false; + } + } + + String to_string() const { + return vformat("[%s:%s]", start.to_string(), end.to_string()); + } + _FORCE_INLINE_ void load(const Dictionary &p_params) { start.load(p_params["start"]); end.load(p_params["end"]); @@ -203,6 +232,17 @@ struct TextDocumentPositionParams { } }; +struct ReferenceContext { + /** + * Include the declaration of the current symbol. + */ + bool includeDeclaration; +}; + +struct ReferenceParams : TextDocumentPositionParams { + ReferenceContext context; +}; + struct DocumentLinkParams { /** * The document to provide document links for. @@ -343,8 +383,8 @@ struct Command { } }; -// Use namespace instead of enumeration to follow the LSP specifications -// lsp::EnumName::EnumValue is OK but lsp::EnumValue is not +// Use namespace instead of enumeration to follow the LSP specifications. +// `lsp::EnumName::EnumValue` is OK but `lsp::EnumValue` is not. namespace TextDocumentSyncKind { /** @@ -436,7 +476,7 @@ struct RenameOptions { /** * Renames should be checked and tested before being executed. */ - bool prepareProvider = false; + bool prepareProvider = true; Dictionary to_json() { Dictionary dict; @@ -794,12 +834,12 @@ static const String Markdown = "markdown"; */ struct MarkupContent { /** - * The type of the Markup + * The type of the Markup. */ String kind; /** - * The content itself + * The content itself. */ String value; @@ -821,8 +861,8 @@ struct MarkupContent { }; // Use namespace instead of enumeration to follow the LSP specifications -// lsp::EnumName::EnumValue is OK but lsp::EnumValue is not -// And here C++ compilers are unhappy with our enumeration name like Color, File, RefCounted etc. +// `lsp::EnumName::EnumValue` is OK but `lsp::EnumValue` is not. +// And here C++ compilers are unhappy with our enumeration name like `Color`, `File`, `RefCounted` etc. /** * The kind of a completion entry. */ @@ -854,7 +894,7 @@ static const int Operator = 24; static const int TypeParameter = 25; }; // namespace CompletionItemKind -// Use namespace instead of enumeration to follow the LSP specifications +// Use namespace instead of enumeration to follow the LSP specifications. /** * Defines whether the insert text in a completion item should be interpreted as * plain text or a snippet. @@ -1070,8 +1110,8 @@ struct CompletionList { }; // Use namespace instead of enumeration to follow the LSP specifications -// lsp::EnumName::EnumValue is OK but lsp::EnumValue is not -// And here C++ compilers are unhappy with our enumeration name like String, Array, Object etc +// `lsp::EnumName::EnumValue` is OK but `lsp::EnumValue` is not +// And here C++ compilers are unhappy with our enumeration name like `String`, `Array`, `Object` etc /** * A symbol kind. */ @@ -1105,70 +1145,6 @@ static const int TypeParameter = 26; }; // namespace SymbolKind /** - * Represents information about programming constructs like variables, classes, - * interfaces etc. - */ -struct SymbolInformation { - /** - * The name of this symbol. - */ - String name; - - /** - * The kind of this symbol. - */ - int kind = SymbolKind::File; - - /** - * Indicates if this symbol is deprecated. - */ - bool deprecated = false; - - /** - * The location of this symbol. The location's range is used by a tool - * to reveal the location in the editor. If the symbol is selected in the - * tool the range's start information is used to position the cursor. So - * the range usually spans more then the actual symbol's name and does - * normally include things like visibility modifiers. - * - * The range doesn't have to denote a node range in the sense of a abstract - * syntax tree. It can therefore not be used to re-construct a hierarchy of - * the symbols. - */ - Location location; - - /** - * The name of the symbol containing this symbol. This information is for - * user interface purposes (e.g. to render a qualifier in the user interface - * if necessary). It can't be used to re-infer a hierarchy for the document - * symbols. - */ - String containerName; - - _FORCE_INLINE_ Dictionary to_json() const { - Dictionary dict; - dict["name"] = name; - dict["kind"] = kind; - dict["deprecated"] = deprecated; - dict["location"] = location.to_json(); - dict["containerName"] = containerName; - return dict; - } -}; - -struct DocumentedSymbolInformation : public SymbolInformation { - /** - * A human-readable string with additional information - */ - String detail; - - /** - * A human-readable string that represents a doc-comment. - */ - String documentation; -}; - -/** * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document symbols can be * hierarchical and they have two ranges: one that encloses its definition and one that points to its most interesting range, * e.g. the range of an identifier. @@ -1186,12 +1162,12 @@ struct DocumentSymbol { String detail; /** - * Documentation for this symbol + * Documentation for this symbol. */ String documentation; /** - * Class name for the native symbols + * Class name for the native symbols. */ String native_class; @@ -1206,6 +1182,13 @@ struct DocumentSymbol { bool deprecated = false; /** + * If `true`: Symbol is local to script and cannot be accessed somewhere else. + * + * For example: local variable inside a `func`. + */ + bool local = false; + + /** * The range enclosing this symbol not including leading/trailing whitespace but everything else * like comments. This information is typically used to determine if the clients cursor is * inside the symbol to reveal in the symbol in the UI. @@ -1238,35 +1221,21 @@ struct DocumentSymbol { dict["documentation"] = documentation; dict["native_class"] = native_class; } - Array arr; - arr.resize(children.size()); - for (int i = 0; i < children.size(); i++) { - arr[i] = children[i].to_json(with_doc); + if (!children.is_empty()) { + Array arr; + for (int i = 0; i < children.size(); i++) { + if (children[i].local) { + continue; + } + arr.push_back(children[i].to_json(with_doc)); + } + if (!children.is_empty()) { + dict["children"] = arr; + } } - dict["children"] = arr; return dict; } - void symbol_tree_as_list(const String &p_uri, Vector<DocumentedSymbolInformation> &r_list, const String &p_container = "", bool p_join_name = false) const { - DocumentedSymbolInformation si; - if (p_join_name && !p_container.is_empty()) { - si.name = p_container + ">" + name; - } else { - si.name = name; - } - si.kind = kind; - si.containerName = p_container; - si.deprecated = deprecated; - si.location.uri = p_uri; - si.location.range = range; - si.detail = detail; - si.documentation = documentation; - r_list.push_back(si); - for (int i = 0; i < children.size(); i++) { - children[i].symbol_tree_as_list(p_uri, r_list, si.name, p_join_name); - } - } - _FORCE_INLINE_ MarkupContent render() const { MarkupContent markdown; if (detail.length()) { @@ -1750,7 +1719,7 @@ struct ServerCapabilities { /** * The server provides find references support. */ - bool referencesProvider = false; + bool referencesProvider = true; /** * The server provides document highlight support. diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp index 874cbc6ee8..01772a2e38 100644 --- a/modules/gdscript/tests/gdscript_test_runner.cpp +++ b/modules/gdscript/tests/gdscript_test_runner.cpp @@ -149,6 +149,10 @@ GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_l // Set all warning levels to "Warn" in order to test them properly, even the ones that default to error. ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true); for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) { + if (i == GDScriptWarning::UNTYPED_DECLARATION) { + // TODO: Add ability for test scripts to specify which warnings to enable/disable for testing. + continue; + } String warning_setting = GDScriptWarning::get_settings_path_from_code((GDScriptWarning::Code)i); ProjectSettings::get_singleton()->set_setting(warning_setting, (int)GDScriptWarning::WARN); } diff --git a/modules/gdscript/tests/scripts/analyzer/errors/typed_array_init_with_unconvertable_in_literal.gd b/modules/gdscript/tests/scripts/analyzer/errors/typed_array_init_with_unconvertable_in_literal.gd index 25cde1d40b..7cc5aaf44f 100644 --- a/modules/gdscript/tests/scripts/analyzer/errors/typed_array_init_with_unconvertable_in_literal.gd +++ b/modules/gdscript/tests/scripts/analyzer/errors/typed_array_init_with_unconvertable_in_literal.gd @@ -1,4 +1,4 @@ func test(): - var unconvertable := 1 - var typed: Array[Object] = [unconvertable] + var unconvertible := 1 + var typed: Array[Object] = [unconvertible] print('not ok') diff --git a/modules/gdscript/tests/scripts/lsp/class.notest.gd b/modules/gdscript/tests/scripts/lsp/class.notest.gd new file mode 100644 index 0000000000..53d0b14d72 --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/class.notest.gd @@ -0,0 +1,132 @@ +extends Node + +class Inner1 extends Node: +# ^^^^^^ class1 -> class1 + var member1 := 42 + # ^^^^^^^ class1:member1 -> class1:member1 + var member2 : int = 13 + # ^^^^^^^ class1:member2 -> class1:member2 + var member3 = 1337 + # ^^^^^^^ class1:member3 -> class1:member3 + + signal changed(old, new) + # ^^^^^^^ class1:signal -> class1:signal + func my_func(arg1: int, arg2: String, arg3): + # | | | | | | ^^^^ class1:func:arg3 -> class1:func:arg3 + # | | | | ^^^^ class1:func:arg2 -> class1:func:arg2 + # | | ^^^^ class1:func:arg1 -> class1:func:arg1 + # ^^^^^^^ class1:func -> class1:func + print(arg1, arg2, arg3) + # | | | | ^^^^ -> class1:func:arg3 + # | | ^^^^ -> class1:func:arg2 + # ^^^^ -> class1:func:arg1 + changed.emit(arg1, arg3) + # | | | ^^^^ -> class1:func:arg3 + # | ^^^^ -> class1:func:arg1 + #<^^^^^ -> class1:signal + return arg1 + arg2.length() + arg3 + # | | | | ^^^^ -> class1:func:arg3 + # | | ^^^^ -> class1:func:arg2 + # ^^^^ -> class1:func:arg1 + +class Inner2: +# ^^^^^^ class2 -> class2 + var member1 := 42 + # ^^^^^^^ class2:member1 -> class2:member1 + var member2 : int = 13 + # ^^^^^^^ class2:member2 -> class2:member2 + var member3 = 1337 + # ^^^^^^^ class2:member3 -> class2:member3 + + signal changed(old, new) + # ^^^^^^^ class2:signal -> class2:signal + func my_func(arg1: int, arg2: String, arg3): + # | | | | | | ^^^^ class2:func:arg3 -> class2:func:arg3 + # | | | | ^^^^ class2:func:arg2 -> class2:func:arg2 + # | | ^^^^ class2:func:arg1 -> class2:func:arg1 + # ^^^^^^^ class2:func -> class2:func + print(arg1, arg2, arg3) + # | | | | ^^^^ -> class2:func:arg3 + # | | ^^^^ -> class2:func:arg2 + # ^^^^ -> class2:func:arg1 + changed.emit(arg1, arg3) + # | | | ^^^^ -> class2:func:arg3 + # | ^^^^ -> class2:func:arg1 + #<^^^^^ -> class2:signal + return arg1 + arg2.length() + arg3 + # | | | | ^^^^ -> class2:func:arg3 + # | | ^^^^ -> class2:func:arg2 + # ^^^^ -> class2:func:arg1 + +class Inner3 extends Inner2: +# | | ^^^^^^ -> class2 +# ^^^^^^ class3 -> class3 + var whatever = "foo" + # ^^^^^^^^ class3:whatever -> class3:whatever + + func _init(): + # ^^^^^ class3:init + # Note: no self-ref check here: resolves to `Object._init`. + # usages of `Inner3.new()` DO resolve to this `_init` + pass + + class NestedInInner3: + # ^^^^^^^^^^^^^^ class3:nested1 -> class3:nested1 + var some_value := 42 + # ^^^^^^^^^^ class3:nested1:some_value -> class3:nested1:some_value + + class AnotherNestedInInner3 extends NestedInInner3: + #! | | ^^^^^^^^^^^^^^ -> class3:nested1 + # ^^^^^^^^^^^^^^^^^^^^^ class3:nested2 -> class3:nested2 + var another_value := 13 + # ^^^^^^^^^^^^^ class3:nested2:another_value -> class3:nested2:another_value + +func _ready(): + var inner1 = Inner1.new() + # | | ^^^^^^ -> class1 + # ^^^^^^ func:class1 -> func:class1 + var value1 = inner1.my_func(1,"",3) + # | | | | ^^^^^^^ -> class1:func + # | | ^^^^^^ -> func:class1 + # ^^^^^^ func:class1:value1 -> func:class1:value1 + var value2 = inner1.member3 + # | | | | ^^^^^^^ -> class1:member3 + # | | ^^^^^^ -> func:class1 + # ^^^^^^ func:class1:value2 -> func:class1:value2 + print(value1, value2) + # | | ^^^^^^ -> func:class1:value2 + # ^^^^^^ -> func:class1:value1 + + var inner3 = Inner3.new() + # | | | | ^^^ -> class3:init + # | | ^^^^^^ -> class3 + # ^^^^^^ func:class3 -> func:class3 + print(inner3) + # ^^^^^^ -> func:class3 + + var nested1 = Inner3.NestedInInner3.new() + # | | | | ^^^^^^^^^^^^^^ -> class3:nested1 + # | | ^^^^^^ -> class3 + # ^^^^^^^ func:class3:nested1 -> func:class3:nested1 + var value_nested1 = nested1.some_value + # | | | | ^^^^^^^^^^ -> class3:nested1:some_value + # | | ^^^^^^^ -> func:class3:nested1 + # ^^^^^^^^^^^^^ func:class3:nested1:value + print(value_nested1) + # ^^^^^^^^^^^^^ -> func:class3:nested1:value + + var nested2 = Inner3.AnotherNestedInInner3.new() + # | | | | ^^^^^^^^^^^^^^^^^^^^^ -> class3:nested2 + # | | ^^^^^^ -> class3 + # ^^^^^^^ func:class3:nested2 -> func:class3:nested2 + var value_nested2 = nested2.some_value + # | | | | ^^^^^^^^^^ -> class3:nested1:some_value + # | | ^^^^^^^ -> func:class3:nested2 + # ^^^^^^^^^^^^^ func:class3:nested2:value + var another_value_nested2 = nested2.another_value + # | | | | ^^^^^^^^^^^^^ -> class3:nested2:another_value + # | | ^^^^^^^ -> func:class3:nested2 + # ^^^^^^^^^^^^^^^^^^^^^ func:class3:nested2:another_value_nested + print(value_nested2, another_value_nested2) + # | | ^^^^^^^^^^^^^^^^^^^^^ -> func:class3:nested2:another_value_nested + # ^^^^^^^^^^^^^ -> func:class3:nested2:value diff --git a/modules/gdscript/tests/scripts/lsp/enums.notest.gd b/modules/gdscript/tests/scripts/lsp/enums.notest.gd new file mode 100644 index 0000000000..38b9ec110a --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/enums.notest.gd @@ -0,0 +1,26 @@ +extends Node + +enum {UNIT_NEUTRAL, UNIT_ENEMY, UNIT_ALLY} +# | | | | ^^^^^^^^^ enum:unnamed:ally -> enum:unnamed:ally +# | | ^^^^^^^^^^ enum:unnamed:enemy -> enum:unnamed:enemy +# ^^^^^^^^^^^^ enum:unnamed:neutral -> enum:unnamed:neutral +enum Named {THING_1, THING_2, ANOTHER_THING = -1} +# | | | | | | ^^^^^^^^^^^^^ enum:named:thing3 -> enum:named:thing3 +# | | | | ^^^^^^^ enum:named:thing2 -> enum:named:thing2 +# | | ^^^^^^^ enum:named:thing1 -> enum:named:thing1 +# ^^^^^ enum:named -> enum:named + +func f(arg): + match arg: + UNIT_ENEMY: print(UNIT_ENEMY) + # | ^^^^^^^^^^ -> enum:unnamed:enemy + #<^^^^^^^^ -> enum:unnamed:enemy + Named.THING_2: print(Named.THING_2) + #! | | | | | ^^^^^^^ -> enum:named:thing2 + # | | | ^^^^^ -> enum:named + #! | ^^^^^^^ -> enum:named:thing2 + #<^^^ -> enum:named + _: print(UNIT_ENEMY, Named.ANOTHER_THING) + #! | | | | ^^^^^^^^^^^^^ -> enum:named:thing3 + # | | ^^^^^ -> enum:named + # ^^^^^^^^^^ -> enum:unnamed:enemy diff --git a/modules/gdscript/tests/scripts/lsp/indentation.notest.gd b/modules/gdscript/tests/scripts/lsp/indentation.notest.gd new file mode 100644 index 0000000000..c25d73a719 --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/indentation.notest.gd @@ -0,0 +1,28 @@ +extends Node + +var root = 0 +# ^^^^ 0_indent -> 0_indent + +func a(): + var alpha: int = root + 42 + # | | ^^^^ -> 0_indent + # ^^^^^ 1_indent -> 1_indent + if alpha > 42: + # ^^^^^ -> 1_indent + var beta := alpha + 13 + # | | ^^^^ -> 1_indent + # ^^^^ 2_indent -> 2_indent + if beta > alpha: + # | | ^^^^^ -> 1_indent + # ^^^^ -> 2_indent + var gamma = beta + 1 + # | | ^^^^ -> 2_indent + # ^^^^^ 3_indent -> 3_indent + print(gamma) + # ^^^^^ -> 3_indent + print(beta) + # ^^^^ -> 2_indent + print(alpha) + # ^^^^^ -> 1_indent + print(root) + # ^^^^ -> 0_indent diff --git a/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd b/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd new file mode 100644 index 0000000000..6f5d468eea --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/lambdas.notest.gd @@ -0,0 +1,91 @@ +extends Node + +var lambda_member1 := func(alpha: int, beta): return alpha + beta +# | | | | | | | | ^^^^ -> \1:beta +# | | | | | | ^^^^^ -> \1:alpha +# | | | | ^^^^ \1:beta -> \1:beta +# | | ^^^^^ \1:alpha -> \1:alpha +# ^^^^^^^^^^^^^^ \1 -> \1 + +var lambda_member2 := func(alpha, beta: int) -> int: +# | | | | | | +# | | | | | | +# | | | | ^^^^ \2:beta -> \2:beta +# | | ^^^^^ \2:alpha -> \2:alpha +# ^^^^^^^^^^^^^^ \2 -> \2 + return alpha + beta + # | | ^^^^ -> \2:beta + # ^^^^^ -> \2:alpha + +var lambda_member3 := func add_values(alpha, beta): return alpha + beta +# | | | | | | | | ^^^^ -> \3:beta +# | | | | | | ^^^^^ -> \3:alpha +# | | | | ^^^^ \3:beta -> \3:beta +# | | ^^^^^ \3:alpha -> \3:alpha +# ^^^^^^^^^^^^^^ \3 -> \3 + +var lambda_multiline = func(alpha: int, beta: int) -> int: +# | | | | | | +# | | | | | | +# | | | | ^^^^ \multi:beta -> \multi:beta +# | | ^^^^^ \multi:alpha -> \multi:alpha +# ^^^^^^^^^^^^^^^^ \multi -> \multi + print(alpha + beta) + # | | ^^^^ -> \multi:beta + # ^^^^^ -> \multi:alpha + var tmp = alpha + beta + 42 + # | | | | ^^^^ -> \multi:beta + # | | ^^^^^ -> \multi:alpha + # ^^^ \multi:tmp -> \multi:tmp + print(tmp) + # ^^^ -> \multi:tmp + if tmp > 50: + # ^^^ -> \multi:tmp + tmp += alpha + # | ^^^^^ -> \multi:alpha + #<^ -> \multi:tmp + else: + tmp -= beta + # | ^^^^ -> \multi:beta + #<^ -> \multi:tmp + print(tmp) + # ^^^ -> \multi:tmp + return beta + tmp + alpha + # | | | | ^^^^^ -> \multi:alpha + # | | ^^^ -> \multi:tmp + # ^^^^ -> \multi:beta + + +var some_name := "foo bar" +# ^^^^^^^^^ member:some_name -> member:some_name + +func _ready() -> void: + var a = lambda_member1.call(1,2) + # ^^^^^^^^^^^^^^ -> \1 + var b = lambda_member2.call(1,2) + # ^^^^^^^^^^^^^^ -> \2 + var c = lambda_member3.call(1,2) + # ^^^^^^^^^^^^^^ -> \3 + var d = lambda_multiline.call(1,2) + # ^^^^^^^^^^^^^^^^ -> \multi + print(a,b,c,d) + + var lambda_local = func(alpha, beta): return alpha + beta + # | | | | | | | | ^^^^ -> \local:beta + # | | | | | | ^^^^^ -> \local:alpha + # | | | | ^^^^ \local:beta -> \local:beta + # | | ^^^^^ \local:alpha -> \local:alpha + # ^^^^^^^^^^^^ \local -> \local + + var value := 42 + # ^^^^^ local:value -> local:value + var lambda_capture = func(): return value + some_name.length() + # | | | | ^^^^^^^^^ -> member:some_name + # | | ^^^^^ -> local:value + # ^^^^^^^^^^^^^^ \capture -> \capture + + var z = lambda_local.call(1,2) + # ^^^^^^^^^^^^ -> \local + var x = lambda_capture.call() + # ^^^^^^^^^^^^^^ -> \capture + print(z,x) diff --git a/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd b/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd new file mode 100644 index 0000000000..b6cc46f7da --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/local_variables.notest.gd @@ -0,0 +1,25 @@ +extends Node + +var member := 2 +# ^^^^^^ member -> member + +func test_member() -> void: + var test := member + 42 + # | | ^^^^^^ -> member + # ^^^^ test -> test + test += 3 + #<^^ -> test + member += 5 + #<^^^^ -> member + test = return_arg(test) + # | ^^^^ -> test + #<^^ -> test + print(test) + # ^^^^ -> test + +func return_arg(arg: int) -> int: +# ^^^ arg -> arg + arg += 2 + #<^ -> arg + return arg + # ^^^ -> arg
\ No newline at end of file diff --git a/modules/gdscript/tests/scripts/lsp/properties.notest.gd b/modules/gdscript/tests/scripts/lsp/properties.notest.gd new file mode 100644 index 0000000000..8dfaee2e5b --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/properties.notest.gd @@ -0,0 +1,65 @@ +extends Node + +var prop1 := 42 +# ^^^^^ prop1 -> prop1 +var prop2 : int = 42 +# ^^^^^ prop2 -> prop2 +var prop3 := 42: +# ^^^^^ prop3 -> prop3 + get: + return prop3 + 13 + # ^^^^^ -> prop3 + set(value): + # ^^^^^ prop3:value -> prop3:value + prop3 = value - 13 + # | ^^^^^ -> prop3:value + #<^^^ -> prop3 +var prop4: int: +# ^^^^^ prop4 -> prop4 + get: + return 42 +var prop5 := 42: +# ^^^^^ prop5 -> prop5 + set(value): + # ^^^^^ prop5:value -> prop5:value + prop5 = value - 13 + # | ^^^^^ -> prop5:value + #<^^^ -> prop5 + +var prop6: +# ^^^^^ prop6 -> prop6 + get = get_prop6, + # ^^^^^^^^^ -> get_prop6 + set = set_prop6 + # ^^^^^^^^^ -> set_prop6 +func get_prop6(): +# ^^^^^^^^^ get_prop6 -> get_prop6 + return 42 +func set_prop6(value): +# | | ^^^^^ set_prop6:value -> set_prop6:value +# ^^^^^^^^^ set_prop6 -> set_prop6 + print(value) + # ^^^^^ -> set_prop6:value + +var prop7: +# ^^^^^ prop7 -> prop7 + get = get_prop7 + # ^^^^^^^^^ -> get_prop7 +func get_prop7(): +# ^^^^^^^^^ get_prop7 -> get_prop7 + return 42 + +var prop8: +# ^^^^^ prop8 -> prop8 + set = set_prop8 + # ^^^^^^^^^ -> set_prop8 +func set_prop8(value): +# | | ^^^^^ set_prop8:value -> set_prop8:value +# ^^^^^^^^^ set_prop8 -> set_prop8 + print(value) + # ^^^^^ -> set_prop8:value + +const const_var := 42 +# ^^^^^^^^^ const_var -> const_var +static var static_var := 42 +# ^^^^^^^^^^ static_var -> static_var diff --git a/modules/gdscript/tests/scripts/lsp/scopes.notest.gd b/modules/gdscript/tests/scripts/lsp/scopes.notest.gd new file mode 100644 index 0000000000..20b8fb9bd7 --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/scopes.notest.gd @@ -0,0 +1,106 @@ +extends Node + +var member := 2 +# ^^^^^^ public -> public + +signal some_changed(new_value) +# | | ^^^^^^^^^ signal:parameter -> signal:parameter +# ^^^^^^^^^^^^ signal -> signal +var some_value := 42: +# ^^^^^^^^^^ property -> property + get: + return some_value + # ^^^^^^^^^^ -> property + set(value): + # ^^^^^ property:set:value -> property:set:value + some_changed.emit(value) + # | ^^^^^ -> property:set:value + #<^^^^^^^^^^ -> signal + some_value = value + # | ^^^^^ -> property:set:value + #<^^^^^^^^ -> property + +func v(): + var value := member + 2 + # | | ^^^^^^ -> public + # ^^^^^ v:value -> v:value + print(value) + # ^^^^^ -> v:value + if value > 0: + # ^^^^^ -> v:value + var beta := value + 2 + # | | ^^^^^ -> v:value + # ^^^^ v:if:beta -> v:if:beta + print(beta) + # ^^^^ -> v:if:beta + + for counter in beta: + # | | ^^^^ -> v:if:beta + # ^^^^^^^ v:if:counter -> v:if:counter + print (counter) + # ^^^^^^^ -> v:if:counter + + else: + for counter in value: + # | | ^^^^^ -> v:value + # ^^^^^^^ v:else:counter -> v:else:counter + print(counter) + # ^^^^^^^ -> v:else:counter + +func f(): + var func1 = func(value): print(value + 13) + # | | | | ^^^^^ -> f:func1:value + # | | ^^^^^ f:func1:value -> f:func1:value + # ^^^^^ f:func1 -> f:func1 + var func2 = func(value): print(value + 42) + # | | | | ^^^^^ -> f:func2:value + # | | ^^^^^ f:func2:value -> f:func2:value + # ^^^^^ f:func2 -> f:func2 + + func1.call(1) + #<^^^ -> f:func1 + func2.call(2) + #<^^^ -> f:func2 + +func m(): + var value = 42 + # ^^^^^ m:value -> m:value + + match value: + # ^^^^^ -> m:value + 13: + print(value) + # ^^^^^ -> m:value + [var start, _, var end]: + # | | ^^^ m:match:array:end -> m:match:array:end + # ^^^^^ m:match:array:start -> m:match:array:start + print(start + end) + # | | ^^^ -> m:match:array:end + # ^^^^^ -> m:match:array:start + { "name": var name }: + # ^^^^ m:match:dict:var -> m:match:dict:var + print(name) + # ^^^^ -> m:match:dict:var + var whatever: + # ^^^^^^^^ m:match:var -> m:match:var + print(whatever) + # ^^^^^^^^ -> m:match:var + +func m2(): + var value = 42 + # ^^^^^ m2:value -> m2:value + + match value: + # ^^^^^ -> m2:value + { "name": var name }: + # ^^^^ m2:match:dict:var -> m2:match:dict:var + print(name) + # ^^^^ -> m2:match:dict:var + [var name, ..]: + # ^^^^ m2:match:array:var -> m2:match:array:var + print(name) + # ^^^^ -> m2:match:array:var + var name: + # ^^^^ m2:match:var -> m2:match:var + print(name) + # ^^^^ -> m2:match:var diff --git a/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd new file mode 100644 index 0000000000..338000fa0e --- /dev/null +++ b/modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd @@ -0,0 +1,56 @@ +extends Node + +var value := 42 +# ^^^^^ member:value -> member:value + +func variable(): + var value = value + 42 + #! | | ^^^^^ -> member:value + # ^^^^^ variable:value -> variable:value + print(value) + # ^^^^^ -> variable:value + +func array(): + var value = [1,value,3,value+4] + #! | | | | ^^^^^ -> member:value + #! | | ^^^^^ -> member:value + # ^^^^^ array:value -> array:value + print(value) + # ^^^^^ -> array:value + +func dictionary(): + var value = { + # ^^^^^ dictionary:value -> dictionary:value + "key1": value, + #! ^^^^^ -> member:value + "key2": 1 + value + 3, + #! ^^^^^ -> member:value + } + print(value) + # ^^^^^ -> dictionary:value + +func for_loop(): + for value in value: + # | | ^^^^^ -> member:value + # ^^^^^ for:value -> for:value + print(value) + # ^^^^^ -> for:value + +func for_range(): + for value in range(5, value): + # | | ^^^^^ -> member:value + # ^^^^^ for:range:value -> for:range:value + print(value) + # ^^^^^ -> for:range:value + +func matching(): + match value: + # ^^^^^ -> member:value + 42: print(value) + # ^^^^^ -> member:value + [var value, ..]: print(value) + # | | ^^^^^ -> match:array:value + # ^^^^^ match:array:value -> match:array:value + var value: print(value) + # | | ^^^^^ -> match:var:value + # ^^^^^ match:var:value -> match:var:value diff --git a/modules/gdscript/tests/test_lsp.h b/modules/gdscript/tests/test_lsp.h new file mode 100644 index 0000000000..e57df00e2d --- /dev/null +++ b/modules/gdscript/tests/test_lsp.h @@ -0,0 +1,480 @@ +/**************************************************************************/ +/* test_lsp.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_LSP_H +#define TEST_LSP_H + +#ifdef TOOLS_ENABLED + +#include "tests/test_macros.h" + +#include "../language_server/gdscript_extend_parser.h" +#include "../language_server/gdscript_language_protocol.h" +#include "../language_server/gdscript_workspace.h" +#include "../language_server/godot_lsp.h" + +#include "core/io/dir_access.h" +#include "core/io/file_access_pack.h" +#include "core/os/os.h" +#include "editor/editor_help.h" +#include "editor/editor_node.h" +#include "modules/gdscript/gdscript_analyzer.h" +#include "modules/regex/regex.h" + +#include "thirdparty/doctest/doctest.h" + +template <> +struct doctest::StringMaker<lsp::Position> { + static doctest::String convert(const lsp::Position &p_val) { + return p_val.to_string().utf8().get_data(); + } +}; + +template <> +struct doctest::StringMaker<lsp::Range> { + static doctest::String convert(const lsp::Range &p_val) { + return p_val.to_string().utf8().get_data(); + } +}; + +template <> +struct doctest::StringMaker<GodotPosition> { + static doctest::String convert(const GodotPosition &p_val) { + return p_val.to_string().utf8().get_data(); + } +}; + +namespace GDScriptTests { + +// LSP GDScript test scripts are located inside project of other GDScript tests: +// Cannot reset `ProjectSettings` (singleton) -> Cannot load another workspace and resources in there. +// -> Reuse GDScript test project. LSP specific scripts are then placed inside `lsp` folder. +// Access via `res://lsp/my_script.notest.gd`. +const String root = "modules/gdscript/tests/scripts/"; + +/* + * After use: + * * `memdelete` returned `GDScriptLanguageProtocol`. + * * Call `GDScriptTests::::finish_language`. + */ +GDScriptLanguageProtocol *initialize(const String &p_root) { + Error err = OK; + Ref<DirAccess> dir(DirAccess::open(p_root, &err)); + REQUIRE_MESSAGE(err == OK, "Could not open specified root directory"); + String absolute_root = dir->get_current_dir(); + init_language(absolute_root); + + GDScriptLanguageProtocol *proto = memnew(GDScriptLanguageProtocol); + + Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace(); + workspace->root = absolute_root; + // On windows: `C:/...` -> `C%3A/...`. + workspace->root_uri = "file:///" + absolute_root.lstrip("/").replace_first(":", "%3A"); + + return proto; +} + +lsp::Position pos(const int p_line, const int p_character) { + lsp::Position p; + p.line = p_line; + p.character = p_character; + return p; +} + +lsp::Range range(const lsp::Position p_start, const lsp::Position p_end) { + lsp::Range r; + r.start = p_start; + r.end = p_end; + return r; +} + +lsp::TextDocumentPositionParams pos_in(const lsp::DocumentUri &p_uri, const lsp::Position p_pos) { + lsp::TextDocumentPositionParams params; + params.textDocument.uri = p_uri; + params.position = p_pos; + return params; +} + +const lsp::DocumentSymbol *test_resolve_symbol_at(const String &p_uri, const lsp::Position p_pos, const String &p_expected_uri, const String &p_expected_name, const lsp::Range &p_expected_range) { + Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace(); + + lsp::TextDocumentPositionParams params = pos_in(p_uri, p_pos); + const lsp::DocumentSymbol *symbol = workspace->resolve_symbol(params); + CHECK(symbol); + + if (symbol) { + CHECK_EQ(symbol->uri, p_expected_uri); + CHECK_EQ(symbol->name, p_expected_name); + CHECK_EQ(symbol->selectionRange, p_expected_range); + } + + return symbol; +} + +struct InlineTestData { + lsp::Range range; + String text; + String name; + String ref; + + static bool try_parse(const Vector<String> &p_lines, const int p_line_number, InlineTestData &r_data) { + String line = p_lines[p_line_number]; + + RegEx regex = RegEx("^\\t*#[ |]*(?<range>(?<left><)?\\^+)(\\s+(?<name>(?!->)\\S+))?(\\s+->\\s+(?<ref>\\S+))?"); + Ref<RegExMatch> match = regex.search(line); + if (match.is_null()) { + return false; + } + + // Find first line without leading comment above current line. + int target_line = p_line_number; + while (target_line >= 0) { + String dedented = p_lines[target_line].lstrip("\t"); + if (!dedented.begins_with("#")) { + break; + } + target_line--; + } + if (target_line < 0) { + return false; + } + r_data.range.start.line = r_data.range.end.line = target_line; + + String marker = match->get_string("range"); + int i = line.find(marker); + REQUIRE(i >= 0); + r_data.range.start.character = i; + if (!match->get_string("left").is_empty()) { + // Include `#` (comment char) in range. + r_data.range.start.character--; + } + r_data.range.end.character = i + marker.length(); + + String target = p_lines[target_line]; + r_data.text = target.substr(r_data.range.start.character, r_data.range.end.character - r_data.range.start.character); + + r_data.name = match->get_string("name"); + r_data.ref = match->get_string("ref"); + + return true; + } +}; + +Vector<InlineTestData> read_tests(const String &p_path) { + Error err; + String source = FileAccess::get_file_as_string(p_path, &err); + REQUIRE_MESSAGE(err == OK, vformat("Cannot read '%s'", p_path)); + + // Format: + // ```gdscript + // var foo = bar + baz + // # | | | | ^^^ name -> ref + // # | | ^^^ -> ref + // # ^^^ name + // + // func my_func(): + // # ^^^^^^^ name + // var value = foo + 42 + // # ^^^^^ name + // print(value) + // # ^^^^^ -> ref + // ``` + // + // * `^`: Range marker. + // * `name`: Unique name. Can contain any characters except whitespace chars. + // * `ref`: Reference to unique name. + // + // Notes: + // * If range should include first content-char (which is occupied by `#`): use `<` for next marker. + // -> Range expands 1 to left (-> includes `#`). + // * Note: Means: Range cannot be single char directly marked by `#`, but must be at least two chars (marked with `#<`). + // * Comment must start at same ident as line its marked (-> because of tab alignment...). + // * Use spaces to align after `#`! -> for correct alignment + // * Between `#` and `^` can be spaces or `|` (to better visualize what's marked below). + PackedStringArray lines = source.split("\n"); + + PackedStringArray names; + Vector<InlineTestData> data; + for (int i = 0; i < lines.size(); i++) { + InlineTestData d; + if (InlineTestData::try_parse(lines, i, d)) { + if (!d.name.is_empty()) { + // Safety check: names must be unique. + if (names.find(d.name) != -1) { + FAIL(vformat("Duplicated name '%s' in '%s'. Names must be unique!", d.name, p_path)); + } + names.append(d.name); + } + + data.append(d); + } + } + + return data; +} + +void test_resolve_symbol(const String &p_uri, const InlineTestData &p_test_data, const Vector<InlineTestData> &p_all_data) { + if (p_test_data.ref.is_empty()) { + return; + } + + SUBCASE(vformat("Can resolve symbol '%s' at %s to '%s'", p_test_data.text, p_test_data.range.to_string(), p_test_data.ref).utf8().get_data()) { + const InlineTestData *target = nullptr; + for (int i = 0; i < p_all_data.size(); i++) { + if (p_all_data[i].name == p_test_data.ref) { + target = &p_all_data[i]; + break; + } + } + REQUIRE_MESSAGE(target, vformat("No target for ref '%s'", p_test_data.ref)); + + Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace(); + lsp::Position pos = p_test_data.range.start; + + SUBCASE("start of identifier") { + pos.character = p_test_data.range.start.character; + test_resolve_symbol_at(p_uri, pos, p_uri, target->text, target->range); + } + + SUBCASE("inside identifier") { + pos.character = (p_test_data.range.end.character + p_test_data.range.start.character) / 2; + test_resolve_symbol_at(p_uri, pos, p_uri, target->text, target->range); + } + + SUBCASE("end of identifier") { + pos.character = p_test_data.range.end.character; + test_resolve_symbol_at(p_uri, pos, p_uri, target->text, target->range); + } + } +} + +Vector<InlineTestData> filter_ref_towards(const Vector<InlineTestData> &p_data, const String &p_name) { + Vector<InlineTestData> res; + + for (const InlineTestData &d : p_data) { + if (d.ref == p_name) { + res.append(d); + } + } + + return res; +} + +void test_resolve_symbols(const String &p_uri, const Vector<InlineTestData> &p_test_data, const Vector<InlineTestData> &p_all_data) { + for (const InlineTestData &d : p_test_data) { + test_resolve_symbol(p_uri, d, p_all_data); + } +} + +void assert_no_errors_in(const String &p_path) { + Error err; + String source = FileAccess::get_file_as_string(p_path, &err); + REQUIRE_MESSAGE(err == OK, vformat("Cannot read '%s'", p_path)); + + GDScriptParser parser; + err = parser.parse(source, p_path, true); + REQUIRE_MESSAGE(err == OK, vformat("Errors while parsing '%s'", p_path)); + + GDScriptAnalyzer analyzer(&parser); + err = analyzer.analyze(); + REQUIRE_MESSAGE(err == OK, vformat("Errors while analyzing '%s'", p_path)); +} + +inline lsp::Position lsp_pos(int line, int character) { + lsp::Position p; + p.line = line; + p.character = character; + return p; +} + +void test_position_roundtrip(lsp::Position p_lsp, GodotPosition p_gd, const PackedStringArray &p_lines) { + GodotPosition actual_gd = GodotPosition::from_lsp(p_lsp, p_lines); + CHECK_EQ(p_gd, actual_gd); + lsp::Position actual_lsp = p_gd.to_lsp(p_lines); + CHECK_EQ(p_lsp, actual_lsp); +} + +// Note: +// * Cursor is BETWEEN chars +// * `va|r` -> cursor between `a`&`r` +// * `var` +// ^ +// -> Character on `r` -> cursor between `a`&`r`s for tests: +// * Line & Char: +// * LSP: both 0-based +// * Godot: both 1-based +TEST_SUITE("[Modules][GDScript][LSP]") { + TEST_CASE("Can convert positions to and from Godot") { + String code = R"(extends Node + +var member := 42 + +func f(): + var value := 42 + return value + member)"; + PackedStringArray lines = code.split("\n"); + + SUBCASE("line after end") { + lsp::Position lsp = lsp_pos(7, 0); + GodotPosition gd(8, 1); + test_position_roundtrip(lsp, gd, lines); + } + SUBCASE("first char in first line") { + lsp::Position lsp = lsp_pos(0, 0); + GodotPosition gd(1, 1); + test_position_roundtrip(lsp, gd, lines); + } + + SUBCASE("with tabs") { + // On `v` in `value` in `var value := ...`. + lsp::Position lsp = lsp_pos(5, 6); + GodotPosition gd(6, 13); + test_position_roundtrip(lsp, gd, lines); + } + + SUBCASE("doesn't fail with column outside of character length") { + lsp::Position lsp = lsp_pos(2, 100); + GodotPosition::from_lsp(lsp, lines); + + GodotPosition gd(3, 100); + gd.to_lsp(lines); + } + + SUBCASE("doesn't fail with line outside of line length") { + lsp::Position lsp = lsp_pos(200, 100); + GodotPosition::from_lsp(lsp, lines); + + GodotPosition gd(300, 100); + gd.to_lsp(lines); + } + + SUBCASE("special case: negative line for root class") { + GodotPosition gd(-1, 0); + lsp::Position expected = lsp_pos(0, 0); + lsp::Position actual = gd.to_lsp(lines); + CHECK_EQ(actual, expected); + } + SUBCASE("special case: lines.length() + 1 for root class") { + GodotPosition gd(lines.size() + 1, 0); + lsp::Position expected = lsp_pos(lines.size(), 0); + lsp::Position actual = gd.to_lsp(lines); + CHECK_EQ(actual, expected); + } + } + TEST_CASE("[workspace][resolve_symbol]") { + GDScriptLanguageProtocol *proto = initialize(root); + REQUIRE(proto); + Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace(); + + { + String path = "res://lsp/local_variables.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + SUBCASE("Can get correct ranges for public variables") { + Vector<InlineTestData> test_data = filter_ref_towards(all_test_data, "member"); + test_resolve_symbols(uri, test_data, all_test_data); + } + SUBCASE("Can get correct ranges for local variables") { + Vector<InlineTestData> test_data = filter_ref_towards(all_test_data, "test"); + test_resolve_symbols(uri, test_data, all_test_data); + } + SUBCASE("Can get correct ranges for local parameters") { + Vector<InlineTestData> test_data = filter_ref_towards(all_test_data, "arg"); + test_resolve_symbols(uri, test_data, all_test_data); + } + } + + SUBCASE("Can get correct ranges for indented variables") { + String path = "res://lsp/indentation.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for scopes") { + String path = "res://lsp/scopes.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for lambda") { + String path = "res://lsp/lambdas.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for inner class") { + String path = "res://lsp/class.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for inner class") { + String path = "res://lsp/enums.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for shadowing & shadowed variables") { + String path = "res://lsp/shadowing_initializer.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + SUBCASE("Can get correct ranges for properties and getter/setter") { + String path = "res://lsp/properties.notest.gd"; + assert_no_errors_in(path); + String uri = workspace->get_file_uri(path); + Vector<InlineTestData> all_test_data = read_tests(path); + test_resolve_symbols(uri, all_test_data, all_test_data); + } + + memdelete(proto); + finish_language(); + } +} + +} // namespace GDScriptTests + +#endif // TOOLS_ENABLED + +#endif // TEST_LSP_H diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 2498cd1eb4..6d1c731e7a 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -289,7 +289,7 @@ private: bool on_state_loss_pending(); bool on_state_exiting(); - // convencience + // convenience void copy_string_to_char_buffer(const String p_string, char *p_buffer, int p_buffer_len); public: diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index 70c72dab07..67b14692a2 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -49,11 +49,11 @@ void GPUParticles2D::set_emitting(bool p_emitting) { // Last cycle ended. active = true; time = 0; - signal_cancled = false; + signal_canceled = false; emission_time = lifetime; active_time = lifetime * (2 - explosiveness_ratio); } else { - signal_cancled = true; + signal_canceled = true; } set_process_internal(true); } else if (!p_emitting) { @@ -429,7 +429,7 @@ void GPUParticles2D::restart() { emitting = true; active = true; - signal_cancled = false; + signal_canceled = false; time = 0; emission_time = lifetime; active_time = lifetime * (2 - explosiveness_ratio); @@ -701,7 +701,7 @@ void GPUParticles2D::_notification(int p_what) { } } if (time > active_time) { - if (active && !signal_cancled) { + if (active && !signal_canceled) { emit_signal(SceneStringNames::get_singleton()->finished); } active = false; diff --git a/scene/2d/gpu_particles_2d.h b/scene/2d/gpu_particles_2d.h index 97690b07fa..3a342e2c22 100644 --- a/scene/2d/gpu_particles_2d.h +++ b/scene/2d/gpu_particles_2d.h @@ -49,7 +49,7 @@ private: bool emitting = false; bool active = false; - bool signal_cancled = false; + bool signal_canceled = false; bool one_shot = false; int amount = 0; double lifetime = 0.0; diff --git a/scene/2d/tile_map.cpp b/scene/2d/tile_map.cpp index 6938358994..0c1aab6198 100644 --- a/scene/2d/tile_map.cpp +++ b/scene/2d/tile_map.cpp @@ -548,7 +548,7 @@ void TileMapLayer::_rendering_occluders_update_cell(CellData &r_cell_data) { RID occluder_id = rs->canvas_light_occluder_create(); rs->canvas_light_occluder_set_enabled(occluder_id, node_visible); rs->canvas_light_occluder_set_transform(occluder_id, tile_map_node->get_global_transform() * xform); - rs->canvas_light_occluder_set_polygon(occluder_id, tile_data->get_occluder(i)->get_rid()); + rs->canvas_light_occluder_set_polygon(occluder_id, tile_map_node->get_transformed_polygon(Ref<Resource>(tile_data->get_occluder(i)), r_cell_data.cell.alternative_tile)->get_rid()); rs->canvas_light_occluder_attach_to_canvas(occluder_id, tile_map_node->get_canvas()); rs->canvas_light_occluder_set_light_mask(occluder_id, tile_set->get_occlusion_layer_light_mask(i)); r_cell_data.occluders.push_back(occluder_id); @@ -783,6 +783,7 @@ void TileMapLayer::_physics_update_cell(CellData &r_cell_data) { for (int shape_index = 0; shape_index < shapes_count; shape_index++) { // Add decomposed convex shapes. Ref<ConvexPolygonShape2D> shape = tile_data->get_collision_polygon_shape(tile_set_physics_layer, polygon_index, shape_index); + shape = tile_map_node->get_transformed_polygon(Ref<Resource>(shape), c.alternative_tile); ps->body_add_shape(body, shape->get_rid()); ps->body_set_shape_as_one_way_collision(body, body_shape_index, one_way_collision, one_way_collision_margin); @@ -985,6 +986,7 @@ void TileMapLayer::_navigation_update_cell(CellData &r_cell_data) { for (unsigned int navigation_layer_index = 0; navigation_layer_index < r_cell_data.navigation_regions.size(); navigation_layer_index++) { Ref<NavigationPolygon> navigation_polygon; navigation_polygon = tile_data->get_navigation_polygon(navigation_layer_index); + navigation_polygon = tile_map_node->get_transformed_polygon(Ref<Resource>(navigation_polygon), c.alternative_tile); RID ®ion = r_cell_data.navigation_regions[navigation_layer_index]; @@ -1074,6 +1076,7 @@ void TileMapLayer::_navigation_draw_cell_debug(const RID &p_canvas_item, const V for (int layer_index = 0; layer_index < tile_set->get_navigation_layers_count(); layer_index++) { Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(layer_index); if (navigation_polygon.is_valid()) { + navigation_polygon = tile_map_node->get_transformed_polygon(Ref<Resource>(navigation_polygon), c.alternative_tile); Vector<Vector2> navigation_polygon_vertices = navigation_polygon->get_vertices(); if (navigation_polygon_vertices.size() < 3) { continue; @@ -2288,21 +2291,14 @@ TypedArray<Vector2i> TileMapLayer::get_used_cells_by_id(int p_source_id, const V Rect2i TileMapLayer::get_used_rect() const { // Return the rect of the currently used area. if (used_rect_cache_dirty) { - bool first = true; used_rect_cache = Rect2i(); if (tile_map.size() > 0) { - if (first) { - used_rect_cache = Rect2i(tile_map.begin()->key.x, tile_map.begin()->key.y, 0, 0); - first = false; - } + used_rect_cache = Rect2i(tile_map.begin()->key.x, tile_map.begin()->key.y, 0, 0); for (const KeyValue<Vector2i, CellData> &E : tile_map) { - used_rect_cache.expand_to(Vector2i(E.key.x, E.key.y)); + used_rect_cache.expand_to(E.key); } - } - - if (!first) { // first is true if every layer is empty. used_rect_cache.size += Vector2i(1, 1); // The cache expands to top-left coordinate, so we add one full tile. } used_rect_cache_dirty = false; @@ -3012,6 +3008,7 @@ void TileMap::_internal_update() { } // Update dirty quadrants on layers. + polygon_cache.clear(); for (Ref<TileMapLayer> &layer : layers) { layer->internal_update(); } @@ -3100,18 +3097,18 @@ void TileMap::draw_tile(RID p_canvas_item, const Vector2 &p_position, const Ref< dest_rect.size.x += FP_ADJUST; dest_rect.size.y += FP_ADJUST; - bool transpose = tile_data->get_transpose(); + bool transpose = tile_data->get_transpose() ^ bool(p_alternative_tile & TileSetAtlasSource::TRANSFORM_TRANSPOSE); if (transpose) { dest_rect.position = (p_position - Vector2(dest_rect.size.y, dest_rect.size.x) / 2 - tile_offset); } else { dest_rect.position = (p_position - dest_rect.size / 2 - tile_offset); } - if (tile_data->get_flip_h()) { + if (tile_data->get_flip_h() ^ bool(p_alternative_tile & TileSetAtlasSource::TRANSFORM_FLIP_H)) { dest_rect.size.x = -dest_rect.size.x; } - if (tile_data->get_flip_v()) { + if (tile_data->get_flip_v() ^ bool(p_alternative_tile & TileSetAtlasSource::TRANSFORM_FLIP_V)) { dest_rect.size.y = -dest_rect.size.y; } @@ -3482,6 +3479,37 @@ Rect2 TileMap::_edit_get_rect() const { } #endif +PackedVector2Array TileMap::_get_transformed_vertices(const PackedVector2Array &p_vertices, int p_alternative_id) { + const Vector2 *r = p_vertices.ptr(); + int size = p_vertices.size(); + + PackedVector2Array new_points; + new_points.resize(size); + Vector2 *w = new_points.ptrw(); + + bool flip_h = (p_alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H); + bool flip_v = (p_alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V); + bool transpose = (p_alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE); + + for (int i = 0; i < size; i++) { + Vector2 v; + if (transpose) { + v = Vector2(r[i].y, r[i].x); + } else { + v = r[i]; + } + + if (flip_h) { + v.x *= -1; + } + if (flip_v) { + v.y *= -1; + } + w[i] = v; + } + return new_points; +} + bool TileMap::_set(const StringName &p_name, const Variant &p_value) { Vector<String> components = String(p_name).split("/", true, 2); if (p_name == "format") { @@ -4239,12 +4267,19 @@ TypedArray<Vector2i> TileMap::get_used_cells_by_id(int p_layer, int p_source_id, Rect2i TileMap::get_used_rect() const { // Return the visible rect of the tilemap. - if (layers.is_empty()) { - return Rect2i(); - } - Rect2 rect = layers[0]->get_used_rect(); - for (unsigned int i = 1; i < layers.size(); i++) { - rect = rect.merge(layers[i]->get_used_rect()); + bool first = true; + Rect2i rect = Rect2i(); + for (const Ref<TileMapLayer> &layer : layers) { + Rect2i layer_rect = layer->get_used_rect(); + if (layer_rect == Rect2i()) { + continue; + } + if (first) { + rect = layer_rect; + first = false; + } else { + rect = rect.merge(layer_rect); + } } return rect; } @@ -4384,6 +4419,57 @@ void TileMap::draw_cells_outline(Control *p_control, const RBSet<Vector2i> &p_ce #undef DRAW_SIDE_IF_NEEDED } +Ref<Resource> TileMap::get_transformed_polygon(Ref<Resource> p_polygon, int p_alternative_id) { + if (!bool(p_alternative_id & (TileSetAtlasSource::TRANSFORM_FLIP_H | TileSetAtlasSource::TRANSFORM_FLIP_V | TileSetAtlasSource::TRANSFORM_TRANSPOSE))) { + return p_polygon; + } + + { + HashMap<Pair<Ref<Resource>, int>, Ref<Resource>, PairHash<Ref<Resource>, int>>::Iterator E = polygon_cache.find(Pair<Ref<Resource>, int>(p_polygon, p_alternative_id)); + if (E) { + return E->value; + } + } + + Ref<ConvexPolygonShape2D> col = p_polygon; + if (col.is_valid()) { + Ref<ConvexPolygonShape2D> ret; + ret.instantiate(); + ret->set_points(_get_transformed_vertices(col->get_points(), p_alternative_id)); + polygon_cache[Pair<Ref<Resource>, int>(p_polygon, p_alternative_id)] = ret; + return ret; + } + + Ref<NavigationPolygon> nav = p_polygon; + if (nav.is_valid()) { + PackedVector2Array new_points = _get_transformed_vertices(nav->get_vertices(), p_alternative_id); + Ref<NavigationPolygon> ret; + ret.instantiate(); + ret->set_vertices(new_points); + + PackedInt32Array indices; + indices.resize(new_points.size()); + int *w = indices.ptrw(); + for (int i = 0; i < new_points.size(); i++) { + w[i] = i; + } + ret->add_polygon(indices); + polygon_cache[Pair<Ref<Resource>, int>(p_polygon, p_alternative_id)] = ret; + return ret; + } + + Ref<OccluderPolygon2D> ocd = p_polygon; + if (ocd.is_valid()) { + Ref<OccluderPolygon2D> ret; + ret.instantiate(); + ret->set_polygon(_get_transformed_vertices(ocd->get_polygon(), p_alternative_id)); + polygon_cache[Pair<Ref<Resource>, int>(p_polygon, p_alternative_id)] = ret; + return ret; + } + + return p_polygon; +} + PackedStringArray TileMap::get_configuration_warnings() const { PackedStringArray warnings = Node::get_configuration_warnings(); diff --git a/scene/2d/tile_map.h b/scene/2d/tile_map.h index 7782d7de96..f804f808cb 100644 --- a/scene/2d/tile_map.h +++ b/scene/2d/tile_map.h @@ -455,6 +455,10 @@ private: void _tile_set_changed(); + // Polygons. + HashMap<Pair<Ref<Resource>, int>, Ref<Resource>, PairHash<Ref<Resource>, int>> polygon_cache; + PackedVector2Array _get_transformed_vertices(const PackedVector2Array &p_vertices, int p_alternative_id); + protected: bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -595,6 +599,7 @@ public: // Helpers? TypedArray<Vector2i> get_surrounding_cells(const Vector2i &coords); void draw_cells_outline(Control *p_control, const RBSet<Vector2i> &p_cells, Color p_color, Transform2D p_transform = Transform2D()); + Ref<Resource> get_transformed_polygon(Ref<Resource> p_polygon, int p_alternative_id); // Virtual function to modify the TileData at runtime. GDVIRTUAL2R(bool, _use_tile_data_runtime_update, int, Vector2i); diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index a7667267a6..12f13bee2f 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -48,11 +48,11 @@ void GPUParticles3D::set_emitting(bool p_emitting) { // Last cycle ended. active = true; time = 0; - signal_cancled = false; + signal_canceled = false; emission_time = lifetime; active_time = lifetime * (2 - explosiveness_ratio); } else { - signal_cancled = true; + signal_canceled = true; } set_process_internal(true); } else if (!p_emitting) { @@ -397,7 +397,7 @@ void GPUParticles3D::restart() { emitting = true; active = true; - signal_cancled = false; + signal_canceled = false; time = 0; emission_time = lifetime * (1 - explosiveness_ratio); active_time = lifetime * (2 - explosiveness_ratio); @@ -465,7 +465,7 @@ void GPUParticles3D::_notification(int p_what) { } } if (time > active_time) { - if (active && !signal_cancled) { + if (active && !signal_canceled) { emit_signal(SceneStringNames::get_singleton()->finished); } active = false; diff --git a/scene/3d/gpu_particles_3d.h b/scene/3d/gpu_particles_3d.h index 6e9083bda2..f3df4f1929 100644 --- a/scene/3d/gpu_particles_3d.h +++ b/scene/3d/gpu_particles_3d.h @@ -62,7 +62,7 @@ private: bool emitting = false; bool active = false; - bool signal_cancled = false; + bool signal_canceled = false; bool one_shot = false; int amount = 0; double lifetime = 0.0; diff --git a/scene/3d/lightmap_gi.cpp b/scene/3d/lightmap_gi.cpp index a666dca658..5dd1e6954d 100644 --- a/scene/3d/lightmap_gi.cpp +++ b/scene/3d/lightmap_gi.cpp @@ -1082,7 +1082,9 @@ LightmapGI::BakeError LightmapGI::bake(Node *p_from_node, String p_image_data_pa Lightmapper::BakeError bake_err = lightmapper->bake(Lightmapper::BakeQuality(bake_quality), use_denoiser, bounces, bias, max_texture_size, directional, Lightmapper::GenerateProbes(gen_probes), environment_image, environment_transform, _lightmap_bake_step_function, &bsud, exposure_normalization); - if (bake_err == Lightmapper::BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES) { + if (bake_err == Lightmapper::BAKE_ERROR_LIGHTMAP_TOO_SMALL) { + return BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL; + } else if (bake_err == Lightmapper::BAKE_ERROR_LIGHTMAP_CANT_PRE_BAKE_MESHES) { return BAKE_ERROR_MESHES_INVALID; } @@ -1566,6 +1568,7 @@ void LightmapGI::_bind_methods() { BIND_ENUM_CONSTANT(BAKE_ERROR_MESHES_INVALID); BIND_ENUM_CONSTANT(BAKE_ERROR_CANT_CREATE_IMAGE); BIND_ENUM_CONSTANT(BAKE_ERROR_USER_ABORTED); + BIND_ENUM_CONSTANT(BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL); BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_DISABLED); BIND_ENUM_CONSTANT(ENVIRONMENT_MODE_SCENE); diff --git a/scene/3d/lightmap_gi.h b/scene/3d/lightmap_gi.h index b9e33cf300..02123ef7ba 100644 --- a/scene/3d/lightmap_gi.h +++ b/scene/3d/lightmap_gi.h @@ -132,6 +132,7 @@ public: BAKE_ERROR_MESHES_INVALID, BAKE_ERROR_CANT_CREATE_IMAGE, BAKE_ERROR_USER_ABORTED, + BAKE_ERROR_TEXTURE_SIZE_TOO_SMALL, }; enum EnvironmentMode { diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 6a5fdc3360..f74d7fb906 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -1523,7 +1523,19 @@ void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_regi p_region.position += Point2(horizontal_padding, vertical_padding); p_region.size -= Point2(horizontal_padding, vertical_padding) * 2; - if (can_fold_line(p_line)) { + bool can_fold = can_fold_line(p_line); + + if (is_line_code_region_start(p_line)) { + Color region_icon_color = theme_cache.folded_code_region_color; + region_icon_color.a = MAX(region_icon_color.a, 0.4f); + if (can_fold) { + theme_cache.can_fold_code_region_icon->draw_rect(get_canvas_item(), p_region, false, region_icon_color); + } else { + theme_cache.folded_code_region_icon->draw_rect(get_canvas_item(), p_region, false, region_icon_color); + } + return; + } + if (can_fold) { theme_cache.can_fold_icon->draw_rect(get_canvas_item(), p_region, false, theme_cache.code_folding_color); return; } @@ -1554,6 +1566,27 @@ bool CodeEdit::can_fold_line(int p_line) const { return false; } + // Check for code region. + if (is_line_code_region_end(p_line)) { + return false; + } + if (is_line_code_region_start(p_line)) { + int region_level = 0; + // Check if there is a valid end region tag. + for (int next_line = p_line + 1; next_line < get_line_count(); next_line++) { + if (is_line_code_region_end(next_line)) { + region_level -= 1; + if (region_level == -1) { + return true; + } + } + if (is_line_code_region_start(next_line)) { + region_level += 1; + } + } + return false; + } + /* Check for full multiline line or block strings / comments. */ int in_comment = is_in_comment(p_line); int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; @@ -1562,13 +1595,13 @@ bool CodeEdit::can_fold_line(int p_line) const { return false; } - int delimter_end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + int delimiter_end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; /* No end line, therefore we have a multiline region over the rest of the file. */ - if (delimter_end_line == -1) { + if (delimiter_end_line == -1) { return true; } /* End line is the same therefore we have a block. */ - if (delimter_end_line == p_line) { + if (delimiter_end_line == p_line) { /* Check we are the start of the block. */ if (p_line - 1 >= 0) { if ((in_string != -1 && is_in_string(p_line - 1) != -1) || (in_comment != -1 && is_in_comment(p_line - 1) != -1)) { @@ -1578,7 +1611,7 @@ bool CodeEdit::can_fold_line(int p_line) const { /* Check it continues for at least one line. */ return ((in_string != -1 && is_in_string(p_line + 1) != -1) || (in_comment != -1 && is_in_comment(p_line + 1) != -1)); } - return ((in_string != -1 && is_in_string(delimter_end_line) != -1) || (in_comment != -1 && is_in_comment(delimter_end_line) != -1)); + return ((in_string != -1 && is_in_string(delimiter_end_line) != -1) || (in_comment != -1 && is_in_comment(delimiter_end_line) != -1)); } /* Otherwise check indent levels. */ @@ -1602,31 +1635,51 @@ void CodeEdit::fold_line(int p_line) { const int line_count = get_line_count() - 1; int end_line = line_count; - int in_comment = is_in_comment(p_line); - int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; - if (in_string != -1 || in_comment != -1) { - end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; - /* End line is the same therefore we have a block of single line delimiters. */ - if (end_line == p_line) { - for (int i = p_line + 1; i <= line_count; i++) { - if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { + // Fold code region. + if (is_line_code_region_start(p_line)) { + int region_level = 0; + for (int endregion_line = p_line + 1; endregion_line < get_line_count(); endregion_line++) { + if (is_line_code_region_start(endregion_line)) { + region_level += 1; + } + if (is_line_code_region_end(endregion_line)) { + region_level -= 1; + if (region_level == -1) { + end_line = endregion_line; break; } - end_line = i; } } - } else { - int start_indent = get_indent_level(p_line); - for (int i = p_line + 1; i <= line_count; i++) { - if (get_line(i).strip_edges().size() == 0) { - continue; - } - if (get_indent_level(i) > start_indent) { - end_line = i; - continue; + set_line_background_color(p_line, theme_cache.folded_code_region_color); + } + + int in_comment = is_in_comment(p_line); + int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; + if (!is_line_code_region_start(p_line)) { + if (in_string != -1 || in_comment != -1) { + end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + // End line is the same therefore we have a block of single line delimiters. + if (end_line == p_line) { + for (int i = p_line + 1; i <= line_count; i++) { + if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { + break; + } + end_line = i; + } } - if (is_in_string(i) == -1 && is_in_comment(i) == -1) { - break; + } else { + int start_indent = get_indent_level(p_line); + for (int i = p_line + 1; i <= line_count; i++) { + if (get_line(i).strip_edges().size() == 0) { + continue; + } + if (get_indent_level(i) > start_indent) { + end_line = i; + continue; + } + if (is_in_string(i) == -1 && is_in_comment(i) == -1) { + break; + } } } } @@ -1677,6 +1730,9 @@ void CodeEdit::unfold_line(int p_line) { break; } _set_line_as_hidden(i, false); + if (is_line_code_region_start(i - 1)) { + set_line_background_color(i - 1, Color(0.0, 0.0, 0.0, 0.0)); + } } queue_redraw(); } @@ -1716,6 +1772,95 @@ TypedArray<int> CodeEdit::get_folded_lines() const { return folded_lines; } +/* Code region */ +void CodeEdit::create_code_region() { + // Abort if there is no selected text. + if (!has_selection()) { + return; + } + // Check that region tag find a comment delimiter and is valid. + if (code_region_start_string.is_empty()) { + WARN_PRINT_ONCE("Cannot create code region without any one line comment delimiters"); + return; + } + begin_complex_operation(); + // Merge selections if selection starts on the same line the previous one ends. + Vector<int> caret_edit_order = get_caret_index_edit_order(); + Vector<int> carets_to_remove; + for (int i = 1; i < caret_edit_order.size(); i++) { + int current_caret = caret_edit_order[i - 1]; + int next_caret = caret_edit_order[i]; + if (get_selection_from_line(current_caret) == get_selection_to_line(next_caret)) { + select(get_selection_from_line(next_caret), get_selection_from_column(next_caret), get_selection_to_line(current_caret), get_selection_to_column(current_caret), next_caret); + carets_to_remove.append(current_caret); + } + } + // Sort and remove backwards to preserve indices. + carets_to_remove.sort(); + for (int i = carets_to_remove.size() - 1; i >= 0; i--) { + remove_caret(carets_to_remove[i]); + } + + // Adding start and end region tags. + int first_region_start = -1; + for (int caret_idx : get_caret_index_edit_order()) { + if (!has_selection(caret_idx)) { + continue; + } + int from_line = get_selection_from_line(caret_idx); + if (first_region_start == -1 || from_line < first_region_start) { + first_region_start = from_line; + } + int to_line = get_selection_to_line(caret_idx); + set_line(to_line, get_line(to_line) + "\n" + code_region_end_string); + insert_line_at(from_line, code_region_start_string + " " + RTR("New Code Region")); + fold_line(from_line); + } + + // Select name of the first region to allow quick edit. + remove_secondary_carets(); + set_caret_line(first_region_start); + int tag_length = code_region_start_string.length() + RTR("New Code Region").length() + 1; + set_caret_column(tag_length); + select(first_region_start, code_region_start_string.length() + 1, first_region_start, tag_length); + + end_complex_operation(); + queue_redraw(); +} + +String CodeEdit::get_code_region_start_tag() const { + return code_region_start_tag; +} + +String CodeEdit::get_code_region_end_tag() const { + return code_region_end_tag; +} + +void CodeEdit::set_code_region_tags(const String &p_start, const String &p_end) { + ERR_FAIL_COND_MSG(p_start == p_end, "Starting and ending region tags cannot be identical."); + ERR_FAIL_COND_MSG(p_start.is_empty(), "Starting region tag cannot be empty."); + ERR_FAIL_COND_MSG(p_end.is_empty(), "Ending region tag cannot be empty."); + code_region_start_tag = p_start; + code_region_end_tag = p_end; + _update_code_region_tags(); +} + +bool CodeEdit::is_line_code_region_start(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + if (code_region_start_string.is_empty()) { + return false; + } + return get_line(p_line).strip_edges().begins_with(code_region_start_string); +} + +bool CodeEdit::is_line_code_region_end(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + if (code_region_start_string.is_empty()) { + return false; + } + return get_line(p_line).strip_edges().begins_with(code_region_end_string); +} + /* Delimiters */ // Strings void CodeEdit::add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) { @@ -2344,6 +2489,14 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); + /* Code region */ + ClassDB::bind_method(D_METHOD("create_code_region"), &CodeEdit::create_code_region); + ClassDB::bind_method(D_METHOD("get_code_region_start_tag"), &CodeEdit::get_code_region_start_tag); + ClassDB::bind_method(D_METHOD("get_code_region_end_tag"), &CodeEdit::get_code_region_end_tag); + ClassDB::bind_method(D_METHOD("set_code_region_tags", "start", "end"), &CodeEdit::set_code_region_tags, DEFVAL("region"), DEFVAL("endregion")); + ClassDB::bind_method(D_METHOD("is_line_code_region_start", "line"), &CodeEdit::is_line_code_region_start); + ClassDB::bind_method(D_METHOD("is_line_code_region_end", "line"), &CodeEdit::is_line_code_region_end); + /* Delimiters */ // Strings ClassDB::bind_method(D_METHOD("add_string_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_string_delimiter, DEFVAL(false)); @@ -2483,8 +2636,11 @@ void CodeEdit::_bind_methods() { /* Theme items */ /* Gutters */ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, code_folding_color); + BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, folded_code_region_color); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, can_fold_icon, "can_fold"); BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, folded_icon, "folded"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, can_fold_code_region_icon, "can_fold_code_region"); + BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, CodeEdit, folded_code_region_icon, "folded_code_region"); BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, CodeEdit, folded_eol_icon); BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, CodeEdit, breakpoint_color); @@ -2628,6 +2784,27 @@ void CodeEdit::_update_gutter_indexes() { } } +/* Code Region */ +void CodeEdit::_update_code_region_tags() { + code_region_start_string = ""; + code_region_end_string = ""; + + if (code_region_start_tag.is_empty() || code_region_end_tag.is_empty()) { + return; + } + + for (int i = 0; i < delimiters.size(); i++) { + if (delimiters[i].type != DelimiterType::TYPE_COMMENT) { + continue; + } + if (delimiters[i].end_key.is_empty() && delimiters[i].line_only == true) { + code_region_start_string = delimiters[i].start_key + code_region_start_tag; + code_region_end_string = delimiters[i].start_key + code_region_end_tag; + return; + } + } +} + /* Delimiters */ void CodeEdit::_update_delimiter_cache(int p_from_line, int p_to_line) { if (delimiters.size() == 0) { @@ -2871,6 +3048,9 @@ void CodeEdit::_add_delimiter(const String &p_start_key, const String &p_end_key delimiter_cache.clear(); _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } } void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type) { @@ -2888,6 +3068,9 @@ void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type delimiter_cache.clear(); _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } break; } } @@ -2931,6 +3114,9 @@ void CodeEdit::_clear_delimiters(DelimiterType p_type) { if (!setting_delimiters) { _update_delimiter_cache(); } + if (p_type == DelimiterType::TYPE_COMMENT) { + _update_code_region_tags(); + } } TypedArray<String> CodeEdit::_get_delimiters(DelimiterType p_type) const { diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index d00bd22cd5..53ff65f376 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -125,6 +125,11 @@ private: /* Line Folding */ bool line_folding_enabled = false; + String code_region_start_string; + String code_region_end_string; + String code_region_start_tag = "region"; + String code_region_end_tag = "endregion"; + void _update_code_region_tags(); /* Delimiters */ enum DelimiterType { @@ -232,8 +237,11 @@ private: struct ThemeCache { /* Gutters */ Color code_folding_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); Ref<Texture2D> can_fold_icon; Ref<Texture2D> folded_icon; + Ref<Texture2D> can_fold_code_region_icon; + Ref<Texture2D> folded_code_region_icon; Ref<Texture2D> folded_eol_icon; Color breakpoint_color = Color(1, 1, 1); @@ -397,6 +405,14 @@ public: bool is_line_folded(int p_line) const; TypedArray<int> get_folded_lines() const; + /* Code region */ + void create_code_region(); + String get_code_region_start_tag() const; + String get_code_region_end_tag() const; + void set_code_region_tags(const String &p_start = "region", const String &p_end = "endregion"); + bool is_line_code_region_start(int p_line) const; + bool is_line_code_region_end(int p_line) const; + /* Delimiters */ void add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only = false); void remove_string_delimiter(const String &p_start_key); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 3bc3dffa58..d27a40779e 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -91,6 +91,13 @@ VBoxContainer *FileDialog::get_vbox() { return vbox; } +void FileDialog::_validate_property(PropertyInfo &p_property) const { + if (p_property.name == "dialog_text") { + // File dialogs have a custom layout, and dialog nodes can't have both a text and a layout. + p_property.usage = PROPERTY_USAGE_NONE; + } +} + void FileDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 58208ad2d5..739cb3e31a 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -166,6 +166,7 @@ private: virtual void _post_popup() override; protected: + void _validate_property(PropertyInfo &p_property) const; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 0803355049..3f4fd88053 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -968,7 +968,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o int gl_size = TS->shaped_text_get_glyph_count(rid); Vector2 gloff = off; - // Draw oulines and shadow. + // Draw outlines and shadow. int processed_glyphs_ol = r_processed_glyphs; for (int i = 0; i < gl_size; i++) { Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 292809b40d..cc519ad38b 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -3000,6 +3000,7 @@ void TextEdit::_update_theme_item_cache() { Control::_update_theme_item_cache(); theme_cache.base_scale = get_theme_default_base_scale(); + theme_cache.folded_code_region_color = get_theme_color(SNAME("folded_code_region_color"), SNAME("CodeEdit")); use_selected_font_color = theme_cache.font_selected_color != Color(0, 0, 0, 0); if (text.get_line_height() + theme_cache.line_spacing < 1) { @@ -6476,6 +6477,7 @@ void TextEdit::_bind_methods() { /* Internal API for CodeEdit */ BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, brace_mismatch_color, "brace_mismatch_color", "CodeEdit"); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, code_folding_color, "code_folding_color", "CodeEdit"); + BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_COLOR, TextEdit, folded_code_region_color, "folded_code_region_color", "CodeEdit"); BIND_THEME_ITEM_EXT(Theme::DATA_TYPE_ICON, TextEdit, folded_eol_icon, "folded_eol_icon", "CodeEdit"); /* Search */ diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index b52fde9361..d874db34bf 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -546,6 +546,7 @@ private: /* Internal API for CodeEdit */ Color brace_mismatch_color; Color code_folding_color = Color(1, 1, 1); + Color folded_code_region_color = Color(1, 1, 1); Ref<Texture2D> folded_eol_icon; /* Search */ diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index f2cd2bc933..c49da1ded1 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -4484,7 +4484,7 @@ struct AnimationCompressionDataState { void commit_temp_packets() { if (temp_packets.size() == 0) { - return; //nohing to do + return; // Nothing to do. } //#define DEBUG_PACKET_PUSH #ifdef DEBUG_PACKET_PUSH diff --git a/scene/resources/tile_set.cpp b/scene/resources/tile_set.cpp index da3a18e71b..4b705bbb34 100644 --- a/scene/resources/tile_set.cpp +++ b/scene/resources/tile_set.cpp @@ -4464,6 +4464,10 @@ bool TileSetAtlasSource::is_position_in_tile_texture_region(const Vector2i p_atl return rect.has_point(p_position); } +int TileSetAtlasSource::alternative_no_transform(int p_alternative_id) { + return p_alternative_id & ~(TRANSFORM_FLIP_H | TRANSFORM_FLIP_V | TRANSFORM_TRANSPOSE); +} + // Getters for texture and tile region (padded or not) Ref<Texture2D> TileSetAtlasSource::get_runtime_texture() const { if (use_texture_padding) { @@ -4547,6 +4551,7 @@ int TileSetAtlasSource::create_alternative_tile(const Vector2i p_atlas_coords, i void TileSetAtlasSource::remove_alternative_tile(const Vector2i p_atlas_coords, int p_alternative_tile) { ERR_FAIL_COND_MSG(!tiles.has(p_atlas_coords), vformat("TileSetAtlasSource has no tile at %s.", String(p_atlas_coords))); ERR_FAIL_COND_MSG(!tiles[p_atlas_coords].alternatives.has(p_alternative_tile), vformat("TileSetAtlasSource has no alternative with id %d for tile coords %s.", p_alternative_tile, String(p_atlas_coords))); + p_alternative_tile = alternative_no_transform(p_alternative_tile); ERR_FAIL_COND_MSG(p_alternative_tile == 0, "Cannot remove the alternative with id 0, the base tile alternative cannot be removed."); memdelete(tiles[p_atlas_coords].alternatives[p_alternative_tile]); @@ -4560,6 +4565,7 @@ void TileSetAtlasSource::remove_alternative_tile(const Vector2i p_atlas_coords, void TileSetAtlasSource::set_alternative_tile_id(const Vector2i p_atlas_coords, int p_alternative_tile, int p_new_id) { ERR_FAIL_COND_MSG(!tiles.has(p_atlas_coords), vformat("TileSetAtlasSource has no tile at %s.", String(p_atlas_coords))); ERR_FAIL_COND_MSG(!tiles[p_atlas_coords].alternatives.has(p_alternative_tile), vformat("TileSetAtlasSource has no alternative with id %d for tile coords %s.", p_alternative_tile, String(p_atlas_coords))); + p_alternative_tile = alternative_no_transform(p_alternative_tile); ERR_FAIL_COND_MSG(p_alternative_tile == 0, "Cannot change the alternative with id 0, the base tile alternative cannot be modified."); ERR_FAIL_COND_MSG(tiles[p_atlas_coords].alternatives.has(p_new_id), vformat("TileSetAtlasSource has already an alternative with id %d at %s.", p_new_id, String(p_atlas_coords))); @@ -4576,7 +4582,7 @@ void TileSetAtlasSource::set_alternative_tile_id(const Vector2i p_atlas_coords, bool TileSetAtlasSource::has_alternative_tile(const Vector2i p_atlas_coords, int p_alternative_tile) const { ERR_FAIL_COND_V_MSG(!tiles.has(p_atlas_coords), false, vformat("The TileSetAtlasSource atlas has no tile at %s.", String(p_atlas_coords))); - return tiles[p_atlas_coords].alternatives.has(p_alternative_tile); + return tiles[p_atlas_coords].alternatives.has(alternative_no_transform(p_alternative_tile)); } int TileSetAtlasSource::get_next_alternative_tile_id(const Vector2i p_atlas_coords) const { @@ -4591,6 +4597,7 @@ int TileSetAtlasSource::get_alternative_tiles_count(const Vector2i p_atlas_coord int TileSetAtlasSource::get_alternative_tile_id(const Vector2i p_atlas_coords, int p_index) const { ERR_FAIL_COND_V_MSG(!tiles.has(p_atlas_coords), TileSetSource::INVALID_TILE_ALTERNATIVE, vformat("The TileSetAtlasSource atlas has no tile at %s.", String(p_atlas_coords))); + p_index = alternative_no_transform(p_index); ERR_FAIL_INDEX_V(p_index, tiles[p_atlas_coords].alternatives_ids.size(), TileSetSource::INVALID_TILE_ALTERNATIVE); return tiles[p_atlas_coords].alternatives_ids[p_index]; @@ -4598,6 +4605,7 @@ int TileSetAtlasSource::get_alternative_tile_id(const Vector2i p_atlas_coords, i TileData *TileSetAtlasSource::get_tile_data(const Vector2i p_atlas_coords, int p_alternative_tile) const { ERR_FAIL_COND_V_MSG(!tiles.has(p_atlas_coords), nullptr, vformat("The TileSetAtlasSource atlas has no tile at %s.", String(p_atlas_coords))); + p_alternative_tile = alternative_no_transform(p_alternative_tile); ERR_FAIL_COND_V_MSG(!tiles[p_atlas_coords].alternatives.has(p_alternative_tile), nullptr, vformat("TileSetAtlasSource has no alternative with id %d for tile coords %s.", p_alternative_tile, String(p_atlas_coords))); return tiles[p_atlas_coords].alternatives[p_alternative_tile]; @@ -4668,6 +4676,10 @@ void TileSetAtlasSource::_bind_methods() { BIND_ENUM_CONSTANT(TILE_ANIMATION_MODE_DEFAULT) BIND_ENUM_CONSTANT(TILE_ANIMATION_MODE_RANDOM_START_TIMES) BIND_ENUM_CONSTANT(TILE_ANIMATION_MODE_MAX) + + BIND_CONSTANT(TRANSFORM_FLIP_H) + BIND_CONSTANT(TRANSFORM_FLIP_V) + BIND_CONSTANT(TRANSFORM_TRANSPOSE) } TileSetAtlasSource::~TileSetAtlasSource() { @@ -4681,6 +4693,7 @@ TileSetAtlasSource::~TileSetAtlasSource() { TileData *TileSetAtlasSource::_get_atlas_tile_data(Vector2i p_atlas_coords, int p_alternative_tile) { ERR_FAIL_COND_V_MSG(!tiles.has(p_atlas_coords), nullptr, vformat("TileSetAtlasSource has no tile at %s.", String(p_atlas_coords))); + p_alternative_tile = alternative_no_transform(p_alternative_tile); ERR_FAIL_COND_V_MSG(!tiles[p_atlas_coords].alternatives.has(p_alternative_tile), nullptr, vformat("TileSetAtlasSource has no alternative with id %d for tile coords %s.", p_alternative_tile, String(p_atlas_coords))); return tiles[p_atlas_coords].alternatives[p_alternative_tile]; diff --git a/scene/resources/tile_set.h b/scene/resources/tile_set.h index f053740db1..d500169843 100644 --- a/scene/resources/tile_set.h +++ b/scene/resources/tile_set.h @@ -62,10 +62,10 @@ class TileSetPluginAtlasNavigation; union TileMapCell { struct { - int32_t source_id : 16; - int16_t coord_x : 16; - int16_t coord_y : 16; - int32_t alternative_tile : 16; + int16_t source_id; + int16_t coord_x; + int16_t coord_y; + int16_t alternative_tile; }; uint64_t _u64t; @@ -598,6 +598,12 @@ public: TILE_ANIMATION_MODE_MAX, }; + enum TransformBits { + TRANSFORM_FLIP_H = 1 << 12, + TRANSFORM_FLIP_V = 1 << 13, + TRANSFORM_TRANSPOSE = 1 << 14, + }; + private: struct TileAlternativesData { Vector2i size_in_atlas = Vector2i(1, 1); @@ -736,6 +742,8 @@ public: Rect2i get_tile_texture_region(Vector2i p_atlas_coords, int p_frame = 0) const; bool is_position_in_tile_texture_region(const Vector2i p_atlas_coords, int p_alternative_tile, Vector2 p_position) const; + static int alternative_no_transform(int p_alternative_id); + // Getters for texture and tile region (padded or not) Ref<Texture2D> get_runtime_texture() const; Rect2i get_runtime_tile_texture_region(Vector2i p_atlas_coords, int p_frame = 0) const; diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp index 61e018c656..7efbc74bf3 100644 --- a/scene/theme/default_theme.cpp +++ b/scene/theme/default_theme.cpp @@ -478,6 +478,8 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const theme->set_icon("executing_line", "CodeEdit", icons["arrow_right"]); theme->set_icon("can_fold", "CodeEdit", icons["arrow_down"]); theme->set_icon("folded", "CodeEdit", icons["arrow_right"]); + theme->set_icon("can_fold_code_region", "CodeEdit", icons["folder_down_arrow"]); + theme->set_icon("folded_code_region", "CodeEdit", icons["folder_right_arrow"]); theme->set_icon("folded_eol_icon", "CodeEdit", icons["text_edit_ellipsis"]); theme->set_font("font", "CodeEdit", Ref<Font>()); @@ -501,6 +503,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const theme->set_color("executing_line_color", "CodeEdit", Color(0.98, 0.89, 0.27)); theme->set_color("current_line_color", "CodeEdit", Color(0.25, 0.25, 0.26, 0.8)); theme->set_color("code_folding_color", "CodeEdit", Color(0.8, 0.8, 0.8, 0.8)); + theme->set_color("folded_code_region_color", "CodeEdit", Color(0.68, 0.46, 0.77, 0.2)); theme->set_color("caret_color", "CodeEdit", control_font_color); theme->set_color("caret_background_color", "CodeEdit", Color(0, 0, 0)); theme->set_color("brace_mismatch_color", "CodeEdit", Color(1, 0.2, 0.2)); diff --git a/scene/theme/icons/folder_down_arrow.svg b/scene/theme/icons/folder_down_arrow.svg new file mode 100644 index 0000000000..3bc4f3f73b --- /dev/null +++ b/scene/theme/icons/folder_down_arrow.svg @@ -0,0 +1 @@ +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm1 5a1 1 0 0 1 1.414-1.414L6 6.172l1.586-1.586A1 1 0 0 1 9 6L6.707 8.293a1 1 0 0 1-1.414 0Z" fill="#fff"/></svg>
\ No newline at end of file diff --git a/scene/theme/icons/folder_right_arrow.svg b/scene/theme/icons/folder_right_arrow.svg new file mode 100644 index 0000000000..a9b81d54f3 --- /dev/null +++ b/scene/theme/icons/folder_right_arrow.svg @@ -0,0 +1 @@ +<svg height="12" width="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"><path d="M2 1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H6V2a1 1 0 0 0-1-1zm3.5 8a1 1 0 0 1-1.414-1.414L5.672 6 4.086 4.414A1 1 0 0 1 5.5 3l2.293 2.293a1 1 0 0 1 0 1.414Z" fill="#fff"/></svg>
\ No newline at end of file diff --git a/scene/theme/theme_owner.cpp b/scene/theme/theme_owner.cpp index 1ba9a055fc..92cffeb228 100644 --- a/scene/theme/theme_owner.cpp +++ b/scene/theme/theme_owner.cpp @@ -212,7 +212,7 @@ void ThemeOwner::get_theme_type_dependencies(const Node *p_for_node, const Strin type_variation = for_w->get_theme_type_variation(); } - // If we are looking for dependencies of the current class (or a variantion of it), check themes from the context. + // If we are looking for dependencies of the current class (or a variation of it), check themes from the context. if (p_theme_type == StringName() || p_theme_type == type_name || p_theme_type == type_variation) { ThemeContext *global_context = _get_active_owner_context(); for (const Ref<Theme> &theme : global_context->get_themes()) { diff --git a/tests/core/math/test_math_funcs.h b/tests/core/math/test_math_funcs.h index e3504ef1e5..d046656b0f 100644 --- a/tests/core/math/test_math_funcs.h +++ b/tests/core/math/test_math_funcs.h @@ -54,6 +54,8 @@ TEST_CASE("[Math] C++ macros") { CHECK(SIGN(-5) == -1.0); CHECK(SIGN(0) == 0.0); CHECK(SIGN(5) == 1.0); + // Check that SIGN(NAN) returns 0.0. + CHECK(SIGN(NAN) == 0.0); } TEST_CASE("[Math] Power of two functions") { diff --git a/tests/scene/test_code_edit.h b/tests/scene/test_code_edit.h index 72421c9cc9..8576b38ce2 100644 --- a/tests/scene/test_code_edit.h +++ b/tests/scene/test_code_edit.h @@ -2839,6 +2839,194 @@ TEST_CASE("[SceneTree][CodeEdit] folding") { memdelete(code_edit); } +TEST_CASE("[SceneTree][CodeEdit] region folding") { + CodeEdit *code_edit = memnew(CodeEdit); + SceneTree::get_singleton()->get_root()->add_child(code_edit); + code_edit->grab_focus(); + + SUBCASE("[CodeEdit] region folding") { + code_edit->set_line_folding_enabled(true); + + // Region tag detection. + code_edit->set_text("#region region_name\nline2\n#endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_start(1)); + CHECK_FALSE(code_edit->is_line_code_region_start(2)); + CHECK_FALSE(code_edit->is_line_code_region_end(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(1)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Region tag customization. + code_edit->set_text("#region region_name\nline2\n#endregion\n#open region_name\nline2\n#close"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + CHECK_FALSE(code_edit->is_line_code_region_start(3)); + CHECK_FALSE(code_edit->is_line_code_region_end(5)); + code_edit->set_code_region_tags("open", "close"); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + CHECK(code_edit->is_line_code_region_start(3)); + CHECK(code_edit->is_line_code_region_end(5)); + code_edit->set_code_region_tags("region", "endregion"); + + // Setting identical start and end region tags should fail. + CHECK(code_edit->get_code_region_start_tag() == "region"); + CHECK(code_edit->get_code_region_end_tag() == "endregion"); + ERR_PRINT_OFF; + code_edit->set_code_region_tags("same_tag", "same_tag"); + ERR_PRINT_ON; + CHECK(code_edit->get_code_region_start_tag() == "region"); + CHECK(code_edit->get_code_region_end_tag() == "endregion"); + + // Region creation with selection adds start / close region lines. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(1, 0, 1, 4); + code_edit->create_code_region(); + CHECK(code_edit->is_line_code_region_start(1)); + CHECK(code_edit->get_line(2).contains("line2")); + CHECK(code_edit->is_line_code_region_end(3)); + + // Region creation without any selection has no effect. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + + // Region creation with multiple selections. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 0, 4, 0); + code_edit->add_caret(2, 5); + code_edit->select(2, 0, 2, 5, 1); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\nline1\n#endregion\nline2\n#region New Code Region\nline3\n#endregion"); + + // Two selections on the same line create only one region. + code_edit->set_text("test line1\ntest line2\ntest line3"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->select(0, 0, 1, 2, 0); + code_edit->add_caret(1, 4); + code_edit->select(1, 4, 2, 5, 1); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "#region New Code Region\ntest line1\ntest line2\ntest line3\n#endregion"); + + // Region tag with // comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Creating region with no valid one line comment delimiter has no effect. + code_edit->set_text("line1\nline2\nline3"); + code_edit->clear_comment_delimiters(); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + code_edit->add_comment_delimiter("/*", "*/"); + code_edit->create_code_region(); + CHECK(code_edit->get_text() == "line1\nline2\nline3"); + + // Choose one line comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("/*", "*/"); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + + // Update code region delimiter when removing comment delimiter. + code_edit->set_text("//region region_name\nline2\n//endregion\n#region region_name\nline2\n#endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + code_edit->add_comment_delimiter("#", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + CHECK_FALSE(code_edit->is_line_code_region_start(3)); + CHECK_FALSE(code_edit->is_line_code_region_end(5)); + code_edit->remove_comment_delimiter("//"); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + CHECK(code_edit->is_line_code_region_start(3)); + CHECK(code_edit->is_line_code_region_end(5)); + + // Update code region delimiter when clearing comment delimiters. + code_edit->set_text("//region region_name\nline2\n//endregion"); + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("//", ""); + CHECK(code_edit->is_line_code_region_start(0)); + CHECK(code_edit->is_line_code_region_end(2)); + code_edit->clear_comment_delimiters(); + CHECK_FALSE(code_edit->is_line_code_region_start(0)); + CHECK_FALSE(code_edit->is_line_code_region_end(2)); + + // Fold region. + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region_name\nline2\nline3\n#endregion\nvisible line"); + CHECK(code_edit->can_fold_line(0)); + for (int i = 1; i < 5; i++) { + CHECK_FALSE(code_edit->can_fold_line(i)); + } + for (int i = 0; i < 5; i++) { + CHECK_FALSE(code_edit->is_line_folded(i)); + } + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + + // Region with no end can't be folded. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region_name\nline2\nline3\n#bad_end_tag\nvisible line"); + CHECK_FALSE(code_edit->can_fold_line(0)); + ERR_PRINT_ON; + + // Bad nested region can't be folded. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region without end\n#region region2\nline3\n#endregion\n#no_end"); + CHECK_FALSE(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + ERR_PRINT_ON; + + // Nested region folding. + ERR_PRINT_OFF; + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region1\n#region region2\nline3\n#endregion\n#endregion"); + CHECK(code_edit->can_fold_line(0)); + CHECK(code_edit->can_fold_line(1)); + code_edit->fold_line(1); + CHECK(code_edit->get_next_visible_line_offset_from(2, 1) == 3); + code_edit->fold_line(0); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + ERR_PRINT_ON; + + // Unfolding a line inside a region unfold whole region. + code_edit->clear_comment_delimiters(); + code_edit->add_comment_delimiter("#", ""); + code_edit->set_text("#region region\ninside\nline3\n#endregion\nvisible"); + code_edit->fold_line(0); + CHECK(code_edit->is_line_folded(0)); + CHECK(code_edit->get_next_visible_line_offset_from(1, 1) == 4); + code_edit->unfold_line(1); + CHECK_FALSE(code_edit->is_line_folded(0)); + } + + memdelete(code_edit); +} + TEST_CASE("[SceneTree][CodeEdit] completion") { CodeEdit *code_edit = memnew(CodeEdit); SceneTree::get_singleton()->get_root()->add_child(code_edit); diff --git a/tests/servers/rendering/test_shader_preprocessor.h b/tests/servers/rendering/test_shader_preprocessor.h index c8c143641b..e12da8a2db 100644 --- a/tests/servers/rendering/test_shader_preprocessor.h +++ b/tests/servers/rendering/test_shader_preprocessor.h @@ -294,7 +294,7 @@ TEST_CASE("[ShaderPreprocessor] Concatenation sorting network") { CHECK_SHADER_EQ(result, expected); } -TEST_CASE("[ShaderPreprocessor] Undefined behaviour") { +TEST_CASE("[ShaderPreprocessor] Undefined behavior") { // None of these are valid concatenation, nor valid shader code. // Don't care about results, just make sure there's no crash. const String filename("somefile.gdshader"); |