From 3a61c999d3b06c67d3dc4d4896fdeb49337f42bc Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 9 Aug 2025 15:05:25 +0200 Subject: [PATCH] Extended CLI with helper commands for debugging --- src/serena/cli.py | 46 +++++++++++++++++++++++++++++++++++++++++++ src/serena/project.py | 18 +++++++++-------- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 384b6cac..5a724dbc 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -460,6 +460,52 @@ class ProjectCommands(AutoRegisteringGroup): ls.save_cache() print(f"Symbols saved to {ls.cache_path}") + @staticmethod + @click.command("is_ignored_path", help="Check if a path is ignored by the project configuration.") + @click.argument("path", type=click.Path(exists=False, file_okay=True, dir_okay=True)) + @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd()) + def is_ignored_path(path: str, project: str) -> None: + """ + Check if a given path is ignored by the project configuration. + + :param path: The path to check. + :param project: The path to the project directory, defaults to the current working directory. + """ + proj = Project.load(os.path.abspath(project)) + if os.path.isabs(path): + path = os.path.relpath(path, start=proj.project_root) + is_ignored = proj.is_ignored_path(path) + click.echo(f"Path '{path}' IS {'ignored' if is_ignored else 'IS NOT ignored'} by the project configuration.") + + @staticmethod + @click.command("index-file", help="Index a single file by saving its symbols to the LSP cache.") + @click.argument("file", type=click.Path(exists=True, file_okay=True, dir_okay=False)) + @click.argument("project", type=click.Path(exists=True, file_okay=False, dir_okay=True), default=os.getcwd()) + @click.option("--verbose", "-v", is_flag=True, help="Print detailed information about the indexed symbols.") + def index_file(file: str, project: str, verbose: bool) -> None: + """ + Index a single file by saving its symbols to the LSP cache, useful for debugging. + :param file: path to the file to index, must be inside the project directory. + :param project: path to the project directory, defaults to the current working directory. + :param verbose: if set, prints detailed information about the indexed symbols. + """ + proj = Project.load(os.path.abspath(project)) + if os.path.isabs(file): + file = os.path.relpath(file, start=proj.project_root) + if proj.is_ignored_path(file, ignore_non_source_files=True): + click.echo(f"'{file}' is ignored or declared as non-code file by the project configuration, won't index.") + exit(1) + ls = proj.create_language_server() + with ls.start_server(): + symbols, _ = ls.request_document_symbols(file, include_body=False) + ls.request_document_symbols(file, include_body=True) + if verbose: + click.echo(f"Symbols in file '{file}':") + for symbol in symbols: + click.echo(f" - {symbol['name']} at line {symbol['selectionRange']['start']['line']} of kind {symbol['kind']}") + ls.save_cache() + click.echo(f"Successfully indexed file '{file}', {len(symbols)} symbols saved to {ls.cache_path}.") + class ToolCommands(AutoRegisteringGroup): """Group for 'tool' subcommands.""" diff --git a/src/serena/project.py b/src/serena/project.py index d6a7ba79..117e3535 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -87,7 +87,7 @@ class Project: def _is_ignored_dirname(self, dirname: str) -> bool: return dirname.startswith(".") - def _is_ignored_relative_path(self, relative_path: str, ignore_non_source_files: bool = True) -> bool: + def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: """ Determine whether a path should be ignored based on file type and ignore patterns. @@ -111,6 +111,10 @@ class Project: # Create normalized path for consistent handling rel_path = Path(relative_path) + # always ignore paths inside .git + if len(rel_path.parts) > 0 and rel_path.parts[0] == ".git": + return True + # Check each part of the path against always fulfilled ignore conditions dir_parts = rel_path.parts if is_file: @@ -121,13 +125,15 @@ class Project: if self._is_ignored_dirname(part): return True - return match_path(relative_path, self.get_ignore_spec(), root_path=self.project_root) + return match_path(str(relative_path), self.get_ignore_spec(), root_path=self.project_root) - def is_ignored_path(self, path: str | Path) -> bool: + def is_ignored_path(self, path: str | Path, ignore_non_source_files: bool = False) -> bool: """ Checks whether the given path is ignored :param path: the path to check, can be absolute or relative + :param ignore_non_source_files: whether to ignore files that are not source files + (according to the file masks determined by the project's programming language) """ path = Path(path) if path.is_absolute(): @@ -135,11 +141,7 @@ class Project: else: relative_path = path - # always ignore paths inside .git - if len(relative_path.parts) > 0 and relative_path.parts[0] == ".git": - return True - - return match_path(str(relative_path), self.get_ignore_spec(), root_path=self.project_root) + return self._is_ignored_relative_path(str(relative_path), ignore_non_source_files=ignore_non_source_files) def is_path_in_project(self, path: str | Path) -> bool: """