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..b4b413c6 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,8 +61,10 @@ 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) { + } catch (failure: Throwable) { val cleanupFailures = mutableListOf() runCatching { (preparation.module as SModuleBase).unregisterModel(model as SModelBase) } .exceptionOrNull()?.let { cleanupFailures += "could not detach model: ${it.message}" } @@ -71,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 8bf018fc..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,7 +1,12 @@ 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.* class ModelCreationSemanticsTest { @@ -60,4 +65,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) + + assertContains(usedDevkits(project, report.modelReference), "jetbrains.mps.devkit.aspect.structure") + assertContains(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("\"") + assertContains(usedDevkits(project, modelReference), "jetbrains.mps.devkit.aspect.dataflow") + } + } + + private fun usedDevkits(project: Project, modelReference: String): List = + project.modelAccess.computeReadAction { + val model = PersistenceFacade.getInstance().createModelReference(modelReference).resolve(project.repository) + (model as 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: 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),