vibefpmgui.gremlins
Gremlins: characters in a file that aren't plain printable ASCII.
Non-ASCII letters and symbols (é, °, smart quotes), invisible ones
(non-breaking and zero-width spaces, a byte-order mark) and control
characters. Compilers are often fussy about them (gfortran rejects most
outside comments and strings), and the invisible ones can't be seen in the
editor. Tabs and line ends are not gremlins.
The editor draws a gremlin in the gutter of each line that has one
(paint_gremlin), tints the characters, and lists them in the Problems
tab (View → Show Gremlins turns it off).
1"""*Gremlins*: characters in a file that aren't plain printable ASCII. 2 3Non-ASCII letters and symbols (``é``, ``°``, smart quotes), invisible ones 4(non-breaking and zero-width spaces, a byte-order mark) and control 5characters. Compilers are often fussy about them (gfortran rejects most 6outside comments and strings), and the invisible ones can't be seen in the 7editor. Tabs and line ends are not gremlins. 8 9The editor draws a gremlin in the gutter of each line that has one 10(`paint_gremlin`), tints the characters, and lists them in the *Problems* 11tab (View → Show Gremlins turns it off). 12""" 13 14from __future__ import annotations 15 16import re 17import unicodedata 18from typing import NamedTuple 19 20from qtpy.QtCore import QPointF, QRectF, Qt 21from qtpy.QtGui import QColor, QPainter, QPainterPath, QPen 22 23MAX_GREMLINS = 2000 # per file (a file in another language would have one per accented letter) 24GREMLIN_RE = re.compile(r"[^\t\n\x20-\x7e]") 25COLOR = "#e8590c" # the tint, badge and Problems color 26 27# Names for the ones that have none, or a long one. 28NAMES = {"\u00a0": "non-breaking space", "\u200b": "zero-width space", "\u200c": "zero-width non-joiner", 29 "\u200d": "zero-width joiner", "\u2060": "word joiner", "\ufeff": "byte-order mark (zero-width)", 30 "\u00ad": "soft hyphen", "\r": "carriage return", "\x0c": "form feed", "\x00": "null", 31 "\x1b": "escape", "\x7f": "delete"} 32 33 34class Gremlin(NamedTuple): 35 line: int # 1-based 36 column: int # 0-based 37 char: str 38 39 @property 40 def code(self) -> str: 41 return f"U+{ord(self.char):04X}" 42 43 @property 44 def message(self) -> str: 45 return describe(self.char) 46 47 48def describe(char: str) -> str: 49 """What a gremlin is, e.g. "non-breaking space, not ASCII" or "latin 50 small letter e with acute (é), not ASCII".""" 51 name = NAMES.get(char) 52 if name is None: 53 if unicodedata.category(char) in ("Cc", "Cf", "Zs", "Zl", "Zp"): # (can't be seen: just the name) 54 name = unicodedata.name(char, "control character").lower() 55 else: 56 return f"{unicodedata.name(char, 'unnamed character').lower()} ({char}), not ASCII" 57 return f"{name}, not ASCII" if ord(char) > 0x7F else f"{name} (a control character)" 58 59 60def find(text: str, limit: int = MAX_GREMLINS) -> list[Gremlin]: 61 """Every gremlin in a text ("\\n" line ends), in order.""" 62 out: list[Gremlin] = [] 63 line, line_start, last = 1, 0, 0 64 for match in GREMLIN_RE.finditer(text): 65 start = match.start() 66 newlines = text.count("\n", last, start) 67 if newlines: 68 line += newlines 69 line_start = text.rfind("\n", 0, start) + 1 70 last = start 71 out.append(Gremlin(line, start - line_start, match.group())) 72 if len(out) >= limit: 73 break 74 return out 75 76 77def paint_gremlin(painter: QPainter, rect: QRectF): 78 """A little gremlin (big ears, round eyes, a grin) filling ``rect``.""" 79 painter.save() 80 painter.setRenderHint(QPainter.Antialiasing) 81 painter.translate(rect.topLeft()) 82 painter.scale(rect.width() / 24, rect.height() / 24) # (drawn on a 24 × 24 grid) 83 green, dark = QColor("#5fb04a"), QColor("#2b5a22") 84 painter.setPen(QPen(dark, 1.2)) 85 painter.setBrush(green) 86 for side in (-1, 1): # ears: long and pointed, out to the sides 87 ear = QPainterPath(QPointF(12 + side * 4, 9)) 88 ear.lineTo(QPointF(12 + side * 11.5, 4)) 89 ear.lineTo(QPointF(12 + side * 6, 14)) 90 ear.closeSubpath() 91 painter.drawPath(ear) 92 painter.drawEllipse(QRectF(5, 5.5, 14, 15)) # the head 93 painter.setPen(Qt.NoPen) 94 painter.setBrush(QColor("#fff7d6")) 95 for x in (9, 15): 96 painter.drawEllipse(QPointF(x, 11.5), 2.6, 2.9) # eyes 97 painter.setBrush(QColor("#c92a2a")) 98 for x in (9.5, 14.5): 99 painter.drawEllipse(QPointF(x, 12), 1.2, 1.4) # (red pupils) 100 painter.setPen(QPen(dark, 1.1)) 101 painter.setBrush(Qt.NoBrush) 102 grin = QPainterPath(QPointF(8.5, 16)) 103 grin.quadTo(QPointF(12, 19), QPointF(15.5, 16)) 104 painter.drawPath(grin) 105 painter.restore()
35class Gremlin(NamedTuple): 36 line: int # 1-based 37 column: int # 0-based 38 char: str 39 40 @property 41 def code(self) -> str: 42 return f"U+{ord(self.char):04X}" 43 44 @property 45 def message(self) -> str: 46 return describe(self.char)
Gremlin(line, column, char)
49def describe(char: str) -> str: 50 """What a gremlin is, e.g. "non-breaking space, not ASCII" or "latin 51 small letter e with acute (é), not ASCII".""" 52 name = NAMES.get(char) 53 if name is None: 54 if unicodedata.category(char) in ("Cc", "Cf", "Zs", "Zl", "Zp"): # (can't be seen: just the name) 55 name = unicodedata.name(char, "control character").lower() 56 else: 57 return f"{unicodedata.name(char, 'unnamed character').lower()} ({char}), not ASCII" 58 return f"{name}, not ASCII" if ord(char) > 0x7F else f"{name} (a control character)"
What a gremlin is, e.g. "non-breaking space, not ASCII" or "latin small letter e with acute (é), not ASCII".
61def find(text: str, limit: int = MAX_GREMLINS) -> list[Gremlin]: 62 """Every gremlin in a text ("\\n" line ends), in order.""" 63 out: list[Gremlin] = [] 64 line, line_start, last = 1, 0, 0 65 for match in GREMLIN_RE.finditer(text): 66 start = match.start() 67 newlines = text.count("\n", last, start) 68 if newlines: 69 line += newlines 70 line_start = text.rfind("\n", 0, start) + 1 71 last = start 72 out.append(Gremlin(line, start - line_start, match.group())) 73 if len(out) >= limit: 74 break 75 return out
Every gremlin in a text ("\n" line ends), in order.
78def paint_gremlin(painter: QPainter, rect: QRectF): 79 """A little gremlin (big ears, round eyes, a grin) filling ``rect``.""" 80 painter.save() 81 painter.setRenderHint(QPainter.Antialiasing) 82 painter.translate(rect.topLeft()) 83 painter.scale(rect.width() / 24, rect.height() / 24) # (drawn on a 24 × 24 grid) 84 green, dark = QColor("#5fb04a"), QColor("#2b5a22") 85 painter.setPen(QPen(dark, 1.2)) 86 painter.setBrush(green) 87 for side in (-1, 1): # ears: long and pointed, out to the sides 88 ear = QPainterPath(QPointF(12 + side * 4, 9)) 89 ear.lineTo(QPointF(12 + side * 11.5, 4)) 90 ear.lineTo(QPointF(12 + side * 6, 14)) 91 ear.closeSubpath() 92 painter.drawPath(ear) 93 painter.drawEllipse(QRectF(5, 5.5, 14, 15)) # the head 94 painter.setPen(Qt.NoPen) 95 painter.setBrush(QColor("#fff7d6")) 96 for x in (9, 15): 97 painter.drawEllipse(QPointF(x, 11.5), 2.6, 2.9) # eyes 98 painter.setBrush(QColor("#c92a2a")) 99 for x in (9.5, 14.5): 100 painter.drawEllipse(QPointF(x, 12), 1.2, 1.4) # (red pupils) 101 painter.setPen(QPen(dark, 1.1)) 102 painter.setBrush(Qt.NoBrush) 103 grin = QPainterPath(QPointF(8.5, 16)) 104 grin.quadTo(QPointF(12, 19), QPointF(15.5, 16)) 105 painter.drawPath(grin) 106 painter.restore()
A little gremlin (big ears, round eyes, a grin) filling rect.