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-30 21:23:51 +00:00
CompletionOptions
2016-09-30 09:30:08 +00:00
};
2016-12-08 01:33:48 +00:00
use LanguageServer\FilesFinder\ { FilesFinder , ClientFilesFinder , FileSystemFilesFinder };
use LanguageServer\ContentRetriever\ { ContentRetriever , ClientContentRetriever , FileSystemContentRetriever };
2016-12-13 00:51:02 +00:00
use LanguageServer\Index\ { DependenciesIndex , GlobalIndex , Index , ProjectIndex , StubsIndex };
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\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-12-13 00:51:02 +00:00
/**
* @ var Server\Window
*/
2016-08-25 13:27:14 +00:00
public $window ;
2016-12-13 00:51:02 +00:00
public $telemetry ;
2016-08-25 13:27:14 +00:00
public $completionItem ;
public $codeLens ;
2016-12-13 00:51:02 +00:00
/**
* @ var ProtocolReader
*/
2017-01-05 03:18:14 +00:00
protected $protocolReader ;
2016-12-13 00:51:02 +00:00
/**
* @ var ProtocolWriter
*/
2017-01-05 03:18:14 +00:00
protected $protocolWriter ;
2016-08-25 13:27:14 +00:00
2016-10-20 01:48:30 +00:00
/**
2016-12-13 00:51:02 +00:00
* @ var LanguageClient
2016-10-20 01:48:30 +00:00
*/
2017-01-05 03:18:14 +00:00
protected $client ;
2016-09-30 09:30:08 +00:00
2016-12-08 01:33:48 +00:00
/**
* @ var FilesFinder
*/
2017-01-05 03:18:14 +00:00
protected $filesFinder ;
2016-12-08 01:33:48 +00:00
/**
* @ var ContentRetriever
*/
2017-01-05 03:18:14 +00:00
protected $contentRetriever ;
/**
* @ var PhpDocumentLoader
*/
protected $documentLoader ;
2016-12-08 01:33:48 +00:00
2016-12-13 00:51:02 +00:00
/**
* @ param PotocolReader $reader
* @ param ProtocolWriter $writer
*/
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-11-30 20:10:05 +00:00
$this -> protocolReader -> on ( 'close' , function () {
$this -> shutdown ();
$this -> exit ();
});
2016-12-15 08:47:59 +00:00
$this -> protocolReader -> on ( 'message' , function ( Message $msg ) {
coroutine ( function () use ( $msg ) {
// Ignore responses, this is the handler for requests and notifications
if ( AdvancedJsonRpc\Response :: isResponse ( $msg -> body )) {
return ;
2016-11-14 09:25:44 +00:00
}
2016-12-15 08:47:59 +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 (
( string ) $e ,
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' );
});
$this -> protocolWriter = $writer ;
$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-12-13 00:51:02 +00:00
* @ return Promise < InitializeResult >
2016-08-25 13:27:14 +00:00
*/
2016-12-13 00:51:02 +00:00
public function initialize ( ClientCapabilities $capabilities , string $rootPath = null , int $processId = null ) : Promise
2016-08-23 09:21:37 +00:00
{
2016-12-13 00:51:02 +00:00
return coroutine ( function () use ( $capabilities , $rootPath , $processId ) {
2017-01-05 03:18:14 +00:00
yield null ;
2016-12-13 00:51:02 +00:00
if ( $capabilities -> xfilesProvider ) {
$this -> filesFinder = new ClientFilesFinder ( $this -> client );
} else {
$this -> filesFinder = new FileSystemFilesFinder ;
}
if ( $capabilities -> xcontentProvider ) {
$this -> contentRetriever = new ClientContentRetriever ( $this -> client );
} else {
$this -> contentRetriever = new FileSystemContentRetriever ;
}
$projectIndex = new ProjectIndex ( new Index , new DependenciesIndex );
$stubsIndex = StubsIndex :: read ();
$globalIndex = new GlobalIndex ( $stubsIndex , $projectIndex );
// The DefinitionResolver should look in stubs, the project source and dependencies
$definitionResolver = new DefinitionResolver ( $globalIndex );
$this -> documentLoader = new PhpDocumentLoader (
$this -> contentRetriever ,
$projectIndex ,
$definitionResolver
2016-12-15 08:47:59 +00:00
);
2016-12-13 00:51:02 +00:00
if ( $rootPath !== null ) {
2017-01-05 03:18:14 +00:00
$this -> index ( $rootPath ) -> otherwise ( '\\LanguageServer\\crash' );
2016-12-13 00:51:02 +00:00
}
$this -> textDocument = new Server\TextDocument (
$this -> documentLoader ,
$definitionResolver ,
$this -> client ,
$globalIndex
2016-12-15 08:47:59 +00:00
);
2016-12-13 00:51:02 +00:00
// workspace/symbol should only look inside the project source and dependencies
$this -> workspace = new Server\Workspace ( $projectIndex , $this -> client );
2016-12-15 08:47:59 +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-12-13 00:51:02 +00:00
$serverCapabilities = new ServerCapabilities ();
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
$serverCapabilities -> textDocumentSync = TextDocumentSyncKind :: FULL ;
// Support "Find all symbols"
$serverCapabilities -> documentSymbolProvider = true ;
// Support "Find all symbols in workspace"
$serverCapabilities -> workspaceSymbolProvider = true ;
// Support "Format Code"
$serverCapabilities -> documentFormattingProvider = true ;
// Support "Go to definition"
$serverCapabilities -> definitionProvider = true ;
// Support "Find all references"
$serverCapabilities -> referencesProvider = true ;
// Support "Hover"
$serverCapabilities -> hoverProvider = true ;
// Support "Completion"
$serverCapabilities -> completionProvider = new CompletionOptions ;
$serverCapabilities -> completionProvider -> resolveProvider = false ;
$serverCapabilities -> completionProvider -> triggerCharacters = [ '$' , '>' ];
return new InitializeResult ( $serverCapabilities );
});
2016-08-25 13:27:14 +00:00
}
/**
* 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
/**
2016-12-13 00:51:02 +00:00
* Will read and parse the passed source files in the project and add them to the appropiate indexes
2016-09-30 09:30:08 +00:00
*
2017-01-05 03:18:14 +00:00
* @ param string $rootPath
2016-11-14 09:25:44 +00:00
* @ return Promise < void >
2016-09-30 09:30:08 +00:00
*/
2017-01-05 03:18:14 +00:00
protected function index ( string $rootPath ) : Promise
2016-09-30 09:30:08 +00:00
{
2017-01-05 03:18:14 +00:00
return coroutine ( function () use ( $rootPath ) {
$pattern = Path :: makeAbsolute ( '**/*.php' , $rootPath );
$uris = yield $this -> filesFinder -> find ( $pattern );
2016-12-13 00:51:02 +00:00
2016-12-08 01:33:48 +00:00
$count = count ( $uris );
2016-11-14 09:25:44 +00:00
$startTime = microtime ( true );
2016-12-13 01:11:29 +00:00
foreach ([ 'Collecting definitions and static references' , 'Collecting dynamic references' ] as $run ) {
$this -> client -> window -> logMessage ( MessageType :: INFO , $run );
foreach ( $uris as $i => $uri ) {
if ( $this -> documentLoader -> isOpen ( $uri )) {
continue ;
2016-12-13 00:51:02 +00:00
}
2016-12-13 01:11:29 +00:00
// Give LS to the chance to handle requests while indexing
yield timeout ();
2016-11-14 09:25:44 +00:00
$this -> client -> window -> logMessage (
2016-12-13 01:11:29 +00:00
MessageType :: LOG ,
" Parsing file $i / $count : { $uri } "
2016-12-15 08:47:59 +00:00
);
2016-12-13 01:11:29 +00:00
try {
$document = yield $this -> documentLoader -> load ( $uri );
if ( ! $document -> isVendored ()) {
$this -> client -> textDocument -> publishDiagnostics ( $uri , $document -> getDiagnostics ());
}
} catch ( ContentTooLargeException $e ) {
$this -> client -> window -> logMessage (
MessageType :: INFO ,
" Ignoring file { $uri } because it exceeds size limit of { $e -> limit } bytes ( { $e -> size } ) "
2016-12-15 08:47:59 +00:00
);
2016-12-13 01:11:29 +00:00
} catch ( Exception $e ) {
$this -> client -> window -> logMessage (
MessageType :: ERROR ,
" Error parsing file { $uri } : " . ( string ) $e
2016-12-15 08:47:59 +00:00
);
2016-12-13 01:11:29 +00:00
}
2016-11-17 21:20:37 +00:00
}
2016-12-13 01:11:29 +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-12-15 08:47:59 +00:00
);
2016-11-17 21:20:37 +00:00
}
2016-11-14 09:25:44 +00:00
});
}
2016-08-22 15:32:31 +00:00
}