Skip to content

Commit

Permalink
Merge pull request #10050 from weirdan/lsp-modernize-progress-reporting
Browse files Browse the repository at this point in the history
Modernize LSP progress reporting
  • Loading branch information
weirdan committed Jul 26, 2023
2 parents be875f5 + a34222a commit 6c0a09a
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Psalm\Internal\LanguageServer\Client\Progress;

use LanguageServerProtocol\LogMessage;
use LanguageServerProtocol\MessageType;
use LogicException;
use Psalm\Internal\LanguageServer\ClientHandler;

/** @internal */
final class LegacyProgress implements ProgressInterface
{
private const STATUS_INACTIVE = 'inactive';
private const STATUS_ACTIVE = 'active';
private const STATUS_FINISHED = 'finished';

private string $status = self::STATUS_INACTIVE;

private ClientHandler $handler;
private ?string $title = null;

public function __construct(ClientHandler $handler)
{
$this->handler = $handler;
}

public function begin(string $title, ?string $message = null, ?int $percentage = null): void
{

if ($this->status === self::STATUS_ACTIVE) {
throw new LogicException('Progress has already been started');
}

if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

$this->title = $title;

$this->notify($message);

$this->status = self::STATUS_ACTIVE;
}

public function update(?string $message = null, ?int $percentage = null): void
{
if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

if ($this->status === self::STATUS_INACTIVE) {
throw new LogicException('Progress has not been started yet');
}

$this->notify($message);
}

public function end(?string $message = null): void
{
if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

if ($this->status === self::STATUS_INACTIVE) {
throw new LogicException('Progress has not been started yet');
}

$this->notify($message);

$this->status = self::STATUS_FINISHED;
}

private function notify(?string $message): void
{
$this->handler->notify(
'telemetry/event',
new LogMessage(
MessageType::INFO,
$this->title . (empty($message) ? '' : (': ' . $message)),
),
);
}
}
121 changes: 121 additions & 0 deletions src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace Psalm\Internal\LanguageServer\Client\Progress;

use LogicException;
use Psalm\Internal\LanguageServer\ClientHandler;

/** @internal */
final class Progress implements ProgressInterface
{
private const STATUS_INACTIVE = 'inactive';
private const STATUS_ACTIVE = 'active';
private const STATUS_FINISHED = 'finished';

private string $status = self::STATUS_INACTIVE;

private ClientHandler $handler;
private string $token;
private bool $withPercentage = false;

public function __construct(ClientHandler $handler, string $token)
{
$this->handler = $handler;
$this->token = $token;
}

public function begin(
string $title,
?string $message = null,
?int $percentage = null
): void {
if ($this->status === self::STATUS_ACTIVE) {
throw new LogicException('Progress has already been started');
}

if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

$notification = [
'token' => $this->token,
'value' => [
'kind' => 'begin',
'title' => $title,
],
];

if ($message !== null) {
$notification['value']['message'] = $message;
}

if ($percentage !== null) {
$notification['value']['percentage'] = $percentage;
$this->withPercentage = true;
}

$this->handler->notify('$/progress', $notification);

$this->status = self::STATUS_ACTIVE;
}

public function end(?string $message = null): void
{
if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

if ($this->status === self::STATUS_INACTIVE) {
throw new LogicException('Progress has not been started yet');
}

$notification = [
'token' => $this->token,
'value' => [
'kind' => 'end',
],
];

if ($message !== null) {
$notification['value']['message'] = $message;
}

$this->handler->notify('$/progress', $notification);

$this->status = self::STATUS_FINISHED;
}

public function update(?string $message = null, ?int $percentage = null): void
{
if ($this->status === self::STATUS_FINISHED) {
throw new LogicException('Progress has already been finished');
}

if ($this->status === self::STATUS_INACTIVE) {
throw new LogicException('Progress has not been started yet');
}

$notification = [
'token' => $this->token,
'value' => [
'kind' => 'report',
],
];

if ($message !== null) {
$notification['value']['message'] = $message;
}

if ($percentage !== null) {
if (!$this->withPercentage) {
throw new LogicException(
'Cannot update percentage for progress '
. 'that was started without percentage',
);
}
$notification['value']['percentage'] = $percentage;
}

$this->handler->notify('$/progress', $notification);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Psalm\Internal\LanguageServer\Client\Progress;

/** @internal */
interface ProgressInterface
{
public function begin(
string $title,
?string $message = null,
?int $percentage = null
): void;

public function update(?string $message = null, ?int $percentage = null): void;
public function end(?string $message = null): void;
}
12 changes: 12 additions & 0 deletions src/Psalm/Internal/LanguageServer/LanguageClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use JsonMapper;
use LanguageServerProtocol\LogMessage;
use LanguageServerProtocol\LogTrace;
use Psalm\Internal\LanguageServer\Client\Progress\LegacyProgress;
use Psalm\Internal\LanguageServer\Client\Progress\Progress;
use Psalm\Internal\LanguageServer\Client\Progress\ProgressInterface;
use Psalm\Internal\LanguageServer\Client\TextDocument as ClientTextDocument;
use Psalm\Internal\LanguageServer\Client\Workspace as ClientWorkspace;

Expand Down Expand Up @@ -131,6 +134,15 @@ public function event(LogMessage $logMessage): void
);
}

public function makeProgress(string $token): ProgressInterface
{
if ($this->server->clientCapabilities->window->workDoneProgress ?? false) {
return new Progress($this->handler, $token);
} else {
return new LegacyProgress($this->handler);
}
}

/**
* Configuration Refreshed from Client
*
Expand Down
31 changes: 12 additions & 19 deletions src/Psalm/Internal/LanguageServer/LanguageServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
use function strpos;
use function substr;
use function trim;
use function uniqid;
use function urldecode;

use const JSON_PRETTY_PRINT;
Expand Down Expand Up @@ -377,29 +378,19 @@ public static function run(
* The initialize request is sent as the first request from the client to the server.
*
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
* @param int|null $processId The process Id of the parent process that started the server.
* Is null if the process has not been started by another process. If the parent process is
* not alive then the server should exit (see exit notification) its process.
* @param ClientInfo|null $clientInfo Information about the client
* @param string|null $locale The locale the client is currently showing the user interface
* in. This must not necessarily be the locale of the operating
* system.
* @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').
* @param string|null $workDoneToken The token to be used to report progress during init.
* @psalm-return Promise<InitializeResult>
* @psalm-suppress PossiblyUnusedParam
*/
public function initialize(
ClientCapabilities $capabilities,
?int $processId = null,
?ClientInfo $clientInfo = null,
?string $locale = null,
?string $rootPath = null,
?string $rootUri = null,
$initializationOptions = null,
?string $trace = null
//?array $workspaceFolders = null //error in json-dispatcher
?string $trace = null,
?string $workDoneToken = null
): Promise {
$this->clientInfo = $clientInfo;
$this->clientCapabilities = $capabilities;
Expand All @@ -412,9 +403,11 @@ public function initialize(

return call(
/** @return Generator<int, true, mixed, InitializeResult> */
function () {
function () use ($workDoneToken) {
$progress = $this->client->makeProgress($workDoneToken ?? uniqid('tkn', true));

$this->logInfo("Initializing...");
$this->clientStatus('initializing');
$progress->begin('Psalm', 'initializing');

// Eventually, this might block on something. Leave it as a generator.
/** @psalm-suppress TypeDoesNotContainType */
Expand All @@ -425,14 +418,14 @@ function () {
$this->project_analyzer->serverMode($this);

$this->logInfo("Initializing: Getting code base...");
$this->clientStatus('initializing', 'getting code base');
$progress->update('getting code base');

$this->logInfo("Initializing: Scanning files ({$this->project_analyzer->threads} Threads)...");
$this->clientStatus('initializing', 'scanning files');
$progress->update('scanning files');
$this->codebase->scanFiles($this->project_analyzer->threads);

$this->logInfo("Initializing: Registering stub files...");
$this->clientStatus('initializing', 'registering stub files');
$progress->update('registering stub files');
$this->codebase->config->visitStubFiles($this->codebase, $this->project_analyzer->progress);

if ($this->textDocument === null) {
Expand Down Expand Up @@ -572,7 +565,7 @@ function () {
}

$this->logInfo("Initializing: Complete.");
$this->clientStatus('initialized');
$progress->end('initialized');

/**
* Information about the server.
Expand Down
4 changes: 1 addition & 3 deletions src/Psalm/Internal/LanguageServer/Server/TextDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,8 @@ public function signatureHelp(TextDocumentIdentifier $textDocument, Position $po
* The code action request is sent from the client to the server to compute commands
* for a given text document and range. These commands are typically code fixes to
* either fix problems or to beautify/refactor code.
*
* @psalm-suppress PossiblyUnusedParam
*/
public function codeAction(TextDocumentIdentifier $textDocument, Range $range, CodeActionContext $context): Promise
public function codeAction(TextDocumentIdentifier $textDocument, CodeActionContext $context): Promise
{
if (!$this->server->client->clientConfiguration->provideCodeActions) {
return new Success(null);
Expand Down
5 changes: 2 additions & 3 deletions src/Psalm/Internal/LanguageServer/Server/Workspace.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,9 @@ public function didChangeWatchedFiles(array $changes): void
/**
* A notification sent from the client to the server to signal the change of configuration settings.
*
* @param mixed $settings
* @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedParam
* @psalm-suppress PossiblyUnusedMethod
*/
public function didChangeConfiguration($settings): void
public function didChangeConfiguration(): void
{
$this->server->logDebug(
'workspace/didChangeConfiguration',
Expand Down
15 changes: 13 additions & 2 deletions tests/LanguageServer/DiagnosticTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,21 @@ public function testSnippetSupportDisabled(): void
);

$write->on('message', function (Message $message) use ($deferred, $server): void {
/** @psalm-suppress PossiblyNullPropertyFetch,UndefinedPropertyFetch,MixedPropertyFetch */
if ($message->body->method === 'telemetry/event' && $message->body->params->message === 'initialized') {
/** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch,UndefinedPropertyFetch */
if ($message->body->method === 'telemetry/event' && ($message->body->params->message ?? null) === 'initialized') {
$this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport);
$deferred->resolve(null);
return;
}

/** @psalm-suppress NullPropertyFetch,PossiblyNullPropertyFetch */
if ($message->body->method === '$/progress'
&& ($message->body->params->value->kind ?? null) === 'end'
&& ($message->body->params->value->message ?? null) === 'initialized'
) {
$this->assertFalse($server->clientCapabilities->textDocument->completion->completionItem->snippetSupport);
$deferred->resolve(null);
return;
}
});

Expand Down

0 comments on commit 6c0a09a

Please sign in to comment.