Skip to content

Commit

Permalink
Upgrade LSP to support additional features
Browse files Browse the repository at this point in the history
  • Loading branch information
tm1000 committed Jan 31, 2022
1 parent a2ad69a commit 427ff3a
Show file tree
Hide file tree
Showing 14 changed files with 644 additions and 234 deletions.
6 changes: 5 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0",
"dnoegel/php-xdg-base-dir": "^0.1.1",
"felixfbecker/advanced-json-rpc": "^3.0.3",
"felixfbecker/language-server-protocol": "^1.5",
"felixfbecker/language-server-protocol": "@dev",
"netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
"nikic/php-parser": "^4.13",
"openlss/lib-array2xml": "^1.0",
Expand Down Expand Up @@ -95,6 +95,10 @@
{
"type": "path",
"url": "examples/plugins/composer-based/echo-checker"
},
{
"type": "path",
"url": "/usr/src/php-language-server-protocol"
}
],
"minimum-stability": "dev",
Expand Down
35 changes: 17 additions & 18 deletions src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Psalm\FileManipulation;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\LanguageServer\ClientConfiguration;
use Psalm\Internal\LanguageServer\LanguageServer;
use Psalm\Internal\LanguageServer\ProtocolStreamReader;
use Psalm\Internal\LanguageServer\ProtocolStreamWriter;
Expand Down Expand Up @@ -263,13 +264,6 @@ class ProjectAnalyzer
*/
public $language_server_use_extended_diagnostic_codes = false;

/**
* If this is true then the language server will send log messages to the client with additional information.
*
* @var bool
*/
public $language_server_verbose = false;

/**
* @param array<ReportOptions> $generated_report_options
*/
Expand Down Expand Up @@ -432,7 +426,8 @@ private function visitAutoloadFiles(): void
);
}

public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void
//public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void
public function server(ClientConfiguration $clientConfiguration): void
{
$this->visitAutoloadFiles();
$this->codebase->diff_methods = true;
Expand Down Expand Up @@ -464,9 +459,9 @@ public function server(?string $address = '127.0.0.1:12345', bool $socket_server

@cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');

if (!$socket_server_mode && $address) {
if (!$clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) {
// Connect to a TCP server
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
$socket = stream_socket_client('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr);
if ($socket === false) {
fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
exit(1);
Expand All @@ -475,17 +470,18 @@ public function server(?string $address = '127.0.0.1:12345', bool $socket_server
new LanguageServer(
new ProtocolStreamReader($socket),
new ProtocolStreamWriter($socket),
$this
$this,
$clientConfiguration
);
Loop::run();
} elseif ($socket_server_mode && $address) {
} elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) {
// Run a TCP Server
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
$tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr);
if ($tcpServer === false) {
fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
fwrite(STDERR, "Could not listen on {$clientConfiguration->TCPServerAddress}. Error $errno\n$errstr");
exit(1);
}
fwrite(STDOUT, "Server listening on $address\n");
fwrite(STDOUT, "Server listening on {$clientConfiguration->TCPServerAddress}\n");

$fork_available = true;
if (!extension_loaded('pcntl')) {
Expand Down Expand Up @@ -527,7 +523,8 @@ function (): void {
new LanguageServer(
$reader,
new ProtocolStreamWriter($socket),
$this
$this,
$clientConfiguration
);
// Just for safety
exit(0);
Expand All @@ -538,7 +535,8 @@ function (): void {
new LanguageServer(
new ProtocolStreamReader($socket),
new ProtocolStreamWriter($socket),
$this
$this,
$clientConfiguration
);
Loop::run();
}
Expand All @@ -549,7 +547,8 @@ function (): void {
new LanguageServer(
new ProtocolStreamReader(STDIN),
new ProtocolStreamWriter(STDOUT),
$this
$this,
$clientConfiguration
);
Loop::run();
}
Expand Down
46 changes: 34 additions & 12 deletions src/Psalm/Internal/Cli/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace Psalm\Internal\Cli;

use LanguageServerProtocol\MessageType;
use Psalm\Config;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\CliUtils;
use Psalm\Internal\Composer;
use Psalm\Internal\ErrorHandler;
use Psalm\Internal\Fork\PsalmRestarter;
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\LanguageServer\ClientConfiguration;
use Psalm\Internal\Provider\ClassLikeStorageCacheProvider;
use Psalm\Internal\Provider\FileProvider;
use Psalm\Internal\Provider\FileReferenceCacheProvider;
Expand Down Expand Up @@ -51,12 +53,15 @@
require_once __DIR__ . '/../CliUtils.php';
require_once __DIR__ . '/../Composer.php';
require_once __DIR__ . '/../IncludeCollector.php';
require_once __DIR__ . '/../LanguageServer/ClientConfiguration.php';

final class LanguageServer
{
/** @param array<int,string> $argv */
public static function run(array $argv): void
{
$clientConfiguration = new ClientConfiguration();

gc_disable();
ErrorHandler::install();
$valid_short_options = [
Expand All @@ -67,6 +72,7 @@ public static function run(array $argv): void
];

$valid_long_options = [
'no-cache',
'clear-cache',
'config:',
'find-dead-code',
Expand Down Expand Up @@ -162,6 +168,8 @@ function (string $arg) use ($valid_long_options): void {
--clear-cache
Clears all cache files that the language server uses for this specific project
--no-cache
--use-ini-defaults
Use PHP-provided ini defaults for memory and error display
Expand Down Expand Up @@ -276,14 +284,21 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class
exit;
}

$providers = new Providers(
new FileProvider,
new ParserCacheProvider($config),
new FileStorageCacheProvider($config),
new ClassLikeStorageCacheProvider($config),
new FileReferenceCacheProvider($config),
new ProjectCacheProvider(Composer::getLockFilePath($current_dir))
);
if (isset($options['no-cache']) || isset($options['i'])) {
$providers = new Providers(
new FileProvider
);
} else {
$providers = new Providers(
new FileProvider,
new ParserCacheProvider($config),
new FileStorageCacheProvider($config),
new ClassLikeStorageCacheProvider($config),
new FileReferenceCacheProvider($config),
new ProjectCacheProvider(Composer::getLockFilePath($current_dir))
);
}


$project_analyzer = new ProjectAnalyzer(
$config,
Expand All @@ -302,7 +317,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class
$project_analyzer->onchange_line_limit = (int) $options['disable-on-change'];
}

$project_analyzer->provide_completion = !isset($options['enable-autocomplete'])
$clientConfiguration->provideCompletion = !isset($options['enable-autocomplete'])
|| !is_string($options['enable-autocomplete'])
|| strtolower($options['enable-autocomplete']) !== 'false';

Expand All @@ -311,13 +326,20 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class
}

if (isset($options['use-extended-diagnostic-codes'])) {
$project_analyzer->language_server_use_extended_diagnostic_codes = true;
$clientConfiguration->VSCodeExtendedDiagnosticCodes = true;
}

if (isset($options['verbose'])) {
$project_analyzer->language_server_verbose = true;
$clientConfiguration->logLevel = $options['verbose'] ? MessageType::LOG : MessageType::INFO;
}

$project_analyzer->server($options['tcp'] ?? null, isset($options['tcp-server']));
$clientConfiguration->TCPServerAddress = $options['tcp'] ?? null;
$clientConfiguration->TCPServerMode = isset($options['tcp-server']);

//Setup Project Analyzer
$project_analyzer->provide_completion = $clientConfiguration->provideCompletion;


$project_analyzer->server($clientConfiguration);
}
}
43 changes: 12 additions & 31 deletions src/Psalm/Internal/LanguageServer/Client/TextDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use LanguageServerProtocol\TextDocumentIdentifier;
use LanguageServerProtocol\TextDocumentItem;
use Psalm\Internal\LanguageServer\ClientHandler;
use Psalm\Internal\LanguageServer\LanguageServer;

use function Amp\call;

Expand All @@ -29,51 +30,31 @@ class TextDocument
*/
private $mapper;

public function __construct(ClientHandler $handler, JsonMapper $mapper)
/**
* @var LanguageServer
*/
private $server;

public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server)
{
$this->handler = $handler;
$this->mapper = $mapper;
$this->server = $server;
}

/**
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
*
* @param Diagnostic[] $diagnostics
*/
public function publishDiagnostics(string $uri, array $diagnostics): void
public function publishDiagnostics(string $uri, array $diagnostics, ?int $version=null): void
{
$this->server->logDebug("textDocument/publishDiagnostics");

$this->handler->notify('textDocument/publishDiagnostics', [
'uri' => $uri,
'diagnostics' => $diagnostics,
'version' => $version,
]);
}

/**
* The content request is sent from a server to a client
* to request the current content of a text document identified by the URI
*
* @param TextDocumentIdentifier $textDocument The document to get the content for
*
* @return Promise<TextDocumentItem> The document's current content
*/
public function xcontent(TextDocumentIdentifier $textDocument): Promise
{
return call(
/**
* @return Generator<int, Promise<object>, object, TextDocumentItem>
*/
function () use ($textDocument) {
/** @var Promise<object> */
$promise = $this->handler->request(
'textDocument/xcontent',
['textDocument' => $textDocument]
);

$result = yield $promise;

/** @var TextDocumentItem */
return $this->mapper->map($result, new TextDocumentItem);
}
);
}
}
64 changes: 64 additions & 0 deletions src/Psalm/Internal/LanguageServer/Client/Workspace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\LanguageServer\Client;

use Amp\Promise;
use JsonMapper;
use Psalm\Internal\LanguageServer\ClientHandler;
use Psalm\Internal\LanguageServer\LanguageServer;

/**
* Provides method handlers for all textDocument/* methods
*/
class Workspace
{
/**
* @var ClientHandler
*/
private $handler;

/**
* @var JsonMapper
* @psalm-suppress UnusedProperty
*/
private $mapper;

/**
* @var LanguageServer
*/
private $server;

public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server)
{
$this->handler = $handler;
$this->mapper = $mapper;
$this->server = $server;
}

/**
* The workspace/configuration request is sent from the server to the client to
* fetch configuration settings from the client. The request can fetch several
* configuration settings in one roundtrip. The order of the returned configuration
* settings correspond to the order of the passed ConfigurationItems (e.g. the first
* item in the response is the result for the first configuration item in the params).
*
* @param string $section The configuration section asked for.
* @param string|null $scopeUri The scope to get the configuration section for.
*/
public function requestConfiguration(string $section, ?string $scopeUri = null): Promise
{
$this->server->logDebug("workspace/configuration");

/** @var Promise<object> */
return $this->handler->request('workspace/configuration', [
'items' => [
[
'section' => $section,
'scopeUri' => $scopeUri,
]
]
]);
}
}
Loading

0 comments on commit 427ff3a

Please sign in to comment.