Skip to content

Commit 3b9469a

Browse files
Merge pull request #140 from LakshyAAAgrawal/feat/php-support
Add PHP support via Intelephense
2 parents fbd5f49 + 115987b commit 3b9469a

8 files changed

Lines changed: 456 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,5 @@ src/multilspy/language_servers/clangd_language_server/static/
408408
# Virtual Environment
409409
.venv/
410410
venv/
411+
412+
src/multilspy/language_servers/intelephense/static/

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pip install multilspy
4949
| dart | Dart |
5050
| ruby | Solargraph |
5151
| kotlin | KotlinLanguageServer |
52+
| php | Intelephense |
53+
| cpp | clangd |
5254

5355

5456
## Usage
@@ -58,7 +60,7 @@ from multilspy import SyncLanguageServer
5860
from multilspy.multilspy_config import MultilspyConfig
5961
from multilspy.multilspy_logger import MultilspyLogger
6062
...
61-
config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby"
63+
config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby", "kotlin", "php"
6264
logger = MultilspyLogger()
6365
lsp = SyncLanguageServer.create(config, logger, "/abs/path/to/project/root/")
6466
with lsp.start_server():

src/multilspy/language_server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo
123123
from multilspy.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer
124124

125125
return ClangdLanguageServer(config, logger, repository_root_path)
126+
elif config.code_language == Language.PHP:
127+
from multilspy.language_servers.intelephense.intelephense import Intelephense
128+
129+
return Intelephense(config, logger, repository_root_path)
130+
126131
else:
127132
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
128133
raise MultilspyException(f"Language {config.code_language} is not supported")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"_description": "This file contains the initialization parameters for the Intelephense PHP Language Server.",
3+
"processId": "$processId",
4+
"rootPath": "$rootPath",
5+
"rootUri": "$rootUri",
6+
"capabilities": {},
7+
"workspaceFolders": [
8+
{
9+
"uri": "$uri",
10+
"name": "$name"
11+
}
12+
]
13+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
3+
"""
4+
5+
from contextlib import asynccontextmanager
6+
import logging
7+
import os
8+
import pathlib
9+
import shutil
10+
import stat
11+
import subprocess
12+
from typing import AsyncIterator
13+
from multilspy.language_server import LanguageServer
14+
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
15+
from multilspy.multilspy_config import MultilspyConfig
16+
from multilspy.multilspy_settings import MultilspySettings
17+
from multilspy.multilspy_utils import PlatformUtils, PlatformId
18+
import json
19+
20+
# Conditionally import pwd module (Unix-only)
21+
if not PlatformUtils.get_platform_id().value.startswith("win"):
22+
import pwd
23+
24+
25+
class Intelephense(LanguageServer):
26+
"""
27+
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
28+
"""
29+
30+
def __init__(self, config, logger, repository_root_path):
31+
"""
32+
Creates an Intelephense instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
33+
"""
34+
35+
executable_path = self.setup_runtime_dependencies(logger, config)
36+
super().__init__(
37+
config,
38+
logger,
39+
repository_root_path,
40+
ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path),
41+
"php",
42+
)
43+
44+
def setup_runtime_dependencies(self, logger, config: MultilspyConfig) -> str:
45+
if config.server_binary:
46+
assert os.path.exists(config.server_binary), f"Server binary not found: {config.server_binary}"
47+
return f"{config.server_binary} --stdio"
48+
49+
with open(
50+
os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r"
51+
) as f:
52+
d = json.load(f)
53+
del d["_description"]
54+
55+
runtime_dependencies = d.get("runtimeDependencies", [])
56+
php_ls_dir = config.server_install_dir or MultilspySettings.get_server_install_directory("intelephense")
57+
58+
is_node_installed = shutil.which("node") is not None
59+
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
60+
is_npm_installed = shutil.which("npm") is not None
61+
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
62+
63+
intelephense_executable_path = os.path.join(
64+
php_ls_dir, "node_modules", ".bin", "intelephense"
65+
)
66+
67+
if not os.path.exists(intelephense_executable_path):
68+
os.makedirs(php_ls_dir, exist_ok=True)
69+
for dependency in runtime_dependencies:
70+
if PlatformUtils.get_platform_id().value.startswith("win"):
71+
subprocess.run(
72+
dependency["command"],
73+
shell=True,
74+
check=True,
75+
cwd=php_ls_dir,
76+
stdout=subprocess.DEVNULL,
77+
stderr=subprocess.DEVNULL,
78+
)
79+
else:
80+
user = pwd.getpwuid(os.getuid()).pw_name
81+
subprocess.run(
82+
dependency["command"],
83+
shell=True,
84+
check=True,
85+
user=user,
86+
cwd=php_ls_dir,
87+
stdout=subprocess.DEVNULL,
88+
stderr=subprocess.DEVNULL,
89+
)
90+
91+
assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again."
92+
os.chmod(
93+
intelephense_executable_path,
94+
os.stat(intelephense_executable_path).st_mode
95+
| stat.S_IXUSR
96+
| stat.S_IXGRP
97+
| stat.S_IXOTH,
98+
)
99+
100+
return f"{intelephense_executable_path} --stdio"
101+
102+
def _get_initialize_params(self, repository_absolute_path: str):
103+
"""
104+
Returns the initialize params for the Intelephense PHP Language Server.
105+
"""
106+
with open(
107+
os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r"
108+
) as f:
109+
d = json.load(f)
110+
111+
del d["_description"]
112+
113+
d["processId"] = os.getpid()
114+
assert d["rootPath"] == "$rootPath"
115+
d["rootPath"] = repository_absolute_path
116+
117+
assert d["rootUri"] == "$rootUri"
118+
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
119+
120+
assert d["workspaceFolders"][0]["uri"] == "$uri"
121+
d["workspaceFolders"][0]["uri"] = pathlib.Path(
122+
repository_absolute_path
123+
).as_uri()
124+
125+
assert d["workspaceFolders"][0]["name"] == "$name"
126+
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
127+
128+
return d
129+
130+
@asynccontextmanager
131+
async def start_server(self) -> AsyncIterator["Intelephense"]:
132+
"""
133+
Start the language server and yield when the server is ready.
134+
"""
135+
136+
async def execute_client_command_handler(params):
137+
return []
138+
139+
async def do_nothing(params):
140+
return
141+
142+
async def window_log_message(msg):
143+
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
144+
145+
self.server.on_request("client/registerCapability", do_nothing)
146+
self.server.on_notification("language/status", do_nothing)
147+
self.server.on_notification("window/logMessage", window_log_message)
148+
self.server.on_request(
149+
"workspace/executeClientCommand", execute_client_command_handler
150+
)
151+
self.server.on_notification("$/progress", do_nothing)
152+
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
153+
self.server.on_notification("language/actionableNotification", do_nothing)
154+
155+
async with super().start_server():
156+
self.logger.log("Starting intelephense server process", logging.INFO)
157+
await self.server.start()
158+
initialize_params = self._get_initialize_params(self.repository_root_path)
159+
160+
init_response = await self.server.send.initialize(initialize_params)
161+
self.logger.log(
162+
f"Received initialize response from intelephense: {init_response}",
163+
logging.INFO,
164+
)
165+
166+
self.server.notify.initialized({})
167+
self.completions_available.set()
168+
169+
try:
170+
yield self
171+
finally:
172+
await self.server.shutdown()
173+
await self.server.stop()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"_description": "Used to download the runtime dependencies for running Intelephense PHP Language Server",
3+
"runtimeDependencies": [
4+
{
5+
"id": "intelephense",
6+
"description": "Intelephense PHP language server for Linux, macOS, and Windows. Both x64 and arm64 are supported.",
7+
"command": "npm install --prefix ./ intelephense@1.14.4"
8+
}
9+
]
10+
}

src/multilspy/multilspy_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Language(str, Enum):
2222
RUBY = "ruby"
2323
DART = "dart"
2424
CPP = "cpp"
25+
PHP = "php"
2526

2627
def __str__(self) -> str:
2728
return self.value

0 commit comments

Comments
 (0)