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

Add support for textDocument/documentLink request #1974

Merged
merged 14 commits into from
Jun 16, 2022
Merged
14 changes: 14 additions & 0 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,20 @@
// }
// ]
// },
// Follow Link
// {
// "command": "lsp_open_link",
// "keys": [
// "UNBOUND"
// ],
// "context": [
// {
// "key": "lsp.session_with_capability",
// "operator": "equal",
// "operand": "documentLinkProvider"
// }
// ]
// },
// Expand Selection (a replacement for ST's "Expand Selection")
// {
// "command": "lsp_expand_selection",
Expand Down
10 changes: 10 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@
// Valid values are "dot", "circle", "bookmark", "sign" or ""
"diagnostics_gutter_marker": "dot",

// Highlight style of links to internal or external resources, like another text document
// or a web site. Link navigation is implemented via the popup on mouse hover.
// Valid values are:
// "underline"
// "none" - disables special highlighting, but still allows to navigate links in the hover popup
// "disabled" - disables highlighting and the hover popup for links
// Note that depending on the syntax and color scheme, some internet URLs may still be
// underlined via regular syntax highlighting.
"link_highlight_style": "underline",

// Enable semantic highlighting in addition to standard syntax highlighting (experimental!).
// Note: Must be supported by the language server and also requires a special rule in the
// color scheme to work. If you use none of the built-in color schemes from Sublime Text,
Expand Down
4 changes: 4 additions & 0 deletions Main.sublime-menu
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
{
"caption": "LSP: Find References",
"command": "lsp_symbol_references"
},
{
"caption": "LSP: Follow Link",
"command": "lsp_open_link"
}
]
},
Expand Down
1 change: 1 addition & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .plugin.core.typing import Any, Optional, List, Type, Dict
from .plugin.core.views import get_uri_and_position_from_location
from .plugin.core.views import LspRunTextCommandHelperCommand
from .plugin.document_link import LspOpenLinkCommand
from .plugin.documents import DocumentSyncListener
from .plugin.documents import TextChangeListener
from .plugin.edit import LspApplyDocumentEditCommand
Expand Down
15 changes: 15 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ class SemanticTokenModifiers:
'items': List[CompletionItem],
}, total=True)

DocumentLink = TypedDict('DocumentLink', {
'range': RangeLsp,
'target': DocumentUri,
'tooltip': str,
'data': Any
}, total=False)
rchl marked this conversation as resolved.
Show resolved Hide resolved

MarkedString = Union[str, Dict[str, str]]

MarkupContent = Dict[str, str]
Expand Down Expand Up @@ -384,6 +391,10 @@ def documentSymbols(cls, params: Mapping[str, Any], view: sublime.View) -> 'Requ
def documentHighlight(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request':
return Request("textDocument/documentHighlight", params, view)

@classmethod
def documentLink(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request':
return Request("textDocument/documentLink", params, view)

@classmethod
def semanticTokensFull(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request':
return Request("textDocument/semanticTokens/full", params, view)
Expand All @@ -400,6 +411,10 @@ def semanticTokensRange(cls, params: Mapping[str, Any], view: sublime.View) -> '
def resolveCompletionItem(cls, params: CompletionItem, view: sublime.View) -> 'Request':
return Request("completionItem/resolve", params, view)

@classmethod
def resolveDocumentLink(cls, params: DocumentLink, view: sublime.View) -> 'Request':
return Request("documentLink/resolve", params, view)

@classmethod
def shutdown(cls) -> 'Request':
return Request("shutdown")
Expand Down
11 changes: 11 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .protocol import DiagnosticSeverity
from .protocol import DiagnosticTag
from .protocol import DidChangeWatchedFilesRegistrationOptions
from .protocol import DocumentLink
from .protocol import DocumentUri
from .protocol import Error
from .protocol import ErrorCode
Expand Down Expand Up @@ -280,6 +281,10 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
"valueSet": symbol_tag_value_set
}
},
"documentLink": {
"dynamicRegistration": True,
"tooltipSupport": True
},
"formatting": {
"dynamicRegistration": True # exceptional
},
Expand Down Expand Up @@ -523,6 +528,12 @@ def unregister_capability_async(
def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optional[int]) -> None:
...

def get_document_link_at_point(self, view: sublime.View, point: int) -> Optional[DocumentLink]:
...

def update_document_link(self, link: DocumentLink) -> None:
...

def do_semantic_tokens_async(self, view: sublime.View) -> None:
...

Expand Down
2 changes: 2 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class Settings:
document_highlight_style = None # type: str
inhibit_snippet_completions = None # type: bool
inhibit_word_completions = None # type: bool
link_highlight_style = None # type: str
log_debug = None # type: bool
log_max_size = None # type: int
log_server = None # type: List[str]
Expand Down Expand Up @@ -230,6 +231,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None:
r("diagnostics_panel_include_severity_level", 4)
r("disabled_capabilities", [])
r("document_highlight_style", "underline")
r("link_highlight_style", "underline")
r("log_debug", False)
r("log_max_size", 8 * 1024)
r("lsp_code_actions_on_save", {})
Expand Down
2 changes: 2 additions & 0 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

MarkdownLangMap = Dict[str, Tuple[Tuple[str, ...], Tuple[str, ...]]]

DOCUMENT_LINK_FLAGS = sublime.HIDE_ON_MINIMAP | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SOLID_UNDERLINE # noqa: E501

_baseflags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_EMPTY_AS_OVERWRITE

DIAGNOSTIC_SEVERITY = [
Expand Down
71 changes: 71 additions & 0 deletions plugin/document_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from .core.logging import debug
from .core.protocol import DocumentLink, Request
from .core.registry import get_position
from .core.registry import LspTextCommand
from .core.typing import Optional
from urllib.parse import unquote, urlparse
import re
import sublime
import webbrowser


class LspOpenLinkCommand(LspTextCommand):
capability = 'documentLinkProvider'

def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) -> bool:
if not super().is_enabled(event, point):
return False
position = get_position(self.view, event)
if not position:
return False
session = self.best_session(self.capability, position)
if not session:
return False
sv = session.session_view_for_view_async(self.view)
if not sv:
return False
link = sv.session_buffer.get_document_link_at_point(self.view, position)
return link is not None

def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:
point = get_position(self.view, event)
if not point:
return
session = self.best_session(self.capability, point)
if not session:
return
sv = session.session_view_for_view_async(self.view)
if not sv:
return
link = sv.session_buffer.get_document_link_at_point(self.view, point)
if not link:
return
target = link.get("target")

if target is not None:
self.open_target(target)
else:
if not session.has_capability("documentLinkProvider.resolveProvider"):
debug("DocumentLink.target is missing, but the server doesn't support documentLink/resolve")
return
session.send_request_async(Request.resolveDocumentLink(link, self.view), self._on_resolved_async)

def _on_resolved_async(self, response: DocumentLink) -> None:
self.open_target(response["target"])

def open_target(self, target: str) -> None:
if target.startswith("file:"):
window = self.view.window()
if window:
decoded = unquote(target) # decode percent-encoded characters
parsed = urlparse(decoded)
filepath = parsed.path
if sublime.platform() == "windows":
filepath = re.sub(r"^/([a-zA-Z]:)", r"\1", filepath) # remove slash preceding drive letter
fn = "{}:{}".format(filepath, parsed.fragment) if parsed.fragment else filepath
window.open_file(fn, flags=sublime.ENCODED_POSITION)
else:
if not (target.lower().startswith("http://") or target.lower().startswith("https://")):
target = "http://" + target
if not webbrowser.open(target):
sublime.status_message("failed to open: " + target)
63 changes: 56 additions & 7 deletions plugin/hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .core.logging import debug
from .core.promise import Promise
from .core.protocol import Diagnostic
from .core.protocol import DocumentLink
from .core.protocol import Error
from .core.protocol import ExperimentalTextDocumentRangeParams
from .core.protocol import Hover
Expand Down Expand Up @@ -118,6 +119,7 @@ def run(
wm = windows.lookup(window)
self._base_dir = wm.get_project_path(self.view.file_name() or "")
self._hover_responses = [] # type: List[Tuple[Hover, Optional[MarkdownLangMap]]]
self._document_link_content = ('', False)
self._actions_by_config = {} # type: Dict[str, List[CodeActionOrCommand]]
self._diagnostics_by_config = [] # type: Sequence[Tuple[SessionBufferProtocol, Sequence[Diagnostic]]]
# TODO: For code actions it makes more sense to use the whole selection under mouse (if available)
Expand All @@ -129,6 +131,8 @@ def run_async() -> None:
return
if not only_diagnostics:
self.request_symbol_hover_async(listener, hover_point)
if userprefs().link_highlight_style in ("underline", "none"):
self.request_document_link_async(listener, hover_point)
self._diagnostics_by_config, covering = listener.diagnostics_touching_point_async(
hover_point, userprefs().show_diagnostics_severity_level)
if self._diagnostics_by_config:
Expand Down Expand Up @@ -183,6 +187,46 @@ def _on_all_settled(
self._hover_responses = hovers
self.show_hover(listener, point, only_diagnostics=False)

def request_document_link_async(self, listener: AbstractViewListener, point: int) -> None:
link_promises = [] # type: List[Promise[DocumentLink]]
for sv in listener.session_views_async():
if not sv.has_capability_async("documentLinkProvider"):
continue
link = sv.session_buffer.get_document_link_at_point(sv.view, point)
if link is None:
continue
target = link.get("target")
if target:
link_promises.append(Promise.resolve(link))
elif sv.has_capability_async("documentLinkProvider.resolveProvider"):
link_promises.append(sv.session.send_request_task(Request.resolveDocumentLink(link, sv.view)).then(
lambda link: self._on_resolved_link(sv.session_buffer, link)))
if link_promises:
continuation = functools.partial(self._on_all_document_links_resolved, listener, point)
Promise.all(link_promises).then(continuation)

def _on_resolved_link(self, session_buffer: SessionBufferProtocol, link: DocumentLink) -> DocumentLink:
session_buffer.update_document_link(link)
return link

def _on_all_document_links_resolved(
self, listener: AbstractViewListener, point: int, links: List[DocumentLink]
) -> None:
contents = []
link_has_standard_tooltip = True
for link in links:
target = link.get("target")
if not target:
continue
title = link.get("tooltip") or "Follow link"
if title != "Follow link":
link_has_standard_tooltip = False
contents.append('<a href="{}">{}</a>'.format(target, title))
if len(contents) > 1:
link_has_standard_tooltip = False
self._document_link_content = ('<br>'.join(contents) if contents else '', link_has_standard_tooltip)
self.show_hover(listener, point, only_diagnostics=False)

def handle_code_actions(
self,
listener: AbstractViewListener,
Expand All @@ -196,11 +240,8 @@ def provider_exists(self, listener: AbstractViewListener, link: LinkKind) -> boo
return bool(listener.session_async('{}Provider'.format(link.lsp_name)))

def symbol_actions_content(self, listener: AbstractViewListener, point: int) -> str:
if userprefs().show_symbol_action_links:
actions = [lk.link(point, self.view) for lk in link_kinds if self.provider_exists(listener, lk)]
if actions:
return '<div class="actions">' + " | ".join(actions) + "</div>"
return ""
actions = [lk.link(point, self.view) for lk in link_kinds if self.provider_exists(listener, lk)]
return " | ".join(actions) if actions else ""

def diagnostics_content(self) -> str:
formatted = []
Expand Down Expand Up @@ -229,8 +270,16 @@ def show_hover(self, listener: AbstractViewListener, point: int, only_diagnostic
def _show_hover(self, listener: AbstractViewListener, point: int, only_diagnostics: bool) -> None:
hover_content = self.hover_content()
contents = self.diagnostics_content() + hover_content + code_actions_content(self._actions_by_config)
if contents and not only_diagnostics and hover_content:
contents += self.symbol_actions_content(listener, point)
link_content, link_has_standard_tooltip = self._document_link_content
if userprefs().show_symbol_action_links and contents and not only_diagnostics and hover_content:
symbol_actions_content = self.symbol_actions_content(listener, point)
if link_content and link_has_standard_tooltip:
symbol_actions_content += ' | ' + link_content
elif link_content:
contents += '<div class="link with-padding">' + link_content + '</div>'
contents += '<div class="actions">' + symbol_actions_content + '</div>'
elif link_content:
contents += '<div class="{}">{}</div>'.format('link with-padding' if contents else 'link', link_content)

_test_contents.clear()
_test_contents.append(contents) # for testing only
Expand Down
Loading