parent
57604e61f1
commit
db28e22378
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
interface class
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
class TestClass
|
||||
{
|
||||
public $testProperty;
|
||||
|
||||
public function testMethod($testParameter)
|
||||
{
|
||||
$testVariable = 123;
|
||||
}
|
||||
}
|
||||
|
||||
trait TestTrait
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
interface TestInterface
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use AdvancedJsonRpc\Notification as NotificationBody;
|
||||
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use LanguageServer\ProtocolWriter;
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, Message};
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*/
|
||||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
*/
|
||||
private $protocolWriter;
|
||||
|
||||
public function __construct(ProtocolWriter $protocolWriter)
|
||||
{
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param Diagnostic[] $diagnostics
|
||||
*/
|
||||
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||
{
|
||||
$this->protocolWriter->write(new Message(new NotificationBody(
|
||||
'textDocument/publishDiagnostics',
|
||||
(object)[
|
||||
'uri' => $uri,
|
||||
'diagnostics' => $diagnostics
|
||||
]
|
||||
)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Client\TextDocument;
|
||||
|
||||
class LanguageClient
|
||||
{
|
||||
/**
|
||||
* Handles textDocument/* methods
|
||||
*
|
||||
* @var Client\TextDocument
|
||||
*/
|
||||
public $textDocument;
|
||||
|
||||
private $protocolWriter;
|
||||
|
||||
public function __construct(ProtocolWriter $writer)
|
||||
{
|
||||
$this->protocolWriter = $writer;
|
||||
$this->textDocument = new TextDocument($writer);
|
||||
}
|
||||
}
|
|
@ -2,13 +2,20 @@
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Server\TextDocument;
|
||||
use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message};
|
||||
use LanguageServer\Protocol\InitializeResult;
|
||||
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody};
|
||||
|
||||
class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
||||
{
|
||||
/**
|
||||
* Handles textDocument/* method calls
|
||||
*
|
||||
* @var Server\TextDocument
|
||||
*/
|
||||
public $textDocument;
|
||||
|
||||
public $telemetry;
|
||||
public $window;
|
||||
public $workspace;
|
||||
|
@ -17,6 +24,7 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
|||
|
||||
private $protocolReader;
|
||||
private $protocolWriter;
|
||||
private $client;
|
||||
|
||||
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
|
||||
{
|
||||
|
@ -47,7 +55,8 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
|
|||
}
|
||||
});
|
||||
$this->protocolWriter = $writer;
|
||||
$this->textDocument = new TextDocumentManager();
|
||||
$this->client = new LanguageClient($writer);
|
||||
$this->textDocument = new Server\TextDocument($this->client);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use AdvancedJsonRpc\Notification;
|
||||
|
||||
/**
|
||||
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||
*/
|
||||
class PublishDiagnosticsNotification extends Notification
|
||||
{
|
||||
/**
|
||||
* @var PublishDiagnosticsParams
|
||||
*/
|
||||
public $params;
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @param Diagnostic[] $diagnostics
|
||||
*/
|
||||
public function __construct(string $uri, array $diagnostics)
|
||||
{
|
||||
$this->method = 'textDocument/publishDiagnostics';
|
||||
$this->params = $params;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol\TextDocument;
|
||||
|
||||
use LanguageServer\Protocol\Params;
|
||||
|
||||
class PublishDiagnosticsParams extends Params
|
||||
{
|
||||
/**
|
||||
* The URI for which diagnostic information is reported.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* An array of diagnostic information items.
|
||||
*
|
||||
* @var LanguageServer\Protocol\Diagnostic[]
|
||||
*/
|
||||
public $diagnostics;
|
||||
}
|
|
@ -10,4 +10,12 @@ class TextDocumentIdentifier
|
|||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* @param string $uri The text document's URI.
|
||||
*/
|
||||
public function __construct(string $uri = null)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer;
|
||||
namespace LanguageServer\Server;
|
||||
|
||||
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier};
|
||||
use LanguageServer\{LanguageClient, ColumnCalculator, SymbolFinder};
|
||||
use LanguageServer\Protocol\{
|
||||
TextDocumentItem,
|
||||
TextDocumentIdentifier,
|
||||
VersionedTextDocumentIdentifier,
|
||||
Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
Range,
|
||||
Position
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*/
|
||||
class TextDocumentManager
|
||||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var PhpParser\Parser
|
||||
|
@ -23,8 +32,16 @@ class TextDocumentManager
|
|||
*/
|
||||
private $asts;
|
||||
|
||||
public function __construct()
|
||||
/**
|
||||
* The lanugage client object to call methods on the client
|
||||
*
|
||||
* @var LanguageServer\LanguageClient
|
||||
*/
|
||||
private $client;
|
||||
|
||||
public function __construct(LanguageClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
$lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos']]);
|
||||
$this->parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer, ['throwOnError' => false]);
|
||||
}
|
||||
|
@ -74,20 +91,39 @@ class TextDocumentManager
|
|||
$this->updateAst($textDocument->uri, $contentChanges[0]->text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-parses a source file, updates the AST and reports parsing errors that may occured as diagnostics
|
||||
*
|
||||
* @param string $uri The URI of the source file
|
||||
* @param string $content The new content of the source file
|
||||
* @return void
|
||||
*/
|
||||
private function updateAst(string $uri, string $content)
|
||||
{
|
||||
$stmts = $this->parser->parse($content);
|
||||
// TODO report errors as diagnostics
|
||||
// foreach ($parser->getErrors() as $error) {
|
||||
// error_log($error->getMessage());
|
||||
// }
|
||||
$diagnostics = [];
|
||||
foreach ($this->parser->getErrors() as $error) {
|
||||
$diagnostic = new Diagnostic();
|
||||
$diagnostic->range = new Range(
|
||||
new Position($error->getStartLine() - 1, $error->hasColumnInfo() ? $error->getStartColumn($content) - 1 : 0),
|
||||
new Position($error->getEndLine() - 1, $error->hasColumnInfo() ? $error->getEndColumn($content) - 1 : 0)
|
||||
);
|
||||
$diagnostic->severity = DiagnosticSeverity::ERROR;
|
||||
$diagnostic->source = 'php';
|
||||
// Do not include "on line ..." in the error message
|
||||
$diagnostic->message = $error->getRawMessage();
|
||||
$diagnostics[] = $diagnostic;
|
||||
}
|
||||
if (count($diagnostics) > 0) {
|
||||
$this->client->textDocument->publishDiagnostics($uri, $diagnostics);
|
||||
}
|
||||
// $stmts can be null in case of a fatal parsing error
|
||||
if ($stmts) {
|
||||
$traverser = new NodeTraverser;
|
||||
$traverser->addVisitor(new NameResolver);
|
||||
$traverser->addVisitor(new ColumnCalculator($content));
|
||||
$traverser->traverse($stmts);
|
||||
}
|
||||
$this->asts[$uri] = $stmts;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ class SymbolFinder extends NodeVisitorAbstract
|
|||
{
|
||||
const NODE_SYMBOL_KIND_MAP = [
|
||||
Node\Stmt\Class_::class => SymbolKind::CLASS_,
|
||||
Node\Stmt\Trait_::class => SymbolKind::CLASS_,
|
||||
Node\Stmt\Interface_::class => SymbolKind::INTERFACE,
|
||||
Node\Stmt\Namespace_::class => SymbolKind::NAMESPACE,
|
||||
Node\Stmt\Function_::class => SymbolKind::FUNCTION,
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Tests\Server;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, Client, LanguageClient};
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, SymbolKind, DiagnosticSeverity};
|
||||
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody};
|
||||
|
||||
class TextDocumentTest extends TestCase
|
||||
{
|
||||
public function testDocumentSymbol()
|
||||
{
|
||||
$textDocument = new Server\TextDocument(new LanguageClient(new MockProtocolStream()));
|
||||
// Trigger parsing of source
|
||||
$textDocumentItem = new TextDocumentItem();
|
||||
$textDocumentItem->uri = 'whatever';
|
||||
$textDocumentItem->languageId = 'php';
|
||||
$textDocumentItem->version = 1;
|
||||
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/Symbols.php');
|
||||
$textDocument->didOpen($textDocumentItem);
|
||||
// Request symbols
|
||||
$result = $textDocument->documentSymbol(new TextDocumentIdentifier('whatever'));
|
||||
$this->assertEquals([
|
||||
[
|
||||
'name' => 'TestNamespace',
|
||||
'kind' => SymbolKind::NAMESPACE,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 2,
|
||||
'character' => 0
|
||||
],
|
||||
'end' => [
|
||||
'line' => 2,
|
||||
'character' => 23
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
],
|
||||
[
|
||||
'name' => 'TestClass',
|
||||
'kind' => SymbolKind::CLASS_,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 4,
|
||||
'character' => 0
|
||||
],
|
||||
'end' => [
|
||||
'line' => 12,
|
||||
'character' => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
],
|
||||
[
|
||||
'name' => 'testProperty',
|
||||
'kind' => SymbolKind::PROPERTY,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 6,
|
||||
'character' => 11
|
||||
],
|
||||
'end' => [
|
||||
'line' => 6,
|
||||
'character' => 23
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => 'TestClass'
|
||||
],
|
||||
[
|
||||
'name' => 'testMethod',
|
||||
'kind' => SymbolKind::METHOD,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 8,
|
||||
'character' => 4
|
||||
],
|
||||
'end' => [
|
||||
'line' => 11,
|
||||
'character' => 4
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
],
|
||||
[
|
||||
'name' => 'testVariable',
|
||||
'kind' => SymbolKind::VARIABLE,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 10,
|
||||
'character' => 8
|
||||
],
|
||||
'end' => [
|
||||
'line' => 10,
|
||||
'character' => 20
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
],
|
||||
[
|
||||
'name' => 'TestTrait',
|
||||
'kind' => SymbolKind::CLASS_,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 14,
|
||||
'character' => 0
|
||||
],
|
||||
'end' => [
|
||||
'line' => 17,
|
||||
'character' => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
],
|
||||
[
|
||||
'name' => 'TestInterface',
|
||||
'kind' => SymbolKind::INTERFACE,
|
||||
'location' => [
|
||||
'uri' => 'whatever',
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 19,
|
||||
'character' => 0
|
||||
],
|
||||
'end' => [
|
||||
'line' => 22,
|
||||
'character' => 0
|
||||
]
|
||||
]
|
||||
],
|
||||
'containerName' => null
|
||||
]
|
||||
], json_decode(json_encode($result), true));
|
||||
}
|
||||
|
||||
public function testParseErrorsArePublishedAsDiagnostics()
|
||||
{
|
||||
$args = null;
|
||||
$client = new LanguageClient(new MockProtocolStream());
|
||||
$client->textDocument = new class($args) extends Client\TextDocument {
|
||||
private $args;
|
||||
public function __construct(&$args)
|
||||
{
|
||||
parent::__construct(new MockProtocolStream());
|
||||
$this->args = &$args;
|
||||
}
|
||||
public function publishDiagnostics(string $uri, array $diagnostics)
|
||||
{
|
||||
$this->args = func_get_args();
|
||||
}
|
||||
};
|
||||
$textDocument = new Server\TextDocument($client);
|
||||
// Trigger parsing of source
|
||||
$textDocumentItem = new TextDocumentItem();
|
||||
$textDocumentItem->uri = 'whatever';
|
||||
$textDocumentItem->languageId = 'php';
|
||||
$textDocumentItem->version = 1;
|
||||
$textDocumentItem->text = file_get_contents(__DIR__ . '/../../fixtures/InvalidFile.php');
|
||||
$textDocument->didOpen($textDocumentItem);
|
||||
$this->assertEquals([
|
||||
'whatever',
|
||||
[[
|
||||
'range' => [
|
||||
'start' => [
|
||||
'line' => 2,
|
||||
'character' => 10
|
||||
],
|
||||
'end' => [
|
||||
'line' => 2,
|
||||
'character' => 14
|
||||
]
|
||||
],
|
||||
'severity' => DiagnosticSeverity::ERROR,
|
||||
'code' => null,
|
||||
'source' => 'php',
|
||||
'message' => "Syntax error, unexpected T_CLASS, expecting T_STRING"
|
||||
]]
|
||||
], json_decode(json_encode($args), true));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue