lsp-test/
├── code.php
└── lsp-test.php
<?php
function sendLspMessage($pipes, $message)
{
$lspMessage = "Content-Length: " . strlen($message) . "\r\n\r\n" . $message;
echo "------------------\n"; // Debugging output
echo ">>>\n$lspMessage\n"; // Debugging output
fwrite($pipes[0], $lspMessage);
fflush($pipes[0]);
// Read response
$response = '';
while (!feof($pipes[1])) {
$line = fgets($pipes[1]);
if ($line === FALSE) {
break;
}
$response .= $line;
if (strpos($response, "\r\n\r\n") !== FALSE) {
// Read the JSON content based on Content-Length
if (preg_match('/Content-Length: (\d+)/', $response, $matches)) {
$contentLength = (int)$matches[1];
$jsonStart = strpos($response, "\r\n\r\n") + 4;
$currentJsonLength = strlen($response) - $jsonStart;
while ($currentJsonLength < $contentLength) {
$chunk = fread($pipes[1], $contentLength - $currentJsonLength);
$response .= $chunk;
$currentJsonLength = strlen($response) - $jsonStart;
}
break;
}
}
}
echo "<<<\n$response\n"; // Debugging output
echo "------------------\n"; // Debugging output"
return $response;
}
function initializeLsp($pipes, $rootPath)
{
$initRequest = [
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'initialize',
'params' => [
'processId' => getmypid(),
'rootUri' => 'file://' . realpath($rootPath),
'capabilities' => [
'textDocument' => [
'synchronization' => [
'didOpen' => TRUE,
'didChange' => TRUE,
'didClose' => TRUE,
],
],
],
],
];
$jsonRequest = json_encode($initRequest);
return sendLspMessage($pipes, $jsonRequest);
}
function openDocument($pipes, $filePath)
{
$lspRequest = [
'jsonrpc' => '2.0',
'method' => 'textDocument/didOpen',
'params' => [
'textDocument' => [
'uri' => 'file://' . realpath($filePath),
'languageId' => 'php',
'version' => 1,
'text' => file_get_contents($filePath),
],
],
];
$jsonRequest = json_encode($lspRequest);
return sendLspMessage($pipes, $jsonRequest);
}
function shutdownLsp($pipes)
{
$shutdownRequest = [
'jsonrpc' => '2.0',
'id' => 2,
'method' => 'shutdown',
'params' => NULL,
];
$jsonRequest = json_encode($shutdownRequest);
return sendLspMessage($pipes, $jsonRequest);
}
function exitLsp($pipes)
{
$exitNotification = [
'jsonrpc' => '2.0',
'method' => 'exit',
'params' => NULL,
];
$jsonRequest = json_encode($exitNotification);
return sendLspMessage($pipes, $jsonRequest);
}
// ----------------------------------------------------
// Launch phpactor language server using proc_open
$descriptorspec = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$lspCmd = 'phpactor language-server';
echo "!!! Starting phpactor language server by command: {$lspCmd}\n";
$process = proc_open($lspCmd, $descriptorspec, $pipes);
if (!is_resource($process)) {
echo "Failed to start phpactor language server, please change the command or check your installation.\n";
exit(1);
}
// Initialize LSP
initializeLsp($pipes, __DIR__);
echo "!!! Initialize completed\n";
// Send initialized notification
$initializedNotification = json_encode(
[
'jsonrpc' => '2.0',
'method' => 'initialized',
'params' => [],
]
);
sendLspMessage($pipes, $initializedNotification);
echo "!!! Initialized notification sent\n";
// Open document
openDocument($pipes, __DIR__ . '/code.php');
echo "!!! Document opened\n";
// Wait for a while to ensure the server is ready and processing the document
sleep(10);
// Shutdown LSP server gracefully
shutdownLsp($pipes);
echo "!!! Shutdown request sent\n";
// Send exit notification
exitLsp($pipes);
echo "!!! Exit notification sent\n";
// Close pipes and process
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
Describe the bug
When a PHP file contains the line namespace Timer;, Phpactor spawns a language-server:diagnostics process that becomes orphaned and does not terminate as expected. This issue does not occur with other namespace names.
Environment
To Reproduce
Create a directory (e.g., lsp-test).
Add two files:
Directory structure:
code.php
Run php lsp-test.php to simulate an LSP client and trigger the issue.
Demo phpactor.mp4