Coverage for vibefpmgui/hover_card.py: 80%
57 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""The HTML of the editor's hover card.
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"""
9from __future__ import annotations
11import html
13from qtpy.QtGui import QColor, QPalette
14from qtpy.QtWidgets import QApplication
16from . import lsp
17from .icons import SYMBOL_BADGES
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"}
28_text_color = None # the card's text color (set by the editor, see set_text_color)
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)
38def _muted() -> str:
39 color = QColor(_text_color or QApplication.palette().color(QPalette.Text))
40 color.setAlpha(150)
41 return color.name(QColor.HexArgb)
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>")
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: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true
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())
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: 86 ↛ 97line 86 didn't jump to line 97 because the condition on line 86 was always true
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: 89 ↛ 91line 89 didn't jump to line 91 because the condition on line 89 was always true
90 header += f" <span style='color:{_muted()}'>{html.escape(what)}</span>"
91 if info.category: 91 ↛ 92line 91 didn't jump to line 92 because the condition on line 91 was never true
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: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 parts.append(f"<p style='margin:0 0 4px 0'><i>{html.escape(info.summary)}</i></p>")
97 if lint_html: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 parts.append(lint_html)
99 if info is not None:
100 parts.append("<hr>")
101 if info is not None: 101 ↛ 106line 101 didn't jump to line 106 because the condition on line 101 was always true
102 parts.append(info.body_html)
103 if info.kind != "intrinsic": 103 ↛ 106line 103 didn't jump to line 106 because the condition on line 103 was always true
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)