mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-16 06:14:41 +00:00
Merge branch 'main' into evaluation
This commit is contained in:
commit
685715dd02
33 files changed
+993
-264
No files matched your search
+1
-1
@@ -216,7 +216,7 @@ tmp/
|
||||
.vscode/
|
||||
|
||||
# Claude settings
|
||||
.claude/settings.local.json
|
||||
/.claude
|
||||
|
||||
# Elixir
|
||||
/test/resources/repos/elixir/test_repo/deps
|
||||
|
||||
+19
-1
@@ -2,11 +2,29 @@
|
||||
|
||||
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)
|
||||
|
||||
* Language Servers:
|
||||
- Add mSL (mIRC Scripting Language) support (custom pygls-based language server; symbols, references, definitions)
|
||||
|
||||
# 1.1.1
|
||||
|
||||
* General:
|
||||
- Enable cert verification for HTTPS request to oraios-software.de #1320
|
||||
|
||||
* 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.
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
@@ -7,10 +7,17 @@
|
||||
Serena is the IDE for your coding agent.
|
||||
</h3>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://discord.com/invite/cVUNQmnV4r"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat-square&labelColor=0a0e14&logo=discord&logoColor=5865F2" alt="discord"></a>
|
||||
<a href="https://github.com/oraios/serena/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-b0e8ff?style=flat-square&labelColor=0a0e14" alt="license"></a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
|
||||
* 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.
|
||||
|
||||
@@ -19,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.
|
||||
|
||||
## What Our "End Users" Say
|
||||
|
||||
@@ -58,7 +65,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
|
||||
|
||||
@@ -84,7 +91,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
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
+2
-2
@@ -8,7 +8,7 @@
|
||||
<li><strong>Extended symbol information:</strong> The `find_symbol` and `find_referencing_symbols` tools (and their JetBrains variants)
|
||||
now can return more information about the symbol (specifically, docstrings and signatures).
|
||||
</li>
|
||||
<li><strong>Type hierarchy tool:</strong> A new tool – exclusive to <a href="https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html">JetBrains mode</a> –
|
||||
<li><strong>Type hierarchy tool:</strong> A new tool – exclusive to <a target="_blank" href="https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html">JetBrains mode</a> –
|
||||
that can fetch a symbol's hierarchy, i.e. superclasses and subclasses, which is very useful in many situations.
|
||||
</li>
|
||||
<li><strong>Compact overviews:</strong> The `get_symbols_overview` tools now return a much more compact
|
||||
@@ -17,6 +17,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
For more detailed information, see our <a href="https://github.com/oraios/serena/blob/main/CHANGELOG.md">changelog</a>.
|
||||
For more detailed information, see our <a target="_blank" href="https://github.com/oraios/serena/blob/main/CHANGELOG.md">changelog</a>.
|
||||
</p>
|
||||
</div>
|
||||
+1
-1
@@ -7,6 +7,6 @@
|
||||
into topics and sharing them across projects.
|
||||
</p>
|
||||
<p>
|
||||
For more detailed information, see the <a href="https://oraios.github.io/serena/02-usage/045_memories.html">documentation</a>.
|
||||
For more detailed information, see the <a target="_blank" href="https://oraios.github.io/serena/02-usage/045_memories.html">documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
+1
-1
@@ -8,6 +8,6 @@
|
||||
to other projects.
|
||||
</p>
|
||||
<p>
|
||||
Find out more about this feature in our <a href="https://oraios.github.io/serena/02-usage/040_workflow.html#reading-from-external-projects">documentation</a>.
|
||||
Find out more about this feature in our <a target="_blank" href="https://oraios.github.io/serena/02-usage/040_workflow.html#reading-from-external-projects">documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
+8
-8
@@ -2,21 +2,21 @@
|
||||
<h3>Serena v1.1: Hooks and New Release Mechanism</h3>
|
||||
<p class="date">April 11, 2026</p>
|
||||
<p>
|
||||
<b>Installation.</b> 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 <a href="https://oraios.github.io/serena/02-usage/010_installation.html">updated installation instructions</a>
|
||||
for more details.
|
||||
<b>Installation.</b> 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 <a target="_blank" href="https://oraios.github.io/serena/02-usage/010_installation.html">updated installation instructions</a>
|
||||
for more details,
|
||||
and update your <a target="_blank" href="https://oraios.github.io/serena/02-usage/030_clients.html">client configurations</a> after having switched
|
||||
to the new installation method.
|
||||
</p>
|
||||
<p>
|
||||
<b>Improving Tool Usage via Hooks.</b> 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 <a href="https://oraios.github.io/serena/02-usage/030_clients.html">updated documentation</a> for more details.
|
||||
Please read the <a target="_blank" href="https://oraios.github.io/serena/02-usage/030_clients.html">updated documentation</a> for more details.
|
||||
We <i>strongly recommend using the hooks</i>, since recent updates in Claude Code and VSCode resulted in
|
||||
reduced performance, which our hook commands compensate for.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>Community.</b> We opened a <a href="https://discord.com/invite/cVUNQmnV4r">Discord server</a> for our community.
|
||||
<b>Community.</b> We opened a <a target="_blank" href="https://discord.com/invite/cVUNQmnV4r">Discord server</a> for our community.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
+8
-2
@@ -4,14 +4,17 @@ requires = ["hatchling"]
|
||||
|
||||
[project]
|
||||
name = "serena-agent"
|
||||
version = "1.1.0"
|
||||
description = ""
|
||||
version = "1.1.1"
|
||||
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",
|
||||
@@ -38,6 +41,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 +349,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]
|
||||
|
||||
@@ -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}"')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.1.1"
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
+4
-5
@@ -410,17 +410,16 @@ 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,
|
||||
"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}")
|
||||
|
||||
@@ -720,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
@@ -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"(?<![.\w])(?:\$)?{escaped}(?![.\w])",
|
||||
re.MULTILINE | re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
|
||||
def did_open(params: lsp.DidOpenTextDocumentParams) -> 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()
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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}"
|
||||
Whitespace-only changes.
@@ -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}"
|
||||
@@ -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"
|
||||
@@ -2911,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "serena-agent"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user