gdharness 1.2.31 → 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 +7212 -6230
- 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/scene_file.gd +38 -1
- 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 +4 -41
- 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 +54 -120
- 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/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/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/index.js +5437 -4469
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Scenes and resources read from their files, by the editor's tools and by a headless engine with
|
|
4
|
+
## none connected, so a read answers the same either way.
|
|
5
|
+
##
|
|
6
|
+
## A scene is read off its SceneState rather than instantiated. Instantiating outside the editor
|
|
7
|
+
## runs every script's member initialisers and _init (measured on 4.7.2, with or without -e), so a
|
|
8
|
+
## read with no editor would have run the game's code on every node; the editor gets placeholders
|
|
9
|
+
## and runs none. The state holds everything the file says, and the scenes it instances theirs,
|
|
10
|
+
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
|
+
## initialisers run, and a resource with a script is made with it.
|
|
12
|
+
|
|
13
|
+
const PropertyValues = preload("property_values.gd")
|
|
14
|
+
const Read = preload("reading.gd")
|
|
15
|
+
const Serialisation = preload("serialisation.gd")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf on a node that
|
|
19
|
+
## instances another scene and every stored property when [param include_properties] is set, the
|
|
20
|
+
## file's value or the default where it sets none. [param depth] -1 reads every level.
|
|
21
|
+
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
if not FileAccess.file_exists(scene_path):
|
|
23
|
+
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
|
+
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
25
|
+
# refusal can say so, where asking for a PackedScene printed the loader's error and a backtrace.
|
|
26
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
27
|
+
if loaded == null:
|
|
28
|
+
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
29
|
+
if not loaded is PackedScene:
|
|
30
|
+
return {
|
|
31
|
+
"ok": false,
|
|
32
|
+
"error":
|
|
33
|
+
(
|
|
34
|
+
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
35
|
+
% [scene_path, loaded.get_class()]
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
var packed: PackedScene = loaded
|
|
39
|
+
var nodes: Dictionary = {}
|
|
40
|
+
var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path])
|
|
41
|
+
if not refused.is_empty():
|
|
42
|
+
return {"ok": false, "error": refused}
|
|
43
|
+
if not nodes.has("."):
|
|
44
|
+
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
49
|
+
## from the outermost root, each instanced scene laid where it is instanced and the instancing
|
|
50
|
+
## file's own entries for it applied over it. Answers a refusal, or "".
|
|
51
|
+
##
|
|
52
|
+
## [param within] is the files being laid, outermost first, so a scene that instances one of them
|
|
53
|
+
## is refused rather than laid forever; the engine refuses to load such a scene, so this is only
|
|
54
|
+
## reached through a state built some other way.
|
|
55
|
+
static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: Array) -> String:
|
|
56
|
+
var base: SceneState = state.get_base_scene_state()
|
|
57
|
+
if base != null and not nodes.has(prefix):
|
|
58
|
+
var refused: String = _lay(base, prefix, nodes, within)
|
|
59
|
+
if not refused.is_empty():
|
|
60
|
+
return refused
|
|
61
|
+
for index: int in state.get_node_count():
|
|
62
|
+
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
63
|
+
var fresh: bool = not nodes.has(path)
|
|
64
|
+
if fresh:
|
|
65
|
+
var instance: PackedScene = state.get_node_instance(index)
|
|
66
|
+
if instance != null:
|
|
67
|
+
if within.has(instance.resource_path):
|
|
68
|
+
return "%s instances %s, which holds it" % [within.back(), instance.resource_path]
|
|
69
|
+
var refused: String = _lay(
|
|
70
|
+
instance.get_state(), path, nodes, within + [instance.resource_path]
|
|
71
|
+
)
|
|
72
|
+
if not refused.is_empty():
|
|
73
|
+
return refused
|
|
74
|
+
if not nodes.has(path):
|
|
75
|
+
return "%s holds no nodes" % instance.resource_path
|
|
76
|
+
nodes[path]["instanceOf"] = instance.resource_path
|
|
77
|
+
elif state.is_node_instance_placeholder(index):
|
|
78
|
+
nodes[path] = _made("InstancePlaceholder")
|
|
79
|
+
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
80
|
+
elif str(state.get_node_type(index)).is_empty():
|
|
81
|
+
# An entry for a node of an instance that no longer has it, which the engine skips too.
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
nodes[path] = _made(str(state.get_node_type(index)))
|
|
85
|
+
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
86
|
+
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
87
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
88
|
+
if fresh:
|
|
89
|
+
# The root of what is laid here is placed by whoever instanced it.
|
|
90
|
+
if path != prefix:
|
|
91
|
+
_place(
|
|
92
|
+
nodes,
|
|
93
|
+
_joined(prefix, str(state.get_node_path(index, true))),
|
|
94
|
+
path,
|
|
95
|
+
state.get_node_index(index)
|
|
96
|
+
)
|
|
97
|
+
var held: Dictionary = nodes[path]["properties"]
|
|
98
|
+
for property: int in state.get_node_property_count(index):
|
|
99
|
+
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
100
|
+
index, property
|
|
101
|
+
)
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
static func _made(type: String) -> Dictionary:
|
|
106
|
+
var children: Array[String] = []
|
|
107
|
+
return {"name": "", "type": type, "properties": {}, "children": children}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
|
|
111
|
+
## added inside an instance) and last when it does not.
|
|
112
|
+
static func _place(nodes: Dictionary, parent: String, path: String, at: int) -> void:
|
|
113
|
+
if not nodes.has(parent):
|
|
114
|
+
return
|
|
115
|
+
var children: Array[String] = nodes[parent]["children"]
|
|
116
|
+
if at >= 0 and at < children.size():
|
|
117
|
+
var _placed: int = children.insert(at, path)
|
|
118
|
+
else:
|
|
119
|
+
children.append(path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param given], a path as a SceneState gives it ("." or "./Body/Tail"), under [param prefix].
|
|
123
|
+
static func _joined(prefix: String, given: String) -> String:
|
|
124
|
+
var relative: String = given.trim_prefix("./")
|
|
125
|
+
if relative == "." or relative.is_empty():
|
|
126
|
+
return prefix
|
|
127
|
+
if prefix == ".":
|
|
128
|
+
return relative
|
|
129
|
+
return prefix + "/" + relative
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
static func _entry(
|
|
133
|
+
nodes: Dictionary, path: String, include_properties: bool, depth: int, current_depth: int
|
|
134
|
+
) -> Dictionary:
|
|
135
|
+
var node: Dictionary = nodes[path]
|
|
136
|
+
var children: Array[Dictionary] = []
|
|
137
|
+
var data: Dictionary = {"name": node["name"], "type": node["type"], "path": path, "children": children}
|
|
138
|
+
# The root's file is the scene being read, so only another node's names a second scene.
|
|
139
|
+
if path != "." and node.has("instanceOf"):
|
|
140
|
+
data["instanceOf"] = node["instanceOf"]
|
|
141
|
+
if include_properties:
|
|
142
|
+
var given: Dictionary = node["properties"]
|
|
143
|
+
data["properties"] = _stored(str(node["type"]), given)
|
|
144
|
+
if depth >= 0 and current_depth >= depth:
|
|
145
|
+
return data
|
|
146
|
+
for child: String in node["children"]:
|
|
147
|
+
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Every property a node of [param type] stores, with the value [param given] sets or the default,
|
|
152
|
+
## read as a node holding those values would answer them.
|
|
153
|
+
##
|
|
154
|
+
## The engine's half is read off a bare node of the class with the file's values set on it, which
|
|
155
|
+
## runs no script: what a node lists depends on its values (a thread group's settings appear only
|
|
156
|
+
## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
|
|
157
|
+
## the class's defaults and the file's entries alone answered neither. The script's half is its
|
|
158
|
+
## declared defaults under the file's values.
|
|
159
|
+
static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
160
|
+
var attached: Variant = given.get("script")
|
|
161
|
+
var script: Script = attached if attached is Script else null
|
|
162
|
+
var scripted: Dictionary = {}
|
|
163
|
+
if script != null:
|
|
164
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
165
|
+
var property: String = str(declared.get("name", ""))
|
|
166
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
167
|
+
scripted[property] = script.get_property_default_value(property)
|
|
168
|
+
var values: Dictionary = {}
|
|
169
|
+
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
|
|
170
|
+
var bare: Object = ClassDB.instantiate(type)
|
|
171
|
+
for property: Variant in given:
|
|
172
|
+
if property != "script" and not scripted.has(property):
|
|
173
|
+
bare.set(str(property), given[property])
|
|
174
|
+
for declared: Dictionary in bare.get_property_list():
|
|
175
|
+
var property: String = str(declared.get("name", ""))
|
|
176
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
177
|
+
values[property] = bare.get(property)
|
|
178
|
+
if bare is Node:
|
|
179
|
+
bare.free()
|
|
180
|
+
for property: String in scripted:
|
|
181
|
+
values[property] = scripted[property]
|
|
182
|
+
# Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
|
|
183
|
+
# for a property that holds the node once it is in a tree.
|
|
184
|
+
values.merge(given, true)
|
|
185
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
186
|
+
var answered: Dictionary = {}
|
|
187
|
+
for property: Variant in values:
|
|
188
|
+
answered[property] = serialiser.serialize_value(values[property])
|
|
189
|
+
return answered
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
|
+
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
|
+
## the project loads.
|
|
195
|
+
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
+
if not FileAccess.file_exists(res_path):
|
|
197
|
+
return {"ok": false, "error": "No resource at " + res_path}
|
|
198
|
+
var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
|
+
if loaded == null:
|
|
200
|
+
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
201
|
+
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
|
+
if loaded is PackedScene:
|
|
203
|
+
return {
|
|
204
|
+
"ok": false,
|
|
205
|
+
"error":
|
|
206
|
+
"%s is a scene: scene_tree reads its nodes, and scene_node get one node's properties." % res_path
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var script: Script = loaded.get_script()
|
|
210
|
+
var scripted: Dictionary = {}
|
|
211
|
+
if script != null:
|
|
212
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
213
|
+
scripted[str(declared.get("name", ""))] = true
|
|
214
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
215
|
+
var properties: Dictionary = {}
|
|
216
|
+
for p: Dictionary in loaded.get_property_list():
|
|
217
|
+
var property: String = str(p.get("name", ""))
|
|
218
|
+
if property.is_empty() or property == "script":
|
|
219
|
+
continue
|
|
220
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
221
|
+
continue
|
|
222
|
+
var value: Variant = loaded.get(property)
|
|
223
|
+
if not include_defaults:
|
|
224
|
+
var default: Variant = (
|
|
225
|
+
script.get_property_default_value(property)
|
|
226
|
+
if scripted.has(property)
|
|
227
|
+
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
228
|
+
)
|
|
229
|
+
if PropertyValues.same(value, default):
|
|
230
|
+
continue
|
|
231
|
+
properties[property] = serialiser.serialize_value(value)
|
|
232
|
+
var answer: Dictionary = {
|
|
233
|
+
"ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
|
|
234
|
+
}
|
|
235
|
+
if script != null:
|
|
236
|
+
answer["script"] = script.resource_path
|
|
237
|
+
return answer
|
|
@@ -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
|