Skip to content

Commit 876e924

Browse files
authored
Handle custom URI schemes in hover text links (#2339)
* Handle custom URI schemes in hover text links Before this change, custom URI schemes would be delegated to the browser. With this change, an attempt is made to look for a session that can handle the URI. Moreover, if there is a fragment present in the URI, then it's assumed that that fragment encodes the (row, col) to jump to. This logic is somewhat dubious, but it's the only reasonable way (as far as I can see) to handle this. Anoher approach could be to introduce a new on_open_uri2_async callback for plugins where they can also provide a (row, col) to jump to. But, then we would also require an opt-in switch for plugins where they would advertise that they can handle the "version 2" of on_open_uri.
1 parent 07daed8 commit 876e924

File tree

3 files changed

+48
-27
lines changed

3 files changed

+48
-27
lines changed

plugin/core/open.py

+23-22
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,37 @@
2121
FRAGMENT_PATTERN = re.compile(r'^L?(\d+)(?:,(\d+))?(?:-L?(\d+)(?:,(\d+))?)?')
2222

2323

24+
def lsp_range_from_uri_fragment(fragment: str) -> Optional[Range]:
25+
match = FRAGMENT_PATTERN.match(fragment)
26+
if match:
27+
selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range
28+
# Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based
29+
# numbers for the LSP Position structure.
30+
start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()]
31+
if start_line:
32+
selection['start']['line'] = start_line
33+
selection['end']['line'] = start_line
34+
if start_column:
35+
selection['start']['character'] = start_column
36+
selection['end']['character'] = start_column
37+
if end_line:
38+
selection['end']['line'] = end_line
39+
selection['end']['character'] = UINT_MAX
40+
if end_column is not None:
41+
selection['end']['character'] = end_column
42+
return selection
43+
return None
44+
45+
2446
def open_file_uri(
2547
window: sublime.Window, uri: DocumentUri, flags: int = 0, group: int = -1
2648
) -> Promise[Optional[sublime.View]]:
2749

28-
def parse_fragment(fragment: str) -> Optional[Range]:
29-
match = FRAGMENT_PATTERN.match(fragment)
30-
if match:
31-
selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range
32-
# Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based
33-
# numbers for the LSP Position structure.
34-
start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()]
35-
if start_line:
36-
selection['start']['line'] = start_line
37-
selection['end']['line'] = start_line
38-
if start_column:
39-
selection['start']['character'] = start_column
40-
selection['end']['character'] = start_column
41-
if end_line:
42-
selection['end']['line'] = end_line
43-
selection['end']['character'] = UINT_MAX
44-
if end_column is not None:
45-
selection['end']['character'] = end_column
46-
return selection
47-
return None
48-
4950
decoded_uri = unquote(uri) # decode percent-encoded characters
5051
parsed = urlparse(decoded_uri)
5152
open_promise = open_file(window, decoded_uri, flags, group)
5253
if parsed.fragment:
53-
selection = parse_fragment(parsed.fragment)
54+
selection = lsp_range_from_uri_fragment(parsed.fragment)
5455
if selection:
5556
return open_promise.then(lambda view: _select_and_center(view, cast(Range, selection)))
5657
return open_promise

plugin/core/sessions.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -1622,13 +1622,13 @@ def run_code_action_async(
16221622
return self._maybe_resolve_code_action(code_action, view) \
16231623
.then(lambda code_action: self._apply_code_action_async(code_action, view))
16241624

1625-
def open_uri_async(
1625+
def try_open_uri_async(
16261626
self,
16271627
uri: DocumentUri,
16281628
r: Optional[Range] = None,
16291629
flags: int = 0,
16301630
group: int = -1
1631-
) -> Promise[Optional[sublime.View]]:
1631+
) -> Optional[Promise[Optional[sublime.View]]]:
16321632
if uri.startswith("file:"):
16331633
return self._open_file_uri_async(uri, r, flags, group)
16341634
# Try to find a pre-existing session-buffer
@@ -1642,7 +1642,17 @@ def open_uri_async(
16421642
# There is no pre-existing session-buffer, so we have to go through AbstractPlugin.on_open_uri_async.
16431643
if self._plugin:
16441644
return self._open_uri_with_plugin_async(self._plugin, uri, r, flags, group)
1645-
return Promise.resolve(None)
1645+
return None
1646+
1647+
def open_uri_async(
1648+
self,
1649+
uri: DocumentUri,
1650+
r: Optional[Range] = None,
1651+
flags: int = 0,
1652+
group: int = -1
1653+
) -> Promise[Optional[sublime.View]]:
1654+
promise = self.try_open_uri_async(uri, r, flags, group)
1655+
return Promise.resolve(None) if promise is None else promise
16461656

16471657
def _open_file_uri_async(
16481658
self,
@@ -1668,7 +1678,7 @@ def _open_uri_with_plugin_async(
16681678
r: Optional[Range],
16691679
flags: int,
16701680
group: int,
1671-
) -> Promise[Optional[sublime.View]]:
1681+
) -> Optional[Promise[Optional[sublime.View]]]:
16721682
# I cannot type-hint an unpacked tuple
16731683
pair = Promise.packaged_task() # type: PackagedTask[Tuple[str, str, str]]
16741684
# It'd be nice to have automatic tuple unpacking continuations
@@ -1693,7 +1703,7 @@ def open_scratch_buffer(title: str, content: str, syntax: str) -> None:
16931703

16941704
pair[0].then(lambda tup: sublime.set_timeout(lambda: open_scratch_buffer(*tup)))
16951705
return result[0]
1696-
return Promise.resolve(None)
1706+
return None
16971707

16981708
def open_location_async(self, location: Union[Location, LocationLink], flags: int = 0,
16991709
group: int = -1) -> Promise[Optional[sublime.View]]:

plugin/hover.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .code_actions import actions_manager
22
from .code_actions import CodeActionOrCommand
33
from .code_actions import CodeActionsByConfigName
4+
from .core.open import lsp_range_from_uri_fragment
45
from .core.open import open_file_uri
56
from .core.open import open_in_browser
67
from .core.promise import Promise
@@ -36,6 +37,7 @@
3637
from .core.views import update_lsp_popup
3738
from .session_view import HOVER_HIGHLIGHT_KEY
3839
from functools import partial
40+
from urllib.parse import urlparse
3941
import html
4042
import mdpopups
4143
import sublime
@@ -362,6 +364,8 @@ def on_select(targets: List[str], idx: int) -> None:
362364
position = {"line": row, "character": col_utf16} # type: Position
363365
r = {"start": position, "end": position} # type: Range
364366
sublime.set_timeout_async(partial(session.open_uri_async, uri, r))
367+
elif urlparse(href).scheme.lower() not in ("", "http", "https"):
368+
sublime.set_timeout_async(partial(self.try_open_custom_uri_async, href))
365369
else:
366370
open_in_browser(href)
367371

@@ -375,3 +379,9 @@ def run_async() -> None:
375379
session.run_code_action_async(actions[index], progress=True, view=self.view)
376380

377381
sublime.set_timeout_async(run_async)
382+
383+
def try_open_custom_uri_async(self, href: str) -> None:
384+
r = lsp_range_from_uri_fragment(urlparse(href).fragment)
385+
for session in self.sessions():
386+
if session.try_open_uri_async(href, r) is not None:
387+
return

0 commit comments

Comments
 (0)