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'>&nbsp;{html.escape(letter.upper() if len(letter) > 1 else letter)}&nbsp;</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" &nbsp;<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" &nbsp;·&nbsp; <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'>&nbsp;{html.escape(issue.code)}&nbsp;"
 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)}&nbsp; <b>{html.escape(name)}</b>" if info.kind else f"<b>{html.escape(name)}</b>"
 89        if what:
 90            header += f" &nbsp;<span style='color:{_muted()}'>{html.escape(what)}</span>"
 91        if info.category:
 92            header += (f" &nbsp;<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 &nbsp;·&nbsp; ⇧F12 references &nbsp;·&nbsp; F2 rename</span></p>")
106    return "".join(parts)
EXTRA_BADGES = {'variable': ('V', '#495057'), 'parameter': ('K', '#5c940d'), 'intrinsic': ('intrinsic', '#4263eb')}
LINT_COLORS = {'lint error': '#e03131', 'lint': '#0ca678', 'fortls error': '#e03131', 'fortls': '#1098ad', 'spelling': '#ae3ec9'}
SOURCE_TITLES = {'fortitude': 'FORTITUDE', 'fortls': 'FORTLS (checks the saved file)', 'spelling': 'SPELLING'}
def set_text_color(color: PySide6.QtGui.QColor):
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).

def badge(kind: str) -> str:
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'>&nbsp;{html.escape(letter.upper() if len(letter) > 1 else letter)}&nbsp;</span>")
def lint_notes_html(issues, fix_ids: list[int] | None = None) -> str:
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" &nbsp;<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" &nbsp;·&nbsp; <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'>&nbsp;{html.escape(issue.code)}&nbsp;"
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).

def card_html( info: vibefpmgui.lsp.HoverInfo | None, name: str, lint_html: str = '') -> str:
 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)}&nbsp; <b>{html.escape(name)}</b>" if info.kind else f"<b>{html.escape(name)}</b>"
 90        if what:
 91            header += f" &nbsp;<span style='color:{_muted()}'>{html.escape(what)}</span>"
 92        if info.category:
 93            header += (f" &nbsp;<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 &nbsp;·&nbsp; ⇧F12 references &nbsp;·&nbsp; F2 rename</span></p>")
107    return "".join(parts)

The whole card for a name: header, lint messages, description, footer.