vibefpmgui.hover_card
The HTML of the editor's hover card.
A header line (the kind as a colored badge, like the outline's, then the
name and what it is), fortitude's messages for that spot, the language
server's description (declaration or manual page, see
vibefpmgui.lsp.hover_info), and a footer with the related shortcuts.
1"""The HTML of the editor's hover card. 2 3A header line (the kind as a colored badge, like the outline's, then the 4name and what it is), fortitude's messages for that spot, the language 5server's description (declaration or manual page, see 6`vibefpmgui.lsp.hover_info`), and a footer with the related shortcuts. 7""" 8 9from __future__ import annotations 10 11import html 12 13from qtpy.QtGui import QColor, QPalette 14from qtpy.QtWidgets import QApplication 15 16from . import lsp 17from .icons import SYMBOL_BADGES 18 19# Kinds the outline has no badge for. 20EXTRA_BADGES = {"variable": ("V", "#495057"), "parameter": ("K", "#5c940d"), 21 "intrinsic": ("intrinsic", "#4263eb")} 22LINT_COLORS = {"lint error": "#e03131", "lint": "#0ca678", "fortls error": "#e03131", "fortls": "#1098ad", 23 "spelling": "#ae3ec9"} 24# The heading over each tool's notes. 25SOURCE_TITLES = {"fortitude": "FORTITUDE", "fortls": "FORTLS (checks the saved file)", "spelling": "SPELLING"} 26 27 28_text_color = None # the card's text color (set by the editor, see set_text_color) 29 30 31def set_text_color(color: QColor): 32 """The color the card's text is drawn in (the editor's, which follows 33 the syntax style rather than the system's light/dark mode).""" 34 global _text_color 35 _text_color = QColor(color) 36 37 38def _muted() -> str: 39 color = QColor(_text_color or QApplication.palette().color(QPalette.Text)) 40 color.setAlpha(150) 41 return color.name(QColor.HexArgb) 42 43 44def badge(kind: str) -> str: 45 letter, color = SYMBOL_BADGES.get(kind) or EXTRA_BADGES.get(kind) or ("?", "#868e96") 46 return (f"<span style='background-color:{color}; color:white; font-weight:700; " 47 f"font-size:small'> {html.escape(letter.upper() if len(letter) > 1 else letter)} </span>") 48 49 50def lint_notes_html(issues, fix_ids: list[int] | None = None) -> str: 51 """fortitude's (and fortls's) messages: a colored rule code, the 52 message, and the fix, under a heading per tool. 53 With ``fix_ids`` (each issue's number in the editor), fixable issues get 54 an "Apply fix" link (``fix:N``), and misspelled words their suggestions 55 (``spell:N:word``) and "Add to Dictionary" (``addword:N``).""" 56 groups: dict[str, list[str]] = {} 57 for n, issue in enumerate(issues): 58 rows = groups.setdefault(getattr(issue, "source", "fortitude"), []) 59 color = LINT_COLORS.get(issue.severity, "#0ca678") 60 link = "" 61 if fix_ids is not None and getattr(issue, "fixable", False): 62 note = "" if issue.fix_safe else f" <i style='color:{_muted()}'>(unsafe — check it)</i>" 63 link = f" <span style='font-size:small'><a href='fix:{fix_ids[n]}'>Apply fix</a>{note}</span>" 64 if fix_ids is not None and hasattr(issue, "suggestions"): 65 words = [f"<a href='spell:{fix_ids[n]}:{html.escape(w)}'>{html.escape(w)}</a>" 66 for w in issue.suggestions()] 67 link = (f"<br><span style='font-size:small'>{', '.join(words) or 'No suggestions'}" 68 f" · <a href='addword:{fix_ids[n]}'>Add to Dictionary</a></span>") 69 fix = (f"<br><span style='color:{_muted()}; font-size:small'>Fix: {html.escape(issue.fix)}</span>{link}" 70 if issue.fix else link) 71 rows.append(f"<tr><td valign='top' style='padding-right:6px'><span style='background-color:{color}; " 72 f"color:white; font-weight:700; font-size:small'> {html.escape(issue.code)} " 73 f"</span></td><td>{html.escape(issue.message)}{fix}</td></tr>") 74 if not groups: 75 return "" 76 return "".join( 77 "<table cellspacing='0' cellpadding='1' style='margin:0 0 2px 0'>" 78 f"<tr><td colspan='2'><span style='color:{_muted()}; font-size:small'>" 79 f"{SOURCE_TITLES.get(source, source.upper())}</span></td></tr>" + "".join(rows) + "</table>" 80 for source, rows in groups.items()) 81 82 83def card_html(info: lsp.HoverInfo | None, name: str, lint_html: str = "") -> str: 84 """The whole card for a name: header, lint messages, description, footer.""" 85 parts = [] 86 if info is not None: 87 what = "" if info.kind == "intrinsic" else info.kind # (the badge says it) 88 header = f"{badge(info.kind)} <b>{html.escape(name)}</b>" if info.kind else f"<b>{html.escape(name)}</b>" 89 if what: 90 header += f" <span style='color:{_muted()}'>{html.escape(what)}</span>" 91 if info.category: 92 header += (f" <span style='color:{_muted()}; font-size:small'>· " 93 f"{html.escape(info.category)}</span>") 94 parts.append(f"<p style='margin:0 0 4px 0'>{header}</p>") 95 if info.summary: 96 parts.append(f"<p style='margin:0 0 4px 0'><i>{html.escape(info.summary)}</i></p>") 97 if lint_html: 98 parts.append(lint_html) 99 if info is not None: 100 parts.append("<hr>") 101 if info is not None: 102 parts.append(info.body_html) 103 if info.kind != "intrinsic": 104 parts.append(f"<p style='margin:6px 0 0 0'><span style='color:{_muted()}; font-size:small'>" 105 "F12 definition · ⇧F12 references · F2 rename</span></p>") 106 return "".join(parts)
32def set_text_color(color: QColor): 33 """The color the card's text is drawn in (the editor's, which follows 34 the syntax style rather than the system's light/dark mode).""" 35 global _text_color 36 _text_color = QColor(color)
The color the card's text is drawn in (the editor's, which follows the syntax style rather than the system's light/dark mode).
45def badge(kind: str) -> str: 46 letter, color = SYMBOL_BADGES.get(kind) or EXTRA_BADGES.get(kind) or ("?", "#868e96") 47 return (f"<span style='background-color:{color}; color:white; font-weight:700; " 48 f"font-size:small'> {html.escape(letter.upper() if len(letter) > 1 else letter)} </span>")
51def lint_notes_html(issues, fix_ids: list[int] | None = None) -> str: 52 """fortitude's (and fortls's) messages: a colored rule code, the 53 message, and the fix, under a heading per tool. 54 With ``fix_ids`` (each issue's number in the editor), fixable issues get 55 an "Apply fix" link (``fix:N``), and misspelled words their suggestions 56 (``spell:N:word``) and "Add to Dictionary" (``addword:N``).""" 57 groups: dict[str, list[str]] = {} 58 for n, issue in enumerate(issues): 59 rows = groups.setdefault(getattr(issue, "source", "fortitude"), []) 60 color = LINT_COLORS.get(issue.severity, "#0ca678") 61 link = "" 62 if fix_ids is not None and getattr(issue, "fixable", False): 63 note = "" if issue.fix_safe else f" <i style='color:{_muted()}'>(unsafe — check it)</i>" 64 link = f" <span style='font-size:small'><a href='fix:{fix_ids[n]}'>Apply fix</a>{note}</span>" 65 if fix_ids is not None and hasattr(issue, "suggestions"): 66 words = [f"<a href='spell:{fix_ids[n]}:{html.escape(w)}'>{html.escape(w)}</a>" 67 for w in issue.suggestions()] 68 link = (f"<br><span style='font-size:small'>{', '.join(words) or 'No suggestions'}" 69 f" · <a href='addword:{fix_ids[n]}'>Add to Dictionary</a></span>") 70 fix = (f"<br><span style='color:{_muted()}; font-size:small'>Fix: {html.escape(issue.fix)}</span>{link}" 71 if issue.fix else link) 72 rows.append(f"<tr><td valign='top' style='padding-right:6px'><span style='background-color:{color}; " 73 f"color:white; font-weight:700; font-size:small'> {html.escape(issue.code)} " 74 f"</span></td><td>{html.escape(issue.message)}{fix}</td></tr>") 75 if not groups: 76 return "" 77 return "".join( 78 "<table cellspacing='0' cellpadding='1' style='margin:0 0 2px 0'>" 79 f"<tr><td colspan='2'><span style='color:{_muted()}; font-size:small'>" 80 f"{SOURCE_TITLES.get(source, source.upper())}</span></td></tr>" + "".join(rows) + "</table>" 81 for source, rows in groups.items())
fortitude's (and fortls's) messages: a colored rule code, the
message, and the fix, under a heading per tool.
With fix_ids (each issue's number in the editor), fixable issues get
an "Apply fix" link (fix:N), and misspelled words their suggestions
(spell:N:word) and "Add to Dictionary" (addword:N).
84def card_html(info: lsp.HoverInfo | None, name: str, lint_html: str = "") -> str: 85 """The whole card for a name: header, lint messages, description, footer.""" 86 parts = [] 87 if info is not None: 88 what = "" if info.kind == "intrinsic" else info.kind # (the badge says it) 89 header = f"{badge(info.kind)} <b>{html.escape(name)}</b>" if info.kind else f"<b>{html.escape(name)}</b>" 90 if what: 91 header += f" <span style='color:{_muted()}'>{html.escape(what)}</span>" 92 if info.category: 93 header += (f" <span style='color:{_muted()}; font-size:small'>· " 94 f"{html.escape(info.category)}</span>") 95 parts.append(f"<p style='margin:0 0 4px 0'>{header}</p>") 96 if info.summary: 97 parts.append(f"<p style='margin:0 0 4px 0'><i>{html.escape(info.summary)}</i></p>") 98 if lint_html: 99 parts.append(lint_html) 100 if info is not None: 101 parts.append("<hr>") 102 if info is not None: 103 parts.append(info.body_html) 104 if info.kind != "intrinsic": 105 parts.append(f"<p style='margin:6px 0 0 0'><span style='color:{_muted()}; font-size:small'>" 106 "F12 definition · ⇧F12 references · F2 rename</span></p>") 107 return "".join(parts)
The whole card for a name: header, lint messages, description, footer.