Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle custom URI schemes in hover text links #2339

Merged
merged 7 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
22 changes: 17 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) \
.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,19 @@ 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)
if promise is None:
raise RuntimeError("unexpected URI scheme")
rchl marked this conversation as resolved.
Show resolved Hide resolved
return promise

def _open_file_uri_async(
self,
Expand All @@ -1668,7 +1680,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 +1705,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
5 changes: 5 additions & 0 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from urllib.parse import urlparse
rchl marked this conversation as resolved.
Show resolved Hide resolved
from .css import css as lsp_css
from .protocol import CodeAction
from .protocol import CodeActionKind
Expand Down Expand Up @@ -984,6 +985,10 @@ def is_location_href(href: str) -> bool:
return href.startswith("location:")


def starts_with_custom_uri_scheme(href: str) -> bool:
jwortmann marked this conversation as resolved.
Show resolved Hide resolved
return urlparse(href).scheme.lower() not in ("", "http", "https")


def _format_diagnostic_related_info(
config: ClientConfig,
info: DiagnosticRelatedInformation,
Expand Down
11 changes: 11 additions & 0 deletions plugin/hover.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from urllib.parse import urlparse
rwols marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -31,6 +33,7 @@
from .core.views import minihtml
from .core.views import range_to_region
from .core.views import show_lsp_popup
from .core.views import starts_with_custom_uri_scheme
from .core.views import text_document_position_params
from .core.views import unpack_href_location
from .core.views import update_lsp_popup
Expand Down Expand Up @@ -362,6 +365,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 starts_with_custom_uri_scheme(href):
sublime.set_timeout_async(partial(self.try_open_custom_uri_async, href))
else:
open_in_browser(href)

Expand All @@ -375,3 +380,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
rwols marked this conversation as resolved.
Show resolved Hide resolved