Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/serena/config/serena_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,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

Expand Down Expand Up @@ -508,6 +509,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,
Expand Down
3 changes: 3 additions & 0 deletions src/serena/ls_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,13 +39,15 @@ 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(
code_language=language,
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}.")
Expand Down
8 changes: 5 additions & 3 deletions src/serena/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ 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 a path should be ignored based on file type and ignore patterns.
Returns False for non-existent paths since they cannot be matched by 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
Expand All @@ -208,8 +208,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):
log.debug(f"Path {abs_path} does not exist, skipping ignore check")
return False
# 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)
Expand Down Expand Up @@ -429,6 +430,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
Expand Down
5 changes: 5 additions & 0 deletions src/serena/resources/project.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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.
Expand Down
56 changes: 43 additions & 13 deletions src/solidlsp/language_servers/pyright_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,27 +59,37 @@ def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
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},
Expand Down Expand Up @@ -212,8 +222,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)
Expand Down
21 changes: 17 additions & 4 deletions src/solidlsp/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,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",
Expand All @@ -411,7 +413,6 @@ class SolidLanguageServer(ABC):
".tox",
".nox", # test runners
".idea", # IDE internals
".serena", # serena's own data
".vscode", # Doesn't contain symbols
}
)
Expand All @@ -422,7 +423,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(".") and dirname not in (".", ".."):
return True
return False

@staticmethod
def _determine_log_level(line: str) -> int:
Expand Down Expand Up @@ -548,6 +553,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(
Expand Down Expand Up @@ -1231,7 +1237,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)
Expand Down Expand Up @@ -2094,6 +2102,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(
Expand Down
2 changes: 2 additions & 0 deletions src/solidlsp/ls_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,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"""

Expand Down
Loading