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)))
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).
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.
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.
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.
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.
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).
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).
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).
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).
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.
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).