Skip to content

Commit 501d26e

Browse files
sunverwerthfelixfbecker
authored andcommitted
Global symbol search (#31)
* Implemented workspace symbol search * Fixed missing TextEdit using declaration * Fixed generating uri when parsing next file. * Cleaned up code. Fixed tests * Fixed PHPDoc for LanguageServer::initialize() * Moved utility functions to utils.php * Added tests for pathToUri and findFilesRecursive * Added command line argument for socket communication * Fixed local variable detection and containerName generation in SymbolFinder * Fixed formatting in ProtocolStreamReader * Store text content in PHPDocument, removed stmts, regenerate on demand * Fixed local variable detection and containerName generation in SymbolFinder. * Added Tests for Project and Workspace * Added test for didChange event * Modified lexer error handling * Removed file that shouldn't have been committed. * Updated sabre/event dependency to 4.0.0 * Updated readme.md to show tcp option * make input stream non-blocking * Correct code style * Use triple equals * Revert change in SymbolFinder * Optimize processFile() a bit * Use MessageType enum instead of number literal * Add missing space * Fixed ProtocolStreamWriter for nonblocking connection. * Suppress fwrite() notice when not all bytes could be written. * Fix another code style issue * Throw Exceotion instead of Error * Added ProtocolStreamWriter test * Correct workspace/symbol documentation * Improve exception in ProtocolStreamWriter::write()
1 parent bc2d6b9 commit 501d26e

24 files changed

+836
-109
lines changed

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,15 @@ to install dependencies.
2626
Run the tests with
2727

2828
vendor/bin/phpunit --bootstrap vendor/autoload.php tests
29+
30+
## Command line arguments
31+
32+
--tcp host:port
33+
34+
Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT.
35+
The server will try to connect to the specified address.
36+
37+
Example:
38+
39+
php bin/php-language-server.php --tcp 127.0.0.1:12345
40+

bin/php-language-server.php

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
ini_set('memory_limit', '-1');
4+
35
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
46
use Sabre\Event\Loop;
57

@@ -10,6 +12,22 @@
1012
}
1113
}
1214

13-
$server = new LanguageServer(new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT));
15+
if (count($argv) >= 3 && $argv[1] === '--tcp') {
16+
$address = $argv[2];
17+
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
18+
if ($socket === false) {
19+
fwrite(STDERR, "Could not connect to language client. Error $errno\n");
20+
fwrite(STDERR, "$errstr\n");
21+
exit(1);
22+
}
23+
$inputStream = $outputStream = $socket;
24+
} else {
25+
$inputStream = STDIN;
26+
$outputStream = STDOUT;
27+
}
28+
29+
stream_set_blocking($inputStream, false);
30+
31+
$server = new LanguageServer(new ProtocolStreamReader($inputStream), new ProtocolStreamWriter($outputStream));
1432

1533
Loop\run();

composer.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,16 @@
2626
"php": ">=7.0",
2727
"nikic/php-parser": "^3.0.0beta1",
2828
"phpdocumentor/reflection-docblock": "^3.0",
29-
"sabre/event": "^3.0",
29+
"sabre/event": "^4.0",
3030
"felixfbecker/advanced-json-rpc": "^1.2"
3131
},
3232
"minimum-stability": "dev",
3333
"prefer-stable": true,
3434
"autoload": {
3535
"psr-4": {
3636
"LanguageServer\\": "src/"
37-
}
37+
},
38+
"files" : ["src/utils.php"]
3839
},
3940
"autoload-dev": {
4041
"psr-4": {

fixtures/recursive/a.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A

fixtures/recursive/search/b.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
B

fixtures/recursive/search/here/c.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Peeakboo!

src/Client/Window.php

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace LanguageServer\Client;
5+
6+
use AdvancedJsonRpc\Notification as NotificationBody;
7+
use PhpParser\{Error, Comment, Node, ParserFactory, NodeTraverser, Lexer};
8+
use PhpParser\NodeVisitor\NameResolver;
9+
use LanguageServer\ProtocolWriter;
10+
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, Message};
11+
12+
/**
13+
* Provides method handlers for all window/* methods
14+
*/
15+
class Window
16+
{
17+
/**
18+
* @var ProtocolWriter
19+
*/
20+
private $protocolWriter;
21+
22+
public function __construct(ProtocolWriter $protocolWriter)
23+
{
24+
$this->protocolWriter = $protocolWriter;
25+
}
26+
27+
/**
28+
* The show message notification is sent from a server to a client to ask the client to display a particular message in the user interface.
29+
*
30+
* @param int $type
31+
* @param string $message
32+
*/
33+
public function showMessage(int $type, string $message)
34+
{
35+
$this->protocolWriter->write(new Message(new NotificationBody(
36+
'window/showMessage',
37+
(object)[
38+
'type' => $type,
39+
'message' => $message
40+
]
41+
)));
42+
}
43+
44+
/**
45+
* The log message notification is sent from the server to the client to ask the client to log a particular message.
46+
*
47+
* @param int $type
48+
* @param string $message
49+
*/
50+
public function logMessage(int $type, string $message)
51+
{
52+
$this->protocolWriter->write(new Message(new NotificationBody(
53+
'window/logMessage',
54+
(object)[
55+
'type' => $type,
56+
'message' => $message
57+
]
58+
)));
59+
}
60+
}

src/LanguageClient.php

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace LanguageServer;
55

66
use LanguageServer\Client\TextDocument;
7+
use LanguageServer\Client\Window;
78

89
class LanguageClient
910
{
@@ -14,11 +15,19 @@ class LanguageClient
1415
*/
1516
public $textDocument;
1617

18+
/**
19+
* Handles window/* methods
20+
*
21+
* @var Client\Window
22+
*/
23+
public $window;
24+
1725
private $protocolWriter;
1826

1927
public function __construct(ProtocolWriter $writer)
2028
{
2129
$this->protocolWriter = $writer;
2230
$this->textDocument = new TextDocument($writer);
31+
$this->window = new Window($writer);
2332
}
2433
}

src/LanguageServer.php

+67-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33
namespace LanguageServer;
44

55
use LanguageServer\Server\TextDocument;
6-
use LanguageServer\Protocol\{ServerCapabilities, ClientCapabilities, TextDocumentSyncKind, Message};
7-
use LanguageServer\Protocol\InitializeResult;
6+
use LanguageServer\Protocol\{
7+
ServerCapabilities,
8+
ClientCapabilities,
9+
TextDocumentSyncKind,
10+
Message,
11+
MessageType,
12+
InitializeResult
13+
};
814
use AdvancedJsonRpc\{Dispatcher, ResponseError, Response as ResponseBody, Request as RequestBody};
15+
use Sabre\Event\Loop;
916

1017
class LanguageServer extends \AdvancedJsonRpc\Dispatcher
1118
{
@@ -16,16 +23,24 @@ class LanguageServer extends \AdvancedJsonRpc\Dispatcher
1623
*/
1724
public $textDocument;
1825

26+
/**
27+
* Handles workspace/* method calls
28+
*
29+
* @var Server\Workspace
30+
*/
31+
public $workspace;
32+
1933
public $telemetry;
2034
public $window;
21-
public $workspace;
2235
public $completionItem;
2336
public $codeLens;
2437

2538
private $protocolReader;
2639
private $protocolWriter;
2740
private $client;
2841

42+
private $project;
43+
2944
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
3045
{
3146
parent::__construct($this, '/');
@@ -56,24 +71,35 @@ public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
5671
});
5772
$this->protocolWriter = $writer;
5873
$this->client = new LanguageClient($writer);
59-
$this->textDocument = new Server\TextDocument($this->client);
74+
75+
$this->project = new Project($this->client);
76+
77+
$this->textDocument = new Server\TextDocument($this->project, $this->client);
78+
$this->workspace = new Server\Workspace($this->project, $this->client);
6079
}
6180

6281
/**
6382
* The initialize request is sent as the first request from the client to the server.
6483
*
65-
* @param string $rootPath The rootPath of the workspace. Is null if no folder is open.
6684
* @param int $processId The process Id of the parent process that started the server.
6785
* @param ClientCapabilities $capabilities The capabilities provided by the client (editor)
86+
* @param string|null $rootPath The rootPath of the workspace. Is null if no folder is open.
6887
* @return InitializeResult
6988
*/
70-
public function initialize(string $rootPath, int $processId, ClientCapabilities $capabilities): InitializeResult
89+
public function initialize(int $processId, ClientCapabilities $capabilities, string $rootPath = null): InitializeResult
7190
{
91+
// start building project index
92+
if ($rootPath) {
93+
$this->indexProject($rootPath);
94+
}
95+
7296
$serverCapabilities = new ServerCapabilities();
7397
// Ask the client to return always full documents (because we need to rebuild the AST from scratch)
7498
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
7599
// Support "Find all symbols"
76100
$serverCapabilities->documentSymbolProvider = true;
101+
// Support "Find all symbols in workspace"
102+
$serverCapabilities->workspaceSymbolProvider = true;
77103
// Support "Format Code"
78104
$serverCapabilities->documentFormattingProvider = true;
79105
return new InitializeResult($serverCapabilities);
@@ -100,4 +126,39 @@ public function exit()
100126
{
101127
exit(0);
102128
}
129+
130+
/**
131+
* Parses workspace files, one at a time.
132+
*
133+
* @param string $rootPath The rootPath of the workspace.
134+
* @return void
135+
*/
136+
private function indexProject(string $rootPath)
137+
{
138+
$fileList = findFilesRecursive($rootPath, '/^.+\.php$/i');
139+
$numTotalFiles = count($fileList);
140+
141+
$startTime = microtime(true);
142+
$fileNum = 0;
143+
144+
$processFile = function() use (&$fileList, &$fileNum, &$processFile, $rootPath, $numTotalFiles, $startTime) {
145+
if ($fileNum < $numTotalFiles) {
146+
$file = $fileList[$fileNum];
147+
$uri = pathToUri($file);
148+
$fileNum++;
149+
$shortName = substr($file, strlen($rootPath) + 1);
150+
$this->client->window->logMessage(MessageType::INFO, "Parsing file $fileNum/$numTotalFiles: $shortName.");
151+
152+
$this->project->getDocument($uri)->updateContent(file_get_contents($file));
153+
154+
Loop\setTimeout($processFile, 0);
155+
} else {
156+
$duration = (int)(microtime(true) - $startTime);
157+
$mem = (int)(memory_get_usage(true) / (1024 * 1024));
158+
$this->client->window->logMessage(MessageType::INFO, "All PHP files parsed in $duration seconds. $mem MiB allocated.");
159+
}
160+
};
161+
162+
Loop\setTimeout($processFile, 0);
163+
}
103164
}

0 commit comments

Comments
 (0)