Repository: sublimelsp/LSP

Issue:
Error handling server payload in php intelephense language server
### language server - [intelephense](https://github.com/bmewburn/intelephense-server)
### Minimal reproduction steps

In a Laravel project in a class that extends TestCase,
after typying `$this->` expect to see method suggestions but LSP throws `Error handling server payload` Exception. 

### Here is the log:

```
:  --> textDocument/didChange
LSP:  --> textDocument/documentHighlight
LSP:      []
LSP:  --> textDocument/didChange
LSP:  --> textDocument/completion
LSP:      {'items': [{'documentation': 'A basic test example.', 'kind': 2, 'detail': 'testBasicTest(): void', 'sortText': 'testBasicTest', 'insertText': 'testBasicTest()', 'label': 'testBasicTest'}, {'detail': 'Illuminate\\Foundation\\Application', 'label': 'app', 'kind': 10}, {'detail': 'array', 'label': 'afterApplicationCreatedCallbacks', 'kind': 10}, {'detail': 'array', 'label': 'beforeApplicationDestroyedCallbacks', 'kind': 10}, {'detail': 'bool', 'label': 'setUpHasRun', 'kind': 10}, {'documentation': 'Creates the application.\n\nNeeds to be implemented by subclasses.', 'kind': 2, 'detail': 'createApplication(): Symfony\\Component\\HttpKernel\\HttpKernelInterface', 'sortText': 'createApplication', 'insertText': 'createApplication()', 'label': 'createApplication'}, {'documentation': 'Setup the test environment.', 'kind': 2, 'detail': 'setUp(): void', 'sortText': 'setUp', 'insertText': 'setUp()', 'label': 'setUp'}, {'documentation': 'Refresh the application instance.', 'kind': 2, 'detail': 'refreshApplication(): void', 'sortText': 'refreshApplication', 'insertText': 'refreshApplication()', 'label': 'refreshApplication'}, {'documentation': 'Boot the testing helper traits.', 'kind': 2, 'detail': 'setUpTraits(): array', 'sortText': 'setUpTraits', 'insertText': 'setUpTraits()', 'label': 'setUpTraits'}, {'documentation': 'Clean up the testing environment before the next test.', 'kind': 2, 'detail': 'tearDown(): void', 'sortText': 'tearDown', 'insertText': 'tearDown()', 'label': 'tearDown'}, {'documentation': 'Register a callback to be run after the application is created.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'afterApplicationCreated(callable $callback): void', 'sortText': 'afterApplicationCreated', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'afterApplicationCreated($0)', 'label': 'afterApplicationCreated'}, {'documentation': 'Register a callback to be run before the application is destroyed.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'beforeApplicationDestroyed(callable $callback): void', 'sortText': 'beforeApplicationDestroyed', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'beforeApplicationDestroyed($0)', 'label': 'beforeApplicationDestroyed'}, {'documentation': 'Register an instance of an object in the container.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'swap(string $abstract, object $instance): object', 'sortText': 'swap', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'swap($0)', 'label': 'swap'}, {'documentation': 'Register an instance of an object in the container.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'instance(string $abstract, object $instance): object', 'sortText': 'instance', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'instance($0)', 'label': 'instance'}, {'detail': 'array', 'label': 'defaultHeaders', 'kind': 10}, {'detail': 'array', 'label': 'serverVariables', 'kind': 10}, {'detail': 'bool', 'label': 'followRedirects', 'kind': 10}, {'documentation': 'Define additional headers to be sent with the request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'withHeaders(array $headers): Tests\\Feature\\ExampleTest', 'sortText': 'withHeaders', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'withHeaders($0)', 'label': 'withHeaders'}, {'documentation': 'Add a header to be sent with the request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'withHeader(string $name, string $value): Tests\\Feature\\ExampleTest', 'sortText': 'withHeader', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'withHeader($0)', 'label': 'withHeader'}, {'documentation': 'Flush all the configured headers.', 'kind': 2, 'detail': 'flushHeaders(): Tests\\Feature\\ExampleTest', 'sortText': 'flushHeaders', 'insertText': 'flushHeaders()', 'label': 'flushHeaders'}, {'documentation': 'Define a set of server variables to be sent with the requests.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'withServerVariables(array $server): Tests\\Feature\\ExampleTest', 'sortText': 'withServerVariables', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'withServerVariables($0)', 'label': 'withServerVariables'}, {'documentation': 'Disable middleware for the test.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'withoutMiddleware([string|array $middleware]): Tests\\Feature\\ExampleTest', 'sortText': 'withoutMiddleware', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'withoutMiddleware($0)', 'label': 'withoutMiddleware'}, {'documentation': 'Enable the given middleware for the test.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'withMiddleware([string|array $middleware]): Tests\\Feature\\ExampleTest', 'sortText': 'withMiddleware', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'withMiddleware($0)', 'label': 'withMiddleware'}, {'documentation': 'Automatically follow any redirects returned from the response.', 'kind': 2, 'detail': 'followingRedirects(): Tests\\Feature\\ExampleTest', 'sortText': 'followingRedirects', 'insertText': 'followingRedirects()', 'label': 'followingRedirects'}, {'documentation': 'Set the referer header to simulate a previous request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'from(string $url): Tests\\Feature\\ExampleTest', 'sortText': 'from', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'from($0)', 'label': 'from'}, {'documentation': 'Visit the given URI with a GET request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'get(string $uri [, array $headers]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'get', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'get($0)', 'label': 'get'}, {'documentation': 'Visit the given URI with a GET request, expecting a JSON response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'getJson(string $uri [, array $headers]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'getJson', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'getJson($0)', 'label': 'getJson'}, {'documentation': 'Visit the given URI with a POST request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'post(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'post', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'post($0)', 'label': 'post'}, {'documentation': 'Visit the given URI with a POST request, expecting a JSON response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'postJson(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'postJson', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'postJson($0)', 'label': 'postJson'}, {'documentation': 'Visit the given URI with a PUT request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'put(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'put', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'put($0)', 'label': 'put'}, {'documentation': 'Visit the given URI with a PUT request, expecting a JSON response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'putJson(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'putJson', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'putJson($0)', 'label': 'putJson'}, {'documentation': 'Visit the given URI with a PATCH request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'patch(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'patch', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'patch($0)', 'label': 'patch'}, {'documentation': 'Visit the given URI with a PATCH request, expecting a JSON response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'patchJson(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'patchJson', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'patchJson($0)', 'label': 'patchJson'}, {'documentation': 'Visit the given URI with a DELETE request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'delete(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'delete', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'delete($0)', 'label': 'delete'}, {'documentation': 'Visit the given URI with a DELETE request, expecting a JSON response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'deleteJson(string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'deleteJson', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'deleteJson($0)', 'label': 'deleteJson'}, {'documentation': 'Call the given URI with a JSON request.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'json(string $method, string $uri [, array $data [, array $headers]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'json', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'json($0)', 'label': 'json'}, {'documentation': 'Call the given URI and return the Response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'call(string $method, string $uri [, array $parameters [, array $cookies [, array $files [, array $server [, string $content]]]]]): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'call', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'call($0)', 'label': 'call'}, {'documentation': 'Turn the given URI into a fully qualified URL.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'prepareUrlForRequest(string $uri): string', 'sortText': 'prepareUrlForRequest', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'prepareUrlForRequest($0)', 'label': 'prepareUrlForRequest'}, {'documentation': 'Transform headers array to array of $_SERVER vars with HTTP_* format.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'transformHeadersToServerVars(array $headers): array', 'sortText': 'transformHeadersToServerVars', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'transformHeadersToServerVars($0)', 'label': 'transformHeadersToServerVars'}, {'documentation': 'Format the header name for the server array.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'formatServerHeaderKey(string $name): string', 'sortText': 'formatServerHeaderKey', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'formatServerHeaderKey($0)', 'label': 'formatServerHeaderKey'}, {'documentation': 'Extract the file uploads from the given data array.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'extractFilesFromDataArray(array $data): array', 'sortText': 'extractFilesFromDataArray', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'extractFilesFromDataArray($0)', 'label': 'extractFilesFromDataArray'}, {'documentation': 'Follow a redirect chain until a non-redirect is received.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'followRedirects(Illuminate\\Http\\Response $response): Illuminate\\Http\\Response', 'sortText': 'followRedirects', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'followRedirects($0)', 'label': 'followRedirects'}, {'documentation': 'Create the test response instance from the given response.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'createTestResponse(Illuminate\\Http\\Response $response): Illuminate\\Foundation\\Testing\\TestResponse', 'sortText': 'createTestResponse', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'createTestResponse($0)', 'label': 'createTestResponse'}, {'documentation': 'Assert that a given where condition exists in the database.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'assertDatabaseHas(string $table, array $data [, string $connection]): Tests\\Feature\\ExampleTest', 'sortText': 'assertDatabaseHas', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'assertDatabaseHas($0)', 'label': 'assertDatabaseHas'}, {'documentation': 'Assert that a given where condition does not exist in the database.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'assertDatabaseMissing(string $table, array $data [, string $connection]): Tests\\Feature\\ExampleTest', 'sortText': 'assertDatabaseMissing', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'assertDatabaseMissing($0)', 'label': 'assertDatabaseMissing'}, {'documentation': 'Assert the given record has been deleted.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'assertSoftDeleted(string $table, array $data [, string $connection]): Tests\\Feature\\ExampleTest', 'sortText': 'assertSoftDeleted', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'assertSoftDeleted($0)', 'label': 'assertSoftDeleted'}, {'documentation': 'Get the database connection.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'getConnection([string|null $connection]): Illuminate\\Database\\Connection', 'sortText': 'getConnection', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'getConnection($0)', 'label': 'getConnection'}, {'documentation': 'Seed a given database connection.', 'insertTextFormat': 2, 'kind': 2, 'detail': 'seed([string $class]): Tests\\Feature\\ExampleTest', 'sortText': 'seed', 'command': {'title': 'Trigger Parameter Hints', 'command': 'editor.action.triggerParameterHints'}, 'insertText': 'seed($0)', 'label': 'seed'}], 'isIncomplete': False}
Error handling server payload
Traceback (most recent call last):
  File "/home/predrag/.config/sublime-text-3/Packages/LSP/plugin/core/rpc.py", line 323, in receive_payload
    self.response_handler(payload)
  File "/home/predrag/.config/sublime-text-3/Packages/LSP/plugin/core/rpc.py", line 342, in response_handler
    self._response_handlers[handler_id](result)
  File "/home/predrag/.config/sublime-text-3/Packages/LSP/plugin/completion.py", line 262, in handle_response
    items = sorted(items, key=lambda item: item["sortText"])
  File "/home/predrag/.config/sublime-text-3/Packages/LSP/plugin/completion.py", line 262, in <lambda>
    items = sorted(items, key=lambda item: item["sortText"])
KeyError: 'sortText'

LSP: <--  textDocument/publishDiagnostics
LSP:      {'diagnostics': [{'message': 'Unexpected CloseBrace.', 'severity': 1, 'range': {'end': {'character': 5, 'line': 20}, 'start': {'character': 4, 'line': 20}}, 'source': 'intelephense'}], 'uri': 'file:///home/predrag/Sites/StudentskaSluzba/tests/Feature/ExampleTest.php'}

```



Discussion:
This looks like my code. Oops! I thought we could rely on servers either sending *all* completion items with the `sortText` property or *no* completion items with the `sortText` property. But intelephense seems to mix both.

Resolving patch:
diff --git a/plugin/completion.py b/plugin/completion.py
--- a/plugin/completion.py
+++ b/plugin/completion.py
@@ -257,9 +257,7 @@ def handle_response(self, response: 'Optional[Dict]'):
                 items = response["items"]
             elif isinstance(response, list):
                 items = response
-            if len(items) > 1 and items[0].get("sortText") is not None:
-                # If the first item has a sortText value, assume all of them have a sortText value.
-                items = sorted(items, key=lambda item: item["sortText"])
+            items = sorted(items, key=lambda item: item.get("sortText", item["label"]))
             self.completions = list(self.format_completion(item) for item in items)
 
             if self.has_resolve_provider:


Test patch: