Repository: sublimelsp/LSP

Issue:
'lsp_symbol_definition' via keybinding  using `ccls' as client.
While I was trying to bind the 'lsp_symbol_definition'-command to my custom keys, I had so suffer from a `... type str does not support Buffer API´.
On revisiting the relevant file ("plugin/goto.py") I found the TODO comment which mentions the future implementation of DocumentLink-support.

By dumping the response-payload of the 'lsp_symbol_definition'-command I found out that the received payload has different keys as beeing parsed by the 'handle_response()' function.
See here:
```
[{targetUri: file:///usr/include/bash/builtins/bashgetopt.h, targetRange: {end: {line: 38, character: 30}, start: {line:
38, character: 0}}, targetSelectionRange: {end: {line: 38, character: 26}, start: {line: 38, character: 11}}}]
```

### Solution

So I added the following snippet to provide the right parsing method in case of this setup/scenario:

```python
            if 'targetUri' in location:
                file_path = uri_to_filename(location['targetUri'])
                start = Point.from_lsp(location['targetRange']['start'])
            else:
                file_path = uri_to_filename(location["uri"])
                start = Point.from_lsp(location['range']['start'])
```

By adding this the desired function of my keybinding worked.

I just wanted to share this in the hope it might be useful.
Best regards !



Discussion:
Instead of targetRange, should we not use targetSelectionRange? Where does the cursor end up on with targetRange versus targetSelectionRange in the case of CCLS? Can you provide some examples?

Here's the definition of LocationLink for reference:

```
interface LocationLink {

	/**
	 * Span of the origin of this link.
	 *
	 * Used as the underlined span for mouse interaction. Defaults to the word range at
	 * the mouse position.
	 */
	originSelectionRange?: Range;

	/**
	 * The target resource identifier of this link.
	 */
	targetUri: DocumentUri;

	/**
	 * The full target range of this link. If the target for example is a symbol then target range is the
	 * range enclosing this symbol not including leading/trailing whitespace but everything else
	 * like comments. This information is typically used to highlight the range in the editor.
	 */
	targetRange: Range;

	/**
	 * The range that should be selected and revealed when this link is being followed, e.g the name of a function.
	 * Must be contained by the the `targetRange`. See also `DocumentSymbol#range`
	 */
	targetSelectionRange: Range;
}
```
Also, is this LocationLink support not something the server should detect support for?

Resolving patch:
diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py
--- a/plugin/core/sessions.py
+++ b/plugin/core/sessions.py
@@ -110,10 +110,10 @@ def get_initialize_params(project_path: str, config: ClientConfig) -> dict:
                 },
                 "formatting": {},
                 "rangeFormatting": {},
-                "declaration": {},
-                "definition": {},
-                "typeDefinition": {},
-                "implementation": {},
+                "declaration": {"linkSupport": True},
+                "definition": {"linkSupport": True},
+                "typeDefinition": {"linkSupport": True},
+                "implementation": {"linkSupport": True},
                 "codeAction": {
                     "codeActionLiteralSupport": {
                         "codeActionKind": {
diff --git a/plugin/goto.py b/plugin/goto.py
--- a/plugin/goto.py
+++ b/plugin/goto.py
@@ -46,8 +46,13 @@ def handle_response(self, response: 'Optional[Any]', position: int) -> None:
             get_jump_history_for_view(self.view).push_selection(self.view)
             # TODO: DocumentLink support.
             location = response if isinstance(response, dict) else response[0]
-            file_path = uri_to_filename(location.get("uri"))
-            start = Point.from_lsp(location['range']['start'])
+            if "targetUri" in location:
+                # TODO: Do something clever with originSelectionRange and targetRange.
+                file_path = uri_to_filename(location["targetUri"])
+                start = Point.from_lsp(location["targetSelectionRange"]["start"])
+            else:
+                file_path = uri_to_filename(location["uri"])
+                start = Point.from_lsp(location["range"]["start"])
             file_location = "{}:{}:{}".format(file_path, start.row + 1, start.col + 1)
             debug("opening location", location)
             window.open_file(file_location, sublime.ENCODED_POSITION)


Test patch:
diff --git a/tests/test_goto.py b/tests/test_goto.py
--- a/tests/test_goto.py
+++ b/tests/test_goto.py
@@ -21,6 +21,16 @@
     }
 ]
 
+RESPONSE_LOCATION_LINK = [
+    {
+        'originSelectionRange': {'start': {'line': 0, 'character': 0}},
+        'targetUri': RESPONSE[0]['uri'],
+        'targetRange': RESPONSE[0]['range'],
+        'targetSelectionRange': RESPONSE[0]['range']
+    }
+]
+
+
 CONTENT = r'''abcdefghijklmnopqrstuvwxyz
 ABCDEFGHIJKLMNOPQRSTUVWXYZ
 0123456789
@@ -36,12 +46,12 @@ def after_cursor(view) -> str:
 
 class GotoTestCase(TextDocumentTestCase):
 
-    def do_run(self, text_document_request: str, subl_command_suffix: str) -> None:
+    def do_run(self, response: dict, text_document_request: str, subl_command_suffix: str) -> None:
         self.view.run_command('insert', {'characters': CONTENT})
         # Put the cursor back at the start of the buffer, otherwise is_at_word fails in goto.py.
         self.view.sel().clear()
         self.view.sel().add(sublime.Region(0, 0))
-        self.client.responses['textDocument/{}'.format(text_document_request)] = RESPONSE
+        self.client.responses['textDocument/{}'.format(text_document_request)] = response
         self.view.run_command('lsp_symbol_{}'.format(subl_command_suffix))
 
     def do_common_checks(self) -> None:
@@ -57,24 +67,48 @@ def do_common_checks(self) -> None:
 
     def test_definition(self):
         yield 100
-        self.do_run('definition', 'definition')
+        self.do_run(RESPONSE, 'definition', 'definition')
+        yield 100
+        self.do_common_checks()
+
+    def test_definition_location_link(self):
+        yield 100
+        self.do_run(RESPONSE_LOCATION_LINK, 'definition', 'definition')
         yield 100
         self.do_common_checks()
 
     def test_type_definition(self):
         yield 100
-        self.do_run('typeDefinition', 'type_definition')
+        self.do_run(RESPONSE, 'typeDefinition', 'type_definition')
+        yield 100
+        self.do_common_checks()
+
+    def test_type_definition_location_link(self):
+        yield 100
+        self.do_run(RESPONSE_LOCATION_LINK, 'typeDefinition', 'type_definition')
         yield 100
         self.do_common_checks()
 
     def test_declaration(self):
         yield 100
-        self.do_run('declaration', 'declaration')
+        self.do_run(RESPONSE, 'declaration', 'declaration')
+        yield 100
+        self.do_common_checks()
+
+    def test_declaration_location_link(self):
+        yield 100
+        self.do_run(RESPONSE_LOCATION_LINK, 'declaration', 'declaration')
         yield 100
         self.do_common_checks()
 
     def test_implementation(self):
         yield 100
-        self.do_run('implementation', 'implementation')
+        self.do_run(RESPONSE, 'implementation', 'implementation')
+        yield 100
+        self.do_common_checks()
+
+    def test_implementation_location_link(self):
+        yield 100
+        self.do_run(RESPONSE_LOCATION_LINK, 'implementation', 'implementation')
         yield 100
         self.do_common_checks()