vibefpmgui.editor_checks

An editor tab's checks: fortitude, spelling, gremlins, and the compiler's, fortls's and the AI's markers.

A part of vibefpmgui.editor.EditorTab.

  1"""An editor tab's checks: fortitude, spelling, gremlins, and the compiler's, fortls's and the AI's markers.
  2
  3A part of `vibefpmgui.editor.EditorTab`."""
  4
  5from __future__ import annotations
  6
  7from qtpy.QtCore import QProcess
  8
  9from . import gremlins as gremlins_mod
 10from . import lint, spelling
 11from .background import call_in_background
 12from .editor_shared import MARKER_COLORS, PROBLEM_SOURCES
 13from .result_tree import Problem
 14
 15PARSE_APART = 200_000  # (characters) fortitude's answer, or a text to spell check, bigger: on a pool thread
 16
 17
 18class EditorChecksMixin:
 19    """The tab's checks and markers (a part of `vibefpmgui.editor.EditorTab`)."""
 20
 21    def can_lint(self) -> bool:
 22        return self.lint_enabled and lint.can_lint(self.path) and lint.executable() is not None
 23
 24    def set_lint_enabled(self, on: bool):
 25        if on:
 26            self.lint(force=True)
 27        else:
 28            self._lint_timer.stop()
 29            self._show_lint([])
 30
 31    def lint(self, force: bool = False):
 32        """Run fortitude on the current text (in the background), unless it
 33        has checked this text of this file already (or is checking it):
 34        opening a file asked twice, and recoloring the syntax counts as a
 35        change of the text too; ``force``: all the same."""
 36        if not self.can_lint():
 37            self._show_lint([])
 38            return
 39        key = (self.path, self.editor.toPlainText())
 40        if not force and key == getattr(self, "_lint_key", None):
 41            return
 42        if self._lint_process is not None:
 43            self._lint_again = True
 44            return
 45        self._lint_again = False
 46        process = QProcess(self)
 47        process.setWorkingDirectory(str(self.path.parent))
 48        # (Methods, not lambdas holding the process: see LspClient.start.)
 49        process.finished.connect(self._lint_process_finished)
 50        process.errorOccurred.connect(self._lint_process_error)
 51        self._lint_process = process
 52        self._lint_key = key
 53        process.start(lint.executable(), lint.command(self.path))
 54        # (The text itself, not the document's revision: recoloring the
 55        # syntax also counts as a revision.)
 56        self._lint_sent_text = self.editor.toPlainText()
 57        process.write(self._lint_sent_text.encode("utf-8"))
 58        process.closeWriteChannel()
 59
 60    def _lint_process_finished(self, *_):
 61        if isinstance(self.sender(), QProcess):
 62            self._lint_finished(self.sender())
 63
 64    def _lint_process_error(self, error):
 65        if error == QProcess.FailedToStart and isinstance(self.sender(), QProcess):
 66            self._lint_finished(self.sender())
 67
 68    def _lint_finished(self, process: QProcess):
 69        if process is not self._lint_process:
 70            return
 71        self._lint_process = None
 72        output = bytes(process.readAllStandardOutput()).decode("utf-8", errors="replace")
 73        process.deleteLater()
 74        self._lint_answer = serial = getattr(self, "_lint_answer", 0) + 1
 75        sent = self._lint_sent_text
 76        if len(output) > PARSE_APART:  # (a big file's thousands of issues: read on a pool thread)
 77            call_in_background(lambda: lint.parse(output),
 78                               lambda issues: self._lint_parsed(serial, sent, issues), self)
 79        else:
 80            self._lint_parsed(serial, sent, lint.parse(output))
 81        if self._lint_again:
 82            self._lint_timer.start()
 83
 84    def _lint_parsed(self, serial: int, sent: str, issues: list):
 85        if serial != self._lint_answer:
 86            return  # (a newer answer came meanwhile)
 87        if self.can_lint():
 88            self._show_lint(issues)
 89            self._lint_text = sent  # the text the issues are about
 90
 91    def apply_lint_fix(self, issue):
 92        """Apply fortitude's fix for one issue (one Undo step). Only if the
 93        text is still the one fortitude checked: otherwise the positions
 94        may be off, so check again first."""
 95        if not issue.fixable:
 96            return
 97        if getattr(self, "_lint_text", None) != self.editor.toPlainText():
 98            self.update_status("the file changed since fortitude checked it — try again in a moment")
 99            self._lint_timer.start()
100            return
101        self.apply_text_edits(issue.edits)
102        self.update_status(f"applied fix: {issue.fix or issue.code}")
103
104    def stop_lint(self):
105        """Before the tab is closed: don't leave fortitude running."""
106        self._lint_timer.stop()
107        if self._lint_process is not None:
108            process, self._lint_process = self._lint_process, None
109            process.kill()
110            process.waitForFinished(1000)
111
112    def _show_lint(self, issues: list[lint.LintIssue]):
113        self._fortitude_issues = list(issues)
114        self._lint_markers = self._markers_for(issues)
115        self._show_issues()
116
117    def set_gremlins_enabled(self, on: bool):
118        self.check_gremlins()
119
120    def check_gremlins(self):
121        """Look for gremlins (non-ASCII and invisible characters) again."""
122        found = gremlins_mod.find(self.editor.toPlainText()) if self.gremlins_enabled else []
123        before = self.editor.gremlins
124        self.editor.set_gremlins(found)
125        if self.editor.gremlins != before:
126            self.update_status()
127            self.problems_changed.emit()
128
129    def set_spelling_enabled(self, on: bool):
130        self.check_spelling() if on else self._show_spelling([])
131
132    def check_spelling(self):
133        """Spell check the comments and strings (as the highlighter last
134        found them: it lexes again after each edit, then this runs)."""
135        self._spelling_timer.stop()
136        if not (self.spelling_enabled and spelling.available()) or getattr(self, "untitled", False):
137            # (A new file not saved yet is "plain text", all prose: its code isn't, until it has a type.)
138            self._show_spelling([])
139            return
140        hl = self.highlighter
141        if not hl.is_current():
142            return  # (the highlighter is behind: it calls again when it's caught up)
143        text, prose, names = hl.text, hl.prose, hl.names
144        self._spelling_check = serial = getattr(self, "_spelling_check", 0) + 1
145        if len(text) > PARSE_APART:  # (a big file's first check reads every comment: on a pool thread)
146            call_in_background(lambda: spelling.check(text, prose, names),
147                               lambda issues: self._spelling_checked(serial, text, issues), self)
148        else:
149            self._spelling_checked(serial, text, spelling.check(text, prose, names))
150
151    def _spelling_checked(self, serial: int, text: str, issues: list):
152        if serial == self._spelling_check and self.highlighter.text is text:  # (not since edited, or asked again)
153            self._show_spelling(issues)
154
155    def _show_spelling(self, issues: list):
156        if issues == self._spelling_issues:
157            return  # (the same words: nothing to redraw, or to tell the Problems tab)
158        self._spelling_issues = issues
159        self._show_issues()
160
161    def show_lsp_diagnostics(self, issues: list):
162        """fortls's check of this file (it sends one when the file is opened
163        or saved), shown like fortitude's issues (View menu: on or off)."""
164        issues = list(issues) if self.lsp_diagnostics_enabled else []
165        self._lsp_issues = issues
166        self._lsp_markers = self._markers_for(issues)
167        self._show_issues()
168
169    @staticmethod
170    def _markers_for(issues) -> dict[int, list[tuple[str, str]]]:
171        markers: dict[int, list[tuple[str, str]]] = {}
172        for issue in issues:
173            markers.setdefault(issue.line, []).append((issue.severity, issue.describe()))
174        return markers
175
176    def _show_issues(self):
177        self.editor.set_lint(self._fortitude_issues + self._lsp_issues + self._spelling_issues)
178        self._apply_markers()
179
180    def show_diagnostics(self, diagnostics):
181        """Mark compiler messages (objects with .line, .severity, .message)."""
182        markers: dict[int, list[tuple[str, str]]] = {}
183        for d in diagnostics:
184            markers.setdefault(d.line, []).append((d.severity, d.message))
185        self._compiler_markers = markers
186        self._apply_markers()
187
188    def show_review(self, findings):
189        """Mark AI review findings (objects with .line, .category, .comment)."""
190        markers: dict[int, list[tuple[str, str]]] = {}
191        for f in findings:
192            markers.setdefault(f.line, []).append(("review", f"AI ({f.category}): {f.comment}"))
193        self._review_markers = markers
194        self._apply_markers()
195
196    def clear_review(self):
197        self._review_markers = {}
198        self._apply_markers()
199
200    def _apply_markers(self):
201        merged: dict[int, list[tuple[str, str]]] = {}
202        for source in (self._compiler_markers, self._review_markers, self._lint_markers, self._lsp_markers):
203            for line, notes in source.items():
204                merged.setdefault(line, []).extend(notes)
205        self.editor.set_markers(merged)
206        self.update_status()
207        self.problems_changed.emit()
208
209    def compiler_diagnostics(self) -> list[tuple[int, str, str]]:
210        """The last build's messages about this file: (line, severity, message), by line."""
211        return [(line, severity, message) for line, notes in sorted(self._compiler_markers.items())
212                for severity, message in notes]
213
214    def problems(self) -> list[Problem]:
215        """Everything flagged in the file, in line order: the last build's
216        compiler messages, the AI review's findings, and the underlined
217        issues (fortitude, fortls, spelling)."""
218        found = []
219        for markers, source in ((self._compiler_markers, "build"), (self._review_markers, "review")):
220            for line, notes in markers.items():
221                for severity, message in notes:
222                    found.append(Problem(line, 0, line, 0, severity, severity, message,
223                                         MARKER_COLORS.get(severity, "#868e96"), source))
224        for line_gremlins in self.editor.gremlins.values():
225            for g in line_gremlins:
226                found.append(Problem(g.line, g.column, g.line, g.column + 1, "gremlin", g.code, g.message,
227                                     gremlins_mod.COLOR, "gremlins"))
228        for issue in self.editor.lint_issues:
229            found.append(Problem(issue.line, max(0, issue.column - 1), issue.end_line, max(0, issue.end_column - 1),
230                                 issue.severity, issue.code, issue.message, MARKER_COLORS[issue.severity],
231                                 PROBLEM_SOURCES.get(issue.severity, "fortitude"), issue))
232        found.sort(key=lambda p: (p.line, p.column))
233        return found
234
235    def _explain_line(self, line: int):
236        """Explain Error with AI on a line: its most serious compiler, fortitude
237        or fortls problem."""
238        rank = {"fatal error": 0, "error": 1, "lint error": 2, "fortls error": 3, "warning": 4}
239        found = [p for p in self.problems() if p.line == line and p.source in ("build", "fortitude", "fortls")]
240        if found:
241            self.explain_requested.emit(min(found, key=lambda p: rank.get(p.severity, 9)))
PARSE_APART = 200000
class EditorChecksMixin:
 19class EditorChecksMixin:
 20    """The tab's checks and markers (a part of `vibefpmgui.editor.EditorTab`)."""
 21
 22    def can_lint(self) -> bool:
 23        return self.lint_enabled and lint.can_lint(self.path) and lint.executable() is not None
 24
 25    def set_lint_enabled(self, on: bool):
 26        if on:
 27            self.lint(force=True)
 28        else:
 29            self._lint_timer.stop()
 30            self._show_lint([])
 31
 32    def lint(self, force: bool = False):
 33        """Run fortitude on the current text (in the background), unless it
 34        has checked this text of this file already (or is checking it):
 35        opening a file asked twice, and recoloring the syntax counts as a
 36        change of the text too; ``force``: all the same."""
 37        if not self.can_lint():
 38            self._show_lint([])
 39            return
 40        key = (self.path, self.editor.toPlainText())
 41        if not force and key == getattr(self, "_lint_key", None):
 42            return
 43        if self._lint_process is not None:
 44            self._lint_again = True
 45            return
 46        self._lint_again = False
 47        process = QProcess(self)
 48        process.setWorkingDirectory(str(self.path.parent))
 49        # (Methods, not lambdas holding the process: see LspClient.start.)
 50        process.finished.connect(self._lint_process_finished)
 51        process.errorOccurred.connect(self._lint_process_error)
 52        self._lint_process = process
 53        self._lint_key = key
 54        process.start(lint.executable(), lint.command(self.path))
 55        # (The text itself, not the document's revision: recoloring the
 56        # syntax also counts as a revision.)
 57        self._lint_sent_text = self.editor.toPlainText()
 58        process.write(self._lint_sent_text.encode("utf-8"))
 59        process.closeWriteChannel()
 60
 61    def _lint_process_finished(self, *_):
 62        if isinstance(self.sender(), QProcess):
 63            self._lint_finished(self.sender())
 64
 65    def _lint_process_error(self, error):
 66        if error == QProcess.FailedToStart and isinstance(self.sender(), QProcess):
 67            self._lint_finished(self.sender())
 68
 69    def _lint_finished(self, process: QProcess):
 70        if process is not self._lint_process:
 71            return
 72        self._lint_process = None
 73        output = bytes(process.readAllStandardOutput()).decode("utf-8", errors="replace")
 74        process.deleteLater()
 75        self._lint_answer = serial = getattr(self, "_lint_answer", 0) + 1
 76        sent = self._lint_sent_text
 77        if len(output) > PARSE_APART:  # (a big file's thousands of issues: read on a pool thread)
 78            call_in_background(lambda: lint.parse(output),
 79                               lambda issues: self._lint_parsed(serial, sent, issues), self)
 80        else:
 81            self._lint_parsed(serial, sent, lint.parse(output))
 82        if self._lint_again:
 83            self._lint_timer.start()
 84
 85    def _lint_parsed(self, serial: int, sent: str, issues: list):
 86        if serial != self._lint_answer:
 87            return  # (a newer answer came meanwhile)
 88        if self.can_lint():
 89            self._show_lint(issues)
 90            self._lint_text = sent  # the text the issues are about
 91
 92    def apply_lint_fix(self, issue):
 93        """Apply fortitude's fix for one issue (one Undo step). Only if the
 94        text is still the one fortitude checked: otherwise the positions
 95        may be off, so check again first."""
 96        if not issue.fixable:
 97            return
 98        if getattr(self, "_lint_text", None) != self.editor.toPlainText():
 99            self.update_status("the file changed since fortitude checked it — try again in a moment")
100            self._lint_timer.start()
101            return
102        self.apply_text_edits(issue.edits)
103        self.update_status(f"applied fix: {issue.fix or issue.code}")
104
105    def stop_lint(self):
106        """Before the tab is closed: don't leave fortitude running."""
107        self._lint_timer.stop()
108        if self._lint_process is not None:
109            process, self._lint_process = self._lint_process, None
110            process.kill()
111            process.waitForFinished(1000)
112
113    def _show_lint(self, issues: list[lint.LintIssue]):
114        self._fortitude_issues = list(issues)
115        self._lint_markers = self._markers_for(issues)
116        self._show_issues()
117
118    def set_gremlins_enabled(self, on: bool):
119        self.check_gremlins()
120
121    def check_gremlins(self):
122        """Look for gremlins (non-ASCII and invisible characters) again."""
123        found = gremlins_mod.find(self.editor.toPlainText()) if self.gremlins_enabled else []
124        before = self.editor.gremlins
125        self.editor.set_gremlins(found)
126        if self.editor.gremlins != before:
127            self.update_status()
128            self.problems_changed.emit()
129
130    def set_spelling_enabled(self, on: bool):
131        self.check_spelling() if on else self._show_spelling([])
132
133    def check_spelling(self):
134        """Spell check the comments and strings (as the highlighter last
135        found them: it lexes again after each edit, then this runs)."""
136        self._spelling_timer.stop()
137        if not (self.spelling_enabled and spelling.available()) or getattr(self, "untitled", False):
138            # (A new file not saved yet is "plain text", all prose: its code isn't, until it has a type.)
139            self._show_spelling([])
140            return
141        hl = self.highlighter
142        if not hl.is_current():
143            return  # (the highlighter is behind: it calls again when it's caught up)
144        text, prose, names = hl.text, hl.prose, hl.names
145        self._spelling_check = serial = getattr(self, "_spelling_check", 0) + 1
146        if len(text) > PARSE_APART:  # (a big file's first check reads every comment: on a pool thread)
147            call_in_background(lambda: spelling.check(text, prose, names),
148                               lambda issues: self._spelling_checked(serial, text, issues), self)
149        else:
150            self._spelling_checked(serial, text, spelling.check(text, prose, names))
151
152    def _spelling_checked(self, serial: int, text: str, issues: list):
153        if serial == self._spelling_check and self.highlighter.text is text:  # (not since edited, or asked again)
154            self._show_spelling(issues)
155
156    def _show_spelling(self, issues: list):
157        if issues == self._spelling_issues:
158            return  # (the same words: nothing to redraw, or to tell the Problems tab)
159        self._spelling_issues = issues
160        self._show_issues()
161
162    def show_lsp_diagnostics(self, issues: list):
163        """fortls's check of this file (it sends one when the file is opened
164        or saved), shown like fortitude's issues (View menu: on or off)."""
165        issues = list(issues) if self.lsp_diagnostics_enabled else []
166        self._lsp_issues = issues
167        self._lsp_markers = self._markers_for(issues)
168        self._show_issues()
169
170    @staticmethod
171    def _markers_for(issues) -> dict[int, list[tuple[str, str]]]:
172        markers: dict[int, list[tuple[str, str]]] = {}
173        for issue in issues:
174            markers.setdefault(issue.line, []).append((issue.severity, issue.describe()))
175        return markers
176
177    def _show_issues(self):
178        self.editor.set_lint(self._fortitude_issues + self._lsp_issues + self._spelling_issues)
179        self._apply_markers()
180
181    def show_diagnostics(self, diagnostics):
182        """Mark compiler messages (objects with .line, .severity, .message)."""
183        markers: dict[int, list[tuple[str, str]]] = {}
184        for d in diagnostics:
185            markers.setdefault(d.line, []).append((d.severity, d.message))
186        self._compiler_markers = markers
187        self._apply_markers()
188
189    def show_review(self, findings):
190        """Mark AI review findings (objects with .line, .category, .comment)."""
191        markers: dict[int, list[tuple[str, str]]] = {}
192        for f in findings:
193            markers.setdefault(f.line, []).append(("review", f"AI ({f.category}): {f.comment}"))
194        self._review_markers = markers
195        self._apply_markers()
196
197    def clear_review(self):
198        self._review_markers = {}
199        self._apply_markers()
200
201    def _apply_markers(self):
202        merged: dict[int, list[tuple[str, str]]] = {}
203        for source in (self._compiler_markers, self._review_markers, self._lint_markers, self._lsp_markers):
204            for line, notes in source.items():
205                merged.setdefault(line, []).extend(notes)
206        self.editor.set_markers(merged)
207        self.update_status()
208        self.problems_changed.emit()
209
210    def compiler_diagnostics(self) -> list[tuple[int, str, str]]:
211        """The last build's messages about this file: (line, severity, message), by line."""
212        return [(line, severity, message) for line, notes in sorted(self._compiler_markers.items())
213                for severity, message in notes]
214
215    def problems(self) -> list[Problem]:
216        """Everything flagged in the file, in line order: the last build's
217        compiler messages, the AI review's findings, and the underlined
218        issues (fortitude, fortls, spelling)."""
219        found = []
220        for markers, source in ((self._compiler_markers, "build"), (self._review_markers, "review")):
221            for line, notes in markers.items():
222                for severity, message in notes:
223                    found.append(Problem(line, 0, line, 0, severity, severity, message,
224                                         MARKER_COLORS.get(severity, "#868e96"), source))
225        for line_gremlins in self.editor.gremlins.values():
226            for g in line_gremlins:
227                found.append(Problem(g.line, g.column, g.line, g.column + 1, "gremlin", g.code, g.message,
228                                     gremlins_mod.COLOR, "gremlins"))
229        for issue in self.editor.lint_issues:
230            found.append(Problem(issue.line, max(0, issue.column - 1), issue.end_line, max(0, issue.end_column - 1),
231                                 issue.severity, issue.code, issue.message, MARKER_COLORS[issue.severity],
232                                 PROBLEM_SOURCES.get(issue.severity, "fortitude"), issue))
233        found.sort(key=lambda p: (p.line, p.column))
234        return found
235
236    def _explain_line(self, line: int):
237        """Explain Error with AI on a line: its most serious compiler, fortitude
238        or fortls problem."""
239        rank = {"fatal error": 0, "error": 1, "lint error": 2, "fortls error": 3, "warning": 4}
240        found = [p for p in self.problems() if p.line == line and p.source in ("build", "fortitude", "fortls")]
241        if found:
242            self.explain_requested.emit(min(found, key=lambda p: rank.get(p.severity, 9)))

The tab's checks and markers (a part of vibefpmgui.editor.EditorTab).

def can_lint(self) -> bool:
22    def can_lint(self) -> bool:
23        return self.lint_enabled and lint.can_lint(self.path) and lint.executable() is not None
def set_lint_enabled(self, on: bool):
25    def set_lint_enabled(self, on: bool):
26        if on:
27            self.lint(force=True)
28        else:
29            self._lint_timer.stop()
30            self._show_lint([])
def lint(self, force: bool = False):
32    def lint(self, force: bool = False):
33        """Run fortitude on the current text (in the background), unless it
34        has checked this text of this file already (or is checking it):
35        opening a file asked twice, and recoloring the syntax counts as a
36        change of the text too; ``force``: all the same."""
37        if not self.can_lint():
38            self._show_lint([])
39            return
40        key = (self.path, self.editor.toPlainText())
41        if not force and key == getattr(self, "_lint_key", None):
42            return
43        if self._lint_process is not None:
44            self._lint_again = True
45            return
46        self._lint_again = False
47        process = QProcess(self)
48        process.setWorkingDirectory(str(self.path.parent))
49        # (Methods, not lambdas holding the process: see LspClient.start.)
50        process.finished.connect(self._lint_process_finished)
51        process.errorOccurred.connect(self._lint_process_error)
52        self._lint_process = process
53        self._lint_key = key
54        process.start(lint.executable(), lint.command(self.path))
55        # (The text itself, not the document's revision: recoloring the
56        # syntax also counts as a revision.)
57        self._lint_sent_text = self.editor.toPlainText()
58        process.write(self._lint_sent_text.encode("utf-8"))
59        process.closeWriteChannel()

Run fortitude on the current text (in the background), unless it has checked this text of this file already (or is checking it): opening a file asked twice, and recoloring the syntax counts as a change of the text too; force: all the same.

def apply_lint_fix(self, issue):
 92    def apply_lint_fix(self, issue):
 93        """Apply fortitude's fix for one issue (one Undo step). Only if the
 94        text is still the one fortitude checked: otherwise the positions
 95        may be off, so check again first."""
 96        if not issue.fixable:
 97            return
 98        if getattr(self, "_lint_text", None) != self.editor.toPlainText():
 99            self.update_status("the file changed since fortitude checked it — try again in a moment")
100            self._lint_timer.start()
101            return
102        self.apply_text_edits(issue.edits)
103        self.update_status(f"applied fix: {issue.fix or issue.code}")

Apply fortitude's fix for one issue (one Undo step). Only if the text is still the one fortitude checked: otherwise the positions may be off, so check again first.

def stop_lint(self):
105    def stop_lint(self):
106        """Before the tab is closed: don't leave fortitude running."""
107        self._lint_timer.stop()
108        if self._lint_process is not None:
109            process, self._lint_process = self._lint_process, None
110            process.kill()
111            process.waitForFinished(1000)

Before the tab is closed: don't leave fortitude running.

def set_gremlins_enabled(self, on: bool):
118    def set_gremlins_enabled(self, on: bool):
119        self.check_gremlins()
def check_gremlins(self):
121    def check_gremlins(self):
122        """Look for gremlins (non-ASCII and invisible characters) again."""
123        found = gremlins_mod.find(self.editor.toPlainText()) if self.gremlins_enabled else []
124        before = self.editor.gremlins
125        self.editor.set_gremlins(found)
126        if self.editor.gremlins != before:
127            self.update_status()
128            self.problems_changed.emit()

Look for gremlins (non-ASCII and invisible characters) again.

def set_spelling_enabled(self, on: bool):
130    def set_spelling_enabled(self, on: bool):
131        self.check_spelling() if on else self._show_spelling([])
def check_spelling(self):
133    def check_spelling(self):
134        """Spell check the comments and strings (as the highlighter last
135        found them: it lexes again after each edit, then this runs)."""
136        self._spelling_timer.stop()
137        if not (self.spelling_enabled and spelling.available()) or getattr(self, "untitled", False):
138            # (A new file not saved yet is "plain text", all prose: its code isn't, until it has a type.)
139            self._show_spelling([])
140            return
141        hl = self.highlighter
142        if not hl.is_current():
143            return  # (the highlighter is behind: it calls again when it's caught up)
144        text, prose, names = hl.text, hl.prose, hl.names
145        self._spelling_check = serial = getattr(self, "_spelling_check", 0) + 1
146        if len(text) > PARSE_APART:  # (a big file's first check reads every comment: on a pool thread)
147            call_in_background(lambda: spelling.check(text, prose, names),
148                               lambda issues: self._spelling_checked(serial, text, issues), self)
149        else:
150            self._spelling_checked(serial, text, spelling.check(text, prose, names))

Spell check the comments and strings (as the highlighter last found them: it lexes again after each edit, then this runs).

def show_lsp_diagnostics(self, issues: list):
162    def show_lsp_diagnostics(self, issues: list):
163        """fortls's check of this file (it sends one when the file is opened
164        or saved), shown like fortitude's issues (View menu: on or off)."""
165        issues = list(issues) if self.lsp_diagnostics_enabled else []
166        self._lsp_issues = issues
167        self._lsp_markers = self._markers_for(issues)
168        self._show_issues()

fortls's check of this file (it sends one when the file is opened or saved), shown like fortitude's issues (View menu: on or off).

def show_diagnostics(self, diagnostics):
181    def show_diagnostics(self, diagnostics):
182        """Mark compiler messages (objects with .line, .severity, .message)."""
183        markers: dict[int, list[tuple[str, str]]] = {}
184        for d in diagnostics:
185            markers.setdefault(d.line, []).append((d.severity, d.message))
186        self._compiler_markers = markers
187        self._apply_markers()

Mark compiler messages (objects with .line, .severity, .message).

def show_review(self, findings):
189    def show_review(self, findings):
190        """Mark AI review findings (objects with .line, .category, .comment)."""
191        markers: dict[int, list[tuple[str, str]]] = {}
192        for f in findings:
193            markers.setdefault(f.line, []).append(("review", f"AI ({f.category}): {f.comment}"))
194        self._review_markers = markers
195        self._apply_markers()

Mark AI review findings (objects with .line, .category, .comment).

def clear_review(self):
197    def clear_review(self):
198        self._review_markers = {}
199        self._apply_markers()
def compiler_diagnostics(self) -> list[tuple[int, str, str]]:
210    def compiler_diagnostics(self) -> list[tuple[int, str, str]]:
211        """The last build's messages about this file: (line, severity, message), by line."""
212        return [(line, severity, message) for line, notes in sorted(self._compiler_markers.items())
213                for severity, message in notes]

The last build's messages about this file: (line, severity, message), by line.

def problems(self) -> list[vibefpmgui.result_tree.Problem]:
215    def problems(self) -> list[Problem]:
216        """Everything flagged in the file, in line order: the last build's
217        compiler messages, the AI review's findings, and the underlined
218        issues (fortitude, fortls, spelling)."""
219        found = []
220        for markers, source in ((self._compiler_markers, "build"), (self._review_markers, "review")):
221            for line, notes in markers.items():
222                for severity, message in notes:
223                    found.append(Problem(line, 0, line, 0, severity, severity, message,
224                                         MARKER_COLORS.get(severity, "#868e96"), source))
225        for line_gremlins in self.editor.gremlins.values():
226            for g in line_gremlins:
227                found.append(Problem(g.line, g.column, g.line, g.column + 1, "gremlin", g.code, g.message,
228                                     gremlins_mod.COLOR, "gremlins"))
229        for issue in self.editor.lint_issues:
230            found.append(Problem(issue.line, max(0, issue.column - 1), issue.end_line, max(0, issue.end_column - 1),
231                                 issue.severity, issue.code, issue.message, MARKER_COLORS[issue.severity],
232                                 PROBLEM_SOURCES.get(issue.severity, "fortitude"), issue))
233        found.sort(key=lambda p: (p.line, p.column))
234        return found

Everything flagged in the file, in line order: the last build's compiler messages, the AI review's findings, and the underlined issues (fortitude, fortls, spelling).