pycforge 0.15.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. pycforge/__init__.py +8 -0
  2. pycforge/__main__.py +2 -0
  3. pycforge/_version.py +1 -0
  4. pycforge/converter/__init__.py +0 -0
  5. pycforge/converter/analysis/__init__.py +5 -0
  6. pycforge/converter/analysis/functions.py +723 -0
  7. pycforge/converter/analysis/model.py +158 -0
  8. pycforge/converter/analysis/planning.py +464 -0
  9. pycforge/converter/analysis/stage.py +1976 -0
  10. pycforge/converter/analysis/symbols.py +237 -0
  11. pycforge/converter/analysis/validation.py +2166 -0
  12. pycforge/converter/c_output/__init__.py +2 -0
  13. pycforge/converter/c_output/conformance.py +297 -0
  14. pycforge/converter/c_output/renderer.py +220 -0
  15. pycforge/converter/conditional_regions/__init__.py +71 -0
  16. pycforge/converter/conditional_regions/analysis.py +481 -0
  17. pycforge/converter/conditional_regions/lowering.py +579 -0
  18. pycforge/converter/conditional_regions/model.py +243 -0
  19. pycforge/converter/conditional_regions/validation.py +690 -0
  20. pycforge/converter/containers/__init__.py +19 -0
  21. pycforge/converter/containers/analysis.py +542 -0
  22. pycforge/converter/containers/lowering.py +290 -0
  23. pycforge/converter/containers/model.py +146 -0
  24. pycforge/converter/contracts/__init__.py +0 -0
  25. pycforge/converter/contracts/configuration.py +178 -0
  26. pycforge/converter/contracts/identifiers.py +68 -0
  27. pycforge/converter/contracts/versions.py +71 -0
  28. pycforge/converter/core/__init__.py +0 -0
  29. pycforge/converter/core/artifact_io.py +87 -0
  30. pycforge/converter/core/cancellation.py +7 -0
  31. pycforge/converter/core/canonicalization.py +254 -0
  32. pycforge/converter/core/context.py +13 -0
  33. pycforge/converter/core/diagnostics.py +95 -0
  34. pycforge/converter/core/enums.py +23 -0
  35. pycforge/converter/core/fingerprint.py +22 -0
  36. pycforge/converter/core/pipeline.py +8 -0
  37. pycforge/converter/core/progress.py +25 -0
  38. pycforge/converter/core/request.py +50 -0
  39. pycforge/converter/core/resource_policy.py +31 -0
  40. pycforge/converter/core/result.py +23 -0
  41. pycforge/converter/core/serialization.py +36 -0
  42. pycforge/converter/core/stage.py +10 -0
  43. pycforge/converter/core/stage_artifact.py +80 -0
  44. pycforge/converter/core/stage_outcome.py +15 -0
  45. pycforge/converter/decision_trace/__init__.py +0 -0
  46. pycforge/converter/decision_trace/recorder.py +52 -0
  47. pycforge/converter/facade.py +459 -0
  48. pycforge/converter/frontend/__init__.py +4 -0
  49. pycforge/converter/frontend/normalizer.py +59 -0
  50. pycforge/converter/frontend/parser.py +20 -0
  51. pycforge/converter/frontend/source_document.py +93 -0
  52. pycforge/converter/frontend/stages.py +518 -0
  53. pycforge/converter/frontend/validation.py +76 -0
  54. pycforge/converter/io/__init__.py +0 -0
  55. pycforge/converter/io/atomic_writer.py +33 -0
  56. pycforge/converter/ir/__init__.py +1 -0
  57. pycforge/converter/ir/c_ir/__init__.py +4 -0
  58. pycforge/converter/ir/c_ir/builder.py +26 -0
  59. pycforge/converter/ir/c_ir/model.py +318 -0
  60. pycforge/converter/ir/c_ir/serialization.py +88 -0
  61. pycforge/converter/ir/c_ir/validation.py +834 -0
  62. pycforge/converter/ir/python_ir/__init__.py +17 -0
  63. pycforge/converter/ir/python_ir/nodes.py +86 -0
  64. pycforge/converter/keyword_calls/__init__.py +68 -0
  65. pycforge/converter/keyword_calls/analysis.py +458 -0
  66. pycforge/converter/keyword_calls/lowering.py +271 -0
  67. pycforge/converter/keyword_calls/model.py +304 -0
  68. pycforge/converter/keyword_calls/validation.py +802 -0
  69. pycforge/converter/keyword_only_calls/__init__.py +68 -0
  70. pycforge/converter/keyword_only_calls/analysis.py +640 -0
  71. pycforge/converter/keyword_only_calls/lowering.py +376 -0
  72. pycforge/converter/keyword_only_calls/model.py +328 -0
  73. pycforge/converter/keyword_only_calls/validation.py +1357 -0
  74. pycforge/converter/lowering.py +1000 -0
  75. pycforge/converter/modules/__init__.py +13 -0
  76. pycforge/converter/modules/analysis.py +1250 -0
  77. pycforge/converter/modules/lowering.py +200 -0
  78. pycforge/converter/modules/model.py +73 -0
  79. pycforge/converter/modules/stage.py +192 -0
  80. pycforge/converter/numeric_semantics/__init__.py +23 -0
  81. pycforge/converter/numeric_semantics/analysis.py +420 -0
  82. pycforge/converter/numeric_semantics/lowering.py +153 -0
  83. pycforge/converter/numeric_semantics/model.py +123 -0
  84. pycforge/converter/records/__init__.py +34 -0
  85. pycforge/converter/records/analysis.py +1394 -0
  86. pycforge/converter/records/lowering.py +393 -0
  87. pycforge/converter/records/model.py +319 -0
  88. pycforge/converter/rules/__init__.py +3 -0
  89. pycforge/converter/support_templates/__init__.py +49 -0
  90. pycforge/converter/support_templates/assembly.py +106 -0
  91. pycforge/converter/support_templates/factories.py +348 -0
  92. pycforge/converter/support_templates/model.py +159 -0
  93. pycforge/converter/support_templates/registry.py +388 -0
  94. pycforge/converter/telemetry/__init__.py +0 -0
  95. pycforge/converter/telemetry/sink.py +39 -0
  96. pycforge/ide/__init__.py +19 -0
  97. pycforge/ide/__main__.py +5 -0
  98. pycforge/ide/_worker_event_codec.py +276 -0
  99. pycforge/ide/_worker_protocol_json.py +197 -0
  100. pycforge/ide/_worker_protocol_types.py +90 -0
  101. pycforge/ide/_worker_request_codec.py +200 -0
  102. pycforge/ide/_worker_request_data.py +260 -0
  103. pycforge/ide/_worker_result_data.py +343 -0
  104. pycforge/ide/action_contract.py +469 -0
  105. pycforge/ide/action_validation.py +229 -0
  106. pycforge/ide/command_palette.py +223 -0
  107. pycforge/ide/controller.py +904 -0
  108. pycforge/ide/controller_conversion.py +546 -0
  109. pycforge/ide/controller_io.py +205 -0
  110. pycforge/ide/editor.py +951 -0
  111. pycforge/ide/editor_commands_qt.py +274 -0
  112. pycforge/ide/editor_lexical.py +43 -0
  113. pycforge/ide/editor_sidebars.py +236 -0
  114. pycforge/ide/editor_syntax.py +243 -0
  115. pycforge/ide/find_replace.py +535 -0
  116. pycforge/ide/icons.py +91 -0
  117. pycforge/ide/io_service.py +196 -0
  118. pycforge/ide/model.py +164 -0
  119. pycforge/ide/panels.py +903 -0
  120. pycforge/ide/positions.py +220 -0
  121. pycforge/ide/process_worker.py +130 -0
  122. pycforge/ide/qt.py +230 -0
  123. pycforge/ide/qt_actions.py +418 -0
  124. pycforge/ide/qt_close.py +169 -0
  125. pycforge/ide/qt_command_palette.py +297 -0
  126. pycforge/ide/qt_contract.py +220 -0
  127. pycforge/ide/qt_documents.py +590 -0
  128. pycforge/ide/qt_editor_buffers.py +113 -0
  129. pycforge/ide/qt_editor_surfaces.py +112 -0
  130. pycforge/ide/qt_menus.py +324 -0
  131. pycforge/ide/qt_projection.py +500 -0
  132. pycforge/ide/qt_shell.py +348 -0
  133. pycforge/ide/qt_shell_interactions.py +320 -0
  134. pycforge/ide/qt_state.py +393 -0
  135. pycforge/ide/qt_workspace_features.py +398 -0
  136. pycforge/ide/qt_workspace_navigation.py +208 -0
  137. pycforge/ide/qt_workspace_observers.py +173 -0
  138. pycforge/ide/qt_workspace_panels.py +545 -0
  139. pycforge/ide/qt_workspace_widgets.py +228 -0
  140. pycforge/ide/resources/icons/about.svg +1 -0
  141. pycforge/ide/resources/icons/add-document.svg +1 -0
  142. pycforge/ide/resources/icons/brand-mark.svg +1 -0
  143. pycforge/ide/resources/icons/bundle-search.svg +1 -0
  144. pycforge/ide/resources/icons/cancel.svg +1 -0
  145. pycforge/ide/resources/icons/check.svg +1 -0
  146. pycforge/ide/resources/icons/chevron-right.svg +1 -0
  147. pycforge/ide/resources/icons/close.svg +1 -0
  148. pycforge/ide/resources/icons/collapse-all.svg +1 -0
  149. pycforge/ide/resources/icons/command-palette.svg +1 -0
  150. pycforge/ide/resources/icons/convert.svg +1 -0
  151. pycforge/ide/resources/icons/copy.svg +1 -0
  152. pycforge/ide/resources/icons/cut.svg +1 -0
  153. pycforge/ide/resources/icons/decision-trace.svg +1 -0
  154. pycforge/ide/resources/icons/details.svg +1 -0
  155. pycforge/ide/resources/icons/diagnostics.svg +1 -0
  156. pycforge/ide/resources/icons/duplicate-line.svg +1 -0
  157. pycforge/ide/resources/icons/exit.svg +1 -0
  158. pycforge/ide/resources/icons/expand-all.svg +1 -0
  159. pycforge/ide/resources/icons/export.svg +1 -0
  160. pycforge/ide/resources/icons/find.svg +1 -0
  161. pycforge/ide/resources/icons/go-to-line.svg +1 -0
  162. pycforge/ide/resources/icons/go-to-output.svg +1 -0
  163. pycforge/ide/resources/icons/go-to-source.svg +1 -0
  164. pycforge/ide/resources/icons/history.svg +1 -0
  165. pycforge/ide/resources/icons/indent.svg +1 -0
  166. pycforge/ide/resources/icons/link-c.svg +1 -0
  167. pycforge/ide/resources/icons/mappings.svg +1 -0
  168. pycforge/ide/resources/icons/module.svg +1 -0
  169. pycforge/ide/resources/icons/move-down.svg +1 -0
  170. pycforge/ide/resources/icons/move-line-down.svg +1 -0
  171. pycforge/ide/resources/icons/move-line-up.svg +1 -0
  172. pycforge/ide/resources/icons/move-up.svg +1 -0
  173. pycforge/ide/resources/icons/next.svg +1 -0
  174. pycforge/ide/resources/icons/open.svg +1 -0
  175. pycforge/ide/resources/icons/outdent.svg +1 -0
  176. pycforge/ide/resources/icons/outline.svg +1 -0
  177. pycforge/ide/resources/icons/paste.svg +1 -0
  178. pycforge/ide/resources/icons/previous.svg +1 -0
  179. pycforge/ide/resources/icons/primary-module.svg +1 -0
  180. pycforge/ide/resources/icons/redo.svg +1 -0
  181. pycforge/ide/resources/icons/remove-document.svg +1 -0
  182. pycforge/ide/resources/icons/replace.svg +1 -0
  183. pycforge/ide/resources/icons/save-as.svg +1 -0
  184. pycforge/ide/resources/icons/save.svg +1 -0
  185. pycforge/ide/resources/icons/select-all.svg +1 -0
  186. pycforge/ide/resources/icons/settings.svg +1 -0
  187. pycforge/ide/resources/icons/show-c.svg +1 -0
  188. pycforge/ide/resources/icons/split-view.svg +1 -0
  189. pycforge/ide/resources/icons/summary.svg +1 -0
  190. pycforge/ide/resources/icons/telemetry.svg +1 -0
  191. pycforge/ide/resources/icons/toggle-comment.svg +1 -0
  192. pycforge/ide/resources/icons/toggle-fold.svg +1 -0
  193. pycforge/ide/resources/icons/undo.svg +1 -0
  194. pycforge/ide/resources/icons/whitespace.svg +1 -0
  195. pycforge/ide/revisions.py +400 -0
  196. pycforge/ide/search_service.py +280 -0
  197. pycforge/ide/session_history.py +150 -0
  198. pycforge/ide/source_editing.py +417 -0
  199. pycforge/ide/source_structure.py +558 -0
  200. pycforge/ide/source_structure_async.py +191 -0
  201. pycforge/ide/supervisor.py +503 -0
  202. pycforge/ide/theme.py +119 -0
  203. pycforge/ide/theme_stylesheet.py +598 -0
  204. pycforge/ide/theme_workspace_stylesheet.py +155 -0
  205. pycforge/ide/visual_tokens.py +142 -0
  206. pycforge/ide/worker_protocol.py +80 -0
  207. pycforge/ide/workspace_search.py +462 -0
  208. pycforge/ide/workspace_session.py +256 -0
  209. pycforge/laboratory/__init__.py +6 -0
  210. pycforge/laboratory/audits.py +2244 -0
  211. pycforge/laboratory/checkpoint_e.py +2685 -0
  212. pycforge/laboratory/cli.py +119 -0
  213. pycforge/laboratory/exit_codes.py +11 -0
  214. pycforge/laboratory/keyword_audit.py +661 -0
  215. pycforge/laboratory/keyword_only_audit.py +481 -0
  216. pycforge-0.15.2.dist-info/METADATA +209 -0
  217. pycforge-0.15.2.dist-info/RECORD +221 -0
  218. pycforge-0.15.2.dist-info/WHEEL +5 -0
  219. pycforge-0.15.2.dist-info/entry_points.txt +5 -0
  220. pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
  221. pycforge-0.15.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,500 @@
1
+ """Bounded snapshot projection and visibility behavior for the Qt workspace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PyQt5.QtCore import QTimer
6
+ from PyQt5.QtGui import QTextCursor
7
+ from PyQt5.QtWidgets import QWidget
8
+
9
+ from .editor import EditorMarker
10
+ from .qt_contract import (
11
+ diagnostic_character_range,
12
+ mapping_character_range,
13
+ qt_range,
14
+ )
15
+
16
+
17
+ class QtProjectionMixin:
18
+ """Project immutable controller snapshots without blocking the event loop."""
19
+
20
+ def _apply_snapshot(self, snapshot) -> None:
21
+ pending_document_id = (
22
+ self._source_sync_document_id
23
+ if self._source_sync_pending else None
24
+ )
25
+ if (
26
+ pending_document_id is not None
27
+ and snapshot.active_document.document_id
28
+ != pending_document_id
29
+ ):
30
+ # A controller-side selection may arrive without passing through
31
+ # the window action. Preserve the old editor text before showing
32
+ # the newly active document.
33
+ if not self._flush_pending_source_sync():
34
+ return
35
+ snapshot = self.controller.snapshot
36
+ self._last_snapshot = snapshot
37
+ active = snapshot.active_document
38
+ active_changed = self._displayed_document_id != active.document_id
39
+ self._displayed_document_id = active.document_id
40
+ pending_active = (
41
+ self._source_sync_pending
42
+ and self._source_sync_document_id == active.document_id
43
+ )
44
+ self._project_workspace_features(
45
+ snapshot,
46
+ active_changed=active_changed,
47
+ )
48
+ output_key = (
49
+ snapshot.result_revision_generation,
50
+ id(snapshot.generated_c),
51
+ )
52
+ if output_key != self._displayed_output_key:
53
+ self._displayed_output_key = output_key
54
+ self._start_output_projection(
55
+ snapshot.generated_c or "", output_key
56
+ )
57
+
58
+ navigator_key = (
59
+ snapshot.active_document_id,
60
+ tuple(
61
+ (
62
+ item.document_id,
63
+ item.module_id,
64
+ item.logical_name,
65
+ item.path,
66
+ item.is_primary,
67
+ item.dirty,
68
+ )
69
+ for item in snapshot.documents
70
+ ),
71
+ )
72
+ if navigator_key != self._navigator_key:
73
+ self._navigator_key = navigator_key
74
+ self.navigator.set_documents(
75
+ snapshot.documents, snapshot.active_document_id
76
+ )
77
+
78
+ dirty = " • modified" if active.dirty else ""
79
+ primary = " · PRIMARY" if active.is_primary else ""
80
+ self.active_module_label.setText(
81
+ f"{active.module_id} · {active.logical_name}{primary}{dirty}"
82
+ )
83
+ self.active_path_label.setText(active.path or "Unsaved document")
84
+ self.active_path_label.setToolTip(active.path or "Unsaved document")
85
+ self.status_document.setText(
86
+ active.logical_name + (" •" if active.dirty else "")
87
+ )
88
+ self._set_state_chip(snapshot.state.value)
89
+ self._set_output_state(snapshot)
90
+ self.linked_c_label.setText(
91
+ self._linked_c_display(snapshot.linked_c_path)
92
+ )
93
+ self.linked_c_label.setToolTip(
94
+ snapshot.linked_c_path or "No linked C destination"
95
+ )
96
+ self.linked_c_label.setAccessibleName(
97
+ "Linked C destination: "
98
+ + (snapshot.linked_c_path or "not set")
99
+ )
100
+
101
+ diagnostic_count = len(snapshot.diagnostics)
102
+ self.tabs.setTabText(
103
+ self.tabs.indexOf(self.diags),
104
+ "Diagnostics"
105
+ + (f" {diagnostic_count}" if diagnostic_count else ""),
106
+ )
107
+ self.tabs.setTabText(
108
+ self.tabs.indexOf(self.mappings),
109
+ "Mappings"
110
+ + (
111
+ f" {len(snapshot.mappings)}"
112
+ if snapshot.mappings else ""
113
+ ),
114
+ )
115
+ if self.tabs.isVisible():
116
+ self._refresh_detail_views(snapshot)
117
+ self._project_editor_markers(snapshot)
118
+ self._configure_actions(snapshot)
119
+ self._configure_progress_state(snapshot)
120
+ self._refresh_watchers(snapshot)
121
+ self._update_window_title(active)
122
+ if pending_active:
123
+ self._present_pending_source_sync()
124
+ elif self.navigator.identity_pending:
125
+ self._present_pending_identity_edit(True)
126
+
127
+ if active_changed:
128
+ self.find_bar.attach_editor(self.source)
129
+
130
+ def _start_output_projection(
131
+ self,
132
+ text: str,
133
+ key: tuple[int | None, int],
134
+ ) -> None:
135
+ """Populate generated C in bounded event-loop slices.
136
+
137
+ Progress-only snapshots never materialize either full editor.
138
+ A new complete result invalidates an older projection by key, and
139
+ only the current projection is permitted to append another slice.
140
+ """
141
+
142
+ self._output_projection_key = key
143
+ self._output_projection_text = text
144
+ self._output_projection_offset = 0
145
+ # ``len`` is constant-time and keeps result-envelope publication from
146
+ # scanning all generated C merely to choose a projection mode. Use a
147
+ # deliberately conservative output threshold; the inspector is
148
+ # read-only, so responsiveness takes priority over whole-file syntax
149
+ # decoration for large results.
150
+ self.output.set_large_file_mode(len(text) >= 64 * 1024)
151
+ self.output.clear()
152
+ if not self.output_panel.isVisible():
153
+ return
154
+ self._append_output_projection_slice(key)
155
+
156
+ def _append_output_projection_slice(
157
+ self,
158
+ key: tuple[int | None, int],
159
+ ) -> None:
160
+ if key != self._output_projection_key:
161
+ return
162
+ start = self._output_projection_offset
163
+ text = self._output_projection_text
164
+ if start >= len(text):
165
+ self._output_projection_text = ""
166
+ self._output_marker_key = None
167
+ if self._last_snapshot is not None:
168
+ self._project_editor_markers(self._last_snapshot)
169
+ return
170
+ end = min(len(text), start + 32_768)
171
+ cursor = self.output.textCursor()
172
+ cursor.movePosition(QTextCursor.End)
173
+ cursor.insertText(text[start:end])
174
+ self._output_projection_offset = end
175
+ QTimer.singleShot(
176
+ 0,
177
+ lambda current_key=key: self._append_output_projection_slice(
178
+ current_key
179
+ ),
180
+ )
181
+
182
+ def _refresh_detail_views(
183
+ self, snapshot, *, force: bool = False
184
+ ) -> None:
185
+ key = (
186
+ id(snapshot.diagnostics),
187
+ snapshot.summary,
188
+ id(snapshot.conversion_summary),
189
+ id(snapshot.decision_trace),
190
+ id(snapshot.telemetry),
191
+ id(snapshot.mappings),
192
+ )
193
+ if not force and key == self._detail_projection_key:
194
+ return
195
+ self._detail_projection_key = key
196
+ self.diags.set_diagnostics(snapshot.diagnostics)
197
+ summary_data = (
198
+ snapshot.conversion_summary
199
+ if snapshot.conversion_summary is not None
200
+ else dict(snapshot.summary)
201
+ )
202
+ self.summary.set_data(summary_data or {})
203
+ self.trace.set_data(snapshot.decision_trace or {})
204
+ self.telemetry.set_data(snapshot.telemetry or {})
205
+ self.mappings.set_mappings(snapshot.mappings)
206
+
207
+ def _project_editor_markers(self, snapshot) -> None:
208
+ active = snapshot.active_document
209
+ revision = self.controller.committed_revision
210
+ source_position_index = None
211
+ if revision is not None:
212
+ try:
213
+ source_position_index = revision.index_for(
214
+ active.document_id
215
+ ).position_index
216
+ except KeyError:
217
+ source_position_index = None
218
+ diagnostics_current = snapshot.state.value not in {
219
+ "empty",
220
+ "stale",
221
+ "converting",
222
+ "cancel-requested",
223
+ "canceled",
224
+ }
225
+ source_key = (
226
+ active.document_id,
227
+ snapshot.state.value if diagnostics_current else "inactive",
228
+ snapshot.source_fingerprint if diagnostics_current else None,
229
+ id(snapshot.diagnostics) if diagnostics_current else None,
230
+ )
231
+ if source_key != self._source_marker_key:
232
+ self._source_marker_key = source_key
233
+ diagnostic_markers: list[EditorMarker] = []
234
+ if diagnostics_current:
235
+ for diagnostic in snapshot.diagnostics:
236
+ if (
237
+ diagnostic.get("source_logical_name")
238
+ != active.logical_name
239
+ and diagnostic.get("source_module_id")
240
+ != active.module_id
241
+ ):
242
+ continue
243
+ start, end = diagnostic_character_range(
244
+ diagnostic,
245
+ active.text,
246
+ source_position_index,
247
+ )
248
+ start, end = qt_range(
249
+ active.text,
250
+ start,
251
+ end,
252
+ source_position_index,
253
+ )
254
+ severity = str(
255
+ diagnostic.get("severity") or "Error"
256
+ )
257
+ kind = (
258
+ "warning"
259
+ if severity in {"Warning", "Approximation"}
260
+ else "info"
261
+ if severity in {"Info", "Information"}
262
+ else "diagnostic"
263
+ )
264
+ diagnostic_markers.append(
265
+ EditorMarker(
266
+ start,
267
+ end,
268
+ kind=kind,
269
+ message=(
270
+ f"{diagnostic.get('code', '')} · "
271
+ f"{diagnostic.get('message', '')}"
272
+ ),
273
+ marker_id=str(
274
+ diagnostic.get("diagnostic_id") or ""
275
+ ),
276
+ )
277
+ )
278
+ self.source.set_diagnostic_ranges(diagnostic_markers)
279
+ self.source_secondary.set_diagnostic_ranges(
280
+ diagnostic_markers
281
+ )
282
+
283
+ output_text = snapshot.generated_c or ""
284
+ output_position_index = self.controller.result_output_index
285
+ output_key = (
286
+ id(snapshot.mappings),
287
+ id(snapshot.generated_c),
288
+ id(output_position_index),
289
+ )
290
+ if output_key == self._output_marker_key:
291
+ return
292
+ self._output_marker_key = output_key
293
+ if (
294
+ output_text
295
+ and snapshot.mappings
296
+ and output_position_index is None
297
+ ):
298
+ self.output.set_mapping_ranges(())
299
+ return
300
+ mapping_markers = []
301
+ for mapping in snapshot.mappings[:5000]:
302
+ if not (
303
+ mapping.get("module_id")
304
+ or mapping.get("source_node_ids")
305
+ ):
306
+ continue
307
+ start, end = mapping_character_range(
308
+ mapping,
309
+ output_text,
310
+ output_position_index,
311
+ )
312
+ start, end = qt_range(
313
+ output_text,
314
+ start,
315
+ end,
316
+ output_position_index,
317
+ )
318
+ mapping_markers.append(
319
+ EditorMarker(
320
+ start,
321
+ end,
322
+ kind="mapping",
323
+ message=(
324
+ f"{mapping.get('module_id') or 'source'} · "
325
+ f"{mapping.get('rule_plan_id') or mapping.get('origin_kind') or ''}"
326
+ ),
327
+ marker_id=str(mapping.get("c_node_id") or ""),
328
+ )
329
+ )
330
+ self.output.set_mapping_ranges(mapping_markers)
331
+
332
+ def _configure_actions(self, snapshot) -> None:
333
+ self.action_registry.refresh()
334
+ self.navigator.add_button.setEnabled(snapshot.can_add_document)
335
+ self.navigator.remove_button.setEnabled(
336
+ snapshot.can_remove_document
337
+ )
338
+
339
+ def _set_state_chip(self, state: str) -> None:
340
+ labels = {
341
+ "observer-incomplete": "TRANSPILED · OBSERVER INCOMPLETE",
342
+ "approximation": "TRANSPILED · APPROXIMATION",
343
+ "warning": "TRANSPILED · WARNING",
344
+ }
345
+ self.state_chip.setText(
346
+ labels.get(state, state.replace("-", " ").upper())
347
+ )
348
+ if state in {"converted"}:
349
+ tone = "success"
350
+ elif state in {
351
+ "warning",
352
+ "approximation",
353
+ "observer-incomplete",
354
+ "stale",
355
+ }:
356
+ tone = "warning"
357
+ elif state in {"rejected", "failed"}:
358
+ tone = "error"
359
+ else:
360
+ tone = "neutral"
361
+ self._set_dynamic_property(self.state_chip, "status", tone)
362
+
363
+ def _set_output_state(self, snapshot) -> None:
364
+ if snapshot.generated_c is None:
365
+ label, tone = "NO RESULT", "neutral"
366
+ elif snapshot.can_save_c:
367
+ label, tone = "CURRENT · SAVE ELIGIBLE", "success"
368
+ elif snapshot.state.value == "stale":
369
+ label, tone = "STALE · LAST COMPLETE C", "warning"
370
+ elif snapshot.state.value == "rejected":
371
+ label, tone = "PREVIOUS C · REQUEST REJECTED", "error"
372
+ elif snapshot.state.value == "converting":
373
+ label, tone = "HELD · TRANSPILATION ACTIVE", "neutral"
374
+ elif snapshot.state.value == "cancel-requested":
375
+ label, tone = "HELD · CANCELLATION REQUESTED", "warning"
376
+ else:
377
+ label, tone = "PREVIOUS C · NOT SAVABLE", "warning"
378
+ self.output_state_label.setText(label)
379
+ self._set_dynamic_property(
380
+ self.output_state_label, "status", tone
381
+ )
382
+
383
+ @staticmethod
384
+ def _set_dynamic_property(
385
+ widget: QWidget, name: str, value: str
386
+ ) -> None:
387
+ widget.setProperty(name, value)
388
+ widget.style().unpolish(widget)
389
+ widget.style().polish(widget)
390
+ widget.update()
391
+
392
+ def _configure_progress_state(self, snapshot) -> None:
393
+ if snapshot.state.value in {"converting", "cancel-requested"}:
394
+ self._configure_progress(snapshot)
395
+ if self._progress_sequence != snapshot.request_sequence:
396
+ self._progress_sequence = snapshot.request_sequence
397
+ self.progress.hide()
398
+ QTimer.singleShot(
399
+ 180,
400
+ lambda sequence=snapshot.request_sequence:
401
+ self._show_progress_if_current(sequence),
402
+ )
403
+ stage = self._STAGE_LABELS.get(
404
+ snapshot.active_stage,
405
+ snapshot.active_stage or "Preparing transpilation pipeline",
406
+ )
407
+ if snapshot.state.value == "cancel-requested":
408
+ self.statusBar().showMessage(
409
+ "Cancel requested — waiting for isolated worker shutdown"
410
+ )
411
+ else:
412
+ self.statusBar().showMessage(f"Transpiling — {stage}")
413
+ else:
414
+ self.progress.hide()
415
+ count = len(snapshot.diagnostics)
416
+ message = snapshot.state.value.replace("-", " ").title()
417
+ if count:
418
+ message += f" — {count} diagnostic" + (
419
+ "" if count == 1 else "s"
420
+ )
421
+ if snapshot.stale_reason:
422
+ message += (
423
+ f" — {snapshot.stale_reason.replace('-', ' ')}"
424
+ )
425
+ self.statusBar().showMessage(message)
426
+
427
+ def _configure_progress(self, snapshot) -> None:
428
+ if snapshot.total_stages:
429
+ self.progress.setRange(0, snapshot.total_stages)
430
+ self.progress.setValue(snapshot.completed_stages)
431
+ self.progress.setFormat("%v/%m stages")
432
+ else:
433
+ self.progress.setRange(0, 0)
434
+ self.progress.setFormat("Transpiling")
435
+
436
+ def _show_progress_if_current(self, sequence: int) -> None:
437
+ snapshot = self.controller.snapshot
438
+ if (
439
+ snapshot.state.value != "converting"
440
+ or snapshot.request_sequence != sequence
441
+ ):
442
+ return
443
+ self._configure_progress(snapshot)
444
+ self.progress.show()
445
+
446
+ def _set_output_visible(self, visible: bool) -> None:
447
+ self.output_panel.setVisible(visible)
448
+ self.output.setVisible(visible)
449
+ self.output_tabs.setVisible(visible)
450
+ self.action_registry.refresh()
451
+ if visible:
452
+ self.output_tabs.setCurrentWidget(self.output)
453
+ if (
454
+ self._output_projection_key is not None
455
+ and self._output_projection_offset
456
+ < len(self._output_projection_text)
457
+ ):
458
+ QTimer.singleShot(
459
+ 0,
460
+ lambda key=self._output_projection_key:
461
+ self._append_output_projection_slice(key),
462
+ )
463
+ QTimer.singleShot(0, self._balance_editor_splitter)
464
+
465
+ def _set_details_visible(self, visible: bool) -> None:
466
+ self.tabs.setVisible(visible)
467
+ self.action_registry.refresh()
468
+ if visible:
469
+ self._refresh_detail_views(
470
+ self.controller.snapshot, force=True
471
+ )
472
+ QTimer.singleShot(0, self._balance_main_splitter)
473
+
474
+ def _balance_editor_splitter(self) -> None:
475
+ available = max(
476
+ 2,
477
+ self.editor_splitter.width()
478
+ - self.editor_splitter.handleWidth(),
479
+ )
480
+ source_width = available // 2
481
+ self.editor_splitter.setSizes(
482
+ (source_width, available - source_width)
483
+ )
484
+
485
+ def _balance_main_splitter(self) -> None:
486
+ available = max(
487
+ 2,
488
+ self.main_splitter.height() - self.main_splitter.handleWidth(),
489
+ )
490
+ workspace_height = round(available * 0.65)
491
+ self.main_splitter.setSizes(
492
+ (workspace_height, available - workspace_height)
493
+ )
494
+
495
+ def _set_navigator_visible(self, visible: bool) -> None:
496
+ self.navigator.setVisible(visible)
497
+ self.action_registry.refresh()
498
+
499
+
500
+ __all__ = ["QtProjectionMixin"]