From ec10323fd39379de1a41d1a62c4c630bbf19e90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 19:29:42 +0100 Subject: [PATCH 1/2] fix(daemon): give a new model the devkits and used languages MPS gives it mops create model and Code Mode's SModule.createModel now run MPS's ModelsAutoImportsManager on the new model, as the MPS New Model dialog does. An aspect model of a language gets its aspect devkit, for example jetbrains.mps.devkit.aspect.structure, so the language makes. Fixes #225 --- .../mops/cli/MakeIntegrationTest.kt | 30 ++++++++++++++++ .../mops/cli/create/CreateModelCommand.kt | 4 ++- .../mops/daemon/CodeCatalog.kt | 2 +- .../mops/daemon/ModelCreator.kt | 3 ++ .../mops/daemon/ModelCreationSemanticsTest.kt | 36 +++++++++++++++++++ docs/reference/create.md | 2 ++ 6 files changed, 75 insertions(+), 2 deletions(-) diff --git a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt index c7e76eb0..8ef45ffa 100644 --- a/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt +++ b/cli/src/integrationTest/kotlin/com/specificlanguages/mops/cli/MakeIntegrationTest.kt @@ -6,6 +6,8 @@ import com.specificlanguages.mops.protocol.MakeResponse import com.specificlanguages.mops.protocol.ProtocolJson import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.parallel.ResourceLock +import kotlin.io.path.pathString +import kotlin.io.path.writeText import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -29,6 +31,34 @@ class MakeIntegrationTest { ) } + @Test + fun `a new language with a structure model created by mops makes`() { + val project = daemons.project("mps-json", copy = "new-language") + val program = project.root.resolve("add-concept.groovy") + program.writeText(""" + project.command { + def model = mops.lookup.requireModel('example.lang.structure') + def concept = model.createNode(mops.lookup.requireConceptByName('jetbrains.mps.lang.structure.ConceptDeclaration')) + concept.properties['name'] = 'Greeting' + model.addRootNode(concept) + concept.name + } + """.trimIndent()) + + for (args in listOf( + arrayOf("create", "language", "example.lang"), + arrayOf("create", "model", "example.lang.structure", "--module", "example.lang"), + arrayOf("code", "run", program.pathString), + )) { + val step = project.run(*args) + assertEquals(0, step.exitCode, step.output) + } + val result = project.run("make", "module", "--json", "example.lang") + + assertEquals(0, result.exitCode, result.output) + assertEquals(MakeOutcome.SUCCESS, (ProtocolJson.decodeResponse(result.stdout) as MakeResponse).outcome, result.output) + } + companion object { @JvmField @RegisterExtension diff --git a/cli/src/main/kotlin/com/specificlanguages/mops/cli/create/CreateModelCommand.kt b/cli/src/main/kotlin/com/specificlanguages/mops/cli/create/CreateModelCommand.kt index 006fecaf..0fa8f9ca 100644 --- a/cli/src/main/kotlin/com/specificlanguages/mops/cli/create/CreateModelCommand.kt +++ b/cli/src/main/kotlin/com/specificlanguages/mops/cli/create/CreateModelCommand.kt @@ -7,7 +7,9 @@ import picocli.CommandLine.Command import picocli.CommandLine.Option import picocli.CommandLine.Parameters -@Command(name = "model", description = ["Create an empty model in a project module."]) +@Command(name = "model", description = ["Create an empty model in a project module.", + "The model gets the devkits and used languages that MPS gives a new model in that module, such as the aspect " + + "devkit for a language's structure model."]) class CreateModelCommand(private val environment: CommandEnvironment) : CliCommand() { @Parameters(index = "0", paramLabel = "MODEL_NAME") lateinit var modelName: String @Option(names = ["--module"], required = true, paramLabel = "MODULE") lateinit var module: String diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/CodeCatalog.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/CodeCatalog.kt index c9391a4a..394dc8f3 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/CodeCatalog.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/CodeCatalog.kt @@ -36,7 +36,7 @@ object CodeCatalog { Entry("Project", "createSolution", "Project.createSolution(String name, Map options = [:]): Solution", "command", "Create a solution."), Entry("Project", "createDevkit", "Project.createDevkit(String name, Map options = [:]): DevKit", "command", "Create a devkit."), Entry("Language", "createGenerator", "Language.createGenerator(String alias, Map options = [:]): Generator", "command", "Create a generator for this language."), - Entry("SModule", "createModel", "SModule.createModel(String name, boolean filePerRoot = false): SModel", "command", "Create a model in this module."), + Entry("SModule", "createModel", "SModule.createModel(String name, boolean filePerRoot = false): SModel", "command", "Create a model in this module. The model gets the devkits and used languages that MPS gives a new model in that module."), Entry("mops.search", "subConceptsOf", "mops.search.subConceptsOf(SAbstractConcept concept, boolean concreteOnly = false, boolean direct = false): List", "read", "Find extending concepts and interface implementors across loaded language runtimes, including libraries. Excludes the queried concept; returns unique concepts sorted by qualified name. concreteOnly excludes abstract concepts and interfaces; direct restricts to immediate superclass or interface relationships. Unloaded languages contribute no results."), Entry("mops.lookup", "conceptByName", "mops.lookup.conceptByName(String name): SAbstractConcept?", "read", "Resolve a concept name; return null on a miss. Ambiguity, malformed names, and untrusted language runtimes remain errors."), Entry("mops.lookup", "requireConceptByName", "mops.lookup.requireConceptByName(String name): SAbstractConcept", "read", "Resolve exactly one concept; throw on a miss."), diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt index c0e9d877..86c6f73f 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt @@ -11,6 +11,7 @@ import jetbrains.mps.extapi.persistence.SourceRootKinds import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryRuleService import jetbrains.mps.extapi.persistence.datasource.PreinstalledDataSourceTypes import jetbrains.mps.persistence.* +import jetbrains.mps.project.ModelsAutoImportsManager import jetbrains.mps.project.Project import org.jetbrains.mps.openapi.model.EditableSModel import org.jetbrains.mps.openapi.model.SModel @@ -60,6 +61,8 @@ class ModelCreator(private val project: Project) { val model = preparation.selected.root!!.createModel(SModelName(preparation.actualName), null, dataSourceType(preparation.format), factoryType(preparation.format)) try { + requireNotNull(project.platform.findComponent(ModelsAutoImportsManager::class.java)) + .performImports(preparation.module, model) (model as EditableSModel).save() } catch (saveFailure: Throwable) { val cleanupFailures = mutableListOf() diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt index 8bf018fc..5dbaf5b2 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt @@ -2,6 +2,7 @@ package com.specificlanguages.mops.daemon import com.specificlanguages.mops.protocol.* import kotlin.io.path.exists +import kotlin.io.path.readText import kotlin.test.* class ModelCreationSemanticsTest { @@ -60,4 +61,39 @@ class ModelCreationSemanticsTest { Unit } } + @Test + fun `an aspect model of a language gets the devkit MPS gives that aspect`() { + SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> + val handler = DomainRequestHandler(projectPath, SharedMpsEnvironment.mpsAccess(project)) + handler.handleDomainRequest(CreateLanguageRequest("", "example.lang")) + val report = assertNotNull(assertIs( + handler.handleDomainRequest(CreateModelRequest("", "example.lang.structure", "example.lang")), + ).report) + + assertEquals(listOf("jetbrains.mps.devkit.aspect.structure"), usedDevkits(project, report.modelReference)) + assertContains(java.nio.file.Path.of(report.location).readText(), "jetbrains.mps.devkit.aspect.structure") + } + } + + @Test + fun `code mode creates an aspect model with the devkit MPS gives that aspect`() { + SharedMpsEnvironment.withOpenProjectCopy { project, _ -> + val response = CodeModeExecutor(SharedMpsEnvironment.mpsAccess(project), project, SharedMpsEnvironment.platform).execute(CodeRunRequest( + "", """ + def owner = project.read { mops.lookup.requireModule('com.specificlanguages.json') } + project.command { owner.createModel('.dataFlow').reference.toString() } + """.trimIndent(), "aspect-model-creation.groovy", + )) + + val modelReference = requireNotNull(response.output).trim().removeSurrounding("\"") + assertEquals(listOf("jetbrains.mps.devkit.aspect.dataflow"), usedDevkits(project, modelReference)) + } + } + + private fun usedDevkits(project: jetbrains.mps.project.Project, modelReference: String): List = + project.modelAccess.computeReadAction { + val model = org.jetbrains.mps.openapi.persistence.PersistenceFacade.getInstance() + .createModelReference(modelReference).resolve(project.repository) + (model as jetbrains.mps.smodel.SModelInternal).importedDevkits().map { it.moduleName!! } + } } diff --git a/docs/reference/create.md b/docs/reference/create.md index 41acaaec..2e62c50e 100644 --- a/docs/reference/create.md +++ b/docs/reference/create.md @@ -122,6 +122,8 @@ Create an empty model in a project module. Usage: mops create model [-h] [--dry-run] [--file-per-root] [--json] --module=MODULE MODEL_NAME Create an empty model in a project module. +The model gets the devkits and used languages that MPS gives a new model in +that module, such as the aspect devkit for a language's structure model. Parameters: MODEL_NAME Options: From b7656f0910b9f1ee6acf0a4d3ca3353e16802f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergej=20Ko=C5=A1=C4=8Dejev?= Date: Wed, 11 Nov 2026 21:23:33 +0100 Subject: [PATCH 2/2] fix(daemon): name the import step in model creation failures; add an aspect model example --- .../mops/daemon/ModelCreator.kt | 6 ++--- .../mops/daemon/ModelCreationSemanticsTest.kt | 18 +++++++++------ examples/specs/editing.creation.groovy | 5 ++++ skills/mops/references/editing.creation.md | 23 ++++++++++--------- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt index 86c6f73f..b4b413c6 100644 --- a/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt +++ b/daemon/src/main/kotlin/com/specificlanguages/mops/daemon/ModelCreator.kt @@ -64,7 +64,7 @@ class ModelCreator(private val project: Project) { requireNotNull(project.platform.findComponent(ModelsAutoImportsManager::class.java)) .performImports(preparation.module, model) (model as EditableSModel).save() - } catch (saveFailure: Throwable) { + } catch (failure: Throwable) { val cleanupFailures = mutableListOf() runCatching { (preparation.module as SModuleBase).unregisterModel(model as SModelBase) } .exceptionOrNull()?.let { cleanupFailures += "could not detach model: ${it.message}" } @@ -74,8 +74,8 @@ class ModelCreator(private val project: Project) { } val residue = if (target?.exists() == true) "; possible residue at $target" else "" val cleanup = cleanupFailures.takeIf { it.isNotEmpty() }?.joinToString("; ", prefix = "; ") ?: "" - throw IllegalStateException("initial save failed for ${preparation.actualName} at ${preparation.selected.location}: " + - "${saveFailure.message ?: saveFailure.javaClass.name}$residue$cleanup", saveFailure) + throw IllegalStateException("initial imports or save failed for ${preparation.actualName} at ${preparation.selected.location}: " + + "${failure.message ?: failure.javaClass.name}$residue$cleanup", failure) } return CreatedModel(model, preparation.selected.location!!) } diff --git a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt index 5dbaf5b2..11872fcf 100644 --- a/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt +++ b/daemon/src/test/kotlin/com/specificlanguages/mops/daemon/ModelCreationSemanticsTest.kt @@ -1,6 +1,10 @@ package com.specificlanguages.mops.daemon import com.specificlanguages.mops.protocol.* +import jetbrains.mps.project.Project +import jetbrains.mps.smodel.SModelInternal +import org.jetbrains.mps.openapi.persistence.PersistenceFacade +import java.nio.file.Path import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.test.* @@ -61,6 +65,7 @@ class ModelCreationSemanticsTest { Unit } } + @Test fun `an aspect model of a language gets the devkit MPS gives that aspect`() { SharedMpsEnvironment.withOpenProjectCopy { project, projectPath -> @@ -70,8 +75,8 @@ class ModelCreationSemanticsTest { handler.handleDomainRequest(CreateModelRequest("", "example.lang.structure", "example.lang")), ).report) - assertEquals(listOf("jetbrains.mps.devkit.aspect.structure"), usedDevkits(project, report.modelReference)) - assertContains(java.nio.file.Path.of(report.location).readText(), "jetbrains.mps.devkit.aspect.structure") + assertContains(usedDevkits(project, report.modelReference), "jetbrains.mps.devkit.aspect.structure") + assertContains(Path.of(report.location).readText(), "jetbrains.mps.devkit.aspect.structure") } } @@ -86,14 +91,13 @@ class ModelCreationSemanticsTest { )) val modelReference = requireNotNull(response.output).trim().removeSurrounding("\"") - assertEquals(listOf("jetbrains.mps.devkit.aspect.dataflow"), usedDevkits(project, modelReference)) + assertContains(usedDevkits(project, modelReference), "jetbrains.mps.devkit.aspect.dataflow") } } - private fun usedDevkits(project: jetbrains.mps.project.Project, modelReference: String): List = + private fun usedDevkits(project: Project, modelReference: String): List = project.modelAccess.computeReadAction { - val model = org.jetbrains.mps.openapi.persistence.PersistenceFacade.getInstance() - .createModelReference(modelReference).resolve(project.repository) - (model as jetbrains.mps.smodel.SModelInternal).importedDevkits().map { it.moduleName!! } + val model = PersistenceFacade.getInstance().createModelReference(modelReference).resolve(project.repository) + (model as SModelInternal).importedDevkits().map { it.moduleName!! } } } diff --git a/examples/specs/editing.creation.groovy b/examples/specs/editing.creation.groovy index f4458386..667446d7 100644 --- a/examples/specs/editing.creation.groovy +++ b/examples/specs/editing.creation.groovy @@ -36,6 +36,11 @@ kind: 'cli', code: '''mops create model .main --module sample.solution''', ], + [ + title: 'Create a language aspect model with its aspect devkit', + kind: 'cli', + code: '''mops create model .structure --module sample.language''', + ], [ title: 'Create a model with one file per root', kind: 'cli', diff --git a/skills/mops/references/editing.creation.md b/skills/mops/references/editing.creation.md index 7abdf7af..b7e71fea 100644 --- a/skills/mops/references/editing.creation.md +++ b/skills/mops/references/editing.creation.md @@ -1,16 +1,17 @@ # Create modules and models -| Task | Command | -| ------------------------------------- | ---------------------------------------------------------------------------- | -| Create a language with a generator | `mops create language sample.language --with-generator` | -| Create a Java solution | `mops create solution sample.solution --usage-preset java` | -| Create a test solution | `mops create solution sample.tests --usage-preset java-tests` | -| Create a devkit | `mops create devkit sample.devkit` | -| Create an embedded generator | `mops create generator --language sample.language --alias main` | -| Create a standalone generator | `mops create generator --language sample.language --alias main --standalone` | -| Create a model relative to a module | `mops create model .main --module sample.solution` | -| Create a model with one file per root | `mops create model .main --module sample.solution --file-per-root` | -| Preview creation | `mops create solution sample.solution --usage-preset java --dry-run` | +| Task | Command | +| ----------------------------------------------------- | ---------------------------------------------------------------------------- | +| Create a language with a generator | `mops create language sample.language --with-generator` | +| Create a Java solution | `mops create solution sample.solution --usage-preset java` | +| Create a test solution | `mops create solution sample.tests --usage-preset java-tests` | +| Create a devkit | `mops create devkit sample.devkit` | +| Create an embedded generator | `mops create generator --language sample.language --alias main` | +| Create a standalone generator | `mops create generator --language sample.language --alias main --standalone` | +| Create a model relative to a module | `mops create model .main --module sample.solution` | +| Create a language aspect model with its aspect devkit | `mops create model .structure --module sample.language` | +| Create a model with one file per root | `mops create model .main --module sample.solution --file-per-root` | +| Preview creation | `mops create solution sample.solution --usage-preset java --dry-run` | Solution presets configure facets, not dependencies, imported languages, devkits, or contents. Supported presets: `not-generated` (default, no facets), `text` (plain-text generation), `java` (Java compiled and loaded by MPS),