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.
- pycforge/__init__.py +8 -0
- pycforge/__main__.py +2 -0
- pycforge/_version.py +1 -0
- pycforge/converter/__init__.py +0 -0
- pycforge/converter/analysis/__init__.py +5 -0
- pycforge/converter/analysis/functions.py +723 -0
- pycforge/converter/analysis/model.py +158 -0
- pycforge/converter/analysis/planning.py +464 -0
- pycforge/converter/analysis/stage.py +1976 -0
- pycforge/converter/analysis/symbols.py +237 -0
- pycforge/converter/analysis/validation.py +2166 -0
- pycforge/converter/c_output/__init__.py +2 -0
- pycforge/converter/c_output/conformance.py +297 -0
- pycforge/converter/c_output/renderer.py +220 -0
- pycforge/converter/conditional_regions/__init__.py +71 -0
- pycforge/converter/conditional_regions/analysis.py +481 -0
- pycforge/converter/conditional_regions/lowering.py +579 -0
- pycforge/converter/conditional_regions/model.py +243 -0
- pycforge/converter/conditional_regions/validation.py +690 -0
- pycforge/converter/containers/__init__.py +19 -0
- pycforge/converter/containers/analysis.py +542 -0
- pycforge/converter/containers/lowering.py +290 -0
- pycforge/converter/containers/model.py +146 -0
- pycforge/converter/contracts/__init__.py +0 -0
- pycforge/converter/contracts/configuration.py +178 -0
- pycforge/converter/contracts/identifiers.py +68 -0
- pycforge/converter/contracts/versions.py +71 -0
- pycforge/converter/core/__init__.py +0 -0
- pycforge/converter/core/artifact_io.py +87 -0
- pycforge/converter/core/cancellation.py +7 -0
- pycforge/converter/core/canonicalization.py +254 -0
- pycforge/converter/core/context.py +13 -0
- pycforge/converter/core/diagnostics.py +95 -0
- pycforge/converter/core/enums.py +23 -0
- pycforge/converter/core/fingerprint.py +22 -0
- pycforge/converter/core/pipeline.py +8 -0
- pycforge/converter/core/progress.py +25 -0
- pycforge/converter/core/request.py +50 -0
- pycforge/converter/core/resource_policy.py +31 -0
- pycforge/converter/core/result.py +23 -0
- pycforge/converter/core/serialization.py +36 -0
- pycforge/converter/core/stage.py +10 -0
- pycforge/converter/core/stage_artifact.py +80 -0
- pycforge/converter/core/stage_outcome.py +15 -0
- pycforge/converter/decision_trace/__init__.py +0 -0
- pycforge/converter/decision_trace/recorder.py +52 -0
- pycforge/converter/facade.py +459 -0
- pycforge/converter/frontend/__init__.py +4 -0
- pycforge/converter/frontend/normalizer.py +59 -0
- pycforge/converter/frontend/parser.py +20 -0
- pycforge/converter/frontend/source_document.py +93 -0
- pycforge/converter/frontend/stages.py +518 -0
- pycforge/converter/frontend/validation.py +76 -0
- pycforge/converter/io/__init__.py +0 -0
- pycforge/converter/io/atomic_writer.py +33 -0
- pycforge/converter/ir/__init__.py +1 -0
- pycforge/converter/ir/c_ir/__init__.py +4 -0
- pycforge/converter/ir/c_ir/builder.py +26 -0
- pycforge/converter/ir/c_ir/model.py +318 -0
- pycforge/converter/ir/c_ir/serialization.py +88 -0
- pycforge/converter/ir/c_ir/validation.py +834 -0
- pycforge/converter/ir/python_ir/__init__.py +17 -0
- pycforge/converter/ir/python_ir/nodes.py +86 -0
- pycforge/converter/keyword_calls/__init__.py +68 -0
- pycforge/converter/keyword_calls/analysis.py +458 -0
- pycforge/converter/keyword_calls/lowering.py +271 -0
- pycforge/converter/keyword_calls/model.py +304 -0
- pycforge/converter/keyword_calls/validation.py +802 -0
- pycforge/converter/keyword_only_calls/__init__.py +68 -0
- pycforge/converter/keyword_only_calls/analysis.py +640 -0
- pycforge/converter/keyword_only_calls/lowering.py +376 -0
- pycforge/converter/keyword_only_calls/model.py +328 -0
- pycforge/converter/keyword_only_calls/validation.py +1357 -0
- pycforge/converter/lowering.py +1000 -0
- pycforge/converter/modules/__init__.py +13 -0
- pycforge/converter/modules/analysis.py +1250 -0
- pycforge/converter/modules/lowering.py +200 -0
- pycforge/converter/modules/model.py +73 -0
- pycforge/converter/modules/stage.py +192 -0
- pycforge/converter/numeric_semantics/__init__.py +23 -0
- pycforge/converter/numeric_semantics/analysis.py +420 -0
- pycforge/converter/numeric_semantics/lowering.py +153 -0
- pycforge/converter/numeric_semantics/model.py +123 -0
- pycforge/converter/records/__init__.py +34 -0
- pycforge/converter/records/analysis.py +1394 -0
- pycforge/converter/records/lowering.py +393 -0
- pycforge/converter/records/model.py +319 -0
- pycforge/converter/rules/__init__.py +3 -0
- pycforge/converter/support_templates/__init__.py +49 -0
- pycforge/converter/support_templates/assembly.py +106 -0
- pycforge/converter/support_templates/factories.py +348 -0
- pycforge/converter/support_templates/model.py +159 -0
- pycforge/converter/support_templates/registry.py +388 -0
- pycforge/converter/telemetry/__init__.py +0 -0
- pycforge/converter/telemetry/sink.py +39 -0
- pycforge/ide/__init__.py +19 -0
- pycforge/ide/__main__.py +5 -0
- pycforge/ide/_worker_event_codec.py +276 -0
- pycforge/ide/_worker_protocol_json.py +197 -0
- pycforge/ide/_worker_protocol_types.py +90 -0
- pycforge/ide/_worker_request_codec.py +200 -0
- pycforge/ide/_worker_request_data.py +260 -0
- pycforge/ide/_worker_result_data.py +343 -0
- pycforge/ide/action_contract.py +469 -0
- pycforge/ide/action_validation.py +229 -0
- pycforge/ide/command_palette.py +223 -0
- pycforge/ide/controller.py +904 -0
- pycforge/ide/controller_conversion.py +546 -0
- pycforge/ide/controller_io.py +205 -0
- pycforge/ide/editor.py +951 -0
- pycforge/ide/editor_commands_qt.py +274 -0
- pycforge/ide/editor_lexical.py +43 -0
- pycforge/ide/editor_sidebars.py +236 -0
- pycforge/ide/editor_syntax.py +243 -0
- pycforge/ide/find_replace.py +535 -0
- pycforge/ide/icons.py +91 -0
- pycforge/ide/io_service.py +196 -0
- pycforge/ide/model.py +164 -0
- pycforge/ide/panels.py +903 -0
- pycforge/ide/positions.py +220 -0
- pycforge/ide/process_worker.py +130 -0
- pycforge/ide/qt.py +230 -0
- pycforge/ide/qt_actions.py +418 -0
- pycforge/ide/qt_close.py +169 -0
- pycforge/ide/qt_command_palette.py +297 -0
- pycforge/ide/qt_contract.py +220 -0
- pycforge/ide/qt_documents.py +590 -0
- pycforge/ide/qt_editor_buffers.py +113 -0
- pycforge/ide/qt_editor_surfaces.py +112 -0
- pycforge/ide/qt_menus.py +324 -0
- pycforge/ide/qt_projection.py +500 -0
- pycforge/ide/qt_shell.py +348 -0
- pycforge/ide/qt_shell_interactions.py +320 -0
- pycforge/ide/qt_state.py +393 -0
- pycforge/ide/qt_workspace_features.py +398 -0
- pycforge/ide/qt_workspace_navigation.py +208 -0
- pycforge/ide/qt_workspace_observers.py +173 -0
- pycforge/ide/qt_workspace_panels.py +545 -0
- pycforge/ide/qt_workspace_widgets.py +228 -0
- pycforge/ide/resources/icons/about.svg +1 -0
- pycforge/ide/resources/icons/add-document.svg +1 -0
- pycforge/ide/resources/icons/brand-mark.svg +1 -0
- pycforge/ide/resources/icons/bundle-search.svg +1 -0
- pycforge/ide/resources/icons/cancel.svg +1 -0
- pycforge/ide/resources/icons/check.svg +1 -0
- pycforge/ide/resources/icons/chevron-right.svg +1 -0
- pycforge/ide/resources/icons/close.svg +1 -0
- pycforge/ide/resources/icons/collapse-all.svg +1 -0
- pycforge/ide/resources/icons/command-palette.svg +1 -0
- pycforge/ide/resources/icons/convert.svg +1 -0
- pycforge/ide/resources/icons/copy.svg +1 -0
- pycforge/ide/resources/icons/cut.svg +1 -0
- pycforge/ide/resources/icons/decision-trace.svg +1 -0
- pycforge/ide/resources/icons/details.svg +1 -0
- pycforge/ide/resources/icons/diagnostics.svg +1 -0
- pycforge/ide/resources/icons/duplicate-line.svg +1 -0
- pycforge/ide/resources/icons/exit.svg +1 -0
- pycforge/ide/resources/icons/expand-all.svg +1 -0
- pycforge/ide/resources/icons/export.svg +1 -0
- pycforge/ide/resources/icons/find.svg +1 -0
- pycforge/ide/resources/icons/go-to-line.svg +1 -0
- pycforge/ide/resources/icons/go-to-output.svg +1 -0
- pycforge/ide/resources/icons/go-to-source.svg +1 -0
- pycforge/ide/resources/icons/history.svg +1 -0
- pycforge/ide/resources/icons/indent.svg +1 -0
- pycforge/ide/resources/icons/link-c.svg +1 -0
- pycforge/ide/resources/icons/mappings.svg +1 -0
- pycforge/ide/resources/icons/module.svg +1 -0
- pycforge/ide/resources/icons/move-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-up.svg +1 -0
- pycforge/ide/resources/icons/move-up.svg +1 -0
- pycforge/ide/resources/icons/next.svg +1 -0
- pycforge/ide/resources/icons/open.svg +1 -0
- pycforge/ide/resources/icons/outdent.svg +1 -0
- pycforge/ide/resources/icons/outline.svg +1 -0
- pycforge/ide/resources/icons/paste.svg +1 -0
- pycforge/ide/resources/icons/previous.svg +1 -0
- pycforge/ide/resources/icons/primary-module.svg +1 -0
- pycforge/ide/resources/icons/redo.svg +1 -0
- pycforge/ide/resources/icons/remove-document.svg +1 -0
- pycforge/ide/resources/icons/replace.svg +1 -0
- pycforge/ide/resources/icons/save-as.svg +1 -0
- pycforge/ide/resources/icons/save.svg +1 -0
- pycforge/ide/resources/icons/select-all.svg +1 -0
- pycforge/ide/resources/icons/settings.svg +1 -0
- pycforge/ide/resources/icons/show-c.svg +1 -0
- pycforge/ide/resources/icons/split-view.svg +1 -0
- pycforge/ide/resources/icons/summary.svg +1 -0
- pycforge/ide/resources/icons/telemetry.svg +1 -0
- pycforge/ide/resources/icons/toggle-comment.svg +1 -0
- pycforge/ide/resources/icons/toggle-fold.svg +1 -0
- pycforge/ide/resources/icons/undo.svg +1 -0
- pycforge/ide/resources/icons/whitespace.svg +1 -0
- pycforge/ide/revisions.py +400 -0
- pycforge/ide/search_service.py +280 -0
- pycforge/ide/session_history.py +150 -0
- pycforge/ide/source_editing.py +417 -0
- pycforge/ide/source_structure.py +558 -0
- pycforge/ide/source_structure_async.py +191 -0
- pycforge/ide/supervisor.py +503 -0
- pycforge/ide/theme.py +119 -0
- pycforge/ide/theme_stylesheet.py +598 -0
- pycforge/ide/theme_workspace_stylesheet.py +155 -0
- pycforge/ide/visual_tokens.py +142 -0
- pycforge/ide/worker_protocol.py +80 -0
- pycforge/ide/workspace_search.py +462 -0
- pycforge/ide/workspace_session.py +256 -0
- pycforge/laboratory/__init__.py +6 -0
- pycforge/laboratory/audits.py +2244 -0
- pycforge/laboratory/checkpoint_e.py +2685 -0
- pycforge/laboratory/cli.py +119 -0
- pycforge/laboratory/exit_codes.py +11 -0
- pycforge/laboratory/keyword_audit.py +661 -0
- pycforge/laboratory/keyword_only_audit.py +481 -0
- pycforge-0.15.2.dist-info/METADATA +209 -0
- pycforge-0.15.2.dist-info/RECORD +221 -0
- pycforge-0.15.2.dist-info/WHEEL +5 -0
- pycforge-0.15.2.dist-info/entry_points.txt +5 -0
- pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
- pycforge-0.15.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"""Bounded source outline and breadcrumb observers for open documents.
|
|
2
|
+
|
|
3
|
+
Structure is derived from the converter's declared Python 3.11 parser and
|
|
4
|
+
normalizer, but remains presentation-only. Invalid source, observer failure,
|
|
5
|
+
or a superseded request produces no converter fact and cannot change source
|
|
6
|
+
eligibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from time import monotonic
|
|
13
|
+
from typing import Callable, Iterable
|
|
14
|
+
|
|
15
|
+
from pycforge.converter.frontend.normalizer import PythonNormalizer
|
|
16
|
+
from pycforge.converter.frontend.parser import Python311ParserAdapter
|
|
17
|
+
from pycforge.converter.frontend.source_document import SourceDocument
|
|
18
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MAX_STRUCTURE_DOCUMENTS = 64
|
|
22
|
+
MAX_OUTLINE_SYMBOLS = 4_096
|
|
23
|
+
MAX_OUTLINE_DEPTH = 64
|
|
24
|
+
MAX_OUTLINE_NAME_CHARS = 256
|
|
25
|
+
MAX_OUTLINE_TEXT_CHARS = 256 * 1024
|
|
26
|
+
MAX_STRUCTURE_DOCUMENT_ID_CHARS = 128
|
|
27
|
+
MAX_STRUCTURE_MODULE_ID_CHARS = 255
|
|
28
|
+
MAX_STRUCTURE_LOGICAL_NAME_CHARS = 4_096
|
|
29
|
+
MAX_STRUCTURE_WORKSPACE_KEY_CHARS = 256
|
|
30
|
+
|
|
31
|
+
DEFAULT_OUTLINE_DEPTH = 32
|
|
32
|
+
DEFAULT_OUTLINE_NAME_CHARS = 160
|
|
33
|
+
DEFAULT_OUTLINE_TEXT_CHARS = 128 * 1024
|
|
34
|
+
_SYMBOL_KINDS = frozenset(
|
|
35
|
+
{"Module", "ClassDef", "FunctionDef", "AsyncFunctionDef"}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class SourceStructureDocument:
|
|
41
|
+
"""One already-open source document captured by an observer request."""
|
|
42
|
+
|
|
43
|
+
document_id: str
|
|
44
|
+
module_id: str
|
|
45
|
+
logical_name: str
|
|
46
|
+
text: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class OutlineSymbol:
|
|
51
|
+
"""One flat, parent-linked outline record with code-point positions."""
|
|
52
|
+
|
|
53
|
+
document_id: str
|
|
54
|
+
module_id: str
|
|
55
|
+
logical_name: str
|
|
56
|
+
node_id: str
|
|
57
|
+
parent_node_id: str | None
|
|
58
|
+
kind: str
|
|
59
|
+
name: str
|
|
60
|
+
detail: str
|
|
61
|
+
depth: int
|
|
62
|
+
start: int
|
|
63
|
+
end: int
|
|
64
|
+
start_line: int
|
|
65
|
+
start_column: int
|
|
66
|
+
end_line: int
|
|
67
|
+
end_column: int
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class SourceStructureResult:
|
|
72
|
+
"""A bounded, non-authoritative structure snapshot."""
|
|
73
|
+
|
|
74
|
+
generation: int
|
|
75
|
+
workspace_key: str
|
|
76
|
+
symbols: tuple[OutlineSymbol, ...]
|
|
77
|
+
total_symbol_count: int
|
|
78
|
+
invalid_document_ids: tuple[str, ...]
|
|
79
|
+
observer_failed_document_ids: tuple[str, ...]
|
|
80
|
+
truncated: bool
|
|
81
|
+
elapsed_seconds: float
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class _Candidate:
|
|
86
|
+
node_id: str
|
|
87
|
+
raw_parent_id: str | None
|
|
88
|
+
kind: str
|
|
89
|
+
name: str
|
|
90
|
+
start: int
|
|
91
|
+
end: int
|
|
92
|
+
start_line: int
|
|
93
|
+
start_column: int
|
|
94
|
+
end_line: int
|
|
95
|
+
end_column: int
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _captured_documents(
|
|
99
|
+
documents: Iterable[SourceStructureDocument],
|
|
100
|
+
) -> tuple[SourceStructureDocument, ...]:
|
|
101
|
+
records = tuple(documents)
|
|
102
|
+
if not 1 <= len(records) <= MAX_STRUCTURE_DOCUMENTS:
|
|
103
|
+
raise ValueError("structure requires between 1 and 64 open documents")
|
|
104
|
+
document_ids: list[str] = []
|
|
105
|
+
module_ids: list[str] = []
|
|
106
|
+
logical_names: list[str] = []
|
|
107
|
+
for document in records:
|
|
108
|
+
if not isinstance(document, SourceStructureDocument):
|
|
109
|
+
raise TypeError(
|
|
110
|
+
"structure documents must be SourceStructureDocument values"
|
|
111
|
+
)
|
|
112
|
+
for label, value, maximum in (
|
|
113
|
+
(
|
|
114
|
+
"document ID",
|
|
115
|
+
document.document_id,
|
|
116
|
+
MAX_STRUCTURE_DOCUMENT_ID_CHARS,
|
|
117
|
+
),
|
|
118
|
+
(
|
|
119
|
+
"module ID",
|
|
120
|
+
document.module_id,
|
|
121
|
+
MAX_STRUCTURE_MODULE_ID_CHARS,
|
|
122
|
+
),
|
|
123
|
+
(
|
|
124
|
+
"logical name",
|
|
125
|
+
document.logical_name,
|
|
126
|
+
MAX_STRUCTURE_LOGICAL_NAME_CHARS,
|
|
127
|
+
),
|
|
128
|
+
):
|
|
129
|
+
if (
|
|
130
|
+
not isinstance(value, str)
|
|
131
|
+
or not value
|
|
132
|
+
or len(value) > maximum
|
|
133
|
+
):
|
|
134
|
+
raise ValueError(f"structure {label} must be non-empty text")
|
|
135
|
+
if not isinstance(document.text, str):
|
|
136
|
+
raise TypeError("structure document text must be a string")
|
|
137
|
+
document_ids.append(document.document_id)
|
|
138
|
+
module_ids.append(document.module_id)
|
|
139
|
+
logical_names.append(document.logical_name)
|
|
140
|
+
for label, values in (
|
|
141
|
+
("document IDs", document_ids),
|
|
142
|
+
("module IDs", module_ids),
|
|
143
|
+
("logical names", logical_names),
|
|
144
|
+
):
|
|
145
|
+
if len(values) != len(set(values)):
|
|
146
|
+
raise ValueError(f"structure {label} must be unique")
|
|
147
|
+
return records
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _bounded_int(
|
|
151
|
+
value: object,
|
|
152
|
+
*,
|
|
153
|
+
name: str,
|
|
154
|
+
maximum: int,
|
|
155
|
+
) -> int:
|
|
156
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
157
|
+
raise TypeError(f"{name} must be an integer")
|
|
158
|
+
if not 1 <= value <= maximum:
|
|
159
|
+
raise ValueError(f"{name} must be between 1 and {maximum}")
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _clipped_name(value: str, budget: int) -> tuple[str, bool]:
|
|
164
|
+
if len(value) <= budget:
|
|
165
|
+
return value, False
|
|
166
|
+
if budget == 1:
|
|
167
|
+
return "\N{HORIZONTAL ELLIPSIS}", True
|
|
168
|
+
return value[: budget - 1] + "\N{HORIZONTAL ELLIPSIS}", True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _span_values(
|
|
172
|
+
span: object,
|
|
173
|
+
*,
|
|
174
|
+
text_length: int,
|
|
175
|
+
) -> tuple[int, int, int, int, int, int] | None:
|
|
176
|
+
if not isinstance(span, dict):
|
|
177
|
+
return None
|
|
178
|
+
start = span.get("start")
|
|
179
|
+
end = span.get("end")
|
|
180
|
+
if not isinstance(start, dict) or not isinstance(end, dict):
|
|
181
|
+
return None
|
|
182
|
+
values = (
|
|
183
|
+
start.get("offset"),
|
|
184
|
+
end.get("offset"),
|
|
185
|
+
start.get("line"),
|
|
186
|
+
start.get("column"),
|
|
187
|
+
end.get("line"),
|
|
188
|
+
end.get("column"),
|
|
189
|
+
)
|
|
190
|
+
if any(isinstance(value, bool) or not isinstance(value, int) for value in values):
|
|
191
|
+
return None
|
|
192
|
+
start_offset, end_offset, start_line, start_column, end_line, end_column = values
|
|
193
|
+
if (
|
|
194
|
+
start_offset < 0
|
|
195
|
+
or end_offset < start_offset
|
|
196
|
+
or end_offset > text_length
|
|
197
|
+
or start_line < 1
|
|
198
|
+
or end_line < start_line
|
|
199
|
+
or start_column < 0
|
|
200
|
+
or end_column < 0
|
|
201
|
+
):
|
|
202
|
+
return None
|
|
203
|
+
return (
|
|
204
|
+
start_offset,
|
|
205
|
+
end_offset,
|
|
206
|
+
start_line,
|
|
207
|
+
start_column,
|
|
208
|
+
end_line,
|
|
209
|
+
end_column,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _document_candidates(
|
|
214
|
+
document: SourceStructureDocument,
|
|
215
|
+
) -> tuple[_Candidate, ...]:
|
|
216
|
+
source = SourceDocument.create(document.logical_name, document.text)
|
|
217
|
+
tree = Python311ParserAdapter().parse(source, "3.11")
|
|
218
|
+
normalized = PythonNormalizer().normalize(tree, source)
|
|
219
|
+
known = {node.node_id for node in normalized.nodes}
|
|
220
|
+
parents: dict[str, str] = {}
|
|
221
|
+
for node in normalized.nodes:
|
|
222
|
+
for field_name, value in node.fields:
|
|
223
|
+
for child_id in python_ir_reference_ids(
|
|
224
|
+
node.kind, field_name, value, known
|
|
225
|
+
):
|
|
226
|
+
parents.setdefault(child_id, node.node_id)
|
|
227
|
+
|
|
228
|
+
candidates: list[_Candidate] = []
|
|
229
|
+
for node in normalized.nodes:
|
|
230
|
+
if node.kind not in _SYMBOL_KINDS:
|
|
231
|
+
continue
|
|
232
|
+
if node.kind == "Module":
|
|
233
|
+
candidates.append(
|
|
234
|
+
_Candidate(
|
|
235
|
+
node.node_id,
|
|
236
|
+
None,
|
|
237
|
+
node.kind,
|
|
238
|
+
document.module_id,
|
|
239
|
+
0,
|
|
240
|
+
len(document.text),
|
|
241
|
+
1,
|
|
242
|
+
0,
|
|
243
|
+
document.text.count("\n") + 1,
|
|
244
|
+
0,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
continue
|
|
248
|
+
fields = dict(node.fields)
|
|
249
|
+
name = fields.get("name")
|
|
250
|
+
if not isinstance(name, str) or not name:
|
|
251
|
+
continue
|
|
252
|
+
values = _span_values(
|
|
253
|
+
node.provenance.source_span,
|
|
254
|
+
text_length=len(document.text),
|
|
255
|
+
)
|
|
256
|
+
if values is None:
|
|
257
|
+
continue
|
|
258
|
+
candidates.append(
|
|
259
|
+
_Candidate(
|
|
260
|
+
node.node_id,
|
|
261
|
+
parents.get(node.node_id),
|
|
262
|
+
node.kind,
|
|
263
|
+
name,
|
|
264
|
+
*values,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
candidate_ids = {candidate.node_id for candidate in candidates}
|
|
268
|
+
resolved: list[_Candidate] = []
|
|
269
|
+
for candidate in candidates:
|
|
270
|
+
parent = candidate.raw_parent_id
|
|
271
|
+
seen: set[str] = set()
|
|
272
|
+
while parent is not None and parent not in candidate_ids:
|
|
273
|
+
if parent in seen:
|
|
274
|
+
parent = None
|
|
275
|
+
break
|
|
276
|
+
seen.add(parent)
|
|
277
|
+
parent = parents.get(parent)
|
|
278
|
+
resolved.append(
|
|
279
|
+
_Candidate(
|
|
280
|
+
candidate.node_id,
|
|
281
|
+
parent,
|
|
282
|
+
candidate.kind,
|
|
283
|
+
candidate.name,
|
|
284
|
+
candidate.start,
|
|
285
|
+
candidate.end,
|
|
286
|
+
candidate.start_line,
|
|
287
|
+
candidate.start_column,
|
|
288
|
+
candidate.end_line,
|
|
289
|
+
candidate.end_column,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
return tuple(resolved)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _nearest_symbol_parents(
|
|
296
|
+
candidates: tuple[_Candidate, ...],
|
|
297
|
+
) -> tuple[dict[str, str | None], dict[str, int]]:
|
|
298
|
+
candidate_ids = {candidate.node_id for candidate in candidates}
|
|
299
|
+
raw_parents = {
|
|
300
|
+
candidate.node_id: candidate.raw_parent_id
|
|
301
|
+
for candidate in candidates
|
|
302
|
+
}
|
|
303
|
+
parents: dict[str, str | None] = {}
|
|
304
|
+
for candidate in candidates:
|
|
305
|
+
parent = candidate.raw_parent_id
|
|
306
|
+
seen: set[str] = set()
|
|
307
|
+
while parent is not None and parent not in candidate_ids:
|
|
308
|
+
if parent in seen:
|
|
309
|
+
parent = None
|
|
310
|
+
break
|
|
311
|
+
seen.add(parent)
|
|
312
|
+
parent = raw_parents.get(parent)
|
|
313
|
+
parents[candidate.node_id] = parent
|
|
314
|
+
|
|
315
|
+
depths: dict[str, int] = {}
|
|
316
|
+
|
|
317
|
+
def depth_for(node_id: str) -> int:
|
|
318
|
+
if node_id in depths:
|
|
319
|
+
return depths[node_id]
|
|
320
|
+
chain: list[str] = []
|
|
321
|
+
current: str | None = node_id
|
|
322
|
+
seen: set[str] = set()
|
|
323
|
+
while current is not None and current not in depths:
|
|
324
|
+
if current in seen:
|
|
325
|
+
for item in chain:
|
|
326
|
+
depths[item] = 0
|
|
327
|
+
return 0
|
|
328
|
+
seen.add(current)
|
|
329
|
+
chain.append(current)
|
|
330
|
+
current = parents.get(current)
|
|
331
|
+
depth = depths.get(current, -1) + 1
|
|
332
|
+
for item in reversed(chain):
|
|
333
|
+
depths[item] = depth
|
|
334
|
+
depth += 1
|
|
335
|
+
return depths[node_id]
|
|
336
|
+
|
|
337
|
+
for candidate in candidates:
|
|
338
|
+
depth_for(candidate.node_id)
|
|
339
|
+
return parents, depths
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def build_source_structure(
|
|
343
|
+
documents: Iterable[SourceStructureDocument],
|
|
344
|
+
*,
|
|
345
|
+
generation: int = 0,
|
|
346
|
+
workspace_key: str = "",
|
|
347
|
+
max_symbols: int = MAX_OUTLINE_SYMBOLS,
|
|
348
|
+
max_depth: int = DEFAULT_OUTLINE_DEPTH,
|
|
349
|
+
max_name_chars: int = DEFAULT_OUTLINE_NAME_CHARS,
|
|
350
|
+
max_text_chars: int = DEFAULT_OUTLINE_TEXT_CHARS,
|
|
351
|
+
cancelled: Callable[[], bool] | None = None,
|
|
352
|
+
) -> SourceStructureResult | None:
|
|
353
|
+
"""Build an inert structure projection from captured open text only."""
|
|
354
|
+
|
|
355
|
+
started = monotonic()
|
|
356
|
+
records = _captured_documents(documents)
|
|
357
|
+
if isinstance(generation, bool) or not isinstance(generation, int):
|
|
358
|
+
raise TypeError("structure generation must be an integer")
|
|
359
|
+
if generation < 0:
|
|
360
|
+
raise ValueError("structure generation must be non-negative")
|
|
361
|
+
if not isinstance(workspace_key, str):
|
|
362
|
+
raise TypeError("workspace key must be a string")
|
|
363
|
+
if len(workspace_key) > MAX_STRUCTURE_WORKSPACE_KEY_CHARS:
|
|
364
|
+
raise ValueError("workspace key exceeds the observer identity limit")
|
|
365
|
+
max_symbols = _bounded_int(
|
|
366
|
+
max_symbols, name="max_symbols", maximum=MAX_OUTLINE_SYMBOLS
|
|
367
|
+
)
|
|
368
|
+
max_depth = _bounded_int(
|
|
369
|
+
max_depth, name="max_depth", maximum=MAX_OUTLINE_DEPTH
|
|
370
|
+
)
|
|
371
|
+
max_name_chars = _bounded_int(
|
|
372
|
+
max_name_chars,
|
|
373
|
+
name="max_name_chars",
|
|
374
|
+
maximum=MAX_OUTLINE_NAME_CHARS,
|
|
375
|
+
)
|
|
376
|
+
max_text_chars = _bounded_int(
|
|
377
|
+
max_text_chars,
|
|
378
|
+
name="max_text_chars",
|
|
379
|
+
maximum=MAX_OUTLINE_TEXT_CHARS,
|
|
380
|
+
)
|
|
381
|
+
is_cancelled = cancelled or (lambda: False)
|
|
382
|
+
symbols: list[OutlineSymbol] = []
|
|
383
|
+
invalid: list[str] = []
|
|
384
|
+
observer_failed: list[str] = []
|
|
385
|
+
total_symbols = 0
|
|
386
|
+
text_chars = 0
|
|
387
|
+
truncated = False
|
|
388
|
+
output_full = False
|
|
389
|
+
|
|
390
|
+
for document in records:
|
|
391
|
+
if is_cancelled():
|
|
392
|
+
return None
|
|
393
|
+
try:
|
|
394
|
+
candidates = _document_candidates(document)
|
|
395
|
+
except (
|
|
396
|
+
SyntaxError,
|
|
397
|
+
UnicodeError,
|
|
398
|
+
ValueError,
|
|
399
|
+
RecursionError,
|
|
400
|
+
):
|
|
401
|
+
invalid.append(document.document_id)
|
|
402
|
+
continue
|
|
403
|
+
except Exception:
|
|
404
|
+
observer_failed.append(document.document_id)
|
|
405
|
+
continue
|
|
406
|
+
if is_cancelled():
|
|
407
|
+
return None
|
|
408
|
+
parents, depths = _nearest_symbol_parents(candidates)
|
|
409
|
+
ordered = sorted(
|
|
410
|
+
candidates,
|
|
411
|
+
key=lambda item: (
|
|
412
|
+
item.start,
|
|
413
|
+
depths[item.node_id],
|
|
414
|
+
item.end,
|
|
415
|
+
item.kind,
|
|
416
|
+
item.node_id,
|
|
417
|
+
),
|
|
418
|
+
)
|
|
419
|
+
total_symbols += len(ordered)
|
|
420
|
+
stored_ids = {symbol.node_id for symbol in symbols}
|
|
421
|
+
for candidate in ordered:
|
|
422
|
+
depth = depths[candidate.node_id]
|
|
423
|
+
if depth > max_depth:
|
|
424
|
+
truncated = True
|
|
425
|
+
continue
|
|
426
|
+
name, name_truncated = _clipped_name(
|
|
427
|
+
candidate.name, max_name_chars
|
|
428
|
+
)
|
|
429
|
+
truncated = truncated or name_truncated
|
|
430
|
+
detail = {
|
|
431
|
+
"Module": "module",
|
|
432
|
+
"ClassDef": "class",
|
|
433
|
+
"FunctionDef": "function",
|
|
434
|
+
"AsyncFunctionDef": "async function",
|
|
435
|
+
}[candidate.kind]
|
|
436
|
+
parent_id = parents[candidate.node_id]
|
|
437
|
+
if parent_id is not None and parent_id not in stored_ids:
|
|
438
|
+
truncated = True
|
|
439
|
+
continue
|
|
440
|
+
required_text = len(name) + len(detail)
|
|
441
|
+
if (
|
|
442
|
+
output_full
|
|
443
|
+
or len(symbols) >= max_symbols
|
|
444
|
+
or text_chars + required_text > max_text_chars
|
|
445
|
+
):
|
|
446
|
+
output_full = True
|
|
447
|
+
truncated = True
|
|
448
|
+
continue
|
|
449
|
+
symbols.append(
|
|
450
|
+
OutlineSymbol(
|
|
451
|
+
document.document_id,
|
|
452
|
+
document.module_id,
|
|
453
|
+
document.logical_name,
|
|
454
|
+
candidate.node_id,
|
|
455
|
+
parent_id,
|
|
456
|
+
candidate.kind,
|
|
457
|
+
name,
|
|
458
|
+
detail,
|
|
459
|
+
depth,
|
|
460
|
+
candidate.start,
|
|
461
|
+
candidate.end,
|
|
462
|
+
candidate.start_line,
|
|
463
|
+
candidate.start_column,
|
|
464
|
+
candidate.end_line,
|
|
465
|
+
candidate.end_column,
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
stored_ids.add(candidate.node_id)
|
|
469
|
+
text_chars += required_text
|
|
470
|
+
|
|
471
|
+
return SourceStructureResult(
|
|
472
|
+
generation,
|
|
473
|
+
workspace_key,
|
|
474
|
+
tuple(symbols),
|
|
475
|
+
total_symbols,
|
|
476
|
+
tuple(invalid),
|
|
477
|
+
tuple(observer_failed),
|
|
478
|
+
truncated,
|
|
479
|
+
monotonic() - started,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def breadcrumbs_for_position(
|
|
484
|
+
result: SourceStructureResult,
|
|
485
|
+
document_id: str,
|
|
486
|
+
position: int,
|
|
487
|
+
) -> tuple[OutlineSymbol, ...]:
|
|
488
|
+
"""Return the root-to-leaf symbol chain containing a code-point offset."""
|
|
489
|
+
|
|
490
|
+
if not isinstance(result, SourceStructureResult):
|
|
491
|
+
raise TypeError("result must be a SourceStructureResult")
|
|
492
|
+
if not isinstance(document_id, str) or not document_id:
|
|
493
|
+
raise ValueError("document ID must be non-empty text")
|
|
494
|
+
if isinstance(position, bool) or not isinstance(position, int):
|
|
495
|
+
raise TypeError("breadcrumb position must be an integer")
|
|
496
|
+
if position < 0:
|
|
497
|
+
raise ValueError("breadcrumb position must be non-negative")
|
|
498
|
+
|
|
499
|
+
by_id = {
|
|
500
|
+
symbol.node_id: symbol
|
|
501
|
+
for symbol in result.symbols
|
|
502
|
+
if symbol.document_id == document_id
|
|
503
|
+
}
|
|
504
|
+
containing = [
|
|
505
|
+
symbol
|
|
506
|
+
for symbol in by_id.values()
|
|
507
|
+
if (
|
|
508
|
+
symbol.start <= position < max(symbol.start + 1, symbol.end)
|
|
509
|
+
or (
|
|
510
|
+
symbol.kind == "Module"
|
|
511
|
+
and position == symbol.end
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
]
|
|
515
|
+
if not containing:
|
|
516
|
+
return ()
|
|
517
|
+
current = max(
|
|
518
|
+
containing,
|
|
519
|
+
key=lambda item: (
|
|
520
|
+
item.depth,
|
|
521
|
+
item.start,
|
|
522
|
+
-(item.end - item.start),
|
|
523
|
+
),
|
|
524
|
+
)
|
|
525
|
+
chain: list[OutlineSymbol] = []
|
|
526
|
+
seen: set[str] = set()
|
|
527
|
+
while current.node_id not in seen:
|
|
528
|
+
seen.add(current.node_id)
|
|
529
|
+
chain.append(current)
|
|
530
|
+
if current.parent_node_id is None:
|
|
531
|
+
break
|
|
532
|
+
parent = by_id.get(current.parent_node_id)
|
|
533
|
+
if parent is None:
|
|
534
|
+
break
|
|
535
|
+
current = parent
|
|
536
|
+
return tuple(reversed(chain))
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def __getattr__(name: str):
|
|
540
|
+
if name == "AsyncSourceStructureService":
|
|
541
|
+
from .source_structure_async import AsyncSourceStructureService
|
|
542
|
+
|
|
543
|
+
return AsyncSourceStructureService
|
|
544
|
+
raise AttributeError(name)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
__all__ = [
|
|
548
|
+
"AsyncSourceStructureService",
|
|
549
|
+
"MAX_OUTLINE_DEPTH",
|
|
550
|
+
"MAX_OUTLINE_NAME_CHARS",
|
|
551
|
+
"MAX_OUTLINE_SYMBOLS",
|
|
552
|
+
"MAX_OUTLINE_TEXT_CHARS",
|
|
553
|
+
"OutlineSymbol",
|
|
554
|
+
"SourceStructureDocument",
|
|
555
|
+
"SourceStructureResult",
|
|
556
|
+
"breadcrumbs_for_position",
|
|
557
|
+
"build_source_structure",
|
|
558
|
+
]
|