diff --git a/plugin/core/open.py b/plugin/core/open.py index 094b48ee1..84fc03465 100644 --- a/plugin/core/open.py +++ b/plugin/core/open.py @@ -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 diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index f0080bc36..ae918a325 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -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 @@ -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, @@ -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 @@ -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]]: diff --git a/plugin/hover.py b/plugin/hover.py index 2a3528750..ee24e34ab 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -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 @@ -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 @@ -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) @@ -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