From 43457db31da38b3f8629f3b76f7ec72c02e2299d Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 10 Nov 2025 15:59:42 +0100 Subject: [PATCH 01/12] Do not ignore /.serena, only .serena in test folders --- .gitignore | 2 +- .serena/.gitignore | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .serena/.gitignore diff --git a/.gitignore b/.gitignore index 27086cc8..89bbe0b2 100644 --- a/.gitignore +++ b/.gitignore @@ -200,6 +200,7 @@ pylint.html /*.yml !*.template.yml /agent-ui +/test/**/.serena # dynamic LS installations /src/multilspy/language_servers/*/static @@ -220,7 +221,6 @@ pylint.html *.tmp tmp/ -.serena/ .vscode/ # Claude settings diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..14d86ad6 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache From e2775684c4e4bea05cbf5f2a03b8c2b92d9e88f7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 10 Nov 2025 16:01:03 +0100 Subject: [PATCH 02/12] Add EditMemoryTool Implements #738 --- src/serena/project.py | 8 ++++---- src/serena/tools/file_tools.py | 18 +++++++++++++++++- src/serena/tools/memory_tools.py | 25 ++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/serena/project.py b/src/serena/project.py index c1227523..3cd5eb08 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -26,21 +26,21 @@ class MemoriesManager: self._memory_dir.mkdir(parents=True, exist_ok=True) self._encoding = SERENA_FILE_ENCODING - def _get_memory_file_path(self, name: str) -> Path: + def get_memory_file_path(self, name: str) -> Path: # strip all .md from the name. Models tend to get confused, sometimes passing the .md extension and sometimes not. name = name.replace(".md", "") filename = f"{name}.md" return self._memory_dir / filename def load_memory(self, name: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) if not memory_file_path.exists(): return f"Memory file {name} not found, consider creating it with the `write_memory` tool if you need it." with open(memory_file_path, encoding=self._encoding) as f: return f.read() def save_memory(self, name: str, content: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) with open(memory_file_path, "w", encoding=self._encoding) as f: f.write(content) return f"Memory {name} written." @@ -49,7 +49,7 @@ class MemoriesManager: return [f.name.replace(".md", "") for f in self._memory_dir.iterdir() if f.is_file()] def delete_memory(self, name: str) -> str: - memory_file_path = self._get_memory_file_path(name) + memory_file_path = self.get_memory_file_path(name) memory_file_path.unlink() return f"Memory {name} deleted." diff --git a/src/serena/tools/file_tools.py b/src/serena/tools/file_tools.py index 6a7479bf..1d091535 100644 --- a/src/serena/tools/file_tools.py +++ b/src/serena/tools/file_tools.py @@ -195,7 +195,23 @@ class ReplaceRegexTool(Tool, ToolMarkerCanEdit): If this is set to False and the regex matches multiple occurrences, an error will be returned (and you may retry with a revised, more specific regex). """ - self.project.validate_relative_path(relative_path, require_not_ignored=True) + return self.replace_regex( + relative_path, regex, repl, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True + ) + + def replace_regex( + self, + relative_path: str, + regex: str, + repl: str, + allow_multiple_occurrences: bool = False, + require_not_ignored: bool = True, + ) -> str: + """ + Performs the regex replacement, with additional options not exposed in the tool. + This function can be used internally by other tools. + """ + self.project.validate_relative_path(relative_path, require_not_ignored=require_not_ignored) with EditedFileContext(relative_path, self.agent) as context: original_content = context.get_original_content() updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE) diff --git a/src/serena/tools/memory_tools.py b/src/serena/tools/memory_tools.py index f005c8af..c6e2b93b 100644 --- a/src/serena/tools/memory_tools.py +++ b/src/serena/tools/memory_tools.py @@ -1,6 +1,6 @@ import json -from serena.tools import Tool +from serena.tools import ReplaceRegexTool, Tool class WriteMemoryTool(Tool): @@ -64,3 +64,26 @@ class DeleteMemoryTool(Tool): or no longer relevant for the project. """ return self.memories_manager.delete_memory(memory_file_name) + + +class EditMemoryTool(Tool): + def apply( + self, + memory_file_name: str, + regex: str, + repl: str, + ) -> str: + r""" + Replaces content matching a regular expression in a memory. + + :param memory_file_name: the name of the memory + :param regex: a Python-style regular expression (flags DOTALL and MULTILINE enabled, i.e. + '.' matches all characters, multi-line matching is enabled). + Apply the usual escaping as needed for reserved characters in Python-style regex. + :param repl: the string to replace the matched content with, which may contain + backreferences like \1, \2, etc. for groups matched by the regex. + Insert new content verbatim, except for backslashes, which have to be escaped. + """ + replace_regex_tool = self.agent.get_tool(ReplaceRegexTool) + rel_path = self.memories_manager.get_memory_file_path(memory_file_name).relative_to(self.get_project_root()) + return replace_regex_tool.replace_regex(str(rel_path), regex, repl, require_not_ignored=False) From 0a40f580dd2424a1bf36668322f86e231ca25817 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 10 Nov 2025 18:50:10 +0100 Subject: [PATCH 03/12] Improve handling of custom LS-specific settings --- .../language_servers/csharp_language_server.py | 6 +++--- src/solidlsp/language_servers/eclipse_jdtls.py | 2 +- src/solidlsp/language_servers/intelephense.py | 9 ++++----- src/solidlsp/ls.py | 1 + src/solidlsp/settings.py | 11 ++++++++++- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index d5531fd2..ae044aae 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -248,8 +248,7 @@ class CSharpLanguageServer(SolidLanguageServer): Returns a tuple of (dotnet_path, language_server_dll_path). """ language_enum = cls.get_language_enum_instance() - ls_specific_settings = solidlsp_settings.ls_specific_settings or {} - language_specific_config = ls_specific_settings.get(language_enum, {}) + language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", [])) logger.log("Resolving runtime dependencies", logging.DEBUG) @@ -448,7 +447,8 @@ class CSharpLanguageServer(SolidLanguageServer): logger.log("Downloading .NET 9 runtime...", logging.INFO) dotnet_dir.mkdir(parents=True, exist_ok=True) - custom_dotnet_runtime_url = solidlsp_settings.ls_specific_settings.get(cls.get_language_enum_instance(), {}).get( + custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) + custom_dotnet_runtime_url = custom_settings.get( "dotnet_runtime_url" ) if custom_dotnet_runtime_url is not None: diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index e1181902..095767e0 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -343,7 +343,7 @@ class EclipseJDTLS(SolidLanguageServer): repo_uri = pathlib.Path(repository_absolute_path).as_uri() # Load user's Maven and Gradle configuration paths from ls_specific_settings["java"] - custom_java_settings = self._solidlsp_settings.ls_specific_settings.get(self.get_language_enum_instance(), {}) + custom_java_settings = self._custom_settings # Maven settings: default to ~/.m2/settings.xml default_maven_settings = os.path.join(os.path.expanduser("~"), ".m2", "settings.xml") diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index dfac4eb5..f03262da 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -26,8 +26,8 @@ class Intelephense(SolidLanguageServer): Provides PHP specific instantiation of the LanguageServer class using Intelephense. You can pass the following entries in ls_specific_settings["php"]: - - maxMemory - - maxFileSize + - maxMemory: sets intelephense.maxMemory + - maxFileSize: sets intelephense.files.maxSize """ @override @@ -130,9 +130,8 @@ class Intelephense(SolidLanguageServer): if license_key: initialization_options["licenceKey"] = license_key - custom_intelephense_settings = self._solidlsp_settings.ls_specific_settings.get(self.get_language_enum_instance(), {}) - max_memory = custom_intelephense_settings.get("maxMemory") - max_file_size = custom_intelephense_settings.get("maxFileSize") + max_memory = self._custom_settings.get("maxMemory") + max_file_size = self._custom_settings.get("maxFileSize") if max_memory is not None: initialization_options["intelephense.maxMemory"] = max_memory if max_file_size is not None: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 93575dbb..5da8b68d 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -176,6 +176,7 @@ class SolidLanguageServer(ABC): as opposed to HTTP, TCP modes supported by some language servers. """ self._solidlsp_settings = solidlsp_settings + self._custom_settings = solidlsp_settings.get_ls_specific_settings(self.get_language_enum_instance()) self._encoding = config.encoding self.logger = logger self.repository_root_path: str = repository_root_path diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 66ecadf2..0e9e9afc 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -23,7 +23,7 @@ class SolidLSPSettings: For instance, if this is ".solidlsp" and the project is located at "/home/user/myproject", then Solid-LSP will store project-specific data in "/home/user/myproject/.solidlsp". """ - ls_specific_settings: dict["Language", Any] = field(default_factory=dict) + ls_specific_settings: dict["Language", dict[str, Any]] = field(default_factory=dict) """ Advanced configuration option allowing to configure language server implementation specific options. Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available. @@ -37,3 +37,12 @@ class SolidLSPSettings: @property def ls_resources_dir(self): return os.path.join(str(self.solidlsp_dir), "language_servers", "static") + + def get_ls_specific_settings(self, language: "Language") -> dict[str, Any]: + """ + Get the language server specific settings for the given language. + + :param language: The programming language. + :return: A dictionary of settings for the language server. + """ + return self.ls_specific_settings.get(language, {}) From 064224d6aef0908b9546f952c13b81caec188410 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 10 Nov 2025 20:08:58 +0100 Subject: [PATCH 04/12] Refactor retrieval of LS-specific settings, adding abstraction and ensuring that all relevant retrievals are logged --- .../csharp_language_server.py | 10 +---- .../language_servers/eclipse_jdtls.py | 41 +++++++++---------- src/solidlsp/ls.py | 7 +++- src/solidlsp/settings.py | 29 ++++++++++++- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index ae044aae..09b2570d 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -247,16 +247,10 @@ class CSharpLanguageServer(SolidLanguageServer): Ensure .NET runtime and Microsoft.CodeAnalysis.LanguageServer are available. Returns a tuple of (dotnet_path, language_server_dll_path). """ - language_enum = cls.get_language_enum_instance() language_specific_config = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) runtime_dependency_overrides = cast(list[dict[str, Any]], language_specific_config.get("runtime_dependencies", [])) logger.log("Resolving runtime dependencies", logging.DEBUG) - if language_specific_config: - logger.log( - f"Language-specific config for {language_enum}: {language_specific_config}", - logging.DEBUG, - ) runtime_dependencies = RuntimeDependencyCollection( _RUNTIME_DEPENDENCIES, @@ -448,9 +442,7 @@ class CSharpLanguageServer(SolidLanguageServer): dotnet_dir.mkdir(parents=True, exist_ok=True) custom_settings = solidlsp_settings.get_ls_specific_settings(cls.get_language_enum_instance()) - custom_dotnet_runtime_url = custom_settings.get( - "dotnet_runtime_url" - ) + custom_dotnet_runtime_url = custom_settings.get("dotnet_runtime_url") if custom_dotnet_runtime_url is not None: logger.log(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}", logging.INFO) url = custom_dotnet_runtime_url diff --git a/src/solidlsp/language_servers/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls.py index 095767e0..d5566a4b 100644 --- a/src/solidlsp/language_servers/eclipse_jdtls.py +++ b/src/solidlsp/language_servers/eclipse_jdtls.py @@ -343,52 +343,51 @@ class EclipseJDTLS(SolidLanguageServer): repo_uri = pathlib.Path(repository_absolute_path).as_uri() # Load user's Maven and Gradle configuration paths from ls_specific_settings["java"] - custom_java_settings = self._custom_settings # Maven settings: default to ~/.m2/settings.xml - default_maven_settings = os.path.join(os.path.expanduser("~"), ".m2", "settings.xml") - maven_user_settings = None - if "maven_user_settings" in custom_java_settings: + default_maven_settings_path = os.path.join(os.path.expanduser("~"), ".m2", "settings.xml") + custom_maven_settings_path = self._custom_settings.get("maven_user_settings") + if custom_maven_settings_path is not None: # User explicitly provided a path - maven_settings_path = custom_java_settings["maven_user_settings"] - if not os.path.exists(maven_settings_path): + if not os.path.exists(custom_maven_settings_path): error_msg = ( - f"Maven settings file not found: {maven_settings_path}. " + f"Provided maven settings file not found: {custom_maven_settings_path}. " f"Fix: create the file, update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> maven_user_settings), " - f"or remove the setting to use default (~/.m2/settings.xml)" + f"or remove the setting to use default ({default_maven_settings_path})" ) self.logger.log(error_msg, logging.ERROR) raise FileNotFoundError(error_msg) - maven_user_settings = maven_settings_path - self.logger.log(f"Using Maven settings from custom location: {maven_user_settings}", logging.INFO) - elif os.path.exists(default_maven_settings): - maven_user_settings = default_maven_settings - self.logger.log(f"Using Maven settings from default location: {maven_user_settings}", logging.INFO) + maven_settings_path = custom_maven_settings_path + self.logger.log(f"Using Maven settings from custom location: {maven_settings_path}", logging.INFO) + elif os.path.exists(default_maven_settings_path): + maven_settings_path = default_maven_settings_path + self.logger.log(f"Using Maven settings from default location: {maven_settings_path}", logging.INFO) else: + maven_settings_path = None self.logger.log( - f"Maven settings not found at default location ({default_maven_settings}), will use JDTLS defaults", logging.INFO + f"Maven settings not found at default location ({default_maven_settings_path}), will use JDTLS defaults", logging.INFO ) # Gradle user home: default to ~/.gradle default_gradle_home = os.path.join(os.path.expanduser("~"), ".gradle") - gradle_user_home = None - if "gradle_user_home" in custom_java_settings: + custom_gradle_home = self._custom_settings.get("gradle_user_home") + if custom_gradle_home is not None: # User explicitly provided a path - gradle_user_home_path = custom_java_settings["gradle_user_home"] - if not os.path.exists(gradle_user_home_path): + if not os.path.exists(custom_gradle_home): error_msg = ( - f"Gradle user home directory not found: {gradle_user_home_path}. " + f"Gradle user home directory not found: {custom_gradle_home}. " f"Fix: create the directory, update path in ~/.serena/serena_config.yml (ls_specific_settings -> java -> gradle_user_home), " f"or remove the setting to use default (~/.gradle)" ) self.logger.log(error_msg, logging.ERROR) raise FileNotFoundError(error_msg) - gradle_user_home = gradle_user_home_path + gradle_user_home = custom_gradle_home self.logger.log(f"Using Gradle user home from custom location: {gradle_user_home}", logging.INFO) elif os.path.exists(default_gradle_home): gradle_user_home = default_gradle_home self.logger.log(f"Using Gradle user home from default location: {gradle_user_home}", logging.INFO) else: + gradle_user_home = None self.logger.log( f"Gradle user home not found at default location ({default_gradle_home}), will use JDTLS defaults", logging.INFO ) @@ -579,7 +578,7 @@ class EclipseJDTLS(SolidLanguageServer): "checkProjectSettingsExclusions": False, "updateBuildConfiguration": "interactive", "maven": { - "userSettings": maven_user_settings, + "userSettings": maven_settings_path, "globalSettings": None, "notCoveredPluginExecutionSeverity": "warning", "defaultMojoExecutionAction": "ignore", diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 5da8b68d..ceec60df 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -176,7 +176,12 @@ class SolidLanguageServer(ABC): as opposed to HTTP, TCP modes supported by some language servers. """ self._solidlsp_settings = solidlsp_settings - self._custom_settings = solidlsp_settings.get_ls_specific_settings(self.get_language_enum_instance()) + lang = self.get_language_enum_instance() + self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang) + logger.log( + f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}", + logging.DEBUG, + ) self._encoding = config.encoding self.logger = logger self.repository_root_path: str = repository_root_path diff --git a/src/solidlsp/settings.py b/src/solidlsp/settings.py index 0e9e9afc..dff5df9a 100644 --- a/src/solidlsp/settings.py +++ b/src/solidlsp/settings.py @@ -2,14 +2,19 @@ Defines settings for Solid-LSP """ +import logging import os import pathlib from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +from sensai.util.string import ToStringMixin + if TYPE_CHECKING: from solidlsp.ls_config import Language +log = logging.getLogger(__name__) + @dataclass class SolidLSPSettings: @@ -38,11 +43,31 @@ class SolidLSPSettings: def ls_resources_dir(self): return os.path.join(str(self.solidlsp_dir), "language_servers", "static") - def get_ls_specific_settings(self, language: "Language") -> dict[str, Any]: + class CustomLSSettings(ToStringMixin): + def __init__(self, settings: dict[str, Any] | None) -> None: + self.settings = settings or {} + + def get(self, key: str, default_value: Any = None) -> Any: + """ + Returns the custom setting for the given key or the default value if not set. + If a custom value is set for the given key, the retrieval is logged. + + :param key: the key + :param default_value: the default value to use if no custom value is set + :return: the value + """ + if key in self.settings: + value = self.settings[key] + log.info("Using custom LS setting %s for key '%s'", value, key) + else: + value = default_value + return value + + def get_ls_specific_settings(self, language: "Language") -> CustomLSSettings: """ Get the language server specific settings for the given language. :param language: The programming language. :return: A dictionary of settings for the language server. """ - return self.ls_specific_settings.get(language, {}) + return self.CustomLSSettings(self.ls_specific_settings.get(language)) From cb038418eb527086b85d0659f31ada0294140b72 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 10 Nov 2025 20:29:46 +0100 Subject: [PATCH 05/12] Intelephense: Add customizable setting ignore_vendor Resolves #731 --- src/solidlsp/language_servers/intelephense.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/solidlsp/language_servers/intelephense.py b/src/solidlsp/language_servers/intelephense.py index f03262da..aabcb583 100644 --- a/src/solidlsp/language_servers/intelephense.py +++ b/src/solidlsp/language_servers/intelephense.py @@ -28,15 +28,12 @@ class Intelephense(SolidLanguageServer): You can pass the following entries in ls_specific_settings["php"]: - maxMemory: sets intelephense.maxMemory - maxFileSize: sets intelephense.files.maxSize + - ignore_vendor: whether or ignore directories named "vendor" (default: true) """ @override def is_ignored_dirname(self, dirname: str) -> bool: - # For PHP projects, we should ignore: - # - vendor: third-party dependencies list[str]: @@ -100,6 +97,18 @@ class Intelephense(SolidLanguageServer): ) self.request_id = 0 + # For PHP projects, we should ignore: + # - node_modules: if the project has JavaScript components + # - cache: commonly used for caching + # - (configurable) vendor: third-party dependencies InitializeParams: """ Returns the initialization params for the Intelephense Language Server. From 2d7527bcde281ea62cb545d9c3c56da428cad25d Mon Sep 17 00:00:00 2001 From: David Bernazal Date: Thu, 13 Nov 2025 02:23:15 -0600 Subject: [PATCH 06/12] fix: Adding window support for ElixirTools on init (#752) * Adding window support for ElixirTools init * Update type to map --- src/solidlsp/language_servers/elixir_tools/elixir_tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index 1986c062..5c7a6fb5 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -250,6 +250,11 @@ class ElixirTools(SolidLanguageServer): "didChangeConfiguration": {"dynamicRegistration": True}, "executeCommand": {"dynamicRegistration": True}, }, + "window": { + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "showDocument": {"support": True}, + "workDoneProgress": True, + }, }, "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], } From 2b4fd66674969df6b3005b1f5b2555c06c6aa5c2 Mon Sep 17 00:00:00 2001 From: Adra <150769865+DraPraks@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:22:37 +0700 Subject: [PATCH 07/12] feat: Added project create command with `--name`, `--index`, and auto-creation (#755) Previous command `generate_yml` has been removed --- docs/02-usage/020_running.md | 7 +- docs/02-usage/040_workflow.md | 6 +- docs/02-usage/050_configuration.md | 9 +- src/serena/cli.py | 78 ++++++- test/serena/test_cli_project_commands.py | 268 +++++++++++++++++++++++ 5 files changed, 347 insertions(+), 21 deletions(-) create mode 100644 test/serena/test_cli_project_commands.py diff --git a/docs/02-usage/020_running.md b/docs/02-usage/020_running.md index 1af40b13..f248a4c7 100644 --- a/docs/02-usage/020_running.md +++ b/docs/02-usage/020_running.md @@ -178,9 +178,12 @@ Here are some examples of commands you might find useful: tools description find_symbol # creating a new Serena project in the current directory - project generate-yml + project create -# indexing the project in the current directory +# creating and immediately indexing a project + project create --index + +# indexing the project in the current directory (auto-creates if needed) project index # run a health check on the project in the current directory diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index aca2143a..fcc65824 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -22,17 +22,19 @@ You can create a project either To explicitly create a project, use the following command while in the project directory: - project generate-yml [options] + project create [options] For instance, when using `uvx`, run - uvx --from git+https://github.com/oraios/serena serena project generate-yml [options] + uvx --from git+https://github.com/oraios/serena serena project create [options] * For an empty project, you will need to specify the programming language (e.g., `--language python`). * For an existing project, the main programming language will be detected automatically, but you can choose to explicitly specify multiple languages by passing the `--language` parameter multiple times (e.g. `--language python --language typescript`). + * You can optionally specify a custom project name with `--name "My Project"`. + * You can immediately index the project after creation with `--index`. After creation, you can adjust the project settings in the generated `.serena/project.yml` file. diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index bf1a85f7..11e363cb 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -22,13 +22,8 @@ Serena is configured in four places: See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments. 3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever that project is activated. This file will be autogenerated when you first use Serena on that project, but you can also - generate it explicitly with - - ```shell - uvx --from git+https://github.com/oraios/serena serena project generate-yml - ``` - - (or use the `--directory` command version). + create it explicitly with `serena project create [options]` (see the [Project Workflow](040_workflow.md#project-creation--indexing) + for details on available options). 4. Through the context and modes (see below). After the initial setup, continue with one of the sections below, depending on how you diff --git a/src/serena/cli.py b/src/serena/cli.py index fb78cd3d..178d643d 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -414,15 +414,21 @@ class ProjectCommands(AutoRegisteringGroup): ) @staticmethod - @click.command("generate-yml", help="Generate a project.yml file.") - @click.argument("project_path", type=click.Path(exists=True, file_okay=False), default=os.getcwd()) - @click.option( - "--language", type=str, multiple=True, help="Programming language(s); inferred if not specified. Can be passed multiple times." - ) - def generate_yml(project_path: str, language: tuple[str, ...]) -> None: + def _create_project(project_path: str, name: str | None, language: tuple[str, ...]) -> ProjectConfig: + """ + Helper method to create a project configuration file. + + :param project_path: Path to the project directory + :param name: Optional project name (defaults to directory name if not specified) + :param language: Tuple of language names + :return: The generated ProjectConfig instance + :raises FileExistsError: If project.yml already exists + :raises ValueError: If an unsupported language is specified + """ yml_path = os.path.join(project_path, ProjectConfig.rel_path_to_project_yml()) if os.path.exists(yml_path): raise FileExistsError(f"Project file {yml_path} already exists.") + languages: list[Language] = [] if language: for lang in language: @@ -431,12 +437,51 @@ class ProjectCommands(AutoRegisteringGroup): except ValueError: all_langs = [l.value for l in Language] raise ValueError(f"Unknown language '{lang}'. Supported: {all_langs}") - generated_conf = ProjectConfig.autogenerate(project_root=project_path, languages=languages if languages else None) - print(f"Generated project.yml with languages {generated_conf.languages} at {yml_path}.") + + generated_conf = ProjectConfig.autogenerate( + project_root=project_path, project_name=name, languages=languages if languages else None + ) + return generated_conf @staticmethod - @click.command("index", help="Index a project by saving symbols to the LSP cache.") + @click.command("create", help="Create a new Serena project configuration.") + @click.argument("project_path", type=click.Path(exists=True, file_okay=False), default=os.getcwd()) + @click.option("--name", type=str, default=None, help="Project name; defaults to directory name if not specified.") + @click.option( + "--language", type=str, multiple=True, help="Programming language(s); inferred if not specified. Can be passed multiple times." + ) + @click.option("--index", is_flag=True, help="Index the project after creation.") + @click.option( + "--log-level", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), + default="WARNING", + help="Log level for indexing (only used if --index is set).", + ) + @click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file (only used if --index is set).") + def create(project_path: str, name: str | None, language: tuple[str, ...], index: bool, log_level: str, timeout: float) -> None: + try: + generated_conf = ProjectCommands._create_project(project_path, name, language) + yml_path = os.path.join(project_path, ProjectConfig.rel_path_to_project_yml()) + click.echo(f"Generated project.yml with languages {generated_conf.languages} at {yml_path}.") + + if index: + click.echo("Indexing project...") + ProjectCommands._index_project(project_path, log_level, timeout=timeout) + except FileExistsError as e: + raise click.ClickException(f"Project already exists: {e}\nUse 'serena project index' to index an existing project.") + except ValueError as e: + raise click.ClickException(str(e)) + + @staticmethod + @click.command("index", help="Index a project by saving symbols to the LSP cache. Auto-creates project.yml if it doesn't exist.") @click.argument("project", type=click.Path(exists=True), default=os.getcwd(), required=False) + @click.option("--name", type=str, default=None, help="Project name (only used if auto-creating project.yml).") + @click.option( + "--language", + type=str, + multiple=True, + help="Programming language(s) (only used if auto-creating project.yml). Inferred if not specified.", + ) @click.option( "--log-level", type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), @@ -444,7 +489,20 @@ class ProjectCommands(AutoRegisteringGroup): help="Log level for indexing.", ) @click.option("--timeout", type=float, default=10, help="Timeout for indexing a single file.") - def index(project: str, log_level: str, timeout: float) -> None: + def index(project: str, name: str | None, language: tuple[str, ...], log_level: str, timeout: float) -> None: + # Check if project.yml exists, if not auto-create it + yml_path = os.path.join(project, ProjectConfig.rel_path_to_project_yml()) + if not os.path.exists(yml_path): + click.echo(f"Project configuration not found at {yml_path}. Auto-creating...") + try: + generated_conf = ProjectCommands._create_project(project, name, language) + click.echo(f"Generated project.yml with languages {generated_conf.languages} at {yml_path}.") + except FileExistsError: + # Race condition - file was created between check and creation + pass + except ValueError as e: + raise click.ClickException(str(e)) + ProjectCommands._index_project(project, log_level, timeout=timeout) @staticmethod diff --git a/test/serena/test_cli_project_commands.py b/test/serena/test_cli_project_commands.py new file mode 100644 index 00000000..adf64670 --- /dev/null +++ b/test/serena/test_cli_project_commands.py @@ -0,0 +1,268 @@ +"""Tests for CLI project commands (create, index).""" + +import os +import shutil +import tempfile +import time + +import pytest +from click.testing import CliRunner + +from serena.cli import ProjectCommands +from serena.config.serena_config import ProjectConfig + +pytestmark = pytest.mark.filterwarnings("ignore::UserWarning") + + +@pytest.fixture +def temp_project_dir(): + """Create a temporary directory for testing.""" + tmpdir = tempfile.mkdtemp() + try: + yield tmpdir + finally: + # if windows, wait a bit to avoid PermissionError on cleanup + if os.name == "nt": + time.sleep(0.2) + shutil.rmtree(tmpdir, ignore_errors=True) + + +@pytest.fixture +def temp_project_dir_with_python_file(): + """Create a temporary directory with a Python file for testing.""" + tmpdir = tempfile.mkdtemp() + try: + # Create a simple Python file so language detection works + py_file = os.path.join(tmpdir, "test.py") + with open(py_file, "w") as f: + f.write("def hello():\n pass\n") + yield tmpdir + finally: + # if windows, wait a bit to avoid PermissionError on cleanup + if os.name == "nt": + time.sleep(0.2) + shutil.rmtree(tmpdir, ignore_errors=True) + + +@pytest.fixture +def cli_runner(): + """Create a CliRunner for testing Click commands.""" + return CliRunner() + + +class TestProjectCreate: + """Tests for 'project create' command.""" + + def test_create_basic_with_language(self, cli_runner, temp_project_dir): + """Test basic project creation with explicit language.""" + result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"]) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + assert "python" in result.output.lower() + + # Verify project.yml was created + yml_path = os.path.join(temp_project_dir, ".serena", "project.yml") + assert os.path.exists(yml_path), f"project.yml not found at {yml_path}" + + def test_create_auto_detect_language(self, cli_runner, temp_project_dir_with_python_file): + """Test project creation with auto-detected language.""" + result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir_with_python_file]) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + assert "python" in result.output.lower() + + # Verify project.yml was created + yml_path = os.path.join(temp_project_dir_with_python_file, ".serena", "project.yml") + assert os.path.exists(yml_path) + + def test_create_with_name(self, cli_runner, temp_project_dir): + """Test project creation with custom name and explicit language.""" + result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--name", "my-custom-project", "--language", "python"]) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + + # Verify project.yml was created + yml_path = os.path.join(temp_project_dir, ".serena", "project.yml") + assert os.path.exists(yml_path) + + def test_create_with_language(self, cli_runner, temp_project_dir): + """Test project creation with specified language.""" + result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"]) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + assert "python" in result.output.lower() + + def test_create_with_multiple_languages(self, cli_runner, temp_project_dir): + """Test project creation with multiple languages.""" + result = cli_runner.invoke( + ProjectCommands.create, + [temp_project_dir, "--language", "python", "--language", "typescript"], + ) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + + def test_create_with_invalid_language(self, cli_runner, temp_project_dir): + """Test project creation with invalid language raises error.""" + result = cli_runner.invoke( + ProjectCommands.create, + [temp_project_dir, "--language", "invalid-lang"], + ) + assert result.exit_code != 0, "Should fail with invalid language" + assert "Unknown language" in result.output or "invalid-lang" in result.output + + def test_create_already_exists(self, cli_runner, temp_project_dir): + """Test that creating a project twice fails gracefully.""" + # Create once with explicit language + result1 = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"]) + assert result1.exit_code == 0 + + # Try to create again - should fail gracefully + result2 = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"]) + assert result2.exit_code != 0, "Should fail when project.yml already exists" + assert "already exists" in result2.output.lower() + assert "Error:" in result2.output # Should be user-friendly error + + def test_create_with_index_flag(self, cli_runner, temp_project_dir_with_python_file): + """Test project creation with --index flag performs indexing.""" + result = cli_runner.invoke( + ProjectCommands.create, + [temp_project_dir_with_python_file, "--language", "python", "--index", "--log-level", "ERROR", "--timeout", "5"], + ) + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Generated project.yml" in result.output + assert "Indexing project" in result.output + + # Verify project.yml was created + yml_path = os.path.join(temp_project_dir_with_python_file, ".serena", "project.yml") + assert os.path.exists(yml_path) + + # Verify cache directory was created (proof of indexing) + cache_dir = os.path.join(temp_project_dir_with_python_file, ".serena", "cache") + assert os.path.exists(cache_dir), "Cache directory should exist after indexing" + + def test_create_without_index_flag(self, cli_runner, temp_project_dir): + """Test that project creation without --index does NOT perform indexing.""" + result = cli_runner.invoke(ProjectCommands.create, [temp_project_dir, "--language", "python"]) + assert result.exit_code == 0 + assert "Generated project.yml" in result.output + assert "Indexing" not in result.output + + # Verify cache directory was NOT created + cache_dir = os.path.join(temp_project_dir, ".serena", "cache") + assert not os.path.exists(cache_dir), "Cache directory should not exist without --index" + + +class TestProjectIndex: + """Tests for 'project index' command.""" + + def test_index_auto_creates_project_with_files(self, cli_runner, temp_project_dir_with_python_file): + """Test that index command auto-creates project.yml if it doesn't exist (with source files).""" + result = cli_runner.invoke(ProjectCommands.index, [temp_project_dir_with_python_file, "--log-level", "ERROR", "--timeout", "5"]) + # Should succeed and perform indexing + assert result.exit_code == 0, f"Command failed: {result.output}" + assert "Auto-creating" in result.output or "Indexing" in result.output + + # Verify project.yml was auto-created + yml_path = os.path.join(temp_project_dir_with_python_file, ".serena", "project.yml") + assert os.path.exists(yml_path), "project.yml should be auto-created" + + def test_index_with_explicit_language(self, cli_runner, temp_project_dir): + """Test index with explicit --language for empty directory.""" + result = cli_runner.invoke( + ProjectCommands.index, + [temp_project_dir, "--language", "python", "--log-level", "ERROR", "--timeout", "5"], + ) + # Should succeed even without source files if language is explicit + assert result.exit_code == 0, f"Command failed: {result.output}" + + yml_path = os.path.join(temp_project_dir, ".serena", "project.yml") + assert os.path.exists(yml_path) + + def test_index_with_language_auto_creates(self, cli_runner, temp_project_dir): + """Test index with --language option for auto-creation.""" + result = cli_runner.invoke( + ProjectCommands.index, + [temp_project_dir, "--language", "python", "--log-level", "ERROR"], + ) + assert result.exit_code == 0 or "Indexing" in result.output + + yml_path = os.path.join(temp_project_dir, ".serena", "project.yml") + assert os.path.exists(yml_path) + + def test_index_is_equivalent_to_create_with_index(self, cli_runner, temp_project_dir_with_python_file): + """Test that 'index' behaves like 'create --index' for new projects.""" + # Use manual temp directory creation with Windows-safe cleanup + # to avoid PermissionError on Windows CI when language servers hold file locks + dir1 = tempfile.mkdtemp() + dir2 = tempfile.mkdtemp() + + try: + # Setup both directories with same file + for d in [dir1, dir2]: + with open(os.path.join(d, "test.py"), "w") as f: + f.write("def hello():\n pass\n") + + # Run 'create --index' on dir1 + result1 = cli_runner.invoke( + ProjectCommands.create, [dir1, "--language", "python", "--index", "--log-level", "ERROR", "--timeout", "5"] + ) + + # Run 'index' on dir2 + result2 = cli_runner.invoke(ProjectCommands.index, [dir2, "--language", "python", "--log-level", "ERROR", "--timeout", "5"]) + + # Both should succeed + assert result1.exit_code == 0, f"create --index failed: {result1.output}" + assert result2.exit_code == 0, f"index failed: {result2.output}" + + # Both should create project.yml + assert os.path.exists(os.path.join(dir1, ".serena", "project.yml")) + assert os.path.exists(os.path.join(dir2, ".serena", "project.yml")) + + # Both should create cache (proof of indexing) + assert os.path.exists(os.path.join(dir1, ".serena", "cache")) + assert os.path.exists(os.path.join(dir2, ".serena", "cache")) + finally: + # Windows-safe cleanup: wait for file handles to be released + if os.name == "nt": + time.sleep(0.2) + # Use ignore_errors to handle lingering file locks on Windows + shutil.rmtree(dir1, ignore_errors=True) + shutil.rmtree(dir2, ignore_errors=True) + + +class TestProjectCreateHelper: + """Tests for _create_project helper method.""" + + def test_create_project_helper_returns_config(self, temp_project_dir): + """Test that _create_project returns a ProjectConfig with explicit language.""" + config = ProjectCommands._create_project(temp_project_dir, "test-project", ("python",)) + assert isinstance(config, ProjectConfig) + assert config.project_name == "test-project" + + def test_create_project_helper_with_auto_detect(self, temp_project_dir_with_python_file): + """Test _create_project with auto-detected language.""" + config = ProjectCommands._create_project(temp_project_dir_with_python_file, "my-project", ()) + assert isinstance(config, ProjectConfig) + assert config.project_name == "my-project" + assert len(config.languages) >= 1 + + def test_create_project_helper_with_languages(self, temp_project_dir): + """Test _create_project with language specification.""" + config = ProjectCommands._create_project(temp_project_dir, None, ("python", "typescript")) + assert isinstance(config, ProjectConfig) + assert len(config.languages) >= 1 + + def test_create_project_helper_file_exists_error(self, temp_project_dir): + """Test _create_project raises error if project.yml exists.""" + # Create project first with explicit language + ProjectCommands._create_project(temp_project_dir, None, ("python",)) + + # Try to create again - should raise FileExistsError + with pytest.raises(FileExistsError): + ProjectCommands._create_project(temp_project_dir, None, ("python",)) + + +if __name__ == "__main__": + # For manual testing, you can run this file directly: + # uv run pytest test/serena/test_cli_project_commands.py -v + pytest.main([__file__, "-v"]) From 91421ce9855ba598a16ed60a6e65c642bf99c7ca Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 14 Nov 2025 11:46:03 +0700 Subject: [PATCH 08/12] Docs: fix link (jupyterbook doesn't use standard markdown references) --- docs/02-usage/040_workflow.md | 1 + docs/02-usage/050_configuration.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index fcc65824..f4589443 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -12,6 +12,7 @@ setting up a project with Serena typically involves the following steps: 3. **Onboarding**: Getting Serena familiar with the project (creating memories) 4. **Working on coding tasks**: Using Serena to help you with actual coding tasks in the project +(project-creation-indexing)= ## Project Creation & Indexing You can create a project either diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 11e363cb..341c53d7 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -22,7 +22,7 @@ Serena is configured in four places: See for a detailed explanation. You can override all entries from the `serena_config.yml` through command line arguments. 3. In the `.serena/project.yml` file within your project. This will hold project-level configuration that is used whenever that project is activated. This file will be autogenerated when you first use Serena on that project, but you can also - create it explicitly with `serena project create [options]` (see the [Project Workflow](040_workflow.md#project-creation--indexing) + create it explicitly with `serena project create [options]` (see the [](project-creation-indexing) for details on available options). 4. Through the context and modes (see below). From b5fda5674b067164dd761b733c51f254bed5b2cb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 14 Nov 2025 11:50:28 +0700 Subject: [PATCH 09/12] Docs: fixed cutoff sentence --- docs/02-usage/040_workflow.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/02-usage/040_workflow.md b/docs/02-usage/040_workflow.md index f4589443..35afb55e 100644 --- a/docs/02-usage/040_workflow.md +++ b/docs/02-usage/040_workflow.md @@ -60,7 +60,10 @@ You can either choose to do this * "Activate the project /path/to/my_project" (for first-time activation with auto-creation) * "Activate the project my_project" - Note that this option requires the `activate_project` tool to be active (which it isn't in context `ide-assistant` where t. + Note that this option requires the `activate_project` tool to be active, + which it isn't in the (default version) of context `ide-assistant` if a project is provided at startup. + (The tool is deactivated because we assume that in the ide-assistant context the user will only work on the open project and have + no need to switch it.) * when the MCP server starts, by passing the project path or name as a command-line argument (e.g. when working on a fixed project in `ide-assistant` mode): `--project ` From 568ee77e42655f049fedca123591459b84fcc9df Mon Sep 17 00:00:00 2001 From: Anatolii Prylutskyi Date: Sat, 15 Nov 2025 18:54:58 +0200 Subject: [PATCH 10/12] Fix: Expose only serena scripts in Nix package, exclude Python interpreter (#753) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Expose only serena scripts in Nix package, exclude Python interpreter Modified flake.nix to use a wrapper derivation that filters the virtual environment to expose only serena-specific executables (serena, serena-mcp-server, index-project) while excluding the Python interpreter and other venv binaries. The full virtual environment is still accessible via 'nix build .#serena-env' for development purposes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Remove deprecated commands from Nix package exports This change removes serena-mcp-server and index-project from the default Nix package outputs, exposing only the serena command. These commands are deprecated and will be removed soon, so they should not be part of the public package interface. Addresses PR review feedback requesting to expose only the serena command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- flake.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 7529365b..e311aa0d 100644 --- a/flake.nix +++ b/flake.nix @@ -76,7 +76,11 @@ formatter = pkgs.alejandra; packages = { - serena = pythonSet.mkVirtualEnv "serena" workspace.deps.default; + serena-env = pythonSet.mkVirtualEnv "serena-env" workspace.deps.default; + serena = pkgs.runCommand "serena" {} '' + mkdir -p $out/bin + ln -s ${packages.serena-env}/bin/serena $out/bin/serena + ''; default = packages.serena; }; From 4688a4877097f5df73d252256002e3a6c2afca2b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 17 Nov 2025 15:46:01 +0100 Subject: [PATCH 11/12] Use a single loop over source files to index project Resolves #765 --- src/serena/cli.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 178d643d..017f0d5a 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -529,23 +529,20 @@ class ProjectCommands(AutoRegisteringGroup): files = proj.gather_source_files() - servers = list(ls_mgr.iter_language_servers()) - for k, ls in enumerate(servers, start=1): - click.echo(f"Indexing for language {ls.language.value} ({k}/{len(servers)}) …") - collected_exceptions: list[Exception] = [] - files_failed = [] - for i, f in enumerate(tqdm(files, desc="Indexing")): - try: - ls.request_document_symbols(f, include_body=False) - ls.request_document_symbols(f, include_body=True) - except Exception as e: - log.error(f"Failed to index {f}, continuing.") - collected_exceptions.append(e) - files_failed.append(f) - if (i + 1) % 10 == 0: - ls.save_cache() - ls.save_cache() - click.echo(f"Symbols saved to {ls.cache_path}") + collected_exceptions: list[Exception] = [] + files_failed = [] + for i, f in enumerate(tqdm(files, desc="Indexing")): + try: + ls = ls_mgr.get_language_server(f) + ls.request_document_symbols(f, include_body=False) + ls.request_document_symbols(f, include_body=True) + except Exception as e: + log.error(f"Failed to index {f}, continuing.") + collected_exceptions.append(e) + files_failed.append(f) + if (i + 1) % 10 == 0: + ls_mgr.save_all_caches() + ls_mgr.save_all_caches() if len(files_failed) > 0: os.makedirs(os.path.dirname(log_file), exist_ok=True) From 98e6189ac8b8f446f0d5072d2a96e978acd3708f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 17 Nov 2025 16:20:07 +0100 Subject: [PATCH 12/12] Show counts per language after indexing --- src/serena/cli.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/serena/cli.py b/src/serena/cli.py index 017f0d5a..458822be 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -1,3 +1,4 @@ +import collections import glob import json import os @@ -11,6 +12,7 @@ from typing import Any, Literal import click from sensai.util import logging from sensai.util.logging import FileLoggerContext, datetime_tag +from sensai.util.string import dict_string from tqdm import tqdm from serena.agent import SerenaAgent @@ -531,17 +533,21 @@ class ProjectCommands(AutoRegisteringGroup): collected_exceptions: list[Exception] = [] files_failed = [] + language_file_counts = collections.defaultdict(lambda: 0) for i, f in enumerate(tqdm(files, desc="Indexing")): try: ls = ls_mgr.get_language_server(f) ls.request_document_symbols(f, include_body=False) ls.request_document_symbols(f, include_body=True) + language_file_counts[ls.language] += 1 except Exception as e: log.error(f"Failed to index {f}, continuing.") collected_exceptions.append(e) files_failed.append(f) if (i + 1) % 10 == 0: ls_mgr.save_all_caches() + language_file_counts = {k.value: v for k, v in language_file_counts.items()} + click.echo(f"Indexed files per language: {dict_string(language_file_counts, brackets=None)}") ls_mgr.save_all_caches() if len(files_failed) > 0: