16
16
17
17
package org.modelix.mps.model.sync.bulk
18
18
19
- import com.intellij.openapi.application.ApplicationManager
20
19
import com.intellij.openapi.project.ProjectManager
20
+ import jetbrains.mps.ide.ThreadUtils
21
21
import jetbrains.mps.ide.project.ProjectHelper
22
22
import jetbrains.mps.smodel.SNodeUtil
23
+ import jetbrains.mps.smodel.StaticReference
23
24
import jetbrains.mps.smodel.adapter.ids.MetaIdHelper
24
25
import jetbrains.mps.smodel.adapter.ids.SConceptId
26
+ import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory
25
27
import jetbrains.mps.smodel.adapter.structure.concept.SConceptAdapterById
26
28
import jetbrains.mps.smodel.language.ConceptRegistry
27
29
import jetbrains.mps.smodel.language.StructureRegistry
@@ -33,6 +35,7 @@ import kotlinx.serialization.json.decodeFromStream
33
35
import org.jetbrains.mps.openapi.model.EditableSModel
34
36
import org.jetbrains.mps.openapi.module.SModule
35
37
import org.jetbrains.mps.openapi.module.SRepository
38
+ import org.modelix.model.api.INode
36
39
import org.modelix.model.data.ModelData
37
40
import org.modelix.model.mpsadapters.MPSModuleAsNode
38
41
import org.modelix.model.mpsadapters.MPSRepositoryAsNode
@@ -43,6 +46,32 @@ import org.modelix.model.sync.bulk.isModuleIncluded
43
46
import java.io.File
44
47
import java.util.concurrent.atomic.AtomicInteger
45
48
49
+ /* *
50
+ * Identifier of the `name` property in the `INamedConcept` concept.
51
+ * See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
52
+ */
53
+ @Suppress(" MagicNumber" )
54
+ private val namePropertyOfINamedConceptConcept = MetaAdapterFactory .getProperty(
55
+ - 0x3154ae6ada15b0deL ,
56
+ - 0x646defc46a3573f4L ,
57
+ 0x110396eaaa4L ,
58
+ 0x110396ec041L ,
59
+ " name" ,
60
+ )
61
+
62
+ /* *
63
+ * Identifier of the `resolveInfo` property in the `IResolveInfoConcept` concept.
64
+ * See https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L355
65
+ */
66
+ @Suppress(" MagicNumber" )
67
+ private val resolveInfoPropertyOfIResolveInfoConcept = MetaAdapterFactory .getProperty(
68
+ - 0x3154ae6ada15b0deL ,
69
+ - 0x646defc46a3573f4L ,
70
+ 0x116b17c6e46L ,
71
+ 0x116b17cd415L ,
72
+ " resolveInfo" ,
73
+ )
74
+
46
75
object MPSBulkSynchronizer {
47
76
48
77
@JvmStatic
@@ -94,56 +123,118 @@ object MPSBulkSynchronizer {
94
123
if (jsonFiles.isNullOrEmpty()) error(" no json files found for included modules" )
95
124
96
125
println (" Found ${jsonFiles.size} modules to be imported" )
97
- val access = repository.modelAccess
98
- access.executeCommandInEDT {
126
+ val getModulesToImport = {
99
127
val allModules = repository.modules
100
128
val includedModules: Iterable <SModule > = allModules.filter {
101
129
isModuleIncluded(it.moduleName!! , includedModuleNames, includedModulePrefixes)
102
130
}
103
131
val numIncludedModules = includedModules.count()
104
- val repoAsNode = MPSRepositoryAsNode (repository)
105
- println (" Importing modules..." )
106
- try {
132
+ val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
133
+ println (" Importing module ${index + 1 } of $numIncludedModules : '${module.moduleName} '" )
134
+ val fileName = inputPath + File .separator + module.moduleName + " .json"
135
+ val moduleFile = File (fileName)
136
+ if (moduleFile.exists()) {
137
+ val expectedData: ModelData = moduleFile.inputStream().use(Json ::decodeFromStream)
138
+ sequenceOf(ExistingAndExpectedNode (MPSModuleAsNode (module), expectedData))
139
+ } else {
140
+ println (" Skip importing ${module.moduleName} } because $fileName does not exist." )
141
+ sequenceOf()
142
+ }
143
+ }
144
+ modulesToImport
145
+ }
146
+ importModelsIntoRepository(repository, MPSRepositoryAsNode (repository), continueOnError, getModulesToImport)
147
+ }
148
+
149
+ /* *
150
+ * Import specified models into the repository.
151
+ * [getModulesToImport] is a lambda to be executed with read access in MPS.
152
+ */
153
+ @JvmStatic
154
+ fun importModelsIntoRepository (
155
+ repository : SRepository ,
156
+ rootOfImport : INode ,
157
+ continueOnError : Boolean ,
158
+ getModulesToImport : () -> Sequence <ExistingAndExpectedNode >,
159
+ ) {
160
+ val access = repository.modelAccess
161
+ ThreadUtils .runInUIThreadAndWait {
162
+ access.executeCommand {
107
163
println (" Importing modules..." )
108
- // `modulesToImport` lazily produces modules to import
109
- // so that loaded model data can be garbage collected.
110
- val modulesToImport = includedModules.asSequence().flatMapIndexed { index, module ->
111
- println (" Importing module ${index + 1 } of $numIncludedModules : '${module.moduleName} '" )
112
- val fileName = inputPath + File .separator + module.moduleName + " .json"
113
- val moduleFile = File (fileName)
114
- if (moduleFile.exists()) {
115
- val expectedData: ModelData = moduleFile.inputStream().use(Json ::decodeFromStream)
116
- sequenceOf(ExistingAndExpectedNode (MPSModuleAsNode (module), expectedData))
117
- } else {
118
- println (" Skip importing ${module.moduleName} } because $fileName does not exist." )
119
- sequenceOf()
120
- }
164
+ try {
165
+ println (" Importing modules..." )
166
+ // `modulesToImport` lazily produces modules to import
167
+ // so that loaded model data can be garbage collected.
168
+ val modulesToImport = getModulesToImport()
169
+ ModelImporter (rootOfImport, continueOnError).importIntoNodes(modulesToImport)
170
+ println (" Import finished." )
171
+ } catch (ex: Exception ) {
172
+ // Exceptions are only visible in the MPS log file by default
173
+ ex.printStackTrace()
121
174
}
122
- ModelImporter (repoAsNode, continueOnError).importIntoNodes(modulesToImport)
123
175
println (" Import finished." )
124
- } catch (ex: Exception ) {
125
- // Exceptions are only visible in the MPS log file by default
126
- ex.printStackTrace()
127
176
}
128
- println (" Import finished." )
129
177
}
130
178
131
- ApplicationManager .getApplication().invokeAndWait {
179
+ ThreadUtils .runInUIThreadAndWait {
132
180
println (" Persisting changes..." )
133
- access.executeCommandInEDT {
181
+ access.executeCommand {
134
182
enableWorkaroundForFilePerRootPersistence(repository)
183
+ updateUnsetResolveInfo(repository)
135
184
repository.saveAll()
136
185
}
137
186
println (" Changes persisted." )
138
187
}
139
188
}
140
189
190
+ /* *
191
+ * Workaround for MPS not being able to set the `resolveInfo` property on a reference.
192
+ * This is the case when the concept of the target node cannot be loaded/is not valid.
193
+ * Without this workaround, the `resolve` attribute in serialized references
194
+ * (e.g. <ref role="3SLt5I" node="3vHUMVfa0RY" resolve="referencedNodeA" />)
195
+ * will not be set, updated or even removed.
196
+ *
197
+ * The `resolve` is for example removed when the node that contains the reference is moved.
198
+ *
199
+ * The workaround follows the logic of MPS but without relying on the concept being loaded/valid.
200
+ * Without this workaround a bulk sync can remove the `resolve` info unintentionally
201
+ * and produce unwanted file changes.
202
+ */
203
+ private fun updateUnsetResolveInfo (repository : SRepository ) {
204
+ val changedModels = repository.modules.asSequence()
205
+ .flatMap { it.models }
206
+ .mapNotNull { it as ? EditableSModel }
207
+ .filter { it.isChanged }
208
+ val references = changedModels
209
+ .flatMap { org.jetbrains.mps.openapi.model.SNodeUtil .getDescendants(it) }
210
+ .flatMap { it.references }
211
+ .mapNotNull { it as ? StaticReference }
212
+
213
+ references.forEach { reference ->
214
+ val target = reference.targetNode ? : return @forEach
215
+ // A concept is not valid, for example, when the language could not be loaded.
216
+ if (target.concept.isValid) {
217
+ return @forEach
218
+ }
219
+ // Try guessing the resolve info following the logic of MPS.
220
+ // Use the logic like in MPS but without relying on the concept being loaded.
221
+ // https://github.com/JetBrains/MPS/blob/5bb20b8a104c08206490e0f3fad70304fa0e0151/core/kernel/kernelSolution/source_gen/jetbrains/mps/util/SNodeOperations.java#L230
222
+ val newResolveInfo = target.getProperty(resolveInfoPropertyOfIResolveInfoConcept)
223
+ ? : target.getProperty(namePropertyOfINamedConceptConcept)
224
+ if (newResolveInfo != reference.resolveInfo) {
225
+ // This workaround works with different persistence because it sets the `resolveInfo`
226
+ // using `jetbrains.mps.smodel.SReference` which is not specific to any persistence.
227
+ reference.resolveInfo = newResolveInfo
228
+ }
229
+ }
230
+ }
231
+
141
232
/* *
142
233
* Workaround for MPS not being able to read the name property of the node during the save process
143
234
* in case FilePerRootPersistence is used.
144
- * This is because the concept is not properly loaded and in the MPS code it checks if the concept is a subconcept
145
- * of INamedConcept.
146
- * Without this workaround the id of the root node will be used instead of the name, resulting in renamed files.
235
+ * This is because the concept is not properly loaded,
236
+ * and in the MPS code it checks if the concept is a subconcept of INamedConcept.
237
+ * Without this workaround, the id of the root node will be used instead of the name, resulting in renamed files.
147
238
*/
148
239
@JvmStatic
149
240
private fun enableWorkaroundForFilePerRootPersistence (repository : SRepository ) {
0 commit comments