2016-08-22 15:32:31 +00:00
< ? php
2016-09-30 09:54:49 +00:00
declare ( strict_types = 1 );
2016-08-22 15:32:31 +00:00
namespace LanguageServer ;
2016-09-30 09:30:08 +00:00
use LanguageServer\Protocol\ {
ServerCapabilities ,
ClientCapabilities ,
TextDocumentSyncKind ,
Message ,
MessageType ,
2016-10-20 01:48:30 +00:00
InitializeResult ,
2016-11-14 09:25:44 +00:00
TextDocumentIdentifier
2016-09-30 09:30:08 +00:00
};
2016-10-20 01:31:12 +00:00
use AdvancedJsonRpc ;
2016-11-30 12:34:18 +00:00
use Sabre\Event\Promise ;
2016-11-14 09:25:44 +00:00
use function Sabre\Event\coroutine ;
2016-10-19 10:41:53 +00:00
use Exception ;
2016-10-20 01:31:12 +00:00
use Throwable ;
2016-11-14 09:25:44 +00:00
use Webmozart\Glob\Iterator\GlobIterator ;
use Webmozart\Glob\Glob ;
use Webmozart\PathUtil\Path ;
use Sabre\Uri ;
2016-11-30 12:34:18 +00:00
use function Sabre\Event\Loop\setTimeout ;
2016-08-22 15:32:31 +00:00
2016-10-20 01:31:12 +00:00
class LanguageServer extends AdvancedJsonRpc\Dispatcher
2016-08-22 15:32:31 +00:00
{
2016-09-02 19:13:30 +00:00
/**
* Handles textDocument /* method calls
*
* @ var Server\TextDocument
*/
2016-08-25 13:27:14 +00:00
public $textDocument ;
2016-09-02 19:13:30 +00:00
2016-09-30 09:30:08 +00:00
/**
* Handles workspace /* method calls
*
* @ var Server\Workspace
*/
public $workspace ;
2016-08-25 13:27:14 +00:00
public $telemetry ;
public $window ;
public $completionItem ;
public $codeLens ;
2016-11-14 09:25:44 +00:00
/**
* ClientCapabilities
*/
private $clientCapabilities ;
2016-08-25 13:27:14 +00:00
private $protocolReader ;
private $protocolWriter ;
2016-09-02 19:13:30 +00:00
private $client ;
2016-08-25 13:27:14 +00:00
2016-10-20 01:48:30 +00:00
/**
* The root project path that was passed to initialize ()
*
* @ var string
*/
private $rootPath ;
2016-09-30 09:30:08 +00:00
private $project ;
2016-08-25 13:27:14 +00:00
public function __construct ( ProtocolReader $reader , ProtocolWriter $writer )
2016-08-22 15:32:31 +00:00
{
2016-08-25 13:27:14 +00:00
parent :: __construct ( $this , '/' );
$this -> protocolReader = $reader ;
2016-10-31 10:47:21 +00:00
$this -> protocolReader -> on ( 'message' , function ( Message $msg ) {
2016-11-14 09:25:44 +00:00
coroutine ( function () use ( $msg ) {
// Ignore responses, this is the handler for requests and notifications
if ( AdvancedJsonRpc\Response :: isResponse ( $msg -> body )) {
return ;
2016-10-20 01:36:03 +00:00
}
2016-11-14 09:25:44 +00:00
$result = null ;
$error = null ;
try {
// Invoke the method handler to get a result
$result = yield $this -> dispatch ( $msg -> body );
} catch ( AdvancedJsonRpc\Error $e ) {
// If a ResponseError is thrown, send it back in the Response
$error = $e ;
} catch ( Throwable $e ) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error (
$e -> getMessage (),
AdvancedJsonRpc\ErrorCode :: INTERNAL_ERROR ,
null ,
$e
);
}
// Only send a Response for a Request
// Notifications do not send Responses
if ( AdvancedJsonRpc\Request :: isRequest ( $msg -> body )) {
if ( $error !== null ) {
$responseBody = new AdvancedJsonRpc\ErrorResponse ( $msg -> body -> id , $error );
} else {
$responseBody = new AdvancedJsonRpc\SuccessResponse ( $msg -> body -> id , $result );
}
$this -> protocolWriter -> write ( new Message ( $responseBody ));
}
}) -> otherwise ( '\\LanguageServer\\crash' );
2016-08-25 13:27:14 +00:00
});
$this -> protocolWriter = $writer ;
2016-10-31 10:47:21 +00:00
$this -> client = new LanguageClient ( $reader , $writer );
2016-08-22 15:32:31 +00:00
}
2016-08-22 21:48:20 +00:00
2016-08-25 13:27:14 +00:00
/**
* 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 )
2016-09-30 09:30:08 +00:00
* @ param string | null $rootPath The rootPath of the workspace . Is null if no folder is open .
2016-11-23 17:38:57 +00:00
* @ 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 .
2016-08-25 13:27:14 +00:00
* @ return InitializeResult
*/
2016-11-23 17:38:57 +00:00
public function initialize ( ClientCapabilities $capabilities , string $rootPath = null , int $processId = null ) : InitializeResult
2016-08-23 09:21:37 +00:00
{
2016-10-20 01:48:30 +00:00
$this -> rootPath = $rootPath ;
2016-11-14 09:25:44 +00:00
$this -> clientCapabilities = $capabilities ;
$this -> project = new Project ( $this -> client , $capabilities );
$this -> textDocument = new Server\TextDocument ( $this -> project , $this -> client );
$this -> workspace = new Server\Workspace ( $this -> project , $this -> client );
2016-10-20 01:48:30 +00:00
2016-09-30 09:30:08 +00:00
// start building project index
2016-10-20 01:48:30 +00:00
if ( $rootPath !== null ) {
2016-11-14 09:25:44 +00:00
$this -> indexProject () -> otherwise ( '\\LanguageServer\\crash' );
2016-09-30 09:30:08 +00:00
}
2016-11-30 12:34:18 +00:00
if ( extension_loaded ( 'xdebug' )) {
setTimeout ( function () {
$this -> client -> window -> showMessage ( MessageType :: WARNING , 'You are running PHP Language Server with xdebug enabled. This has a major impact on server performance.' );
}, 1 );
}
2016-08-25 13:27:14 +00:00
$serverCapabilities = new ServerCapabilities ();
2016-08-23 09:21:37 +00:00
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
2016-08-25 13:27:14 +00:00
$serverCapabilities -> textDocumentSync = TextDocumentSyncKind :: FULL ;
2016-08-23 09:21:37 +00:00
// Support "Find all symbols"
2016-08-25 13:27:14 +00:00
$serverCapabilities -> documentSymbolProvider = true ;
2016-09-30 09:30:08 +00:00
// Support "Find all symbols in workspace"
$serverCapabilities -> workspaceSymbolProvider = true ;
2016-09-06 10:54:34 +00:00
// Support "Format Code"
$serverCapabilities -> documentFormattingProvider = true ;
2016-10-08 12:59:08 +00:00
// Support "Go to definition"
$serverCapabilities -> definitionProvider = true ;
2016-10-11 23:45:15 +00:00
// Support "Find all references"
$serverCapabilities -> referencesProvider = true ;
2016-10-19 10:31:32 +00:00
// Support "Hover"
$serverCapabilities -> hoverProvider = true ;
2016-10-08 12:59:08 +00:00
2016-08-25 13:27:14 +00:00
return new InitializeResult ( $serverCapabilities );
}
/**
* The shutdown request is sent from the client to the server . It asks the server to shut down , but to not exit
* ( otherwise the response might not be delivered correctly to the client ) . There is a separate exit notification that
* asks the server to exit .
*
* @ return void
*/
public function shutdown ()
{
2016-11-14 09:25:44 +00:00
unset ( $this -> project );
2016-08-25 13:27:14 +00:00
}
/**
* A notification to ask the server to exit its process .
*
* @ return void
*/
public function exit ()
{
exit ( 0 );
2016-08-23 09:21:37 +00:00
}
2016-09-30 09:30:08 +00:00
/**
* Parses workspace files , one at a time .
*
2016-11-14 09:25:44 +00:00
* @ return Promise < void >
2016-09-30 09:30:08 +00:00
*/
2016-11-14 09:25:44 +00:00
private function indexProject () : Promise
2016-09-30 09:30:08 +00:00
{
2016-11-14 09:25:44 +00:00
return coroutine ( function () {
$textDocuments = yield $this -> findPhpFiles ();
$count = count ( $textDocuments );
$startTime = microtime ( true );
2016-11-17 21:20:37 +00:00
foreach ( $textDocuments as $i => $textDocument ) {
// Give LS to the chance to handle requests while indexing
yield timeout ();
$this -> client -> window -> logMessage (
2016-11-18 12:24:26 +00:00
MessageType :: LOG ,
2016-11-17 21:20:37 +00:00
" Parsing file $i / $count : { $textDocument -> uri } "
);
try {
yield $this -> project -> loadDocument ( $textDocument -> uri );
2016-11-18 12:24:26 +00:00
} catch ( ContentTooLargeException $e ) {
$this -> client -> window -> logMessage (
MessageType :: INFO ,
" Ignoring file { $textDocument -> uri } because it exceeds size limit of { $e -> limit } bytes ( { $e -> size } ) "
);
2016-11-17 21:20:37 +00:00
} catch ( Exception $e ) {
2016-11-14 09:25:44 +00:00
$this -> client -> window -> logMessage (
2016-11-17 21:20:37 +00:00
MessageType :: ERROR ,
" Error parsing file { $textDocument -> uri } : " . ( string ) $e
2016-11-14 09:25:44 +00:00
);
2016-11-17 21:20:37 +00:00
}
}
2016-11-14 09:25:44 +00:00
$duration = ( int )( microtime ( true ) - $startTime );
$mem = ( int )( memory_get_usage ( true ) / ( 1024 * 1024 ));
$this -> client -> window -> logMessage (
MessageType :: INFO ,
" All $count PHP files parsed in $duration seconds. $mem MiB allocated. "
);
});
}
2016-09-30 09:30:08 +00:00
2016-11-14 09:25:44 +00:00
/**
* Returns all PHP files in the workspace .
* If the client does not support workspace / files , it falls back to searching the file system directly .
*
* @ return Promise < TextDocumentIdentifier [] >
*/
private function findPhpFiles () : Promise
{
return coroutine ( function () {
$textDocuments = [];
$pattern = Path :: makeAbsolute ( '**/*.php' , $this -> rootPath );
if ( $this -> clientCapabilities -> xfilesProvider ) {
// Use xfiles request
foreach ( yield $this -> client -> workspace -> xfiles () as $textDocument ) {
$path = Uri\parse ( $textDocument -> uri )[ 'path' ];
if ( Glob :: match ( $path , $pattern )) {
$textDocuments [] = $textDocument ;
}
}
2016-09-30 09:30:08 +00:00
} else {
2016-11-14 09:25:44 +00:00
// Use the file system
foreach ( new GlobIterator ( $pattern ) as $path ) {
$textDocuments [] = new TextDocumentIdentifier ( pathToUri ( $path ));
yield timeout ();
}
2016-09-30 09:30:08 +00:00
}
2016-11-14 09:25:44 +00:00
return $textDocuments ;
});
2016-09-30 09:30:08 +00:00
}
2016-08-22 15:32:31 +00:00
}