openscad-parser 2.5.2__tar.gz → 2.6.1__tar.gz
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.
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/PKG-INFO +1 -1
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/pyproject.toml +1 -1
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/__init__.py +257 -55
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/builder.py +11 -12
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/source_map.py +8 -4
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/README.rst +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/__init__.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/nodes.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/pretty_print.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/scope.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/ast/serialization.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/cli.py +0 -0
- {openscad_parser-2.5.2 → openscad_parser-2.6.1}/src/openscad_parser/grammar.py +0 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
1
3
|
import os
|
|
4
|
+
import pickle
|
|
2
5
|
import platform
|
|
3
6
|
from typing import Optional
|
|
4
7
|
from arpeggio import NoMatch
|
|
@@ -253,22 +256,240 @@ def getASTfromString(code: str, include_comments: bool = False, origin: str = "<
|
|
|
253
256
|
return ast
|
|
254
257
|
|
|
255
258
|
|
|
256
|
-
# Module-level cache for AST trees
|
|
259
|
+
# Module-level in-memory cache for per-file AST trees (no includes resolved)
|
|
260
|
+
# Key: tuple of (absolute file path (str), include_comments (bool))
|
|
261
|
+
# Value: tuple of (AST nodes, modification timestamp)
|
|
262
|
+
_ast_cache: dict[tuple[str, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
263
|
+
|
|
264
|
+
# Resolved (includes-expanded) cache
|
|
257
265
|
# Key: tuple of (absolute file path (str), include_comments (bool), process_includes (bool))
|
|
258
266
|
# Value: tuple of (AST nodes, modification timestamp)
|
|
259
|
-
|
|
267
|
+
_resolved_cache: dict[tuple[str, bool, bool], tuple[list[ASTNode] | None, float]] = {}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _get_disk_cache_dir() -> Optional[str]:
|
|
271
|
+
"""Get the disk cache directory, creating it if needed."""
|
|
272
|
+
cache_dir = os.environ.get('OPENSCAD_PARSER_CACHE_DIR')
|
|
273
|
+
if not cache_dir:
|
|
274
|
+
home = os.path.expanduser('~')
|
|
275
|
+
if platform.system() == 'Darwin':
|
|
276
|
+
cache_dir = os.path.join(home, 'Library', 'Caches', 'openscad_parser')
|
|
277
|
+
elif platform.system() == 'Windows': # pragma: no cover
|
|
278
|
+
cache_dir = os.path.join(os.environ.get('LOCALAPPDATA', home), 'openscad_parser', 'cache')
|
|
279
|
+
else:
|
|
280
|
+
cache_dir = os.path.join(home, '.cache', 'openscad_parser')
|
|
281
|
+
try:
|
|
282
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
283
|
+
return cache_dir
|
|
284
|
+
except OSError: # pragma: no cover
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _disk_cache_path(file_path: str, include_comments: bool) -> Optional[str]:
|
|
289
|
+
"""Get the disk cache file path for a given source file."""
|
|
290
|
+
cache_dir = _get_disk_cache_dir()
|
|
291
|
+
if not cache_dir:
|
|
292
|
+
return None # pragma: no cover
|
|
293
|
+
key = f"{file_path}:{include_comments}"
|
|
294
|
+
h = hashlib.sha256(key.encode()).hexdigest()[:16]
|
|
295
|
+
return os.path.join(cache_dir, f"{h}.pickle")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _load_from_disk_cache(file_path: str, include_comments: bool, current_mtime: float) -> Optional[list[ASTNode]]:
|
|
299
|
+
"""Try to load a file's AST from disk cache."""
|
|
300
|
+
cache_path = _disk_cache_path(file_path, include_comments)
|
|
301
|
+
if not cache_path or not os.path.exists(cache_path):
|
|
302
|
+
return None
|
|
303
|
+
try:
|
|
304
|
+
with open(cache_path, 'rb') as f:
|
|
305
|
+
cached_mtime, ast = pickle.load(f)
|
|
306
|
+
if cached_mtime == current_mtime:
|
|
307
|
+
return ast
|
|
308
|
+
except (OSError, pickle.UnpicklingError, ValueError, EOFError):
|
|
309
|
+
pass
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _save_to_disk_cache(file_path: str, include_comments: bool, mtime: float, ast: list[ASTNode] | None):
|
|
314
|
+
"""Save a file's AST to disk cache and update the manifest."""
|
|
315
|
+
cache_path = _disk_cache_path(file_path, include_comments)
|
|
316
|
+
if not cache_path:
|
|
317
|
+
return # pragma: no cover
|
|
318
|
+
try:
|
|
319
|
+
with open(cache_path, 'wb') as f:
|
|
320
|
+
pickle.dump((mtime, ast), f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
321
|
+
except OSError: # pragma: no cover
|
|
322
|
+
return
|
|
323
|
+
cache_fname = os.path.basename(cache_path)
|
|
324
|
+
_manifest_update(cache_fname, file_path)
|
|
325
|
+
_evict_stale_cache()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _manifest_path() -> Optional[str]:
|
|
329
|
+
"""Get the path to the cache manifest file."""
|
|
330
|
+
cache_dir = _get_disk_cache_dir()
|
|
331
|
+
if not cache_dir:
|
|
332
|
+
return None # pragma: no cover
|
|
333
|
+
return os.path.join(cache_dir, "manifest.json")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _manifest_load() -> dict[str, str]:
|
|
337
|
+
"""Load the manifest: {cache_filename: source_file_path}."""
|
|
338
|
+
path = _manifest_path()
|
|
339
|
+
if not path or not os.path.exists(path):
|
|
340
|
+
return {}
|
|
341
|
+
try:
|
|
342
|
+
with open(path, 'r') as f:
|
|
343
|
+
return json.load(f)
|
|
344
|
+
except (OSError, json.JSONDecodeError):
|
|
345
|
+
return {}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _manifest_save(manifest: dict[str, str]):
|
|
349
|
+
"""Save the manifest to disk."""
|
|
350
|
+
path = _manifest_path()
|
|
351
|
+
if not path:
|
|
352
|
+
return # pragma: no cover
|
|
353
|
+
try:
|
|
354
|
+
with open(path, 'w') as f:
|
|
355
|
+
json.dump(manifest, f)
|
|
356
|
+
except OSError: # pragma: no cover
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _manifest_update(cache_fname: str, source_path: str):
|
|
361
|
+
"""Add or update an entry in the manifest."""
|
|
362
|
+
manifest = _manifest_load()
|
|
363
|
+
manifest[cache_fname] = source_path
|
|
364
|
+
_manifest_save(manifest)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _evict_stale_cache():
|
|
368
|
+
"""Remove cache entries whose source files no longer exist."""
|
|
369
|
+
cache_dir = _get_disk_cache_dir()
|
|
370
|
+
if not cache_dir:
|
|
371
|
+
return # pragma: no cover
|
|
372
|
+
manifest = _manifest_load()
|
|
373
|
+
if not manifest:
|
|
374
|
+
return
|
|
375
|
+
stale_keys = [
|
|
376
|
+
fname for fname, source_path in manifest.items()
|
|
377
|
+
if not os.path.exists(source_path)
|
|
378
|
+
]
|
|
379
|
+
if not stale_keys:
|
|
380
|
+
return
|
|
381
|
+
for fname in stale_keys:
|
|
382
|
+
cache_file = os.path.join(cache_dir, fname)
|
|
383
|
+
try:
|
|
384
|
+
os.remove(cache_file)
|
|
385
|
+
except OSError:
|
|
386
|
+
pass
|
|
387
|
+
del manifest[fname]
|
|
388
|
+
_manifest_save(manifest)
|
|
260
389
|
|
|
261
390
|
|
|
262
391
|
def clear_ast_cache():
|
|
263
392
|
"""Clear the in-memory AST cache.
|
|
264
|
-
|
|
393
|
+
|
|
265
394
|
This function removes all cached AST trees, forcing all subsequent
|
|
266
395
|
calls to getASTfromFile() to re-parse files.
|
|
267
|
-
|
|
396
|
+
|
|
268
397
|
Example:
|
|
269
398
|
clear_ast_cache() # Clear all cached ASTs
|
|
270
399
|
"""
|
|
271
400
|
_ast_cache.clear()
|
|
401
|
+
_resolved_cache.clear()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def clear_disk_cache():
|
|
405
|
+
"""Clear the on-disk AST cache.
|
|
406
|
+
|
|
407
|
+
This function removes all cached AST files from disk, forcing all subsequent
|
|
408
|
+
calls to re-parse files from scratch.
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
clear_disk_cache() # Remove all disk-cached ASTs
|
|
412
|
+
"""
|
|
413
|
+
cache_dir = _get_disk_cache_dir()
|
|
414
|
+
if cache_dir and os.path.isdir(cache_dir):
|
|
415
|
+
for fname in os.listdir(cache_dir):
|
|
416
|
+
if fname.endswith('.pickle') or fname == 'manifest.json':
|
|
417
|
+
try:
|
|
418
|
+
os.remove(os.path.join(cache_dir, fname))
|
|
419
|
+
except OSError: # pragma: no cover
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _parse_single_file(file_path: str, include_comments: bool = False) -> list[ASTNode] | None:
|
|
424
|
+
"""Parse a single file without resolving includes. Uses memory and disk cache.
|
|
425
|
+
|
|
426
|
+
Returns the AST with IncludeStatement nodes intact (not expanded).
|
|
427
|
+
"""
|
|
428
|
+
if not os.path.exists(file_path):
|
|
429
|
+
raise FileNotFoundError(f"File {file_path} not found")
|
|
430
|
+
|
|
431
|
+
current_mtime = os.path.getmtime(file_path)
|
|
432
|
+
cache_key = (file_path, include_comments)
|
|
433
|
+
|
|
434
|
+
# Check in-memory cache
|
|
435
|
+
if cache_key in _ast_cache:
|
|
436
|
+
cached_ast, cached_mtime = _ast_cache[cache_key]
|
|
437
|
+
if cached_mtime == current_mtime:
|
|
438
|
+
return cached_ast
|
|
439
|
+
|
|
440
|
+
# Check disk cache
|
|
441
|
+
disk_result = _load_from_disk_cache(file_path, include_comments, current_mtime)
|
|
442
|
+
if disk_result is not None:
|
|
443
|
+
_ast_cache[cache_key] = (disk_result, current_mtime)
|
|
444
|
+
return disk_result
|
|
445
|
+
|
|
446
|
+
# Parse the file
|
|
447
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
448
|
+
code = f.read()
|
|
449
|
+
|
|
450
|
+
source_map = SourceMap()
|
|
451
|
+
source_map.add_origin(file_path, code)
|
|
452
|
+
|
|
453
|
+
parser = getOpenSCADParser(reduce_tree=False, include_comments=include_comments)
|
|
454
|
+
ast = parse_ast(parser, code, source_map=source_map)
|
|
455
|
+
|
|
456
|
+
# Cache in memory and on disk
|
|
457
|
+
_ast_cache[cache_key] = (ast, current_mtime)
|
|
458
|
+
_save_to_disk_cache(file_path, include_comments, current_mtime, ast)
|
|
459
|
+
|
|
460
|
+
return ast
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _resolve_includes(ast_nodes: list[ASTNode] | None, current_file: str,
|
|
464
|
+
include_comments: bool = False,
|
|
465
|
+
visited: set | None = None) -> list[ASTNode] | None:
|
|
466
|
+
"""Resolve IncludeStatement nodes by parsing and inlining referenced files."""
|
|
467
|
+
if ast_nodes is None:
|
|
468
|
+
return None
|
|
469
|
+
if visited is None:
|
|
470
|
+
visited = set()
|
|
471
|
+
|
|
472
|
+
result = []
|
|
473
|
+
for node in ast_nodes:
|
|
474
|
+
if isinstance(node, IncludeStatement):
|
|
475
|
+
filename = node.filepath.val
|
|
476
|
+
lib_file = findLibraryFile(current_file, filename)
|
|
477
|
+
if lib_file is None:
|
|
478
|
+
raise FileNotFoundError(
|
|
479
|
+
f"Included file '{filename}' not found. "
|
|
480
|
+
f"Searched relative to: {current_file if current_file else 'current directory'}"
|
|
481
|
+
)
|
|
482
|
+
lib_file = os.path.abspath(lib_file)
|
|
483
|
+
if lib_file in visited:
|
|
484
|
+
continue
|
|
485
|
+
visited.add(lib_file)
|
|
486
|
+
included_ast = _parse_single_file(lib_file, include_comments)
|
|
487
|
+
included_ast = _resolve_includes(included_ast, lib_file, include_comments, visited)
|
|
488
|
+
if included_ast:
|
|
489
|
+
result.extend(included_ast)
|
|
490
|
+
else:
|
|
491
|
+
result.append(node)
|
|
492
|
+
return result
|
|
272
493
|
|
|
273
494
|
|
|
274
495
|
def getASTfromFile(file: str, include_comments: bool = False, process_includes: bool = True) -> list[ASTNode] | None:
|
|
@@ -278,20 +499,21 @@ def getASTfromFile(file: str, include_comments: bool = False, process_includes:
|
|
|
278
499
|
This function reads the contents of the provided OpenSCAD file, processes include statements,
|
|
279
500
|
parses it using the OpenSCAD parser, and returns the resulting AST (or list of AST nodes).
|
|
280
501
|
|
|
281
|
-
The function caches AST trees in memory. Cache entries are automatically
|
|
282
|
-
if
|
|
502
|
+
The function caches AST trees both in memory and on disk. Cache entries are automatically
|
|
503
|
+
invalidated if a file's modification timestamp changes, ensuring updated files are re-parsed.
|
|
504
|
+
Each included file is parsed independently and cached separately, so only changed files
|
|
505
|
+
need re-parsing.
|
|
283
506
|
|
|
284
507
|
Important: The `process_includes` parameter affects the AST structure:
|
|
285
|
-
|
|
286
|
-
- When `process_includes=True` (default): Include statements are processed
|
|
287
|
-
meaning the included file
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
508
|
+
|
|
509
|
+
- When `process_includes=True` (default): Include statements are processed and resolved,
|
|
510
|
+
meaning the included file's AST nodes are inlined, and the AST will NOT contain
|
|
511
|
+
`IncludeStatement` nodes. The AST represents the code as if all includes have been expanded.
|
|
512
|
+
|
|
291
513
|
- When `process_includes=False`: Include statements are NOT processed, and the AST will
|
|
292
514
|
contain `IncludeStatement` nodes wherever `include <file>` statements appear in the
|
|
293
515
|
source code.
|
|
294
|
-
|
|
516
|
+
|
|
295
517
|
Note: Unlike `include` statements, `use <file>` statements are ALWAYS parsed into
|
|
296
518
|
`UseStatement` AST nodes, regardless of the `process_includes` setting. This is because
|
|
297
519
|
`use` statements only affect module and function lookup at runtime, not source inclusion.
|
|
@@ -316,54 +538,34 @@ def getASTfromFile(file: str, include_comments: bool = False, process_includes:
|
|
|
316
538
|
# Get AST with IncludeStatement nodes instead of processing includes
|
|
317
539
|
ast_with_include_nodes = getASTfromFile("my_model.scad", process_includes=False)
|
|
318
540
|
"""
|
|
319
|
-
# Get absolute path for consistent cache keys
|
|
320
541
|
file_path = os.path.abspath(file)
|
|
321
|
-
|
|
322
|
-
# Check if file exists and get its modification time
|
|
542
|
+
|
|
323
543
|
if not os.path.exists(file_path):
|
|
324
544
|
raise FileNotFoundError(f"File {file} not found")
|
|
325
|
-
|
|
545
|
+
|
|
326
546
|
current_mtime = os.path.getmtime(file_path)
|
|
327
|
-
|
|
328
|
-
#
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
547
|
+
|
|
548
|
+
# For process_includes=False, just parse the single file
|
|
549
|
+
if not process_includes:
|
|
550
|
+
return _parse_single_file(file_path, include_comments)
|
|
551
|
+
|
|
552
|
+
# Check resolved cache (in-memory only since resolved ASTs depend on multiple files)
|
|
553
|
+
resolved_key = (file_path, include_comments, True)
|
|
554
|
+
if resolved_key in _resolved_cache:
|
|
555
|
+
cached_ast, cached_mtime = _resolved_cache[resolved_key]
|
|
335
556
|
if cached_mtime == current_mtime:
|
|
336
557
|
return cached_ast
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if process_includes:
|
|
349
|
-
try:
|
|
350
|
-
source_map = process_includes_func(source_map, file_path)
|
|
351
|
-
except FileNotFoundError as e:
|
|
352
|
-
# Re-raise file not found errors as-is
|
|
353
|
-
raise
|
|
354
|
-
except Exception as e: # pragma: no cover
|
|
355
|
-
raise Exception(f"Error processing includes: {e}")
|
|
356
|
-
|
|
357
|
-
# Get the combined string for parsing
|
|
358
|
-
combined_code = source_map.get_combined_string()
|
|
359
|
-
|
|
360
|
-
# Parse
|
|
361
|
-
parser = getOpenSCADParser(reduce_tree=False, include_comments=include_comments)
|
|
362
|
-
ast = parse_ast(parser, combined_code, source_map=source_map)
|
|
363
|
-
|
|
364
|
-
# Cache the result with current modification time
|
|
365
|
-
_ast_cache[cache_key] = (ast, current_mtime)
|
|
366
|
-
|
|
558
|
+
|
|
559
|
+
# Parse the file independently (uses per-file cache)
|
|
560
|
+
ast = _parse_single_file(file_path, include_comments)
|
|
561
|
+
|
|
562
|
+
# Resolve all include statements recursively
|
|
563
|
+
visited = {file_path}
|
|
564
|
+
ast = _resolve_includes(ast, file_path, include_comments, visited)
|
|
565
|
+
|
|
566
|
+
# Cache the resolved result
|
|
567
|
+
_resolved_cache[resolved_key] = (ast, current_mtime)
|
|
568
|
+
|
|
367
569
|
return ast
|
|
368
570
|
|
|
369
571
|
|
|
@@ -80,8 +80,13 @@ class SemanticChildren(list):
|
|
|
80
80
|
self._rule_map = rule_map
|
|
81
81
|
|
|
82
82
|
def __getattr__(self, name):
|
|
83
|
+
if name == '_rule_map':
|
|
84
|
+
raise AttributeError(name)
|
|
83
85
|
return self._rule_map.get(name, [])
|
|
84
86
|
|
|
87
|
+
def __reduce__(self):
|
|
88
|
+
return (list, (list(self),))
|
|
89
|
+
|
|
85
90
|
def get_rule(self, rule_name, index=0):
|
|
86
91
|
"""Return the index-th result for rule_name, or [] if absent/out of range."""
|
|
87
92
|
results = self._rule_map.get(rule_name, [])
|
|
@@ -95,7 +100,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
95
100
|
|
|
96
101
|
def __init__(self, parser, source_map=None, file=""):
|
|
97
102
|
"""Initialize the visitor with the parser and optional source map or file path.
|
|
98
|
-
|
|
103
|
+
|
|
99
104
|
Args:
|
|
100
105
|
parser: The Arpeggio parser instance (needed to access input for position conversion)
|
|
101
106
|
source_map: Optional SourceMap for tracking positions across multiple origins
|
|
@@ -105,10 +110,12 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
105
110
|
self.parser = parser
|
|
106
111
|
if source_map is not None:
|
|
107
112
|
self.source_map = source_map
|
|
113
|
+
self._has_source_map = bool(source_map._segments)
|
|
108
114
|
self.file = "" # Not used when source_map is provided
|
|
109
115
|
else:
|
|
110
116
|
# Backward compatibility: create a simple source map from file
|
|
111
117
|
self.source_map = SourceMap()
|
|
118
|
+
self._has_source_map = False
|
|
112
119
|
if file:
|
|
113
120
|
# We can't get the content here, but we'll handle it in _get_node_position
|
|
114
121
|
self.file = file
|
|
@@ -194,17 +201,9 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
194
201
|
"""Return the combined-string offset one past the last character of node."""
|
|
195
202
|
if node is None:
|
|
196
203
|
return 0
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
end = getattr(node, 'position', 0)
|
|
200
|
-
for child in node:
|
|
201
|
-
child_end = self._get_node_end_position(child)
|
|
202
|
-
if child_end > end:
|
|
203
|
-
end = child_end
|
|
204
|
+
end = getattr(node, 'position_end', None)
|
|
205
|
+
if end is not None:
|
|
204
206
|
return end
|
|
205
|
-
except TypeError:
|
|
206
|
-
pass
|
|
207
|
-
# Terminal: start + length of matched value
|
|
208
207
|
pos = getattr(node, 'position', 0)
|
|
209
208
|
val = getattr(node, 'value', '')
|
|
210
209
|
return pos + len(str(val))
|
|
@@ -225,7 +224,7 @@ class ASTBuilderVisitor(PTNodeVisitor):
|
|
|
225
224
|
end_pos = self._get_node_end_position(node)
|
|
226
225
|
|
|
227
226
|
# Use SourceMap if available to map position back to original origin
|
|
228
|
-
if
|
|
227
|
+
if self._has_source_map:
|
|
229
228
|
return self.source_map.get_location(char_pos, end_pos)
|
|
230
229
|
else:
|
|
231
230
|
# Fallback: calculate line/column from character position
|
|
@@ -172,11 +172,15 @@ class SourceMap:
|
|
|
172
172
|
# Calculate the actual line/column for the after segment
|
|
173
173
|
# Count lines in the part that was removed + before
|
|
174
174
|
removed_and_before = segment.content[:replace_end_in_segment]
|
|
175
|
-
line_count = removed_and_before.count('\n')
|
|
175
|
+
line_count = removed_and_before.count('\n') + line_count_adjustment
|
|
176
176
|
if line_count > 0:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if line_count_adjustment and removed_and_before.count('\n') == 0:
|
|
178
|
+
after_segment.start_line = segment.start_line + line_count
|
|
179
|
+
after_segment.start_column = 1
|
|
180
|
+
else:
|
|
181
|
+
last_newline = removed_and_before.rfind('\n')
|
|
182
|
+
after_segment.start_line = segment.start_line + line_count
|
|
183
|
+
after_segment.start_column = len(removed_and_before) - last_newline
|
|
180
184
|
else:
|
|
181
185
|
after_segment.start_line = segment.start_line
|
|
182
186
|
after_segment.start_column = segment.start_column + len(removed_and_before)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|