Merge pull request #119 from oraios/php-intelephense-lsp

Support for PHP, extended multi-platform testing
This commit is contained in:
Michael Panchenko
2025-05-19 16:51:55 +02:00
committed by GitHub
17 changed files with 813 additions and 417 deletions
+35 -17
View File
@@ -1,13 +1,20 @@
name: Tests on Ubuntu
name: Tests on CI
on: [pull_request]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
cpu:
runs-on: ubuntu-latest
name: Tests on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
if: "!contains(github.event.head_commit.message, 'ci skip')"
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11"]
steps:
- uses: actions/checkout@v3
@@ -15,28 +22,39 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: "${{ matrix.python-version }}"
- uses: actions/setup-go@v5
with:
go-version: '>=1.17.0'
# Add Go bin directory to PATH for this workflow
# GITHUB_PATH is a special file that GitHub Actions uses to modify PATH
# Writing to this file adds the directory to the PATH for subsequent steps
- name: Install gopls
run: |
go install golang.org/x/tools/gopls@latest
echo "$HOME/go/bin" >> $GITHUB_PATH
shell: bash
run: go install golang.org/x/tools/gopls@latest
- name: Install uv
shell: bash
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Cache uv virtualenv
id: cache-uv
uses: actions/cache@v3
with:
path: .venv
key: uv-venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('uv.lock') }}
- name: Create virtual environment
run: uv venv
shell: bash
run: |
if [ ! -d ".venv" ]; then
uv venv
fi
- name: Install dependencies
shell: bash
run: uv pip install -e ".[dev]"
- name: Test with pytest
run: uv run poe test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
shell: bash
# Currently java Language Server is not working on macos
run: |
if [ "${{ matrix.os }}" = "macos-latest" ]; then
uv run pytest -m "not java"
else
uv run poe test
fi
-2
View File
@@ -11,6 +11,4 @@
"sensai",
"vibing"
],
"basedpyright.disableLanguageServices": true,
"python.languageServer": "None"
}
+1
View File
@@ -17,6 +17,7 @@ Changes prior to the next official version change will appear here.
* Language Servers:
* Add further file extensions considered by the language servers for Python (.pyi), JavaScript (.jsx) and TypeScript (.tsx, .jsx)
* Updated multilspy, adding support for Kotlin, Dart and C/C++ and several improvements.
* Added support for PhP
# 2025-04-07
+4 -2
View File
@@ -50,14 +50,16 @@ With Serena, we provide
* Python
* Java (_Note_: startup is slow, initial startup especially so)
* TypeScript
* PhP
* Go (need to install go and gopls first)
* Rust
* C/C++
* indirect support (may require some code changes/manual installation) for:
* Ruby (untested)
* Go (untested)
* C# (untested)
* Rust (untested)
* Kotlin (untested)
* Dart (untested)
* C/C++ (untested)
These languages are supported by the language server library [multilspy](https://github.com/microsoft/multilspy), which Serena uses under the hood.
But we did not explicitly test whether the support for these languages actually works.
+1 -24
View File
@@ -51,7 +51,6 @@ dev = [
"mypy>=1.4.1",
"poethepoet>=0.20.0",
"pytest>=8.0.2",
"pytest-cov",
"ruff>=0.0.285",
"toml-sort>=0.24.2",
"types-pyyaml>=6.0.12.20241230",
@@ -116,27 +115,10 @@ lint = [
"_black_check",
"_ruff_check",
]
clean-nbs = "python docs/nbstripout.py"
format = [
"_ruff_format",
"_black_format"
]
_autogen_rst = "python docs/autogen_rst.py"
_sphinx_build = "sphinx-build -W -b html docs docs/_build"
_jb_generate_toc = "python docs/create_toc.py"
_jb_generate_config = "jupyter-book config sphinx docs/"
doc-clean = "rm -rf docs/_build"
doc-generate-files = [
"_autogen_rst",
"_jb_generate_toc",
"_jb_generate_config"
]
doc-spellcheck = "sphinx-build -W -b spelling docs docs/_build"
doc-build = [
"doc-generate-files",
"doc-spellcheck",
"_sphinx_build"
]
_mypy = "mypy src/serena"
type-check = [
"_mypy",
@@ -263,12 +245,6 @@ max-complexity = 20
"tests/**" = [
"D103"
]
"docs/**" = [
"D103"
]
"examples/**" = [
"D103"
]
"scripts/**" = [
"D103"
]
@@ -280,4 +256,5 @@ markers = [
"java: language server running for Java",
"rust: language server running for Rust",
"typescript: language server running for TypeScript",
"php: language server running for PHP",
]
+20 -14
View File
@@ -27,7 +27,7 @@ from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, searc
from . import multilspy_types
from .lsp_protocol_handler import lsp_types as LSPTypes
from .lsp_protocol_handler.lsp_constants import LSPConstants
from .lsp_protocol_handler.lsp_types import SymbolKind
from .lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind
from .lsp_protocol_handler.server import (
Error,
LanguageServerHandler,
@@ -183,6 +183,10 @@ class LanguageServer:
from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer
return ClangdLanguageServer(config, logger, repository_root_path)
elif config.code_language == Language.PHP:
from multilspy.language_servers.intelephense.intelephense import Intelephense
return Intelephense(config, logger, repository_root_path)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
raise MultilspyException(f"Language {config.code_language} is not supported")
@@ -491,6 +495,9 @@ class LanguageServer:
)
return deleted_text
async def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]:
return await self.server.send.definition(definition_params)
async def request_definition(
self, relative_file_path: str, line: int, column: int
) -> List[multilspy_types.Location]:
@@ -514,19 +521,18 @@ class LanguageServer:
with self.open_file(relative_file_path):
# sending request to the language server and waiting for response
response = await self.server.send.definition(
{
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(
str(PurePath(self.repository_root_path, relative_file_path))
).as_uri()
},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
},
}
)
definition_params = cast(DefinitionParams, {
LSPConstants.TEXT_DOCUMENT: {
LSPConstants.URI: pathlib.Path(
str(PurePath(self.repository_root_path, relative_file_path))
).as_uri()
},
LSPConstants.POSITION: {
LSPConstants.LINE: line,
LSPConstants.CHARACTER: column,
},
})
response = await self._send_definition_request(definition_params)
ret: List[multilspy_types.Location] = []
if isinstance(response, list):
@@ -0,0 +1,36 @@
{
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
"processId": "os.getpid()",
"locale": "en",
"rootPath": "$rootPath",
"rootUri": "$rootUri",
"capabilities": {
"textDocument": {
"synchronization": {
"didSave": true,
"dynamicRegistration": true
},
"completion": {
"dynamicRegistration": true,
"completionItem": {
"snippetSupport": true
}
},
"definition": {
"dynamicRegistration": true
}
},
"workspace": {
"workspaceFolders": true,
"didChangeConfiguration": {
"dynamicRegistration": true
}
}
},
"workspaceFolders": [
{
"uri": "$uri",
"name": "$name"
}
]
}
@@ -0,0 +1,203 @@
"""
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
"""
import asyncio
import json
import shutil
import logging
import os
import subprocess
import pathlib
from contextlib import asynccontextmanager
from time import sleep
from typing import AsyncIterator
from overrides import override
from multilspy.multilspy_logger import MultilspyLogger
from multilspy.language_server import LanguageServer
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_utils import PlatformUtils, PlatformId
class Intelephense(LanguageServer):
"""
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
"""
@override
def is_ignored_dirname(self, dirname: str) -> bool:
# For PHP projects, we should ignore:
# - vendor: third-party dependencies managed by Composer
# - node_modules: if the project has JavaScript components
# - cache: commonly used for caching
return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"]
def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str:
"""
Setup runtime dependencies for Intelephense.
"""
platform_id = PlatformUtils.get_platform_id()
valid_platforms = [
PlatformId.LINUX_x64,
PlatformId.LINUX_arm64,
PlatformId.OSX,
PlatformId.OSX_x64,
PlatformId.OSX_arm64,
PlatformId.WIN_x64,
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment"
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f:
d = json.load(f)
del d["_description"]
runtime_dependencies = d.get("runtimeDependencies", [])
intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp")
# Verify both node and npm are installed
is_node_installed = shutil.which('node') is not None
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
is_npm_installed = shutil.which('npm') is not None
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
# Install intelephense if not already installed
if not os.path.exists(intelephense_ls_dir):
os.makedirs(intelephense_ls_dir, exist_ok=True)
for dependency in runtime_dependencies:
# Windows doesn't support the 'user' parameter and doesn't have pwd module
if PlatformUtils.get_platform_id().value.startswith("win"):
subprocess.run(
dependency["command"],
shell=True,
check=True,
cwd=intelephense_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
# On Unix-like systems, run as non-root user
import pwd
user = pwd.getpwuid(os.getuid()).pw_name
subprocess.run(
dependency["command"],
shell=True,
check=True,
user=user,
cwd=intelephense_ls_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense")
assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again."
return f"{intelephense_executable_path} --stdio"
def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str):
# Setup runtime dependencies before initializing
intelephense_cmd = self.setup_runtime_dependencies(logger, config)
super().__init__(
config,
logger,
repository_root_path,
ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path),
"php"
)
self.server_ready = asyncio.Event()
self.request_id = 0
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the TypeScript Language Server.
"""
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f:
d = json.load(f)
del d["_description"]
d["processId"] = os.getpid()
assert d["rootPath"] == "$rootPath"
d["rootPath"] = repository_absolute_path
assert d["rootUri"] == "$rootUri"
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["uri"] == "$uri"
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
assert d["workspaceFolders"][0]["name"] == "$name"
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
return d
@asynccontextmanager
async def start_server(self) -> AsyncIterator["Intelephense"]:
"""Start Intelephense server process"""
async def register_capability_handler(params):
return
async def window_log_message(msg):
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
async def do_nothing(params):
return
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
async with super().start_server():
self.logger.log("Starting Intelephense server process", logging.INFO)
await self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
self.logger.log(
"Sending initialize request from LSP client to LSP server and awaiting response",
logging.INFO,
)
init_response = await self.server.send.initialize(initialize_params)
self.logger.log(
"After sent initialize params",
logging.INFO,
)
# Verify server capabilities
assert "textDocumentSync" in init_response["capabilities"]
assert "completionProvider" in init_response["capabilities"]
assert "definitionProvider" in init_response["capabilities"]
self.server.notify.initialized({})
self.completions_available.set()
# Intelephense server is typically ready immediately after initialization
self.server_ready.set()
await self.server_ready.wait()
yield self
await self.server.shutdown()
await self.server.stop()
@override
# For some reason, the LS may need longer to process this, so we just retry
async def _send_references_request(self, relative_file_path: str, line: int, column: int):
# TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is
# despite the LS having processed requests already. I don't know what causes this, but sleeping
# one second helps. It may be that sleeping only once is enough but that's hard to reliably test.
# May be related to the time it takes to read the files or something like that.
# The sleeping doesn't seem to be needed on all systems
sleep(1)
return await super()._send_references_request(relative_file_path, line, column)
@override
async def _send_definition_request(self, definition_params: DefinitionParams):
# TODO: same as above, also only a problem if the definition is in another file
sleep(1)
return await super()._send_definition_request(definition_params)
@@ -0,0 +1,10 @@
{
"_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense",
"runtimeDependencies": [
{
"id": "intelephense",
"description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.",
"command": "npm install --prefix ./ intelephense@1.14.4"
}
]
}
+3
View File
@@ -37,6 +37,7 @@ class Language(str, Enum):
RUBY = "ruby"
DART = "dart"
CPP = "cpp"
PHP = "php"
def __str__(self) -> str:
return self.value
@@ -65,6 +66,8 @@ class Language(str, Enum):
return FilenameMatcher("*.kt", "*.kts")
case self.DART:
return FilenameMatcher("*.dart")
case self.PHP:
return FilenameMatcher("*.php")
case _:
raise ValueError(f"Unhandled language: {self}")
+1 -1
View File
@@ -81,7 +81,7 @@ class ProjectConfig(ToStringMixin):
SERENA_DEFAULT_PROJECT_FILE = "project.yml"
@classmethod
def rel_path_to_project_yml(cls):
def rel_path_to_project_yml(cls) -> str:
return os.path.join(cls.SERENA_MANAGED_DIR, cls.SERENA_DEFAULT_PROJECT_FILE)
@classmethod
+150
View File
@@ -0,0 +1,150 @@
from pathlib import Path
import pytest
from multilspy.language_server import SyncLanguageServer
from multilspy.multilspy_config import Language
@pytest.mark.php
class TestPhpLanguageServer:
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_ls_is_running(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
"""Test that the language server starts and stops successfully."""
# The fixture already handles start and stop
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_find_definition_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
# In index.php:
# Line 9 (1-indexed): $greeting = greet($userName);
# Line 11 (1-indexed): echo $greeting;
# We want to find the definition of $greeting (defined on line 9)
# from its usage in echo $greeting; on line 11.
# LSP is 0-indexed: definition on line 8, usage on line 10.
# $greeting in echo $greeting; is at char 5 on line 11 (0-indexed: line 10, char 5)
# e c h o $ g r e e t i n g
# ^ char 5
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6) # cursor on 'g' in $greeting
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
assert len(definition_location_list) == 1
definition_location = definition_location_list[0]
assert definition_location["uri"].endswith("index.php")
# Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0
assert definition_location["range"]["start"]["line"] == 9
assert definition_location["range"]["start"]["character"] == 0
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_find_definition_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5) # helperFunction
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
assert len(definition_location_list) == 1
definition_location = definition_location_list[0]
assert definition_location["uri"].endswith("helper.php")
assert definition_location["range"]["start"]["line"] == 2
assert definition_location["range"]["start"]["character"] == 0
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_find_definition_simple_variable(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
file_path = str(repo_path / "simple_var.php")
# In simple_var.php:
# Line 2 (1-indexed): $localVar = "test";
# Line 3 (1-indexed): echo $localVar;
# LSP is 0-indexed: definition on line 1, usage on line 2
# Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5)
# $localVar in echo $localVar; (e c h o $ l o c a l V a r)
# ^ char 5
definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
assert len(definition_location_list) == 1
definition_location = definition_location_list[0]
assert definition_location["uri"].endswith("simple_var.php")
assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed)
assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed)
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_find_references_within_file(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
index_php_path = str(repo_path / "index.php")
# In index.php (0-indexed lines):
# Line 9: $greeting = greet($userName); // Definition of $greeting
# Line 11: echo $greeting; // Usage of $greeting
# Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g')
references = language_server.request_references(index_php_path, 11, 6)
assert references
# Intelephense, when asked for references from usage, seems to only return the usage itself.
assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)"
expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}] # Usage: echo $greeting (points to $)
# Convert actual references to a comparable format and sort
actual_locations = sorted(
[
{
"uri_suffix": loc["uri"].split("/")[-1],
"line": loc["range"]["start"]["line"],
"character": loc["range"]["start"]["character"],
}
for loc in references
],
key=lambda x: (x["uri_suffix"], x["line"], x["character"]),
)
expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"]))
assert actual_locations == expected_locations
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
def test_find_references_across_files(self, language_server: SyncLanguageServer, repo_path: Path) -> None:
index_php_path = str(repo_path / "index.php")
# In index.php (0-indexed lines):
# Line 13: helperFunction(); // Usage of helperFunction
# Find references for helperFunction from its usage (line 13, char 0 for 'h')
references = language_server.request_references(index_php_path, 13, 0)
assert references, f"Expected non-empty references for helperFunction but got {references=}"
# Intelephense might return 1 (usage) or 2 (usage + definition) references.
# Let's check for at least the usage in index.php
# Definition is in helper.php, line 2, char 0 (based on previous findings)
# Usage is in index.php, line 13, char 0
actual_locations_comparable = []
for loc in references:
actual_locations_comparable.append(
{
"uri_suffix": loc["uri"].split("/")[-1],
"line": loc["range"]["start"]["line"],
"character": loc["range"]["start"]["character"],
}
)
usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0}
definition_in_helper_php = {"uri_suffix": "helper.php", "line": 2, "character": 0}
assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found"
# Depending on Intelephense's behavior, it might also include the definition.
# For now, we are flexible: it must find the usage. If it finds more, that's also okay.
# If we want to be strict about finding both or only one, this part would need adjustment.
if len(references) == 2:
assert (
definition_in_helper_php in actual_locations_comparable
), "Definition of helperFunction in helper.php expected but not found when 2 references returned"
elif len(references) == 1:
# If only one reference, ensure it's the usage we definitely expect
assert actual_locations_comparable[0] == usage_in_index_php
else:
assert False, f"Expected 1 or 2 references, but got {len(references)}"
@@ -0,0 +1,7 @@
<?php
function helperFunction(): void {
echo "Helper function was called.";
}
?>
@@ -0,0 +1,16 @@
<?php
require_once 'helper.php';
function greet(string $name): string {
return "Hello, " . $name . "!";
}
$userName = "PHP User";
$greeting = greet($userName);
echo $greeting;
helperFunction();
?>
@@ -0,0 +1,4 @@
<?php
$localVar = "test";
echo $localVar;
?>
+8 -3
View File
@@ -47,10 +47,15 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent,symbol_name,def_file,ref_file",
[
(Language.PYTHON, "User", "test_repo/models.py", "test_repo/services.py"),
(Language.PYTHON, "User", os.path.join("test_repo", "models.py"), os.path.join("test_repo", "services.py")),
(Language.GO, "Helper", "main.go", "main.go"),
(Language.JAVA, "Model", "src/main/java/test_repo/Model.java", "src/main/java/test_repo/Main.java"),
(Language.RUST, "add", "src/lib.rs", "src/main.rs"),
(
Language.JAVA,
"Model",
os.path.join("src", "main", "java", "test_repo", "Model.java"),
os.path.join("src", "main", "java", "test_repo", "Main.java"),
),
(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs")),
(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts"),
],
indirect=["serena_agent"],
Generated
+314 -354
View File
File diff suppressed because it is too large Load Diff