2016-09-30 09:30:08 +00:00
|
|
|
<?php
|
2016-09-30 09:54:49 +00:00
|
|
|
declare(strict_types = 1);
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
namespace LanguageServer;
|
|
|
|
|
2016-11-14 09:25:44 +00:00
|
|
|
use Throwable;
|
2016-10-10 13:06:02 +00:00
|
|
|
use InvalidArgumentException;
|
2017-01-25 00:38:11 +00:00
|
|
|
use Sabre\Event\{Loop, Promise, EmitterInterface};
|
2017-02-07 22:20:12 +00:00
|
|
|
use Sabre\Uri;
|
2016-09-30 09:30:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Transforms an absolute file path into a URI as used by the language server protocol.
|
|
|
|
*
|
|
|
|
* @param string $filepath
|
|
|
|
* @return string
|
|
|
|
*/
|
2016-10-24 17:35:37 +00:00
|
|
|
function pathToUri(string $filepath): string
|
|
|
|
{
|
2016-09-30 09:30:08 +00:00
|
|
|
$filepath = trim(str_replace('\\', '/', $filepath), '/');
|
2016-10-10 13:06:02 +00:00
|
|
|
$parts = explode('/', $filepath);
|
|
|
|
// Don't %-encode the colon after a Windows drive letter
|
|
|
|
$first = array_shift($parts);
|
|
|
|
if (substr($first, -1) !== ':') {
|
2016-11-19 11:25:52 +00:00
|
|
|
$first = rawurlencode($first);
|
2016-10-10 13:06:02 +00:00
|
|
|
}
|
2016-11-07 09:24:49 +00:00
|
|
|
$parts = array_map('rawurlencode', $parts);
|
2016-10-10 13:06:02 +00:00
|
|
|
array_unshift($parts, $first);
|
|
|
|
$filepath = implode('/', $parts);
|
2016-09-30 09:30:08 +00:00
|
|
|
return 'file:///' . $filepath;
|
|
|
|
}
|
2016-10-10 13:06:02 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Transforms URI into file path
|
|
|
|
*
|
|
|
|
* @param string $uri
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function uriToPath(string $uri)
|
|
|
|
{
|
|
|
|
$fragments = parse_url($uri);
|
|
|
|
if ($fragments === null || !isset($fragments['scheme']) || $fragments['scheme'] !== 'file') {
|
|
|
|
throw new InvalidArgumentException("Not a valid file URI: $uri");
|
|
|
|
}
|
|
|
|
$filepath = urldecode($fragments['path']);
|
2016-10-11 22:53:21 +00:00
|
|
|
if (strpos($filepath, ':') !== false) {
|
|
|
|
if ($filepath[0] === '/') {
|
|
|
|
$filepath = substr($filepath, 1);
|
|
|
|
}
|
|
|
|
$filepath = str_replace('/', '\\', $filepath);
|
|
|
|
}
|
|
|
|
return $filepath;
|
2016-10-10 13:06:02 +00:00
|
|
|
}
|
2016-11-14 09:25:44 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Throws an exception on the next tick.
|
|
|
|
* Useful for letting a promise crash the process on rejection.
|
|
|
|
*
|
|
|
|
* @param Throwable $err
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
function crash(Throwable $err)
|
|
|
|
{
|
|
|
|
Loop\nextTick(function () use ($err) {
|
|
|
|
throw $err;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a promise that is resolved after x seconds.
|
|
|
|
* Useful for giving back control to the event loop inside a coroutine.
|
|
|
|
*
|
|
|
|
* @param int $seconds
|
|
|
|
* @return Promise <void>
|
|
|
|
*/
|
|
|
|
function timeout($seconds = 0): Promise
|
|
|
|
{
|
|
|
|
$promise = new Promise;
|
|
|
|
Loop\setTimeout([$promise, 'fulfill'], $seconds);
|
|
|
|
return $promise;
|
|
|
|
}
|
2016-11-18 14:22:24 +00:00
|
|
|
|
2017-01-25 00:38:11 +00:00
|
|
|
/**
|
|
|
|
* Returns a promise that is fulfilled once the passed event was triggered on the passed EventEmitter
|
|
|
|
*
|
|
|
|
* @param EmitterInterface $emitter
|
|
|
|
* @param string $event
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
function waitForEvent(EmitterInterface $emitter, string $event): Promise
|
|
|
|
{
|
|
|
|
$p = new Promise;
|
|
|
|
$emitter->once($event, [$p, 'fulfill']);
|
|
|
|
return $p;
|
|
|
|
}
|
|
|
|
|
2016-11-30 21:23:51 +00:00
|
|
|
/**
|
|
|
|
* Returns the part of $b that is not overlapped by $a
|
|
|
|
* Example:
|
|
|
|
*
|
|
|
|
* stripStringOverlap('whatever<?', '<?php') === 'php'
|
|
|
|
*
|
|
|
|
* @param string $a
|
|
|
|
* @param string $b
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function stripStringOverlap(string $a, string $b): string
|
|
|
|
{
|
|
|
|
$aLen = strlen($a);
|
|
|
|
$bLen = strlen($b);
|
|
|
|
for ($i = 1; $i <= $bLen; $i++) {
|
|
|
|
if (substr($b, 0, $i) === substr($a, $aLen - $i)) {
|
|
|
|
return substr($b, $i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $b;
|
|
|
|
}
|
2017-02-07 22:20:12 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Use for sorting an array of URIs by number of segments
|
|
|
|
* in ascending order.
|
|
|
|
*
|
|
|
|
* @param array $uriList
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
function sortUrisLevelOrder(&$uriList)
|
|
|
|
{
|
|
|
|
usort($uriList, function ($a, $b) {
|
|
|
|
return substr_count(Uri\parse($a)['path'], '/') - substr_count(Uri\parse($b)['path'], '/');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks a document against the composer.json to see if it
|
|
|
|
* is a vendored document
|
|
|
|
*
|
|
|
|
* @param PhpDocument $document
|
|
|
|
* @param \stdClass|null $composerJson
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool
|
|
|
|
{
|
|
|
|
$path = Uri\parse($document->getUri())['path'];
|
|
|
|
$vendorDir = getVendorDir($composerJson);
|
|
|
|
return strpos($path, "/$vendorDir/") !== false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check a given URI against the composer.json to see if it
|
|
|
|
* is a vendored URI
|
|
|
|
*
|
|
|
|
* @param string $uri
|
2017-10-23 05:54:38 +00:00
|
|
|
* @param \stdClass|null $composerJson
|
2017-02-07 22:20:12 +00:00
|
|
|
* @return string|null
|
|
|
|
*/
|
|
|
|
function getPackageName(string $uri, \stdClass $composerJson = null)
|
|
|
|
{
|
|
|
|
$vendorDir = str_replace('/', '\/', getVendorDir($composerJson));
|
|
|
|
preg_match("/\/$vendorDir\/([^\/]+\/[^\/]+)\//", $uri, $matches);
|
|
|
|
return $matches[1] ?? null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to get the vendor directory from composer.json
|
|
|
|
* or default to 'vendor'
|
|
|
|
*
|
|
|
|
* @param \stdClass|null $composerJson
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function getVendorDir(\stdClass $composerJson = null): string
|
|
|
|
{
|
|
|
|
return $composerJson->config->{'vendor-dir'} ?? 'vendor';
|
|
|
|
}
|