Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SAbstractConcept>", "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."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>()
runCatching { (preparation.module as SModuleBase).unregisterModel(model as SModelBase) }
.exceptionOrNull()?.let { cleanupFailures += "could not detach model: ${it.message}" }
Expand All @@ -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!!)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<ModelCreationResponse>(
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<String> =
project.modelAccess.computeReadAction {
val model = PersistenceFacade.getInstance().createModelReference(modelReference).resolve(project.repository)
(model as SModelInternal).importedDevkits().map { it.moduleName!! }
}
}
2 changes: 2 additions & 0 deletions docs/reference/create.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/specs/editing.creation.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
23 changes: 12 additions & 11 deletions skills/mops/references/editing.creation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.