Skip to content

Commit

Permalink
Handle custom URI schemes in hover text links (#2339)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
rwols authored Oct 20, 2023
1 parent 07daed8 commit 876e924
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 27 deletions.
45 changes: 23 additions & 22 deletions plugin/core/open.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,37 @@
FRAGMENT_PATTERN = re.compile(r'^L?(\d+)(?:,(\d+))?(?:-L?(\d+)(?:,(\d+))?)?')


def lsp_range_from_uri_fragment(fragment: str) -> Optional[Range]:
match = FRAGMENT_PATTERN.match(fragment)
if match:
selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range
# Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based
# numbers for the LSP Position structure.
start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()]
if start_line:
selection['start']['line'] = start_line
selection['end']['line'] = start_line
if start_column:
selection['start']['character'] = start_column
selection['end']['character'] = start_column
if end_line:
selection['end']['line'] = end_line
selection['end']['character'] = UINT_MAX
if end_column is not None:
selection['end']['character'] = end_column
return selection
return None


def open_file_uri(
window: sublime.Window, uri: DocumentUri, flags: int = 0, group: int = -1
) -> Promise[Optional[sublime.View]]:

def parse_fragment(fragment: str) -> Optional[Range]:
match = FRAGMENT_PATTERN.match(fragment)
if match:
selection = {'start': {'line': 0, 'character': 0}, 'end': {'line': 0, 'character': 0}} # type: Range
# Line and column numbers in the fragment are assumed to be 1-based and need to be converted to 0-based
# numbers for the LSP Position structure.
start_line, start_column, end_line, end_column = [max(0, int(g) - 1) if g else None for g in match.groups()]
if start_line:
selection['start']['line'] = start_line
selection['end']['line'] = start_line
if start_column:
selection['start']['character'] = start_column
selection['end']['character'] = start_column
if end_line:
selection['end']['line'] = end_line
selection['end']['character'] = UINT_MAX
if end_column is not None:
selection['end']['character'] = end_column
return selection
return None

decoded_uri = unquote(uri) # decode percent-encoded characters
parsed = urlparse(decoded_uri)
open_promise = open_file(window, decoded_uri, flags, group)
if parsed.fragment:
selection = parse_fragment(parsed.fragment)
selection = lsp_range_from_uri_fragment(parsed.fragment)
if selection:
return open_promise.then(lambda view: _select_and_center(view, cast(Range, selection)))
return open_promise
Expand Down
20 changes: 15 additions & 5 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,13 +1622,13 @@ def run_code_action_async(
return self._maybe_resolve_code_action(code_action, view) \
.then(lambda code_action: self._apply_code_action_async(code_action, view))

def open_uri_async(
def try_open_uri_async(
self,
uri: DocumentUri,
r: Optional[Range] = None,
flags: int = 0,
group: int = -1
) -> Promise[Optional[sublime.View]]:
) -> Optional[Promise[Optional[sublime.View]]]:
if uri.startswith("file:"):
return self._open_file_uri_async(uri, r, flags, group)
# Try to find a pre-existing session-buffer
Expand All @@ -1642,7 +1642,17 @@ def open_uri_async(
# There is no pre-existing session-buffer, so we have to go through AbstractPlugin.on_open_uri_async.
if self._plugin:
return self._open_uri_with_plugin_async(self._plugin, uri, r, flags, group)
return Promise.resolve(None)
return None

def open_uri_async(
self,
uri: DocumentUri,
r: Optional[Range] = None,
flags: int = 0,
group: int = -1
) -> Promise[Optional[sublime.View]]:
promise = self.try_open_uri_async(uri, r, flags, group)
return Promise.resolve(None) if promise is None else promise

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

pair[0].then(lambda tup: sublime.set_timeout(lambda: open_scratch_buffer(*tup)))
return result[0]
return Promise.resolve(None)
return None

def open_location_async(self, location: Union[Location, LocationLink], flags: int = 0,
group: int = -1) -> Promise[Optional[sublime.View]]:
Expand Down
10 changes: 10 additions & 0 deletions plugin/hover.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .code_actions import actions_manager
from .code_actions import CodeActionOrCommand
from .code_actions import CodeActionsByConfigName
from .core.open import lsp_range_from_uri_fragment
from .core.open import open_file_uri
from .core.open import open_in_browser
from .core.promise import Promise
Expand Down Expand Up @@ -36,6 +37,7 @@
from .core.views import update_lsp_popup
from .session_view import HOVER_HIGHLIGHT_KEY
from functools import partial
from urllib.parse import urlparse
import html
import mdpopups
import sublime
Expand Down Expand Up @@ -362,6 +364,8 @@ def on_select(targets: List[str], idx: int) -> None:
position = {"line": row, "character": col_utf16} # type: Position
r = {"start": position, "end": position} # type: Range
sublime.set_timeout_async(partial(session.open_uri_async, uri, r))
elif urlparse(href).scheme.lower() not in ("", "http", "https"):
sublime.set_timeout_async(partial(self.try_open_custom_uri_async, href))
else:
open_in_browser(href)

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

sublime.set_timeout_async(run_async)

def try_open_custom_uri_async(self, href: str) -> None:
r = lsp_range_from_uri_fragment(urlparse(href).fragment)
for session in self.sessions():
if session.try_open_uri_async(href, r) is not None:
return

0 comments on commit 876e924

Please sign in to comment.