mphe / godot-diagnostic-list

A Godot editor plugin that provides project-wide diagnostics for GDScript files.
MIT License
20 stars 3 forks source link

Error: Class "ClassName" hides a global script class #1

Closed Gamemap closed 9 months ago

Gamemap commented 9 months ago

Thank you for this addon, it helped me a lot while converting my project to static typing!

Now I was trying to use class_name in my project, but this addon now shows this error (the Godot Editor isn't showing it): Class "ClassName" hides a global script class

This happens also on a new project with just this addon installed on every class_name of this addon. (ProjectSetting debug/gdscript/warnings/exclude_addons disabled)

E.g.: Class "DiagnosticList_LSPClient" hides a global script class This is the output, when enabling the LSPClient debug log:

{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, 
"message": "Class \"DiagnosticList_LSPClient\" hides a global script class.", "range": { 
"end": { "character": 35, "line": 1 }, "start": { "character": 0, "line": 1 } }, "severity": 1, "source": "gdscript" }], 
"uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_test/addons/diagnosticlist/LSPClient.gd" } }

Is it an issue on the godot side, or is it possible to fix in this addon?

mphe commented 9 months ago

Glad you like the addon!

It is likely a bug in path handling causing the same script to be registered under two different file paths, making Godot think these are two separate scripts, hence causing the error. I encountered this type of issue before but thought I had fixed it. Could you please upload a minimal reproduction project and the full log?

Gamemap commented 9 months ago

I'm sorry, but I can't create an own minimal reproduction project with this error. (I can't get the error message.) Since it also occurs with this addon, I uploaded it and set it so that it should show this error, as well as a few others, immediately after starting. I only found an error message that the class name could not be found. (Maybe it is related to this?) I hope this is somehow helpful.


[DiagnosticList] Sending message (length: 872): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Diagnostic.gd","text":"extends RefCounted\nclass_name DiagnosticList_Diagnostic\n\n\nenum Severity {\n\tError,\n\tWarning,\n\tInfo,\n\tHint,\n}\n\n\nclass Pack extends RefCounted:\n\tvar res_uri: StringName\n\tvar diagnostics: Array[DiagnosticList_Diagnostic]\n\n\n## Represents the file path as res:// path\n@export var res_uri: StringName\n@export var line_start: int  # zero-based\n@export var column_start: int  # zero-based\n@export var severity: Severity\n@export var message: String\n\nvar _filename: StringName\n\nfunc get_filename() -> StringName:\n\tif _filename.is_empty():\n\t\t_filename = StringName(res_uri.get_file())\n\treturn _filename\n\n\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 192): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Diagnostic.gd"}}}
[DiagnosticList] Sending message (length: 8894): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/DiagnosticProvider.gd","text":"extends RefCounted\nclass_name DiagnosticList_DiagnosticProvider\n\n## Triggered when new diagnostics for a file arrived.\nsignal on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack)\n\n## Triggered when all outstanding diagnostics have been received.\nsignal on_diagnostics_finished\n\n## Triggered when sources have changed and a diagnostic update is available.\nsignal on_diagnostics_available\n\n## Triggered at the same time as on_publish_diagnostics but provides status information\nsignal on_update_progress(num_remaining: int, num_all: int)\n\n\nclass FileCache extends RefCounted:\n    var content: String = \"\"\n    var last_modified: int = -1\n\n\nvar _diagnostics: Array[DiagnosticList_Diagnostic] = []\nvar _client: DiagnosticList_LSPClient\nvar _script_paths: Array[String] = []\nvar _counts: Array[int] = [ 0, 0, 0, 0 ]\nvar _num_outstanding: int = 0\nvar _dirty: bool = true\nvar _refresh_time: int = 0\nvar _file_cache := {}  # Dict[String, FileCache]\n\n\nfunc _init(client: DiagnosticList_LSPClient) -> void:\n    _client = client\n    _client.on_publish_diagnostics.connect(_on_publish_diagnostics)\n\n    var fs := EditorInterface.get_resource_filesystem()\n\n    # Triggered when saving, removing and moving files.\n    # Also triggers whenever the user is typing or saving in an external editor using LSP.\n    fs.script_classes_updated.connect(_on_script_classes_updated)\n\n    # Triggered when the Godot window receives focus and when moving or deleting files\n    fs.sources_changed.connect(_on_sources_changed)\n\n\nfunc is_updating() -> bool:\n    return _num_outstanding > 0\n\n\n## Refresh diagnostics for all scripts.\n## Returns true on success or false when there are no updates available or when another update is\n## still in progress.\nfunc refresh_diagnostics(force: bool = false) -> bool:\n    # NOTE: We always have to do a full update, because a change in one file can cause errors in\n    # other files, e.g. renaming an identifier.\n\n    # Still waiting for results from the last call\n    if _num_outstanding > 0:\n        _dirty = false  # Dirty will be reset anyway after update has been finished\n        return false\n\n    # Nothing changed -> Nothing to do\n    if not force and not _dirty:\n        return false\n\n    var files_modified := refresh_file_list()\n\n    # No files have actually been modified -> Nothing to do\n    if not force and not files_modified:\n        _dirty = false\n        return false\n\n    _diagnostics.clear()\n    _counts = [ 0, 0, 0, 0 ]\n    _num_outstanding = len(_script_paths)\n    _refresh_time = Time.get_ticks_usec()\n\n    if _num_outstanding > 0:\n        for file in _script_paths:\n            _client.update_diagnostics(file, _file_cache[file].content)\n    else:\n        call_deferred(\"_finish_update\")\n\n    # NOTE: Do not reset _dirty here, because it will be resetted anyway in _finish_update() after\n    # all diagnostics have been received.\n    return true\n\n\n## Rescan the project for script files\n## Returns true when there have been changes, otherwise false.\nfunc refresh_file_list() -> bool:\n    if ProjectSettings.get(\"debug/gdscript/warnings/exclude_addons\"):\n        _script_paths = _gather_scripts(\"res://\", [ \"res://addons\" ])\n    else:\n        _script_paths = _gather_scripts(\"res://\", [])\n\n    var modified: bool = false\n\n    # Update cache\n    for path in _script_paths:\n        var cache: FileCache = _file_cache.get(path)\n        var last_modified: int = FileAccess.get_modified_time(path)\n\n        if not cache:\n            cache = FileCache.new()\n            _file_cache[path] = cache\n            # The next condition will also inevitably be true\n\n        if cache.last_modified != last_modified:\n            cache.last_modified = last_modified\n            cache.content = FileAccess.get_file_as_string(path)\n            modified = true\n\n    # One or more files were deleted\n    if _file_cache.size() > _script_paths.size():\n        modified = true\n\n        # TODO: Could be more efficient, but happens not so often\n        for path: String in _file_cache.keys():\n            if not _script_paths.has(path):\n                _file_cache.erase(path)\n\n    return modified\n\n\n## Get the amount of diagnostics of a given severity.\nfunc get_diagnostic_count(severity: DiagnosticList_Diagnostic.Severity) -> int:\n    return _counts[severity]\n\n\n## Returns all diagnostics of the project\nfunc get_diagnostics() -> Array[DiagnosticList_Diagnostic]:\n    return _diagnostics.duplicate()\n\n\n## Returns the amount of microseconds between requesting the last diagnostic update and the last\n## diagnostic being delivered.\nfunc get_refresh_time_usec() -> int:\n    return _refresh_time\n\n\nfunc are_diagnostics_available() -> bool:\n    return _dirty\n\n\nfunc _finish_update() -> void:\n    # NOTE: When parsing scripts using LSP, the script_classes_updated signal will be fired multiple\n    # times by the engine without any actual changes.\n    # Hence, to prevent false positive dirty flags, reset _dirty back to false when the diagnsotic\n    # update is finished.\n    # FIXME: It might happen that the user makes a change while diagnostics are still refreshing,\n    # In this case, the dirty flag would still be resetted, even though it shouldn't.\n    # This is essentially a tradeoff between efficiency and accuracy.\n    # As I find this exact scenario unlikely to occur regularily, I prefer the more efficient\n    # implementation of updating less often.\n    _dirty = false\n\n    _refresh_time = Time.get_ticks_usec() - _refresh_time\n    on_diagnostics_finished.emit()\n\n\nfunc _mark_dirty() -> void:\n    if not _dirty:\n        # If an update is currently in progress, don't do anything. _dirty will be reset anyway in\n        # _finish_update().\n        if _num_outstanding > 0:\n            return\n\n        _dirty = true\n        on_diagnostics_available.emit()\n\n\nfunc _on_sources_changed(_exist: bool) -> void:\n    _mark_dirty()\n\n\nfunc _on_script_classes_updated() -> void:\n    # NOTE: When using an external editor over LSP, the engine will constantly emit the\n    # script_classes_updated signal whenever the user is typing.\n    # In those cases it is useless to perform an update, as nothing actually changed.\n    # We also cannot safely determine when the user has saved a file except by comparing file\n    # modification timestamps.\n    #\n    # However, whenever the Godot window receives focus, a sources_changed signal is fired.\n    #\n    # Hence, to prevent unnecessary amounts of updates, check whether the Godot window has focus and\n    # if it doesn't, ignore the signal, as the user is likely typing in an external editor.\n    #\n    # When using the internal editor, script_classes_updated will only be fired upon saving.\n    # Hence, when the signal arrives and the Godot window has focus, an update should be performed.\n    if EditorInterface.get_base_control().get_window().has_focus():\n        _mark_dirty()\n\n\nfunc _on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack) -> void:\n    # Ignore unexpected diagnostic updates\n    if _num_outstanding == 0:\n        _client.log_error(\"Received diagnostics without having them requested before\")\n        return\n\n    _diagnostics.append_array(diagnostics.diagnostics)\n    _num_outstanding -= 1\n\n    # Increase new diagnostic counts\n    for diag in diagnostics.diagnostics:\n        _counts[diag.severity] += 1\n\n    on_publish_diagnostics.emit(diagnostics)\n    on_update_progress.emit(_num_outstanding, len(_script_paths))\n\n    if _num_outstanding == 0:\n        _finish_update()\n\n\n# TODO: Consider making ignore_dirs a set if there will ever be more than one entry\nfunc _gather_scripts(searchpath: String, ignore_dirs: Array[String]) -> Array[String]:\n    var root := DirAccess.open(searchpath)\n\n    if not root:\n        push_error(\"Failed to open directory: \", searchpath)\n\n    var paths: Array[String] = []\n\n    if root.file_exists(\".gdignore\"):\n        return paths\n\n    root.include_navigational = false\n    root.list_dir_begin()\n\n    var fname := root.get_next()\n\n    var root_path := root.get_current_dir()\n\n    while not fname.is_empty():\n        var path := root_path.path_join(fname)\n\n        if root.current_is_dir():\n            if not ignore_dirs.has(path):\n                paths.append_array(_gather_scripts(path, ignore_dirs))\n        elif fname.ends_with(\".gd\"):\n            paths.append(path)\n\n        fname = root.get_next()\n\n    root.list_dir_end()\n\n    return paths\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 200): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/DiagnosticProvider.gd"}}}
[DiagnosticList] Sending message (length: 8209): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/LSPClient.gd","text":"extends RefCounted\nclass_name DiagnosticList_LSPClient\n\n## Triggered when connected to the LS.\nsignal on_connected\n\n## Triggered when LSP has been initialized\nsignal on_initialized\n\n## Triggered when new diagnostics for a file arrived.\nsignal on_publish_diagnostics(diagnostics: DiagnosticList_Diagnostic.Pack)\n\n\nconst ENABLE_DEBUG_LOG: bool = true\nconst TICK_INTERVAL_SECONDS_MIN: float = 0.05\nconst TICK_INTERVAL_SECONDS_MAX: float = 30.0\n\n\nvar _jsonrpc := JSONRPC.new()\nvar _client := StreamPeerTCP.new()\nvar _id: int = 0\nvar _timer: Timer\n\n\nfunc _init(root: Node) -> void:\n\t# NOTE: Since this is a RefCounted, it does not have access to the tree, hence plugin.gd passes\n\t# the plugin root node.\n\t_timer = Timer.new()\n\t_timer.wait_time = TICK_INTERVAL_SECONDS_MIN\n\t_timer.autostart = false\n\t_timer.one_shot = false\n\t_timer.timeout.connect(_on_tick)\n\troot.add_child(_timer)\n\n\nfunc disconnect_lsp() -> void:\n\tlog_debug(\"Disconnecting from LSP\")\n\t_timer.stop()\n\t_client.disconnect_from_host()\n\n\nfunc connect_lsp() -> void:\n\tvar settings := EditorInterface.get_editor_settings()\n\tvar port: int = settings.get(\"network/language_server/remote_port\")\n\tvar host: String = settings.get(\"network/language_server/remote_host\")\n\n\tvar err := _client.connect_to_host(host, port)\n\n\tif err != OK:\n\t\tlog_error(\"Failed to connect to LSP server: %s\" % err)\n\n\t# Enable processing\n\t_id = 0\n\t_timer.start()\n\t_reset_tick_interval()\n\n\nfunc update_diagnostics(file_path: String, content: String) -> void:\n\tvar uri := \"file://\" + ProjectSettings.globalize_path(file_path).simplify_path()\n\n\t_send_notification(\"textDocument/didOpen\", {\n\t\t\"textDocument\": {\n\t\t\t\"uri\": uri,\n\t\t\t\"text\": content,\n\t\t\t\"languageId\": \"gdscript\",  # Unused by Godot LSP\n\t\t\t\"version\": 0,  # Unused by Godot LSP\n\t\t}\n\t})\n\n\t# Technically, the Godot LS does nothing on didClose, but send it anyway in case it changes in the future.\n\t_send_notification(\"textDocument/didClose\", {\n\t\t\"textDocument\": {\n\t\t\t\"uri\": uri\n\t\t}\n\t})\n\n\nfunc _reset_tick_interval() -> void:\n\t_timer.start(TICK_INTERVAL_SECONDS_MIN)\n\n\nfunc _update_tick_interval() -> void:\n\t# Double the tick interval to gradiually reduce computation time when not in use.\n\t_timer.wait_time = minf(_timer.wait_time * 2, TICK_INTERVAL_SECONDS_MAX)\n\n\nfunc _on_tick() -> void:\n\tif not _update_status():\n\t\tdisconnect_lsp()\n\t\treturn\n\n\t_update_tick_interval()\n\n\twhile _client.get_available_bytes():\n\t\tvar json := _read_data()\n\n\t\tif json:\n\t\t\tlog_debug(\"Received message:\\n%s\" % json)\n\n\t\t_handle_response(json)\n\t\t_reset_tick_interval()  # Reset timer interval whenever data arrived as there will likely be more data coming\n\n\n## Updates the current socket status and returns true when the main loop should continue.\nfunc _update_status() -> bool:\n\tvar last_status := _client.get_status()\n\n\t_client.poll()\n\n\tvar status := _client.get_status()\n\n\tmatch status:\n\t\tStreamPeerTCP.STATUS_NONE:\n\t\t\treturn false\n\t\tStreamPeerTCP.STATUS_ERROR:\n\t\t\tlog_error(\"StreamPeerTCP error\")\n\t\t\treturn false\n\t\tStreamPeerTCP.STATUS_CONNECTING:\n\t\t\tpass\n\t\tStreamPeerTCP.STATUS_CONNECTED:\n\t\t\t# First time connected -> run initialization\n\t\t\tif last_status != status:\n\t\t\t\tlog_debug(\"Connected to LSP\")\n\t\t\t\ton_connected.emit()\n\t\t\t\t_initialize()\n\n\treturn true\n\n\nfunc _read_data() -> Dictionary:\n\t# NOTE:\n\t# At the moment, Godot only ever transmits headers with a single Content-Length field and\n\t# likewise expects headers with only one field (see gdscript_language_protocol.cpp, line 61).\n\t# Hence, the following also assumes there is only the Content-Length field in the header.\n\t# If Godot ever starts sending additional fields, this will break.\n\n\tvar header := _read_header().strip_edges()\n\tvar content_length := int(header.substr(len(\"Content-Length\")))\n\tvar content := _read_content(content_length)\n\tvar json: Dictionary = JSON.parse_string(content)\n\n\tif not json:\n\t\tlog_error(\"Failed to parse JSON: %s\" % content)\n\t\treturn {}\n\n\treturn json\n\n\nfunc _read_content(length: int) -> String:\n\tvar data := _client.get_data(length)\n\n\tif data[0] != OK:\n\t\tlog_error(\"Failed to read content: %s\" % error_string(data[0]))\n\t\treturn \"\"\n\telse:\n\t\tvar buf: PackedByteArray = data[1]\n\t\treturn buf.get_string_from_utf8()\n\n\nfunc _read_header() -> String:\n\tvar buf := PackedByteArray()\n\tvar char_r := \"\\r\".unicode_at(0)\n\tvar char_n := \"\\n\".unicode_at(0)\n\n\twhile true:\n\t\tvar data := _client.get_data(1)\n\n\t\tif data[0] != OK:\n\t\t\tlog_error(\"Failed to read header: %s\" % error_string(data[0]))\n\t\t\treturn \"\"\n\t\telse:\n\t\t\tbuf.push_back(data[1][0])\n\n\t\tvar bufsize := buf.size()\n\n\t\tif bufsize >= 4 \\\n\t\t\t\tand buf[bufsize - 1] == char_n \\\n\t\t\t\tand buf[bufsize - 2] == char_r \\\n\t\t\t\tand buf[bufsize - 3] == char_n \\\n\t\t\t\tand buf[bufsize - 4] == char_r:\n\t\t\treturn buf.get_string_from_ascii()\n\n\t# This should never happen but the GDScript compiler complains \"not all code paths return a value\"\n\treturn \"\"\n\n\nfunc _handle_response(json: Dictionary) -> void:\n\t# Diagnostics received\n\tif json.get(\"method\") == \"textDocument/publishDiagnostics\":\n\t\ton_publish_diagnostics.emit(_parse_diagnostics(json[\"params\"]))\n\t# Initialization response\n\telif json.get(\"id\") == 0:\n\t\t_send_notification(\"initialized\", {})\n\t\ton_initialized.emit()\n\n\n## Parses the diagnostic information according to the LSP specification.\n## https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#publishDiagnosticsParams\nfunc _parse_diagnostics(params: Dictionary) -> DiagnosticList_Diagnostic.Pack:\n\tvar result := DiagnosticList_Diagnostic.Pack.new()\n\tresult.res_uri = StringName(ProjectSettings.localize_path(str(params[\"uri\"]).replace(\"file://\", \"\")))\n\n\tvar diagnostics: Array[Dictionary] = []\n\tdiagnostics.assign(params[\"diagnostics\"])\n\n\tfor diag in diagnostics:\n\t\tvar range_start: Dictionary = diag[\"range\"][\"start\"]\n\t\tvar entry := DiagnosticList_Diagnostic.new()\n\t\tentry.res_uri = result.res_uri\n\t\tentry.message = diag[\"message\"]\n\t\tentry.severity = (int(diag[\"severity\"]) - 1) as DiagnosticList_Diagnostic.Severity  # One-based in LSP, hence convert to the zero-based enum value\n\t\tentry.line_start = int(range_start[\"line\"])\n\t\tentry.column_start = int(range_start[\"character\"])\n\t\tresult.diagnostics.append(entry)\n\n\treturn result\n\n\nfunc _send_request(method: String, params: Dictionary) -> int:\n\t_send(_jsonrpc.make_request(method, params, _id))\n\t_id += 1\n\treturn _id - 1\n\n\nfunc _send_notification(method: String, params: Dictionary) -> void:\n\t_send(_jsonrpc.make_notification(method, params))\n\n\nfunc _send(json: Dictionary) -> void:\n\tvar content := JSON.stringify(json, \"\", false)\n\tvar content_bytes := content.to_utf8_buffer()\n\tvar header := \"Content-Length: %s\\r\\n\\r\\n\" % len(content)\n\tvar header_bytes := header.to_ascii_buffer()\n\tlog_debug(\"Sending message (length: %s): %s\" % [ len(content), content ])\n\t_client.put_data(header_bytes + content_bytes)\n\t_reset_tick_interval()  # Reset the timer interval because we are expecting a response\n\n\nfunc _initialize() -> void:\n\tvar root_path := ProjectSettings.globalize_path(\"res://\")\n\n\t_send_request(\"initialize\", {\n\t\t\"processId\": null,\n\t\t\"rootPath\": root_path,\n\t\t\"rootUri\": \"file://\" + root_path,\n\t\t\"capabilities\": {\n\t\t\t\"textDocument\": {\n\t\t\t\t\"publishDiagnostics\": {},\n\t\t\t},\n\t\t},\n\t})\n\n\nfunc log_debug(text: String) -> void:\n\tif ENABLE_DEBUG_LOG:\n\t\tprint(\"[DiagnosticList] \", text)\n\nfunc log_error(text: String) -> void:\n\tpush_error(\"[DiagnosticList] \", text)\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 191): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/LSPClient.gd"}}}
[DiagnosticList] Sending message (length: 8559): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd","text":"@tool\nextends Control\nclass_name DiagnosticList_Panel\n\nclass DiagnosticSeveritySettings extends RefCounted:\n\tvar text: String\n\tvar icon: Texture2D\n\tvar color: Color\n\n\tfunc _init(text_: String, icon_id: StringName, color_id: StringName) -> void:\n\t\tself.text = text_\n\t\tself.icon = EditorInterface.get_editor_theme().get_icon(icon_id, &\"EditorIcons\")\n\t\tself.color = EditorInterface.get_editor_theme().get_color(color_id, &\"Editor\")\n\n\n@onready var _btn_refresh_errors: Button = %\"btn_refresh_errors\"\n@onready var _error_list_tree: Tree = %\"error_tree_list\"\n@onready var _cb_auto_refresh: CheckBox = %\"cb_auto_refresh\"\n@onready var _cb_group_by_file: CheckBox = %\"cb_group_by_file\"\n@onready var _label_refresh_time: Label = %\"label_refresh_time\"\n\n# This array will be filled according to each severity type to allow direct indexing\n@onready var _filter_buttons: Array[Button] = [\n\t%\"btn_filter_errors\",\n\t%\"btn_filter_warnings\",\n\t%\"btn_filter_infos\",\n\t%\"btn_filter_hints\",\n]\n\n# This array will be filled according to each severity type to allow direct indexing\n@onready var _severity_settings: Array[DiagnosticSeveritySettings] = [\n\tDiagnosticSeveritySettings.new(\"Error\", &\"StatusError\", &\"error_color\"),\n\tDiagnosticSeveritySettings.new(\"Warning\", &\"StatusWarning\", &\"warning_color\"),\n\tDiagnosticSeveritySettings.new(\"Info\", &\"Popup\", &\"font_color\"),\n\tDiagnosticSeveritySettings.new(\"Hint\", &\"Info\", &\"font_color\"),\n]\n\n@onready var _script_icon: Texture2D = get_theme_icon(&\"Script\", &\"EditorIcons\")\n\nvar _provider: DiagnosticList_DiagnosticProvider\n\n\n## Alternative to _ready(). This will be called by plugin.gd to ensure the code in here only runs\n## when this script is loaded as part of the plugin and not while editing the scene.\nfunc _plugin_ready() -> void:\n\tfor i in len(_filter_buttons):\n\t\tvar btn: Button = _filter_buttons[i]\n\t\tvar severity := _severity_settings[i]\n\t\tbtn.icon = severity.icon\n\n\t# These kinds of severities do not exist yet in Godot LSP, so hide them for now.\n\t_filter_buttons[DiagnosticList_Diagnostic.Severity.Info].hide()\n\t_filter_buttons[DiagnosticList_Diagnostic.Severity.Hint].hide()\n\n\t_cb_auto_refresh.button_pressed = DiagnosticList_Settings.get_auto_refresh()\n\n\t_error_list_tree.columns = 3\n\t_error_list_tree.set_column_title(0, \"Message\")\n\t_error_list_tree.set_column_title(1, \"File\")\n\t_error_list_tree.set_column_title(2, \"Line\")\n\t_error_list_tree.set_column_title_alignment(0, HORIZONTAL_ALIGNMENT_LEFT)\n\t_error_list_tree.set_column_title_alignment(1, HORIZONTAL_ALIGNMENT_LEFT)\n\t_error_list_tree.set_column_title_alignment(2, HORIZONTAL_ALIGNMENT_LEFT)\n\n\tvar line_column_size := _error_list_tree.get_theme_font(\"font\").get_string_size(\n\t\t\"Line 0000\", HORIZONTAL_ALIGNMENT_LEFT, -1, _error_list_tree.get_theme_font_size(\"font_size\"))\n\n\t_error_list_tree.set_column_custom_minimum_width(0, 0)\n\t_error_list_tree.set_column_custom_minimum_width(1, 0)\n\t_error_list_tree.set_column_custom_minimum_width(2, int(line_column_size.x))\n\n\t_error_list_tree.set_column_expand(0, true)\n\t_error_list_tree.set_column_expand(1, true)\n\t_error_list_tree.set_column_expand(2, false)\n\t_error_list_tree.set_column_clip_content(0, true)\n\t_error_list_tree.set_column_clip_content(1, true)\n\t_error_list_tree.set_column_clip_content(2, false)\n\t_error_list_tree.set_column_expand_ratio(0, 4)\n\n\n## Called by plugin.gd when the LSPClient is ready\nfunc start(provider: DiagnosticList_DiagnosticProvider) -> void:\n\t_provider = provider\n\n\t# Now that it is safe to do stuff, connect all the signals\n\t_provider.on_diagnostics_finished.connect(_on_diagnostics_finished)\n\t_provider.on_update_progress.connect(_on_update_progress)\n\n\t_btn_refresh_errors.pressed.connect(_on_force_refresh)\n\t_cb_group_by_file.toggled.connect(_on_group_by_file_toggled)\n\t_cb_auto_refresh.toggled.connect(_on_auto_refresh_toggled)\n\t_error_list_tree.item_activated.connect(_on_item_activated)\n\n\tfor btn in _filter_buttons:\n\t\tbtn.toggled.connect(_on_filter_toggled)\n\n\t# Start checking\n\t_set_status_string(\"\", false)\n\t_start_stop_auto_refresh()\n\n\nfunc refresh() -> void:\n\t# NOTE: This list is sorted by file name as LSP publishes diagnostics per file\n\t# This is important as the group-by-file implementation relies on it.\n\tvar diagnostics := _provider.get_diagnostics()\n\tvar group_by_file := _cb_group_by_file.button_pressed\n\n\tif not group_by_file:\n\t\tdiagnostics.sort_custom(_sort_by_severity)\n\n\t# Show refresh time\n\t_set_status_string(\"Up-to-date\", true)\n\n\t# Clear tree\n\t_error_list_tree.clear()\n\t_error_list_tree.create_item()\n\n\t# Create diagnostics\n\tvar last_uri: StringName\n\tvar parent: TreeItem = null\n\n\tfor diag in diagnostics:\n\t\tif not _filter_buttons[diag.severity].button_pressed:\n\t\t\tcontinue\n\n\t\t# If grouping by file, create header entries if necessary\n\t\tif group_by_file and diag.res_uri != last_uri:\n\t\t\tlast_uri = diag.res_uri\n\t\t\tparent = _error_list_tree.create_item()\n\t\t\tparent.set_text(0, diag.res_uri)\n\t\t\tparent.set_icon(0, _script_icon)\n\t\t\tparent.set_metadata(0, diag)\n\n\t\t_create_entry(diag, parent)\n\n\t# Update diagnostic counts\n\tfor i in len(_filter_buttons):\n\t\t_filter_buttons[i].text = str(_provider.get_diagnostic_count(i))\n\n\nfunc _set_status_string(text: String, with_last_time: bool) -> void:\n\tif with_last_time:\n\t\t_label_refresh_time.text = \"%s\\n%.2f s\" % [ text, _provider.get_refresh_time_usec() / 1000000.0 ]\n\telse:\n\t\t_label_refresh_time.text = text\n\n\nfunc _sort_by_severity(a: DiagnosticList_Diagnostic, b: DiagnosticList_Diagnostic) -> bool:\n\tif a.severity == b.severity:\n\t\treturn a.res_uri < b.res_uri\n\treturn a.severity < b.severity\n\n\nfunc _create_entry(diag: DiagnosticList_Diagnostic, parent: TreeItem) -> void:\n\tvar entry: TreeItem = _error_list_tree.create_item(parent)\n\tvar severity_setting := _severity_settings[diag.severity]\n\t# entry.set_custom_color(0, theme.color)\n\tentry.set_text(0, diag.message)\n\tentry.set_icon(0, severity_setting.icon)\n\tentry.set_text(1, diag.get_filename())\n\tentry.set_tooltip_text(1, diag.res_uri)\n\t# entry.set_text(2, \"Line \" + str(diag.line_start))\n\tentry.set_text(2, str(diag.line_start))\n\tentry.set_metadata(0, diag)  # Meta data is used in _on_item_activated to open the respective script\n\n\nfunc _update_diagnostics(force: bool) -> void:\n\tif _provider.is_updating() or _provider.refresh_diagnostics(force):\n\t\t_set_status_string(\"Updating...\", false)\n\telse:\n\t\t_set_status_string(\"Up-to-date\", true)\n\n\nfunc _start_stop_auto_refresh() -> void:\n\tif _cb_auto_refresh.button_pressed:\n\t\tvisibility_changed.connect(_on_auto_update)\n\t\t_provider.on_diagnostics_available.connect(_on_auto_update)\n\t\t_on_auto_update()  # Also trigger an update immediately\n\telse:\n\t\tvisibility_changed.disconnect(_on_auto_update)\n\t\t_provider.on_diagnostics_available.disconnect(_on_auto_update)\n\n\nfunc _on_item_activated() -> void:\n\tvar selected: TreeItem = _error_list_tree.get_selected()\n\tvar diagnostic: DiagnosticList_Diagnostic = selected.get_metadata(0)\n\n\t# NOTE: Lines and columns are zero-based in LSP, but Godot expects one-based values\n\tEditorInterface.edit_script(load(str(diagnostic.res_uri)), diagnostic.line_start + 1, diagnostic.column_start + 1)\n\n\tif not EditorInterface.get_editor_settings().get(\"text_editor/external/use_external_editor\"):\n\t\tEditorInterface.set_main_screen_editor(\"Script\")\n\n\nfunc _on_force_refresh() -> void:\n\t_update_diagnostics(true)\n\n\nfunc _on_auto_refresh_toggled(toggled_on: bool) -> void:\n\tDiagnosticList_Settings.set_auto_refresh(toggled_on)\n\t_start_stop_auto_refresh()\n\n\nfunc _on_auto_update() -> void:\n\tif is_visible_in_tree():\n\t\t_update_diagnostics(false)\n\n\nfunc _on_update_progress(num_remaining: int, num_all: int) -> void:\n\t_set_status_string(\"Updating...\\n(%d/%d)\" % [ num_all - num_remaining, num_all ], false)\n\n\nfunc _on_diagnostics_finished() -> void:\n\trefresh()\n\nfunc _on_filter_toggled(_toggled_on: bool) -> void:\n\trefresh()\n\nfunc _on_group_by_file_toggled(_toggled_on: bool) -> void:\n\trefresh()\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 187): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd"}}}
[DiagnosticList] Sending message (length: 1048): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/plugin.gd","text":"@tool\nextends EditorPlugin\n\nconst panel_scene = preload(\"res://addons/diagnosticlist/Panel.tscn\")\n\nvar _dock: DiagnosticList_Panel\nvar _client: DiagnosticList_LSPClient\nvar _provider: DiagnosticList_DiagnosticProvider\n\n\nfunc _enter_tree() -> void:\n    _client = DiagnosticList_LSPClient.new(self)\n    _client.on_initialized.connect(_on_lsp_initialized)\n    _client.connect_lsp()\n\n    _dock = panel_scene.instantiate()\n    _dock.ready.connect(func() -> void: _dock._plugin_ready())\n    add_control_to_bottom_panel(_dock, \"Diagnostics\")\n\n\nfunc _exit_tree() -> void:\n    remove_control_from_bottom_panel(_dock)\n    _dock.free()\n    _client.disconnect_lsp()\n\n\nfunc _on_lsp_initialized() -> void:\n    _provider = DiagnosticList_DiagnosticProvider.new(_client)\n    _dock.start(_provider)\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 188): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/plugin.gd"}}}
[DiagnosticList] Sending message (length: 817): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Settings.gd","text":"extends RefCounted\nclass_name DiagnosticList_Settings\n\n\nconst BASE_SETTING_PATH = \"addons/diagnostic_list/\"\nconst SETTING_AUTO_REFRESH = BASE_SETTING_PATH + \"auto_refresh\"\n\n\nstatic func set_auto_refresh(on: bool) -> void:\n\t_set_setting(SETTING_AUTO_REFRESH, on)\n\n\nstatic func get_auto_refresh() -> bool:\n\treturn ProjectSettings.get_setting(SETTING_AUTO_REFRESH, true) as bool\n\n\nstatic func _set_setting(name: String, value: Variant) -> void:\n\tProjectSettings.set_setting(name, value)\n\tProjectSettings.set_as_internal(name, true)\n\tProjectSettings.save()\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 190): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Settings.gd"}}}
[DiagnosticList] Sending message (length: 263): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/call.gd","text":"extends Node\nclass_name TestFile\n\nvar test = 56\n\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 164): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/call.gd"}}}
[DiagnosticList] Sending message (length: 315): {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/Node.gd","text":"extends Node\n\n@onready var node: TestFile = get_child(0)\n\nfunc _ready() -> void:\n\tprint(node.test)\n","languageId":"gdscript","version":0}}}
[DiagnosticList] Sending message (length: 164): {"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{"uri":"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/Node.gd"}}}
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Class \"DiagnosticList_Diagnostic\" hides a global script class.", "range": { "end": { "character": 36, "line": 1 }, "start": { "character": 0, "line": 1 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Diagnostic.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Class \"DiagnosticList_DiagnosticProvider\" hides a global script class.", "range": { "end": { "character": 44, "line": 1 }, "start": { "character": 0, "line": 1 } }, "severity": 1, "source": "gdscript" }, { "code": -1, "message": "Error while getting cache for script \"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/DiagnosticProvider.gd\".", "range": { "end": { "character": 35, "line": 104 }, "start": { "character": 12, "line": 104 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/DiagnosticProvider.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Class \"DiagnosticList_LSPClient\" hides a global script class.", "range": { "end": { "character": 35, "line": 1 }, "start": { "character": 0, "line": 1 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/LSPClient.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Class \"DiagnosticList_Panel\" hides a global script class.", "range": { "end": { "character": 31, "line": 2 }, "start": { "character": 0, "line": 2 } }, "severity": 1, "source": "gdscript" }, { "code": -1, "message": "Error while getting cache for script \"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd\".", "range": { "end": { "character": 73, "line": 31 }, "start": { "character": 1, "line": 31 } }, "severity": 1, "source": "gdscript" }, { "code": -1, "message": "Error while getting cache for script \"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd\".", "range": { "end": { "character": 79, "line": 32 }, "start": { "character": 1, "line": 32 } }, "severity": 1, "source": "gdscript" }, { "code": -1, "message": "Error while getting cache for script \"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd\".", "range": { "end": { "character": 65, "line": 33 }, "start": { "character": 1, "line": 33 } }, "severity": 1, "source": "gdscript" }, { "code": -1, "message": "Error while getting cache for script \"file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd\".", "range": { "end": { "character": 64, "line": 34 }, "start": { "character": 1, "line": 34 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Panel.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/plugin.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Class \"DiagnosticList_Settings\" hides a global script class.", "range": { "end": { "character": 34, "line": 1 }, "start": { "character": 0, "line": 1 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/addons/diagnosticlist/Settings.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": 18, "message": "(UNTYPED_DECLARATION): Variable \"test\" has no static type.", "range": { "end": { "character": 13, "line": 3 }, "start": { "character": 0, "line": 3 } }, "severity": 2, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/call.gd" } }
[DiagnosticList] Received message:
{ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "diagnostics": [{ "code": -1, "message": "Could not find type \"TestFile\" in the current scope.", "range": { "end": { "character": 42, "line": 2 }, "start": { "character": 0, "line": 2 } }, "severity": 1, "source": "gdscript" }], "uri": "file://D:/Victor/Godot_Projekte/4.2rc1/4.2.1/Diagnostic_ClassName_bug/Node.gd" } }

minimal reproduction project: Diagnostic_ClassName_bug.zip

mphe commented 9 months ago

Thanks, I'll look into it. It seems to be a Windows related problem, because I can't reproduce it here on Linux.

mphe commented 9 months ago

Fixed the issue in recent commit. Could you confirm that it works for you? Apparently I never tested with class_name on Windows.

Gamemap commented 9 months ago

Thank you for helping me, but I think the issue was on my side:

I use up to 4 different Godot projects for my game (Dedicated Server Multiplayer) and installed this addon on each project. However, since each project is open on its own Godot instance and Godot defaults the debugging port to 6007, all Godot instances connected to the same debugger and told me that this class_name was already defined there. If only one Godot instance is running, everything works fine.

The prefix "file:///" has solved this problem for the running one instance, but this prefix must also be used here: LSPClient.gd#L204 grafik

I think that it is not possible to fix the problem when running multiple Godot instances, because the debugging port is defined in the editor settings and not just project-wide. But if you have an idea, I'm open to it. :)

mphe commented 9 months ago

The prefix "file:///" has solved this problem for the running one instance, but this prefix must also be used here: LSPClient.gd#L204

Good catch! Missed that one. Fixed in recent commit.

I think that it is not possible to fix the problem when running multiple Godot instances, because the debugging port is defined in the editor settings and not just project-wide. But if you have an idea, I'm open to it. :)

It's actually the language server port, not the debugging port.

Afaik, there is no workaround or fix for this. I've also scratched my head on how to solve this, but it is simply not possible at the moment. According to the source here, Godot does nothing when the language server fails to start, e.g. when the port is already in use by another instance.

The only thing I could do is check the project path reported by the language server against the actual project path and display a warning if they differ, so the user knows what's going on.

Gamemap commented 9 months ago

In my opinion a warning in the Readme and on the asset store would be enough, but an integrated warning would be of course a better user experience.

mphe commented 9 months ago

Added a note in the readme.

Gamemap commented 9 months ago

Thank you, I think the issue can be closed now.

mphe commented 9 months ago

FYI, added another warning directly in Godot. Asset Library update is pending.