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

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: 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()) 

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: 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)}&nbsp; <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" &nbsp;<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" &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: 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 &nbsp;·&nbsp; ⇧F12 references &nbsp;·&nbsp; F2 rename</span></p>") 

106 return "".join(parts)