gdharness 1.2.30 → 1.2.32
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.
- package/build/cli.js +7197 -6159
- package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
- package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
- package/build/godot/operations/audio_buses.gd +14 -1
- package/build/godot/operations/class_cache.gd +146 -35
- package/build/godot/operations/classdb_queries.gd +19 -12
- package/build/godot/operations/file_reads.gd +237 -0
- package/build/godot/operations/gdscript_authoring.gd +136 -19
- package/build/godot/operations/godot_operations.gd +24 -0
- package/build/godot/operations/project_config.gd +16 -6
- package/build/godot/operations/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +4896 -3872
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -4,6 +4,9 @@ const Read = preload("reading.gd")
|
|
|
4
4
|
const Log = preload("logger.gd")
|
|
5
5
|
const Patterns = preload("patterns.gd")
|
|
6
6
|
|
|
7
|
+
const MODIFICATION_TYPES: Array[String] = ["add_constant", "add_variable", "add_signal", "add_function"]
|
|
8
|
+
const BLANKS_AROUND_FUNCTIONS: int = 2
|
|
9
|
+
|
|
7
10
|
var _log: Log
|
|
8
11
|
|
|
9
12
|
|
|
@@ -49,7 +52,9 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
49
52
|
return _log.failure("Script path must end with .gd extension")
|
|
50
53
|
|
|
51
54
|
if FileAccess.file_exists(full_script_path):
|
|
52
|
-
return _log.failure(
|
|
55
|
+
return _log.failure(
|
|
56
|
+
"Script file already exists: " + full_script_path + ". script_edit modify adds to it."
|
|
57
|
+
)
|
|
53
58
|
|
|
54
59
|
var dir: DirAccess = DirAccess.open("res://")
|
|
55
60
|
var script_dir: String = full_script_path.get_base_dir()
|
|
@@ -115,14 +120,14 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
115
120
|
var original_content: String = file.get_as_text()
|
|
116
121
|
file.close()
|
|
117
122
|
|
|
123
|
+
var lines: Array[String] = []
|
|
124
|
+
lines.assign(original_content.split("\n"))
|
|
125
|
+
|
|
118
126
|
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
119
127
|
# missing name used to leave the rest written and the answer saying success over an empty list.
|
|
120
|
-
var problems: Array[String] = _modification_problems(modifications)
|
|
128
|
+
var problems: Array[String] = _modification_problems(modifications, _declared(lines))
|
|
121
129
|
if not problems.is_empty():
|
|
122
130
|
return _log.failure("Nothing was changed: " + "; ".join(problems))
|
|
123
|
-
|
|
124
|
-
var lines: Array[String] = []
|
|
125
|
-
lines.assign(original_content.split("\n"))
|
|
126
131
|
# Each placed line as a zero-based index that moves down whenever a later insertion lands above
|
|
127
132
|
# it, so the numbers answered are where the lines are in the file written, not where each went
|
|
128
133
|
# at the moment it was placed.
|
|
@@ -133,6 +138,8 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
133
138
|
var mod_type: String = str(fields.get("type", ""))
|
|
134
139
|
var at: int = -1
|
|
135
140
|
match mod_type:
|
|
141
|
+
"add_constant":
|
|
142
|
+
at = _add_constant(lines, fields, placed)
|
|
136
143
|
"add_variable":
|
|
137
144
|
at = _add_variable(lines, fields, placed)
|
|
138
145
|
"add_signal":
|
|
@@ -170,10 +177,15 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
170
177
|
|
|
171
178
|
|
|
172
179
|
# Every reason [param modifications] cannot be made as asked, each naming the entry by position.
|
|
173
|
-
|
|
180
|
+
#
|
|
181
|
+
# A name the script already declares is one of them, as is one the call adds twice. GDScript gives a
|
|
182
|
+
# class one namespace for its members, so a second `maximum` does not parse; and the engine reports
|
|
183
|
+
# only the first error it meets, so a second `func damage` behind it was written with nothing said.
|
|
184
|
+
static func _modification_problems(modifications: Array, declared: Dictionary) -> Array[String]:
|
|
174
185
|
var problems: Array[String] = []
|
|
175
186
|
if modifications.is_empty():
|
|
176
187
|
problems.append("modifications is empty")
|
|
188
|
+
var added: Dictionary = {}
|
|
177
189
|
for index: int in range(modifications.size()):
|
|
178
190
|
var mod: Variant = modifications[index]
|
|
179
191
|
var which: String = "modification " + str(index + 1)
|
|
@@ -183,17 +195,55 @@ static func _modification_problems(modifications: Array) -> Array[String]:
|
|
|
183
195
|
var fields: Dictionary = mod
|
|
184
196
|
var mod_type: String = str(fields.get("type", ""))
|
|
185
197
|
var mod_name: String = str(fields.get("name", ""))
|
|
186
|
-
if mod_type not in
|
|
198
|
+
if mod_type not in MODIFICATION_TYPES:
|
|
187
199
|
problems.append(
|
|
188
|
-
|
|
200
|
+
(
|
|
201
|
+
which
|
|
202
|
+
+ " has type '"
|
|
203
|
+
+ mod_type
|
|
204
|
+
+ "', not add_constant, add_variable, add_signal or add_function"
|
|
205
|
+
)
|
|
189
206
|
)
|
|
207
|
+
if mod_type == "add_constant" and str(fields.get("defaultValue", "")).is_empty():
|
|
208
|
+
problems.append(which + " is a constant with no defaultValue, and a constant needs a value")
|
|
190
209
|
if mod_name.is_empty():
|
|
191
210
|
problems.append(which + " has no name")
|
|
192
|
-
|
|
211
|
+
continue
|
|
212
|
+
if not mod_name.is_valid_unicode_identifier():
|
|
193
213
|
problems.append(which + " is named '" + mod_name + "', which GDScript does not accept as a name")
|
|
214
|
+
continue
|
|
215
|
+
if declared.has(mod_name):
|
|
216
|
+
var existing: Dictionary = declared[mod_name]
|
|
217
|
+
problems.append(
|
|
218
|
+
(
|
|
219
|
+
"%s adds %s, and the script already declares %s %s on line %d"
|
|
220
|
+
% [which, mod_name, existing["kind"], mod_name, existing["line"]]
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
elif added.has(mod_name):
|
|
224
|
+
problems.append("%s adds %s, which modification %d adds too" % [which, mod_name, added[mod_name]])
|
|
225
|
+
else:
|
|
226
|
+
added[mod_name] = index + 1
|
|
194
227
|
return problems
|
|
195
228
|
|
|
196
229
|
|
|
230
|
+
# The members [param lines] declares at the top level, by name, as {"kind", "line"} counted from
|
|
231
|
+
# one: variables, constants, signals, functions, enums and inner classes, which share one namespace.
|
|
232
|
+
static func _declared(lines: Array[String]) -> Dictionary:
|
|
233
|
+
var found: Dictionary = {}
|
|
234
|
+
var declaration: RegEx = Patterns.compiled(
|
|
235
|
+
"^(?:static\\s+)?(var|const|signal|func|enum|class)\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
236
|
+
)
|
|
237
|
+
for i: int in range(lines.size()):
|
|
238
|
+
var raw: String = lines[i]
|
|
239
|
+
if raw.is_empty() or raw[0] == "\t" or raw[0] == " ":
|
|
240
|
+
continue
|
|
241
|
+
var matched: RegExMatch = declaration.search(Patterns.without_annotations(raw))
|
|
242
|
+
if matched != null and not found.has(matched.get_string(2)):
|
|
243
|
+
found[matched.get_string(2)] = {"kind": matched.get_string(1), "line": i + 1}
|
|
244
|
+
return found
|
|
245
|
+
|
|
246
|
+
|
|
197
247
|
# Inserts [param new_lines] at [param at] and moves every placed line at or below it down by as many.
|
|
198
248
|
static func _insert(
|
|
199
249
|
lines: Array[String], at: int, new_lines: Array[String], placed: Array[Dictionary]
|
|
@@ -226,6 +276,35 @@ static func _base_named(script: Script) -> String:
|
|
|
226
276
|
return named if not named.is_empty() else base.resource_path
|
|
227
277
|
|
|
228
278
|
|
|
279
|
+
# A constant, typed as a variable is and placed after the header, the signals and the constants
|
|
280
|
+
# already there, which is where the style guide puts them. Its zero-based index, or -1.
|
|
281
|
+
func _add_constant(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
282
|
+
var value: String = str(mod.get("defaultValue", ""))
|
|
283
|
+
var declared_type: String = str(mod.get("varType", ""))
|
|
284
|
+
if declared_type.is_empty():
|
|
285
|
+
declared_type = _type_of_literal(value)
|
|
286
|
+
var const_line: String = "const %s: %s = %s" % [str(mod.get("name", "")), declared_type, value]
|
|
287
|
+
var after: int = 0
|
|
288
|
+
for i: int in range(lines.size()):
|
|
289
|
+
var line: String = Patterns.without_annotations(lines[i])
|
|
290
|
+
if (
|
|
291
|
+
line.begins_with("extends ")
|
|
292
|
+
or line.begins_with("class_name ")
|
|
293
|
+
or line.begins_with("signal ")
|
|
294
|
+
or line.begins_with("const ")
|
|
295
|
+
):
|
|
296
|
+
after = i + 1
|
|
297
|
+
elif (
|
|
298
|
+
line.begins_with("var ")
|
|
299
|
+
or line.begins_with("func ")
|
|
300
|
+
or line.begins_with("static ")
|
|
301
|
+
or line.begins_with("enum ")
|
|
302
|
+
or line.begins_with("class ")
|
|
303
|
+
):
|
|
304
|
+
break
|
|
305
|
+
return after if _insert(lines, after, [const_line], placed) else -1
|
|
306
|
+
|
|
307
|
+
|
|
229
308
|
# Inserts the declaration and answers with its zero-based index, or -1 for one that could not be placed.
|
|
230
309
|
func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
231
310
|
var var_name: String = str(mod.get("name", ""))
|
|
@@ -293,7 +372,12 @@ func _add_signal(lines: Array[String], mod: Dictionary, placed: Array[Dictionary
|
|
|
293
372
|
return insert_line if _insert(lines, insert_line, [signal_line], placed) else -1
|
|
294
373
|
|
|
295
374
|
|
|
296
|
-
# Answers with the zero-based index of the `func` line, below the blank
|
|
375
|
+
# Answers with the zero-based index of the `func` line, below the blank lines that separate it.
|
|
376
|
+
#
|
|
377
|
+
# Two blank lines on each side, counting those already there, because that is the spacing GDScript's
|
|
378
|
+
# style guide gives functions and the one gdformat writes and gdlint checks: one blank line passed
|
|
379
|
+
# the engine and failed every project gating on either. A function last in the file ends it with a
|
|
380
|
+
# newline, which the file is written with only when its last line is empty.
|
|
297
381
|
func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictionary]) -> int:
|
|
298
382
|
var func_name: String = str(mod.get("name", ""))
|
|
299
383
|
var func_params: String = str(mod.get("params", ""))
|
|
@@ -305,21 +389,34 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
305
389
|
var body: String = str(mod.get("body", "pass"))
|
|
306
390
|
var position: String = str(mod.get("position", "end"))
|
|
307
391
|
|
|
392
|
+
var insert_line: int = _function_insertion_point(lines, position)
|
|
308
393
|
var func_lines: Array[String] = []
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
func_lines.
|
|
394
|
+
for i: int in range(_blank_run(lines, insert_line - 1, -1), BLANKS_AROUND_FUNCTIONS):
|
|
395
|
+
func_lines.append("")
|
|
396
|
+
var declared_at: int = insert_line + func_lines.size()
|
|
397
|
+
func_lines.append("func " + func_name + "(" + func_params + ") -> " + return_type + ":")
|
|
312
398
|
|
|
313
399
|
for bl: String in body.split("\n"):
|
|
314
400
|
func_lines.append("\t" + bl)
|
|
315
401
|
|
|
316
|
-
var
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
402
|
+
var below: int = _blank_run(lines, insert_line, 1)
|
|
403
|
+
if insert_line + below < lines.size():
|
|
404
|
+
for i: int in range(below, BLANKS_AROUND_FUNCTIONS):
|
|
405
|
+
func_lines.append("")
|
|
406
|
+
elif below == 0:
|
|
320
407
|
func_lines.append("")
|
|
321
408
|
|
|
322
|
-
return
|
|
409
|
+
return declared_at if _insert(lines, insert_line, func_lines, placed) else -1
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# How many blank lines run from [param from] on in the direction of [param step], 1 or -1.
|
|
413
|
+
static func _blank_run(lines: Array[String], from: int, step: int) -> int:
|
|
414
|
+
var count: int = 0
|
|
415
|
+
var at: int = from
|
|
416
|
+
while at >= 0 and at < lines.size() and lines[at].strip_edges().is_empty():
|
|
417
|
+
count += 1
|
|
418
|
+
at += step
|
|
419
|
+
return count
|
|
323
420
|
|
|
324
421
|
|
|
325
422
|
func _variable_insertion_point(lines: Array[String]) -> int:
|
|
@@ -389,10 +486,30 @@ func _line_after_function(lines: Array[String], prefix: String) -> int:
|
|
|
389
486
|
if line.begins_with(prefix):
|
|
390
487
|
inside = true
|
|
391
488
|
elif inside and (line.begins_with("func ") or line.begins_with("static func ")):
|
|
392
|
-
return i
|
|
489
|
+
return _start_of_declaration(lines, i)
|
|
393
490
|
return lines.size()
|
|
394
491
|
|
|
395
492
|
|
|
493
|
+
# The first line belonging to the declaration on [param at]: the comments and the lines holding only
|
|
494
|
+
# annotations directly above it, at its indentation. A function placed on [param at] itself takes the
|
|
495
|
+
# next function's doc comment, which then documents the wrong one, and its `@rpc` line, which then
|
|
496
|
+
# annotates it. A comment indented further is the end of the body above, and stays with it.
|
|
497
|
+
static func _start_of_declaration(lines: Array[String], at: int) -> int:
|
|
498
|
+
var indent: String = _indentation(lines[at])
|
|
499
|
+
var start: int = at
|
|
500
|
+
while start > 0 and _indentation(lines[start - 1]) == indent:
|
|
501
|
+
var above: String = lines[start - 1].strip_edges()
|
|
502
|
+
var annotation_only: bool = above.begins_with("@") and Patterns.without_annotations(above).is_empty()
|
|
503
|
+
if not above.begins_with("#") and not annotation_only:
|
|
504
|
+
break
|
|
505
|
+
start -= 1
|
|
506
|
+
return start
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
static func _indentation(line: String) -> String:
|
|
510
|
+
return line.substr(0, line.length() - line.strip_edges(true, false).length())
|
|
511
|
+
|
|
512
|
+
|
|
396
513
|
func _script_template(template_name: String) -> String:
|
|
397
514
|
match template_name:
|
|
398
515
|
"singleton":
|
|
@@ -18,6 +18,7 @@ const AudioBuses = preload("audio_buses.gd")
|
|
|
18
18
|
const ClassCache = preload("class_cache.gd")
|
|
19
19
|
const ClassDbQueries = preload("classdb_queries.gd")
|
|
20
20
|
const Dependencies = preload("dependencies.gd")
|
|
21
|
+
const FileReads = preload("file_reads.gd")
|
|
21
22
|
const GdscriptAnalysis = preload("gdscript_analysis.gd")
|
|
22
23
|
const GdscriptAuthoring = preload("gdscript_authoring.gd")
|
|
23
24
|
const ImportPipeline = preload("import_pipeline.gd")
|
|
@@ -26,6 +27,7 @@ const Log = preload("logger.gd")
|
|
|
26
27
|
const Plugins = preload("plugins.gd")
|
|
27
28
|
const ProjectConfig = preload("project_config.gd")
|
|
28
29
|
const ProjectDiagnostics = preload("project_diagnostics.gd")
|
|
30
|
+
const Read = preload("reading.gd")
|
|
29
31
|
const RenameChecks = preload("rename_checks.gd")
|
|
30
32
|
const ResourceFiles = preload("resource_files.gd")
|
|
31
33
|
|
|
@@ -137,6 +139,20 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
137
139
|
payload = ResourceFiles.new(_log).get_uid(params)
|
|
138
140
|
"refresh_class_cache":
|
|
139
141
|
payload = ClassCache.new(_log).refresh_class_cache(params)
|
|
142
|
+
"read_scene_tree":
|
|
143
|
+
payload = _read(
|
|
144
|
+
FileReads.scene_tree(
|
|
145
|
+
str(params.get("scene_path", "")),
|
|
146
|
+
Read.as_int(params.get("depth", -1), -1),
|
|
147
|
+
Read.as_bool(params.get("include_properties", false))
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
"read_resource":
|
|
151
|
+
payload = _read(
|
|
152
|
+
FileReads.resource(
|
|
153
|
+
str(params.get("resource_path", "")), Read.as_bool(params.get("include_defaults", false))
|
|
154
|
+
)
|
|
155
|
+
)
|
|
140
156
|
|
|
141
157
|
# Import and export pipeline
|
|
142
158
|
"get_import_status":
|
|
@@ -220,3 +236,11 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
220
236
|
_log.error("Unknown operation: " + operation)
|
|
221
237
|
|
|
222
238
|
return payload
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## A read the editor answers too, in the shape an operation answers: its refusal as the run's error.
|
|
242
|
+
func _read(read: Dictionary) -> Dictionary:
|
|
243
|
+
if not read.get("ok", false):
|
|
244
|
+
return _log.failure(str(read.get("error", "the read failed")))
|
|
245
|
+
var _answered: bool = read.erase("ok")
|
|
246
|
+
return read
|
|
@@ -117,12 +117,7 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
117
117
|
var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
|
|
118
118
|
if wanted > TYPE_NIL and final_value != null and typeof(final_value) != wanted:
|
|
119
119
|
if not _converts_faithfully(final_value, wanted):
|
|
120
|
-
return _log.failure(
|
|
121
|
-
(
|
|
122
|
-
"%s is declared as %s, and %s (%s) cannot be written as that without losing something."
|
|
123
|
-
% [setting_path, type_string(wanted), str(final_value), type_string(typeof(final_value))]
|
|
124
|
-
)
|
|
125
|
-
)
|
|
120
|
+
return _log.failure(_cannot_hold(setting_path, wanted, final_value))
|
|
126
121
|
final_value = type_convert(final_value, wanted)
|
|
127
122
|
|
|
128
123
|
var answer: Dictionary = {
|
|
@@ -174,6 +169,21 @@ func _converts_faithfully(value: Variant, to: int) -> bool:
|
|
|
174
169
|
return type_convert(type_convert(value, to), typeof(value)) == value
|
|
175
170
|
|
|
176
171
|
|
|
172
|
+
# Why [param value] cannot be written to a setting declared as [param wanted], in the words the
|
|
173
|
+
# editor's property writes use for the same two cases. One sentence for both read "without losing
|
|
174
|
+
# something" about a word given for a number, where nothing would be lost: it cannot become one.
|
|
175
|
+
static func _cannot_hold(setting_path: String, wanted: int, value: Variant) -> String:
|
|
176
|
+
var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
|
|
177
|
+
var declared: String = "%s is declared as %s" % [setting_path, type_string(wanted)]
|
|
178
|
+
var cost: String = " Setting it would save a different value than the one asked for."
|
|
179
|
+
if wanted == TYPE_INT and typeof(value) == TYPE_FLOAT:
|
|
180
|
+
return "%s, which holds whole numbers, and %s would be cut to fit.%s" % [declared, str(value), cost]
|
|
181
|
+
if wanted in numbers and typeof(value) in numbers:
|
|
182
|
+
return "%s, and %s is not the same number as one.%s" % [declared, str(value), cost]
|
|
183
|
+
var given: String = type_string(typeof(value))
|
|
184
|
+
return "%s, and %s (%s) cannot become one." % [declared, JSON.stringify(value), given]
|
|
185
|
+
|
|
186
|
+
|
|
177
187
|
# Add an autoload singleton
|
|
178
188
|
func add_autoload(params: Dictionary) -> Dictionary:
|
|
179
189
|
var name: String = str(params.get("name", ""))
|