From cab36b4220a365936ff82ba91803df3ca0261185 Mon Sep 17 00:00:00 2001 From: Arkh74278 <136110739+Arkh74278@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:25:33 +0300 Subject: [PATCH 01/20] Merge pull request #1312 from Arkh74278/fix/dart-hierarchical-document-symbol-support fix: add hierarchicalDocumentSymbolSupport capability for Dart LSP --- CHANGELOG.md | 5 +- .../language_servers/dart_language_server.py | 8 ++- test/solidlsp/dart/test_dart_basic.py | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8471139..ceda84be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* Language Servers: + - Fix Dart LSP returning only symbol name as body instead of full method body + # 1.1.0 * General: @@ -284,4 +287,4 @@ Fixes: # 2025-04-01 -Initial public version +Initial public version \ No newline at end of file diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 11f78106..f82627c6 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -123,7 +123,13 @@ class DartLanguageServer(SolidLanguageServer): """ root_uri = pathlib.Path(repository_absolute_path).as_uri() initialize_params = { - "capabilities": {}, + "capabilities": { + "textDocument": { + "documentSymbol": { + "hierarchicalDocumentSymbolSupport": True, + } + } + }, "initializationOptions": { "onlyAnalyzeProjectsWithOpenFiles": False, "closingLabels": False, diff --git a/test/solidlsp/dart/test_dart_basic.py b/test/solidlsp/dart/test_dart_basic.py index 0a5b69cf..7dab1fe9 100644 --- a/test/solidlsp/dart/test_dart_basic.py +++ b/test/solidlsp/dart/test_dart_basic.py @@ -375,3 +375,52 @@ class TestDartLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.DART], indirect=True) + def test_symbol_body_contains_full_method(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols return the full method body range, not just the identifier. + + Regression test: when hierarchicalDocumentSymbolSupport was not declared in client + capabilities, the Dart LSP returned SymbolInformation[] (flat format) where + location.range only covered the identifier name (single line), causing find_symbol + with include_body=True to return just the symbol name instead of its implementation. + """ + file_path = os.path.join("lib", "main.dart") + symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols + + # Find the 'add' method — defined across multiple lines in main.dart: + # int add(int a, int b) { + # final result = a + b; + # _history.add('$a + $b = $result'); + # return result; + # } + add_symbol = None + for sym in symbol_list: + if sym.get("name") == "add": + add_symbol = sym + break + if sym.get("name") == "Calculator" and "children" in sym: + for child in sym["children"]: + if child.get("name") == "add": + add_symbol = child + break + if add_symbol: + break + + assert add_symbol is not None, "Could not find 'add' method symbol in main.dart" + + # The body range must span multiple lines (not just the identifier line). + # With hierarchicalDocumentSymbolSupport declared, the Dart LSP returns + # DocumentSymbol[] where range covers the full method body. + body_start = add_symbol["location"]["range"]["start"]["line"] + body_end = add_symbol["location"]["range"]["end"]["line"] + assert body_end > body_start, ( + f"Expected multi-line body range for 'add' method, got start={body_start}, end={body_end}. " + f"This likely means hierarchicalDocumentSymbolSupport is not declared in client capabilities." + ) + + # The body text must contain the method implementation, not just the name. + if add_symbol.get("body"): + body_text = add_symbol["body"].get_text() + assert "return result" in body_text, f"Expected method body to contain implementation, got: {body_text!r}" From 9628fdf1b48cb843be2ef1a10184c01d90d369a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:30:10 +0200 Subject: [PATCH 02/20] Minor typing fix --- src/serena/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ab14239f..c479c80d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -413,7 +413,7 @@ class SerenaAgent: is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" if is_ci: return - params = { + params: dict[str, str | int] = { "os": platform.system(), "dashboard": int(self.serena_config.web_dashboard), "version": self.version, From ca387bdc768f14459559102d1e83eaa7633eea71 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:35:56 +0200 Subject: [PATCH 03/20] Docs: added brief explainer on hooks in Common Pitfalls, removed section on tool-loading in Claude Code --- docs/02-usage/030_clients.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md index 1d6cea12..8280d604 100644 --- a/docs/02-usage/030_clients.md +++ b/docs/02-usage/030_clients.md @@ -40,10 +40,15 @@ A key mechanism for this is to use the appropriate [context](#contexts) when sta (clients-common-pitfalls)= ### Common Pitfalls -**Discoverability of the `serena`**. +**Discoverability of the `serena` command**. Your client may not find the `serena` CLI command, even if it is on your system PATH. In this case, a workaround is to provide the full path to the `serena` executable. +**Serena's tools not being used**. +With some clients, you may experience that Serena's tools are not being used. +This is mainly due to problems in the client itself (like a poorly implemented tool discovery). To counteract this, +Serena comes with a set of commands that can be used in _hooks_. See the sections on hooks for Claude Code and VSCode below. + **Environment Variables**. Some language servers may require additional environment variables to be set (e.g. F# on macOS with Homebrew), which you may need to explicitly add to the MCP server configuration. @@ -117,22 +122,6 @@ Confirm that Claude Code is connected to Serena by running the `/mcp` command an If Serena fails to start fast enough, you should set `MCP_TIMEOUT` to a sufficiently high value (e.g. by adding `export MCP_TIMEOUT=60000` to your shell profile) -**Maximum Token Efficiency.** To maximize token efficiency, you may want to use Claude Code's -*on-demand tool loading* feature, which is supported since at least v2.0.74 of Claude Code. -This feature avoids sending all tool descriptions to Claude upon startup, thus saving tokens. -Instead, Claude will search for tools as needed (but there are no guarantees that it will -search optimally, of course). -To enable this feature, set the environment variable `ENABLE_TOOL_SEARCH=true`. -Depending on your shell, you can also set this on a per-session basis, e.g. using -```shell -ENABLE_TOOL_SEARCH=true claude -``` -in bash/zsh, or using -```shell -set ENABLE_TOOL_SEARCH=true && claude -``` -in Windows CMD to launch Claude Code. - **Hooks.** Due to recent changes (especially dynamic tool loading) in Claude Code, the agent will often fail to make proper use of Serena's tools, either by failing to load them in the beginning or by forgetting the instructions in a long session From abfde9507bff691fbc86f83f5889d3e05dedc0a9 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 11:47:52 +0200 Subject: [PATCH 04/20] Removed obsolete and outdated lessons learned and llm-install.md files. --- lessons_learned.md | 90 ---------------------------------------------- llms-install.md | 29 --------------- 2 files changed, 119 deletions(-) delete mode 100644 lessons_learned.md delete mode 100644 llms-install.md diff --git a/lessons_learned.md b/lessons_learned.md deleted file mode 100644 index b6d15a70..00000000 --- a/lessons_learned.md +++ /dev/null @@ -1,90 +0,0 @@ -# Lessons Learned - -In this document we briefly collect what we have learned while developing and using Serena, -what works well and what doesn't. - -## What Worked - -### Separate Tool Logic From MCP Implementation - -MCP is just another protocol, one should let the details of it creep into the application logic. -The official docs suggest using function annotations to define tools and prompts. While that may be -useful for small projects to get going fast, it is not wise for more serious projects. In Serena, -all tools are defined independently and then converted to instances of `MCPTool` using our `make_tool` -function. - -### Autogenerated PromptFactory - -Prompt templates are central for most LLM applications, so one needs good representations of them in the code, -while at the same time they often need to be customizable and exposed to users. In Serena we address these conflicting -needs by defining prompt templates (in jinja format) in separate yamls that users can easily modify and by autogenerated -a `PromptFactory` class with meaningful method and parameter names from these yamls. The latter is committed to our code. -We separated out the generation logic into the [interprompt](/src/interprompt/README.md) subpackage that can be used as a library. - -### Tempfiles and Snapshots for Testing of Editing Tools - -We test most aspects of Serena by having a small "project" for each supported language in `tests/resources`. -For the editing tools, which would change the code in these projects, we use tempfiles to copy over the code. -The pretty awesome [syrupy](https://github.com/syrupy-project/syrupy) pytest plugin helped in developing -snapshot tests. - -### Dashboard and GUI for Logging - -It is very useful to know what the MCP Server is doing. We collect and display logs in a GUI or a web dashboard, -which helps a lot in seeing what's going on and in identifying any issues. - -### Unrestricted Bash Tool - -We know it's not particularly safe to permit unlimited shell commands outside a sandbox, but we did quite some -evaluations and so far... nothing bad has happened. Seems like the current versions of the AI overlords rarely want to execute `sudo rm - rf /`. -Still, we are working on a safer approach as well as better integration with sandboxing. - -### Multilspy - -The [multilspy](https://github.com/microsoft/multilspy/) project helped us a lot in getting started and stands at the core of Serena. -Many more well known python implementations of language servers were subpar in code quality and design (for example, missing types). - -### Developing Serena with Serena - -We clearly notice that the better the tool gets, the easier it is to make it even better - -## Prompting - -### Shouting and Emotive Language May Be Needed - -When developing the `ReplaceRegexTool` we were initially not able to make Claude 4 (in Claude Desktop) use wildcards to save on output tokens. Neither -examples nor explicit instructions helped. It was only after adding - -``` -IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD! -``` - -to the initial instructions and to the tool description that Claude finally started following the instructions. - -## What Didn't Work - -### Lifespan Handling by MCP Clients - -The MCP technology is clearly very green. Even though there is a lifespan context in the MCP SDK, -many clients, including Claude Desktop, fail to properly clean up, leaving zombie processes behind. -We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running -and can terminate it there. - -### Trusting Asyncio - -Running multiple asyncio apps led to non-deterministic -event loop contamination and deadlocks, which were very hard to debug -and understand. We solved this with a large hammer, by putting all asyncio apps into a separate -process. It made the code much more complex and slightly enhanced RAM requirements, but it seems -like that was the only way to reliably overcome asyncio deadlock issues. - -### Cross-OS Tkinter GUI - -Different OS have different limitations when it comes to starting a window or dealing with Tkinter -installations. This was so messy to get right that we pivoted to a web-dashboard instead - -### Editing Based on Line Numbers - -Not only are LLMs notoriously bad in counting, but also the line numbers change after edit operations, -and LLMs are also often too dumb to understand that they should update the line numbers information they had -received before. We pivoted to string-matching and symbol-name based editing. \ No newline at end of file diff --git a/llms-install.md b/llms-install.md deleted file mode 100644 index b02711c7..00000000 --- a/llms-install.md +++ /dev/null @@ -1,29 +0,0 @@ -# MCP Installation instructions - -This document is mainly used as instructions for AI-assistants like Cline and others that -try to do an automatic install based on freeform instructions. - -0. Make sure `uv` is installed. If not, install it using either `curl -LsSf https://astral.sh/uv/install.sh | sh` (macOS, Linux) or - `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"` (Windows). Find the path to the `uv` executable, - you'll need it later. -1. Clone the repo with `git clone git@github.com:oraios/serena.git` and change into its dir (e.g., `cd serena`) -2. Check if `serena_config.yml` exists. If not, create it with `cp serena_config.template.yml serena_config.yml`. Read the instructions in the config. -3. In the config, check if the path to your project was added. If not, add it to the `projects` section -4. In your project, create a `.serena` if needed and check whether `project.yml` exists there. -5. If no `project.yml` was found, create it using `cp /path/to/serena/myproject.template.yml /path/to/your/project/.serena/project.yml` -6. Read the instructions in `project.yml`. Make sure the `project.yml` has the correct project language configured. - Remove the project_root entry there. -7. Finally, add the Serena MCP server config like this: - -```json - { - "mcpServers": { - ... - "serena": { - "command": "/abs/path/to/uv", - "args": ["run", "--directory", "/abs/path/to/serena", "serena-mcp-server", "/path/to/your/project/.serena/project.yml"] - } - } - } - -``` From acb3a0dc1a4b118c1d707c0cee77632f7d644f24 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 12:54:00 +0200 Subject: [PATCH 05/20] Adding discord link (badge) to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6497f03c..000b8c21 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,13 @@ Serena is the IDE for your coding agent. +
+ discord + license +
+
+ + * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). From 5eb7c028c21ed9a0e11c6b2f821adc59ab268458 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 12:57:20 +0200 Subject: [PATCH 06/20] Readme: fix link to quick-start section --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 000b8c21..5f07841b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ * Serena provides essential **semantic code retrieval, editing and refactoring tools** that are akin to an IDE's capabilities, operating at the symbol level and exploiting relational structure. * It integrates with any client/LLM via the model context protocol (**MCP**). - + Serena's **agent-first tool design** involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns. @@ -26,7 +26,7 @@ more complex codebases. > [!IMPORTANT] > Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. -> Instead, follow our [Quick Start](quick-start) instructions. +> Instead, follow our [Quick Start](#quick-start) instructions. ## How Serena Works @@ -45,7 +45,7 @@ To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -See the [Quick Start](quick-start) section below for information on how to get started. +See the [Quick Start](#quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities From 8a6b45bd9f546dabae1b2c7568cffda782b22acc Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 13:09:07 +0200 Subject: [PATCH 07/20] Make news links open in new window [skip ci] --- news/20260111.html | 4 ++-- news/20260303.html | 2 +- news/20260330.html | 2 +- news/20260411.html | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/news/20260111.html b/news/20260111.html index f8c75150..91fa5031 100644 --- a/news/20260111.html +++ b/news/20260111.html @@ -8,7 +8,7 @@
  • Extended symbol information: The `find_symbol` and `find_referencing_symbols` tools (and their JetBrains variants) now can return more information about the symbol (specifically, docstrings and signatures).
  • -
  • Type hierarchy tool: A new tool – exclusive to JetBrains mode – +
  • Type hierarchy tool: A new tool – exclusive to JetBrains mode – that can fetch a symbol's hierarchy, i.e. superclasses and subclasses, which is very useful in many situations.
  • Compact overviews: The `get_symbols_overview` tools now return a much more compact @@ -17,6 +17,6 @@
  • - For more detailed information, see our changelog. + For more detailed information, see our changelog.

    \ No newline at end of file diff --git a/news/20260303.html b/news/20260303.html index ca91377f..c42233d7 100644 --- a/news/20260303.html +++ b/news/20260303.html @@ -7,6 +7,6 @@ into topics and sharing them across projects.

    - For more detailed information, see the documentation. + For more detailed information, see the documentation.

    \ No newline at end of file diff --git a/news/20260330.html b/news/20260330.html index c78c4be7..3eca0574 100644 --- a/news/20260330.html +++ b/news/20260330.html @@ -8,6 +8,6 @@ to other projects.

    - Find out more about this feature in our documentation. + Find out more about this feature in our documentation.

    \ No newline at end of file diff --git a/news/20260411.html b/news/20260411.html index f337c07f..ce07de08 100644 --- a/news/20260411.html +++ b/news/20260411.html @@ -4,19 +4,19 @@

    Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. - See updated installation instructions + See updated installation instructions for more details.

    Improving Tool Usage via Hooks. To address issues with agents not using Serena's tools (agent drift), we now provide the `serena-hooks` commands and a recommended setup for Claude Code and VSCode. - Please read the updated documentation for more details. + Please read the updated documentation for more details. We strongly recommend using the hooks, since recent updates in Claude Code and VSCode resulted in reduced performance, which our hook commands compensate for.

    - Community. We opened a Discord server for our community. + Community. We opened a Discord server for our community.

    \ No newline at end of file From 4a24c1eaa546cc54edf48828103dfba8aafa3ee0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 13:34:39 +0200 Subject: [PATCH 08/20] Enable cert verification for HTTP request to oraios-software.de #1320 [skip ci] --- CHANGELOG.md | 5 ++++- src/serena/agent.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda84be..66cc5bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,17 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Enable cert verification for HTTPS request to oraios-software.de #1320 + * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body # 1.1.0 * General: - - Add `serena init` and `serena setup` commands - **Major**: Add commands for hooks and documentation of recommended setup. Consider setting up the [recommended hooks](https://oraios.github.io/serena/02-usage/030_clients.html) ! + - Add `serena init` and `serena setup` commands - Rework installation instructions, switching to releases on pypi for distribution. Please update your mcp startup commands! - Add minimal usage data collection on startup (only Serena version, language backend, OS, dashboard enabled status; no personally identifiable information) - Fix: git commit id in Serena version strings was incorrect diff --git a/src/serena/agent.py b/src/serena/agent.py index c479c80d..63a3d7b0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -420,7 +420,7 @@ class SerenaAgent: "backend": self._language_backend.value, } try: - requests.get("https://www.oraios-software.de/serena_usage.php", params=params, timeout=1, verify=False) + requests.get("https://oraios-software.de/serena_usage.php", params=params, timeout=1) except Exception as e: log.debug(f"Failed to send usage info: {e}") From f30b3cc1e8b371220521ae90d86dfbd617ba8ab7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 11 Apr 2026 20:35:50 +0200 Subject: [PATCH 09/20] JBRename: add options for renaming in comments and text occurrences, defaults to True --- CHANGELOG.md | 7 +++++-- src/serena/tools/jetbrains_tools.py | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceda84be..3a06de16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* JetBrains: + - `JetBrainsRenameTool` can now also rename occurrences in comments and text. + * Language Servers: - - Fix Dart LSP returning only symbol name as body instead of full method body + - Fix Dart LSP returning only symbol name as body instead of full method body. # 1.1.0 @@ -287,4 +290,4 @@ Fixes: # 2025-04-01 -Initial public version \ No newline at end of file +Initial public version diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index 6db26e0b..a94d5c99 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -512,16 +512,33 @@ class JetBrainsRenameTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional): Renames a symbol, file or directory throughout the codebase using the JetBrains backend. """ - def apply(self, relative_path: str, new_name: str, name_path: str | None = None) -> str: + def apply( + self, + relative_path: str, + new_name: str, + name_path: str | None = None, + rename_in_comments: bool = False, + rename_in_text_occurrences: bool = False, + ) -> str: """ Renames a symbol, file or directory throughout the codebase. + Note: renaming in comments/text is on a best-effort basis by the IDE; if the symbol name is non-unique, further + verification is recommended. :param relative_path: if `name_path` is passed, the relative path of the file containing the symbol. Otherwise, the path to the directory or file to rename. :param new_name: the new name :param name_path: the name path of the symbol to rename or None if renaming a file or directory. + :param rename_in_comments: whether to also rename occurrences in comments. Default True. + :param rename_in_text_occurrences: whether to also rename occurrences in text. Default True. :return: a status message """ code_editor = JetBrainsCodeEditor(self.project) - result = code_editor.rename_symbol(name_path=name_path, relative_path=relative_path, new_name=new_name) + result = code_editor.rename_symbol( + name_path=name_path, + relative_path=relative_path, + new_name=new_name, + rename_in_comments=rename_in_comments, + rename_in_text_occurrences=rename_in_text_occurrences, + ) return self._to_json(result) From 22959f154fb51c6d1f27a5636ec95da4825445ec Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 14:08:25 +0200 Subject: [PATCH 10/20] Internal: run uv lock as part of bump_version --- scripts/bump_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 2fdd213d..e3426baa 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -44,6 +44,7 @@ def bump_version(major: bool, minor: bool, patch: bool, target_version: str | No if dry_run: click.echo(f"Dry run complete. Version would be bumped to {new_version}") else: + os.system("uv lock") click.echo(f"Bumped version to {new_version}") os.system("git add -u") os.system(f'git commit -m "Release v{new_version}"') From f3de97084b7cc13bc3492c6b0bb5154d4a919801 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 12 Apr 2026 14:08:37 +0200 Subject: [PATCH 11/20] Release v1.1.1 --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- src/serena/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd18a1c..110ecf25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +# 1.1.1 + * General: - Enable cert verification for HTTPS request to oraios-software.de #1320 diff --git a/pyproject.toml b/pyproject.toml index e1638adb..c6daf2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "serena-agent" -version = "1.1.0" +version = "1.1.1" description = "" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 00137aad..151d3432 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" import logging diff --git a/uv.lock b/uv.lock index b0f37c6c..17190a91 100644 --- a/uv.lock +++ b/uv.lock @@ -2911,7 +2911,7 @@ wheels = [ [[package]] name = "serena-agent" -version = "1.1.0" +version = "1.1.1" source = { editable = "." } dependencies = [ { name = "anthropic" }, From 4b2c7bd849fabaa9b3ba87a87f6a94522f58b4c1 Mon Sep 17 00:00:00 2001 From: Jochen Seeber Date: Sat, 11 Apr 2026 21:20:23 +0200 Subject: [PATCH 12/20] Add env var to allow disabling usage reporting In the spirit of being nice and privacy-conscious, setting `SERENA_USAGE_REPORTING` to `false` will disable usage reporting. By default, usage reporting is enabled. --- src/serena/agent.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index ab14239f..4174e0ae 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -410,10 +410,9 @@ class SerenaAgent: self._send_usage_info() def _send_usage_info(self) -> None: - is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" - if is_ci: + if os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("SERENA_USAGE_REPORTING") == "false": return - params = { + params: dict[str, str | int] = { "os": platform.system(), "dashboard": int(self.serena_config.web_dashboard), "version": self.version, From 3193637c751259ed36cbaa7138a1352d7a04676c Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 15:01:55 +0200 Subject: [PATCH 13/20] Document environment variable for opting out of usage reporting --- CHANGELOG.md | 3 +++ docs/02-usage/050_configuration.md | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8471139..7c5bf317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* General: + - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) + # 1.1.0 * General: diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index fe2b0d9e..404e8300 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -863,3 +863,11 @@ prompts: ``` It is advisable to use the default prompt as a starting point and modify it to suit your needs. + +### Usage Reporting + +On startup, Serena reports anonymous usage data to help us understand Serena usage. +Specifically, we collect the Serena version, the operating system & language backend being used as well as the dashboard enabled status. +No personally identifiable information or project-specific information is collected. + +If you want to opt out of usage reporting, set the environment variable `SERENA_USAGE_REPORTING` to `false`. From 3d9dbf33a37a8024fcec1042b4209756ff1f3d97 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:11:43 +0200 Subject: [PATCH 14/20] Improve news item to also mention client config update [skip ci] --- news/20260411.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/news/20260411.html b/news/20260411.html index ce07de08..c0da1719 100644 --- a/news/20260411.html +++ b/news/20260411.html @@ -2,10 +2,12 @@

    Serena v1.1: Hooks and New Release Mechanism

    April 11, 2026

    - Installation. Serena has matured and we are switching to a PyPI-based release and installation mechanism. - The recommended uv-tool-based installation decreases startup times and facilitates CLI usage. - See updated installation instructions - for more details. + Installation. Serena has matured, and we are switching to a PyPI-based release and installation mechanism. + The recommended uv tool-based installation decreases startup times and facilitates CLI usage. + See our updated installation instructions + for more details, + and update your client configurations after having switched + to the new installation method.

    Improving Tool Usage via Hooks. To address issues with agents not using Serena's tools (agent drift), we now provide the `serena-hooks` commands and a recommended @@ -14,9 +16,7 @@ We strongly recommend using the hooks, since recent updates in Claude Code and VSCode resulted in reduced performance, which our hook commands compensate for.

    -

    Community. We opened a Discord server for our community.

    - \ No newline at end of file From 1fb4ab1738e395bb51ebfca7a00479045546d9af Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:21:08 +0200 Subject: [PATCH 15/20] Add information on uninstallation --- docs/02-usage/010_installation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/02-usage/010_installation.md b/docs/02-usage/010_installation.md index 8a3dd922..5b0f216b 100644 --- a/docs/02-usage/010_installation.md +++ b/docs/02-usage/010_installation.md @@ -43,3 +43,9 @@ To update Serena to the latest version, run: To keep informed about updates, make sure you regularly open [Serena's Dashboard](060_dashboard), where we will announce releases along with the new features and improvements they bring. ::: + +## Uninstalling Serena + +Serena can be uninstalled with the following command: + + uv tool uninstall serena-agent From 678f195931a7c0d56e4d52e4ac69ff5074656ae0 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 12 Apr 2026 18:21:46 +0200 Subject: [PATCH 16/20] Ignore .claude --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73c25571..dbc94920 100644 --- a/.gitignore +++ b/.gitignore @@ -216,7 +216,7 @@ tmp/ .vscode/ # Claude settings -.claude/settings.local.json +/.claude # Elixir /test/resources/repos/elixir/test_repo/deps From e7b51173bccbdf52a747f3002203bfecd402643e Mon Sep 17 00:00:00 2001 From: TheSmallKiwi Date: Sat, 11 Apr 2026 19:57:25 -0400 Subject: [PATCH 17/20] HLSL: bump to shader-language-server v1.3.1, remove CRLF workaround The upstream fix for the CRLF stale-tree crash (antaalt/shader-sense#44) was merged and released as v1.3.1. This commit: - Removes the client-side CRLF workaround (open_file override and _preload_crlf_content helper) added in #1305, along with the now-unused imports. - Bumps _DEFAULT_VERSION to 1.3.1 and updates SHA-256 checksums for all three pre-built platform archives (win-x64, linux-x64, win-arm64). The regression test (test_include_file_document_symbols_directly) that exercises the original crash scenario passes against v1.3.1 without the workaround. Co-Authored-By: Claude Sonnet 4.6 --- docs/02-usage/050_configuration.md | 2 +- .../language_servers/hlsl_language_server.py | 99 +------------------ 2 files changed, 5 insertions(+), 96 deletions(-) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index fe2b0d9e..060f0249 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -472,7 +472,7 @@ Supported settings: | Setting | Default | Description | |---|---|---| | `ls_path` | managed install or build | Override the `shader-language-server` executable path. | -| `version` | `1.3.0` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | +| `version` | `1.3.1` | Override the bundled version Serena downloads, or builds from source on macOS, when `ls_path` is not set. | #### Haxe diff --git a/src/solidlsp/language_servers/hlsl_language_server.py b/src/solidlsp/language_servers/hlsl_language_server.py index 2857055b..b6eb7e05 100644 --- a/src/solidlsp/language_servers/hlsl_language_server.py +++ b/src/solidlsp/language_servers/hlsl_language_server.py @@ -7,9 +7,6 @@ import logging import os import pathlib import shutil -from collections.abc import Iterator -from contextlib import contextmanager -from pathlib import Path from typing import Any, cast import psutil @@ -18,11 +15,9 @@ from overrides import override from solidlsp.ls import ( LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, - LSPFileBuffer, SolidLanguageServer, ) from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import SolidLSPException from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.settings import SolidLSPSettings @@ -31,13 +26,13 @@ from .common import RuntimeDependency, RuntimeDependencyCollection log = logging.getLogger(__name__) # GitHub release version to download when not installed locally -_DEFAULT_VERSION = "1.3.0" +_DEFAULT_VERSION = "1.3.1" _GITHUB_RELEASE_BASE = "https://github.com/antaalt/shader-sense/releases/download" _HLSL_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com") _HLSL_SHA256_BY_ASSET = { - "shader-language-server-x86_64-pc-windows-msvc.zip": "a945b000c296cdeebb9ee2d4452cec2a0f26544dd076bb08bfdcade2278296a6", - "shader-language-server-x86_64-unknown-linux-gnu.zip": "8c0a7b36f51cc58593762db3592ae13e21ca3cb982b2526cfaaf7c82e92ca089", - "shader-language-server-aarch64-pc-windows-msvc.zip": "cdbd7b41e71cf6040d5cdb7e211ba4b76671a404ee0f7add281d72d3ab8dfa65", + "shader-language-server-x86_64-pc-windows-msvc.zip": "49081c5547ddde1b8b3b17295282a80ddacbca1d6f5dcd834e2788c02bafa997", + "shader-language-server-x86_64-unknown-linux-gnu.zip": "61710df7ca17a2d063b598936c57c56c49fbf837707a1aa886f9b0193a35be3c", + "shader-language-server-aarch64-pc-windows-msvc.zip": "a3b3799affe2cad27652e788376b46fe76e1a6c2ce45946a486dcb26c9091412", } @@ -277,92 +272,6 @@ class HlslLanguageServer(SolidLanguageServer): log.debug(f"Error cleaning up shader-language-server process tree: {e}") super().stop(shutdown_timeout) - @contextmanager - def open_file(self, relative_file_path: str, open_in_ls: bool = True) -> Iterator[LSPFileBuffer]: - """Open a file for LSP, preserving on-disk CRLF line endings. - - Workaround for an upstream bug in shader-language-server - (antaalt/shader-sense) where `watch_main_file` replaces an already-cached - module's content without re-parsing the tree-sitter tree. When a file is - first pulled into the server's cache via an `#include` from another - shader (where it's read via `std::fs::read_to_string`, preserving CRLF), - and then later opened directly via `textDocument/didOpen` with the - client-normalized LF text, the stored tree still references byte offsets - into the longer CRLF content. The next symbol query slices the new - (shorter) content with stale offsets and panics with - `byte index N is out of bounds` in `shader-sense/src/symbols/symbol_parser.rs`. - - The root-cause fix belongs upstream (the server should call - `update_module` instead of assigning `content` raw). Until then, we - ensure the text we send in `didOpen` matches byte-for-byte what the - server reads from disk by preloading the file buffer with a - CRLF-preserving read before the LSP notification is sent. - - This is the only place in Serena that overrides `open_file`; the fix is - deliberately scoped to the HLSL language server. It mirrors the base - class logic in `SolidLanguageServer.open_file` verbatim except for the - buffer construction branch, where creation is deferred (`open_in_ls=False`) - so the buffer's contents can be preloaded before `ensure_open_in_ls` runs. - """ - if not self.server_started: - log.error("open_file called before Language Server started") - raise SolidLSPException("Language Server not started") - - absolute_file_path = Path(self.repository_root_path, relative_file_path) - uri = absolute_file_path.as_uri() - - if uri in self.open_file_buffers: - fb = self.open_file_buffers[uri] - assert fb.uri == uri - assert fb.ref_count >= 1 - - fb.ref_count += 1 - if open_in_ls: - fb.ensure_open_in_ls() - yield fb - fb.ref_count -= 1 - else: - version = 0 - language_id = self._get_language_id_for_file(relative_file_path) - # Defer the didOpen so we can preload CRLF-preserved content first. - fb = LSPFileBuffer( - abs_path=absolute_file_path, - uri=uri, - encoding=self._encoding, - version=version, - language_id=language_id, - ref_count=1, - language_server=self, - open_in_ls=False, - ) - self._preload_crlf_content(fb) - self.open_file_buffers[uri] = fb - if open_in_ls: - fb.ensure_open_in_ls() - yield fb - fb.ref_count -= 1 - - if self.open_file_buffers[uri].ref_count == 0: - self.open_file_buffers[uri].close() - del self.open_file_buffers[uri] - - def _preload_crlf_content(self, fb: LSPFileBuffer) -> None: - """Populate an LSPFileBuffer with a CRLF-preserving read of its backing file. - - Python's default text-mode open applies universal-newlines translation - (CRLF -> LF), which would desync the client's `didOpen` text from the - server-side `std::fs::read_to_string` view that parsed the dependency - tree. Passing `newline=""` disables the translation so bytes match. - """ - with open(fb.abs_path, encoding=fb.encoding, newline="") as f: - raw = f.read() - # Set the buffer's cached state directly: the contents, the mtime - # (required by the contents property's staleness check), and clear the - # hash so it's recomputed against the new bytes. - fb._contents = raw - fb._read_file_modified_date = fb.abs_path.stat().st_mtime - fb._content_hash = None - @override def is_ignored_dirname(self, dirname: str) -> bool: """Ignore Unity-specific directories that contain no user-authored shaders.""" From fc392a4fb85ad746db635565401cd241e33ecd36 Mon Sep 17 00:00:00 2001 From: nameearly <2741313455@qq.com> Date: Mon, 13 Apr 2026 05:36:11 +0800 Subject: [PATCH 18/20] fixing minor scattered problems (#1324) --- src/serena/agent.py | 2 +- src/serena/task_executor.py | 2 +- src/serena/tools/tools_base.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 99f14bd1..322db7c8 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -719,7 +719,7 @@ class SerenaAgent: if self._language_backend == LanguageBackend.LSP: languages_str = ", ".join([lang.value for lang in proj.project_config.languages]) msg += f"\nProgramming languages: {languages_str}." - msg += "File encoding: {proj.project_config.encoding}." + msg += f"File encoding: {proj.project_config.encoding}." include_memories = self._active_tools.contains_tool_class(ReadMemoryTool) if include_memories: diff --git a/src/serena/task_executor.py b/src/serena/task_executor.py index 256fbf02..5e624296 100644 --- a/src/serena/task_executor.py +++ b/src/serena/task_executor.py @@ -80,7 +80,7 @@ class TaskExecutor: :param timeout: the maximum time to wait in seconds; if None, use the task's own timeout (which may be None to wait indefinitely) - :return: True if the task is done, False if the timeout was reached + :return: the result of the task """ return self.future.result(timeout=timeout) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index bb474349..18badd67 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -430,7 +430,6 @@ tool_packages = ["serena.tools"] @singleton class ToolRegistry: _deleted_tools: list[str] = [ - "think_about_collected_information", "think_about_collected_information", "prepare_for_new_conversation", "summarize_changes", From ab67ecab3b64f4fd91f4c92c73281c8babe87b87 Mon Sep 17 00:00:00 2001 From: Pirates IRC <98669745+PiratesIRC@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:47:51 -0500 Subject: [PATCH 19/20] feat: add mSL (mIRC Scripting Language) support (#1315) * feat: add mSL (mIRC Scripting Language) support Add language server support for mIRC Scripting Language (.mrc files). mSL is used in mIRC and AdiIRC IRC clients for scripting bots, games, and automation. The implementation uses a custom Python-based LSP server (pygls) that parses aliases, events, menus, dialogs, and CTCP handlers. Dependencies (pygls, lsprotocol) are installed in an isolated venv on first use. Includes test repo, integration tests, and documentation updates. * style: apply ruff formatting to msl_language_server.py * fix: remove INITIALIZE handler that crashes pygls 2.x pygls 2.x handles the initialize request internally. Overriding it via @server.feature(lsp.INITIALIZE) causes the LSP subprocess to crash. Removing the handler lets pygls auto-advertise capabilities based on registered features (document_symbol, workspace_symbol). * fix: update embedded LSP script for pygls 2.x pygls 2.x moved LanguageServer from pygls.server to pygls.lsp.server. Update import and bump requirement from pygls>=1.3.0 to pygls>=2.0.0. * refactor: ship mSL LSP as package module, not runtime disk write Per maintainer feedback: the mSL LSP is a Python script, so it should ship as a module inside the package rather than being written to disk at runtime. Changes: - Extract embedded LSP script to msl_lsp_server.py (proper module) - Simplify MslLanguageServer: remove venv creation, disk writes, _create_msl_lsp_files(). DependencyProvider now just returns sys.executable and launches the sibling module directly. - Add pygls>=2.0.0 and lsprotocol>=2023.0.0 to pyproject.toml deps * fix: regenerate uv.lock after upstream v1.1.0 merge * feat(msl): add references, definitions, and expand test coverage - Add textDocument/references handler for cross-file alias reference finding - Add textDocument/definition handler for go-to-definition support - Handle $ prefix on cursor position for mSL identifiers - Fix pre-existing mypy error in agent.py (dict[str, object] -> dict[str, str | int]) - Add MSL to _LANGUAGE_PYTEST_MARKERS in conftest.py - Add MSL parametrized cases to test_find_symbol_stable and test_find_symbol_references_stable - Expand test repo with cross-file reference examples (main.mrc <-> utils.mrc) - Add within-file and cross-file reference tests to test_msl_basic.py - Update docs and CHANGELOG to reflect expanded capabilities Co-Authored-By: Claude Opus 4.6 (1M context) * style: apply ruff formatting to msl_lsp_server.py Co-Authored-By: Claude Opus 4.6 (1M context) * fix(msl): add hover handler, fix cross-file references via filesystem scan Root causes of 4 CI test failures: - textDocument/hover was not implemented (test_find_symbol_stable needs it) - references/workspace_symbol/definition only searched opened documents, missing files not explicitly opened by the client - pygls.uris.to_fs_path returns lowercase drive letters on Windows, causing URI mismatch with the framework's repository path comparison Fixes: - Add textDocument/hover handler returning definition snippets in Markdown - Add _get_all_mrc_files() to scan workspace filesystem for all .mrc files - Read workspace roots from server.workspace (pygls 2.x API) instead of broken monkey-patching of server.lsp - Use pathlib.Path.resolve() to normalize drive letter casing on Windows - Simplify __main__ to just server.start_io() All 12 tests pass locally (10 solidlsp + 2 serena_agent integration). Co-Authored-By: Claude Opus 4.6 (1M context) * MSL language: removed unnecessary Dep. provider * Changelog * deps --------- Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Michael Panchenko --- CHANGELOG.md | 4 + README.md | 2 +- docs/01-about/020_programming-languages.md | 3 + pyproject.toml | 3 + .../language_servers/msl_language_server.py | 107 +++++ .../language_servers/msl_lsp_server.py | 439 ++++++++++++++++++ src/solidlsp/ls_config.py | 12 + test/conftest.py | 1 + test/resources/repos/msl/test_repo/main.mrc | 38 ++ test/resources/repos/msl/test_repo/utils.mrc | 26 ++ test/serena/test_serena_agent.py | 3 + test/solidlsp/msl/__init__.py | 0 test/solidlsp/msl/test_msl_basic.py | 161 +++++++ uv.lock | 44 ++ 14 files changed, 842 insertions(+), 1 deletion(-) create mode 100644 src/solidlsp/language_servers/msl_language_server.py create mode 100644 src/solidlsp/language_servers/msl_lsp_server.py create mode 100644 test/resources/repos/msl/test_repo/main.mrc create mode 100644 test/resources/repos/msl/test_repo/utils.mrc create mode 100644 test/solidlsp/msl/__init__.py create mode 100644 test/solidlsp/msl/test_msl_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 12eb66de..35f4a3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support environment variable `SERENA_USAGE_REPORTING` (set to `false` to disable usage reporting) +* Language Servers: + - Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions) + # 1.1.1 * General: @@ -16,6 +19,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - Fix Dart LSP returning only symbol name as body instead of full method body. + # 1.1.0 * General: diff --git a/README.md b/README.md index 5f07841b..f1b513d1 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,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 -AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 33fcd54d..999782ee 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -84,6 +84,9 @@ Some languages require additional installations or setup steps, as noted. * **Luau** * **Markdown** (must explicitly enable language `markdown`, primarily useful for documentation-heavy projects) +* **mSL** (mIRC Scripting Language) + (auto-installed; no external dependencies required — uses a custom pygls-based LSP server shipped with Serena; + supports document symbols, workspace symbols, references, and go-to-definition for aliases, events, menus, dialogs, and CTCP handlers in `.mrc` files) * **Nix** (requires nixd installation) * **OCaml** diff --git a/pyproject.toml b/pyproject.toml index c6daf2c9..8894c98f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ dependencies = [ "beautifulsoup4==4.14.2", "oraios-pywebview==6.2", "pystray==0.19.5", + "pygls==2.1.1", # used to implement an msl language server + "lsprotocol==2025.0.0", # Transitive deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. "urllib3==2.6.3", @@ -344,6 +346,7 @@ markers = [ "lean4: language server running for Lean 4", "solidity: language server running for Solidity (uses @nomicfoundation/solidity-language-server)", "ansible: language server running for Ansible (uses @ansible/ansible-language-server)", + "msl: language server running for mSL (mIRC Scripting Language)", ] [tool.codespell] diff --git a/src/solidlsp/language_servers/msl_language_server.py b/src/solidlsp/language_servers/msl_language_server.py new file mode 100644 index 00000000..e2596a05 --- /dev/null +++ b/src/solidlsp/language_servers/msl_language_server.py @@ -0,0 +1,107 @@ +""" +Provides mSL (mIRC Scripting Language) specific instantiation of the LanguageServer class. +Uses a custom Python-based LSP server (pygls) for parsing .mrc files. + +The LSP server script is shipped as ``msl_lsp_server.py`` alongside this module +and launched as a subprocess using the current Python interpreter. +""" + +import logging +import os +import pathlib +import sys +import threading + +from solidlsp.ls import ( + SolidLanguageServer, +) +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + +_MSL_LSP_SCRIPT = os.path.join(os.path.dirname(__file__), "msl_lsp_server.py") + + +class MslLanguageServer(SolidLanguageServer): + """ + Provides mSL (mIRC Scripting Language) specific instantiation of the LanguageServer class. + Uses a Python-based LSP server for parsing .mrc files (aliases, events, menus, dialogs). + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates an MslLanguageServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + process_launch_info = ProcessLaunchInfo(cmd=[sys.executable, _MSL_LSP_SCRIPT], cwd=repository_root_path) + super().__init__( + config, + repository_root_path, + process_launch_info=process_launch_info, + language_id="msl", + solidlsp_settings=solidlsp_settings, + ) + self.server_ready = threading.Event() + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """Returns the initialize params for the mSL Language Server.""" + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "references": {"dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + }, + "workspace": { + "workspaceFolders": True, + "symbol": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + } + return initialize_params # type: ignore + + def _start_server(self) -> None: + """Starts the mSL Language Server.""" + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + self.server_ready.set() + + def do_nothing(params: dict) -> None: + pass + + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + log.info("Starting mSL server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request to mSL LSP server") + init_response = self.server.send.initialize(initialize_params) + log.debug(f"Received initialize response: {init_response}") + + self.server.notify.initialized({}) + + # Wait briefly for server readiness + if not self.server_ready.wait(timeout=2.0): + log.info("Timeout waiting for mSL server ready signal, proceeding anyway") + self.server_ready.set() + + log.info("mSL server initialization complete") diff --git a/src/solidlsp/language_servers/msl_lsp_server.py b/src/solidlsp/language_servers/msl_lsp_server.py new file mode 100644 index 00000000..e87d3d16 --- /dev/null +++ b/src/solidlsp/language_servers/msl_lsp_server.py @@ -0,0 +1,439 @@ +"""mSL (mIRC Scripting Language) Language Server. + +A minimal LSP implementation for mIRC scripting language (.mrc files). +Provides document symbols, hover, references, go-to-definition, and workspace symbols +for aliases, events, menus, dialogs, and CTCP handlers. + +Launched as a subprocess by MslLanguageServer. Communicates via stdio. +""" + +import logging +import os +import pathlib +import re + +from lsprotocol import types as lsp +from pygls.lsp.server import LanguageServer + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +server = LanguageServer("msl-lsp", "0.1.0") + +# mSL top-level construct patterns +ALIAS_PATTERN = re.compile( + r"^[ \t]*alias\s+(?:-l\s+)?([a-zA-Z_][\w.]*)\s*(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +EVENT_PATTERN = re.compile( + r"^[ \t]*on\s+(\*|\d+):(\w+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +RAW_EVENT_PATTERN = re.compile( + r"^[ \t]*raw\s+(\d+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) +MENU_PATTERN = re.compile( + r"^[ \t]*menu\s+([^\s{]+)\s*\{", + re.MULTILINE | re.IGNORECASE, +) +DIALOG_PATTERN = re.compile( + r"^[ \t]*dialog\s+(-l\s+)?([a-zA-Z_][\w]*)\s*\{", + re.MULTILINE | re.IGNORECASE, +) +CTCP_PATTERN = re.compile( + r"^[ \t]*ctcp\s+(\*|\d+):(\w+):([^{]*?)(?:\{|$)", + re.MULTILINE | re.IGNORECASE, +) + + +def _get_line_col(text: str, pos: int) -> tuple[int, int]: + lines = text[:pos].split("\n") + return len(lines) - 1, len(lines[-1]) if lines else 0 + + +def _find_block_end(text: str, start: int) -> int: + count, i = 0, start + while i < len(text): + ch = text[i] + if ch == ";" and count > 0: + while i < len(text) and text[i] != "\n": + i += 1 + continue + if ch == "{": + count += 1 + elif ch == "}": + count -= 1 + if count == 0: + return i + i += 1 + return len(text) - 1 + + +def _make_symbol( + name: str, + kind: lsp.SymbolKind, + detail: str, + text: str, + match_start: int, + match_end: int, + match_text: str, +) -> lsp.DocumentSymbol: + sl, sc = _get_line_col(text, match_start) + bs = text.find("{", match_start) + if bs != -1: + el, ec = _get_line_col(text, _find_block_end(text, bs)) + else: + el, ec = _get_line_col(text, match_end) + return lsp.DocumentSymbol( + name=name, + kind=kind, + range=lsp.Range(lsp.Position(sl, 0), lsp.Position(el, ec + 1)), + selection_range=lsp.Range(lsp.Position(sl, sc), lsp.Position(sl, sc + len(match_text))), + detail=detail, + ) + + +def parse_symbols(text: str) -> list[lsp.DocumentSymbol]: + """Parse mSL source code and return document symbols.""" + symbols: list[lsp.DocumentSymbol] = [] + + for m in ALIAS_PATTERN.finditer(text): + symbols.append(_make_symbol(m.group(1), lsp.SymbolKind.Function, "alias", text, m.start(), m.end(), m.group(0))) + + for m in EVENT_PATTERN.finditer(text): + pat = m.group(3).strip().rstrip(":") + name = f"on {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, f"event:{m.group(2)}", text, m.start(), m.end(), m.group(0))) + + for m in RAW_EVENT_PATTERN.finditer(text): + pat = m.group(2).strip().rstrip(":") + name = f"raw {m.group(1)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, "raw event", text, m.start(), m.end(), m.group(0))) + + for m in MENU_PATTERN.finditer(text): + symbols.append(_make_symbol(f"menu {m.group(1)}", lsp.SymbolKind.Module, "menu", text, m.start(), m.end(), m.group(0))) + + for m in DIALOG_PATTERN.finditer(text): + symbols.append(_make_symbol(f"dialog {m.group(2)}", lsp.SymbolKind.Class, "dialog", text, m.start(), m.end(), m.group(0))) + + for m in CTCP_PATTERN.finditer(text): + pat = m.group(3).strip().rstrip(":") + name = f"ctcp {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + symbols.append(_make_symbol(name, lsp.SymbolKind.Event, "ctcp event", text, m.start(), m.end(), m.group(0))) + + symbols.sort(key=lambda s: s.range.start.line) + return symbols + + +def _get_workspace_roots() -> list[str]: + """Get workspace root paths from the server's workspace object. + + After initialization, pygls populates ``server.workspace`` with the + root URI/path and any workspace folders sent by the client. + """ + from pygls.uris import to_fs_path + + roots: list[str] = [] + try: + ws = server.workspace + except (RuntimeError, AttributeError): + return roots + + # workspace.folders is a dict of {uri_string: WorkspaceFolder} + if hasattr(ws, "folders") and ws.folders: + for folder_uri in ws.folders: + fs_path = to_fs_path(folder_uri) + if fs_path: + # resolve() normalizes drive letter casing on Windows + fs_path = str(pathlib.Path(fs_path).resolve()) + if os.path.isdir(fs_path): + roots.append(fs_path) + + # Fall back to root_path + if not roots and hasattr(ws, "root_path") and ws.root_path: + root = str(pathlib.Path(ws.root_path).resolve()) + if os.path.isdir(root): + roots.append(root) + + return roots + + +def _path_to_uri(path: str) -> str: + """Convert a filesystem path to a file URI.""" + return pathlib.Path(path).as_uri() + + +def _get_all_mrc_files() -> list[tuple[str, str, str]]: + """Scan workspace roots for all .mrc files. + + Returns list of (uri, file_path, source_text) tuples. + Prefers content from already-opened documents over disk reads. + """ + results: list[tuple[str, str, str]] = [] + seen_paths: set[str] = set() + + # First, include all files currently open in the workspace + try: + for uri, doc in server.workspace.text_documents.items(): + if uri.endswith(".mrc"): + # Resolve the filesystem path from the URI + from pygls.uris import to_fs_path + + file_path = to_fs_path(uri) or uri + norm = os.path.normcase(os.path.normpath(file_path)) + seen_paths.add(norm) + results.append((uri, file_path, doc.source)) + except (RuntimeError, AttributeError): + pass + + # Then scan workspace roots for any .mrc files not yet opened + for root in _get_workspace_roots(): + for dirpath, _dirnames, filenames in os.walk(root): + for fname in filenames: + if not fname.endswith(".mrc"): + continue + full_path = os.path.join(dirpath, fname) + norm = os.path.normcase(os.path.normpath(full_path)) + if norm in seen_paths: + continue + seen_paths.add(norm) + try: + with open(full_path, encoding="utf-8", errors="replace") as f: + source = f.read() + uri = _path_to_uri(full_path) + results.append((uri, full_path, source)) + except OSError: + pass + + return results + + +def _find_symbol_at_position(text: str, line: int, character: int) -> str | None: + """Find the symbol name at the given position in the text.""" + lines = text.split("\n") + if line >= len(lines): + return None + line_text = lines[line] + + # Check if position is within an alias definition + for m in ALIAS_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return m.group(1) + + # Check if position is within an event definition + for m in EVENT_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + pat = m.group(3).strip().rstrip(":") + return f"on {m.group(1)}:{m.group(2)}" + (f":{pat}" if pat else "") + + # Check if position is within a raw event + for m in RAW_EVENT_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + pat = m.group(2).strip().rstrip(":") + return f"raw {m.group(1)}" + (f":{pat}" if pat else "") + + # Check if position is within a menu + for m in MENU_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"menu {m.group(1)}" + + # Check if position is within a dialog + for m in DIALOG_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"dialog {m.group(2)}" + + # Check if position is within a CTCP handler + for m in CTCP_PATTERN.finditer(text): + sl, _ = _get_line_col(text, m.start()) + if sl == line: + return f"ctcp {m.group(1)}:{m.group(2)}" + + # Fall back: extract word at position (for alias call sites) + if character < len(line_text): + pos = character + # Skip leading $ if cursor is on it (e.g., $format.coins) + if pos < len(line_text) and line_text[pos] == "$": + pos += 1 + start = pos + while start > 0 and (line_text[start - 1].isalnum() or line_text[start - 1] in "_."): + start -= 1 + end = pos + while end < len(line_text) and (line_text[end].isalnum() or line_text[end] in "_."): + end += 1 + word = line_text[start:end] + if word: + return word + return None + + +def _get_symbol_detail(text: str, symbol_name: str) -> str | None: + """Get detail/documentation text for a symbol by finding its definition and extracting context.""" + for sym in parse_symbols(text): + if sym.name == symbol_name: + # Extract the definition line and a few lines of body for hover + text_lines = text.split("\n") + start_line = sym.range.start.line + end_line = min(sym.range.end.line, start_line + 5) + snippet_lines = text_lines[start_line : end_line + 1] + snippet = "\n".join(snippet_lines) + return f"```msl\n{snippet}\n```\n\n**Kind**: {sym.detail}" + return None + + +def _build_call_pattern(alias_name: str) -> re.Pattern[str]: + """Build a regex that matches calls to an alias (both as command and as $identifier).""" + escaped = re.escape(alias_name) + return re.compile( + rf"(? None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE) +def did_change(params: lsp.DidChangeTextDocumentParams) -> None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) +def did_close(params: lsp.DidCloseTextDocumentParams) -> None: + pass + + +@server.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) +def document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.DocumentSymbol]: + """Return document symbols for the given document.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + return parse_symbols(doc.source) + except Exception as e: + logger.error(f"Error: {e}") + return [] + + +@server.feature(lsp.TEXT_DOCUMENT_HOVER) +def hover(params: lsp.HoverParams) -> lsp.Hover | None: + """Return hover information for the symbol at the given position.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position(doc.source, params.position.line, params.position.character) + if not symbol_name: + return None + + # Search all files for the symbol definition to get its detail + for _uri, _path, source in _get_all_mrc_files(): + detail = _get_symbol_detail(source, symbol_name) + if detail: + return lsp.Hover( + contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=detail), + ) + + # Fallback: return the symbol name itself + return lsp.Hover( + contents=lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=f"**{symbol_name}**"), + ) + except Exception as e: + logger.error(f"Error in hover: {e}") + return None + + +@server.feature(lsp.TEXT_DOCUMENT_REFERENCES) +def references(params: lsp.ReferenceParams) -> list[lsp.Location]: + """Find all references to the symbol at the given position across the workspace.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position( + doc.source, + params.position.line, + params.position.character, + ) + if not symbol_name: + return [] + + results: list[lsp.Location] = [] + + # For alias symbols, search for call sites across all .mrc files + # For events/menus/dialogs, only return the definition location + is_alias = not any(symbol_name.startswith(prefix) for prefix in ("on ", "raw ", "menu ", "dialog ", "ctcp ")) + + for uri, _path, source in _get_all_mrc_files(): + if is_alias: + call_pattern = _build_call_pattern(symbol_name) + for m in call_pattern.finditer(source): + ref_line, ref_col = _get_line_col(source, m.start()) + results.append( + lsp.Location( + uri=uri, + range=lsp.Range( + lsp.Position(ref_line, ref_col), + lsp.Position(ref_line, ref_col + len(m.group(0))), + ), + ) + ) + else: + # For non-alias symbols, find the definition + for sym in parse_symbols(source): + if sym.name == symbol_name: + results.append(lsp.Location(uri=uri, range=sym.range)) + + return results + except Exception as e: + logger.error(f"Error finding references: {e}") + return [] + + +@server.feature(lsp.TEXT_DOCUMENT_DEFINITION) +def definition(params: lsp.DefinitionParams) -> list[lsp.Location]: + """Go to definition of the symbol at the given position.""" + try: + doc = server.workspace.get_text_document(params.text_document.uri) + symbol_name = _find_symbol_at_position( + doc.source, + params.position.line, + params.position.character, + ) + if not symbol_name: + return [] + + results: list[lsp.Location] = [] + for uri, _path, source in _get_all_mrc_files(): + for sym in parse_symbols(source): + if sym.name == symbol_name: + results.append(lsp.Location(uri=uri, range=sym.selection_range)) + return results + except Exception as e: + logger.error(f"Error finding definition: {e}") + return [] + + +@server.feature(lsp.WORKSPACE_SYMBOL) +def workspace_symbol(params: lsp.WorkspaceSymbolParams) -> list[lsp.SymbolInformation]: + """Search for symbols across the workspace.""" + query = params.query.lower() + results = [] + for uri, _path, source in _get_all_mrc_files(): + for sym in parse_symbols(source): + if query in sym.name.lower(): + results.append( + lsp.SymbolInformation( + name=sym.name, + kind=sym.kind, + location=lsp.Location(uri=uri, range=sym.range), + container_name=sym.detail, + ) + ) + return results + + +if __name__ == "__main__": + server.start_io() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index ab1a4038..ff137894 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -88,6 +88,12 @@ class Language(str, Enum): Requires MATLAB R2021b or later and Node.js. Set MATLAB_PATH environment variable or configure matlab_path in ls_specific_settings. """ + MSL = "msl" + """mIRC Scripting Language (mSL) language server. + Supports .mrc files used in mIRC and AdiIRC IRC clients. + Uses a custom LSP server based on pygls. Automatically sets up + a virtual environment with pygls dependencies on first use. + """ # 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""" @@ -321,6 +327,8 @@ class Language(str, Enum): return FilenameMatcher("*.sol") case self.ANSIBLE: return FilenameMatcher("*.yaml", "*.yml") + case self.MSL: + return FilenameMatcher("*.mrc") case _: raise ValueError(f"Unhandled language: {self}") @@ -544,6 +552,10 @@ class Language(str, Enum): from solidlsp.language_servers.ansible_language_server import AnsibleLanguageServer return AnsibleLanguageServer + case self.MSL: + from solidlsp.language_servers.msl_language_server import MslLanguageServer + + return MslLanguageServer case _: raise ValueError(f"Unhandled language: {self}") diff --git a/test/conftest.py b/test/conftest.py index ce5761c6..4d3f1154 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -259,6 +259,7 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { Language.JAVA: [pytest.mark.java], Language.KOTLIN: [pytest.mark.kotlin, pytest.mark.skipif(is_ci, reason="Kotlin LSP JVM crashes on restart in CI")], Language.LEAN4: [pytest.mark.lean4, pytest.mark.skipif(_sh.which("lean") is None, reason="Lean is not installed")], + Language.MSL: [pytest.mark.msl], Language.PHP: [pytest.mark.php], Language.PHP_PHPACTOR: [pytest.mark.php], Language.POWERSHELL: [pytest.mark.powershell], diff --git a/test/resources/repos/msl/test_repo/main.mrc b/test/resources/repos/msl/test_repo/main.mrc new file mode 100644 index 00000000..04385ee1 --- /dev/null +++ b/test/resources/repos/msl/test_repo/main.mrc @@ -0,0 +1,38 @@ +; Main game module - tests alias, event, and menu parsing + +alias greet { + msg $chan Hello, $1 $+ ! Welcome aboard. +} + +alias -l calculate.doubloons { + var %base = 100 + var %bonus = $rand(1,50) + return $calc(%base + %bonus) +} + +on *:TEXT:!hello*:#: { + greet $nick +} + +on *:JOIN:#: { + .timer 1 3 greet $nick + var %coins = $format.coins(100) + msg $chan You have %coins +} + +raw 319:*: { + ; Handle channel list numeric + echo -a Channels: $3- +} + +menu channel { + Pirates Game + .Start Game:/start + .End Game:/stop +} + +alias show.player.info { + if ($is.admin($nick)) { + msg $chan $nick is an admin with $format.coins(500) + } +} diff --git a/test/resources/repos/msl/test_repo/utils.mrc b/test/resources/repos/msl/test_repo/utils.mrc new file mode 100644 index 00000000..c7a12266 --- /dev/null +++ b/test/resources/repos/msl/test_repo/utils.mrc @@ -0,0 +1,26 @@ +; Utility aliases and handlers + +alias format.coins { + return $bytes($1,b) doubloons +} + +alias is.admin { + if ($1 isop $chan) return $true + return $false +} + +alias welcome.message { + greet $1 + msg $chan You have $format.coins(100) to start with! +} + +dialog settings { + title "Game Settings" + size -1 -1 200 150 + edit "", 1, 10 10 180 20 + button "Save", 2, 60 40 80 25 +} + +ctcp *:VERSION:*: { + ctcpreply $nick VERSION PiratesIRC v1.0 +} diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 01cd29d1..be078eae 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -47,6 +47,7 @@ def serena_config(): Language.CPP_CCLS, Language.HAXE, Language.LEAN4, + Language.MSL, ]: repo_path = get_repo_path(language) if repo_path.exists(): @@ -199,6 +200,7 @@ class TestSerenaAgent: pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp), pytest.param(Language.HAXE, "Main", "Class", "Main.hx", marks=pytest.mark.haxe), pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4), + pytest.param(Language.MSL, "greet", "Function", "main.mrc", marks=pytest.mark.msl), ], indirect=["serena_agent"], ) @@ -307,6 +309,7 @@ class TestSerenaAgent: marks=pytest.mark.haxe, ), pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4), + pytest.param(Language.MSL, "format.coins", "utils.mrc", "main.mrc", marks=pytest.mark.msl), ], indirect=["serena_agent"], ) diff --git a/test/solidlsp/msl/__init__.py b/test/solidlsp/msl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/msl/test_msl_basic.py b/test/solidlsp/msl/test_msl_basic.py new file mode 100644 index 00000000..92a4cd41 --- /dev/null +++ b/test/solidlsp/msl/test_msl_basic.py @@ -0,0 +1,161 @@ +""" +Basic integration tests for the mSL (mIRC Scripting Language) language server. + +Tests validate document symbols, references, and definitions for aliases, events, +raw events, menus, dialogs, and CTCP handlers using the mSL test repository. +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + +pytestmark = [pytest.mark.msl] + + +class TestMslDocumentSymbols: + """Test document symbol retrieval for mSL constructs.""" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer) -> None: + """Test that the language server starts successfully.""" + assert language_server.is_running() + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_main(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols are returned for the main file.""" + doc_symbols = language_server.request_document_symbols("main.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "greet" in symbol_names, f"greet alias not found. Found: {symbol_names}" + assert "calculate.doubloons" in symbol_names, f"calculate.doubloons alias not found. Found: {symbol_names}" + assert "show.player.info" in symbol_names, f"show.player.info alias not found. Found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_events(self, language_server: SolidLanguageServer) -> None: + """Test that event handlers, raw events, and menus are detected in the main file.""" + doc_symbols = language_server.request_document_symbols("main.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + # Check for on *:TEXT and on *:JOIN events + on_events = [n for n in symbol_names if n.startswith("on ")] + assert len(on_events) >= 2, f"Expected at least 2 event handlers. Found: {on_events}" + # Check for raw event + raw_events = [n for n in symbol_names if n.startswith("raw ")] + assert len(raw_events) >= 1, f"Expected at least 1 raw event handler. Found: {raw_events}" + # Check for menu + menus = [n for n in symbol_names if n.startswith("menu ")] + assert len(menus) >= 1, f"Expected at least 1 menu. Found: {menus}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_utils(self, language_server: SolidLanguageServer) -> None: + """Test that document symbols are returned for the utils file.""" + doc_symbols = language_server.request_document_symbols("utils.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "format.coins" in symbol_names, f"format.coins alias not found. Found: {symbol_names}" + assert "is.admin" in symbol_names, f"is.admin alias not found. Found: {symbol_names}" + assert "welcome.message" in symbol_names, f"welcome.message alias not found. Found: {symbol_names}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_document_symbols_dialog_and_ctcp(self, language_server: SolidLanguageServer) -> None: + """Test that dialog and CTCP handler definitions are detected.""" + doc_symbols = language_server.request_document_symbols("utils.mrc") + all_symbols, root_symbols = doc_symbols.get_all_symbols_and_roots() + + symbol_names = [s.get("name") for s in all_symbols if s.get("name")] + assert "dialog settings" in symbol_names, f"dialog settings not found. Found: {symbol_names}" + # Check for CTCP handler + ctcp_events = [n for n in symbol_names if n.startswith("ctcp ")] + assert len(ctcp_events) >= 1, f"Expected at least 1 ctcp handler. Found: {ctcp_events}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + """Test that the full symbol tree contains expected symbols from both files.""" + from solidlsp.ls_utils import SymbolUtils + + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "format.coins"), "format.coins not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "show.player.info"), "show.player.info not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + """Test that symbol names do not contain unexpected formatting characters.""" + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # mSL symbols can contain periods (e.g., calculate.doubloons) and + # colons/spaces in event names (e.g., "on *:TEXT"), so allow those + if has_malformed_name(s, period_allowed=True, colon_allowed=True, whitespace_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None: + """Test that references to 'greet' are found within main.mrc.""" + file_path = "main.mrc" + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + greet_symbol = next((s for s in all_symbols if s.get("name") == "greet"), None) + assert greet_symbol is not None, "Could not find 'greet' symbol in main.mrc" + + sel_start = greet_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for greet but got {refs=}" + + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # greet is called on line 13 (0-indexed) in main.mrc: `greet $nick` + call_site = {"uri_suffix": "main.mrc", "line": 13} + assert call_site in actual_locations, f"Expected reference to greet at line 13 in main.mrc, got {actual_locations}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None: + """Test that references to 'format.coins' are found across main.mrc and utils.mrc.""" + # format.coins is defined in utils.mrc but called in both main.mrc and utils.mrc + file_path = "utils.mrc" + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + fc_symbol = next((s for s in all_symbols if s.get("name") == "format.coins"), None) + assert fc_symbol is not None, "Could not find 'format.coins' symbol in utils.mrc" + + sel_start = fc_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + + assert refs, f"Expected non-empty references for format.coins but got {refs=}" + + actual_locations = [ + { + "uri_suffix": os.path.basename(ref.get("relativePath", ref.get("uri", ""))), + "line": ref["range"]["start"]["line"], + } + for ref in refs + ] + + # Verify cross-file: at least one reference is in main.mrc (different file from definition) + main_refs = [loc for loc in actual_locations if loc["uri_suffix"] == "main.mrc"] + assert len(main_refs) >= 1, f"Expected at least 1 reference in main.mrc, got {main_refs}" + + @pytest.mark.parametrize("language_server", [Language.MSL], indirect=True) + def test_workspace_symbol(self, language_server: SolidLanguageServer) -> None: + """Test that workspace symbol search returns results.""" + result = language_server.request_workspace_symbol("greet") + assert result is not None, "Workspace symbol search returned None" + assert len(result) > 0, "Workspace symbol search returned no results" + assert any("greet" in str(s.get("name", "")) for s in result), f"Expected at least one result containing 'greet', got {result}" diff --git a/uv.lock b/uv.lock index 17190a91..c89fecb4 100644 --- a/uv.lock +++ b/uv.lock @@ -243,6 +243,19 @@ filecache = [ { name = "filelock" }, ] +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -1291,6 +1304,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] +[[package]] +name = "lsprotocol" +version = "2025.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -2143,6 +2169,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, ] +[[package]] +name = "pygls" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/2e/7bbe061d175c0baddde8fc9edb908a4c31ba5d9165b8c68e3439c3a9f138/pygls-2.1.1.tar.gz", hash = "sha256:1da03ba9053201bb337dcdd8d121df70feb2a91e1a0dcc74de5da79755b1a201", size = 55091, upload-time = "2026-03-25T11:19:10.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/1a/208293b6c350f5abea6941d5606080d4a492644052504f5312e5de30a902/pygls-2.1.1-py3-none-any.whl", hash = "sha256:510a6dea2476177230c7d851125e5948efdf3fdb9ebfd8543fc434972f8faed4", size = 68975, upload-time = "2026-03-25T11:19:11.374Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -2924,12 +2964,14 @@ dependencies = [ { name = "fortls" }, { name = "jinja2" }, { name = "joblib" }, + { name = "lsprotocol" }, { name = "mcp" }, { name = "oraios-pywebview" }, { name = "overrides" }, { name = "pathspec" }, { name = "psutil" }, { name = "pydantic" }, + { name = "pygls" }, { name = "pyright" }, { name = "pystray" }, { name = "python-dotenv" }, @@ -2998,6 +3040,7 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'dev'", specifier = "==3.1.6" }, { name = "joblib", specifier = "==1.5.1" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = "==1.0.4.post1" }, + { name = "lsprotocol", specifier = "==2025.0.0" }, { name = "mcp", specifier = "==1.26.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, { name = "nbconvert", marker = "extra == 'dev'", specifier = "==7.17.0" }, @@ -3009,6 +3052,7 @@ requires-dist = [ { name = "psutil", specifier = "==7.0.0" }, { name = "pyasn1", marker = "extra == 'dev'", specifier = "==0.6.3" }, { name = "pydantic", specifier = "==2.12.5" }, + { name = "pygls", specifier = "==2.1.1" }, { name = "pyinstrument", marker = "extra == 'dev'", specifier = "==5.1.1" }, { name = "pyjwt", marker = "extra == 'dev'", specifier = "==2.12.0" }, { name = "pyright", specifier = "==1.1.403" }, From 1789967968412bd6340532b50a641a564f5a2f18 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 13 Apr 2026 09:17:24 +0200 Subject: [PATCH 20/20] Pyproject: added description and classifiers [ci skip] Description in particular is important since it is shown in pypi --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8894c98f..7ab83e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,16 @@ requires = ["hatchling"] [project] name = "serena-agent" version = "1.1.1" -description = "" +description = "A powerful MCP toolkit for coding, providing semantic retrieval and editing capabilities - the IDE for your agent" authors = [{ name = "Oraios AI", email = "info@oraios-ai.de" }] readme = "README.md" requires-python = ">=3.11, <3.15" classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.13", ] dependencies = [ "requests==2.33.0",