Skip to content

Commit 48abcb9

Browse files
committed
feat: add run configuration for project and single contract build
Fixes #79
1 parent 7501f99 commit 48abcb9

18 files changed

+693
-1
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ platformType = IC
1010
platformVersion = 2024.3.2
1111

1212
platformPlugins =
13-
platformBundledPlugins =
13+
platformBundledPlugins = com.intellij.modules.json
1414

1515
gradleVersion = 8.10.2
1616

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.tonstudio.tact.ide.build
2+
3+
import com.intellij.execution.lineMarker.ExecutorAction
4+
import com.intellij.execution.lineMarker.RunLineMarkerContributor
5+
import com.intellij.icons.AllIcons
6+
import com.intellij.psi.PsiElement
7+
import com.intellij.psi.util.elementType
8+
import org.tonstudio.tact.lang.TactTypes
9+
import org.tonstudio.tact.lang.psi.TactContractType
10+
11+
class TactBuildLineMarkerProvider : RunLineMarkerContributor() {
12+
private val contextActions = ExecutorAction.Companion.getActions(0)
13+
14+
override fun getInfo(element: PsiElement): Info? {
15+
if (element.elementType == TactTypes.IDENTIFIER) {
16+
val parent = element.parent
17+
if (parent is TactContractType) {
18+
return Info(AllIcons.Toolwindows.ToolWindowBuild, contextActions)
19+
}
20+
21+
return null
22+
}
23+
24+
return null
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.tonstudio.tact.ide.build
2+
3+
import com.intellij.execution.lineMarker.ExecutorAction
4+
import com.intellij.execution.lineMarker.RunLineMarkerContributor
5+
import com.intellij.icons.AllIcons
6+
import com.intellij.json.JsonElementTypes
7+
import com.intellij.json.psi.JsonProperty
8+
import com.intellij.json.psi.JsonStringLiteral
9+
import com.intellij.psi.PsiElement
10+
import com.intellij.psi.util.elementType
11+
12+
class TactBuildProjectLineMarkerProvider : RunLineMarkerContributor() {
13+
private val contextActions = ExecutorAction.Companion.getActions(0)
14+
15+
override fun getInfo(element: PsiElement): Info? {
16+
if (element.elementType != JsonElementTypes.DOUBLE_QUOTED_STRING && element.elementType != JsonElementTypes.SINGLE_QUOTED_STRING)
17+
return null
18+
19+
val elementContent = element.text.slice(1..element.text.lastIndex - 1)
20+
if (elementContent != "name") return null
21+
22+
val parent = element.parent.parent
23+
if (parent is JsonProperty) {
24+
if (parent.name == "name") {
25+
val projectName = parent.value as? JsonStringLiteral ?: return null
26+
return Info(AllIcons.Toolwindows.ToolWindowBuild, contextActions) { "Build project \"${projectName.value}\"" }
27+
}
28+
}
29+
30+
return null
31+
}
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.tonstudio.tact.ide.build
2+
3+
import com.intellij.execution.configurations.GeneralCommandLine
4+
import com.intellij.execution.configurations.PtyCommandLine
5+
import com.intellij.execution.process.AnsiEscapeDecoder
6+
import com.intellij.execution.process.KillableProcessHandler
7+
import com.intellij.openapi.util.Key
8+
import com.intellij.util.io.BaseDataReader
9+
import com.intellij.util.io.BaseOutputReader
10+
11+
class TactProcessHandler(commandLine: GeneralCommandLine) : KillableProcessHandler(commandLine), AnsiEscapeDecoder.ColoredTextAcceptor {
12+
private val decoder: AnsiEscapeDecoder?
13+
14+
init {
15+
setHasPty(commandLine is PtyCommandLine)
16+
setShouldDestroyProcessRecursively(!hasPty())
17+
decoder = null // if (!hasPty()) AnsiEscapeDecoder() else null
18+
}
19+
20+
override fun notifyTextAvailable(text: String, outputType: Key<*>) {
21+
decoder?.escapeText(text, outputType, this) ?: super.notifyTextAvailable(text, outputType)
22+
}
23+
24+
override fun coloredTextAvailable(text: String, attributes: Key<*>) {
25+
super.notifyTextAvailable(text, attributes)
26+
}
27+
28+
override fun readerOptions(): BaseOutputReader.Options = object : BaseOutputReader.Options() {
29+
override fun policy(): BaseDataReader.SleepingPolicy =
30+
if (hasPty() || java.lang.Boolean.getBoolean("output.reader.blocking.mode")) {
31+
BaseDataReader.SleepingPolicy.BLOCKING
32+
} else {
33+
BaseDataReader.SleepingPolicy.NON_BLOCKING
34+
}
35+
36+
override fun splitToLines(): Boolean = !hasPty()
37+
}
38+
}
39+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.tonstudio.tact.ide.build.project
2+
3+
import com.intellij.execution.Executor
4+
import com.intellij.execution.configurations.ConfigurationFactory
5+
import com.intellij.execution.configurations.LocatableConfiguration
6+
import com.intellij.execution.configurations.RunConfigurationBase
7+
import com.intellij.execution.configurations.RunProfileState
8+
import com.intellij.execution.runners.ExecutionEnvironment
9+
import com.intellij.openapi.project.Project
10+
import org.jdom.Element
11+
12+
open class TactBuildConfiguration(project: Project, factory: ConfigurationFactory?, name: String?) :
13+
RunConfigurationBase<TactBuildConfigurationOptions>(project, factory, name),
14+
LocatableConfiguration {
15+
16+
override fun getOptions() = super.getOptions() as TactBuildConfigurationOptions
17+
18+
var fileName: String
19+
get() = options.fileName
20+
set(value) {
21+
options.fileName = value
22+
}
23+
24+
var projectName: String
25+
get() = options.projectName
26+
set(value) {
27+
options.projectName = value
28+
}
29+
30+
override fun writeExternal(element: Element) {
31+
super<RunConfigurationBase>.writeExternal(element)
32+
33+
with(element) {
34+
writeString("fileName", fileName)
35+
writeString("projectName", projectName)
36+
}
37+
}
38+
39+
override fun isGeneratedName(): Boolean {
40+
return false
41+
}
42+
43+
override fun readExternal(element: Element) {
44+
super<RunConfigurationBase>.readExternal(element)
45+
46+
with(element) {
47+
readString("fileName")?.let { fileName = it }
48+
readString("projectName")?.let { projectName = it }
49+
}
50+
}
51+
52+
override fun getConfigurationEditor() = TactBuildConfigurationEditor(project)
53+
54+
override fun checkConfiguration() {}
55+
56+
override fun getState(executor: Executor, executionEnvironment: ExecutionEnvironment): RunProfileState? {
57+
return TactBuildConfigurationRunState(executionEnvironment, this)
58+
}
59+
60+
private fun Element.writeString(name: String, value: String) {
61+
val opt = Element("option")
62+
opt.setAttribute("name", name)
63+
opt.setAttribute("value", value)
64+
addContent(opt)
65+
}
66+
67+
private fun Element.readString(name: String): String? =
68+
children
69+
.find { it.name == "option" && it.getAttributeValue("name") == name }
70+
?.getAttributeValue("value")
71+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.tonstudio.tact.ide.build.project
2+
3+
import com.intellij.execution.configuration.EnvironmentVariablesComponent
4+
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
5+
import com.intellij.openapi.options.SettingsEditor
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.ui.DialogPanel
8+
import com.intellij.ui.dsl.builder.*
9+
import javax.swing.JComponent
10+
import javax.swing.JPanel
11+
12+
open class TactBuildConfigurationEditor(private val project: Project) : SettingsEditor<TactBuildConfiguration>() {
13+
data class Model(
14+
var fileName: String = "",
15+
var projectName: String = "",
16+
)
17+
18+
private val environmentVariables = EnvironmentVariablesComponent()
19+
private lateinit var mainPanel: DialogPanel
20+
private val model = Model()
21+
22+
init {
23+
environmentVariables.label.text = "Environment:"
24+
}
25+
26+
override fun resetEditorFrom(demoRunConfiguration: TactBuildConfiguration) {
27+
with(model) {
28+
fileName = demoRunConfiguration.fileName
29+
projectName = demoRunConfiguration.projectName
30+
}
31+
32+
mainPanel.reset()
33+
}
34+
35+
override fun applyEditorTo(demoRunConfiguration: TactBuildConfiguration) {
36+
mainPanel.apply()
37+
38+
with(demoRunConfiguration) {
39+
fileName = model.fileName
40+
projectName = model.projectName
41+
}
42+
}
43+
44+
override fun createEditor(): JComponent = component()
45+
46+
private fun component(): JPanel {
47+
mainPanel = panel {
48+
row("Config File:") {
49+
textFieldWithBrowseButton(
50+
FileChooserDescriptorFactory.createSingleFileDescriptor("tact.config.json").withTitle("Select Tact Config File"),
51+
project,
52+
)
53+
.align(AlignX.FILL)
54+
.bindText(model::fileName)
55+
}.bottomGap(BottomGap.NONE)
56+
57+
row("Project name:") {
58+
textField()
59+
.align(AlignX.FILL)
60+
.bindText(model::projectName)
61+
.comment("Project name to build")
62+
}.bottomGap(BottomGap.NONE)
63+
}
64+
return mainPanel
65+
}
66+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.tonstudio.tact.ide.build.project
2+
3+
import com.intellij.execution.configurations.RunConfigurationOptions
4+
5+
class TactBuildConfigurationOptions : RunConfigurationOptions() {
6+
private var _fileName = string("").provideDelegate(this, "fileNameRunConfiguration")
7+
private var _projectName = string("").provideDelegate(this, "projectNameRunConfiguration")
8+
9+
var fileName: String
10+
get() = _fileName.getValue(this) ?: ""
11+
set(value) {
12+
_fileName.setValue(this, value)
13+
}
14+
15+
var projectName: String
16+
get() = _projectName.getValue(this) ?: ""
17+
set(value) {
18+
_projectName.setValue(this, value)
19+
}
20+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.tonstudio.tact.ide.build.project
2+
3+
import com.intellij.execution.actions.ConfigurationContext
4+
import com.intellij.execution.actions.LazyRunConfigurationProducer
5+
import com.intellij.execution.configurations.ConfigurationFactory
6+
import com.intellij.execution.configurations.ConfigurationTypeUtil
7+
import com.intellij.json.psi.JsonFile
8+
import com.intellij.json.psi.JsonProperty
9+
import com.intellij.json.psi.JsonStringLiteral
10+
import com.intellij.openapi.roots.TestSourcesFilter
11+
import com.intellij.openapi.util.Ref
12+
import com.intellij.psi.PsiElement
13+
14+
class TactBuildConfigurationProducer : LazyRunConfigurationProducer<TactBuildConfiguration>() {
15+
override fun getConfigurationFactory(): ConfigurationFactory =
16+
ConfigurationTypeUtil.findConfigurationType(TactBuildConfigurationType.ID)!!
17+
.configurationFactories[0]
18+
19+
override fun isConfigurationFromContext(
20+
configuration: TactBuildConfiguration,
21+
context: ConfigurationContext,
22+
): Boolean {
23+
val element = context.location?.psiElement ?: return false
24+
val containingFile = element.containingFile ?: return false
25+
if (TestSourcesFilter.isTestSources(containingFile.virtualFile, element.project)) {
26+
return false
27+
}
28+
29+
if (containingFile !is JsonFile || containingFile.name != "tact.config.json") {
30+
return false
31+
}
32+
33+
val projectName = getProjectName(element) ?: return false
34+
35+
return configuration.fileName == containingFile.virtualFile.path && configuration.projectName == projectName
36+
}
37+
38+
override fun setupConfigurationFromContext(
39+
configuration: TactBuildConfiguration,
40+
context: ConfigurationContext,
41+
sourceElement: Ref<PsiElement>,
42+
): Boolean {
43+
val element = sourceElement.get()
44+
val containingFile = element.containingFile ?: return false
45+
if (TestSourcesFilter.isTestSources(containingFile.virtualFile, element.project)) {
46+
return false
47+
}
48+
49+
if (containingFile !is JsonFile || containingFile.name != "tact.config.json") {
50+
return false
51+
}
52+
53+
val configPath = containingFile.virtualFile.path
54+
val projectName = getProjectName(element) ?: return false
55+
56+
configuration.name = "Build project \"$projectName\""
57+
configuration.fileName = configPath
58+
configuration.projectName = projectName
59+
return true
60+
}
61+
62+
private fun getProjectName(element: PsiElement?): String?{
63+
val jsonProperty = element?.parent?.parent as? JsonProperty
64+
val projectName = jsonProperty?.value as? JsonStringLiteral ?: return null
65+
return projectName.value
66+
}
67+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.tonstudio.tact.ide.build.project
2+
3+
import com.intellij.execution.ExecutionResult
4+
import com.intellij.execution.Executor
5+
import com.intellij.execution.configurations.CommandLineState
6+
import com.intellij.execution.configurations.GeneralCommandLine
7+
import com.intellij.execution.configurations.RunProfileState
8+
import com.intellij.execution.process.ProcessHandler
9+
import com.intellij.execution.runners.ExecutionEnvironment
10+
import com.intellij.execution.runners.ProgramRunner
11+
import org.tonstudio.tact.configurations.TactConfigurationUtil
12+
import org.tonstudio.tact.ide.build.TactProcessHandler
13+
import org.tonstudio.tact.ide.build.tryRelativizePath
14+
import org.tonstudio.tact.toolchain.TactToolchainService.Companion.toolchainSettings
15+
16+
class TactBuildConfigurationRunState(
17+
val env: ExecutionEnvironment,
18+
val conf: TactBuildConfiguration,
19+
) : RunProfileState {
20+
21+
override fun execute(executor: Executor, runner: ProgramRunner<*>): ExecutionResult? {
22+
val state = object : CommandLineState(env) {
23+
override fun startProcess(): ProcessHandler {
24+
val project = env.project
25+
26+
val root = project.toolchainSettings.toolchain().rootDir()
27+
if (root == null) {
28+
throw Error("Can't build: ${TactConfigurationUtil.TOOLCHAIN_NOT_SETUP}")
29+
}
30+
31+
val tactCompiler = root.findChild("bin")?.findChild("tact.js")
32+
if (tactCompiler == null) {
33+
throw Error("Can't build: Cannot find Tact compiler executable")
34+
}
35+
36+
val (configPath, projectRoot) = tryRelativizePath(project, conf.fileName)
37+
38+
val commandLine = GeneralCommandLine()
39+
.withExePath(tactCompiler.path)
40+
.withWorkDirectory(projectRoot)
41+
.withCharset(Charsets.UTF_8)
42+
.withParameters("--config", configPath)
43+
.withParameters("--project", conf.projectName)
44+
.withRedirectErrorStream(true)
45+
46+
return TactProcessHandler(commandLine)
47+
}
48+
}
49+
50+
return state.execute(executor, runner)
51+
}
52+
}

0 commit comments

Comments
 (0)