Merge pull request #1642 from weiconghe/feat/perl-configurable-file-filter

Perl: expose file_filter/ignore_dirs via ls_specific_settings (#1449)
This commit is contained in:
Dr. Dominik Jain authored and GitHub committed 2026-07-12 13:20:01 +02:00
commit 9bbbfea8d6
7 files changed
+341 -9

No files matched your search

+6
View File
@@ -56,6 +56,12 @@ Status of the `main` branch. Changes prior to the next official version change w
(e.g. JDTLS reporting Lombok-generated classes under `target/classes`). Missing paths are now
classified by the usual ignore rules, and symbol locations resolving to non-existent files are skipped.
- Solidity: fix diagnostics intermittently coming back empty on slow or cold environments (macOS/Windows CI).
- Perl: expose `file_filter` and `ignore_dirs` via `ls_specific_settings["perl"]`, so projects with
non-standard extensions (e.g. `.cgi`, `.psgi`) can make those files visible to Perl::LanguageServer.
Configured extensions are also synced into the Perl source-file matcher (now a cached singleton),
keeping `find_symbol` / symbol indexing consistent with the LS; the matcher is reset on every
language server activation so one project's reconfiguration does not leak into the next.
`FilenameMatcher` gains `add_extensions` / `reset` methods. Defaults are unchanged. #1449
- C/C++ (clangd): improve support and documentation for Unreal Engine 5 projects.
- HLSL (`shader-language-server`): pass `--locked` to `cargo install` when building from source
on macOS (and in the manual-install instructions), honoring the crate's packaged `Cargo.lock`.
+28
View File
@@ -871,6 +871,34 @@ Notes:
- Use the FPC compiler driver (`fpc`/`fpc.exe`), not backend compilers like `ppc386.exe`.
- These settings are passed as environment variables to the pasls process.
#### Perl
Serena uses [Perl::LanguageServer](https://metacpan.org/pod/Perl::LanguageServer) for Perl support. Install Perl and the server with `cpanm Perl::LanguageServer`; Linux and macOS only (the server does not run on Windows).
Perl::LanguageServer only indexes files whose extension is in its `perl.fileFilter` and skips directories listed in `perl.ignoreDirs`. Both are exposed below so projects with non-standard extensions (e.g. `.cgi` / `.psgi` web handlers) can make those files visible (#1449).
**Configuration:**
Configure the language server via `ls_specific_settings.perl` in `serena_config.yml`:
| Setting | Default | Description |
| -------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `file_filter` | `[".pm", ".pl", ".t"]` | File extensions (with leading dot) that Perl::LanguageServer should index, e.g. `[".pm", ".pl", ".t", ".cgi"]`. |
| `ignore_dirs` | `[".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]` | Directory names Perl::LanguageServer should skip when indexing. |
Example configuration:
```yaml
ls_specific_settings:
perl:
file_filter: [".pm", ".pl", ".t", ".cgi", ".psgi"]
ignore_dirs: [".git", "blib", "local", "vendor", "cover_db"]
```
Notes:
- Extensions added via `file_filter` are also synced into Serena's Perl source-file matcher, so `find_symbol` and symbol indexing treat the same files as the language server. Defaults are unchanged when these keys are omitted.
- The matcher is reset on every language server activation, so one project's `file_filter` does not leak into another.
#### PHP (`Intelephense`)
Serena uses Intelephense for the `php` language key.
@@ -12,7 +12,7 @@ from typing import Any
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeConfigurationParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -20,15 +20,26 @@ from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Defaults handed to Perl::LanguageServer via its `perl.fileFilter` / `perl.ignoreDirs` settings.
# Exposed for override via `ls_specific_settings["perl"]`. Extensions added via `file_filter` are
# also synced into Language.PERL.get_source_fn_matcher() (a cached singleton) so Serena's symbol
# index and the language server agree on which files are Perl sources (#1449).
_DEFAULT_FILE_FILTER: list[str] = [".pm", ".pl", ".t"]
_DEFAULT_IGNORE_DIRS: list[str] = [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]
class PerlLanguageServer(SolidLanguageServer):
"""
Provides Perl specific instantiation of the LanguageServer class using Perl::LanguageServer.
"""
# Keep in sync with Language.PERL.get_source_fn_matcher() in solidlsp/ls_config.py:
# extensions missing here are invisible to Perl::LanguageServer's project index.
_FILE_FILTER: list = [".pm", ".pl", ".t"]
You can pass the following entries in ``ls_specific_settings["perl"]``:
- file_filter: List of file extensions (with leading dot) that Perl::LanguageServer should
index, e.g. ``[".pm", ".pl", ".t", ".cgi"]``. Defaults to ``[".pm", ".pl", ".t"]``.
The same extensions are added to Serena's Perl source-file matcher, so ``find_symbol``
and symbol indexing treat them consistently (see #1449).
- ignore_dirs: Directory names Perl::LanguageServer should skip when indexing. Defaults to
``[".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"]``.
"""
@staticmethod
def _get_perl_version() -> str | None:
@@ -101,6 +112,31 @@ class PerlLanguageServer(SolidLanguageServer):
return "perl -MPerl::LanguageServer -e 'Perl::LanguageServer::run'"
@staticmethod
def _resolve_filter_settings(solidlsp_settings: SolidLSPSettings) -> tuple[list[str], list[str]]:
"""Resolve the ``fileFilter`` / ``ignoreDirs`` to hand to Perl::LanguageServer.
Reads optional overrides from ``ls_specific_settings["perl"]`` (keys ``file_filter`` and
``ignore_dirs``); falls back to the defaults otherwise. Extracted as a pure function so the
configuration plumbing can be unit-tested without starting the language server.
"""
perl_settings = solidlsp_settings.get_ls_specific_settings(Language.PERL)
file_filter = perl_settings.get("file_filter", list(_DEFAULT_FILE_FILTER))
ignore_dirs = perl_settings.get("ignore_dirs", list(_DEFAULT_IGNORE_DIRS))
return file_filter, ignore_dirs
@staticmethod
def _sync_source_fn_matcher(file_filter: list[str]) -> None:
"""Keep Serena's Perl source-file matcher in sync with the LS ``fileFilter``.
``Language.PERL.get_source_fn_matcher()`` is a ``@cache``d per-language singleton, so adding
the configured extensions here propagates to every consumer of the matcher — symbol index
traversal (``project.is_ignored_path``), the LS ignore check (``ls.is_ignored_path``), and
language composition detection. Without this, ``find_symbol`` would not surface symbols in
files whose extensions were added to ``file_filter`` (#1449).
"""
Language.PERL.get_source_fn_matcher().add_extensions(*file_filter)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
perl_ls_cmd = self._setup_runtime_dependencies()
@@ -109,6 +145,10 @@ class PerlLanguageServer(SolidLanguageServer):
config, repository_root_path, ProcessLaunchInfo(cmd=perl_ls_cmd, cwd=repository_root_path), "perl", solidlsp_settings
)
self.request_id = 0
self._file_filter, self._ignore_dirs = self._resolve_filter_settings(solidlsp_settings)
# Sync Serena's source-file matcher with the configured extensions so find_symbol and the
# language server agree on which files are Perl sources (see #1449).
self._sync_source_fn_matcher(self._file_filter)
def _create_base_initialize_params(self) -> dict:
"""
@@ -153,8 +193,8 @@ class PerlLanguageServer(SolidLanguageServer):
perl_config = {
"perlInc": [self.repository_root_path, "."],
"fileFilter": self._FILE_FILTER,
"ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
"fileFilter": self._file_filter,
"ignoreDirs": self._ignore_dirs,
}
return [perl_config]
@@ -189,8 +229,8 @@ class PerlLanguageServer(SolidLanguageServer):
"settings": {
"perl": {
"perlInc": [self.repository_root_path, "."],
"fileFilter": self._FILE_FILTER,
"ignoreDirs": [".git", ".svn", "blib", "local", ".carton", "vendor", "_build", "cover_db"],
"fileFilter": self._file_filter,
"ignoreDirs": self._ignore_dirs,
}
}
}
+5
View File
@@ -699,6 +699,11 @@ class SolidLanguageServer(ABC):
"""
identifies the language server (not to be confused with the language id passed to the language server)
"""
# The source filename matcher is a @cache'd per-language singleton. A previous project may
# have extended it (e.g. Perl's file_filter adding .cgi); reset it here so every activation
# starts from the language's default extensions, then language-server subclasses re-apply
# their own settings during the rest of __init__.
self.language.get_source_fn_matcher().reset()
self._published_diagnostics: dict[str, list[ls_types.Diagnostic]] = {}
self._published_diagnostics_generation_by_uri: dict[str, int] = {}
self._published_diagnostics_generation = 0
+33
View File
@@ -26,6 +26,38 @@ class FilenameMatcher:
"""
self._file_extensions = list(set(file_extensions)) if case_sensitive else list(set(ext.lower() for ext in file_extensions))
self._case_sensitive = case_sensitive
# Snapshot of the initial configuration, used by ``reset``. Relevant for matchers that are
# per-language singletons (``Language.get_source_fn_matcher`` is ``@cache``d): extensions added
# via ``add_extensions`` for one project must not leak into the next, so the singleton is reset
# to this snapshot at every language server initialisation.
self._initial_file_extensions = list(self._file_extensions)
def reset(self) -> None:
"""
Restore the matcher to its initial set of extensions (as provided at construction).
Undoes any extensions added via :meth:`add_extensions`. Intended for the per-language
singleton matchers, which are reset at the start of every language server initialisation so
that a previous project's reconfiguration does not leak into a newly activated one.
"""
self._file_extensions = list(self._initial_file_extensions)
def add_extensions(self, *file_extensions: str) -> None:
"""
Add further file extensions to this matcher (idempotent).
This is intended for matchers that are per-language singletons, i.e. those returned by
:meth:`Language.get_source_fn_matcher` (which is ``@cache``d): extensions that a user
configures for a language server (e.g. ``.cgi`` for Perl) can be added here so that every
consumer of the matcher — symbol index traversal, ignore checks, language composition —
treats the same set of files as sources, staying in sync with the language server.
:param file_extensions: the additional file extensions, e.g. ``.cgi``
"""
for ext in file_extensions:
norm = ext if self._case_sensitive else ext.lower()
if norm not in self._file_extensions:
self._file_extensions.append(norm)
def is_relevant_filename(self, fn: str) -> bool:
if not self._case_sensitive:
@@ -311,6 +343,7 @@ class Language(str, Enum):
"""
return self.get_ls_class().supports_implementation_request()
# NOTE: Caching results in a singleton per enum item, which is a precondition for persistent configuration of the matcher.
@cache
def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
View File
Whitespace-only changes.
+220
View File
@@ -0,0 +1,220 @@
"""Configuration plumbing for ``PerlLanguageServer``'s fileFilter / ignoreDirs.
``Perl::LanguageServer`` only indexes files whose extension is in ``perl.fileFilter`` and skips
directories listed in ``perl.ignoreDirs``; both are pushed to the LS at startup (see
``_start_server``). The defaults must stay backward-compatible, but projects with non-standard
layouts — e.g. ``.cgi`` / ``.psgi`` Perl web handlers that can dominate a mature codebase — need
a way to extend visibility (#1449).
These tests pin the configuration plumbing without starting the language server (which would
require a Perl runtime), so they run in every environment. They also cover the source-file matcher
sync that keeps ``find_symbol`` consistent with the LS (#1449).
"""
from pathlib import Path
from solidlsp.language_servers.perl_language_server import (
_DEFAULT_FILE_FILTER,
_DEFAULT_IGNORE_DIRS,
PerlLanguageServer,
)
from solidlsp.ls_config import FilenameMatcher, Language
from solidlsp.settings import SolidLSPSettings
def _settings(tmp_path: Path, ls_specific_settings: dict | None = None) -> SolidLSPSettings:
"""A SolidLSPSettings rooted under ``tmp_path`` with optional perl overrides."""
return SolidLSPSettings(
solidlsp_dir=str(tmp_path / ".solidlsp"),
ls_specific_settings=ls_specific_settings or {},
)
class TestResolveFilterSettings:
def test_defaults_when_no_ls_specific_settings(self, tmp_path: Path) -> None:
# Behaviour unchanged for projects that don't set ls_specific_settings at all.
file_filter, ignore_dirs = PerlLanguageServer._resolve_filter_settings(_settings(tmp_path))
assert file_filter == _DEFAULT_FILE_FILTER
assert ignore_dirs == _DEFAULT_IGNORE_DIRS
def test_defaults_when_perl_key_absent(self, tmp_path: Path) -> None:
# ls_specific_settings configured for another language must not leak into Perl.
settings = _settings(tmp_path, {Language.PYTHON: {"something": "unrelated"}})
file_filter, ignore_dirs = PerlLanguageServer._resolve_filter_settings(settings)
assert file_filter == _DEFAULT_FILE_FILTER
assert ignore_dirs == _DEFAULT_IGNORE_DIRS
def test_custom_file_filter_makes_extra_extensions_visible(self, tmp_path: Path) -> None:
# #1449: a Perl web backend must be able to surface .cgi / .psgi handlers.
custom = [".pm", ".pl", ".t", ".cgi", ".psgi"]
settings = _settings(tmp_path, {Language.PERL: {"file_filter": custom}})
file_filter, _ = PerlLanguageServer._resolve_filter_settings(settings)
assert file_filter == custom
assert ".cgi" in file_filter
def test_custom_ignore_dirs(self, tmp_path: Path) -> None:
custom = [".git", "blib", "local", "cover_db", "t"]
settings = _settings(tmp_path, {Language.PERL: {"ignore_dirs": custom}})
_, ignore_dirs = PerlLanguageServer._resolve_filter_settings(settings)
assert ignore_dirs == custom
def test_both_overrides_apply_independently(self, tmp_path: Path) -> None:
file_filter = [".pm", ".pl", ".cgi"]
ignore_dirs = [".git", "vendor"]
settings = _settings(
tmp_path,
{Language.PERL: {"file_filter": file_filter, "ignore_dirs": ignore_dirs}},
)
resolved_filter, resolved_dirs = PerlLanguageServer._resolve_filter_settings(settings)
assert resolved_filter == file_filter
assert resolved_dirs == ignore_dirs
def test_default_lists_are_not_mutated_across_instances(self, tmp_path: Path) -> None:
# The resolver must copy the module-level defaults, otherwise one instance mutating its
# returned list (or the LS handlers appending to self._file_filter) would corrupt every
# subsequently created default-configured instance.
file_filter, _ = PerlLanguageServer._resolve_filter_settings(_settings(tmp_path))
file_filter.append(".cgi")
next_filter, _ = PerlLanguageServer._resolve_filter_settings(_settings(tmp_path))
assert next_filter == _DEFAULT_FILE_FILTER
assert ".cgi" not in _DEFAULT_FILE_FILTER
class TestFilenameMatcherAddExtensions:
def test_adds_new_extension(self) -> None:
matcher = FilenameMatcher(".pm", ".pl")
matcher.add_extensions(".cgi")
assert matcher.is_relevant_filename("lib/Foo.pm")
assert matcher.is_relevant_filename("web/handler.cgi")
def test_is_idempotent(self) -> None:
# Re-adding an existing extension must not duplicate it; calling sync twice (e.g. across
# two PerlLanguageServer instances) must be a no-op.
matcher = FilenameMatcher(".pm", ".pl")
baseline = len(matcher._file_extensions)
matcher.add_extensions(".pm", ".pl")
matcher.add_extensions(".cgi")
matcher.add_extensions(".cgi")
assert len(matcher._file_extensions) == baseline + 1
assert matcher.is_relevant_filename("foo.cgi")
def test_respects_case_insensitivity(self) -> None:
matcher = FilenameMatcher(".pm", case_sensitive=False)
matcher.add_extensions(".CGI")
# case-insensitive matcher normalises to lower case
assert matcher.is_relevant_filename("Handler.Cgi")
assert ".cgi" in matcher._file_extensions
assert ".CGI" not in matcher._file_extensions
class TestFilenameMatcherReset:
def test_reset_undoes_added_extensions(self) -> None:
matcher = FilenameMatcher(".pm", ".pl")
matcher.add_extensions(".cgi", ".psgi")
assert matcher.is_relevant_filename("h.cgi")
matcher.reset()
assert not matcher.is_relevant_filename("h.cgi")
assert not matcher.is_relevant_filename("h.psgi")
assert matcher.is_relevant_filename("h.pm")
def test_reset_restores_original_after_case_insensitive_add(self) -> None:
matcher = FilenameMatcher(".PM", case_sensitive=False)
matcher.add_extensions(".cgi")
matcher.reset()
# original extension survives the round-trip (normalised to lower case at construction)
assert matcher._file_extensions == [".pm"]
assert not matcher.is_relevant_filename("h.cgi")
def test_reset_is_idempotent(self) -> None:
# SolidLanguageServer.__init__ calls reset on every activation; repeated resets must be safe
# and must not shrink below the original configuration.
matcher = FilenameMatcher(".pm", ".pl")
baseline = len(matcher._file_extensions)
matcher.add_extensions(".cgi")
matcher.reset()
matcher.reset()
assert len(matcher._file_extensions) == baseline
class TestSourceFnMatcherSync:
def test_custom_file_filter_extends_perl_matcher(self, tmp_path: Path) -> None:
# #1449: find_symbol relies on Language.PERL.get_source_fn_matcher(); unless the configured
# extensions are synced into it, symbols in .cgi/.psgi files stay invisible even though the
# LS indexes them. get_source_fn_matcher() is a @cache singleton, so reset() afterwards.
matcher = Language.PERL.get_source_fn_matcher()
try:
assert not matcher.is_relevant_filename("handler.cgi") # guard: not matched by default
file_filter, _ = PerlLanguageServer._resolve_filter_settings(
_settings(tmp_path, {Language.PERL: {"file_filter": [".pm", ".pl", ".t", ".cgi", ".psgi"]}})
)
PerlLanguageServer._sync_source_fn_matcher(file_filter)
assert matcher.is_relevant_filename("lib/Foo.pm")
assert matcher.is_relevant_filename("web/handler.cgi")
assert matcher.is_relevant_filename("app.psgi")
finally:
matcher.reset()
def test_default_file_filter_leaves_matcher_unchanged(self, tmp_path: Path) -> None:
# The default file_filter matches the existing Perl matcher extensions, so syncing it must
# be a no-op (no duplicate entries, no new matches).
matcher = Language.PERL.get_source_fn_matcher()
try:
initial = list(matcher._file_extensions)
file_filter, _ = PerlLanguageServer._resolve_filter_settings(_settings(tmp_path))
PerlLanguageServer._sync_source_fn_matcher(file_filter)
assert sorted(matcher._file_extensions) == sorted(initial)
finally:
matcher.reset()
def test_reset_prevents_cross_project_leak(self, tmp_path: Path) -> None:
# The matcher is a per-language singleton shared across projects. Project A reconfigures
# file_filter (adds .cgi); when project B is activated, SolidLanguageServer.__init__ resets
# the matcher first, so project B must NOT see .cgi even though project A added it.
# Mirrors the reset-then-sync ordering of PerlLanguageServer.__init__.
matcher = Language.PERL.get_source_fn_matcher()
try:
# project A: custom file_filter with .cgi
filter_a, _ = PerlLanguageServer._resolve_filter_settings(
_settings(tmp_path, {Language.PERL: {"file_filter": [".pm", ".pl", ".t", ".cgi"]}})
)
PerlLanguageServer._sync_source_fn_matcher(filter_a)
assert matcher.is_relevant_filename("handler.cgi")
# project B activates: base __init__ resets, then default config is applied (no .cgi)
matcher.reset()
filter_b, _ = PerlLanguageServer._resolve_filter_settings(_settings(tmp_path / "proj_b"))
PerlLanguageServer._sync_source_fn_matcher(filter_b)
assert not matcher.is_relevant_filename("handler.cgi"), (
"project B inherited project A's .cgi — reset did not undo the reconfiguration"
)
assert matcher.is_relevant_filename("lib/Foo.pm")
finally:
matcher.reset()