mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
Switch to amp v3
This commit is contained in:
parent
f2c77ca848
commit
8859e58464
@ -24,8 +24,8 @@
|
||||
"ext-mbstring": "*",
|
||||
"ext-tokenizer": "*",
|
||||
"composer-runtime-api": "^2",
|
||||
"amphp/amp": "^2.4.2",
|
||||
"amphp/byte-stream": "^1.5",
|
||||
"amphp/amp": "^3",
|
||||
"amphp/byte-stream": "^2",
|
||||
"composer/semver": "^1.4 || ^2.0 || ^3.0",
|
||||
"composer/xdebug-handler": "^2.0 || ^3.0",
|
||||
"dnoegel/php-xdg-base-dir": "^0.1.1",
|
||||
@ -44,7 +44,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"amphp/phpunit-util": "^2.0",
|
||||
"amphp/phpunit-util": "^3",
|
||||
"bamarni/composer-bin-plugin": "^1.4",
|
||||
"brianium/paratest": "^6.9",
|
||||
"mockery/mockery": "^1.5",
|
||||
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Client;
|
||||
|
||||
use Amp\Promise;
|
||||
use JsonMapper;
|
||||
use Psalm\Internal\LanguageServer\ClientHandler;
|
||||
use Psalm\Internal\LanguageServer\LanguageServer;
|
||||
@ -42,11 +41,11 @@ class Workspace
|
||||
* @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
|
||||
public function requestConfiguration(string $section, ?string $scopeUri = null): object
|
||||
{
|
||||
$this->server->logDebug("workspace/configuration");
|
||||
|
||||
/** @var Promise<object> */
|
||||
/** @var object */
|
||||
return $this->handler->request('workspace/configuration', [
|
||||
'items' => [
|
||||
[
|
||||
|
@ -8,11 +8,7 @@ use AdvancedJsonRpc\Notification;
|
||||
use AdvancedJsonRpc\Request;
|
||||
use AdvancedJsonRpc\Response;
|
||||
use AdvancedJsonRpc\SuccessResponse;
|
||||
use Amp\Deferred;
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
|
||||
use function Amp\call;
|
||||
use Amp\DeferredFuture;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -37,24 +33,19 @@ class ClientHandler
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise<mixed> Resolved with the result of the request or rejected with an error
|
||||
* @return mixed Resolved with the result of the request or rejected with an error
|
||||
*/
|
||||
public function request(string $method, $params): Promise
|
||||
public function request(string $method, $params)
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
|
||||
return call(
|
||||
/**
|
||||
* @return Generator<int, Promise, mixed, Promise<mixed>>
|
||||
*/
|
||||
function () use ($id, $method, $params): Generator {
|
||||
yield $this->protocolWriter->write(
|
||||
$this->protocolWriter->write(
|
||||
new Message(
|
||||
new Request($id, $method, (object) $params),
|
||||
),
|
||||
);
|
||||
|
||||
$deferred = new Deferred();
|
||||
$deferred = new DeferredFuture();
|
||||
|
||||
$listener =
|
||||
function (Message $msg) use ($id, $deferred, &$listener): void {
|
||||
@ -69,17 +60,15 @@ class ClientHandler
|
||||
// Received a response
|
||||
$this->protocolReader->removeListener('message', $listener);
|
||||
if (SuccessResponse::isSuccessResponse($msg->body)) {
|
||||
$deferred->resolve($msg->body->result);
|
||||
$deferred->complete($msg->body->result);
|
||||
} else {
|
||||
$deferred->fail($msg->body->error);
|
||||
$deferred->error($msg->body->error);
|
||||
}
|
||||
}
|
||||
};
|
||||
$this->protocolReader->on('message', $listener);
|
||||
|
||||
return $deferred->promise();
|
||||
},
|
||||
);
|
||||
return $deferred->getFuture()->await();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,10 +11,6 @@ use AdvancedJsonRpc\ErrorResponse;
|
||||
use AdvancedJsonRpc\Request;
|
||||
use AdvancedJsonRpc\Response;
|
||||
use AdvancedJsonRpc\SuccessResponse;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use JsonMapper;
|
||||
use LanguageServerProtocol\ClientCapabilities;
|
||||
@ -56,10 +52,9 @@ use Psalm\Internal\Provider\ParserCacheProvider;
|
||||
use Psalm\Internal\Provider\ProjectCacheProvider;
|
||||
use Psalm\Internal\Provider\Providers;
|
||||
use Psalm\IssueBuffer;
|
||||
use Revolt\EventLoop;
|
||||
use Throwable;
|
||||
|
||||
use function Amp\asyncCoroutine;
|
||||
use function Amp\call;
|
||||
use function array_combine;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
@ -170,64 +165,52 @@ class LanguageServer extends Dispatcher
|
||||
);
|
||||
$this->protocolReader->on(
|
||||
'message',
|
||||
asyncCoroutine(
|
||||
function (Message $msg): void {
|
||||
if (!$msg->body) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore responses, this is the handler for requests and notifications
|
||||
if (Response::isResponse($msg->body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
$error = null;
|
||||
try {
|
||||
// Invoke the method handler to get a result
|
||||
$result = $this->dispatch($msg->body);
|
||||
} catch (Error $e) {
|
||||
// If a ResponseError is thrown, send it back in the Response
|
||||
$error = $e;
|
||||
} catch (Throwable $e) {
|
||||
// If an unexpected error occurred, send back an INTERNAL_ERROR error response
|
||||
$error = new Error(
|
||||
(string) $e,
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
null,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
if ($error !== null) {
|
||||
$this->logError($error->message);
|
||||
}
|
||||
// Only send a Response for a Request
|
||||
// Notifications do not send Responses
|
||||
/**
|
||||
* @return Generator<int, Promise, mixed, void>
|
||||
* @psalm-suppress UndefinedPropertyFetch
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
function (Message $msg): Generator {
|
||||
if (!$msg->body) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore responses, this is the handler for requests and notifications
|
||||
if (Response::isResponse($msg->body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = null;
|
||||
$error = null;
|
||||
try {
|
||||
// Invoke the method handler to get a result
|
||||
/**
|
||||
* @var Promise|null
|
||||
*/
|
||||
$dispatched = $this->dispatch($msg->body);
|
||||
if ($dispatched !== null) {
|
||||
$result = yield $dispatched;
|
||||
} else {
|
||||
$result = null;
|
||||
}
|
||||
} catch (Error $e) {
|
||||
// If a ResponseError is thrown, send it back in the Response
|
||||
$error = $e;
|
||||
} catch (Throwable $e) {
|
||||
// If an unexpected error occurred, send back an INTERNAL_ERROR error response
|
||||
$error = new Error(
|
||||
(string) $e,
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
null,
|
||||
$e,
|
||||
);
|
||||
}
|
||||
if (Request::isRequest($msg->body)) {
|
||||
if ($error !== null) {
|
||||
$this->logError($error->message);
|
||||
$responseBody = new ErrorResponse($msg->body->id, $error);
|
||||
} else {
|
||||
$responseBody = new SuccessResponse($msg->body->id, $result);
|
||||
}
|
||||
// Only send a Response for a Request
|
||||
// Notifications do not send Responses
|
||||
/**
|
||||
* @psalm-suppress UndefinedPropertyFetch
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
if (Request::isRequest($msg->body)) {
|
||||
if ($error !== null) {
|
||||
$responseBody = new ErrorResponse($msg->body->id, $error);
|
||||
} else {
|
||||
$responseBody = new SuccessResponse($msg->body->id, $result);
|
||||
}
|
||||
yield $this->protocolWriter->write(new Message($responseBody));
|
||||
}
|
||||
},
|
||||
),
|
||||
$this->protocolWriter->write(new Message($responseBody));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
$this->protocolReader->on(
|
||||
@ -323,7 +306,7 @@ class LanguageServer extends Dispatcher
|
||||
$clientConfiguration,
|
||||
$progress,
|
||||
);
|
||||
Loop::run();
|
||||
EventLoop::run();
|
||||
} elseif ($clientConfiguration->TCPServerMode && $clientConfiguration->TCPServerAddress) {
|
||||
// Run a TCP Server
|
||||
$tcpServer = stream_socket_server('tcp://' . $clientConfiguration->TCPServerAddress, $errno, $errstr);
|
||||
@ -346,7 +329,7 @@ class LanguageServer extends Dispatcher
|
||||
$clientConfiguration,
|
||||
$progress,
|
||||
);
|
||||
Loop::run();
|
||||
EventLoop::run();
|
||||
}
|
||||
} else {
|
||||
// Use STDIO
|
||||
@ -359,7 +342,7 @@ class LanguageServer extends Dispatcher
|
||||
$clientConfiguration,
|
||||
$progress,
|
||||
);
|
||||
Loop::run();
|
||||
EventLoop::run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,7 +360,6 @@ class LanguageServer extends Dispatcher
|
||||
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
|
||||
* @param mixed $initializationOptions
|
||||
* @param string|null $trace The initial trace setting. If omitted trace is disabled ('off').
|
||||
* @psalm-return Promise<InitializeResult>
|
||||
* @psalm-suppress PossiblyUnusedParam
|
||||
*/
|
||||
public function initialize(
|
||||
@ -390,184 +372,174 @@ class LanguageServer extends Dispatcher
|
||||
$initializationOptions = null,
|
||||
?string $trace = null
|
||||
//?array $workspaceFolders = null //error in json-dispatcher
|
||||
): Promise {
|
||||
): InitializeResult {
|
||||
$this->clientInfo = $clientInfo;
|
||||
$this->clientCapabilities = $capabilities;
|
||||
$this->trace = $trace;
|
||||
return call(
|
||||
/** @return Generator<int, true, mixed, InitializeResult> */
|
||||
function () {
|
||||
$this->logInfo("Initializing...");
|
||||
$this->clientStatus('initializing');
|
||||
|
||||
// Eventually, this might block on something. Leave it as a generator.
|
||||
/** @psalm-suppress TypeDoesNotContainType */
|
||||
if (false) {
|
||||
yield true;
|
||||
}
|
||||
$this->logInfo("Initializing...");
|
||||
$this->clientStatus('initializing');
|
||||
|
||||
$this->project_analyzer->serverMode($this);
|
||||
$this->project_analyzer->serverMode($this);
|
||||
|
||||
$this->logInfo("Initializing: Getting code base...");
|
||||
$this->clientStatus('initializing', 'getting code base');
|
||||
$this->logInfo("Initializing: Getting code base...");
|
||||
$this->clientStatus('initializing', 'getting code base');
|
||||
|
||||
$this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)...");
|
||||
$this->clientStatus('initializing', 'scanning files');
|
||||
$this->codebase->scanFiles($this->project_analyzer->threads);
|
||||
$this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)...");
|
||||
$this->clientStatus('initializing', 'scanning files');
|
||||
$this->codebase->scanFiles($this->project_analyzer->threads);
|
||||
|
||||
$this->logInfo("Initializing: Registering stub files...");
|
||||
$this->clientStatus('initializing', 'registering stub files');
|
||||
$this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress);
|
||||
$this->logInfo("Initializing: Registering stub files...");
|
||||
$this->clientStatus('initializing', 'registering stub files');
|
||||
$this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress);
|
||||
|
||||
if ($this->textDocument === null) {
|
||||
$this->textDocument = new ServerTextDocument(
|
||||
$this,
|
||||
$this->codebase,
|
||||
$this->project_analyzer,
|
||||
);
|
||||
}
|
||||
if ($this->textDocument === null) {
|
||||
$this->textDocument = new ServerTextDocument(
|
||||
$this,
|
||||
$this->codebase,
|
||||
$this->project_analyzer,
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->workspace === null) {
|
||||
$this->workspace = new ServerWorkspace(
|
||||
$this,
|
||||
$this->codebase,
|
||||
$this->project_analyzer,
|
||||
);
|
||||
}
|
||||
if ($this->workspace === null) {
|
||||
$this->workspace = new ServerWorkspace(
|
||||
$this,
|
||||
$this->codebase,
|
||||
$this->project_analyzer,
|
||||
);
|
||||
}
|
||||
|
||||
$serverCapabilities = new ServerCapabilities();
|
||||
$serverCapabilities = new ServerCapabilities();
|
||||
|
||||
//The server provides execute command support.
|
||||
$serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']);
|
||||
//The server provides execute command support.
|
||||
$serverCapabilities->executeCommandProvider = new ExecuteCommandOptions(['test']);
|
||||
|
||||
$textDocumentSyncOptions = new TextDocumentSyncOptions();
|
||||
$textDocumentSyncOptions = new TextDocumentSyncOptions();
|
||||
|
||||
//Open and close notifications are sent to the server.
|
||||
$textDocumentSyncOptions->openClose = true;
|
||||
//Open and close notifications are sent to the server.
|
||||
$textDocumentSyncOptions->openClose = true;
|
||||
|
||||
$saveOptions = new SaveOptions();
|
||||
//The client is supposed to include the content on save.
|
||||
$saveOptions->includeText = true;
|
||||
$textDocumentSyncOptions->save = $saveOptions;
|
||||
$saveOptions = new SaveOptions();
|
||||
//The client is supposed to include the content on save.
|
||||
$saveOptions->includeText = true;
|
||||
$textDocumentSyncOptions->save = $saveOptions;
|
||||
|
||||
/**
|
||||
* Change notifications are sent to the server. See
|
||||
* TextDocumentSyncKind.None, TextDocumentSyncKind.Full and
|
||||
* TextDocumentSyncKind.Incremental. If omitted it defaults to
|
||||
* TextDocumentSyncKind.None.
|
||||
*/
|
||||
if ($this->project_analyzer->onchange_line_limit === 0) {
|
||||
/**
|
||||
* Documents should not be synced at all.
|
||||
*/
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::NONE;
|
||||
} else {
|
||||
/**
|
||||
* Documents are synced by always sending the full content
|
||||
* of the document.
|
||||
*/
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::FULL;
|
||||
}
|
||||
/**
|
||||
* Change notifications are sent to the server. See
|
||||
* TextDocumentSyncKind.None, TextDocumentSyncKind.Full and
|
||||
* TextDocumentSyncKind.Incremental. If omitted it defaults to
|
||||
* TextDocumentSyncKind.None.
|
||||
*/
|
||||
if ($this->project_analyzer->onchange_line_limit === 0) {
|
||||
/**
|
||||
* Documents should not be synced at all.
|
||||
*/
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::NONE;
|
||||
} else {
|
||||
/**
|
||||
* Documents are synced by always sending the full content
|
||||
* of the document.
|
||||
*/
|
||||
$textDocumentSyncOptions->change = TextDocumentSyncKind::FULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines how text documents are synced. Is either a detailed structure
|
||||
* defining each notification or for backwards compatibility the
|
||||
* TextDocumentSyncKind number. If omitted it defaults to
|
||||
* `TextDocumentSyncKind.None`.
|
||||
*/
|
||||
$serverCapabilities->textDocumentSync = $textDocumentSyncOptions;
|
||||
/**
|
||||
* Defines how text documents are synced. Is either a detailed structure
|
||||
* defining each notification or for backwards compatibility the
|
||||
* TextDocumentSyncKind number. If omitted it defaults to
|
||||
* `TextDocumentSyncKind.None`.
|
||||
*/
|
||||
$serverCapabilities->textDocumentSync = $textDocumentSyncOptions;
|
||||
|
||||
/**
|
||||
* The server provides document symbol support.
|
||||
* Support "Find all symbols"
|
||||
*/
|
||||
$serverCapabilities->documentSymbolProvider = false;
|
||||
/**
|
||||
* The server provides workspace symbol support.
|
||||
* Support "Find all symbols in workspace"
|
||||
*/
|
||||
$serverCapabilities->workspaceSymbolProvider = false;
|
||||
/**
|
||||
* The server provides goto definition support.
|
||||
* Support "Go to definition"
|
||||
*/
|
||||
$serverCapabilities->definitionProvider = true;
|
||||
/**
|
||||
* The server provides find references support.
|
||||
* Support "Find all references"
|
||||
*/
|
||||
$serverCapabilities->referencesProvider = false;
|
||||
/**
|
||||
* The server provides hover support.
|
||||
* Support "Hover"
|
||||
*/
|
||||
$serverCapabilities->hoverProvider = true;
|
||||
/**
|
||||
* The server provides document symbol support.
|
||||
* Support "Find all symbols"
|
||||
*/
|
||||
$serverCapabilities->documentSymbolProvider = false;
|
||||
/**
|
||||
* The server provides workspace symbol support.
|
||||
* Support "Find all symbols in workspace"
|
||||
*/
|
||||
$serverCapabilities->workspaceSymbolProvider = false;
|
||||
/**
|
||||
* The server provides goto definition support.
|
||||
* Support "Go to definition"
|
||||
*/
|
||||
$serverCapabilities->definitionProvider = true;
|
||||
/**
|
||||
* The server provides find references support.
|
||||
* Support "Find all references"
|
||||
*/
|
||||
$serverCapabilities->referencesProvider = false;
|
||||
/**
|
||||
* The server provides hover support.
|
||||
* Support "Hover"
|
||||
*/
|
||||
$serverCapabilities->hoverProvider = true;
|
||||
|
||||
/**
|
||||
* The server provides completion support.
|
||||
* Support "Completion"
|
||||
*/
|
||||
if ($this->project_analyzer->provide_completion) {
|
||||
$serverCapabilities->completionProvider = new CompletionOptions();
|
||||
/**
|
||||
* The server provides support to resolve additional
|
||||
* information for a completion item.
|
||||
*/
|
||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||
/**
|
||||
* Most tools trigger completion request automatically without explicitly
|
||||
* requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they
|
||||
* do so when the user starts to type an identifier. For example if the user
|
||||
* types `c` in a JavaScript file code complete will automatically pop up
|
||||
* present `console` besides others as a completion item. Characters that
|
||||
* make up identifiers don't need to be listed here.
|
||||
*
|
||||
* If code complete should automatically be trigger on characters not being
|
||||
* valid inside an identifier (for example `.` in JavaScript) list them in
|
||||
* `triggerCharacters`.
|
||||
*/
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "];
|
||||
}
|
||||
/**
|
||||
* The server provides completion support.
|
||||
* Support "Completion"
|
||||
*/
|
||||
if ($this->project_analyzer->provide_completion) {
|
||||
$serverCapabilities->completionProvider = new CompletionOptions();
|
||||
/**
|
||||
* The server provides support to resolve additional
|
||||
* information for a completion item.
|
||||
*/
|
||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||
/**
|
||||
* Most tools trigger completion request automatically without explicitly
|
||||
* requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they
|
||||
* do so when the user starts to type an identifier. For example if the user
|
||||
* types `c` in a JavaScript file code complete will automatically pop up
|
||||
* present `console` besides others as a completion item. Characters that
|
||||
* make up identifiers don't need to be listed here.
|
||||
*
|
||||
* If code complete should automatically be trigger on characters not being
|
||||
* valid inside an identifier (for example `.` in JavaScript) list them in
|
||||
* `triggerCharacters`.
|
||||
*/
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>', ':',"[", "(", ",", " "];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether code action supports the `data` property which is
|
||||
* preserved between a `textDocument/codeAction` and a
|
||||
* `codeAction/resolve` request.
|
||||
*
|
||||
* Support "Code Actions" if we support data
|
||||
*
|
||||
* @since LSP 3.16.0
|
||||
*/
|
||||
if ($this->clientCapabilities->textDocument->publishDiagnostics->dataSupport ?? false) {
|
||||
$serverCapabilities->codeActionProvider = true;
|
||||
}
|
||||
/**
|
||||
* Whether code action supports the `data` property which is
|
||||
* preserved between a `textDocument/codeAction` and a
|
||||
* `codeAction/resolve` request.
|
||||
*
|
||||
* Support "Code Actions" if we support data
|
||||
*
|
||||
* @since LSP 3.16.0
|
||||
*/
|
||||
if ($this->clientCapabilities->textDocument->publishDiagnostics->dataSupport ?? false) {
|
||||
$serverCapabilities->codeActionProvider = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server provides signature help support.
|
||||
*/
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']);
|
||||
/**
|
||||
* The server provides signature help support.
|
||||
*/
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions(['(', ',']);
|
||||
|
||||
if ($this->client->clientConfiguration->baseline !== null) {
|
||||
$this->logInfo('Utilizing Baseline: '.$this->client->clientConfiguration->baseline);
|
||||
$this->issue_baseline= ErrorBaseline::read(
|
||||
new FileProvider,
|
||||
$this->client->clientConfiguration->baseline,
|
||||
);
|
||||
}
|
||||
if ($this->client->clientConfiguration->baseline !== null) {
|
||||
$this->logInfo('Utilizing Baseline: '.$this->client->clientConfiguration->baseline);
|
||||
$this->issue_baseline= ErrorBaseline::read(
|
||||
new FileProvider,
|
||||
$this->client->clientConfiguration->baseline,
|
||||
);
|
||||
}
|
||||
|
||||
$this->logInfo("Initializing: Complete.");
|
||||
$this->clientStatus('initialized');
|
||||
$this->logInfo("Initializing: Complete.");
|
||||
$this->clientStatus('initialized');
|
||||
|
||||
/**
|
||||
* Information about the server.
|
||||
*
|
||||
* @since LSP 3.15.0
|
||||
*/
|
||||
$initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION);
|
||||
/**
|
||||
* Information about the server.
|
||||
*
|
||||
* @since LSP 3.15.0
|
||||
*/
|
||||
$initializeResultServerInfo = new InitializeResultServerInfo('Psalm Language Server', PSALM_VERSION);
|
||||
|
||||
return new InitializeResult($serverCapabilities, $initializeResultServerInfo);
|
||||
},
|
||||
);
|
||||
return new InitializeResult($serverCapabilities, $initializeResultServerInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -647,12 +619,12 @@ class LanguageServer extends Dispatcher
|
||||
*/
|
||||
public function doVersionedAnalysisDebounce(array $files, ?int $version = null): void
|
||||
{
|
||||
Loop::cancel($this->versionedAnalysisDelayToken);
|
||||
EventLoop::cancel($this->versionedAnalysisDelayToken);
|
||||
if ($this->client->clientConfiguration->onChangeDebounceMs === null) {
|
||||
$this->doVersionedAnalysis($files, $version);
|
||||
} else {
|
||||
/** @psalm-suppress MixedAssignment,UnusedPsalmSuppress */
|
||||
$this->versionedAnalysisDelayToken = Loop::delay(
|
||||
$this->versionedAnalysisDelayToken = EventLoop::delay(
|
||||
$this->client->clientConfiguration->onChangeDebounceMs,
|
||||
fn() => $this->doVersionedAnalysis($files, $version),
|
||||
);
|
||||
@ -666,7 +638,7 @@ class LanguageServer extends Dispatcher
|
||||
*/
|
||||
public function doVersionedAnalysis(array $files, ?int $version = null): void
|
||||
{
|
||||
Loop::cancel($this->versionedAnalysisDelayToken);
|
||||
EventLoop::cancel($this->versionedAnalysisDelayToken);
|
||||
try {
|
||||
$this->logDebug("Doing Analysis from version: $version");
|
||||
$this->codebase->reloadFiles(
|
||||
@ -819,7 +791,7 @@ class LanguageServer extends Dispatcher
|
||||
* which they have sent a shutdown request. Clients should also wait with sending the exit notification until they
|
||||
* have received a response from the shutdown request.
|
||||
*/
|
||||
public function shutdown(): Promise
|
||||
public function shutdown(): void
|
||||
{
|
||||
$this->clientStatus('closing');
|
||||
$this->logInfo("Shutting down...");
|
||||
@ -830,7 +802,6 @@ class LanguageServer extends Dispatcher
|
||||
$scanned_files,
|
||||
);
|
||||
$this->clientStatus('closed');
|
||||
return new Success(null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -5,12 +5,10 @@ declare(strict_types=1);
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
use Amp\ByteStream\ResourceInputStream;
|
||||
use Amp\Promise;
|
||||
use Amp\ByteStream\ReadableResourceStream;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use Revolt\EventLoop;
|
||||
|
||||
use function Amp\asyncCall;
|
||||
use function explode;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
@ -45,16 +43,11 @@ class ProtocolStreamReader implements ProtocolReader
|
||||
*/
|
||||
public function __construct($input)
|
||||
{
|
||||
$input = new ResourceInputStream($input);
|
||||
asyncCall(
|
||||
/**
|
||||
* @return Generator<int, Promise<?string>, ?string, void>
|
||||
*/
|
||||
function () use ($input): Generator {
|
||||
$input = new ReadableResourceStream($input);
|
||||
EventLoop::queue(
|
||||
function () use ($input): void {
|
||||
while ($this->is_accepting_new_requests) {
|
||||
$read_promise = $input->read();
|
||||
|
||||
$chunk = yield $read_promise;
|
||||
$chunk = $input->read();
|
||||
|
||||
if ($chunk === null) {
|
||||
break;
|
||||
|
@ -4,29 +4,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use Amp\ByteStream\ResourceOutputStream;
|
||||
use Amp\Promise;
|
||||
use Amp\ByteStream\WritableResourceStream;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ProtocolStreamWriter implements ProtocolWriter
|
||||
{
|
||||
private ResourceOutputStream $output;
|
||||
private WritableResourceStream $output;
|
||||
|
||||
/**
|
||||
* @param resource $output
|
||||
*/
|
||||
public function __construct($output)
|
||||
{
|
||||
$this->output = new ResourceOutputStream($output);
|
||||
$this->output = new WritableResourceStream($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write(Message $msg): Promise
|
||||
public function write(Message $msg): void
|
||||
{
|
||||
return $this->output->write((string)$msg);
|
||||
$this->output->write((string)$msg);
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer;
|
||||
|
||||
use Amp\Promise;
|
||||
|
||||
interface ProtocolWriter
|
||||
{
|
||||
/**
|
||||
* Sends a Message to the client
|
||||
*
|
||||
* @return Promise Resolved when the message has been fully written out to the output stream
|
||||
* Sends a Message to the client.
|
||||
*/
|
||||
public function write(Message $msg): Promise;
|
||||
public function write(Message $msg): void;
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Server;
|
||||
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use LanguageServerProtocol\CodeAction;
|
||||
use LanguageServerProtocol\CodeActionContext;
|
||||
use LanguageServerProtocol\CodeActionKind;
|
||||
@ -166,12 +164,11 @@ class TextDocument
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The text document
|
||||
* @param Position $position The position inside the text document
|
||||
* @psalm-return Promise<Location>|Promise<null>
|
||||
*/
|
||||
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
public function definition(TextDocumentIdentifier $textDocument, Position $position): ?Location
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideDefinition) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
@ -182,34 +179,32 @@ class TextDocument
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($reference === null) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
$code_location = $this->codebase->getSymbolLocationByReference($reference);
|
||||
|
||||
if (!$code_location) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Success(
|
||||
new Location(
|
||||
LanguageServer::pathToUri($code_location->file_path),
|
||||
new Range(
|
||||
new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1),
|
||||
new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1),
|
||||
),
|
||||
return new Location(
|
||||
LanguageServer::pathToUri($code_location->file_path),
|
||||
new Range(
|
||||
new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1),
|
||||
new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -220,12 +215,11 @@ class TextDocument
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The text document
|
||||
* @param Position $position The position inside the text document
|
||||
* @psalm-return Promise<Hover>|Promise<null>
|
||||
*/
|
||||
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
public function hover(TextDocumentIdentifier $textDocument, Position $position): ?Hover
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideHover) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
@ -236,32 +230,32 @@ class TextDocument
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$reference = $this->codebase->getReferenceAtPositionAsReference($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($reference === null) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$markup = $this->codebase->getMarkupContentForSymbolByReference($reference);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($markup === null) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Success(new Hover($markup, $reference->range));
|
||||
return new Hover($markup, $reference->range);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -276,12 +270,11 @@ class TextDocument
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The text document
|
||||
* @param Position $position The position
|
||||
* @psalm-return Promise<array<empty, empty>>|Promise<CompletionList>|Promise<null>
|
||||
*/
|
||||
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
public function completion(TextDocumentIdentifier $textDocument, Position $position): ?CompletionList
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideCompletion) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
@ -292,7 +285,7 @@ class TextDocument
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -314,42 +307,42 @@ class TextDocument
|
||||
$file_path,
|
||||
);
|
||||
}
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
return new CompletionList($completion_items, false);
|
||||
}
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$type_context = $this->codebase->getTypeContextAtPosition($file_path, $position);
|
||||
if ($type_context) {
|
||||
$completion_items = $this->codebase->getCompletionItemsForType($type_context);
|
||||
return new Success(new CompletionList($completion_items, false));
|
||||
return new CompletionList($completion_items, false);
|
||||
}
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
} catch (TypeParseTreeException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logError('completion not found at ' . $position->line . ':' . $position->character);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signature help request is sent from the client to the server to request signature
|
||||
* information at a given cursor position.
|
||||
*/
|
||||
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
|
||||
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): ?SignatureHelp
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideSignatureHelp) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
@ -360,37 +353,35 @@ class TextDocument
|
||||
|
||||
//This currently doesnt work right with out of project files
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$argument_location = $this->codebase->getFunctionArgumentAtPosition($file_path, $position);
|
||||
} catch (UnanalyzedFileException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($argument_location === null) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$signature_information = $this->codebase->getSignatureInformation($argument_location[0], $file_path);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$this->server->logThrowable($e);
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$signature_information) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Success(
|
||||
new SignatureHelp(
|
||||
[$signature_information],
|
||||
0,
|
||||
$argument_location[1],
|
||||
),
|
||||
return new SignatureHelp(
|
||||
[$signature_information],
|
||||
0,
|
||||
$argument_location[1],
|
||||
);
|
||||
}
|
||||
|
||||
@ -401,10 +392,10 @@ class TextDocument
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedParam
|
||||
*/
|
||||
public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise
|
||||
public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): ?array
|
||||
{
|
||||
if (!$this->server->client->clientConfiguration->provideCodeActions) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->server->logDebug(
|
||||
@ -415,7 +406,7 @@ class TextDocument
|
||||
|
||||
//Don't report code actions for files we arent watching
|
||||
if (!$this->codebase->config->isInProjectDirs($file_path)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
$fixers = [];
|
||||
@ -479,11 +470,9 @@ class TextDocument
|
||||
}
|
||||
|
||||
if (empty($fixers)) {
|
||||
return new Success(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Success(
|
||||
array_values($fixers),
|
||||
);
|
||||
return array_values($fixers);
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\LanguageServer\Server;
|
||||
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use InvalidArgumentException;
|
||||
use LanguageServerProtocol\FileChangeType;
|
||||
use LanguageServerProtocol\FileEvent;
|
||||
@ -126,7 +124,7 @@ class Workspace
|
||||
* @param mixed $arguments
|
||||
* @psalm-suppress PossiblyUnusedMethod
|
||||
*/
|
||||
public function executeCommand(string $command, $arguments): Promise
|
||||
public function executeCommand(string $command, $arguments): void
|
||||
{
|
||||
$this->server->logDebug(
|
||||
'workspace/executeCommand',
|
||||
@ -155,7 +153,5 @@ class Workspace
|
||||
$this->server->emitVersionedIssues([$file => $arguments['uri']]);
|
||||
break;
|
||||
}
|
||||
|
||||
return new Success(null);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Psalm\Tests\LanguageServer;
|
||||
|
||||
use Amp\Deferred;
|
||||
use Amp\DeferredFuture;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\IssueData;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
@ -21,7 +21,6 @@ use Psalm\Tests\LanguageServer\Message as MessageBody;
|
||||
use Psalm\Tests\LanguageServer\MockProtocolStream;
|
||||
use Psalm\Tests\TestConfig;
|
||||
|
||||
use function Amp\Promise\wait;
|
||||
use function rand;
|
||||
|
||||
class DiagnosticTest extends AsyncTestCase
|
||||
@ -65,7 +64,7 @@ class DiagnosticTest extends AsyncTestCase
|
||||
public function testSnippetSupportDisabled(): void
|
||||
{
|
||||
// Create a new promisor
|
||||
$deferred = new Deferred;
|
||||
$deferred = new DeferredFuture;
|
||||
|
||||
$this->setTimeout(5000);
|
||||
$clientConfiguration = new ClientConfiguration();
|
||||
@ -91,11 +90,11 @@ class DiagnosticTest extends AsyncTestCase
|
||||
/** @psalm-suppress PossiblyNullPropertyFetch,UndefinedPropertyFetch,MixedPropertyFetch */
|
||||
if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') {
|
||||
$this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport);
|
||||
$deferred->resolve(null);
|
||||
$deferred->complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
wait($deferred->promise());
|
||||
$deferred->getFuture()->await();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,14 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Tests\LanguageServer;
|
||||
|
||||
use Amp\Deferred;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Psalm\Internal\LanguageServer\EmitterInterface;
|
||||
use Psalm\Internal\LanguageServer\EmitterTrait;
|
||||
use Psalm\Internal\LanguageServer\Message;
|
||||
use Psalm\Internal\LanguageServer\ProtocolReader;
|
||||
use Psalm\Internal\LanguageServer\ProtocolWriter;
|
||||
use Revolt\EventLoop;
|
||||
|
||||
/**
|
||||
* A fake duplex protocol stream
|
||||
@ -24,17 +22,10 @@ class MockProtocolStream implements ProtocolReader, ProtocolWriter, EmitterInter
|
||||
*
|
||||
* @psalm-suppress PossiblyUnusedReturnValue
|
||||
*/
|
||||
public function write(Message $msg): Promise
|
||||
public function write(Message $msg): void
|
||||
{
|
||||
Loop::defer(function () use ($msg): void {
|
||||
EventLoop::queue(function () use ($msg): void {
|
||||
$this->emit('message', [Message::parse((string)$msg)]);
|
||||
});
|
||||
|
||||
// Create a new promisor
|
||||
$deferred = new Deferred;
|
||||
|
||||
$deferred->resolve(null);
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user