From 7aab0bbb2f4a9548da6ffaa7e2e600922b4d9b07 Mon Sep 17 00:00:00 2001 From: Terrance Orletsky Date: Thu, 26 Feb 2026 11:04:01 -0500 Subject: [PATCH 1/4] Add ignore_all_dot_files config option; fix Pyright dot-directory support Adds a new project.yml option `ignore_all_dot_files` (default: true) that controls whether directories starting with "." are automatically excluded from symbol indexing. Previously, ALL dot-prefixed directories were silently ignored (including .github, .husky, etc.). Setting `ignore_all_dot_files: false` now allows Serena to index symbols within those directories. Changes: - ls_config.py: add ignore_all_dot_files field to LanguageServerConfig - ls.py: is_ignored_dirname() respects ignore_all_dot_files; .git always ignored - serena_config.py: add ignore_all_dot_files to ProjectConfig - ls_manager.py: pass ignore_all_dot_files through LanguageServerFactory - project.py: propagate ignore_all_dot_files to LanguageServerFactory - project.template.yml: document the new option - pyright_server.py: make _get_initialize_params an instance method; build exclude/include lists based on ignore_all_dot_files; handle workspace/configuration requests to reinforce include paths with Pyright --- src/serena/config/serena_config.py | 2 + src/serena/ls_manager.py | 3 + src/serena/project.py | 1 + src/serena/resources/project.template.yml | 5 ++ .../language_servers/pyright_server.py | 56 ++++++++++++++----- src/solidlsp/ls.py | 7 ++- src/solidlsp/ls_config.py | 2 + 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index fa754d8abe..3f58128777 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -274,6 +274,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes): additional_workspace_folders: list[str] = field(default_factory=list) read_only: bool = False ignore_all_files_in_gitignore: bool = True + ignore_all_dot_files: bool = True initial_prompt: str = "" encoding: str = DEFAULT_SOURCE_FILE_ENCODING @@ -500,6 +501,7 @@ def _from_dict(cls, data: dict[str, Any], local_override_keys: list[str]) -> Sel read_only_memory_patterns=data.get("read_only_memory_patterns", []), ignored_memory_patterns=data.get("ignored_memory_patterns", []), ignore_all_files_in_gitignore=data["ignore_all_files_in_gitignore"], + ignore_all_dot_files=data["ignore_all_dot_files"], initial_prompt=data["initial_prompt"], encoding=data["encoding"], line_ending=line_ending, diff --git a/src/serena/ls_manager.py b/src/serena/ls_manager.py index 11d6feab23..8441e6e205 100644 --- a/src/serena/ls_manager.py +++ b/src/serena/ls_manager.py @@ -29,6 +29,7 @@ def __init__( ls_specific_settings: dict | None = None, additional_workspace_folders: list[str] | None = None, trace_lsp_communication: bool = False, + ignore_all_dot_files: bool = True, ): self.project_root = project_root self.project_data_path = project_data_path @@ -38,6 +39,7 @@ def __init__( self.ls_specific_settings = ls_specific_settings self.additional_workspace_folders = additional_workspace_folders or [] self.trace_lsp_communication = trace_lsp_communication + self.ignore_all_dot_files = ignore_all_dot_files def create_language_server(self, language: Language) -> SolidLanguageServer: ls_config = LanguageServerConfig( @@ -45,6 +47,7 @@ def create_language_server(self, language: Language) -> SolidLanguageServer: ignored_paths=self.ignored_patterns, trace_lsp_communication=self.trace_lsp_communication, encoding=self.encoding, + ignore_all_dot_files=self.ignore_all_dot_files, ) log.info(f"Creating language server instance for {self.project_root}, language={language}.") diff --git a/src/serena/project.py b/src/serena/project.py index ae062420e8..a432855280 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -662,6 +662,7 @@ def create_language_server_manager(self) -> LanguageServerManager: ls_specific_settings=ls_specific_settings, additional_workspace_folders=self.project_config.additional_workspace_folders, trace_lsp_communication=self.serena_config.trace_lsp_communication, + ignore_all_dot_files=self.project_config.ignore_all_dot_files, ) self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory) return self.language_server_manager diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index e5c877fe03..7e07eb2816 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -51,6 +51,11 @@ language_backend: # whether to use project's .gitignore files to ignore files ignore_all_files_in_gitignore: true +# whether to ignore all directories whose name starts with a dot (e.g. .git, .venv, .config). +# Set to false if you need symbol retrieval in dot-prefixed directories (e.g. .github workflows). +# Note: .git is always ignored regardless of this setting. +ignore_all_dot_files: true + # advanced configuration option allowing to configure language server-specific options. # Maps the language key to the options. # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. diff --git a/src/solidlsp/language_servers/pyright_server.py b/src/solidlsp/language_servers/pyright_server.py index 7f774836bd..604d33c729 100644 --- a/src/solidlsp/language_servers/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_server.py @@ -57,27 +57,37 @@ def _create_launch_command(self, core_path: str) -> list[str]: def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] - @staticmethod - def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Pyright Language Server. """ + # Build exclude list based on ignore_all_dot_files setting. + # Pyright's default behavior excludes all dot-prefixed directories (**/.*). + # When ignore_all_dot_files=False, we only exclude specific known directories + # and explicitly include "." to override pyright's internal dot-directory exclusion. + exclude = [ + "**/.git", + "**/__pycache__", + "**/build", + "**/dist", + ] + if self._ignore_all_dot_files: + exclude.extend(["**/.venv", "**/.env", "**/.pixi"]) + + init_options: dict = { + "exclude": exclude, + "reportMissingImports": "error", + } + + if not self._ignore_all_dot_files: + init_options["include"] = ["."] + # Create basic initialization parameters initialize_params = { # type: ignore "processId": os.getpid(), "rootPath": repository_absolute_path, "rootUri": pathlib.Path(repository_absolute_path).as_uri(), - "initializationOptions": { - "exclude": [ - "**/__pycache__", - "**/.venv", - "**/.env", - "**/build", - "**/dist", - "**/.pixi", - ], - "reportMissingImports": "error", - }, + "initializationOptions": init_options, "capabilities": { "workspace": { "workspaceEdit": {"documentChanges": True}, @@ -210,8 +220,28 @@ def check_experimental_status(params: dict) -> None: if not self.found_source_files: self.analysis_complete.set() + def workspace_configuration_handler(params: dict) -> list: + """Handle workspace/configuration requests from pyright. + + Pyright requests python.analysis settings through this mechanism. + We use it to control include/exclude paths, particularly to allow + dot-prefixed directories when ignore_all_dot_files is False. + """ + log.info(f"Received workspace/configuration request: {params}") + items = params.get("items", []) + results = [] + for item in items: + section = item.get("section", "") + if section == "python.analysis" and not self._ignore_all_dot_files: + exclude = ["**/.git", "**/__pycache__", "**/build", "**/dist"] + results.append({"include": ["."], "exclude": exclude}) + else: + results.append({}) + return results + # Set up notification handlers self.server.on_request("client/registerCapability", do_nothing) + self.server.on_request("workspace/configuration", workspace_configuration_handler) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6bab82aaa6..1654fc5b88 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -381,7 +381,11 @@ def is_ignored_dirname(self, dirname: str) -> bool: A language-specific condition for directories that should always be ignored. For example, venv in Python and node_modules in JS/TS should be ignored always. """ - return dirname in self._ALWAYS_IGNORED_DIRS + if dirname in self._ALWAYS_IGNORED_DIRS: + return True + if self._ignore_all_dot_files and dirname.startswith("."): + return True + return False @staticmethod def _determine_log_level(line: str) -> int: @@ -506,6 +510,7 @@ def __init__( self._ls_resources_dir = self.ls_resources_dir(solidlsp_settings) log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}") self._encoding = config.encoding + self._ignore_all_dot_files = config.ignore_all_dot_files self.repository_root_path: str = repository_root_path log.debug( diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index cc062b2920..606a3e676b 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -743,6 +743,8 @@ class LanguageServerConfig: start_independent_lsp_process: bool = True ignored_paths: list[str] = field(default_factory=list) """Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries""" + ignore_all_dot_files: bool = True + """Whether to ignore all directories whose name starts with a dot (e.g. .git, .venv, .config)""" encoding: str = "utf-8" """File encoding to use when reading source files""" From 1079f0f4b810d7dfb99aa3574e67b739e579eb9f Mon Sep 17 00:00:00 2001 From: Terrance Orletsky Date: Thu, 7 May 2026 03:26:56 -0600 Subject: [PATCH 2/4] Remove .serena from _ALWAYS_IGNORED_DIRS Serena stores project memories as .md files inside .serena/. With markdown configured as a language, symbol retrieval (get_symbols_overview, find_symbol) should work on these files. Cache and state files within .serena/ are non-.md and won't match the markdown LSP's source filter. The ignore_all_dot_files flag (default: true) still protects dot-prefixed directories in general. Users who set ignore_all_dot_files: false to access .serena/ memories opt in explicitly. --- src/solidlsp/ls.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 1654fc5b88..8a455ffa74 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -354,7 +354,9 @@ class SolidLanguageServer(ABC): DOCUMENT_SYMBOL_CACHE_FILENAME = "document_symbols.pkl" # Directories that should always be ignored regardless of language: - # VCS internals, virtual environments, caches, and serena's own data. + # VCS internals, virtual environments, caches, and tool internals. + # Note: .serena is intentionally excluded — memory files stored there + # are valid targets for symbol retrieval when markdown is configured. _ALWAYS_IGNORED_DIRS = frozenset( { ".git", @@ -370,7 +372,6 @@ class SolidLanguageServer(ABC): ".tox", ".nox", # test runners ".idea", # IDE internals - ".serena", # serena's own data ".vscode", # Doesn't contain symbols } ) From e73afdd0bc1ade8d0b3d94c5b3941b21318a91ec Mon Sep 17 00:00:00 2001 From: Terrance Orletsky Date: Sun, 10 May 2026 03:01:06 -0600 Subject: [PATCH 3/4] Fix cross-package references filtered by dot-directory check is_ignored_dirname("..") was returning True because ".." starts with "." and ignore_all_dot_files defaults to True. This caused all cross-package references with ".." in their relative path to be silently dropped. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/solidlsp/ls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 8a455ffa74..6b47604dee 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -384,7 +384,7 @@ def is_ignored_dirname(self, dirname: str) -> bool: """ if dirname in self._ALWAYS_IGNORED_DIRS: return True - if self._ignore_all_dot_files and dirname.startswith("."): + if self._ignore_all_dot_files and dirname.startswith(".") and dirname not in (".", ".."): return True return False From 6151a2847f529377493d52e5c79a012604ef04ff Mon Sep 17 00:00:00 2001 From: Terrance Orletsky Date: Fri, 15 May 2026 16:18:22 -0600 Subject: [PATCH 4/4] Fix FileNotFoundError crash on broken symlinks in is_ignored_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: When ignore_all_dot_files is set to false, the directory walker enters dot-prefixed directories that were previously skipped. If those directories contain broken symlinks (e.g. .sync-reload pointing to a non-existent target via ../), the following crash sequence occurs: 1. os.listdir() returns broken symlinks as valid directory entries 2. Path().resolve().relative_to() follows the symlink, yielding a relative path to the (non-existent) target 3. is_ignored_path() calls os.path.exists() on the resolved path 4. os.path.exists() returns False for the broken target 5. FileNotFoundError is raised, crashing the entire find_symbol call This was observed in a WordPress project where sibling theme directories (atlas-iconic/, atlas-swm/) contained .sync-reload symlinks pointing to ../atlas/.sync-reload which did not exist. The error manifested as all find_symbol calls failing with: "FileNotFoundError - File .../content/themes/atlas/.sync-reload not found, the ignore check cannot be performed" Changes: - ls.py is_ignored_path(): Return True (treat as ignored) instead of raising FileNotFoundError when a path does not exist. This is the correct semantic — a non-existent path cannot contain symbols and should be skipped, not crash the tool. - project.py _is_ignored_relative_path(): Same fix. Updated docstring to reflect the new behavior (no longer raises FileNotFoundError). - ls.py request_full_symbol_tree(): Add early broken-symlink check in the directory walker (os.path.islink + not os.path.exists) before attempting Path.resolve(), preventing the resolved path from ever reaching is_ignored_path. The is_ignored_path fix is defensive (handles any caller), while the directory walker fix is preventive (stops broken symlinks at source). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/serena/project.py | 8 +++++--- src/solidlsp/ls.py | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/serena/project.py b/src/serena/project.py index a432855280..01faf39b09 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -429,8 +429,8 @@ def _ignored_patterns(self) -> list[str]: def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: """ - Determine whether an existing path should be ignored based on file type and ignore patterns. - Raises `FileNotFoundError` if the path does not exist. + Determine whether a path should be ignored based on file type and ignore patterns. + Non-existent paths (e.g. broken symlinks) are treated as ignored. :param relative_path: Relative path to check :param ignore_non_source_files: whether files that are not source files (according to the file masks @@ -446,7 +446,9 @@ def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source abs_path = os.path.join(self.project_root, relative_path) if not os.path.exists(abs_path): - raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + # Non-existent paths (e.g. broken symlinks) are treated as ignored + log.debug("Path %s does not exist (possibly a broken symlink), treating as ignored", abs_path) + return True # Check file extension if it's a file is_file = os.path.isfile(abs_path) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 6b47604dee..256c57d8ee 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1169,7 +1169,9 @@ def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = T """ abs_path = os.path.join(self.repository_root_path, relative_path) if not os.path.exists(abs_path): - raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + # Non-existent paths (e.g. broken symlinks) are treated as ignored + log.debug("Path %s does not exist (possibly a broken symlink), treating as ignored", abs_path) + return True # Check file extension if it's a file is_file = os.path.isfile(abs_path) @@ -2090,6 +2092,11 @@ def process_directory(rel_dir_path: str) -> list[ls_types.UnifiedSymbolInformati for contained_dir_or_file_name in contained_dir_or_file_names: contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name) + # Skip broken symlinks early — os.listdir returns them but they have no valid target + if os.path.islink(contained_dir_or_file_abs_path) and not os.path.exists(contained_dir_or_file_abs_path): + log.debug("Skipping broken symlink: %s", contained_dir_or_file_abs_path) + continue + # obtain relative path try: contained_dir_or_file_rel_path = str(