diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3d0cd..a3ec240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ +# Latest + +Changes prior to the next official version change will appear here. + # 2025-04-07 -* Allow Serena to switch between projects (project activation) - * Add central Serena configuration in `serena_config.yml`, which - * contains the list of available projects - * allows to configure whether project activation is enabled - * now contains the GUI logging configuration (project configurations no longer do) - * Add new tools `activate_project` and `get_active_project` - * Providing a project configuration file in the launch parameters is now optional +* Serena core: + * New tool: FindReferencingCodeSnippets + * Adjusted prompt in CreateTextFileTool to prevent writing partial content (see [here](https://www.reddit.com/r/ClaudeAI/comments/1jpavtm/comment/mloek1x/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)). + * FindSymbolTool: allow passing a file for restricting search, not just a directory (Gemini was too dumb to pass directories) + * Allow Serena to switch between projects (project activation) + * Add central Serena configuration in `serena_config.yml`, which + * contains the list of available projects + * allows to configure whether project activation is enabled + * now contains the GUI logging configuration (project configurations no longer do) + * Add new tools `activate_project` and `get_active_project` + * Providing a project configuration file in the launch parameters is now optional * Logging: * Improve error reporting in case of initialization failure: open a new GUI log window showing the error or ensure that the existing log window remains visible for some time @@ -19,5 +27,3 @@ # 2025-04-01 Initial public version - - diff --git a/README.md b/README.md index 147c664..e2792ca 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ want to use Serena. That's it! Save the config and then restart Claude Desktop. -Note: on Windows and MacOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source +Note: on Windows and macOS there are official Claude Desktop applications by Anthropic, for Linux there is an [open-source community version](https://github.com/aaddrick/claude-desktop-debian). ⚠️ Be sure to fully quit the Claude Desktop application, as closing Claude will just minimize it to the system tray – at least on Windows. @@ -571,6 +571,7 @@ Here is the full list of Serena's tools with a short description (output of `uv * `delete_lines`: Deletes a range of lines within a file. * `delete_memory`: Deletes a memory from Serena's project-specific memory store. * `execute_shell_command`: Executes a shell command. +* `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). * `get_active_project`: Gets the name of the currently active project (if any) and lists existing projects @@ -592,4 +593,4 @@ Here is the full list of Serena's tools with a short description (output of `uv * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -* `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. \ No newline at end of file +* `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 6f22540..7b4c760 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -746,7 +746,7 @@ class LanguageServer: self._cache_has_changed = True return result - async def request_full_symbol_tree(self, start_dir_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. @@ -755,19 +755,16 @@ class LanguageServer: that are within the repository. Will ignore all directories that start with a dot (.) and __pycache__ directories. - Args: - start_dir_relative_path: if passed, only the symbols within this directory will be considered. - include_body: whether to include the body of the symbols in the result. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + :param include_body: whether to include the body of the symbols in the result. - Returns: - A list of root symbols representing the top-level packages/modules in the project. + :return: A list of root symbols representing the top-level packages/modules in the project. """ - if not self.server_started: - self.logger.log( - "request_full_symbol_tree called before Language Server started", - logging.ERROR, - ) - raise MultilspyException("Language Server not started") + if within_relative_path is not None and os.path.isfile(within_relative_path): + _, root_nodes = await self.request_document_symbols(within_relative_path, include_body=include_body) + return root_nodes # Helper function to check if a path should be ignored def should_ignore_dir(path: str) -> bool: @@ -854,7 +851,7 @@ class LanguageServer: return result # Start from the root or the specified directory - start_path = start_dir_relative_path or "." + start_path = within_relative_path or "." return await process_directory(start_path) @staticmethod @@ -1589,7 +1586,7 @@ class SyncLanguageServer: ).result() return result - def request_full_symbol_tree(self, start_package_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project and build a tree of symbols. Note: this may be slow the first time it is called. @@ -1598,15 +1595,16 @@ class SyncLanguageServer: that are within the repository. Will ignore all directories that start with a dot (.) and __pycache__ directories. - Args: - start_package_relative_path: if passed, only the symbols within this directory will be considered. - include_body: whether to include the body of the symbols in the result. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + If None, the entire codebase will be considered. + :param include_body: whether to include the body of the symbols in the result. - Returns: - A list of root symbols representing the top-level packages/modules in the project. + :return: A list of root symbols representing the top-level packages/modules in the project. """ result = asyncio.run_coroutine_threadsafe( - self.language_server.request_full_symbol_tree(start_package_relative_path, include_body), self.loop + self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop ).result() return result diff --git a/src/serena/agent.py b/src/serena/agent.py index ac459bc..78acc7a 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -22,7 +22,7 @@ from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind -from serena import serena_root_path +from serena import __version__, serena_root_path from serena.gui_log_viewer import GuiLogViewer, GuiLogViewerHandler from serena.llm.prompt_factory import PromptFactory from serena.symbol import SymbolLocation, SymbolManager @@ -145,7 +145,7 @@ class SerenaAgent: self._gui_log_handler = GuiLogViewerHandler(GuiLogViewer(title="Serena Logs"), level=log_level, format_string=LOG_FORMAT) Logger.root.addHandler(self._gui_log_handler) - log.info(f"Starting Serena server (process id={os.getpid()}, parent process id={os.getppid()})") + log.info(f"Starting Serena server (version={__version__}, process id={os.getpid()}, parent process id={os.getppid()})") log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) self.prompt_factory = PromptFactory() @@ -511,6 +511,10 @@ class CreateTextFileTool(Tool): You can also use insert_at_line to insert content at a specific line for existing files if the symbolic operations are not the right choice for what you want to do. + If ever used on an existing file, the content has to be the complete content of that file (so it + may never end with something like "The remaining content of the file is left unchanged."). + For operations that just replace a part of a file, use the replace_lines or the symbolic editing tools instead. + :param relative_path: the relative path to the file to create :param content: the (utf-8-encoded) content to write to the file :return: a message indicating success or failure @@ -604,11 +608,11 @@ class FindSymbolTool(Tool): self, name: str, depth: int = 0, + within_relative_path: str | None = None, include_body: bool = False, include_kinds: list[int] | None = None, exclude_kinds: list[int] | None = None, substring_matching: bool = False, - dir_relative_path: str | None = None, max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, ) -> str: """ @@ -624,7 +628,9 @@ class FindSymbolTool(Tool): (e.g. depth 1 will retrieve methods and attributes for the case where the symbol refers to a class). Provide a non-zero depth if you intend to subsequently query symbols that are contained in the retrieved symbol. - :param dir_relative_path: pass a directory relative path to only consider symbols within this directory. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. If None, the entire codebase will be considered. :param include_body: whether to include the body of all symbols in the result. You should only use this if you actually need the body of the symbol for the task at hand (for example, for a deep analysis @@ -653,7 +659,7 @@ class FindSymbolTool(Tool): include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching, - dir_relative_path=dir_relative_path, + within_relative_path=within_relative_path, ) symbol_dicts = [s.to_dict(kind=True, location=True, depth=depth, include_body=include_body) for s in symbols] result = json.dumps(symbol_dicts) @@ -715,6 +721,50 @@ class FindReferencingSymbolsTool(Tool): return self._limit_length(result, max_answer_chars) +class FindReferencingCodeSnippetsTool(Tool): + """ + Finds code snippets in which the symbol at the given location is referenced. + """ + + def apply( + self, + relative_path: str, + line: int, + column: int, + context_lines_before: int = 0, + context_lines_after: int = 0, + max_answer_chars: int = _DEFAULT_MAX_ANSWER_LENGTH, + ) -> str: + """ + Returns short code snippets where the symbol at the given location is referenced. + + Contrary to the `find_referencing_symbols` tool, this tool returns references that are not symbols but instead + code snippets that may or may not be contained in a symbol (for example, file-level calls). + It may make sense to use this tool to get a quick overview of the code that references + the symbol. Usually, just looking at code snippets is not enough to understand the full context, + unless the case you are investigating is very simple, + or you already have read the relevant symbols using the find_referencing_symbols tool and + now want to get an overview of how the referenced symbol (at the given location) is used in them. + The size of the snippets is controlled by the context_lines_before and context_lines_after parameters. + + :param relative_path: the relative path to the file containing the symbol + :param line: the line number of the symbol to find references for + :param column: the column of the symbol to find references for + :param context_lines_before: the number of lines to include before the line containing the reference + :param context_lines_after: the number of lines to include after the line containing the reference + :param max_answer_chars: if the output is longer than this number of characters, + no content will be returned. Don't adjust unless there is really no other way to get the content + required for the task. Instead, if the output is too long, you should + make a stricter query. + """ + matches = self.language_server.request_references_with_content( + relative_path, line, column, context_lines_before, context_lines_after + ) + result = [match.to_display_string() for match in matches] + result_json_str = json.dumps(result) + return self._limit_length(result_json_str, max_answer_chars) + + class ReplaceSymbolBodyTool(Tool): """ Replaces the full definition of a symbol. diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 9c698f4..1819b1a 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -200,7 +200,7 @@ class SymbolManager: def find_by_name( self, name: str, - dir_relative_path: str | None = None, + within_relative_path: str | None = None, include_body: bool = False, include_kinds: Sequence[SymbolKind] | None = None, exclude_kinds: Sequence[SymbolKind] | None = None, @@ -210,7 +210,9 @@ class SymbolManager: Find all symbols that match the given name. :param name: the name of the symbol to find - :param dir_relative_path: pass a directory relative path to only consider symbols within this directory. + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. If None, the entire codebase will be considered. :param include_body: whether to include the body of all symbols in the result. Note: you can filter out the bodies of the children if you set include_children_body=False @@ -224,7 +226,7 @@ class SymbolManager: :return: a list of symbols that match the given name """ symbols: list[Symbol] = [] - symbol_roots = self.lang_server.request_full_symbol_tree(start_package_relative_path=dir_relative_path, include_body=include_body) + symbol_roots = self.lang_server.request_full_symbol_tree(within_relative_path=within_relative_path, include_body=include_body) for root in symbol_roots: symbols.extend( Symbol(root).find(name, include_kinds=include_kinds, exclude_kinds=exclude_kinds, substring_matching=substring_matching) diff --git a/test/multilspy/test_symbol_retrieval.py b/test/multilspy/test_symbol_retrieval.py index dd630b5..14fe1fc 100644 --- a/test/multilspy/test_symbol_retrieval.py +++ b/test/multilspy/test_symbol_retrieval.py @@ -366,7 +366,7 @@ class TestLanguageServerSymbols: def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer): """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file - examples_package_roots = language_server.request_full_symbol_tree(start_package_relative_path="examples") + examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples") assert len(examples_package_roots) == 1 examples_package = examples_package_roots[0] assert examples_package["name"] == "examples"