opencrbasic 1.0.0__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.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Indentation formatter for editor tooling (LSP-07b), kept free of any protocol library.
|
|
2
|
+
|
|
3
|
+
`format_indentation` returns edits that replace the *leading whitespace* of physical lines and
|
|
4
|
+
nothing else: no text after the first non-blank character changes, the line count and the line
|
|
5
|
+
endings stay, and no physical line is made longer than the loader's 510-character buffer allows.
|
|
6
|
+
The language server converts a `TextEdit` to an LSP edit like a `editing.Span`. Rules, refusals and
|
|
7
|
+
the preservation evidence: `docs/lsp-api.md` "Formatting (LSP-07b)".
|
|
8
|
+
|
|
9
|
+
Block words are not listed here: the structure comes from `editing` (`_roles`, `_classify`,
|
|
10
|
+
`_block_events`), the same classification folding and the outline use.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import bisect
|
|
14
|
+
import re
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import NamedTuple
|
|
18
|
+
|
|
19
|
+
from . import lexer, source
|
|
20
|
+
from .editing import _block_events, _classify, _roles, _tolerant_tokens
|
|
21
|
+
from .parser import _DIRECTIVE_RE, _split_text_segments
|
|
22
|
+
|
|
23
|
+
__all__ = ["TextEdit", "apply_edits", "format_indentation", "format_range", "format_refusal"]
|
|
24
|
+
|
|
25
|
+
_UNITS_RE = re.compile(r"\s*Units\b", re.I)
|
|
26
|
+
_MID_WORDS = frozenset({"CASE", "ELSE", "ELSEIF"})
|
|
27
|
+
_CASE = "CASE" # a virtual frame: the body of one `Case` of a `Select Case`
|
|
28
|
+
_IMPLICIT_END = "SLOWSEQUENCE" # editing._IMPLICIT_END: ends at the next one or at EndProg
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class TextEdit:
|
|
33
|
+
"""Replace `text[line][start:end]` by `new_text`. `line` is 0-based; columns are decoded
|
|
34
|
+
(Latin-1) columns, i.e. byte offsets into the physical line, before LSP position conversion.
|
|
35
|
+
Every edit of this module has `start == 0`."""
|
|
36
|
+
|
|
37
|
+
line: int
|
|
38
|
+
start: int
|
|
39
|
+
end: int
|
|
40
|
+
new_text: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def apply_edits(text: str, edits: list[TextEdit]) -> str:
|
|
44
|
+
"""`text` with `edits` applied (edits must not overlap; any order)."""
|
|
45
|
+
lines = text.split("\n")
|
|
46
|
+
for e in edits:
|
|
47
|
+
lines[e.line] = lines[e.line][: e.start] + e.new_text + lines[e.line][e.end :]
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def format_refusal(result) -> str | None:
|
|
52
|
+
"""Why formatting must not be offered for the file behind an `AnalysisResult` (or
|
|
53
|
+
`CompileResult`): it has errors. None: formatting may be offered. Formatting itself works on
|
|
54
|
+
text alone and never reads a result; this is the policy gate for the server."""
|
|
55
|
+
if getattr(result, "implementation_failures", None) or getattr(result, "implementation_exceptions", None):
|
|
56
|
+
return "the analysis failed internally"
|
|
57
|
+
if not result.ok:
|
|
58
|
+
return "the program has errors; fix them before formatting"
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------ the plan
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _Frame(NamedTuple):
|
|
66
|
+
family: str
|
|
67
|
+
last: int # last physical line of the opener statement (0-based)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _Dir(NamedTuple):
|
|
71
|
+
base: list # block stack at the `#If`
|
|
72
|
+
ends: list # block stack at the end of each earlier branch
|
|
73
|
+
first: int
|
|
74
|
+
last: int
|
|
75
|
+
has_else: bool
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _Group(NamedTuple):
|
|
79
|
+
first: int
|
|
80
|
+
last: int
|
|
81
|
+
level: int
|
|
82
|
+
enter: int # block depth before the statement
|
|
83
|
+
directive: bool
|
|
84
|
+
skip: bool # edits suppressed (the statement is left alone)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load(text: str) -> tuple[list[source.LogicalLine], int | None]:
|
|
88
|
+
"""The loader's logical lines (includes unresolved) up to the first overflow line, and that
|
|
89
|
+
line (0-based; the loader, and the compiler, stop reading there)."""
|
|
90
|
+
lines, _ = source.load_text(text, "t", directory=None)
|
|
91
|
+
out = []
|
|
92
|
+
for ll in lines:
|
|
93
|
+
if ll.phys is None:
|
|
94
|
+
continue
|
|
95
|
+
if ll.kind == "overflow":
|
|
96
|
+
return out, ll.phys[0] - 1
|
|
97
|
+
if ll.kind in ("code", "include_warning", "include_error"):
|
|
98
|
+
out.append(ll)
|
|
99
|
+
return out, None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _phys(ll: source.LogicalLine) -> tuple[int, int]:
|
|
103
|
+
"""The physical span of a logical line, which `source.load_text` sets on every line it makes."""
|
|
104
|
+
assert ll.phys is not None
|
|
105
|
+
return ll.phys
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _inside_string(ll: source.LogicalLine) -> bool:
|
|
109
|
+
"""True when the text cannot be read as tokens, a continuation line starts inside a string, or
|
|
110
|
+
a continued line holds a `Units` segment (its label is verbatim text, blanks included): changing
|
|
111
|
+
the leading blanks of a continuation line would change the program."""
|
|
112
|
+
if _phys(ll)[1] > _phys(ll)[0] and any(_UNITS_RE.match(seg) for seg in _split_text_segments(ll.text)):
|
|
113
|
+
return True
|
|
114
|
+
try:
|
|
115
|
+
tokens = lexer.tokenize(ll.text)
|
|
116
|
+
except lexer.LexError as e:
|
|
117
|
+
if e.message == "unterminated string" or _phys(ll)[1] > _phys(ll)[0]:
|
|
118
|
+
return True
|
|
119
|
+
return False
|
|
120
|
+
strings = [(t.col, t.col + len(t.text)) for t in tokens if t.kind == "STRING"]
|
|
121
|
+
for piece in ll.pieces:
|
|
122
|
+
if piece.line > _phys(ll)[0] and piece.col == 0:
|
|
123
|
+
if any(a < piece.start < b for a, b in strings):
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _plan(text: str, profile: str) -> tuple[dict[int, int], int]:
|
|
129
|
+
"""`({0-based physical line: indent level}, exclusive line limit)`: the level every editable
|
|
130
|
+
line should have, and the first line from which on nothing is edited."""
|
|
131
|
+
roles = _roles(profile)
|
|
132
|
+
lines = [ln.removesuffix("\r") for ln in text.split("\n")]
|
|
133
|
+
logical, overflow = _load(text)
|
|
134
|
+
events: dict[int, list[tuple[str, str]]] = defaultdict(list)
|
|
135
|
+
for role, family, _name, first, _last in _block_events(text, profile):
|
|
136
|
+
if family != "#IF":
|
|
137
|
+
events[first].append((role, family))
|
|
138
|
+
|
|
139
|
+
limit = len(lines) if overflow is None else overflow
|
|
140
|
+
stack: list[_Frame] = []
|
|
141
|
+
dirs: list[_Dir] = []
|
|
142
|
+
groups: list[_Group] = []
|
|
143
|
+
|
|
144
|
+
def drop(k: int) -> None:
|
|
145
|
+
nonlocal limit
|
|
146
|
+
for f in stack[k + 1 :]:
|
|
147
|
+
if f.family not in (_IMPLICIT_END, _CASE):
|
|
148
|
+
limit = min(limit, f.last + 1) # never closed: nothing after it is edited
|
|
149
|
+
del stack[k:]
|
|
150
|
+
|
|
151
|
+
def close(family: str, first: int) -> None:
|
|
152
|
+
nonlocal limit
|
|
153
|
+
for k in range(len(stack) - 1, -1, -1):
|
|
154
|
+
if stack[k].family == family:
|
|
155
|
+
drop(k)
|
|
156
|
+
return
|
|
157
|
+
limit = min(limit, first) # a closer with no opener
|
|
158
|
+
|
|
159
|
+
halted = False
|
|
160
|
+
for ll in logical:
|
|
161
|
+
first, last = _phys(ll)[0] - 1, _phys(ll)[1] - 1
|
|
162
|
+
if halted or first >= limit:
|
|
163
|
+
break
|
|
164
|
+
enter = len(stack)
|
|
165
|
+
d = _DIRECTIVE_RE.match(ll.text) if ll.kind == "code" else None
|
|
166
|
+
if d:
|
|
167
|
+
kind = d.group(1).upper()
|
|
168
|
+
if kind in ("IF", "IF_NO_REMOVE"):
|
|
169
|
+
dirs.append(_Dir(list(stack), [], first, last, False))
|
|
170
|
+
elif kind in ("ELSE", "ELSEIF") and dirs:
|
|
171
|
+
top = dirs.pop()
|
|
172
|
+
top.ends.append(list(stack))
|
|
173
|
+
stack[:] = top.base
|
|
174
|
+
dirs.append(top._replace(has_else=top.has_else or kind == "ELSE"))
|
|
175
|
+
elif kind == "ENDIF" and dirs:
|
|
176
|
+
top = dirs.pop()
|
|
177
|
+
ends = top.ends + [list(stack)]
|
|
178
|
+
if not top.has_else:
|
|
179
|
+
ends.append(top.base)
|
|
180
|
+
if all([f.family for f in e] == [f.family for f in ends[0]] for e in ends):
|
|
181
|
+
stack[:] = ends[-1]
|
|
182
|
+
else:
|
|
183
|
+
limit = min(limit, last + 1) # branches leave different blocks open
|
|
184
|
+
groups.append(_Group(first, last, 0, enter, True, False))
|
|
185
|
+
continue
|
|
186
|
+
evs = [e for e in events.get(first, [])]
|
|
187
|
+
segs = _split_text_segments(ll.text) if ll.kind == "code" else []
|
|
188
|
+
toks = [t for t in _tolerant_tokens(segs[0]) if t.kind != "EOF"] if segs else []
|
|
189
|
+
word = toks[0].upper if toks and toks[0].kind == "IDENT" else ""
|
|
190
|
+
second = toks[1].upper if len(toks) > 1 and toks[1].kind == "IDENT" else None
|
|
191
|
+
lead = _classify(word, second, roles) if word else None
|
|
192
|
+
if word == "CASE":
|
|
193
|
+
if stack and stack[-1].family == _CASE:
|
|
194
|
+
stack.pop()
|
|
195
|
+
level = len(stack)
|
|
196
|
+
if stack and stack[-1].family == "SELECT":
|
|
197
|
+
stack.append(_Frame(_CASE, last))
|
|
198
|
+
elif word in ("ELSE", "ELSEIF"):
|
|
199
|
+
level = len(stack) - 1 if stack and stack[-1].family == "IF" else len(stack)
|
|
200
|
+
else:
|
|
201
|
+
if lead and lead[0] == "close" and evs and evs[0] == lead:
|
|
202
|
+
close(lead[1], first)
|
|
203
|
+
evs = evs[1:]
|
|
204
|
+
elif lead == ("open", _IMPLICIT_END):
|
|
205
|
+
for k in range(len(stack) - 1, -1, -1):
|
|
206
|
+
if stack[k].family == _IMPLICIT_END:
|
|
207
|
+
drop(k)
|
|
208
|
+
break
|
|
209
|
+
level = len(stack)
|
|
210
|
+
for role, family in evs:
|
|
211
|
+
if role == "open":
|
|
212
|
+
if family == _IMPLICIT_END:
|
|
213
|
+
for k in range(len(stack) - 1, -1, -1):
|
|
214
|
+
if stack[k].family == _IMPLICIT_END:
|
|
215
|
+
drop(k)
|
|
216
|
+
break
|
|
217
|
+
stack.append(_Frame(family, last))
|
|
218
|
+
else:
|
|
219
|
+
close(family, first)
|
|
220
|
+
if ll.kind == "code" and not dirs and not stack and lead == ("close", "BEGINPROG"):
|
|
221
|
+
halted = True # the compiler stops reading at EndProg
|
|
222
|
+
limit = min(limit, last + 1)
|
|
223
|
+
groups.append(_Group(first, last, level, enter, False, _inside_string(ll) if ll.kind == "code" else False))
|
|
224
|
+
for f in stack:
|
|
225
|
+
if f.family not in (_IMPLICIT_END, _CASE):
|
|
226
|
+
limit = min(limit, f.last + 1) # still open at the end of the text
|
|
227
|
+
for dr in dirs:
|
|
228
|
+
limit = min(limit, dr.last + 1)
|
|
229
|
+
|
|
230
|
+
levels: dict[int, int] = {}
|
|
231
|
+
for g in groups:
|
|
232
|
+
if g.skip:
|
|
233
|
+
continue
|
|
234
|
+
levels[g.first] = g.level
|
|
235
|
+
for k in range(g.first + 1, g.last + 1):
|
|
236
|
+
levels[k] = g.level + 1 # continuation lines (and pulled lines)
|
|
237
|
+
levels = {n: v for n, v in levels.items() if n < limit}
|
|
238
|
+
covered = {k for g in groups for k in range(g.first, g.last + 1)}
|
|
239
|
+
starts = [g.first for g in groups]
|
|
240
|
+
for n, line in enumerate(lines):
|
|
241
|
+
if n in covered or not line.lstrip(" \t").startswith("'") or n >= limit:
|
|
242
|
+
continue
|
|
243
|
+
j = bisect.bisect_right(starts, n)
|
|
244
|
+
if j >= len(groups):
|
|
245
|
+
levels[n] = len(stack) if not halted else 0
|
|
246
|
+
continue
|
|
247
|
+
g = groups[j]
|
|
248
|
+
levels[n] = g.enter if g.directive else g.level
|
|
249
|
+
return levels, limit
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _leading(line: str) -> int:
|
|
253
|
+
return len(line) - len(line.lstrip(" \t"))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def format_indentation(text: str, profile: str = "cr6", indent: str = " ") -> list[TextEdit]:
|
|
257
|
+
"""Edits that set the leading whitespace of the lines of `text` (decoded Latin-1) to
|
|
258
|
+
`indent * level`. See the module docstring and `docs/lsp-api.md` for the rules."""
|
|
259
|
+
return _format(text, profile, indent, 0, None)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def format_range(text: str, start_line: int, end_line: int, profile: str = "cr6", indent: str = " ") -> list[TextEdit]:
|
|
263
|
+
"""`format_indentation` restricted to the 0-based physical lines `start_line..end_line`
|
|
264
|
+
(inclusive). Levels are computed from the whole text, so the range needs no context."""
|
|
265
|
+
return _format(text, profile, indent, start_line, end_line)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _format(text: str, profile: str, indent: str, lo: int, hi: int | None) -> list[TextEdit]:
|
|
269
|
+
if indent.strip(" \t") or not indent:
|
|
270
|
+
raise ValueError("indent must be a non-empty string of spaces and tabs")
|
|
271
|
+
levels, _limit = _plan(text, profile)
|
|
272
|
+
lines = text.split("\n")
|
|
273
|
+
edits: dict[int, TextEdit] = {}
|
|
274
|
+
for n, level in levels.items():
|
|
275
|
+
if n < lo or (hi is not None and n > hi):
|
|
276
|
+
continue
|
|
277
|
+
body = lines[n].removesuffix("\r")
|
|
278
|
+
if not body.strip(" \t"):
|
|
279
|
+
continue # blank lines keep their (trailing) blanks
|
|
280
|
+
width = _leading(body)
|
|
281
|
+
want = indent * level
|
|
282
|
+
if body[:width] != want:
|
|
283
|
+
edits[n] = TextEdit(n, 0, width, want)
|
|
284
|
+
return _within_buffer(text, lines, edits)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _first_overflow(text: str) -> tuple[int, int] | None:
|
|
288
|
+
"""Physical lines (first, last; 0-based) of the first overflow of `text`, None: no overflow."""
|
|
289
|
+
loaded, _ = source.load_text(text, "t", directory=None)
|
|
290
|
+
for ll in loaded:
|
|
291
|
+
if ll.kind == "overflow":
|
|
292
|
+
return _phys(ll)[0] - 1, _phys(ll)[1] - 1
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _within_buffer(text: str, lines: list[str], edits: dict[int, TextEdit]) -> list[TextEdit]:
|
|
297
|
+
"""Drop the edits of any statement that a new indentation would push over the loader's
|
|
298
|
+
510-character buffer (a statement's lines are counted together when continued)."""
|
|
299
|
+
base = _first_overflow(text)
|
|
300
|
+
while edits:
|
|
301
|
+
new = apply_edits(text, list(edits.values()))
|
|
302
|
+
found = _first_overflow(new)
|
|
303
|
+
if found is None or found == base:
|
|
304
|
+
break
|
|
305
|
+
hit = [n for n in edits if found[0] <= n <= found[1]]
|
|
306
|
+
if not hit:
|
|
307
|
+
break
|
|
308
|
+
for n in hit:
|
|
309
|
+
del edits[n]
|
|
310
|
+
return [edits[n] for n in sorted(edits)]
|