mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
- PHP/Intelephense: expose `file_filter` via `ls_specific_settings["php"]`, so that additional extensions containing PHP sources (e.g. Drupal's `.module` / `.install` / `.inc` / `.theme`) become visible to the symbol tools and are indexed by the language server - PHP: treat `.phtml` files as PHP sources by default (all PHP language servers)
This commit is contained in:
1 parent
5c85d961a0
commit
f631a4bceb
6 files changed
+157
-2
No files matched your search
@@ -42,6 +42,12 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
it fits, otherwise truncated with a trailing '...' and a note) before falling back to bare line
|
||||
numbers, so agents can pick the right match without re-reading files. #1640
|
||||
|
||||
* Language Servers:
|
||||
- PHP: treat `.phtml` files as PHP sources by default (all PHP language servers) #1710
|
||||
- PHP/Intelephense: expose `file_filter` via `ls_specific_settings["php"]`, so that additional
|
||||
extensions containing PHP sources (e.g. Drupal's `.module` / `.install` / `.inc` / `.theme`)
|
||||
become visible to the symbol tools and are indexed by the language server #1710
|
||||
|
||||
* JetBrains:
|
||||
- Allow external files from dependencies (specified via references like "<ext:FileUtil.class|472e0a13>") to be
|
||||
- read via `ReadFileTool`
|
||||
|
||||
@@ -931,6 +931,19 @@ Supported settings:
|
||||
| `ignore_vendor` | `true` | Ignore directories named `vendor` while indexing the project. |
|
||||
| `maxFileSize` | unset | Forwarded as `intelephense.files.maxSize` in `initializationOptions`. |
|
||||
| `maxMemory` | unset | Forwarded as `intelephense.maxMemory` in `initializationOptions`. |
|
||||
| `file_filter` | unset | Additional file extensions (with leading dot) to treat as PHP sources, e.g. `[".module", ".install"]`; added to the defaults `.php` / `.phtml` (#1710). |
|
||||
|
||||
Example configuration making Drupal source files visible to the symbol tools:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
php:
|
||||
file_filter: [".module", ".install", ".inc", ".theme", ".profile", ".engine"]
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Extensions added via `file_filter` are synced into Serena's PHP source-file matcher and pushed to Intelephense as `intelephense.files.associations` globs at startup, so `find_symbol` and the language server treat the same files as PHP sources.
|
||||
- The matcher is reset on every language server activation, so one project's `file_filter` does not leak into another.
|
||||
|
||||
#### PHP (`Phpactor`)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from overrides import override
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, DidChangeConfigurationParams, LocationLink
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
from ..lsp_protocol_handler import lsp_types
|
||||
@@ -35,6 +35,11 @@ class Intelephense(SolidLanguageServer):
|
||||
- maxMemory: sets intelephense.maxMemory
|
||||
- maxFileSize: sets intelephense.files.maxSize
|
||||
- ignore_vendor: whether or ignore directories named "vendor" (default: true)
|
||||
- file_filter: list of additional file extensions (with leading dot) to treat as PHP
|
||||
sources, e.g. [".module", ".inc"]. The extensions are added to the defaults (".php",
|
||||
".phtml") of Serena's PHP source-file matcher and are pushed to Intelephense via
|
||||
files.associations, such that the symbol tools and the language server treat the
|
||||
same files as PHP sources (see #1710).
|
||||
"""
|
||||
|
||||
@override
|
||||
@@ -106,6 +111,12 @@ class Intelephense(SolidLanguageServer):
|
||||
self._ignored_dirnames.add("vendor")
|
||||
log.info(f"Ignoring the following directories for PHP projects: {', '.join(sorted(self._ignored_dirnames))}")
|
||||
|
||||
# extend the source-file matcher with any additional extensions configured via `file_filter`,
|
||||
# such that Serena's symbol tools treat the added extensions as PHP sources as well (#1710)
|
||||
file_filter = self._custom_settings.get("file_filter")
|
||||
if file_filter:
|
||||
self.language.get_source_fn_matcher().add_extensions(*file_filter)
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
|
||||
|
||||
@@ -184,6 +195,17 @@ class Intelephense(SolidLanguageServer):
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# push the file associations derived from the source-file matcher (the default extensions
|
||||
# plus any configured via `file_filter`), such that Intelephense indexes the same set of
|
||||
# files that Serena's symbol tools treat as PHP sources (#1710). This cannot be passed in
|
||||
# the initialize request: Intelephense reads configuration exclusively from
|
||||
# workspace/didChangeConfiguration (its initializationOptions only cover storagePath,
|
||||
# globalStoragePath, clearCache and licenceKey).
|
||||
associations = [f"*{ext}" for ext in self.language.get_source_fn_matcher().file_extensions]
|
||||
intelephense_config: DidChangeConfigurationParams = {"settings": {"intelephense": {"files": {"associations": associations}}}}
|
||||
log.info(f"Sending workspace/didChangeConfiguration with file associations: {associations}")
|
||||
self.server.notify.workspace_did_change_configuration(intelephense_config)
|
||||
|
||||
# Intelephense server is typically ready immediately after initialization
|
||||
# TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for!
|
||||
|
||||
|
||||
@@ -42,6 +42,13 @@ class FilenameMatcher:
|
||||
"""
|
||||
self._file_extensions = list(self._initial_file_extensions)
|
||||
|
||||
@property
|
||||
def file_extensions(self) -> list[str]:
|
||||
"""The file extensions currently registered with this matcher, including any added via
|
||||
:meth:`add_extensions`. Returned as a copy.
|
||||
"""
|
||||
return list(self._file_extensions)
|
||||
|
||||
def add_extensions(self, *file_extensions: str) -> None:
|
||||
"""
|
||||
Add further file extensions to this matcher (idempotent).
|
||||
@@ -448,7 +455,8 @@ class Language(str, Enum):
|
||||
case self.DART:
|
||||
return FilenameMatcher(".dart")
|
||||
case self.PHP | self.PHP_PHPACTOR | self.PHP_PHPANTOM:
|
||||
return FilenameMatcher(".php")
|
||||
# .phtml is a standard (yet outdated) extension for PHP sources
|
||||
return FilenameMatcher(".php", ".phtml")
|
||||
case self.R:
|
||||
return FilenameMatcher(".R", ".r", ".Rmd", ".Rnw")
|
||||
case self.PERL:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Drupal-style module file: ordinary PHP kept in a non-.php extension.
|
||||
*
|
||||
* Only visible to the symbol tools when ls_specific_settings["php"]["file_filter"] includes
|
||||
* ".module" (see #1710); inert for all tests running with default settings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function drupal_module_help($route_name) {
|
||||
return 'Help for ' . $route_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_cron().
|
||||
*/
|
||||
function drupal_module_cron() {
|
||||
helperFunction();
|
||||
}
|
||||
|
||||
class DrupalModuleController {
|
||||
public function build(): string {
|
||||
return drupal_module_help('help.page.drupal_module');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Configuration plumbing for ``Intelephense``'s ``file_filter``.
|
||||
|
||||
Serena treats only ``.php`` / ``.phtml`` files as PHP sources by default, while Drupal projects
|
||||
keep ordinary PHP in ``.module`` / ``.install`` / ``.inc`` / ``.theme`` / ``.profile`` /
|
||||
``.engine`` files, which therefore stay invisible to ``find_symbol`` and friends.
|
||||
``ls_specific_settings["php"]`` accepts a ``file_filter`` key with additional extensions,
|
||||
mirroring the Perl mechanism from #1449 / #1642 (see #1710).
|
||||
|
||||
The unit tests cover the default source-file matcher. The configuration plumbing itself lives
|
||||
inline in ``Intelephense.__init__`` (matcher sync) and ``_start_server`` (the
|
||||
``intelephense.files.associations`` push) and is exercised end-to-end by the integration test
|
||||
at the bottom, which starts a real Intelephense and is gated by the ``php`` marker like the
|
||||
rest of the PHP suite.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.ls_config import FilenameMatcher, Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
from test.conftest import get_repo_path, start_ls_context
|
||||
|
||||
|
||||
class TestPhpSourceFnMatcherDefaults:
|
||||
def test_phtml_matched_by_default_for_all_php_language_servers(self) -> None:
|
||||
# .phtml is a standard (yet outdated) PHP extension, so all PHP language servers treat it
|
||||
# as a PHP source by default (#1710).
|
||||
for language in (Language.PHP, Language.PHP_PHPACTOR, Language.PHP_PHPANTOM):
|
||||
matcher = language.get_source_fn_matcher()
|
||||
assert matcher.is_relevant_filename("index.php"), f"{language}: .php not matched"
|
||||
assert matcher.is_relevant_filename("template.phtml"), f"{language}: .phtml not matched"
|
||||
|
||||
def test_module_not_matched_by_default(self) -> None:
|
||||
# guard for the integration test below: .module files only become visible via file_filter
|
||||
assert not Language.PHP.get_source_fn_matcher().is_relevant_filename("hooks.module")
|
||||
|
||||
def test_file_extensions_property_returns_copy(self) -> None:
|
||||
# _create_base_initialize_params derives the files.associations globs from this property;
|
||||
# mutating the returned list must not affect the matcher.
|
||||
matcher = FilenameMatcher(".php", ".phtml")
|
||||
extensions = matcher.file_extensions
|
||||
extensions.append(".module")
|
||||
|
||||
assert ".module" not in matcher.file_extensions
|
||||
assert not matcher.is_relevant_filename("hooks.module")
|
||||
|
||||
|
||||
@pytest.mark.php
|
||||
class TestFileFilterIntegration:
|
||||
"""End-to-end check that a custom ``file_filter`` makes a Drupal-style file visible.
|
||||
|
||||
Starts a real Intelephense, hence gated by the ``php`` marker. The ``drupal_module.module``
|
||||
fixture stays invisible to every test that runs with default settings.
|
||||
"""
|
||||
|
||||
def test_module_file_symbols_and_references_visible(self) -> None:
|
||||
with start_ls_context(
|
||||
Language.PHP,
|
||||
ls_specific_settings={Language.PHP: {"file_filter": [".module"]}},
|
||||
) as ls:
|
||||
# Layer 2 (files.associations) must be asserted FIRST: the reference in the
|
||||
# never-opened drupal_module.module can only come from the server's association-driven
|
||||
# background index. request_full_symbol_tree below didOpens every matched file in the
|
||||
# LS, after which this assertion could pass even without the associations.
|
||||
helper_php_path = str(get_repo_path(Language.PHP) / "helper.php")
|
||||
references = ls.request_references(helper_php_path, 2, len("function "))
|
||||
assert any(ref["uri"].endswith("drupal_module.module") for ref in references), (
|
||||
f"helperFunction call in drupal_module.module not found in references: {references}"
|
||||
)
|
||||
|
||||
# Layer 1 (Serena's source matcher): the .module file takes part in symbol traversal.
|
||||
symbols = ls.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "drupal_module_help"), (
|
||||
"drupal_module_help from drupal_module.module not found in the symbol tree"
|
||||
)
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "DrupalModuleController"), (
|
||||
"DrupalModuleController from drupal_module.module not found in the symbol tree"
|
||||
)
|
||||
Reference in new issue
Block a user