import mdpopups import sublime import sublime_plugin import webbrowser import os from html import escape from .code_actions import actions_manager, run_code_action_or_command from .code_actions import CodeActionOrCommand from .core.configurations import is_supported_syntax from .core.popups import popups from .core.protocol import Request, DiagnosticSeverity, Diagnostic, DiagnosticRelatedInformation, Point from .core.registry import session_for_view, LspTextCommand, windows from .core.settings import client_configs, settings from .core.typing import List, Optional, Any, Dict from .core.views import make_link from .core.views import text_document_position_params from .diagnostics import filter_by_point, view_diagnostics SUBLIME_WORD_MASK = 515 class HoverHandler(sublime_plugin.ViewEventListener): def __init__(self, view: sublime.View) -> None: self.view = view @classmethod def is_applicable(cls, view_settings: dict) -> bool: if 'hover' in settings.disabled_capabilities: return False syntax = view_settings.get('syntax') if syntax: return is_supported_syntax(syntax, client_configs.all) else: return False def on_hover(self, point: int, hover_zone: int) -> None: if hover_zone != sublime.HOVER_TEXT or self.view.is_popup_visible(): return self.view.run_command("lsp_hover", {"point": point}) _test_contents = [] # type: List[str] class_for_severity = { DiagnosticSeverity.Error: 'errors', DiagnosticSeverity.Warning: 'warnings', DiagnosticSeverity.Information: 'info', DiagnosticSeverity.Hint: 'hints' } class GotoKind: __slots__ = ("lsp_name", "label", "subl_cmd_name") def __init__(self, lsp_name: str, label: str, subl_cmd_name: str) -> None: self.lsp_name = lsp_name self.label = label self.subl_cmd_name = subl_cmd_name goto_kinds = [ GotoKind("definition", "Definition", "definition"), GotoKind("typeDefinition", "Type Definition", "type_definition"), GotoKind("declaration", "Declaration", "declaration"), GotoKind("implementation", "Implementation", "implementation") ] class LspHoverCommand(LspTextCommand): def __init__(self, view: sublime.View) -> None: super().__init__(view) self._base_dir = None # type: Optional[str] def is_likely_at_symbol(self, point: int) -> bool: word_at_sel = self.view.classify(point) return bool(word_at_sel & SUBLIME_WORD_MASK) def run(self, edit: sublime.Edit, point: Optional[int] = None) -> None: hover_point = point or self.view.sel()[0].begin() self._base_dir = windows.lookup(self.view.window()).get_project_path(self.view.file_name() or "") self._hover = None # type: Optional[Any] self._actions_by_config = {} # type: Dict[str, List[CodeActionOrCommand]] self._diagnostics_by_config = {} # type: Dict[str, List[Diagnostic]] if self.is_likely_at_symbol(hover_point): self.request_symbol_hover(hover_point) self._diagnostics_by_config = filter_by_point(view_diagnostics(self.view), Point(*self.view.rowcol(hover_point))) if self._diagnostics_by_config: self.request_code_actions(hover_point) self.request_show_hover(hover_point) def request_symbol_hover(self, point: int) -> None: # todo: session_for_view looks up windowmanager twice (config and for sessions) # can we memoize some part (eg. where no point is provided?) session = session_for_view(self.view, 'hoverProvider', point) if session: document_position = text_document_position_params(self.view, point) if session.client: session.client.send_request( Request.hover(document_position), lambda response: self.handle_response(response, point)) def request_code_actions(self, point: int) -> None: actions_manager.request(self.view, point, lambda response: self.handle_code_actions(response, point)) def handle_code_actions(self, responses: Dict[str, List[CodeActionOrCommand]], point: int) -> None: self._actions_by_config = responses self.request_show_hover(point) def handle_response(self, response: Optional[Any], point: int) -> None: self._hover = response self.request_show_hover(point) def symbol_actions_content(self) -> str: actions = [] for goto_kind in goto_kinds: if self.has_client_with_capability(goto_kind.lsp_name + "Provider"): actions.append(make_link(goto_kind.lsp_name, goto_kind.label)) if self.has_client_with_capability('referencesProvider'): actions.append(make_link('references', 'References')) if self.has_client_with_capability('renameProvider'): actions.append(make_link('rename', 'Rename')) return "<p class='actions'>" + " | ".join(actions) + "</p>" def format_diagnostic_related_info(self, info: DiagnosticRelatedInformation) -> str: file_path = info.location.file_path if self._base_dir and file_path.startswith(self._base_dir): file_path = os.path.relpath(file_path, self._base_dir) location = "{}:{}:{}".format(file_path, info.location.range.start.row+1, info.location.range.start.col+1) link = make_link("location:{}".format(location), location) return "{}: {}".format(link, escape(info.message)) def format_diagnostic(self, diagnostic: 'Diagnostic') -> str: diagnostic_message = escape(diagnostic.message, False).replace('\n', '<br>') related_infos = [self.format_diagnostic_related_info(info) for info in diagnostic.related_info] related_content = "<pre class='related_info'>" + "<br>".join(related_infos) + "</pre>" if related_infos else "" if diagnostic.source: return "<pre class=\"{}\">[{}] {}{}</pre>".format(class_for_severity[diagnostic.severity], diagnostic.source, diagnostic_message, related_content) else: return "<pre class=\"{}\">{}{}</pre>".format(class_for_severity[diagnostic.severity], diagnostic_message, related_content) def diagnostics_content(self) -> str: formatted = [] for config_name in self._diagnostics_by_config: by_severity = {} # type: Dict[int, List[str]] formatted.append("<div class='diagnostics'>") for diagnostic in self._diagnostics_by_config[config_name]: by_severity.setdefault(diagnostic.severity, []).append(self.format_diagnostic(diagnostic)) for severity, items in by_severity.items(): formatted.append("<div>") formatted.extend(items) formatted.append("</div>") if config_name in self._actions_by_config: action_count = len(self._actions_by_config[config_name]) if action_count > 0: href = "{}:{}".format('code-actions', config_name) text = "{} ({})".format('Code Actions', action_count) formatted.append("<div class=\"actions\">{}</div>".format(make_link(href, text))) formatted.append("</div>") return "".join(formatted) def hover_content(self) -> str: contents = [] # type: List[Any] if isinstance(self._hover, dict): response_content = self._hover.get('contents') if response_content: if isinstance(response_content, list): contents = response_content else: contents = [response_content] formatted = [] for item in contents: value = "" language = None if isinstance(item, str): value = item else: value = item.get("value") language = item.get("language") if language: formatted.append("```{}\n{}\n```\n".format(language, value)) else: formatted.append(value) if formatted: frontmatter_config = mdpopups.format_frontmatter({'allow_code_wrap': True}) return mdpopups.md2html(self.view, frontmatter_config + "\n".join(formatted)) return "" def request_show_hover(self, point: int) -> None: sublime.set_timeout(lambda: self.show_hover(point), 50) def show_hover(self, point: int) -> None: contents = self.diagnostics_content() + self.hover_content() if contents and settings.show_symbol_action_links: contents += self.symbol_actions_content() _test_contents.clear() _test_contents.append(contents) # for testing only if contents: mdpopups.show_popup( self.view, contents, css=popups.stylesheet, md=False, flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, location=point, wrapper_class=popups.classname, max_width=800, on_navigate=lambda href: self.on_hover_navigate(href, point)) def on_hover_navigate(self, href: str, point: int) -> None: for goto_kind in goto_kinds: if href == goto_kind.lsp_name: self.run_command_from_point(point, "lsp_symbol_" + goto_kind.subl_cmd_name) return if href == 'references': self.run_command_from_point(point, "lsp_symbol_references") elif href == 'rename': self.run_command_from_point(point, "lsp_symbol_rename") elif href.startswith('code-actions'): _, config_name = href.split(":") titles = [command["title"] for command in self._actions_by_config[config_name]] sel = self.view.sel() sel.clear() sel.add(sublime.Region(point, point)) self.view.show_popup_menu(titles, lambda i: self.handle_code_action_select(config_name, i)) elif href.startswith('location'): _, file_path, location = href.split(":", 2) file_path = os.path.join(self._base_dir, file_path) if self._base_dir else file_path window = self.view.window() if window: window.open_file(file_path + ":" + location, sublime.ENCODED_POSITION | sublime.TRANSIENT) else: webbrowser.open_new_tab(href) def handle_code_action_select(self, config_name: str, index: int) -> None: if index > -1: selected = self._actions_by_config[config_name][index] run_code_action_or_command(self.view, config_name, selected) def run_command_from_point(self, point: int, command_name: str, args: Optional[Any] = None) -> None: sel = self.view.sel() sel.clear() sel.add(sublime.Region(point, point)) self.view.run_command(command_name, args)