gdharness 1.3.42 → 1.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/build/cli.js +383 -243
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +51 -17
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
  33. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
  34. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  35. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  36. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  37. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  38. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  39. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  40. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  41. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  42. package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
  43. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  44. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
  45. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  46. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  47. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  49. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  50. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  51. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  52. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
  53. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  54. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  55. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  56. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  57. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  58. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  59. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
  60. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  61. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  62. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  63. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  64. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  65. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  66. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  67. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  68. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  69. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  70. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  71. package/build/godot/loader/loader.gd +9 -9
  72. package/build/godot/operations/animation_reads.gd +55 -33
  73. package/build/godot/operations/audio_buses.gd +48 -19
  74. package/build/godot/operations/class_cache.gd +16 -11
  75. package/build/godot/operations/classdb_queries.gd +89 -44
  76. package/build/godot/operations/declared_values.gd +68 -32
  77. package/build/godot/operations/dependencies.gd +190 -109
  78. package/build/godot/operations/editor_tools.gd +10 -10
  79. package/build/godot/operations/export_presets.gd +100 -54
  80. package/build/godot/operations/file_reads.gd +83 -42
  81. package/build/godot/operations/file_walk.gd +12 -5
  82. package/build/godot/operations/gdscript_analysis.gd +86 -65
  83. package/build/godot/operations/gdscript_authoring.gd +83 -36
  84. package/build/godot/operations/godot_operations.gd +17 -11
  85. package/build/godot/operations/import_pipeline.gd +110 -59
  86. package/build/godot/operations/input_actions.gd +62 -39
  87. package/build/godot/operations/patterns.gd +3 -1
  88. package/build/godot/operations/plugins.gd +36 -16
  89. package/build/godot/operations/project_config.gd +61 -32
  90. package/build/godot/operations/project_diagnostics.gd +24 -11
  91. package/build/godot/operations/project_file.gd +16 -16
  92. package/build/godot/operations/project_validation.gd +71 -44
  93. package/build/godot/operations/property_paths.gd +22 -12
  94. package/build/godot/operations/property_values.gd +143 -78
  95. package/build/godot/operations/reading.gd +51 -17
  96. package/build/godot/operations/rename_checks.gd +2 -2
  97. package/build/godot/operations/resource_files.gd +11 -9
  98. package/build/godot/operations/same_values.gd +4 -4
  99. package/build/godot/operations/scene_connections.gd +27 -13
  100. package/build/godot/operations/script_check.gd +52 -49
  101. package/build/godot/operations/serialisation.gd +75 -51
  102. package/build/godot/operations/setting_reads.gd +7 -6
  103. package/build/godot/operations/value_shapes.gd +5 -6
  104. package/build/godot/tests/listed.gd +321 -268
  105. package/build/keeper.js +43 -43
  106. package/package.json +1 -1
@@ -1,8 +1,7 @@
1
1
  @tool
2
2
  extends "animation_tools.gd"
3
-
4
- ## The tracks and keys of an animation, beside the players and libraries the script it extends edits,
5
- ## whose helpers it shares.
3
+ ## The tracks and keys of an animation, beside the players and libraries the script it extends
4
+ ## edits, whose helpers it shares.
6
5
 
7
6
 
8
7
  # =============================================================================
@@ -15,19 +14,19 @@ func add_animation_track(args: Dictionary) -> Dictionary:
15
14
  var given_track: Variant = args.get("track", {})
16
15
 
17
16
  if scene_path.strip_edges() == "res://":
18
- return {"ok": false, "error": "Missing scenePath"}
17
+ return { "ok": false, "error": "Missing scenePath" }
19
18
  if animation_name.strip_edges().is_empty():
20
- return {"ok": false, "error": "Missing animationName"}
19
+ return { "ok": false, "error": "Missing animationName" }
21
20
  var track: Dictionary = given_track if given_track is Dictionary else {}
22
21
  if track.is_empty():
23
- return {"ok": false, "error": "Missing track"}
24
- # The schema leaves keyframes out of what track requires, since remove_track finds a track without
25
- # them.
22
+ return { "ok": false, "error": "Missing track" }
23
+ # The schema leaves keyframes out of what track requires, since remove_track finds a track
24
+ # without them.
26
25
  if not track.has("keyframes"):
27
- return {"ok": false, "error": "Missing track.keyframes"}
26
+ return { "ok": false, "error": "Missing track.keyframes" }
28
27
  var keyed: Array = _keyframes(track)
29
28
  if not keyed[0]:
30
- return {"ok": false, "error": keyed[1]}
29
+ return { "ok": false, "error": keyed[1] }
31
30
 
32
31
  # Asked of the player, which knows every library: looking only in the default one refused
33
32
  # "moves/walk" as missing.
@@ -48,13 +47,16 @@ func add_animation_track(args: Dictionary) -> Dictionary:
48
47
  target_path = "."
49
48
  var target: Node = _animated(player, target_path)
50
49
  if target == null:
51
- return scene.refuse(
52
- (
50
+ return (
51
+ scene
52
+ . refuse(
53
53
  (
54
- "No node at %s from the player's root node, %s, which is what the track path is read "
55
- + "against."
54
+ (
55
+ "No node at %s from the player's root node, %s, which is what the track "
56
+ + "path is read against."
57
+ )
58
+ % [target_path, str(player.root_node)]
56
59
  )
57
- % [target_path, str(player.root_node)]
58
60
  )
59
61
  )
60
62
  var keyframes: Array = keyed[1]
@@ -120,7 +122,12 @@ func add_animation_track(args: Dictionary) -> Dictionary:
120
122
  ## animates: two tracks for one path both play, the later overriding the earlier, and a key meant
121
123
  ## to extend the motion was split off where reading the animation showed two halves.
122
124
  func _add_track(
123
- anim: Animation, track: Dictionary, target: Node, target_path: String, keyframes: Array, replacing: bool
125
+ anim: Animation,
126
+ track: Dictionary,
127
+ target: Node,
128
+ target_path: String,
129
+ keyframes: Array,
130
+ replacing: bool
124
131
  ) -> Array:
125
132
  var track_type: String = str(track.get("type", ""))
126
133
  var keys: Array = []
@@ -148,7 +155,7 @@ func _add_track(
148
155
  return _not_added("keyframes[%d].args must be a list." % index)
149
156
  var raw: Array = given
150
157
  var arguments: Array = _fitted(target, method_name, _parse_method_args(raw))
151
- keys.append({"method": method_name, "args": arguments})
158
+ keys.append({ "method": method_name, "args": arguments })
152
159
  if misfit.is_empty():
153
160
  misfit = _key_misfit(target, method_name, arguments, index)
154
161
  method_found = target.has_method(method_name)
@@ -156,7 +163,10 @@ func _add_track(
156
163
  else:
157
164
  return _not_added(
158
165
  (
159
- "add_track makes property and method tracks, and track.type is %s; remove_track takes any."
166
+ (
167
+ "add_track makes property and method tracks, and track.type is %s; "
168
+ + "remove_track takes any."
169
+ )
160
170
  % track_type
161
171
  )
162
172
  )
@@ -178,14 +188,16 @@ func _add_track(
178
188
  return _not_added(
179
189
  (
180
190
  (
181
- "Track %d already animates %s and has a key at %s s, which keyframes[%d] would "
182
- + "replace. Nothing was written; replaceKeys true replaces it."
191
+ "Track %d already animates %s and has a key at %s s, which "
192
+ + "keyframes[%d] would replace. Nothing was written; replaceKeys true "
193
+ + "replaces it."
183
194
  )
184
195
  % [track_idx, str(path), _seconds(time), index]
185
196
  )
186
197
  )
187
- # A method key's value is its call, {"method": ..., "args": [...]}. The time is as asked:
188
- # the key's own, read back from a scene's 32-bit times, puts 0.1 at 0.100000001490116.
198
+ # A method key's value is its call, {"method": ..., "args": [...]}. The time is as
199
+ # asked: the key's own, read back from a scene's 32-bit times, puts 0.1 at
200
+ # 0.100000001490116.
189
201
  (
190
202
  replaced
191
203
  . append(
@@ -228,7 +240,9 @@ static func _fitted(target: Node, method: String, arguments: Array) -> Array:
228
240
  declared = GdhRead.as_int(parameter.get("type", TYPE_NIL))
229
241
  if value is float and declared != TYPE_FLOAT:
230
242
  var number: float = value
231
- if is_finite(number) and number == floorf(number) and absf(number) < 9007199254740992.0:
243
+ if (is_finite(number)
244
+ and number == floorf(number)
245
+ and absf(number) < 9_007_199_254_740_992.0):
232
246
  value = int(number)
233
247
  fitted.append(value)
234
248
  return fitted
@@ -256,10 +270,16 @@ static func _key_misfit(target: Node, method: String, arguments: Array, index: i
256
270
  else ("1 argument" if arguments.size() == 1 else "%d arguments" % arguments.size())
257
271
  )
258
272
  if arguments.size() < needed:
259
- return "keyframes[%d] calls %s with %s, and it needs %d%s" % [index, method, passed, needed, fails]
273
+ return (
274
+ "keyframes[%d] calls %s with %s, and it needs %d%s"
275
+ % [index, method, passed, needed, fails]
276
+ )
260
277
  if arguments.size() > parameters.size():
261
278
  var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
262
- return "keyframes[%d] calls %s with %s, and it takes %s%s" % [index, method, passed, most, fails]
279
+ return (
280
+ "keyframes[%d] calls %s with %s, and it takes %s%s"
281
+ % [index, method, passed, most, fails]
282
+ )
263
283
  for at: int in arguments.size():
264
284
  var parameter: Dictionary = parameters[at]
265
285
  var taken_type: int = GdhRead.as_int(parameter.get("type", TYPE_NIL))
@@ -276,7 +296,7 @@ static func _key_misfit(target: Node, method: String, arguments: Array, index: i
276
296
  method,
277
297
  str(parameter.get("name", "")),
278
298
  type_string(taken_type),
279
- fails
299
+ fails,
280
300
  ]
281
301
  )
282
302
  return ""
@@ -293,22 +313,22 @@ func remove_animation_track(args: Dictionary) -> Dictionary:
293
313
  var animation_name: String = str(args.get("animationName", ""))
294
314
  var given_track: Variant = args.get("track", {})
295
315
  if scene_path.strip_edges() == "res://":
296
- return {"ok": false, "error": "Missing scenePath"}
316
+ return { "ok": false, "error": "Missing scenePath" }
297
317
  if animation_name.strip_edges().is_empty():
298
- return {"ok": false, "error": "Missing animationName"}
318
+ return { "ok": false, "error": "Missing animationName" }
299
319
  var track: Dictionary = given_track if given_track is Dictionary else {}
300
320
  if track.is_empty():
301
- return {"ok": false, "error": "Missing track"}
321
+ return { "ok": false, "error": "Missing track" }
302
322
  var track_type: String = str(track.get("type", ""))
303
323
  if not track_type in GdhAnimationReads.TRACK_TYPE_NAMES.values():
304
- return {"ok": false, "error": "Unsupported track.type: " + track_type}
324
+ return { "ok": false, "error": "Unsupported track.type: " + track_type }
305
325
  var node_path: String = str(track.get("nodePath", ""))
306
326
  var property: String = str(track.get("property", ""))
307
327
  var shape: Array = [track_type, node_path, property]
308
328
  var method: String = str(track.get("method", "")) if track_type == "method" else ""
309
329
  var read_times: Variant = _times_of(args.get("times"))
310
330
  if read_times is String:
311
- return {"ok": false, "error": read_times}
331
+ return { "ok": false, "error": read_times }
312
332
  var times: Array = read_times
313
333
 
314
334
  var found: Array = _animation_at(scene_path, player_node_path, animation_name)
@@ -335,7 +355,7 @@ func remove_animation_track(args: Dictionary) -> Dictionary:
335
355
  % [
336
356
  animation_name,
337
357
  track_type,
338
- node_path if property.is_empty() else "%s:%s" % [node_path, property]
358
+ node_path if property.is_empty() else "%s:%s" % [node_path, property],
339
359
  ]
340
360
  )
341
361
  if held.is_empty():
@@ -353,7 +373,9 @@ func remove_animation_track(args: Dictionary) -> Dictionary:
353
373
  var timed: Array = _keys_at(anim, calling, times)
354
374
  if timed[1] != "":
355
375
  var on: String = node_path if property.is_empty() else "%s:%s" % [node_path, property]
356
- return scene.refuse("%s's %s track on %s %s" % [animation_name, track_type, on, timed[1]])
376
+ return scene.refuse(
377
+ "%s's %s track on %s %s" % [animation_name, track_type, on, timed[1]]
378
+ )
357
379
  calling = timed[0]
358
380
  var keys: int = 0
359
381
  var tracks_removed: int = 0
@@ -383,7 +405,7 @@ func remove_animation_track(args: Dictionary) -> Dictionary:
383
405
  if loaded.get_track_count() != left or _key_total(loaded) != keys_left:
384
406
  return "the track would still be in %s when the scene loads." % animation_name
385
407
  return ""
386
- var removed: Dictionary = {"type": track_type, "nodePath": node_path}
408
+ var removed: Dictionary = { "type": track_type, "nodePath": node_path }
387
409
  if not property.is_empty():
388
410
  removed["property"] = property
389
411
  if not method.is_empty():
@@ -432,8 +454,8 @@ static func _times_of(given: Variant) -> Variant:
432
454
 
433
455
 
434
456
  ## Of the keys [param calling] names, as [method _keys_calling] answers them, the ones at
435
- ## [param times], as [[[track, key]...], ""], or with the second the words refusing a time none is at.
436
- ## Removing a key took removing its whole track, so moving one key in time rewrote every other.
457
+ ## [param times], as [[[track, key]...], ""], or with the second the words refusing a time none is
458
+ ## at. Removing a key took removing its whole track, so moving one key in time rewrote every other.
437
459
  static func _keys_at(anim: Animation, calling: Array, times: Array) -> Array:
438
460
  var at: Array = []
439
461
  var held: PackedStringArray = []
@@ -457,7 +479,10 @@ static func _keys_at(anim: Animation, calling: Array, times: Array) -> Array:
457
479
  var _named: bool = named.append(str(given))
458
480
  return [
459
481
  at,
460
- "has no key at %s s; its keys are at %s s. Nothing was removed." % [", ".join(named), ", ".join(held)]
482
+ (
483
+ "has no key at %s s; its keys are at %s s. Nothing was removed."
484
+ % [", ".join(named), ", ".join(held)]
485
+ ),
461
486
  ]
462
487
 
463
488
 
@@ -483,7 +508,9 @@ static func _key_total(anim: Animation) -> int:
483
508
  static func _track_shape(anim: Animation, index: int) -> Array:
484
509
  var path: NodePath = anim.track_get_path(index)
485
510
  return [
486
- GdhAnimationReads.TRACK_TYPE_NAMES.get(anim.track_get_type(index), str(anim.track_get_type(index))),
511
+ GdhAnimationReads.TRACK_TYPE_NAMES.get(
512
+ anim.track_get_type(index), str(anim.track_get_type(index))
513
+ ),
487
514
  str(path.get_concatenated_names()),
488
515
  str(path.get_concatenated_subnames()),
489
516
  ]
@@ -508,7 +535,8 @@ func _keyframes(track: Dictionary) -> Array:
508
535
  var seconds: float = GdhRead.as_float(time)
509
536
  if times.has(seconds):
510
537
  return [
511
- false, "keyframes[%d] and [%d] are both at %s seconds." % [times[seconds], index, seconds]
538
+ false,
539
+ "keyframes[%d] and [%d] are both at %s seconds." % [times[seconds], index, seconds],
512
540
  ]
513
541
  times[seconds] = index
514
542
  return [true, keyframes]
@@ -530,7 +558,9 @@ func _key_values(target: Node, prop_name: String, keyframes: Array) -> Array:
530
558
  # A subname such as position:x keys one component, which is a number.
531
559
  var component: Variant = _properties.parse(raw, TYPE_FLOAT)
532
560
  if not component is float:
533
- return [false, "keyframes[%d].value keys %s, which is a number." % [index, prop_name]]
561
+ return [
562
+ false, "keyframes[%d].value keys %s, which is a number." % [index, prop_name],
563
+ ]
534
564
  values.append(component)
535
565
  continue
536
566
  var value: Array = _properties.value_for(target, declared, raw)
@@ -1,6 +1,5 @@
1
1
  @tool
2
2
  extends Node
3
-
4
3
  ## The global classes the running editor is holding.
5
4
  ##
6
5
  ## The editor fixes this list when it starts and adds to it only from a filesystem scan, and
@@ -51,10 +50,10 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
51
50
  func reload_script(args: Dictionary) -> Dictionary:
52
51
  var asked: String = str(args.get("scriptPath", ""))
53
52
  if asked.is_empty():
54
- return {"ok": false, "error": "scriptPath is required"}
55
- var path: String = _spelled_as_on_disk(asked)
53
+ return { "ok": false, "error": "scriptPath is required" }
54
+ var path: String = _spelled_as_on_disk(asked, {})
56
55
  if not ResourceLoader.exists(path):
57
- return {"ok": false, "error": "No script at " + path}
56
+ return { "ok": false, "error": "No script at " + path }
58
57
 
59
58
  # The copy the editor holds, when it holds one, and otherwise a load that reads the file, rather
60
59
  # than load(): a load recompiles a script whose base has been reloaded from the text it holds,
@@ -63,11 +62,13 @@ func reload_script(args: Dictionary) -> Dictionary:
63
62
  # the reload below compiled it clean.
64
63
  var cached: Resource = ResourceLoader.get_cached_ref(path)
65
64
  var loaded: Resource = (
66
- cached if cached != null else ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
65
+ cached
66
+ if cached != null
67
+ else ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
67
68
  )
68
69
  var script: GDScript = loaded as GDScript
69
70
  if script == null:
70
- return {"ok": false, "error": path + " is not a GDScript"}
71
+ return { "ok": false, "error": path + " is not a GDScript" }
71
72
 
72
73
  # What the copy had before anything touched it, which is the reading that shows the fault rather
73
74
  # than the one that mends it. Without it a caller cannot tell a copy that was already current
@@ -82,7 +83,10 @@ func reload_script(args: Dictionary) -> Dictionary:
82
83
  # method added since is still absent from the method list.
83
84
  var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
84
85
  if reader == null:
85
- return {"ok": false, "error": "Could not read " + path + ": " + str(FileAccess.get_open_error())}
86
+ return {
87
+ "ok": false,
88
+ "error": "Could not read " + path + ": " + str(FileAccess.get_open_error()),
89
+ }
86
90
  var previous: String = script.source_code
87
91
  var text: String = reader.get_as_text()
88
92
  reader.close()
@@ -90,11 +94,11 @@ func reload_script(args: Dictionary) -> Dictionary:
90
94
  # Godot keeps one parse of each script for other scripts to resolve its class through, and a
91
95
  # reload drops that parse only when the text it compiles differs from the text that was parsed.
92
96
  # A parse that failed because a class it names was not yet known has the file's own text, so it
93
- # outlived every reload: the class answered with all its methods while each script naming it read
94
- # "because of a parser error" until the editor restarted, for as long as anything held the parse,
95
- # such as a file open in an editor's language server. Compiling once with a line more makes the
96
- # texts differ, which drops that parse and those of the scripts that resolved through it; the
97
- # file's own text is compiled below.
97
+ # outlived every reload: the class answered with all its methods while each script naming it
98
+ # read "because of a parser error" until the editor restarted, for as long as anything held the
99
+ # parse, such as a file open in an editor's language server. Compiling once with a line more
100
+ # makes the texts differ, which drops that parse and those of the scripts that resolved through
101
+ # it; the file's own text is compiled below.
98
102
  script.source_code = text + "\n"
99
103
  var _dropped: Error = script.reload(true)
100
104
  script.source_code = text
@@ -103,11 +107,11 @@ func reload_script(args: Dictionary) -> Dictionary:
103
107
  # caller needs then: a failure travels back as its message alone, and a project met one and
104
108
  # could not tell whether the copy had been damaged by it or before it.
105
109
  #
106
- # And the old text is compiled again. A failed reload leaves the object unusable while its member
107
- # lists read as before, measured on 4.7.2: it can make no instance, and an instance it already made
108
- # has lost its methods, and the old text put back uncompiled leaves it that way. Compiling the text
109
- # it was built from puts it back, and whether that worked is said, since the text may no longer
110
- # compile against what it names.
110
+ # And the old text is compiled again. A failed reload leaves the object unusable while its
111
+ # member lists read as before, measured on 4.7.2: it can make no instance, and an instance it
112
+ # already made has lost its methods, and the old text put back uncompiled leaves it that way.
113
+ # Compiling the text it was built from puts it back, and whether that worked is said, since the
114
+ # text may no longer compile against what it names.
111
115
  var failed: Error = script.reload(true)
112
116
  var answer: Dictionary = {
113
117
  "ok": true,
@@ -129,8 +133,8 @@ func reload_script(args: Dictionary) -> Dictionary:
129
133
  return answer
130
134
 
131
135
 
132
- ## Which of [code]scriptPaths[/code] the editor holds a compiled copy of whose text is not the file's,
133
- ## without loading or reloading anything. A script compiled against such a copy fails on the
136
+ ## Which of [code]scriptPaths[/code] the editor holds a compiled copy of whose text is not the
137
+ ## file's, without loading or reloading anything. A script compiled against such a copy fails on the
134
138
  ## members the file has gained, and a reload of it fails the same way until the copy is reloaded
135
139
  ## first; the rescan that met this blamed the new classes the script also named, which were fine.
136
140
  ##
@@ -140,15 +144,16 @@ func reload_script(args: Dictionary) -> Dictionary:
140
144
  func scripts_behind(args: Dictionary) -> Dictionary:
141
145
  var given: Variant = args.get("scriptPaths", [])
142
146
  if not given is Array:
143
- return {"ok": false, "error": "scriptPaths must be a list of res:// paths"}
147
+ return { "ok": false, "error": "scriptPaths must be a list of res:// paths" }
144
148
  var asked: Array = given
145
149
  var unsaved: PackedStringArray = []
146
150
  if Engine.is_editor_hint():
147
151
  unsaved = EditorInterface.get_script_editor().get_unsaved_files()
148
152
  var behind: Array[String] = []
149
153
  var edited: Array[String] = []
154
+ var listings: Dictionary = {}
150
155
  for entry: Variant in asked:
151
- var path: String = _spelled_as_on_disk(str(entry))
156
+ var path: String = _spelled_as_on_disk(str(entry), listings)
152
157
  var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
153
158
  if script == null:
154
159
  continue
@@ -163,13 +168,13 @@ func scripts_behind(args: Dictionary) -> Dictionary:
163
168
  edited.append(path)
164
169
  else:
165
170
  behind.append(path)
166
- return {"ok": true, "behind": behind, "unsavedInEditor": edited}
171
+ return { "ok": true, "behind": behind, "unsavedInEditor": edited }
167
172
 
168
173
 
169
- ## Has the script editor's tabs reread what changed on disk, as the editor does when its window takes
170
- ## the focus, which a hidden editor's never does. A tab with unsaved edits is left alone and named:
171
- ## saving it writes its text over the file, and rereading would ask a question nobody can answer in a
172
- ## hidden editor's dialog.
174
+ ## Has the script editor's tabs reread what changed on disk, as the editor does when its window
175
+ ## takes the focus, which a hidden editor's never does. A tab with unsaved edits is left alone and
176
+ ## named: saving it writes its text over the file, and rereading would ask a question nobody can
177
+ ## answer in a hidden editor's dialog.
173
178
  func _reread_open_tabs(path: String, answer: Dictionary) -> void:
174
179
  # The engine fixtures reload from a script run outside any editor, where there are no tabs and
175
180
  # EditorInterface has none of its methods.
@@ -192,28 +197,46 @@ func _reread_open_tabs(path: String, answer: Dictionary) -> void:
192
197
  ## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
193
198
  ## another spelling misses that copy and compiles a new one: reloading that answers with a fresh
194
199
  ## compile's members and leaves the copy the editor holds as it was.
195
- static func _spelled_as_on_disk(path: String) -> String:
200
+ ##
201
+ ## [param listings] keeps each directory's entries for the length of one call that spells many
202
+ ## paths. Listed again for every path, a check of the 416 classes of one project listed a folder of
203
+ ## eight hundred files twice per class, and held an editor's main thread for close to a minute on a
204
+ ## CI runner, with every request behind it waiting.
205
+ static func _spelled_as_on_disk(path: String, listings: Dictionary) -> String:
196
206
  var local: String = ProjectSettings.localize_path(path)
197
207
  if not local.begins_with("res://"):
198
208
  return local
199
209
  var spelled: String = "res://"
200
210
  for step: String in local.trim_prefix("res://").split("/", false):
201
- var listing: DirAccess = DirAccess.open(spelled)
202
- if listing == null:
211
+ if not listings.has(spelled):
212
+ listings[spelled] = _entries_by_spelling(spelled)
213
+ var entries: Variant = listings[spelled]
214
+ if entries == null:
203
215
  return local
204
- var found: String = ""
205
- for entry: String in listing.get_directories() + listing.get_files():
206
- if entry == step:
207
- found = entry
208
- break
209
- if found.is_empty() and entry.nocasecmp_to(step) == 0:
210
- found = entry
216
+ var exact: Dictionary = entries[0]
217
+ var folded: Dictionary = entries[1]
218
+ var found: String = step if exact.has(step) else str(folded.get(step.to_lower(), ""))
211
219
  if found.is_empty():
212
220
  return local
213
221
  spelled = spelled.path_join(found)
214
222
  return spelled
215
223
 
216
224
 
225
+ ## The entries of [param directory] by their own spelling and by their spelling in lower case, the
226
+ ## first of those that fold together winning, or null for a directory that cannot be opened.
227
+ static func _entries_by_spelling(directory: String) -> Variant:
228
+ var listing: DirAccess = DirAccess.open(directory)
229
+ if listing == null:
230
+ return null
231
+ var exact: Dictionary = {}
232
+ var folded: Dictionary = {}
233
+ for entry: String in listing.get_directories() + listing.get_files():
234
+ exact[entry] = true
235
+ if not folded.has(entry.to_lower()):
236
+ folded[entry.to_lower()] = entry
237
+ return [exact, folded]
238
+
239
+
217
240
  func _method_names(script: GDScript) -> Array[String]:
218
241
  var names: Array[String] = []
219
242
  for entry: Dictionary in script.get_script_method_list():
@@ -223,11 +246,11 @@ func _method_names(script: GDScript) -> Array[String]:
223
246
 
224
247
 
225
248
  ## The script's constants, those of the scripts it extends included as its methods include theirs,
226
- ## and the keys of each one that is a Dictionary as `Kind.SHORT`, which is how the copy holds a named
227
- ## enum: a stale enum member is denied as a member of the enum rather than of the class. A constant
228
- ## Dictionary that is not an enum is listed the same way, since the map does not tell the two apart.
229
- ## The engine's map holds only the script's own, so a script extending a class_name listed every
230
- ## method it inherited and none of the constants.
249
+ ## and the keys of each one that is a Dictionary as `Kind.SHORT`, which is how the copy holds a
250
+ ## named enum: a stale enum member is denied as a member of the enum rather than of the class. A
251
+ ## constant Dictionary that is not an enum is listed the same way, since the map does not tell the
252
+ ## two apart. The engine's map holds only the script's own, so a script extending a class_name
253
+ ## listed every method it inherited and none of the constants.
231
254
  func _constant_names(script: GDScript) -> Array[String]:
232
255
  var names: Array[String] = []
233
256
  var holder: Script = script
@@ -250,7 +273,7 @@ func _constant_names(script: GDScript) -> Array[String]:
250
273
 
251
274
  func global_classes(_args: Dictionary) -> Dictionary:
252
275
  if not _editor_plugin:
253
- return {"ok": false, "error": "Editor plugin unavailable"}
276
+ return { "ok": false, "error": "Editor plugin unavailable" }
254
277
 
255
278
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
256
279
  var names: Array[String] = []
@@ -1,6 +1,5 @@
1
1
  @tool
2
2
  extends Node
3
-
4
3
  ## The editor's view of the files on disk: scans, the reimports asked for, where each stands, and
5
4
  ## files moved under it.
6
5
 
@@ -8,20 +7,17 @@ const GdhRead = preload("../reading.gd")
8
7
  const GdhSceneFile = preload("../scene_file.gd")
9
8
 
10
9
  var _editor_plugin: EditorPlugin = null
11
-
12
10
  ## How many scans the editor has completed, counted off `sources_changed`, which it emits at the end
13
11
  ## of every scan once the reimport the scan found is done: the one moment a scan is over. Neither
14
12
  ## flag it offers says so. A threaded scan stops reporting itself when its thread finishes, and its
15
13
  ## import runs a frame or more later when the thread is joined; an editor in the background took
16
14
  ## long enough over that frame that a rescan answered before the import of 211 changed scenes began.
17
15
  var _scans_completed: int = 0
18
-
19
16
  ## When the last scan completed, on the engine's clock, or -1 before the first. The editor writes
20
17
  ## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
21
18
  ## in between reads a file being rewritten; how long ago the scan ended is what tells a caller
22
19
  ## whether that write can still be coming.
23
20
  var _scan_finished_at_msec: int = -1
24
-
25
21
  ## How many reimports asked for through [method reimport_files] have finished. Counted here rather
26
22
  ## than off `resources_reimported`, which a scan's own import emits as well.
27
23
  var _reimports_completed: int = 0
@@ -53,7 +49,7 @@ func _scan_completed(_sources_exist: bool) -> void:
53
49
  ## another scan.
54
50
  func rescan_filesystem(args: Dictionary) -> Dictionary:
55
51
  if not _editor_plugin:
56
- return {"ok": false, "error": "Editor plugin unavailable"}
52
+ return { "ok": false, "error": "Editor plugin unavailable" }
57
53
 
58
54
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
59
55
  # Not over a scan or an import the editor is already running: a scan asked for then starts a
@@ -62,13 +58,13 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
62
58
  var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
63
59
  var started: bool = false
64
60
  var completed_before: int = _scans_completed
65
- var uids: Dictionary = {"reread": [], "duplicated": []}
61
+ var uids: Dictionary = { "reread": [], "duplicated": [] }
66
62
  if not GdhRead.as_bool(args.get("statusOnly", false)) and not busy:
67
63
  uids = _reread_changed_uids(filesystem)
68
- # Files the caller has just written, moved or deleted, read again one by one. The scan finds a
69
- # script that appeared and leaves the global class of one that has gone in the editor's list,
70
- # which it then writes back into the class cache at a path that is not there; reading the gone
71
- # path again is what drops it.
64
+ # Files the caller has just written, moved or deleted, read again one by one. The scan finds
65
+ # a script that appeared and leaves the global class of one that has gone in the editor's
66
+ # list, which it then writes back into the class cache at a path that is not there; reading
67
+ # the gone path again is what drops it.
72
68
  var named: Variant = args.get("updateFiles", [])
73
69
  if named is Array:
74
70
  var paths: Array = named
@@ -137,14 +133,14 @@ func _reread_changed_uids(filesystem: EditorFileSystem) -> Dictionary:
137
133
  reread.append(path)
138
134
  reread.sort()
139
135
  duplicated.sort()
140
- return {"reread": reread, "duplicated": duplicated}
136
+ return { "reread": reread, "duplicated": duplicated }
141
137
 
142
138
 
143
139
  ## Every `.uid` under [param directory], skipping what the editor skips: hidden directories, any
144
140
  ## holding a `.gdignore`, and any below the project's own that holds a `project.godot` of its own,
145
- ## which the editor passes over as another project. A nested project's UIDs are never in the editor's
146
- ## table, so without that last one its files would be read again on every rescan, and one holding a
147
- ## copy of an outer file's `.uid` named as a duplicate on disk.
141
+ ## which the editor passes over as another project. A nested project's UIDs are never in the
142
+ ## editor's table, so without that last one its files would be read again on every rescan, and one
143
+ ## holding a copy of an outer file's `.uid` named as a duplicate on disk.
148
144
  func _uid_files(directory: String, found: Array[String]) -> void:
149
145
  var listing: DirAccess = DirAccess.open(directory)
150
146
  if listing == null or FileAccess.file_exists(directory.path_join(".gdignore")):
@@ -166,12 +162,13 @@ func _uid_files(directory: String, found: Array[String]) -> void:
166
162
  ## Apart from `rescan_filesystem` because asking must not start or count anything.
167
163
  func scan_status(_args: Dictionary) -> Dictionary:
168
164
  if not _editor_plugin:
169
- return {"ok": false, "error": "Editor plugin unavailable"}
165
+ return { "ok": false, "error": "Editor plugin unavailable" }
170
166
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
171
- # The scenes open in tabs, the one shown and the ones holding unsaved edits: a scene edit on disk
172
- # meets what the editor holds of it, and nothing said which scenes those were. A tab of a scene
173
- # never saved has no path, and a new project's editor opens with an empty one, so it is left out
174
- # of the open; one with edits is kept among the unsaved, as the edits a close would lose.
167
+ # The scenes open in tabs, the one shown and the ones holding unsaved edits: a scene edit on
168
+ # disk meets what the editor holds of it, and nothing said which scenes those were. A tab of a
169
+ # scene never saved has no path, and a new project's editor opens with an empty one, so it is
170
+ # left out of the open; one with edits is kept among the unsaved, as the edits a close would
171
+ # lose.
175
172
  var shown: Node = EditorInterface.get_edited_scene_root()
176
173
  var open: Array[String] = []
177
174
  open.assign(Array(EditorInterface.get_open_scenes()).filter(_has_path))
@@ -204,14 +201,14 @@ func _has_path(path: String) -> bool:
204
201
  ## while the editor scans or imports, since `reimport_files` refuses to run inside an import.
205
202
  func reimport_files(args: Dictionary) -> Dictionary:
206
203
  if not _editor_plugin:
207
- return {"ok": false, "error": "Editor plugin unavailable"}
204
+ return { "ok": false, "error": "Editor plugin unavailable" }
208
205
  var paths: Array[String] = []
209
206
  var given: Variant = args.get("paths", [])
210
207
  if given is Array:
211
208
  for path: Variant in given:
212
209
  paths.append(str(path))
213
210
  if paths.is_empty():
214
- return {"ok": false, "error": "reimport_files needs paths"}
211
+ return { "ok": false, "error": "reimport_files needs paths" }
215
212
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
216
213
  var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
217
214
  if not busy:
@@ -282,7 +279,12 @@ func files_moved(args: Dictionary) -> Dictionary:
282
279
  reloaded.append(scene)
283
280
  reloaded.sort()
284
281
  unsaved.sort()
285
- return {"ok": true, "retargetedTabs": retargeted, "reloadedScenes": reloaded, "unsavedScenes": unsaved}
282
+ return {
283
+ "ok": true,
284
+ "retargetedTabs": retargeted,
285
+ "reloadedScenes": reloaded,
286
+ "unsavedScenes": unsaved,
287
+ }
286
288
 
287
289
 
288
290
  func _reimport(paths: PackedStringArray) -> void: