diff --git a/README.md b/README.md index 2fadd208..feba7f7b 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,120 @@ -

+

-* :rocket: Serena is a powerful **coding agent toolkit** capable of turning an LLM into a fully-featured agent that works **directly on your codebase**. - Unlike most other tools, it is not tied to an LLM, framework or an interface, making it easy to use it in a variety of ways. -* :wrench: Serena provides essential **semantic code retrieval and editing tools** that are akin to an IDE's capabilities, extracting code entities at the symbol level and exploiting relational structure. When combined with an existing coding agent, these tools greatly enhance (token) efficiency. -* :free: Serena is **free & open-source**, enhancing the capabilities of LLMs you already have access to free of charge. +

+ Serena is the IDE for your coding agent. +

-You can think of Serena as providing IDE-like tools to your LLM/coding agent. -With it, the agent no longer needs to read entire files, perform grep-like searches or basic string replacements to find the right parts of the code and to edit code. -Instead, it can use code-centric tools like `find_symbol`, `find_referencing_symbols` and `insert_after_symbol`. +* 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 is under active development! See the latest updates, upcoming features, and lessons learned to stay up to date. -

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

- Changelog - Lessons Learned -

+Practically, this means that your agent operates **faster, more efficiently and more reliably**, especially in larger and +more complex codebases. -> [!TIP] -> The [**Serena JetBrains plugin**](#the-serena-jetbrains-plugin) has been released! +## Features -## LLM Integration +Serena supports two code intelligence backends: -Serena provides the necessary [tools](https://oraios.github.io/serena/01-about/035_tools.html) for coding workflows, but an LLM is required to do the actual work, -orchestrating tool use. +* **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative. +* **The Serena JetBrains Plugin**, which leverages the capabilities of your JetBrains IDE. -In general, Serena can be integrated with an LLM in several ways: +Language Servers, while very powerful, have inherent limitations. +The JetBrains variant is the more advanced solution, as detailed below. It is also more memory-efficient, especially in +multi-agent scenarios. -* by using the **model context protocol (MCP)**. - Serena provides an MCP server which integrates with - * Claude Code and Claude Desktop, - * terminal-based clients like Codex, Gemini-CLI, Qwen3-Coder, rovodev, OpenHands CLI and others, - * IDEs like VSCode, Cursor or IntelliJ, - * Extensions like Cline or Roo Code - * Local clients like [OpenWebUI](https://docs.openwebui.com/openapi-servers/mcp), [Jan](https://jan.ai/docs/mcp-examples/browser/browserbase#enable-mcp), [Agno](https://docs.agno.com/introduction/playground) and others -* by using [mcpo to connect it to ChatGPT](docs/03-special-guides/serena_on_chatgpt.md) or other clients that don't support MCP but do support tool calling via OpenAPI. -* by incorporating Serena's tools into an agent framework of your choice, as illustrated [here](docs/03-special-guides/custom_agent.md). - Serena's tool implementation is decoupled from the framework-specific code and can thus easily be adapted to any agent framework. +### Language Support + +#### Language Servers + +When using Serena's language server backend, we provide **support for over 40 programming languages**, including +AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, 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. + +#### The Serena JetBrains Plugin + +The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) +leverages the powerful code analysis capabilities of your JetBrains IDE. +The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, +including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others (Rider and CLion are unsupported though). + + + +See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions on how to apply the plugin. + +### Retrieval + +Serena's retrieval tools allow agents to explore codebases at the symbol level, understanding structure and relationships +without reading entire files. + +| Capability | Language Servers | JetBrains Plugin | +|----------------------------------|------------------|------------------| +| find symbol | yes | yes | +| symbol overview (file outline) | yes | yes | +| find referencing symbols | yes | yes | +| search in project dependencies | -- | yes | +| type hierarchy | -- | yes | +| find declaration | -- | yes | +| find implementations | -- | yes | +| query external projects | yes | yes | + +### Refactoring + +Without precise refactoring tools, agents are forced to resort to unreliable and expensive search and replace operations. + +| Capability | Language Servers | JetBrains Plugin | +|-------------------------------------------|--------------------|-----------------------------------| +| rename | yes (only symbols) | yes (symbols, files, directories) | +| move (symbol, file, directory) | -- | yes | +| inline | -- | yes | +| propagate deletions (remove unused code) | -- | yes | + +### Symbolic Editing + +Serena's symbolic editing tools are less error-prone and much more token-efficient than typical alternatives. + +| Capability | Language Servers | JetBrains Plugin | +|------------------------|-------------------|------------------| +| replace symbol body | yes | yes | +| insert after symbol | yes | yes | +| insert before symbol | yes | yes | +| safe delete | yes | yes | + +### Basic Features + +Beyond its semantic capabilities, Serena includes a set of basic utilities for completeness. +When Serena is used inside an agentic harness such as Claude Code or Codex, these tools are typically disabled by default, +since the surrounding harness already provides overlapping file, search, and shell capabilities. + +- **`search_for_pattern`** – flexible regex search across the codebase +- **`replace_content`** – agent-optimised regex-based and literal text replacement +- **`list_dir` / `find_file`** – directory listing and file search +- **`read_file`** – read files or file chunks +- **`execute_shell_command`** – run shell commands (e.g. builds, tests, linters) + +### Memory Management + +A memory system is elemental to long-lived agent workflows, especially when knowledge is to be shared across +sessions, users and projects. +Despite its simplicity, we received positive feedback from many users who tend to combine Serena's memory management system with their +agent's internal system (e.g., `AGENTS.md` files). +It can easily be disabled if you prefer to use something else. + +### Configurability + +Active tools, tool descriptions, prompts, language backend details and many other aspects of Serena +can be flexibly configured on a per-case basis by simply adjusting a few lines of YAML. +To achieve this, Serena offers multiple levels of (composable) configuration: + +* global configuration +* MCP launch command (CLI) configuration +* per-project configuration (with local overrides) +* execution context-specific configuration (e.g. for particular clients) +* dynamically composable configuration fragments (modes) ## Serena in Action @@ -57,51 +131,13 @@ Note how Serena's tools enable Claude to find and edit the right symbols. https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 -## Programming Language Support & Semantic Analysis Capabilities - -Serena provides a set of versatile code querying and editing functionalities -based on symbolic understanding of the code. -Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer -making use of an IDE's capabilities would. -Serena can efficiently find the right context and do the right thing even in very large and -complex projects! - -There are two alternative technologies powering these capabilities: - -* **Language servers** implementing the language server Protocol (LSP) — the free/open-source alternative. -* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing - capabilities of your JetBrains IDE. - -You can choose either of these backends depending on your preferences and requirements. - -### Language Servers - -Serena incorporates a powerful abstraction layer for the integration of language servers -that implement the language server protocol (LSP). -The underlying language servers are typically open-source projects (like Serena) or at least freely available for use. - -With Serena's LSP library, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, F# (currently with some bugs), GLSL, Go, Groovy (partial support), Haskell, 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. - -> [!IMPORTANT] -> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. - -### The Serena JetBrains Plugin - -As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) -leverages the powerful code analysis capabilities of your JetBrains IDE. -The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, -including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others (Rider and CLion are unsupported though). - - - -The plugin offers the most robust and most powerful Serena experience. -See our [documentation page](https://oraios.github.io/serena/02-usage/025_jetbrains_plugin.html) for further details and instructions. - ## Quick Start **Prerequisites**. Serena is managed by *uv*. If you don’t already have it, you need to [install uv](https://docs.astral.sh/uv/getting-started/installation/) before proceeding. +> [!NOTE] +> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details. + **Starting the MCP Server**. The easiest way to start the Serena MCP server is by running the latest version from GitHub using uvx. Issue this command to see available options: @@ -124,74 +160,9 @@ Follow the link for specific instructions on how to set up Serena for Claude Cod Please refer to the [user guide](https://oraios.github.io/serena/02-usage/000_intro.html) for detailed instructions on how to use Serena effectively. -## Community Feedback - -Most users report that Serena has strong positive effects on the results of their coding agents, even when used within -very capable agents like Claude Code. Serena is often described to be a [game changer](https://www.reddit.com/r/ClaudeAI/comments/1lfsdll/try_out_serena_mcp_thank_me_later/), providing an enormous [productivity boost](https://www.reddit.com/r/ClaudeCode/comments/1mguoia/absolutely_insane_improvement_of_claude_code). - -Serena excels at navigating and manipulating complex codebases, providing tools that support precise code retrieval and editing in the presence of large, strongly structured codebases. -However, when dealing with tasks that involve only very few/small files, you may not benefit from including Serena on top of your existing coding agent. -In particular, when writing code from scratch, Serena will not provide much value initially, as the more complex structures that Serena handles more gracefully than simplistic, file-based approaches are yet to be created. - -Several videos and blog posts have talked about Serena: - -* YouTube: - * [AI Labs](https://www.youtube.com/watch?v=wYWyJNs1HVk&t=1s) - * [Yo Van Eyck](https://www.youtube.com/watch?v=UqfxuQKuMo8&t=45s) - * [JeredBlu](https://www.youtube.com/watch?v=fzPnM3ySmjE&t=32s) - -* Blog posts: - * [Serena's Design Principles](https://medium.com/@souradip1000/deconstructing-serenas-mcp-powered-semantic-code-understanding-architecture-75802515d116) - * [Serena with Claude Code (in Japanese)](https://blog.lai.so/serena/) - * [Turning Claude Code into a Development Powerhouse](https://robertmarshall.dev/blog/turning-claude-code-into-a-development-powerhouse/) - ## Acknowledgements -### Sponsors - -We are very grateful to our [sponsors](https://github.com/sponsors/oraios) who help us drive Serena's development. The core team -(the founders of [Oraios AI](https://oraios-ai.de/)) put in a lot of work in order to turn Serena into a useful open source project. -So far, there is no business model behind this project, and sponsors are our only source of income from it. - -Sponsors help us dedicating more time to the project, managing contributions, and working on larger features (like better tooling based on more advanced -LSP features, VSCode integration, debugging via the DAP, and several others). -If you find this project useful to your work, or would like to accelerate the development of Serena, consider becoming a sponsor. - -We are proud to announce that the Visual Studio Code team, together with Microsoft’s Open Source Programs Office and GitHub Open Source -have decided to sponsor Serena with a one-time contribution! - -

- Visual Studio Code sponsor logo -

- -### Community Contributions - A significant part of Serena, especially support for various languages, was contributed by the open source community. We are very grateful for the many contributors who made this possible and who played an important role in making Serena what it is today. -### Technologies -We built Serena on top of multiple existing open-source technologies, the most important ones being: - -1. [multilspy](https://github.com/microsoft/multilspy). - A library which wraps language server implementations and adapts them for interaction via Python. - It provided the basis for our library Solid-LSP (`src/solidlsp`). - Solid-LSP provides pure synchronous LSP calls and extends the original library with the symbolic logic - that Serena required. -2. [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) -3. All the language servers that we use through Solid-LSP. - -Without these projects, Serena would not have been possible (or would have been significantly more difficult to build). - -## Customizing and Extending Serena - -It is straightforward to extend Serena's AI functionality with your own ideas. -Simply implement a new tool by subclassing -`serena.agent.Tool` and implement the `apply` method with a signature -that matches the tool's requirements. -Once implemented, `SerenaAgent` will automatically have access to the new tool. - -It is also relatively straightforward to add [support for a new programming language](/.serena/memories/adding_new_language_support_guide.md). - -We look forward to seeing what the community will come up with! -For details on contributing, see [contributing guidelines](/CONTRIBUTING.md). diff --git a/docs/01-about/.gitignore b/docs/01-about/.gitignore index e1fbb867..463af074 100644 --- a/docs/01-about/.gitignore +++ b/docs/01-about/.gitignore @@ -1 +1,4 @@ -/035_tools.md \ No newline at end of file +# auto-generated files +/000_intro.md +/005_features.md +/035_tools.md diff --git a/docs/01-about/000_intro.md b/docs/01-about/000_intro.md deleted file mode 100644 index 3fbd8b64..00000000 --- a/docs/01-about/000_intro.md +++ /dev/null @@ -1,14 +0,0 @@ -# About Serena - -* Serena is a powerful **coding agent toolkit** capable of turning an LLM into a fully-featured agent that works - **directly on your codebase**. - Unlike most other tools, it is not tied to an LLM, framework or an interface, making it easy to use it in a variety of ways. -* Serena provides essential **semantic code retrieval and editing tools** that are akin to an IDE's capabilities, - extracting code entities at the symbol level and exploiting relational structure. - When combined with an existing coding agent, these tools greatly enhance (token) efficiency. -* Serena is **free & open-source**, enhancing the capabilities of LLMs you already have access to free of charge. - -Therefore, you can think of Serena as providing IDE-like tools to your LLM/coding agent. -With it, the agent no longer needs to read entire files, perform grep-like searches or string replacements to find and -edit the right code. -Instead, it can use code-centred tools like `find_symbol`, `find_referencing_symbols` and `insert_after_symbol`. diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 241e0b62..11585ea6 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -1,7 +1,7 @@ # Language Support Serena provides a set of versatile code querying and editing functionalities -based on symbolic understanding of the code. +based on symbolic understanding of the code across a wide range of programming languages. Equipped with these capabilities, Serena discovers and edits code just like a seasoned developer making use of an IDE's capabilities would. Serena can efficiently find the right context and do the right thing even in very large and @@ -13,7 +13,7 @@ There are two alternative technologies powering these capabilities: * **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE. -You can choose either of these backends depending on your preferences and requirements. +See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ## Language Servers @@ -113,25 +113,15 @@ see Serena's [memory on that](https://github.com/oraios/serena/blob/main/.serena ## The Serena JetBrains Plugin -As an alternative to language servers, the [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) -leverages the powerful code analysis capabilities of JetBrains IDEs. -The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs, -including IntelliJ IDEA, PyCharm, Android Studio, WebStorm, PhpStorm, RubyMine, GoLand, and potentially others -(Rider and CLion are unsupported though). +The [Serena JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) leverages the powerful code analysis capabilities of JetBrains IDEs. +The plugin naturally supports all programming languages and frameworks that are supported by JetBrains IDEs. When using the plugin, Serena connects to an instance of your JetBrains IDE via the plugin. For users who already work in a JetBrains IDE, this means Serena seamlessly integrates with the IDE instance you typically have open anyway, -requiring no additional setup or configuration beyond the plugin itself. This approach offers several key advantages: +requiring no additional setup or configuration beyond the plugin itself. -* **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena -* **No additional setup**: No need to download or configure separate language servers -* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration -* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks - -Even if you prefer to work in a different code editor, you can still benefit from the JetBrains plugin by running -a JetBrains IDE instance (most have free community editions) alongside your preferred editor with your project -opened and indexed. Serena will connect to the IDE for code analysis while you continue working in your editor -of choice. +* See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for a high-level overview of its benefits and usage details. +* See the [Features](005_features) section for a detailed comparison of the capabilities provided by the JetBrains Plugin vs. language servers. ```{raw} html

@@ -140,5 +130,3 @@ of choice.

``` - -See the [JetBrains Plugin documentation](../02-usage/025_jetbrains_plugin) for usage details. diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 32c69591..c8af4a14 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -14,23 +14,39 @@ leverage the powerful code analysis and editing capabilities of your JetBrains I We recommend the JetBrains plugin as the preferred way of using Serena, especially for users of JetBrains IDEs. +How it works: +1. Install the plugin in your JetBrains IDE +2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) +3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) +4. Start coding via your MCP client as usual + **Purchasing the JetBrains Plugin supports the Serena project.** The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena. - ## Advantages of the JetBrains Plugin There are multiple features that are only available when using the JetBrains plugin: * **External library indexing**: Dependencies and libraries are fully indexed and accessible to Serena -* **No additional setup**: No need to download or configure separate language servers -* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration -* **Multi-language excellence**: First-class support for polyglot projects with multiple languages and frameworks -* **Enhanced retrieval capabilities**: The plugin supports additional retrieval tools for type hierarchy information as well as fast and reliable documentation/type signature retrieval +* **Enhanced retrieval & refactoring capabilities**: The plugin adds additional [tools](../01-about/035_tools) (e.g. type + hierarchy retrieval, move, find declaration, inline symbol, etc.) + and transforms the underlying mechanisms of shared tools to build upon the IDE's capabilities. +* **Improved multi-agent support**: A single IDE instance naturally serves arbitrarily many agent sessions without requiring additional resources. +* **Enhanced performance**: Faster tool execution thanks to optimized IDE integration. +* **Multi-language excellence** and **framework support**: First-class support for polyglot projects with multiple languages. + and frameworks (whatever is recognised by your IDE as a symbol will also be available to Serena) +* **No additional setup**: No need to download or configure separate language servers. -We are also working on additional features like a `move_symbol` tool and debugging-related capabilities that +We are also working on additional features like debugging and advanced introspection capabilities, which will be available exclusively through the JetBrains plugin. +:::{note} +With Serena's JetBrains tools, we try to offer the latest features. +As a result, some of them are considered as beta features (see [tool list](../01-about/035_tools)), which may have some quirks. +Please report your experience with these tools if they do not work as expected. +::: + +(configure-jetbrains)= ## Configuring Serena to Use the JetBrains Plugin After installing the plugin, you need to configure Serena to use it. @@ -79,15 +95,16 @@ you will see `Languages: Using JetBrains backend` in the configuration overview. You will also notice that your client will use the JetBrains-specific tools like `jet_brains_find_symbol` and others like it. +(jetbrains-workflow)= ## Workflow Having installed the plugin in your IDE and having configured Serena to use the JetBrains backend, the general workflow is simple: 1. Open the project you want to work on in your JetBrains IDE -2. Open the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)) +2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)) 3. Start using Serena tools as usual -Note that the folder that is open in your IDE and the project's root folder must match. +Note that the project folder that is open in your IDE and the Serena project root folder must match. :::{tip} If you need to work on multiple projects in the same agent session, create a monorepo folder diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 5b4ca422..6dfd37f0 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -1,5 +1,6 @@ import logging import os +import re import shutil from pathlib import Path from typing import Optional, List @@ -152,9 +153,10 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"): with open(target_file, "w") as f: f.write("\n\n") f.write("# Tools\n\n") - f.write("Find the full list of Serena's tools below. \n") + f.write("Find the full list of Serena's tools below.\n\n") f.write("Note that in most configurations, only a subset of these tools will be enabled simultaneously.\n") - f.write("Tools marked as optional are disabled by default.\n\n") + f.write("Tools marked as *optional* are disabled by default.\n\n") + f.write("Tools marked as [BETA] were recently introduced and may not be fully robust yet.\n\n") tools_by_module = ToolRegistry().get_registered_tools_by_module() priority_modules = {"serena.tools.symbol_tools": 1, "serena.tools.jetbrains_tools": 2} @@ -165,16 +167,54 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"): module = module.replace("serena.tools.", "") text.with_line(f"* **{module}**") for tool in tools: - info = " *(optional)*" if tool.is_optional else "" + info = "" + if tool.is_optional: + info += " *(optional)*" + if tool.is_beta: + info += " [BETA]" text.with_line(f"* `{tool.tool_name}`{info}: {tool.class_docstring}", indent=2) f.write(text.build()) +def autogen_about_intro_features(): + readme_path = Path(__file__).parent.parent / "README.md" + with open(readme_path, "r", encoding="utf-8") as f: + readme_contents = f.read() + + match = re.search(r"(.*?)(.*?)^(## Features.*?)^## ", readme_contents, re.DOTALL | re.MULTILINE) + assert match, f"Failed to extract about texts from README.md. {__file__} probably needs to be updated." + + tagline = match.group(1).strip() + about_text = match.group(2).strip() + features_text = match.group(3).strip() + + autogen_info = f"\n\n" + + with open(Path(__file__).parent / "01-about" / "000_intro.md", "w", encoding="utf-8") as f: + f.write(autogen_info) + f.write("# About Serena\n\n") + f.write(f"**{tagline}**\n\n") + f.write(f"{about_text}\n\n") + + jetbrains_marketplace_link = ('```{raw} html\n' + '

' + '' + '

\n```') + + with open(Path(__file__).parent / "01-about" / "005_features.md", "w", encoding="utf-8") as f: + f.write(autogen_info) + features_text = re.subn(r"^#", r"", features_text, flags=re.MULTILINE)[0] + features_text = re.sub(r' Sequence[SerenaAgentMode]: + return [SerenaAgentMode.load(mode_name) for mode_name in self._default_modes or []] + + def get_base_modes(self) -> Sequence[SerenaAgentMode]: + return [SerenaAgentMode.load(mode_name) for mode_name in self._base_modes or []] + class SerenaAgent: def __init__( @@ -371,7 +378,7 @@ class SerenaAgent: self.serena_config, self._language_backend, self._context, self._active_modes, self._active_project ) self._exposed_tools = self._base_toolset.to_available_tools(self._all_tools) - log.info(f"Number of exposed tools: {len(self._exposed_tools)}") + log.info(f"Number of exposed tools: {len(self._exposed_tools)}. Exposed tools: {self._exposed_tools.tool_names}") # update the active tools (considering the active project, if any) self._active_tools: AvailableTools @@ -427,11 +434,20 @@ class SerenaAgent: # consider modes # Since modes can be dynamically turned on and off, we don't include their definitions directly, - # but for the initially active modes, we make sure that the tools they enable are included. - for mode in modes.get_modes(): + # For the initially active dynamic modes, we make sure that the tools they enable are included. + for mode in modes.get_default_modes(): tool_inclusion_definitions.append( NamedToolInclusionDefinition( - name=f"InitialModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools + name=f"InitialDynamicModeInclusions[{mode.name}]", included_optional_tools=mode.included_optional_tools + ) + ) + # For the base modes, we also apply the tool exclusions, since they apply throughout the entire session + for base_mode in modes.get_base_modes(): + tool_inclusion_definitions.append( + NamedToolInclusionDefinition( + name=f"BaseMode[{base_mode.name}]", + included_optional_tools=base_mode.included_optional_tools, + excluded_tools=base_mode.excluded_tools, ) ) diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 430c8c2a..2232f66b 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -57,6 +57,16 @@ class CodeEditor(Generic[TSymbol], ABC): """ raise NotImplementedError("This method must be overridden for each subclass") + def read_file(self, relative_path: str) -> str: + """ + Reads the content of a file. + + :param relative_path: the relative path of the file to read + :return: the content of the file + """ + with self._open_file_context(relative_path) as file: + return file.get_contents() + @contextmanager def edited_file_context(self, relative_path: str) -> Iterator["CodeEditor.EditedFile"]: """ @@ -220,15 +230,8 @@ class CodeEditor(Generic[TSymbol], ABC): edited_file.delete_text_between_positions(start_pos, end_pos) @abstractmethod - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: - """ - Renames the symbol with the given name throughout the codebase. - - :param name_path: the name path of the symbol to rename - :param relative_file_path: the relative path of the file containing the symbol - :param new_name: the new name for the symbol - :return: a status message - """ + def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> str: + pass class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): @@ -337,8 +340,16 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): operation.apply() return len(operations) - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: - symbol = self._find_unique_symbol(name_path, relative_file_path) + def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> str: + """ + Renames a symbol, file, or directory throughout the codebase. + + :param name_path: the name path of the symbol to rename + :param relative_path: the relative path of the file containing the symbol. + :param new_name: the new name + :return: a status message + """ + symbol = self._find_unique_symbol(name_path, relative_path) if not symbol.location.has_position_in_file(): raise ValueError(f"Symbol '{name_path}' does not have a valid position in file for renaming") @@ -346,9 +357,9 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]): assert symbol.location.line is not None assert symbol.location.column is not None - lang_server = self._get_language_server(relative_file_path) + lang_server = self._get_language_server(relative_path) rename_result = lang_server.request_rename_symbol_edit( - relative_file_path=relative_file_path, line=symbol.location.line, column=symbol.location.column, new_name=new_name + relative_file_path=relative_path, line=symbol.location.line, column=symbol.location.column, new_name=new_name ) if rename_result is None: raise ValueError( @@ -415,13 +426,31 @@ class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]): ) return JetBrainsSymbol(symbols[0], self._project) - def rename_symbol(self, name_path: str, relative_file_path: str, new_name: str) -> str: + def rename_symbol( + self, + name_path: str | None, + relative_path: str, + new_name: str, + rename_in_comments: bool = False, + rename_in_text_occurrences: bool = False, + ) -> str: + """ + Renames a code symbol, file, or directory throughout the codebase. + + :param name_path: the name path of the symbol to rename. Set to None for renaming a file or directory. + :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 rename_in_comments: whether to rename occurrences of the symbol in comments + :param rename_in_text_occurrences: whether to rename occurrences of the symbol in text + :return: a status message + """ with JetBrainsPluginClient.from_project(self._project) as client: client.rename_symbol( name_path=name_path, - relative_path=relative_file_path, + relative_path=relative_path, new_name=new_name, - rename_in_comments=False, - rename_in_text_occurrences=False, + rename_in_comments=rename_in_comments, + rename_in_text_occurrences=rename_in_text_occurrences, ) return "Success" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 9a8f3f50..257b8540 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -742,6 +742,8 @@ class SerenaDashboardViewer: width=self.width, height=self.height, hidden=self.start_minimized, + text_select=True, + zoomable=True, ) assert window is not None self.window = window diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index f237a079..f3ed6dc9 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -425,6 +425,21 @@ class JetBrainsPluginClient(ToStringMixin): self._postprocess_symbol_collection_response(symbol_collection) return symbol_collection + def move_symbol( + self, + name_path: str | None, + relative_path: str | None, + target_parent_name_path: str | None, + target_relative_path: str | None, + ) -> dict[str, Any]: + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "targetParentNamePath": target_parent_name_path, + "targetRelativePath": target_relative_path, + } + return self._make_request("POST", "/moveSymbol", request_data) + def find_references(self, name_path: str, relative_path: str, include_quick_info: bool) -> jb.SymbolCollectionResponse: """ Finds references to a symbol. @@ -504,8 +519,44 @@ class JetBrainsPluginClient(ToStringMixin): } return cast(jb.TypeHierarchyResponse, self._make_request("POST", "/getSubtypes", request_data)) + def safe_delete(self, name_path: str | None, relative_path: str, delete_even_if_used: bool, propagate: bool) -> dict[str, Any]: + """ + Safely deletes a symbol, checking for usages first. + + :param name_path: the name path of the symbol to delete + :param relative_path: the relative path to the file containing the symbol + :param delete_even_if_used: if True, delete the symbol even if it has usages + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "deleteEvenIfUsed": delete_even_if_used, + "propagate": propagate, + } + return self._make_request("POST", "/safeDelete", request_data) + + def inline_symbol( + self, + name_path: str, + relative_path: str, + keep_definition: bool, + ) -> dict[str, Any]: + """ + Inlines a method, replacing all call sites with the method body. + + :param name_path: the name path of the method to inline + :param relative_path: the relative path to the file containing the method + :param keep_definition: if True, keep the original method definition after inlining + """ + request_data = { + "namePath": name_path, + "relativePath": relative_path, + "keepDefinition": keep_definition, + } + return self._make_request("POST", "/inlineSymbol", request_data) + def rename_symbol( - self, name_path: str, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool + self, name_path: str | None, relative_path: str, new_name: str, rename_in_comments: bool, rename_in_text_occurrences: bool ) -> None: """ Renames a symbol. @@ -536,6 +587,46 @@ class JetBrainsPluginClient(ToStringMixin): } self._make_request("POST", "/refreshFile", request_data) + def find_declaration( + self, relative_path: str, line: int, col: int, include_body: bool, include_quick_info: bool + ) -> jb.SymbolCollectionResponse: + """ + Finds the declaration of the symbol at the given location. + + :param relative_path: the relative path to the file + :param line: the line number (0-based) + :param col: the column number (0-based) + :param include_body: whether to include the symbol body + :param include_quick_info: whether to include quick info about the symbol + """ + request_data = { + "relativePath": relative_path, + "line": line, + "col": col, + "includeBody": include_body, + "includeQuickInfo": include_quick_info, + } + symbol_collection = cast(jb.SymbolCollectionResponse, self._make_request("POST", "/findDeclaration", request_data)) + self._postprocess_symbol_collection_response(symbol_collection) + return symbol_collection + + def find_implementations(self, relative_path: str, name_path: str, include_quick_info: bool) -> jb.SymbolCollectionResponse: + """ + Finds the implementations of a symbol. + + :param relative_path: the relative path to the file containing the symbol + :param name_path: the name path of the symbol + :param include_quick_info: whether to include quick info about the symbol + """ + request_data = { + "relativePath": relative_path, + "namePath": name_path, + "includeQuickInfo": include_quick_info, + } + symbol_collection = cast(jb.SymbolCollectionResponse, self._make_request("POST", "/findImplementations", request_data)) + self._postprocess_symbol_collection_response(symbol_collection) + return symbol_collection + def close(self) -> None: self._session.close() diff --git a/src/serena/resources/config/internal_modes/jetbrains.yml b/src/serena/resources/config/internal_modes/jetbrains.yml index 863f3beb..a08b189f 100644 --- a/src/serena/resources/config/internal_modes/jetbrains.yml +++ b/src/serena/resources/config/internal_modes/jetbrains.yml @@ -1,6 +1,7 @@ description: JetBrains tools replace language server-based tools prompt: | - You have access to the very powerful JetBrains tools for symbolic operations: + You have access to the very powerful JetBrains tools for symbolic operations. + These party replace regular tools you were informed about: * `jet_brains_find_symbol` replaces `find_symbol` * `jet_brains_find_referencing_symbols` replaces `find_referencing_symbols` * `jet_brains_get_symbols_overview` replaces `get_symbols_overview` @@ -9,8 +10,16 @@ excluded_tools: - find_referencing_symbols - get_symbols_overview - restart_language_server + - safe_delete_symbol + - rename_symbol included_optional_tools: - - jet_brains_find_symbol + - jet_brains_find_declaration + - jet_brains_find_implementations - jet_brains_find_referencing_symbols + - jet_brains_find_symbol - jet_brains_get_symbols_overview + - jet_brains_inline_symbol + - jet_brains_move - jet_brains_type_hierarchy + - jet_brains_rename + - jet_brains_safe_delete diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml index 125a6818..de648821 100644 --- a/src/serena/resources/config/prompt_templates/system_prompt.yml +++ b/src/serena/resources/config/prompt_templates/system_prompt.yml @@ -3,7 +3,6 @@ # is encouraged (via the tool description) to call at the beginning of the conversation. prompts: system_prompt: | - You are a professional coding agent. You have access to semantic coding tools upon which you rely heavily for all your work. You operate in a resource-efficient and intelligent manner, always keeping in mind to not read or generate content that is not needed for the task at hand. diff --git a/src/serena/resources/dashboard/news/20260404.html b/src/serena/resources/dashboard/news/20260404.html new file mode 100644 index 00000000..debdfb63 --- /dev/null +++ b/src/serena/resources/dashboard/news/20260404.html @@ -0,0 +1,23 @@ +
+

Serena v1.0 Is Here!

+

April 3, 2026

+

+ After a full year of work since the first version of Serena, we are excited to announce the release of Serena v1.0! +

+

+ Since our last update, we have now added support for exciting new retrieval and refactoring tools, especially for users of the JetBrains backend. +

+
    +
  • safe_delete, jet_brains_safe_delete (beta):
    Safely deletes a symbol, checking for remaining usages first
  • +
  • jet_brains_move (beta):
    Moves a symbol, file or directory to a new location, updating all references
  • +
  • jet_brains_find_declaration:
    Finds the declaration of a symbol using the JetBrains backend
  • +
  • jet_brains_find_implementations:
    Finds the implementations of a symbol (e.g. abstract method)
  • +
  • jet_brains_inline_symbol (beta):
    Inlines a symbol, replacing all call sites with the symbol’s body
  • +
+

+ We are far from done. Many new features will be coming your way – for both the LSP and JetBrains backends. +

+

+ Thank you to our contributors, users and customers! +

+
\ No newline at end of file diff --git a/src/serena/tools/jetbrains_tools.py b/src/serena/tools/jetbrains_tools.py index bc0b99bc..1c4ef63f 100644 --- a/src/serena/tools/jetbrains_tools.py +++ b/src/serena/tools/jetbrains_tools.py @@ -3,10 +3,12 @@ from collections import Counter from typing import Any, Literal import serena.jetbrains.jetbrains_types as jb +from serena.code_editor import JetBrainsCodeEditor from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient from serena.jetbrains.jetbrains_types import SymbolDTO from serena.symbol import JetBrainsSymbolDictGrouper -from serena.tools import Tool, ToolMarkerOptional, ToolMarkerSymbolicRead +from serena.tools import Tool, ToolMarkerBeta, ToolMarkerOptional, ToolMarkerSymbolicEdit, ToolMarkerSymbolicRead +from serena.util.text_utils import find_text_coordinates log = logging.getLogger(__name__) @@ -111,6 +113,112 @@ class JetBrainsFindSymbolTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): return self._limit_length(result, max_answer_chars, shortened_result_factories=[create_shortened_result]) +class JetBrainsMoveTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): + """ + Moves a symbol, file or directory to a new location using the JetBrains backend, updating all references + """ + + def apply( + self, + relative_path: str, + name_path: str | None = None, + target_relative_path: str | None = None, + target_parent_name_path: str | None = None, + ) -> str: + """ + Moves a symbol, file or directory to a different location. The target location is the new parent + of the symbol, i.e. the moved entity is never renamed by the operation, only moved. + References to affected symbols are automatically updated. + + Valid moves: + - Symbol: + * (relative_path, name_path) -> new parent symbol (target_relative_path, target_parent_name_path) + * (relative_path, name_path) -> top level of target file or directory (target_relative_path) + Always consider the concrete language-specific semantics! + - target is a file: valid for languages like Python, where files are modules + - target is a directory: valid for languages like Java, where directories are packages and can contain classes + - File or directory: + * relative_path -> new parent directory (target_relative_path) + + :param relative_path: the relative path to the file containing the symbol to move. + :param name_path: the name path of the symbol to move (empty for moving file or dir). + :param target_relative_path: the relative path of the target directory or file. + :param target_parent_name_path: the name path of the target parent symbol. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.move_symbol( + name_path=name_path, + relative_path=relative_path, + target_parent_name_path=target_parent_name_path, + target_relative_path=target_relative_path, + ) + return self._to_json(response_dict) + + +class JetBrainsSafeDeleteTool(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): + """ + Safely deletes a symbol using the JetBrains backend, checking for remaining usages first + """ + + def apply( + self, + relative_path: str, + name_path: str | None = None, + delete_even_if_used: bool = False, + propagate: bool = False, + ) -> str: + """ + Safely deletes a symbol, checking for usages first. It is also + possible to request deleting of usages and cleaning up of unused code. + + :param relative_path: the relative path to the file containing the symbol to delete. + :param name_path: the name path of the symbol to delete. + A name path identifies a symbol within a source file, e.g. "MyClass/my_method". + :param delete_even_if_used: whether to force deletion even if the symbol still has usages. + Default is False (safe mode: will report usages instead of deleting). + :param propagate: whether to propagate the deletion to usages of the symbol and also + remove symbols that become unused after the deletion. Default is False. + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.safe_delete( + name_path=name_path, + relative_path=relative_path, + delete_even_if_used=delete_even_if_used, + propagate=propagate, + ) + return self._to_json(response_dict) + + +class JetBrainsInlineSymbol(Tool, ToolMarkerSymbolicEdit, ToolMarkerOptional, ToolMarkerBeta): + """ + Inlines a symbol using the JetBrains backend, replacing all call sites with the symbol's body + """ + + def apply( + self, + name_path: str, + relative_path: str, + keep_definition: bool = False, + ) -> str: + """ + Inlines a symbol (usually a method/function, but also classes may be amenable to inlining, + which turns invocation into anonymous class creation), + replacing all call sites with the symbol's body. + + :param name_path: the name path of the symbol to inline. + :param relative_path: the relative path to the file containing the symbol to inline. + :param keep_definition: whether to keep the original method definition after inlining all call sites. + May be ignored in some cases (e.g. when inlining a class). + """ + with JetBrainsPluginClient.from_project(self.project) as client: + response_dict = client.inline_symbol( + name_path=name_path, + relative_path=relative_path, + keep_definition=keep_definition, + ) + return self._to_json(response_dict) + + class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): """ Finds symbols that reference the given symbol using the JetBrains backend @@ -126,14 +234,12 @@ class JetBrainsFindReferencingSymbolsTool(Tool, ToolMarkerSymbolicRead, ToolMark max_answer_chars: int = -1, ) -> str: """ - Finds symbols that reference the symbol at the given `name_path`. + Finds symbols that reference the symbol at the specified symbol, i.e. returns symbols whose definitions (e.g. a function body) contain a reference to the given symbol. The result will contain metadata about the referencing symbols. - :param name_path: name path of the symbol for which to find references; matching logic as described in find symbol tool. - :param relative_path: the relative path to the file containing the symbol for which to find references. - Note that here you can't pass a directory but must pass a file. + :param name_path: name path of the symbol for which to find references + :param relative_path: the relative path to the file containing the symbol (must be a file, not a directory) :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. - :return: a list of JSON objects with the symbols referencing the requested symbol """ with JetBrainsPluginClient.from_project(self.project) as client: response_dict = client.find_references( @@ -188,7 +294,6 @@ class JetBrainsGetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOp :param depth: depth up to which descendants shall be retrieved (e.g., use 1 to also retrieve immediate children). :param max_answer_chars: max characters for the result (-1 for default). If exceeded, no content/a shortened result is returned. :param include_file_documentation: whether to include the file's docstring. Default False. - :return: a JSON object containing the symbols grouped by kind in a compact format. """ with JetBrainsPluginClient.from_project(self.project) as client: symbol_overview = client.get_symbols_overview( @@ -327,3 +432,74 @@ class JetBrainsTypeHierarchyTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptiona result = self._to_json(result_dict) return self._limit_length(result, max_answer_chars) + + +class JetBrainsFindDeclarationTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): + """ + Finds the declaration of a symbol using the JetBrains backend + """ + + def apply(self, relative_path: str, regex: str, include_body: bool = False) -> str: + r""" + Finds the declaration of a symbol. + + :param relative_path: the relative path to the source file containing the symbol for which to find the declaration. + :param regex: a regular expression with one group, where the group matches the symbol for which to perform the lookup. + For example, to find the declaration of the `process` method in a call like `obj.process()`, + pass an expression like "obj\.(process)\(process_input_arg=37\)". + Prefer regexes with sufficiently large context around the group to render the match unambiguous. + Uses Python syntax with MULTILINE and DOTALL flags enabled. + :param include_body: whether to include the symbol's body in the result. Default False. + """ + editor = self.create_code_editor() + content = editor.read_file(relative_path) + coords = find_text_coordinates(content, regex, require_unique=True) + assert coords is not None + with JetBrainsPluginClient.from_project(self.project) as client: + symbol_collection = client.find_declaration( + relative_path=relative_path, line=coords.line, col=coords.col, include_quick_info=False, include_body=include_body + ) + result = self._to_json(symbol_collection) + return result + + +class JetBrainsFindImplementationsTool(Tool, ToolMarkerSymbolicRead, ToolMarkerOptional): + """ + Finds the implementations of a symbol using the JetBrains backend + """ + + def apply(self, relative_path: str, name_path: str) -> str: + """ + Finds the implementations of a symbol. + + :param relative_path: the relative path to the source file containing the symbol for which to find implementations. + :param name_path: name path of the symbol for which to find implementations + """ + with JetBrainsPluginClient.from_project(self.project) as client: + symbol_collection = client.find_implementations( + relative_path=relative_path, + name_path=name_path, + include_quick_info=False, + ) + result = self._to_json(symbol_collection) + return result + + +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: + """ + Renames a symbol, file or directory throughout the codebase. + + :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. + :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) + return self._to_json(result) diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 2df83bc1..bff6f2fd 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -409,6 +409,7 @@ class InsertBeforeSymbolTool(Tool, ToolMarkerSymbolicEdit): class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit): """ Renames a symbol throughout the codebase using language server refactoring capabilities. + For JB, we use a separate tool. """ def apply( @@ -427,6 +428,47 @@ class RenameSymbolTool(Tool, ToolMarkerSymbolicEdit): :param new_name: the new name for the symbol :return: result summary indicating success or failure """ - code_editor = self.create_code_editor() - status_message = code_editor.rename_symbol(name_path, relative_file_path=relative_path, new_name=new_name) + code_editor = self.create_ls_code_editor() + status_message = code_editor.rename_symbol(name_path, relative_path=relative_path, new_name=new_name) return status_message + + +class SafeDeleteSymbol(Tool, ToolMarkerSymbolicEdit): + def apply( + self, + name_path_pattern: str, + relative_path: str, + ) -> str: + """ + Deletes the symbol if it is safe to do so (i.e., if there are no references to it) + or returns a list of references to it. + + :param name_path_pattern: name path of the symbol to delete (definitions in the `find_symbol` tool apply) + :param relative_path: the relative path to the file containing the symbol to delete + """ + ls_symbol_retriever = self.create_language_server_symbol_retriever() + symbol = ls_symbol_retriever.find_unique(name_path_pattern, substring_matching=False, within_relative_path=relative_path) + symbol_rel_path = symbol.relative_path + assert symbol_rel_path is not None, f"Symbol {name_path_pattern} has no relative path, this is likely a bug." + assert symbol_rel_path == relative_path, f"Symbol {name_path_pattern} is not in the expected relative path {relative_path}." + symbol_name_path = symbol.get_name_path() + + symbol_line = symbol.line + symbol_col = symbol.column + assert symbol_line is not None and symbol_col is not None, ( + f"Symbol {name_path_pattern} has no identifier position, this is likely a bug." + ) + lang_server = ls_symbol_retriever.get_language_server(symbol_rel_path) + references_locations = lang_server.request_references(symbol_rel_path, symbol_line, symbol_col) + file_to_lines: dict[str, list[int]] = defaultdict(list) + if references_locations: + for ref_loc in references_locations: + ref_relative_path = ref_loc.get("relativePath") + if ref_relative_path is None: + continue + file_to_lines[ref_relative_path].append(ref_loc["range"]["start"]["line"]) + if file_to_lines: + return f"Cannot delete, the symbol {symbol_name_path} is referenced in: {self._to_json(file_to_lines)}" + code_editor = self.create_ls_code_editor() + code_editor.delete_symbol(symbol_name_path, relative_file_path=symbol_rel_path) + return SUCCESS_RESULT diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 18eed68f..cb2b299d 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -21,7 +21,7 @@ from solidlsp.ls_exceptions import SolidLSPException if TYPE_CHECKING: from serena.agent import SerenaAgent - from serena.code_editor import CodeEditor + from serena.code_editor import CodeEditor, LanguageServerCodeEditor from serena.symbol import LanguageServerSymbolRetriever log = logging.getLogger(__name__) @@ -58,16 +58,23 @@ class Component(ABC): return self.agent.get_active_project_or_raise() def create_code_editor(self) -> "CodeEditor": - from ..code_editor import JetBrainsCodeEditor, LanguageServerCodeEditor + from ..code_editor import JetBrainsCodeEditor match self.agent.get_language_backend(): case LanguageBackend.LSP: - return LanguageServerCodeEditor(self.create_language_server_symbol_retriever()) + return self.create_ls_code_editor() case LanguageBackend.JETBRAINS: return JetBrainsCodeEditor(project=self.project) case _: raise ValueError + def create_ls_code_editor(self) -> "LanguageServerCodeEditor": + from ..code_editor import LanguageServerCodeEditor + + if not self.agent.is_using_language_server(): + raise Exception("Cannot create LanguageServerCodeEditor; agent is not in language server mode.") + return LanguageServerCodeEditor(self.create_language_server_symbol_retriever()) + class ToolMarker: """ @@ -103,6 +110,12 @@ class ToolMarkerSymbolicEdit(ToolMarkerCanEdit): """ +class ToolMarkerBeta(ToolMarker): + """ + Marker for tools that are considered beta features (may not be fully robust) + """ + + class ApplyMethodProtocol(Protocol): """Callable protocol for the apply method of a tool.""" @@ -400,6 +413,7 @@ class EditedFileContext: class RegisteredTool: tool_class: type[Tool] is_optional: bool + is_beta: bool tool_name: str @property @@ -422,10 +436,11 @@ class ToolRegistry: if not any(cls.__module__.startswith(pkg) for pkg in tool_packages): continue is_optional = issubclass(cls, ToolMarkerOptional) + is_beta = issubclass(cls, ToolMarkerBeta) name = cls.get_name_from_cls() if name in self._tool_dict: raise ValueError(f"Duplicate tool name found: {name}. Tool classes must have unique names.") - self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name) + self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name, is_beta=is_beta) def get_registered_tools_by_module(self) -> dict[str, list[RegisteredTool]]: """ diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 158170a4..343f26e2 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -120,10 +120,8 @@ class PrepareForNewConversationTool(Tool): class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject): """ - Provides instructions on how to use the Serena toolbox. - Should only be used in settings where the system prompt is not read automatically by the client. - - NOTE: Some MCP clients (including Claude Desktop) do not read the system prompt automatically! + Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') + for clients that do not read the initial instructions when the MCP server is connected. """ def apply(self) -> str: diff --git a/src/serena/util/text_utils.py b/src/serena/util/text_utils.py index 8f10fe0b..20c7b0b7 100644 --- a/src/serena/util/text_utils.py +++ b/src/serena/util/text_utils.py @@ -11,6 +11,7 @@ from bs4 import BeautifulSoup from joblib import Parallel, delayed from serena.constants import DEFAULT_SOURCE_FILE_ENCODING +from solidlsp.ls_utils import TextUtils log = logging.getLogger(__name__) @@ -537,21 +538,29 @@ class TextCoords: """ -def find_text_coordinates(content: str, regex: str) -> TextCoords | None: +def find_text_coordinates(content: str, regex: str, require_unique: bool = False) -> TextCoords | None: """ Finds the line and column number of the first match of a regex pattern in the given content. :param content: the text content to search through :param regex: the regular expression pattern to search for; it must match part of a single line, and contain exactly one group that captures the position of interest (e.g., the exact variable name to find the coordinates of) - :return: a tuple of (line_number, column_number) for the first match found, both 0-based, or None if no match is found + :param require_unique: if True, raises an error if not exactly one match is found; + if False, returns None if no match is found, and returns the coordinates of the first match if multiple matches are found + :return: the coordinates of the match or None """ - pattern = re.compile(regex) - for line_number, line in enumerate(content.splitlines()): - match = pattern.search(line) - if match: - if len(match.groups()) != 1: - raise ValueError(f"Regex must contain exactly one group to capture the position, but found {len(match.groups())} groups.") - column_number = match.start(1) - return TextCoords(line_number, column_number) - return None + pattern = re.compile(regex, flags=re.MULTILINE | re.DOTALL) + matches = list(pattern.finditer(content)) + if len(matches) == 0: + if require_unique: + raise ValueError(f"No match found for regex: {regex}") + return None + else: + if require_unique and len(matches) > 1: + raise ValueError(f"Match must be unique; found {len(matches)} matches for regex: {regex}") + match = matches[0] + if len(match.groups()) != 1: + raise ValueError(f"Regex must contain exactly one group to capture the position, but found {len(match.groups())} groups.") + index_in_content = match.start(1) + line, col = TextUtils.get_line_col_from_index(content, index_in_content) + return TextCoords(line, col) diff --git a/test/conftest.py b/test/conftest.py index ee476321..48443484 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any import pytest +from _pytest.mark import Mark, MarkDecorator from sensai.util.logging import configure from serena.config.serena_config import SerenaConfig, SerenaPaths @@ -244,6 +245,39 @@ Flag indicating whether the tests are running in the GitHub CI environment. is_windows = platform.system() == "Windows" +_LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { + Language.CLOJURE: [ + pytest.mark.clojure, + pytest.mark.skipif(not is_clojure_cli_available(), reason="clojure CLI is not installed"), + ], + Language.CPP: [pytest.mark.cpp], + Language.CPP_CCLS: [pytest.mark.cpp], + Language.CSHARP: [pytest.mark.csharp], + Language.FSHARP: [pytest.mark.fsharp], + Language.GO: [pytest.mark.go], + 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.PHP: [pytest.mark.php], + Language.PHP_PHPACTOR: [pytest.mark.php], + Language.POWERSHELL: [pytest.mark.powershell], + Language.PYTHON: [pytest.mark.python], + Language.PYTHON_JEDI: [pytest.mark.python], + Language.PYTHON_TY: [pytest.mark.python], + Language.RUST: [pytest.mark.rust], + Language.TYPESCRIPT: [pytest.mark.typescript], +} + + +def get_pytest_markers(language: Language) -> list[MarkDecorator | Mark]: + """Pytest markers for a language. + + The returned list contains the primary language marker and any + environment-dependent skip markers shared across the test suite. + """ + return _LANGUAGE_PYTEST_MARKERS[language] + + def _determine_disabled_languages() -> list[Language]: """ Determine which language tests should be disabled (based on the environment) diff --git a/test/resources/repos/typescript/test_repo/index.ts b/test/resources/repos/typescript/test_repo/index.ts index 7b8bd1da..724a67c6 100644 --- a/test/resources/repos/typescript/test_repo/index.ts +++ b/test/resources/repos/typescript/test_repo/index.ts @@ -14,3 +14,7 @@ export function helperFunction() { } helperFunction(); + +export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; +} diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 347862ca..f614627e 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -76,6 +76,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_delete_symbol_vue[test_case0] @@ -736,6 +740,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case2-before] @@ -761,6 +769,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case3-after] @@ -786,6 +798,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol[test_case3-before] @@ -811,6 +827,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_insert_in_rel_to_symbol_vue[test_case0-after] @@ -1888,6 +1908,10 @@ helperFunction(); + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } + ''' # --- # name: test_replace_body_vue[test_case0] diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index e2ca7362..51a0227c 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -12,7 +12,14 @@ import pytest from serena.agent import SerenaAgent from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig from serena.project import Project -from serena.tools import SUCCESS_RESULT, FindReferencingSymbolsTool, FindSymbolTool, ReplaceContentTool, ReplaceSymbolBodyTool +from serena.tools import ( + SUCCESS_RESULT, + FindReferencingSymbolsTool, + FindSymbolTool, + ReplaceContentTool, + ReplaceSymbolBodyTool, + SafeDeleteSymbol, +) from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.conftest import get_repo_path, is_ci, language_tests_enabled @@ -589,3 +596,92 @@ class TestSerenaAgent: relative_path="ws_manager.js", mode="regex", ) + + @pytest.mark.parametrize( + "serena_agent,name_path,relative_path", + [ + pytest.param( + Language.PYTHON, + "User", + os.path.join("test_repo", "models.py"), + marks=pytest.mark.python, + ), + pytest.param( + Language.JAVA, + "Model", + os.path.join("src", "main", "java", "test_repo", "Model.java"), + marks=pytest.mark.java, + ), + pytest.param( + Language.KOTLIN, + "Model", + os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"), + marks=[pytest.mark.kotlin] + ([pytest.mark.skip(reason="Kotlin LSP JVM crashes on restart in CI")] if is_ci else []), + ), + pytest.param( + Language.TYPESCRIPT, + "helperFunction", + "index.ts", + marks=pytest.mark.typescript, + ), + ], + indirect=["serena_agent"], + ) + def test_safe_delete_symbol_blocked_by_references(self, serena_agent: SerenaAgent, name_path: str, relative_path: str): + """ + Tests that SafeDeleteSymbol refuses to delete a symbol that is referenced elsewhere + and returns a message listing the referencing files. + """ + # wrap in modification context as a safety net: if the tool has a bug and deletes anyway, + # the file will be restored, preventing corruption of test resources + with project_file_modification_context(serena_agent, relative_path): + safe_delete_tool = serena_agent.get_tool(SafeDeleteSymbol) + result = safe_delete_tool.apply(name_path_pattern=name_path, relative_path=relative_path) + assert "Cannot delete" in result, f"Expected deletion to be blocked due to existing references, but got: {result}" + assert "referenced in" in result, f"Expected reference information in result, but got: {result}" + + @pytest.mark.parametrize( + "serena_agent,name_path,relative_path", + [ + pytest.param( + Language.PYTHON, + "Timer", + os.path.join("test_repo", "utils.py"), + marks=pytest.mark.python, + ), + pytest.param( + Language.JAVA, + "ModelUser", + os.path.join("src", "main", "java", "test_repo", "ModelUser.java"), + marks=pytest.mark.java, + ), + pytest.param( + Language.KOTLIN, + "ModelUser", + os.path.join("src", "main", "kotlin", "test_repo", "ModelUser.kt"), + marks=[pytest.mark.kotlin] + ([pytest.mark.skip(reason="Kotlin LSP JVM crashes on restart in CI")] if is_ci else []), + ), + pytest.param( + Language.TYPESCRIPT, + "unusedStandaloneFunction", + "index.ts", + marks=pytest.mark.typescript, + ), + ], + indirect=["serena_agent"], + ) + def test_safe_delete_symbol_succeeds_when_no_references(self, serena_agent: SerenaAgent, name_path: str, relative_path: str): + """ + Tests that SafeDeleteSymbol successfully deletes a symbol that has no references + and that the symbol is actually removed from the file. + """ + with project_file_modification_context(serena_agent, relative_path): + safe_delete_tool = serena_agent.get_tool(SafeDeleteSymbol) + result = safe_delete_tool.apply(name_path_pattern=name_path, relative_path=relative_path) + assert result == SUCCESS_RESULT, f"Expected successful deletion, but got: {result}" + + # verify the symbol was actually removed from the file + file_content = read_project_file(serena_agent.get_active_project(), relative_path) + assert name_path not in file_content, ( + f"Expected symbol {name_path} to be removed from {relative_path}, but it still appears in the file content" + )