X Tutup
Skip to content

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

Open
Meorge wants to merge 1 commit intogodotengine:masterfrom
Meorge:language-server-tomfoolery
Open

Add support for deprecated symbols to language server and script editor#100019
Meorge wants to merge 1 commit intogodotengine:masterfrom
Meorge:language-server-tomfoolery

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

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.

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.

@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
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.

@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... 😅

@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
@Meorge Meorge force-pushed the language-server-tomfoolery branch from d9e37ed to 4e82439 Compare February 5, 2025 23:40
@Meorge Meorge force-pushed the language-server-tomfoolery branch 4 times, most recently from a7ec64c to d94d23e Compare May 27, 2025 05:22
@Meorge Meorge force-pushed the language-server-tomfoolery branch 5 times, most recently from 4009c98 to fe414a3 Compare June 3, 2025 03:19
@Meorge
Copy link
Contributor Author

Meorge commented Jun 3, 2025

At the GDScript meeting today, this PR was discussed and it was proposed to see how this functionality could potentially be merged with #101370 .

If that PR provides a more uniform, reliable interface to symbol documentation, then I think that approach could be better than what I do in this PR. I'll have to try pulling the other PR and prototyping out the deprecated symbol support in the coming days.

@Meorge Meorge force-pushed the language-server-tomfoolery branch 2 times, most recently from e9fc88a to deda281 Compare June 10, 2025 00:49
@Meorge
Copy link
Contributor Author

Meorge commented Jun 10, 2025

Looking through #101370 some more, I feel like I understand a bit better how these two PRs would fit together. I don't think the other PR would change much about how this PR would function.

The general flow, as I understand it, is:

  • The code structure is analyzed for potential completion candidates. This is where this PR adds the information about whether or not something is deprecated.
  • If the user is typing arguments for a function, then Editor: Use documentation tooltip for function autocompletion #101370 uses the information from the previous step to look up and display the relevant documentation.

Because this PR performs its actions before #101370, merging the latter first wouldn't change how this one would work. However, the deprecation status data added to completion options by this PR might be able to be incorporated into #101370 to display whether or not the function a user is currently trying to use is deprecated.

@Meorge Meorge force-pushed the language-server-tomfoolery branch 4 times, most recently from 4d6026c to 40bfec9 Compare June 17, 2025 15:54
@Meorge Meorge force-pushed the language-server-tomfoolery branch 2 times, most recently from 0078e01 to e7f1d3d Compare July 2, 2025 17:10
@atirutw
Copy link
Contributor

atirutw commented Nov 26, 2025

Probably not my place to ask, but what's the rationale behind using ## @deprecated comments rather than a @deprecated before a symbol's declaration like with @rpc, @export, etc.? I think most people would figure it's more consistent than a comment thing.

@Meorge
Copy link
Contributor Author

Meorge commented Nov 26, 2025

They are an already-existing feature in documentation comments; this PR simply reads the data from that.

@Meorge
Copy link
Contributor Author

Meorge commented Feb 8, 2026

Somewhere along the line, it looks like the Godot script editor support for deprecated symbols in the autocomplete was broken, so now they don't appear with the strikethrough anymore. However, they still appear correctly in VS Code. I'll be able to look more into this soon.

@Meorge
Copy link
Contributor Author

Meorge commented Feb 12, 2026

Strikethrough issue should be fixed! On macOS, the line does feel like it might be ever-so-slightly higher than it should be, but I'm not sure how it appears on other platforms, or how "incorrect" this actually is:

CleanShot 2026-02-12 at 09 34 55@2x

Copy link
Contributor

@AdriaandeJongh AdriaandeJongh left a comment

Choose a reason for hiding this comment

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

I haven't looked at the code, but from a usability perspective this looks good. I was not able to check all the possible usages of deprecation supported by this PR, but the few I did try worked well. I think the place where most users will come across this is deprecated engine functions, and those show up correctly as strikethrough in autocomplete now.

@akien-mga
Copy link
Member

akien-mga commented Mar 9, 2026

Could you amend the commit message to remove the noise caused by previous intermediate commits being squashed?

There are 4 pages worth of this:

image

I noticed most of your merged PRs ended up having similar noise in the git log, which can't be corrected now, but it would be good that current and future PRs have a cleaner commit message (see https://github.com/godotengine/godot/blob/master/CONTRIBUTING.md#format-your-commit-messages-with-readability-in-mind ).

When rebasing, you can use fixup instead of squash to squash commits while discarding the message of the fixup commit(s).

@Meorge
Copy link
Contributor Author

Meorge commented Mar 9, 2026

Certainly, will work on doing that now! Sorry for the long concatenated messages on the previous ones; I'll ensure I keep those short and clean for future PRs as well 😅

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.

10 participants

X Tutup