jaclang 0.7.14__py3-none-any.whl → 0.7.17__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.

Potentially problematic release.


This version of jaclang might be problematic. Click here for more details.

Files changed (131) hide show
  1. jaclang/cli/cli.py +147 -77
  2. jaclang/cli/cmdreg.py +9 -12
  3. jaclang/compiler/__init__.py +19 -53
  4. jaclang/compiler/absyntree.py +94 -16
  5. jaclang/compiler/constant.py +8 -8
  6. jaclang/compiler/jac.lark +4 -3
  7. jaclang/compiler/parser.py +41 -25
  8. jaclang/compiler/passes/ir_pass.py +4 -13
  9. jaclang/compiler/passes/main/__init__.py +1 -1
  10. jaclang/compiler/passes/main/access_modifier_pass.py +96 -147
  11. jaclang/compiler/passes/main/fuse_typeinfo_pass.py +155 -54
  12. jaclang/compiler/passes/main/import_pass.py +99 -75
  13. jaclang/compiler/passes/main/py_collect_dep_pass.py +70 -0
  14. jaclang/compiler/passes/main/pyast_gen_pass.py +328 -565
  15. jaclang/compiler/passes/main/pyast_load_pass.py +33 -6
  16. jaclang/compiler/passes/main/pyjac_ast_link_pass.py +7 -0
  17. jaclang/compiler/passes/main/registry_pass.py +37 -3
  18. jaclang/compiler/passes/main/schedules.py +9 -2
  19. jaclang/compiler/passes/main/sym_tab_build_pass.py +10 -6
  20. jaclang/compiler/passes/main/tests/__init__.py +1 -1
  21. jaclang/compiler/passes/main/tests/fixtures/autoimpl.empty.impl.jac +0 -0
  22. jaclang/compiler/passes/main/tests/fixtures/autoimpl.jac +1 -1
  23. jaclang/compiler/passes/main/tests/fixtures/py_imp_test.jac +29 -0
  24. jaclang/compiler/passes/main/tests/fixtures/pygame_mock/__init__.py +3 -0
  25. jaclang/compiler/passes/main/tests/fixtures/pygame_mock/color.py +3 -0
  26. jaclang/compiler/passes/main/tests/fixtures/pygame_mock/constants.py +5 -0
  27. jaclang/compiler/passes/main/tests/fixtures/pygame_mock/display.py +2 -0
  28. jaclang/compiler/passes/main/tests/test_import_pass.py +72 -13
  29. jaclang/compiler/passes/main/type_check_pass.py +22 -5
  30. jaclang/compiler/passes/tool/jac_formatter_pass.py +135 -89
  31. jaclang/compiler/passes/tool/tests/fixtures/corelib.jac +37 -41
  32. jaclang/compiler/passes/tool/tests/fixtures/corelib_fmt.jac +37 -42
  33. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/access_mod_check.jac +27 -0
  34. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/architype_test.jac +13 -0
  35. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/comment_alignment.jac +11 -0
  36. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/comments.jac +13 -0
  37. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/decorator_stack.jac +37 -0
  38. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/esc_keywords.jac +5 -0
  39. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/long_names.jac +19 -0
  40. jaclang/compiler/passes/tool/tests/fixtures/general_format_checks/triple_quoted_string.jac +6 -0
  41. jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +11 -0
  42. jaclang/compiler/passes/tool/tests/test_unparse_validate.py +33 -39
  43. jaclang/compiler/passes/transform.py +4 -0
  44. jaclang/compiler/passes/utils/mypy_ast_build.py +45 -0
  45. jaclang/compiler/semtable.py +31 -7
  46. jaclang/compiler/symtable.py +16 -11
  47. jaclang/compiler/tests/test_importer.py +25 -10
  48. jaclang/langserve/engine.py +104 -118
  49. jaclang/langserve/sem_manager.py +379 -0
  50. jaclang/langserve/server.py +24 -11
  51. jaclang/langserve/tests/fixtures/base_module_structure.jac +27 -6
  52. jaclang/langserve/tests/fixtures/circle.jac +3 -3
  53. jaclang/langserve/tests/fixtures/circle_err.jac +3 -3
  54. jaclang/langserve/tests/fixtures/circle_pure.test.jac +3 -3
  55. jaclang/langserve/tests/fixtures/import_include_statements.jac +1 -1
  56. jaclang/langserve/tests/fixtures/rename.jac +30 -0
  57. jaclang/langserve/tests/test_sem_tokens.py +277 -0
  58. jaclang/langserve/tests/test_server.py +287 -17
  59. jaclang/langserve/utils.py +184 -98
  60. jaclang/plugin/builtin.py +1 -1
  61. jaclang/plugin/default.py +288 -92
  62. jaclang/plugin/feature.py +65 -27
  63. jaclang/plugin/spec.py +62 -23
  64. jaclang/plugin/tests/fixtures/other_root_access.jac +82 -0
  65. jaclang/plugin/tests/test_jaseci.py +414 -42
  66. jaclang/runtimelib/architype.py +650 -0
  67. jaclang/{core → runtimelib}/constructs.py +5 -8
  68. jaclang/{core → runtimelib}/context.py +86 -59
  69. jaclang/runtimelib/importer.py +361 -0
  70. jaclang/runtimelib/machine.py +158 -0
  71. jaclang/runtimelib/memory.py +158 -0
  72. jaclang/{core → runtimelib}/utils.py +30 -15
  73. jaclang/settings.py +5 -4
  74. jaclang/tests/fixtures/abc.jac +3 -3
  75. jaclang/tests/fixtures/access_checker.jac +12 -17
  76. jaclang/tests/fixtures/access_modifier.jac +88 -33
  77. jaclang/tests/fixtures/baddy.jac +3 -0
  78. jaclang/tests/fixtures/baddy.test.jac +3 -0
  79. jaclang/tests/fixtures/bar.jac +34 -0
  80. jaclang/tests/fixtures/byllmissue.jac +1 -5
  81. jaclang/tests/fixtures/chandra_bugs2.jac +11 -10
  82. jaclang/tests/fixtures/cls_method.jac +41 -0
  83. jaclang/tests/fixtures/dblhello.jac +6 -0
  84. jaclang/tests/fixtures/deep/one_lev.jac +3 -3
  85. jaclang/tests/fixtures/deep/one_lev_dup.jac +2 -3
  86. jaclang/tests/fixtures/deep_import_mods.jac +13 -0
  87. jaclang/tests/fixtures/edge_node_walk.jac +1 -1
  88. jaclang/tests/fixtures/edge_ops.jac +1 -1
  89. jaclang/tests/fixtures/edges_walk.jac +1 -1
  90. jaclang/tests/fixtures/err.impl.jac +3 -0
  91. jaclang/tests/fixtures/err.jac +4 -2
  92. jaclang/tests/fixtures/err_runtime.jac +15 -0
  93. jaclang/tests/fixtures/foo.jac +43 -0
  94. jaclang/tests/fixtures/gendot_bubble_sort.jac +1 -1
  95. jaclang/tests/fixtures/hello.jac +4 -0
  96. jaclang/tests/fixtures/impl_grab.impl.jac +2 -1
  97. jaclang/tests/fixtures/impl_grab.jac +4 -1
  98. jaclang/tests/fixtures/import.jac +9 -0
  99. jaclang/tests/fixtures/index_slice.jac +30 -0
  100. jaclang/tests/fixtures/jp_importer_auto.jac +14 -0
  101. jaclang/tests/fixtures/maxfail_run_test.jac +4 -4
  102. jaclang/tests/fixtures/needs_import.jac +2 -2
  103. jaclang/tests/fixtures/pyfunc_1.py +1 -1
  104. jaclang/tests/fixtures/pyfunc_2.py +5 -2
  105. jaclang/tests/fixtures/pygame_mock/__init__.py +3 -0
  106. jaclang/tests/fixtures/pygame_mock/color.py +3 -0
  107. jaclang/tests/fixtures/pygame_mock/constants.py +5 -0
  108. jaclang/tests/fixtures/pygame_mock/display.py +2 -0
  109. jaclang/tests/fixtures/pygame_mock/inner/__init__.py +0 -0
  110. jaclang/tests/fixtures/pygame_mock/inner/iner_mod.py +2 -0
  111. jaclang/tests/fixtures/registry.jac +9 -0
  112. jaclang/tests/fixtures/run_test.jac +4 -4
  113. jaclang/tests/fixtures/semstr.jac +1 -4
  114. jaclang/tests/fixtures/simple_archs.jac +1 -1
  115. jaclang/tests/test_cli.py +109 -3
  116. jaclang/tests/test_language.py +170 -68
  117. jaclang/tests/test_reference.py +2 -3
  118. jaclang/utils/helpers.py +45 -21
  119. jaclang/utils/test.py +9 -0
  120. jaclang/utils/treeprinter.py +30 -7
  121. {jaclang-0.7.14.dist-info → jaclang-0.7.17.dist-info}/METADATA +3 -2
  122. {jaclang-0.7.14.dist-info → jaclang-0.7.17.dist-info}/RECORD +126 -90
  123. jaclang/core/architype.py +0 -502
  124. jaclang/core/importer.py +0 -344
  125. jaclang/core/memory.py +0 -99
  126. jaclang/tests/fixtures/aott_raise.jac +0 -25
  127. jaclang/tests/fixtures/package_import.jac +0 -6
  128. /jaclang/{core → runtimelib}/__init__.py +0 -0
  129. /jaclang/{core → runtimelib}/test.py +0 -0
  130. {jaclang-0.7.14.dist-info → jaclang-0.7.17.dist-info}/WHEEL +0 -0
  131. {jaclang-0.7.14.dist-info → jaclang-0.7.17.dist-info}/entry_points.txt +0 -0
@@ -14,7 +14,6 @@ from jaclang.compiler.codeloc import CodeLocInfo
14
14
  from jaclang.compiler.constant import SymbolType
15
15
  from jaclang.compiler.passes.transform import Alert
16
16
  from jaclang.compiler.symtable import Symbol, SymbolTable
17
- from jaclang.utils.helpers import import_target_to_relative_path
18
17
  from jaclang.vendor.pygls import uris
19
18
 
20
19
  import lsprotocol.types as lspt
@@ -130,12 +129,25 @@ def position_within_node(node: ast.AstNode, line: int, character: int) -> bool:
130
129
  return False
131
130
 
132
131
 
133
- def collect_symbols(node: SymbolTable) -> list[lspt.DocumentSymbol]:
132
+ def find_index(
133
+ sem_tokens: list[int],
134
+ line: int,
135
+ char: int,
136
+ ) -> Optional[int]:
137
+ """Find index."""
138
+ index = None
139
+ for i, j in enumerate(
140
+ [get_token_start(i, sem_tokens) for i in range(0, len(sem_tokens), 5)]
141
+ ):
142
+ if j[0] == line and j[1] <= char <= j[2]:
143
+ return i
144
+
145
+ return index
146
+
147
+
148
+ def get_symbols_for_outline(node: SymbolTable) -> list[lspt.DocumentSymbol]:
134
149
  """Recursively collect symbols from the AST."""
135
150
  symbols = []
136
- if node is None:
137
- return symbols
138
-
139
151
  for key, item in node.tab.items():
140
152
  if (
141
153
  key in dir(builtins)
@@ -143,23 +155,20 @@ def collect_symbols(node: SymbolTable) -> list[lspt.DocumentSymbol]:
143
155
  or item.decl.loc.mod_path != node.owner.loc.mod_path
144
156
  ):
145
157
  continue
146
- else:
147
-
148
- pos = create_range(item.decl.loc)
149
- symbol = lspt.DocumentSymbol(
150
- name=key,
151
- kind=kind_map(item.decl),
152
- range=pos,
153
- selection_range=pos,
154
- children=[],
155
- )
156
- symbols.append(symbol)
158
+ pos = create_range(item.decl.loc)
159
+ symbol = lspt.DocumentSymbol(
160
+ name=key,
161
+ kind=kind_map(item.decl),
162
+ range=pos,
163
+ selection_range=pos,
164
+ children=[],
165
+ )
166
+ symbols.append(symbol)
157
167
 
158
168
  for sub_tab in [
159
169
  i for i in node.kid if i.owner.loc.mod_path == node.owner.loc.mod_path
160
170
  ]:
161
- sub_symbols = collect_symbols(sub_tab)
162
-
171
+ sub_symbols = get_symbols_for_outline(sub_tab)
163
172
  if isinstance(
164
173
  sub_tab.owner,
165
174
  (ast.IfStmt, ast.ElseStmt, ast.WhileStmt, ast.IterForStmt, ast.InForStmt),
@@ -264,19 +273,20 @@ def label_map(sub_tab: SymbolType) -> lspt.CompletionItemKind:
264
273
  )
265
274
 
266
275
 
267
- def get_mod_path(mod_path: ast.ModulePath, name_node: ast.Name) -> str | None:
276
+ def get_mod_path(
277
+ mod_path: ast.ModulePath, name_node: ast.Name
278
+ ) -> str | None: # TODO: This should go away
268
279
  """Get path for a module import name."""
269
280
  ret_target = None
270
- module_location_path = mod_path.loc.mod_path
271
281
  if mod_path.parent and (
272
282
  (
273
283
  isinstance(mod_path.parent.parent, ast.Import)
274
- and mod_path.parent.parent.hint.tag.value == "py"
284
+ and mod_path.parent.parent.is_py
275
285
  )
276
286
  or (
277
287
  isinstance(mod_path.parent, ast.Import)
278
288
  and mod_path.parent.from_loc
279
- and mod_path.parent.hint.tag.value == "py"
289
+ and mod_path.parent.is_py
280
290
  )
281
291
  ):
282
292
  if mod_path.path and name_node in mod_path.path:
@@ -286,39 +296,35 @@ def get_mod_path(mod_path: ast.ModulePath, name_node: ast.Name) -> str | None:
286
296
  else ""
287
297
  )
288
298
  else:
289
- temporary_path_str = mod_path.path_str
290
- sys.path.append(os.path.dirname(module_location_path))
299
+ temporary_path_str = mod_path.dot_path_str
300
+ sys.path.append(os.path.dirname(mod_path.loc.mod_path))
291
301
  spec = importlib.util.find_spec(temporary_path_str)
292
- sys.path.remove(os.path.dirname(module_location_path))
302
+ sys.path.remove(os.path.dirname(mod_path.loc.mod_path))
293
303
  if spec and spec.origin and spec.origin.endswith(".py"):
294
304
  ret_target = spec.origin
295
305
  elif mod_path.parent and (
296
306
  (
297
307
  isinstance(mod_path.parent.parent, ast.Import)
298
- and mod_path.parent.parent.hint.tag.value == "jac"
308
+ and mod_path.parent.parent.is_jac
299
309
  )
300
310
  or (
301
311
  isinstance(mod_path.parent, ast.Import)
302
312
  and mod_path.parent.from_loc
303
- and mod_path.parent.hint.tag.value == "jac"
313
+ and mod_path.parent.is_jac
304
314
  )
305
315
  ):
306
- ret_target = import_target_to_relative_path(
307
- level=mod_path.level,
308
- target=mod_path.path_str,
309
- base_path=os.path.dirname(module_location_path),
310
- )
316
+ ret_target = mod_path.resolve_relative_path()
311
317
  return ret_target
312
318
 
313
319
 
314
320
  def get_item_path(mod_item: ast.ModuleItem) -> tuple[str, tuple[int, int]] | None:
315
321
  """Get path."""
316
322
  item_name = mod_item.name.value
317
- if mod_item.from_parent.hint.tag.value == "py" and mod_item.from_parent.from_loc:
323
+ if mod_item.from_parent.is_py and mod_item.from_parent.from_loc:
318
324
  path = get_mod_path(mod_item.from_parent.from_loc, mod_item.name)
319
325
  if path:
320
326
  return get_definition_range(path, item_name)
321
- elif mod_item.from_parent.hint.tag.value == "jac":
327
+ elif mod_item.from_parent.is_jac:
322
328
  mod_node = mod_item.from_mod_path
323
329
  if mod_node.sub_module and mod_node.sub_module._sym_tab:
324
330
  for symbol_name, symbol in mod_node.sub_module._sym_tab.tab.items():
@@ -364,50 +370,6 @@ def get_definition_range(
364
370
  return None
365
371
 
366
372
 
367
- def locate_affected_token(
368
- tokens: list[int],
369
- change_start_line: int,
370
- change_start_char: int,
371
- change_end_line: int,
372
- change_end_char: int,
373
- ) -> Optional[int]:
374
- """Find in which token change is occurring."""
375
- token_index = 0
376
- current_line = 0
377
- line_char_offset = 0
378
-
379
- while token_index < len(tokens):
380
- token_line_delta = tokens[token_index]
381
- token_start_char = tokens[token_index + 1]
382
- token_length = tokens[token_index + 2]
383
-
384
- if token_line_delta > 0:
385
- current_line += token_line_delta
386
- line_char_offset = 0
387
- token_abs_start_char = line_char_offset + token_start_char
388
- token_abs_end_char = token_abs_start_char + token_length
389
- if (
390
- current_line == change_start_line == change_end_line
391
- and token_abs_start_char <= change_start_char
392
- and change_end_char <= token_abs_end_char
393
- ):
394
- return token_index
395
- if (
396
- current_line == change_start_line
397
- and token_abs_start_char <= change_start_char < token_abs_end_char
398
- ):
399
- return token_index
400
- if (
401
- current_line == change_end_line
402
- and token_abs_start_char < change_end_char <= token_abs_end_char
403
- ):
404
- return token_index
405
-
406
- line_char_offset += token_start_char
407
- token_index += 5
408
- return None
409
-
410
-
411
373
  def collect_all_symbols_in_scope(
412
374
  sym_tab: SymbolTable, up_tree: bool = True
413
375
  ) -> list[lspt.CompletionItem]:
@@ -419,7 +381,7 @@ def collect_all_symbols_in_scope(
419
381
  while current_tab is not None and current_tab not in visited:
420
382
  visited.add(current_tab)
421
383
  for name, symbol in current_tab.tab.items():
422
- if name not in dir(builtins):
384
+ if name not in dir(builtins) and symbol.sym_type != SymbolType.IMPL:
423
385
  symbols.append(
424
386
  lspt.CompletionItem(label=name, kind=label_map(symbol.sym_type))
425
387
  )
@@ -431,26 +393,32 @@ def collect_all_symbols_in_scope(
431
393
 
432
394
  def parse_symbol_path(text: str, dot_position: int) -> list[str]:
433
395
  """Parse text and return a list of symbols."""
434
- text = text[:dot_position].strip()
435
- pattern = re.compile(r"\b\w+\(\)?|\b\w+\b")
436
- matches = pattern.findall(text)
437
- if text.endswith("."):
438
- matches.append("")
439
- symbol_path = []
440
- i = 0
441
- while i < len(matches):
442
- if matches[i].endswith("("):
443
- i += 1
444
- continue
445
- elif "(" in matches[i]:
446
- symbol_path.append(matches[i])
447
- elif matches[i] == "":
448
- pass
396
+ text = text[:dot_position][:-1].strip()
397
+ valid_character_pattern = re.compile(r"[a-zA-Z0-9_]")
398
+
399
+ reversed_text = text[::-1]
400
+ all_words = []
401
+ current_word = []
402
+ for char in reversed_text:
403
+ if valid_character_pattern.fullmatch(char):
404
+ current_word.append(char)
405
+ elif char == ".":
406
+ if current_word:
407
+ all_words.append("".join(current_word[::-1]))
408
+ current_word = []
449
409
  else:
450
- symbol_path.append(matches[i])
451
- i += 1
410
+ if current_word:
411
+ all_words.append("".join(current_word[::-1]))
412
+ current_word = []
413
+ break
414
+
415
+ all_words = (
416
+ all_words[::-1]
417
+ if not current_word
418
+ else ["".join(current_word[::-1])] + all_words[::-1]
419
+ )
452
420
 
453
- return symbol_path
421
+ return all_words
454
422
 
455
423
 
456
424
  def resolve_symbol_path(sym_name: str, node_tab: SymbolTable) -> str:
@@ -463,6 +431,11 @@ def resolve_symbol_path(sym_name: str, node_tab: SymbolTable) -> str:
463
431
  for name, symbol in current_tab.tab.items():
464
432
  if name not in dir(builtins) and name == sym_name:
465
433
  path = symbol.defn[0]._sym_type
434
+ if symbol.sym_type == SymbolType.ENUM_ARCH:
435
+ if isinstance(current_tab.owner, ast.Module):
436
+ return current_tab.owner.name + "." + sym_name
437
+ elif isinstance(current_tab.owner, ast.AstSymbolNode):
438
+ return current_tab.owner.name_spec._sym_type + "." + sym_name
466
439
  return path
467
440
  current_tab = current_tab.parent if current_tab.parent != current_tab else None
468
441
  return ""
@@ -553,8 +526,121 @@ def resolve_completion_symbol_table(
553
526
  )
554
527
  else:
555
528
  completion_items = []
556
-
529
+ if isinstance(current_symbol_table.owner, (ast.Ability, ast.AbilityDef)):
530
+ return completion_items
557
531
  completion_items.extend(
558
532
  collect_all_symbols_in_scope(current_symbol_table, up_tree=False)
559
533
  )
560
534
  return completion_items
535
+
536
+
537
+ def get_token_start(
538
+ token_index: int | None, sem_tokens: list[int]
539
+ ) -> tuple[int, int, int]:
540
+ """Return the starting position of a token."""
541
+ if token_index is None or token_index >= len(sem_tokens):
542
+ return 0, 0, 0
543
+
544
+ current_line = 0
545
+ current_char = 0
546
+ current_tok_index = 0
547
+
548
+ while current_tok_index < len(sem_tokens):
549
+ token_line_delta = sem_tokens[current_tok_index]
550
+ token_start_char = sem_tokens[current_tok_index + 1]
551
+
552
+ if token_line_delta > 0:
553
+ current_line += token_line_delta
554
+ current_char = 0
555
+ if current_tok_index == token_index:
556
+ if token_line_delta > 0:
557
+ return (
558
+ current_line,
559
+ token_start_char,
560
+ token_start_char + sem_tokens[current_tok_index + 2],
561
+ )
562
+ return (
563
+ current_line,
564
+ current_char + token_start_char,
565
+ current_char + token_start_char + sem_tokens[current_tok_index + 2],
566
+ )
567
+
568
+ current_char += token_start_char
569
+ current_tok_index += 5
570
+
571
+ return (
572
+ current_line,
573
+ current_char,
574
+ current_char + sem_tokens[current_tok_index + 2],
575
+ )
576
+
577
+
578
+ def find_surrounding_tokens(
579
+ change_start_line: int,
580
+ change_start_char: int,
581
+ change_end_line: int,
582
+ change_end_char: int,
583
+ sem_tokens: list[int],
584
+ ) -> tuple[int | None, int | None, bool]:
585
+ """Find the indices of the previous and next tokens surrounding the change."""
586
+ prev_token_index = None
587
+ next_token_index = None
588
+ inside_tok = False
589
+ for i, tok in enumerate(
590
+ [get_token_start(i, sem_tokens) for i in range(0, len(sem_tokens), 5)][0:]
591
+ ):
592
+ if (not (prev_token_index is None or next_token_index is None)) and (
593
+ tok[0] > change_end_line
594
+ or (tok[0] == change_end_line and tok[1] > change_end_char)
595
+ ):
596
+ prev_token_index = i * 5
597
+ break
598
+ elif (
599
+ change_start_line == tok[0] == change_end_line
600
+ and tok[1] <= change_start_char
601
+ and tok[2] >= change_end_char
602
+ ):
603
+ prev_token_index = i * 5
604
+ inside_tok = True
605
+ break
606
+ elif (tok[0] < change_start_line) or (
607
+ tok[0] == change_start_line and tok[1] < change_start_char
608
+ ):
609
+ prev_token_index = i * 5
610
+ elif (tok[0] > change_end_line) or (
611
+ tok[0] == change_end_line and tok[1] > change_end_char
612
+ ):
613
+ next_token_index = i * 5
614
+ break
615
+
616
+ return prev_token_index, next_token_index, inside_tok
617
+
618
+
619
+ def get_line_of_code(line_number: int, lines: list[str]) -> Optional[tuple[str, int]]:
620
+ """Get the line of code, and the first non-space character index."""
621
+ if 0 <= line_number < len(lines):
622
+ line = lines[line_number].rstrip("\n")
623
+ first_non_space = len(line) - len(line.lstrip())
624
+ return line, (
625
+ first_non_space + 4
626
+ if line.strip().endswith(("(", "{", "["))
627
+ else first_non_space
628
+ )
629
+ return None
630
+
631
+
632
+ def add_unique_text_edit(
633
+ changes: dict[str, list[lspt.TextEdit]], key: str, new_edit: lspt.TextEdit
634
+ ) -> None:
635
+ """Add a new text edit to the changes dictionary if it is unique."""
636
+ if key not in changes:
637
+ changes[key] = [new_edit]
638
+ else:
639
+ for existing_edit in changes[key]:
640
+ if (
641
+ existing_edit.range.start == new_edit.range.start
642
+ and existing_edit.range.end == new_edit.range.end
643
+ and existing_edit.new_text == new_edit.new_text
644
+ ):
645
+ return
646
+ changes[key].append(new_edit)
jaclang/plugin/builtin.py CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from typing import Optional
9
- from jaclang.core.constructs import NodeArchitype
9
+ from jaclang.runtimelib.constructs import NodeArchitype
10
10
 
11
11
 
12
12
  def dotgen(