From 1ab1c460cba047a6c4fcb3132fa80582f050cd55 Mon Sep 17 00:00:00 2001 From: Erik M Jacobs Date: Tue, 12 May 2026 10:15:32 -0400 Subject: [PATCH 1/3] feat: add GDScript (Godot Engine) language server support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1446. Adds Language.GDSCRIPT backed by a new TCPLanguageServer that extends LanguageServerInterface directly — no intermediate transport abstraction layer. Changes: - ls_process.py: adds TCPConnectionInfo dataclass and TCPLanguageServer, a LanguageServerInterface subclass that manages a TCP socket directly (mirroring how StdioLanguageServer manages a subprocess). Stop order closes the socket before the BufferedReader wrapper to avoid a deadlock where BufferedReader.close() blocks on its internal read lock while readline() holds it waiting for data. - ls_config.py: adds Language.GDSCRIPT with .gd/.gdscript file matcher. - language_servers/godot_language_server.py: GodotLanguageServer connects to the Godot editor's TCP LSP (port 6008, same for Godot 3 and 4), registers Godot-specific notification handlers, and performs the initialize/initialized handshake. Godot version is auto-detected from config_version in project.godot. - docs: adds GDScript to the language support list and a dedicated setup guide covering prerequisites, setup steps, performance characteristics (no workspace/symbol support → first full scan is slow, disk cache persists across sessions), and known limitations. Tested against Godot 4.6.1 on Linux (Fedora 41): TCP connection, initialize handshake, textDocument/documentSymbol returning full symbol tree, and clean shutdown in <1s. Co-Authored-By: Claude Sonnet 4.6 --- docs/01-about/020_programming-languages.md | 4 + .../godot_gdscript_setup_guide_for_serena.md | 78 +++++++++ .../language_servers/godot_language_server.py | 127 ++++++++++++++ src/solidlsp/ls_config.py | 12 ++ src/solidlsp/ls_process.py | 155 ++++++++++++++++++ 5 files changed, 376 insertions(+) create mode 100644 docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md create mode 100644 src/solidlsp/language_servers/godot_language_server.py diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 95601e85..4ba86b77 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -71,6 +71,10 @@ Some languages require additional installations or setup steps, as noted. (requires [.NET v8.0+](https://dotnet.microsoft.com/en-us/download/dotnet); uses FsAutoComplete/Ionide, which is auto-installed; for Homebrew .NET on macOS, set DOTNET_ROOT in your environment) * **Fortran** (requires installation of fortls: `pip install fortls`) +* **GDScript** (Godot Engine) + (requires the Godot editor to be running with its built-in LSP enabled — default on port 6008; + Serena connects over TCP and does not launch Godot itself; + see the [GDScript Setup Guide](../03-special-guides/godot_gdscript_setup_guide_for_serena) for details) * **Go** (requires installation of `gopls`) * **Groovy** diff --git a/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md b/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md new file mode 100644 index 00000000..8a5520c4 --- /dev/null +++ b/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md @@ -0,0 +1,78 @@ +# GDScript (Godot Engine) Setup Guide for Serena + +This guide explains how to prepare a Godot project so that Serena can provide code intelligence for GDScript (`.gd`) files via Godot's built-in Language Server Protocol (LSP) support. + +Unlike most language servers, Serena does **not** launch a separate LSP process for GDScript. Instead, Serena connects over TCP to the LSP server that the Godot editor itself runs while it is open. This means the Godot editor must already be running with your project loaded before you start Serena. + +--- +## Prerequisites + +- Godot Engine 3.x or 4.x installed and available on your system. +- A Godot project with a `project.godot` file at the project root. + +No additional language server needs to be installed — Godot ships with a built-in LSP server that is enabled by default. + +--- +## How It Works + +When a Godot project is open in the editor, the editor listens for LSP connections on **TCP port 6008** (the same default for both Godot 3 and Godot 4). Serena connects to this port and communicates using standard LSP Content-Length framing. + +Serena automatically detects which major version of Godot your project targets by reading the `config_version` field from `project.godot`: + +- `config_version` ≥ 5 → Godot 4 +- `config_version` = 4 → Godot 3 + +No additional configuration is needed for version detection. + +--- +## Setup Steps + +1. Open your Godot project in the Godot editor and allow it to finish loading. + +2. Verify that the built-in LSP is enabled (it is on by default): + - Go to **Editor → Editor Settings → Network → Language Server** + - Confirm **"Use Language Server"** is checked. + - Confirm the port is **6008** (the default). + +3. Add `gdscript` to the `languages` list in your project's Serena configuration: + ```yaml + # .serena/project.yml + languages: + - gdscript + ``` + +4. Start Serena in your project root. Serena will connect to the already-running Godot editor automatically. + +--- +## Using Serena with GDScript + +- Serena recognizes `.gd` and `.gdscript` files. The language identifier used in LSP communication is `gdscript`. +- The Godot editor must remain open for the entire Serena session. If the editor is closed, Serena will lose its connection and will need to be restarted once the editor is open again. +- On first use, you may see a brief delay while Serena establishes the TCP connection and the editor indexes your project files — this is expected. + +--- +## Performance Note + +Godot's built-in LSP does not implement the `workspace/symbol` request (global symbol search across the whole project). As a result, when Serena performs a workspace-wide symbol search — for example, calling `find_symbol` without a `relative_path` — it must fall back to sending a `textDocument/documentSymbol` request for **every `.gd` file** in the project individually. + +For large projects, this initial full-project scan can take **30–60 seconds or more**. + +To mitigate this: + +- **Pass `relative_path` whenever possible.** Narrowing a search to a specific file or subdirectory avoids scanning the entire workspace. +- **Let the first full scan complete.** Serena caches the results to disk after the initial scan. Subsequent Serena sessions will use the on-disk cache and start instantly. + +--- +## Known Limitations + +| Limitation | Detail | +|---|---| +| No `workspace/symbol` support | Godot's LSP does not implement this request. All symbol lookups fall back to per-file `documentSymbol` requests, making the first full-project scan slow. | +| Editor must stay open | Serena requires the Godot editor to be running throughout the session. Closing the editor breaks the TCP connection; restart Serena after reopening the editor. | +| Slower workspace operations | `find_symbol` and `find_references` across the whole workspace are slower than for languages whose LSP servers support native workspace-wide queries. | + +--- +## Reference + +- Godot LSP documentation: [https://docs.godotengine.org/en/stable/tutorials/editor/external_editor.html](https://docs.godotengine.org/en/stable/tutorials/editor/external_editor.html) +- GDScript language reference: [https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/) diff --git a/src/solidlsp/language_servers/godot_language_server.py b/src/solidlsp/language_servers/godot_language_server.py new file mode 100644 index 00000000..c90ce3fd --- /dev/null +++ b/src/solidlsp/language_servers/godot_language_server.py @@ -0,0 +1,127 @@ +"""GDScript language server for Godot Engine projects. + +Connects to an already-running Godot editor via TCP on port 6008. +Both Godot 3 and Godot 4 (tested through 4.6.x) use this port. + +The editor must be open with its built-in language server enabled (default). +""" + +import logging +import os +import pathlib +from collections.abc import Callable + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_process import LanguageServerInterface, TCPConnectionInfo, TCPLanguageServer +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +# config_version in project.godot: 5 → Godot 4, 4 → Godot 3 +_GODOT4_CONFIG_VERSION = 5 + +LSP_PORT = 6008 + + +class GodotLanguageServer(SolidLanguageServer): + """GDScript language server that connects to a running Godot editor. + + Both Godot 3 and Godot 4 expose an LSP server on TCP port 6008. + The Godot editor must already be running — this class connects to it + rather than launching it. + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: + self._godot_version = self._detect_godot_version(repository_root_path) + log.info("Detected Godot version %d for project at %s", self._godot_version, repository_root_path) + self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=LSP_PORT) + + # Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it + super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings) + + @staticmethod + def _detect_godot_version(repo_path: str) -> int: + """Read project.godot to determine the major Godot version. Defaults to 4.""" + project_file = os.path.join(repo_path, "project.godot") + try: + with open(project_file, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line.startswith("config_version="): + version = int(line.split("=", 1)[1]) + return 4 if version >= _GODOT4_CONFIG_VERSION else 3 + except (FileNotFoundError, ValueError, OSError): + pass + return 4 + + def _create_language_server_interface( + self, + process_launch_info: ProcessLaunchInfo, + logging_fn: Callable[[str, str, StringDict | str], None] | None, + ) -> LanguageServerInterface: + return TCPLanguageServer( + connection_info=self._conn_info, + language=self.language, + determine_log_level=self._determine_log_level, + logger=logging_fn, + request_timeout=30.0, + ) + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + root_uri = pathlib.Path(repository_absolute_path).as_uri() + params = { + "processId": os.getpid(), + "rootUri": root_uri, + "rootPath": repository_absolute_path, + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + "declaration": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "completion": { + "dynamicRegistration": True, + "completionItem": {"snippetSupport": True}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "publishDiagnostics": {"relatedInformation": True}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + }, + }, + } + return params # type: ignore + + def _start_server(self) -> None: + def do_nothing(params: dict) -> None: + return + + # Godot sends this notification immediately on connect to report the open project path. + self.server.on_notification("gdscript_client/changeWorkspace", do_nothing) + # Godot-specific capability advertisement (not standard LSP). + self.server.on_notification("gdscript/capabilities", do_nothing) + self.server.on_notification("window/logMessage", lambda msg: log.info("LSP: window/logMessage: %s", msg)) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_request("client/registerCapability", lambda params: None) + + log.info("Connecting to Godot LSP at %s:%d", self._conn_info.host, self._conn_info.port) + self.server.start() + + initialize_params = self._get_initialize_params(self.repository_root_path) + log.info("Sending LSP initialize request to Godot") + self.server.send.initialize(initialize_params) + self.server.notify.initialized({}) + log.info("Godot LSP initialized") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 0817c3a3..2e2c5c3a 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -123,6 +123,12 @@ class Language(str, Enum): by the same server, since SPARK is distinguished by pragmas/aspects in source rather than by file extension. """ + GDSCRIPT = "gdscript" + """GDScript language server for Godot Engine projects (Godot 3 and 4). + Connects to the Godot editor's built-in LSP server over TCP (port 6008). + The editor must already be running with its built-in LSP enabled (default). + Supports .gd and .gdscript files. + """ # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" @@ -472,6 +478,8 @@ class Language(str, Enum): return FilenameMatcher(".bsl", ".os") case self.ADA: return FilenameMatcher(".ads", ".adb", ".ada", case_sensitive=False) + case self.GDSCRIPT: + return FilenameMatcher(".gd", ".gdscript") case self.HTML: return FilenameMatcher(".html", ".htm") case self.SCSS: @@ -727,6 +735,10 @@ class Language(str, Enum): from solidlsp.language_servers.ada_language_server import AdaLanguageServer return AdaLanguageServer + case self.GDSCRIPT: + from solidlsp.language_servers.godot_language_server import GodotLanguageServer + + return GodotLanguageServer case self.HTML: from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer diff --git a/src/solidlsp/ls_process.py b/src/solidlsp/ls_process.py index 8bbf6d02..2bc5849d 100644 --- a/src/solidlsp/ls_process.py +++ b/src/solidlsp/ls_process.py @@ -3,6 +3,7 @@ import json import logging import os import platform +import socket import subprocess import threading import time @@ -620,3 +621,157 @@ class StdioLanguageServer(LanguageServerInterface): # Log the error but don't raise to prevent cascading failures log.error(f"Failed to write to stdin: {e}") return + + +@dataclass +class TCPConnectionInfo: + """Connection parameters for an LSP server reachable over a TCP socket.""" + + host: str = "127.0.0.1" + port: int = 6008 + connection_timeout: float = 30.0 + retry_interval: float = 1.0 + + +class TCPLanguageServer(LanguageServerInterface): + """LanguageServerInterface that connects to an already-running LSP server over TCP. + + Unlike :class:`StdioLanguageServer`, this class does not own the server process — it + connects to an externally-managed server (e.g. the Godot editor) and simply closes + the socket on stop. No LSP ``shutdown``/``exit`` sequence is sent, because the remote + server is expected to keep running after Serena disconnects. + """ + + def __init__( + self, + connection_info: TCPConnectionInfo, + language: Language, + determine_log_level: Callable[[str], int], + logger: Callable[[str, str, StringDict | str], None] | None = None, + request_timeout: float | None = None, + ) -> None: + super().__init__(language, determine_log_level, logger, request_timeout) + self._connection_info = connection_info + self._sock: socket.socket | None = None + self._file: Any = None # socket.makefile("rb") - buffered reader + self._write_lock = threading.Lock() + + def is_running(self) -> bool: + return self._sock is not None + + def _start(self) -> None: + deadline = time.monotonic() + self._connection_info.connection_timeout + last_exc: Exception | None = None + while True: + try: + sock = socket.create_connection((self._connection_info.host, self._connection_info.port), timeout=5.0) + sock.settimeout(None) + self._sock = sock + self._file = sock.makefile("rb") + log.info("TCPLanguageServer connected to %s:%d", self._connection_info.host, self._connection_info.port) + break + except OSError as exc: + last_exc = exc + remaining = deadline - time.monotonic() + if remaining <= 0: + raise RuntimeError( + f"Could not connect to {self._connection_info.host}:{self._connection_info.port} " + f"within {self._connection_info.connection_timeout}s" + ) from last_exc + log.debug( + "TCPLanguageServer: connection failed (%s), retrying in %.1fs (%.0fs left)", + exc, + self._connection_info.retry_interval, + remaining, + ) + time.sleep(min(self._connection_info.retry_interval, remaining)) + + threading.Thread( + target=self._read_loop, + name=f"LSP-tcp-reader:{self.language.value}", + daemon=True, + ).start() + + def _stop(self, timeout: float) -> None: + # Close the socket first — this unblocks any readline() in the reader thread. + # BufferedReader.close() acquires an internal lock that readline() holds while + # blocked waiting for data; closing the socket first causes readline() to return + # with an error, releasing the lock before we call f.close(). + sock = self._sock + self._sock = None + f = self._file + self._file = None + if sock: + try: + sock.shutdown(socket.SHUT_RDWR) + except Exception: + pass + try: + sock.close() + except Exception: + pass + if f: + try: + f.close() + except Exception: + pass + + def _send_payload(self, payload: StringDict) -> None: + sock = self._sock + if sock is None: + return + self._trace("solidlsp", "ls", payload) + data = b"".join(create_message(payload)) + with self._write_lock: + try: + sock.sendall(data) + except OSError as e: + log.error("Failed to write to TCP language server: %s", e) + + def _read_loop(self) -> None: + """Read Content-Length-framed LSP messages from the TCP socket and dispatch them.""" + exception: Exception | None = None + try: + while self._sock is not None: + f = self._file + if f is None: + break + try: + line = f.readline() + except OSError as exc: + if not self._is_stopping: + exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc) + break + if not line: + break + try: + num_bytes = content_length(line) + except ValueError: + continue + if num_bytes is None: + continue + while line and line.strip(): + try: + line = f.readline() + except OSError: + line = b"" + break + if not line: + continue + try: + body = f.read(num_bytes) + except OSError as exc: + if not self._is_stopping: + exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc) + break + if len(body) < num_bytes: + break + self._handle_body(body) + except Exception as exc: + exception = LanguageServerTerminatedException("Unexpected error in TCP language server read loop", self.language, cause=exc) + log.info("TCP language server read loop has terminated") + if not self._is_stopping: + if exception is None: + exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.language) + log.error(str(exception)) + self._cancel_pending_requests(exception) From a1c281dc02a280faee1d7ad1dd72556282e26de7 Mon Sep 17 00:00:00 2001 From: Erik M Jacobs Date: Tue, 12 May 2026 16:18:51 -0400 Subject: [PATCH 2/3] docs: add GDScript to README language list and CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 715a2c6f..a284566b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - No longer store temporary files (e.g. downloads) in `~/solidlsp_tmp`; instead, use OS-specific temporary directories + - Add **GDScript** (Godot Engine) support. Serena connects over TCP to the Godot editor's built-in LSP server (port 6008, same for Godot 3 and 4) — no separate language server process to install. Godot major version is auto-detected from `config_version` in `project.godot`. Note: Godot's LSP does not implement `workspace/symbol`; first workspace-wide scans fall back to per-file requests and can be slow for large projects (results are cached to disk). See the [GDScript Setup Guide](https://oraios.github.io/serena/03-special-guides/godot_gdscript_setup_guide_for_serena.html) for details. Closes #1446. * Dashboard: - UI polish: switch UI font to Inter (with system fallbacks) and use JetBrains Mono only for code/logs/paths/identifiers; refine the light/dark palette with softer borders, clearer text hierarchy, and a more nuanced shadow/elevation system; introduce a consistent spacing scale; keep the orange accent. diff --git a/README.md b/README.md index 9fe71225..35287ba9 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +Ada / SPARK, AL, Angular, Ansible, Bash, BSL, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GDScript, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin From ef22522ee2db668e4add82e645090170b4fbd2f3 Mon Sep 17 00:00:00 2001 From: Erik M Jacobs Date: Wed, 13 May 2026 10:03:46 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(gdscript):=20address=20review=20feedbac?= =?UTF-8?q?k=20=E2=80=94=20configurable=20port/timeout,=20safer=20version?= =?UTF-8?q?=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename LSP_PORT → DEFAULT_GODOT_LS_PORT and add DEFAULT_GODOT_REQUEST_TIMEOUT - Make port and request_timeout overridable via ls_specific_settings["gdscript"] by reading self._custom_settings in _create_language_server_interface - Move self._conn_info setup into _create_language_server_interface so it can consume ls_specific_settings (which requires super().__init__ to have run) - Fix _detect_godot_version to return int | None instead of always defaulting to 4: use an explicit config_version→major dict; return None for unrecognized versions and detection failures; caller logs warning on None - Update setup guide and configuration reference docs with new settings Co-Authored-By: Claude Sonnet 4.6 --- docs/02-usage/050_configuration.md | 21 +++++++++++ .../godot_gdscript_setup_guide_for_serena.md | 18 +++++++++- .../language_servers/godot_language_server.py | 36 +++++++++++++------ 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index ddc095b1..db09f320 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -487,6 +487,27 @@ Supported settings: | `fsautocomplete_version` | `0.83.0` | Override the FsAutoComplete version Serena installs as a .NET tool. | +#### GDScript (Godot Engine) + +Serena connects to the Godot editor's built-in LSP server over TCP. No separate process is launched. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `port` | `6008` | TCP port the running Godot editor listens on for LSP connections. | +| `request_timeout` | `30.0` | Seconds to wait for a response from the Godot LSP server. | + +Example: + +```yaml +ls_specific_settings: + gdscript: + port: 6008 + request_timeout: 60.0 +``` + + #### Go (`gopls`) Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started. diff --git a/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md b/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md index 8a5520c4..dce20bcc 100644 --- a/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md +++ b/docs/03-special-guides/godot_gdscript_setup_guide_for_serena.md @@ -19,9 +19,11 @@ When a Godot project is open in the editor, the editor listens for LSP connectio Serena automatically detects which major version of Godot your project targets by reading the `config_version` field from `project.godot`: -- `config_version` ≥ 5 → Godot 4 +- `config_version` = 5 → Godot 4 - `config_version` = 4 → Godot 3 +If `config_version` is not recognized (e.g. from a future Godot release), Serena logs a warning and still connects — both Godot 3 and Godot 4 use the same port. + No additional configuration is needed for version detection. --- @@ -71,6 +73,20 @@ To mitigate this: | Editor must stay open | Serena requires the Godot editor to be running throughout the session. Closing the editor breaks the TCP connection; restart Serena after reopening the editor. | | Slower workspace operations | `find_symbol` and `find_references` across the whole workspace are slower than for languages whose LSP servers support native workspace-wide queries. | +--- +## Advanced Configuration + +You can customize Serena's GDScript connection via `ls_specific_settings` in your `serena_config.yml` or `project.yml`: + +```yaml +ls_specific_settings: + gdscript: + port: 6008 # TCP port the Godot editor listens on (default: 6008) + request_timeout: 30.0 # seconds to wait for an LSP response (default: 30.0) +``` + +These override the defaults only if explicitly set. + --- ## Reference diff --git a/src/solidlsp/language_servers/godot_language_server.py b/src/solidlsp/language_servers/godot_language_server.py index c90ce3fd..b7ed3996 100644 --- a/src/solidlsp/language_servers/godot_language_server.py +++ b/src/solidlsp/language_servers/godot_language_server.py @@ -20,10 +20,11 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) -# config_version in project.godot: 5 → Godot 4, 4 → Godot 3 -_GODOT4_CONFIG_VERSION = 5 +# Maps config_version in project.godot to the Godot major version +_CONFIG_VERSION_TO_GODOT_MAJOR: dict[int, int] = {4: 3, 5: 4} -LSP_PORT = 6008 +DEFAULT_GODOT_LS_PORT = 6008 +DEFAULT_GODOT_REQUEST_TIMEOUT = 30.0 class GodotLanguageServer(SolidLanguageServer): @@ -32,42 +33,55 @@ class GodotLanguageServer(SolidLanguageServer): Both Godot 3 and Godot 4 expose an LSP server on TCP port 6008. The Godot editor must already be running — this class connects to it rather than launching it. + + ls_specific_settings for ``gdscript``: + - ``port`` (int): TCP port the Godot editor's LSP listens on (default: 6008). + - ``request_timeout`` (float): seconds to wait for an LSP response (default: 30.0). """ def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings) -> None: self._godot_version = self._detect_godot_version(repository_root_path) - log.info("Detected Godot version %d for project at %s", self._godot_version, repository_root_path) - self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=LSP_PORT) + if self._godot_version is not None: + log.info("Detected Godot version %d for project at %s", self._godot_version, repository_root_path) + else: + log.warning("Could not detect Godot version for project at %s", repository_root_path) # Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings) @staticmethod - def _detect_godot_version(repo_path: str) -> int: - """Read project.godot to determine the major Godot version. Defaults to 4.""" + def _detect_godot_version(repo_path: str) -> int | None: + """Read project.godot to determine the major Godot version. + + Returns None if detection fails or the config_version is unrecognized. + """ project_file = os.path.join(repo_path, "project.godot") try: with open(project_file, encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("config_version="): - version = int(line.split("=", 1)[1]) - return 4 if version >= _GODOT4_CONFIG_VERSION else 3 + config_version = int(line.split("=", 1)[1]) + return _CONFIG_VERSION_TO_GODOT_MAJOR.get(config_version) except (FileNotFoundError, ValueError, OSError): pass - return 4 + return None def _create_language_server_interface( self, process_launch_info: ProcessLaunchInfo, logging_fn: Callable[[str, str, StringDict | str], None] | None, ) -> LanguageServerInterface: + settings: dict = self._custom_settings or {} + port = settings.get("port", DEFAULT_GODOT_LS_PORT) + request_timeout = settings.get("request_timeout", DEFAULT_GODOT_REQUEST_TIMEOUT) + self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=port) return TCPLanguageServer( connection_info=self._conn_info, language=self.language, determine_log_level=self._determine_log_level, logger=logging_fn, - request_timeout=30.0, + request_timeout=request_timeout, ) @staticmethod