Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for deprecated symbols to language server and script editor #100019

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

Meorge
Copy link
Contributor

@Meorge Meorge commented Dec 4, 2024

Related to godotengine/godot-proposals#11079 !

This PR adds the ability for code completions from the language server to be marked as deprecated, according to the class documentation, so that they can be displayed as such in a code editor. It also implements this display for the built-in script editor:

In Godot's built-in script editor:
CleanShot 2024-12-04 at 09 58 12@2x

In VS Code:
CleanShot 2024-12-04 at 09 59 08@2x

Note

While the functionality is mostly working, the code itself is still in a messy WIP state. I'm definitely planning on improving its structure (and squashing commits, etc) before opening the full pull request. That being said, if you see patterns or things in the code that could be improved, please let me know!

Tested cases

Methods

A user-defined method marked with `@deprecated`
## This method is deprecated.
## @deprecated
func do_something():
    print("Hello world")

func _ready():
	# do_something will show as deprecated
	do_som... 
A class built-in to Godot and marked as deprecated in the documentation
func _ready():
	var player = AnimationPlayer.new()
	# get_method_call_mode will show as deprecated
	player.get_method_c... 

Classes

Main class of a file marked with `@deprecated`
class_name MyOldClass
extends Node

## This class is deprecated.
## @deprecated
# main.gd
extends Node

func _ready():
	# MyOldClass will show as deprecated
    var i = MyOl... 
Inner class marked with `@deprecated`
## This inner class is deprecated.
## @deprecated
class InnerDeprecatedClass:
	pass

func _ready():
	# InnerDeprecatedClass will show as deprecated
	var i = InnerDep...
A class built-in to Godot and marked as deprecated in the documentation
func _ready():
	# AnimatedTexture will show as deprecated
	var i = AnimatedTex...
A class built-in to Godot and marked as deprecated in the documentation, used as a type
func _ready():
	# AnimatedTexture will show as deprecated
	var i: AnimatedTex...
An autoload with a global variable name
# deprecated_autoload.gd
extends Node

## This autoload is registered with the name DeprecatedAutoload,
## and is marked as deprecated.
## @deprecated

func do_thing():
   print("a thing has been done")
# main.gd
func _ready():
    # DeprecatedAutoload will show as deprecated
    DeprecatedAu...

[!WARNING]
I've found this doesn't always work. It seems to involve something with the .godot folder preventing the doc data from being fully parsed, so I think it's outside the scope of this PR to try to fix.

Enums and constants

A user-defined constant, marked with `@deprecated`
## This is a deprecated constant.
## @deprecated
const DEPRECATED_CONST = "hello"

func _ready():
	# DEPRECATED_CONST will show as deprecated
	print(DEPRECATED_CON...
A user-defined named enum, marked with `@deprecated`
## This is a deprecated enum.
## @deprecated
enum DeprecatedEnum { ONE, TWO, THREE }

func _ready():
	# DeprecatedEnum will show as deprecated
	print(Depre...
Individual values in a user-defined anonymous enum, marked with `@deprecated`
enum {
    ANONYMOUS_ONE,
    ## This one is deprecated
    ## @deprecated
    ANONYMOUS_TWO
}

func _ready():
	# ANONYMOUS_TWO will show as deprecated
	print(ANONYMOUS_T...
Individual values in a user-defined named enum, marked with `@deprecated`
enum NamedEnum {
	ONE,
	## This is a deprecated constant in an enum.
	## @deprecated
	TWO
}

func _ready():
	# NamedEnum.TWO will show as deprecated
	print(NamedEnum.TW... 
Individual values in an enum from a class built-in to Godot and marked as deprecated in the documentation
func _ready():
	# ANIMATION_PROCESS_PHYSICS will show as deprecated
	var i = AnimationPlayer.ANIMATION_PROCESS_PH...

	# Will also show as deprecated
	i = AnimationPlayer.AnimationProcessCallback.ANIMATION_PROCESS_PH...
Constants in `@GlobalScope`
func _ready():
	# PROPERTY_HINT_NODE_PATH_TO_EDITED_NODE will show as deprecated
	print(PROPERTY_HINT_NODE_PATH_T...

Properties

A user-defined property marked with `@deprecated`
## This is a deprecated property.
## @deprecated
var deprecated_property: float = 1.0

## This is a deprecated property with a getter and setter.
## @deprecated
var deprecated_property_2: float = 2.0:
    get:
        return sqrt(5)
    set(value):
        deprecated_property_2 = value

func _ready():
	# deprecated_property and deprecated_property_2 will show as deprecated
	print(deprecated_prop...
A class built-in to Godot and marked as deprecated in the documentation
func _ready():
	# auto_translate will show as deprecated
    var i = Control.new().auto_tr...

Signals

A user-defined signal marked with `@deprecated`
## This is a deprecated signal.
## @deprecated
signal deprecated_signal

func _ready():
	# deprecated_signal will show as deprecated
	depreca...
A class built-in to Godot and marked as deprecated in the documentation
func _ready():
	# setup_local_to_scene_requested will show as deprecated
	Resource.new().setup_local_t...

To-Do

Language server

  • Fix behavior with autoloads (determine how to retrieve their class's DocData)
  • General clean-up of code structure - it's messy right now! (Maybe add some static methods for getting is-deprecated hashmaps?)
    • I've tried to standardize the variable names used so I think it's better now, but would still appreciate suggestions if you have them!

Godot Script editor

  • Ensure the strikethrough scales correctly as the editor scale is changed

@Meorge
Copy link
Contributor Author

Meorge commented Dec 7, 2024

It seems like just about everything is working, except for global variable names for autoloads. As stated above:

Warning

Autoload global variable names will not display as deprecated.

# deprecated_autoload.gd
extends Node

## This autoload is registered with the name DeprecatedAutoload,
## and is marked as deprecated.
## @deprecated

func do_thing():
   print("a thing has been done")
# main.gd
func _ready():
    # DeprecatedAutoload will *not* show as deprecated
    DeprecatedAu...

I've spent a fair amount of time this evening trying to figure out how to get to the DocData::ClassDoc for an autoload, and based on the existing code it looks like I'd need some kind of function that maps script paths to ClassNodes (or DocData::ClassDocs).

What are others' thoughts about not displaying global variable names for autoloads as deprecated even if they're marked as deprecated in their GDScript file's documentation comments?

@HolonProduction
Copy link
Member

From what I see autoload doc data is also registered in EditorHelp::get_doc_data().class_list under the name of the autoload, but the update behavior seems very flaky so I'm not sure whether we can rely on it.

Besides that the only way I know of would be getting the path of the autoload, loading the script and getting the info from the script.

Would need some testing regarding performance impact though, since in the worst case autocompletion gets called with every keystroke so we need to make sure that the resource loader caches the script and doc.

In general before merging we need to test this PR for performance. We already have the problem, that autocompletion can trigger parsing and analyzing of a lot of scripts with each keystroke. This PR shouldn't add more scripts to that list.

@Meorge
Copy link
Contributor Author

Meorge commented Dec 7, 2024

From what I see autoload doc data is also registered in EditorHelp::get_doc_data().class_list under the name of the autoload, but the update behavior seems very flaky so I'm not sure whether we can rely on it.

I'm finding this to be shaky as well. I have a test project that I'm using, and can do the following:

  1. I delete the project's .godot folder, then open the project. If I go straight to "Search Help" and start typing the name of the global variable the autoload is registered under, it appears in the list.
  2. I make a change to a script file in the project and save it, then close the project.
  3. I re-open the project and go to "Search Help" again, and start typing the name of the global variable the autoload is registered under. It doesn't appear anymore.

Indeed, using the following snippet:

for (const KeyValue<String, DocData::ClassDoc> &E2 : EditorHelp::get_doc_data()->class_list) {
    print_line(vformat("Class %s found", E2.key));
}

I do get the following output:

...
Class PackedColorArray found
Class PackedVector4Array found
Class @GlobalScope found
Class @GDScript found
Class DeprecatedAutoload found
Class DeprecatedClass found
Class MyScene.InnerDeprecatedClass found
Class MyScene found

(where DeprecatedAutoload is the name of the autoload's global variable, not its class_name).

And indeed, if I delete .godot then open the project, I can get the DeprecatedAutoload to display as deprecated:

CleanShot 2024-12-07 at 11 46 48

So it seems to me like something being cached in .godot is causing the doc generation to halt prematurely, such that autoloads aren't being added. Last night as I was working on this, I noticed my other custom classes also weren't printing out. I'll look into getting an issue filed for this, if one doesn't already exist!


Regarding performance, I agree that's definitely a big concern, and one I've been conscious of as I've been working through putting this feature together. Across the file, there are a few ways I've gotten deprecated data from the docs:

When I have the Node for a symbol, I can grab the data from it directly via something like member.m_class->doc_data.is_deprecated. As far as I can tell, this would have very little overhead.

For checking if a class is deprecated, I have to get EditorHelp::get_doc_data()->class_list and access it by key. This seems like it might have a bit more overhead.

So far, I'm most nervous about when I want to check whether signals/properties/methods on some class are deprecated. My process so far has been to:

  1. Get the ClassDoc for the given class.
  2. Create an empty HashMap<StringName, bool> to store names and whether or not they are deprecated.
  3. Iterate over the relevant vector within the ClassDoc, and for each item, add an entry to the HashMap where the key is the item's name and the value is whether or not the item is deprecated.
  4. When creating the vector of CodeCompletionOptions, check if each one exists in the HashMap, and if so, set its deprecated value to the corresponding value in the HashMap.

In practice, that looks like this:

// Up at the top of the method, so it can be reused:
const HashMap<String, DocData::ClassDoc> class_doc_map = EditorHelp::get_doc_data()->class_list;

// ...

// Here we're going to create the list of constants to provide as autocomplete suggestions.
HashMap<StringName, Variant> constants;
scr->get_constants(&constants);

// Create a HashMap mapping constants' names to their deprecated status.
HashMap<StringName, bool> const_is_deprecated_map;
for (const DocData::ConstantDoc &constant_doc : class_doc_map.get(base_type.class_type->get_global_name()).constants) {
    const_is_deprecated_map.insert(constant_doc.name, constant_doc.is_deprecated);
}

for (const KeyValue<StringName, Variant> &E : constants) {
    int location = p_recursion_depth + _get_constant_location(scr, E.key);
    ScriptLanguage::CodeCompletionOption option(E.key.operator String(), ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location);

    // Add the constant's deprecated status if we have it.
    if (const_is_deprecated_map.has(E.key.operator String())) {
        option.deprecated = const_is_deprecated_map.get(E.key.operator String());
    }
    r_result.insert(option.display, option);
}

Without changing how the doc data is structured, I'm not sure how this could be made much more efficient. These hashmaps could perhaps be computed earlier on, alongside the rest of the doc data, and then cached? Although then that introduces multiple sources of truth for deprecated statuses, which isn't ideal.

@Meorge Meorge force-pushed the language-server-tomfoolery branch from fcace42 to 120e7b4 Compare December 11, 2024 23:27
@HolonProduction
Copy link
Member

For checking if a class is deprecated, I have to get EditorHelp::get_doc_data()->class_list and access it by key. This seems like it might have a bit more overhead.

I wouldn't worry about hashmap lookups and such, that should be fine. Just triggering parsing and analyzing for a script can get very expensive.

modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
r_result.insert(option.display, option);
}
} break;
case GDScriptParser::ClassNode::Member::CONSTANT: {
if (member.constant->get_datatype().is_meta_type && p_context.current_class->outer != nullptr) {
// TODO: Describe this case and its examples.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO: Describe this case and its examples.
// Preloaded type within this script.
// const MyType = preload("res://my_type.gd)
// var m: MyT... # <-- will suggest MyType

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble verifying this. When I try assembling this in my project, I don't see the completion option show up at all - so that may be a bug with the script parser overall 😅

CleanShot 2024-12-12 at 08 55 06@2x

Copy link
Member

@HolonProduction HolonProduction Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah forgot, it's broken on master. #94996 would fix it but I can't get any reviews it seems :(

Funnily enough the PR fixes this in a different code path. Might need to investigate this one again. My bad I fixed a similar case in another place but this one as well. Just from looking at the code it should work in inner classes only at the moment.

modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/language_server/godot_lsp.h Outdated Show resolved Hide resolved
modules/gdscript/language_server/godot_lsp.h Outdated Show resolved Hide resolved
Copy link
Contributor Author

@Meorge Meorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the behaviors I tried to verify with tests but wasn't able to. Specifically, it seemed like _find_enumeration_candidates was never being triggered. They may well be bugs we should file separate issues for.

modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
r_result.insert(option.display, option);
}
} break;
case GDScriptParser::ClassNode::Member::CONSTANT: {
if (member.constant->get_datatype().is_meta_type && p_context.current_class->outer != nullptr) {
// TODO: Describe this case and its examples.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble verifying this. When I try assembling this in my project, I don't see the completion option show up at all - so that may be a bug with the script parser overall 😅

CleanShot 2024-12-12 at 08 55 06@2x

modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
modules/gdscript/gdscript_editor.cpp Outdated Show resolved Hide resolved
@Meorge Meorge force-pushed the language-server-tomfoolery branch from b368813 to 6d9a9fe Compare December 20, 2024 03:00
@Meorge Meorge marked this pull request as ready for review December 20, 2024 03:01
@Meorge Meorge requested review from a team as code owners December 20, 2024 03:01
@Meorge Meorge force-pushed the language-server-tomfoolery branch from 6d9a9fe to 8310299 Compare December 20, 2024 04:26
@Meorge
Copy link
Contributor Author

Meorge commented Dec 20, 2024

There was an issue where I was attempting to access editor theming, and causing some of the CI to fail, but I think I found a fix for that. If all the checks don't pass, I'll try to continue working through them on another branch so that they put less strain on the main repo's CI resources 😅

@dalexeev
Copy link
Member

Just a note that after #91060 local variables and constants can also be deprecated/experimental. Not sure how useful this is in practice and whether LSP supports deprecating local identifiers, but it would be nice to add it.

@Meorge
Copy link
Contributor Author

Meorge commented Dec 20, 2024

For me, practically speaking, it doesn't make sense for a local variable to be deprecated - if so then I feel like it's time for a refactor of your code 😅 But, if documentation comments are parsed for local identifiers and that includes deprecated status, I also agree that for consistency's sake it should be added.

And added it is! I may not push it to the branch that this PR is tracking immediately, as I'd like to get the CI issues resolved first.

CleanShot 2024-12-20 at 11 03 20@2x
CleanShot 2024-12-20 at 11 03 41@2x

@Meorge Meorge requested a review from a team as a code owner December 20, 2024 20:12
@@ -1209,6 +1294,16 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
if (!base_type.is_meta_type) {
List<PropertyInfo> members;
scr->get_script_property_list(&members);

HashSet<StringName> deprecated_properties;
if (base_type.class_type && class_doc_map.has(base_type.class_type->get_global_name())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you shouldn't use get_global_name() to determine the doc class name. Because for unnamed classes, inner classes and autoload singletons the name may differ from the global name.

After #91060 there is a new method Script::get_doc_class_name(), but there is no counterpart for GDScriptParser::ClassNode. I reused GDScriptDocGen::doctype_from_gdtype() (which uses GDScriptDocGen::_get_class_name()).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'll look more into this once I have the compatibility method stuff working and CI tests passing.

CodeCompletionOptions need to be given the correct deprecated status still

Variables marked as deprecated are recognized

Attempt to get deprecated info about GDScript functions

Consult DocData for more things being marked deprecated

@GDscript methods and other things work now!

Support for methods on built-in classes

Add support for enums, and clean up some of the code to not use iterators when not necessary

Added more deprecated checks

Attempt to understand named enum property documentation

Display deprecated functions with strikethrough in built-in script editor

Named enum members now working!

Recognize deprecated built-in classes when used as types

Add deprecated checks for some spots, while trying to find autoload spot

Added better TODO note

Autoloads do technically kinda work, but only if you clear .godot
@Meorge
Copy link
Contributor Author

Meorge commented Dec 26, 2024

I'm currently working on getting the CI checks to pass. By adding the deprecated argument to CodeEdit::add_code_completion_option, I broke compatibility for that method and am now trying to get a compatibility method for it to work correctly. To avoid using Godot's CI resources but still run the checks, I'm continuing it on the branch at https://github.com/Meorge/godot/tree/language-server-tomfoolery-dev. It seems to be failing with the "Hash changed" error.

In code_edit.compat.inc:

void CodeEdit::_add_code_completion_option_compat_100019(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color, const Ref<Resource> &p_icon, const Variant &p_value, int p_location) {
	add_code_completion_option(p_type, p_display_text, p_insert_text, p_text_color, p_icon, p_value, p_location, false);
}

void CodeEdit::_bind_compatibility_methods() {
	// ...
	ClassDB::bind_compatibility_method(D_METHOD("add_code_completion_option", "type", "display_text", "insert_text", "text_color", "icon", "value", "location"), &CodeEdit::_add_code_completion_option_compat_100019, DEFVAL(Color(1, 1, 1)), DEFVAL(Ref<Resource>()), DEFVAL(Variant::NIL), DEFVAL(LOCATION_OTHER), DEFVAL(false));
}

In code_edit.h:

#ifndef DISABLE_DEPRECATED
	// ...
	void _add_code_completion_option_compat_100019(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color = Color(1, 1, 1), const Ref<Resource> &p_icon = Ref<Resource>(), const Variant &p_value = Variant::NIL, int p_location = LOCATION_OTHER);
	static void _bind_compatibility_methods();
#endif

And, in 4.3-stable.expected:

GH-100019
--------
Validate extension JSON: Error: Field 'classes/CodeEdit/methods/add_code_completion_option/arguments': size changed value in new API, from 7 to 8.

New argument `deprecated` added to `add_code_completion_option`. Compatibility method registered.

However I'm still getting the error:

Compatibility to 4.3-stable is broken in the following ways:
Validate extension JSON: Error: Hash changed for 'classes/CodeEdit/methods/add_code_completion_option', from EB1A746E to 611C3D20. This means that the function has changed and no compatibility function was provided.

I feel like I've followed all of the steps at https://docs.godotengine.org/en/stable/contributing/development/handling_compatibility_breakages.html, so I'm feeling a bit out of ideas at the moment... 😅

Continue to try to get compatibility method to work

Avoid making changes to original compatibility method

Maybe it will work this time??

Please

Add another API compatibility check thing
@Meorge Meorge force-pushed the language-server-tomfoolery branch from f0f34f4 to d9e37ed Compare December 28, 2024 01:33
@Meorge Meorge requested review from a team as code owners December 28, 2024 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants