From 5d92d2db25ba20a7eb8244cfe3c40e882a9de3c1 Mon Sep 17 00:00:00 2001 From: Linh Pham Date: Tue, 20 Feb 2024 10:59:49 -0800 Subject: [PATCH] Migrate Project Gen to Skate (#744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR moves Project Gen Compose app to Skate so it can be called directly from the IDE. I made some modifications, including: * Add a new `ProjectGenWindow` that extends `DialogWrapper` to wrap around the Compose app * Create `projectgenlock` on the fly and delete it when dialog is dismissed * Add an Alert dialog on file path errors * Grab the `DarkMode` status from the IDE Theme using `JBColors.isBright` * Make the `Quit` button to exit the dialog. It was exiting the application before * Remove the old `TerminalWrapper` code Remaining work I haven't been able to port over. Running into some issues that might take more time * Updating the `Font` to `Lato` * Adding the icon of the app Test: Light mode https://github.com/slackhq/slack-gradle-plugin/assets/12748234/69b35521-a0b6-411a-a9fc-f8d1c7a8077a Dark mode https://github.com/slackhq/slack-gradle-plugin/assets/12748234/32e1a53c-a768-4bd2-afd5-c447b1114dcd --- gradle/libs.versions.toml | 5 + skate-plugin/build.gradle.kts | 10 + .../slack/sgp/intellij/SkatePluginSettings.kt | 8 - .../sgp/intellij/projectgen/DesktopColors.kt | 153 +++++++++ .../projectgen/PrefixTransformation.kt | 47 +++ .../projectgen/ProjectGenMenuAction.kt | 24 +- .../projectgen/ProjectGenPresenter.kt | 256 +++++++++++++++ .../intellij/projectgen/ProjectGenScreen.kt | 41 +++ .../sgp/intellij/projectgen/ProjectGenUi.kt | 177 ++++++++++ .../intellij/projectgen/ProjectGenWindow.kt | 93 ++++++ .../projectgen/RealTerminalViewWrapper.kt | 29 -- .../intellij/projectgen/SlackDesktopTheme.kt | 50 +++ .../projectgen/TerminalViewWrapper.kt | 22 -- .../sgp/intellij/projectgen/UiElement.kt | 80 +++++ .../slack/sgp/intellij/projectgen/models.kt | 305 ++++++++++++++++++ .../slack/sgp/intellij/util/ProjectUtils.kt | 2 - .../sgp/intellij/ProjectGenMenuActionTest.kt | 61 ---- .../intellij/fakes/FakeTerminalViewWrapper.kt | 37 --- 18 files changed, 1221 insertions(+), 179 deletions(-) create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/DesktopColors.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/PrefixTransformation.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenPresenter.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenScreen.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenUi.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenWindow.kt delete mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/RealTerminalViewWrapper.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/SlackDesktopTheme.kt delete mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/TerminalViewWrapper.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/UiElement.kt create mode 100644 skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/models.kt delete mode 100644 skate-plugin/src/test/kotlin/com/slack/sgp/intellij/ProjectGenMenuActionTest.kt delete mode 100644 skate-plugin/src/test/kotlin/com/slack/sgp/intellij/fakes/FakeTerminalViewWrapper.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c69e34af1..aa806914f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ agp = "8.2.2" agpAlpha = "8.3.0-rc02" anvil = "2.4.9-1-8" bugsnagGradle = "8.1.0" +circuit = "0.19.1" compose-jb = "1.5.12" coroutines = "1.8.0" dependencyAnalysisPlugin = "1.30.0" @@ -12,6 +13,7 @@ errorproneGradle = "3.0.1" jdk = "21" jna = "5.14.0" kotlin = "1.9.22" +kotlinPoet = "1.16.0" ksp = "1.9.22-1.0.17" ktfmt = "0.47" mavenPublish = "0.27.0" @@ -30,6 +32,7 @@ wire = "4.9.3" [plugins] bestPracticesPlugin = { id = "com.autonomousapps.plugin-best-practices-plugin", version = "0.10" } buildConfig = { id = "com.github.gmazzo.buildconfig", version = "5.0.0" } +compose = { id = "org.jetbrains.compose", version = "1.5.12"} dependencyAnalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyAnalysisPlugin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } @@ -55,6 +58,7 @@ autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.1.0" bugsnag = "com.bugsnag:bugsnag:3.7.1" clikt = "com.github.ajalt.clikt:clikt:4.2.2" commonsText = "org.apache.commons:commons-text:1.11.0" +circuit = { module = "com.slack.circuit:circuit-foundation", version.ref = "circuit"} coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "coroutines" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } @@ -84,6 +88,7 @@ gradlePlugins-wire = { module = "com.squareup.wire:wire-gradle-plugin", version. guava = "com.google.guava:guava:33.0.0-jre" kotlinCliUtil = "com.slack.cli:kotlin-cli-util:2.6.3" kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } +kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoet"} kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } ktfmt = { module = "com.facebook:ktfmt", version.ref = "ktfmt" } jgrapht = "org.jgrapht:jgrapht-core:1.5.2" diff --git a/skate-plugin/build.gradle.kts b/skate-plugin/build.gradle.kts index 2a5007013..07b92689d 100644 --- a/skate-plugin/build.gradle.kts +++ b/skate-plugin/build.gradle.kts @@ -24,6 +24,7 @@ plugins { alias(libs.plugins.intellij) alias(libs.plugins.pluginUploader) alias(libs.plugins.buildConfig) + alias(libs.plugins.compose) } group = "com.slack.intellij" @@ -92,7 +93,16 @@ buildConfig { } dependencies { + implementation(compose.animation) + implementation(compose.desktop.currentOs) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.material3) + implementation(compose.ui) implementation(libs.bugsnag) { exclude(group = "org.slf4j") } + implementation(libs.circuit) + implementation(libs.gradlePlugins.compose) + implementation(libs.kotlin.poet) implementation(libs.okhttp) implementation(libs.okhttp.loggingInterceptor) implementation(projects.tracing) diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt index 1da960a92..eff388cd5 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkatePluginSettings.kt @@ -26,7 +26,6 @@ import org.jetbrains.annotations.VisibleForTesting internal const val DEFAULT_TRANSLATOR_SOURCE_MODELS_PACKAGE_NAME = "slack.api.schemas" @VisibleForTesting internal const val DEFAULT_TRANSLATOR_FILE_NAME_SUFFIX = "Translator.kt" @VisibleForTesting internal const val DEFAULT_TRANSLATOR_ENUM_IDENTIFIER = "name" -internal const val DEFAULT_PROJECT_GEN_CLI_COMMAND = "./slackw generate-project" internal const val DEFAULT_CODE_OWNER_FILE_PATH = "config/code-ownership/code_ownership.csv" /** Manages user-specific settings for the Skate plugin */ @@ -70,12 +69,6 @@ class SkatePluginSettings : SimplePersistentStateComponent TerminalViewWrapper = ::RealTerminalViewWrapper, - private val offline: Boolean = false, -) : AnAction() { +class ProjectGenMenuAction @JvmOverloads constructor(private val offline: Boolean = false) : + AnAction() { private val skateSpanBuilder = SkateSpanBuilder() private val startTimestamp = Instant.now() override fun actionPerformed(e: AnActionEvent) { val currentProject: Project = e.project ?: return - val projectGenRunCommand = currentProject.projectGenRunCommand() if (!currentProject.isProjectGenMenuActionEnabled()) return - - executeProjectGenCommand(projectGenRunCommand, currentProject) + ProjectGenWindow(currentProject).show() if (currentProject.isTracingEnabled()) { sendUsageTrace(currentProject) } } - fun executeProjectGenCommand(command: String, project: Project) { - val terminalCommand = TerminalCommand(command, project.basePath, PROJECT_GEN_TAB_NAME) - terminalViewWrapper(project).executeCommand(terminalCommand) - skateSpanBuilder.addSpanTag("event", SkateTracingEvent(PROJECT_GEN_OPENED)) - } - fun sendUsageTrace(project: Project) { + skateSpanBuilder.addSpanTag("event", SkateTracingEvent(PROJECT_GEN_OPENED)) SkateTraceReporter(project, offline) .createPluginUsageTraceAndSendTrace( "project_generator", @@ -63,8 +51,4 @@ constructor( skateSpanBuilder.getKeyValueList(), ) } - - companion object { - const val PROJECT_GEN_TAB_NAME: String = "ProjectGen" - } } diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenPresenter.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenPresenter.kt new file mode 100644 index 000000000..d53e4e37e --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenPresenter.kt @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.sgp.intellij.projectgen + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.slack.circuit.runtime.presenter.Presenter +import java.io.File +import org.apache.commons.io.FileExistsException +import slack.tooling.projectgen.* +import slack.tooling.projectgen.CheckboxElement +import slack.tooling.projectgen.ComposeFeature +import slack.tooling.projectgen.DaggerFeature +import slack.tooling.projectgen.DividerElement +import slack.tooling.projectgen.KotlinFeature +import slack.tooling.projectgen.SectionElement +import slack.tooling.projectgen.TextElement + +internal class ProjectGenPresenter( + private val rootDir: String, + private val onDismissDialog: () -> Unit, +) : Presenter { + private val path = + TextElement( + "", + "Path (Required)", + description = "The Gradle-style project path (e.g. ':emoji')", + prefixTransformation = ":", + ) + + private val packageName = + TextElement( + "", + "Package Name (Required)", + description = + "The project package name (must start with 'slack.') This is used for both source packages and android.namespace.", + prefixTransformation = "slack.", + ) + + private val android = + CheckboxElement( + false, + name = "Android", + hint = "This project is an Android library project (default is JVM only).", + ) + private val androidViewBinding = + CheckboxElement(false, name = "ViewBinding", hint = "Enables ViewBinding.", indentLevel = 1) + + private val androidResources = + CheckboxElement(false, name = "Resources", hint = "Enables android resources.", indentLevel = 1) + + private val androidResourcePrefix = + TextElement( + "", + "Resource prefix (required)", + description = + "Android library projects that enable resources should use a resource prefix to avoid resource merging collisions. Examples include 'sk_', 'emoji_', 'slack_', etc.", + indentLevel = 3, + initialVisibility = false, + dependentElements = listOf(androidResources), + ) + + private val robolectric = + CheckboxElement( + false, + name = "Robolectric", + hint = "Enables Robolectric for unit tests.", + indentLevel = 1, + ) + + private val androidTest = + CheckboxElement( + false, + name = "Android Tests", + hint = "Enables instrumentation tests.", + indentLevel = 1, + ) + + private val dagger = CheckboxElement(false, name = "Dagger", hint = "Enables Dagger and Anvil.") + + private val daggerRuntimeOnly = + CheckboxElement( + false, + name = "Runtime Only", + hint = "Only add the Dagger runtime dependencies, no code gen.", + indentLevel = 1, + ) + + private val compose = CheckboxElement(false, name = "Compose", hint = "Enables Jetpack Compose.") + + private val uiElements = + mutableStateListOf( + SectionElement("Path Details", "Required"), + TextElement(rootDir, "Project root dir", readOnly = true), + path, + packageName, + DividerElement, + SectionElement("Features", "Select all that apply"), + android, + androidResources, + androidResourcePrefix, + androidViewBinding, + robolectric, + androidTest, + dagger, + daggerRuntimeOnly, + compose, + ) + + @Composable + override fun present(): ProjectGenScreen.State { + // Wire UI element dependencies + androidViewBinding.isVisible = android.isChecked + androidResources.isVisible = android.isChecked + androidResourcePrefix.isVisible = androidResources.isChecked + robolectric.isVisible = android.isChecked + androidTest.isVisible = android.isChecked + daggerRuntimeOnly.isVisible = dagger.isChecked + + var showDoneDialog by remember { mutableStateOf(false) } + var showErrorDialog by remember { mutableStateOf(false) } + + return ProjectGenScreen.State( + uiElements = uiElements, + showDoneDialog = showDoneDialog, + showErrorDialog = showErrorDialog, + canGenerate = path.value.isNotBlank() && packageName.value.isNotBlank(), + ) { event -> + when (event) { + ProjectGenScreen.Event.Quit -> onDismissDialog() + ProjectGenScreen.Event.Reset -> { + showDoneDialog = false + showErrorDialog = false + resetElements() + } + ProjectGenScreen.Event.Generate -> { + try { + generate() + showDoneDialog = true + } catch (e: FileExistsException) { + showDoneDialog = false + showErrorDialog = true + } + } + } + } + } + + private fun resetElements() { + path.reset() + packageName.reset() + android.reset() + androidResourcePrefix.reset() + androidViewBinding.reset() + androidResources.reset() + dagger.reset() + daggerRuntimeOnly.reset() + robolectric.reset() + compose.reset() + } + + private fun generate() { + generate( + rootDir = File(rootDir), + path = ":${path.value}", + packageName = "slack.${packageName.value}", + android = android.isChecked, + androidFeatures = + buildSet { + if (androidViewBinding.isChecked) { + add("view-binding") + } + }, + androidResourcePrefix = androidResourcePrefix.value.takeIf { androidResources.isChecked }, + dagger = dagger.isChecked, + daggerFeatures = + buildSet { + if (daggerRuntimeOnly.isChecked) { + add("runtime-only") + } + }, + robolectric = robolectric.isChecked, + compose = compose.isChecked, + androidTest = androidTest.isChecked, + ) + } + + @Suppress("LongParameterList") + private fun generate( + rootDir: File, + path: String, + packageName: String, + android: Boolean, + androidFeatures: Set, + androidResourcePrefix: String?, + dagger: Boolean, + daggerFeatures: Set, + robolectric: Boolean, + compose: Boolean, + androidTest: Boolean, + ) { + val features = mutableListOf() + val androidLibraryEnabled = + android && (androidFeatures.isNotEmpty() || androidTest || androidResourcePrefix != null) + if (androidLibraryEnabled) { + features += + AndroidLibraryFeature( + androidResourcePrefix, + viewBindingEnabled = "view-binding" in androidFeatures, + androidTest = androidTest, + packageName = packageName, + ) + } + + features += KotlinFeature(packageName, isAndroid = android) + + if (dagger) { + features += DaggerFeature(runtimeOnly = "runtime-only" in daggerFeatures) + } + + if (compose) { + features += ComposeFeature + } + + if (robolectric) { + features += RobolectricFeature + } + + val buildFile = BuildFile(emptyList()) + val readMeFile = ReadMeFile() + + val project = Project(path, buildFile, readMeFile, features) + if (project.checkValidPath(rootDir)) { + project.writeTo(rootDir) + } else { + throw FileExistsException() + } + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenScreen.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenScreen.kt new file mode 100644 index 000000000..fb8fad3d9 --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenScreen.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.sgp.intellij.projectgen + +import androidx.compose.runtime.snapshots.SnapshotStateList +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState +import com.slack.circuit.runtime.screen.Screen +import slack.tooling.projectgen.UiElement + +internal object ProjectGenScreen : Screen { + data class State( + val uiElements: SnapshotStateList, + // TODO make this a "next page" instead? + val showDoneDialog: Boolean, + val showErrorDialog: Boolean, + val canGenerate: Boolean, + val eventSink: (Event) -> Unit, + ) : CircuitUiState + + sealed interface Event : CircuitUiEvent { + object Generate : Event + + object Quit : Event + + object Reset : Event + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenUi.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenUi.kt new file mode 100644 index 000000000..fb1589f3d --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenUi.kt @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.sgp.intellij.projectgen + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring.StiffnessMediumLow +import androidx.compose.animation.core.spring +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.VerticalScrollbar +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Divider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import slack.tooling.projectgen.CheckboxElement +import slack.tooling.projectgen.DividerElement +import slack.tooling.projectgen.SectionElement +import slack.tooling.projectgen.TextElement + +private const val INDENT_SIZE = 16 // dp + +// @OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ProjectGen(state: ProjectGenScreen.State, modifier: Modifier = Modifier) { + if (state.showDoneDialog) { + StatusDialog( + text = "Done! Don't forget to re-sync Android Studio!", + onQuit = { state.eventSink(ProjectGenScreen.Event.Quit) }, + onDismiss = { state.eventSink(ProjectGenScreen.Event.Reset) }, + ) + } + if (state.showErrorDialog) { + StatusDialog( + text = "Failed to generate projects since project already exists", + onQuit = { state.eventSink(ProjectGenScreen.Event.Quit) }, + onDismiss = { state.eventSink(ProjectGenScreen.Event.Reset) }, + ) + } + val scrollState = rememberScrollState(0) + Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { + Column( + Modifier.padding(16.dp) + .verticalScroll(scrollState) + .animateContentSize(spring(stiffness = StiffnessMediumLow)), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + for (element in state.uiElements) { + if (!element.isVisible) continue + when (element) { + DividerElement -> { + Divider() + } + is SectionElement -> { + Column { + Text(element.title, style = MaterialTheme.typography.titleLarge) + Text(element.description, style = MaterialTheme.typography.bodySmall) + } + } + is CheckboxElement -> { + Feature( + element.name, + element.hint, + element.isChecked, + indent = (element.indentLevel * INDENT_SIZE).dp, + onEnabledChange = { element.isChecked = it }, + ) + } + is TextElement -> { + Column(Modifier.padding(start = (element.indentLevel * INDENT_SIZE).dp)) { + // TODO Add validation? + TextField( + element.value, + label = { Text(element.label) }, + onValueChange = { newValue -> element.value = newValue }, + visualTransformation = + element.prefixTransformation?.let(::PrefixTransformation) + ?: VisualTransformation.None, + readOnly = element.readOnly, + enabled = element.enabled, + ) + element.description?.let { Text(it, style = MaterialTheme.typography.bodySmall) } + } + } + } + } + Button( + modifier = Modifier.fillMaxWidth(), + enabled = state.canGenerate, + onClick = { state.eventSink(ProjectGenScreen.Event.Generate) }, + content = { Text("Generate") }, + ) + } + VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), + adapter = rememberScrollbarAdapter(scrollState), + ) + } + } +} + +@Composable +private fun Feature( + name: String, + hint: String, + enabled: Boolean, + modifier: Modifier = Modifier, + indent: Dp = 0.dp, + onEnabledChange: (Boolean) -> Unit, +) { + Row(modifier.padding(start = indent), verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = enabled, onCheckedChange = onEnabledChange) + Column { + Text(name) + Text(text = hint, style = MaterialTheme.typography.bodySmall) + } + } +} + +@Preview +@Composable +private fun PreviewFeature() { + Feature( + name = "Compose for Desktop", + hint = "Enable Compose for Desktop support", + enabled = true, + onEnabledChange = {}, + ) +} + +@Composable +private fun StatusDialog(text: String, onQuit: () -> Unit, onDismiss: () -> Unit) { + // No M3 AlertDialog in compose-jb yet + // https://github.com/JetBrains/compose-multiplatform/issues/2037 + @Suppress("ComposeM2Api") + (AlertDialog( + onDismissRequest = { onDismiss() }, + confirmButton = { Button(onClick = { onQuit() }) { Text("Quit") } }, + dismissButton = { Button(onClick = { onDismiss() }) { Text("Dismiss") } }, + text = { Text(text) }, + )) +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenWindow.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenWindow.kt new file mode 100644 index 000000000..dcc68b1bf --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/ProjectGenWindow.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.sgp.intellij.projectgen + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.awt.ComposePanel +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.slack.circuit.foundation.Circuit +import com.slack.circuit.foundation.CircuitContent +import com.slack.circuit.runtime.ui.ui +import java.io.File +import java.nio.file.FileSystems +import java.nio.file.Paths +import javax.swing.Action +import javax.swing.JComponent + +class ProjectGenWindow(private val currentProject: Project?) : DialogWrapper(currentProject) { + init { + init() + title = "Project Generator" + } + + override fun createCenterPanel(): JComponent { + return ComposePanel().apply { + setBounds(0, 0, 600, 800) + setContent { dialogContent() } + } + } + + @Composable + fun dialogContent() { + val rootDir = remember { + val path = + currentProject?.basePath + ?: FileSystems.getDefault() + .getPath(".") + .toAbsolutePath() + .normalize() + .toFile() + .absolutePath + check(Paths.get(path).toFile().isDirectory) { "Must pass a valid directory" } + path + } + File("$rootDir/.projectgenlock").createNewFile() + + val circuit = remember { + Circuit.Builder() + .addPresenterFactory { _, _, _ -> ProjectGenPresenter(rootDir, ::doOKAction) } + .addUiFactory { _, _ -> + ui { state, modifier -> ProjectGen(state, modifier) } + } + .build() + } + SlackDesktopTheme() { CircuitContent(ProjectGenScreen, circuit = circuit) } + } + + /* Disable default OK and Cancel action button in Dialog window. */ + override fun createActions(): Array = emptyArray() + + override fun doCancelAction() { + super.doCancelAction() + // Remove projectlock file when exit application + deleteProjectLock() + } + + override fun doOKAction() { + super.doOKAction() + // Remove projectlock file when exit application + deleteProjectLock() + } + + private fun deleteProjectLock() { + val projectLockFile = File(currentProject?.basePath + "/.projectgenlock") + if (projectLockFile.exists()) { + projectLockFile.delete() + } + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/RealTerminalViewWrapper.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/RealTerminalViewWrapper.kt deleted file mode 100644 index f8fc079be..000000000 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/RealTerminalViewWrapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2023 Slack Technologies, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.slack.sgp.intellij.projectgen - -import com.intellij.openapi.project.Project -import org.jetbrains.plugins.terminal.TerminalView - -/* Wrapper around Jetbrains TerminalView to help simplify testing */ -class RealTerminalViewWrapper(private val project: Project) : TerminalViewWrapper { - override fun executeCommand(command: TerminalCommand) { - // Create new terminal window to run given command - TerminalView.getInstance(project) - .createLocalShellWidget(command.projectPath, command.tabName) - .executeCommand(command.command) - } -} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/SlackDesktopTheme.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/SlackDesktopTheme.kt new file mode 100644 index 000000000..15e49083f --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/SlackDesktopTheme.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.sgp.intellij.projectgen + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import com.intellij.ui.JBColor + +private val FONT_FAMILY = FontFamily.Default + +@Composable +fun SlackDesktopTheme(useDarkMode: Boolean = !JBColor.isBright(), content: @Composable () -> Unit) { + val typography = + MaterialTheme.typography.copy( + displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = FONT_FAMILY), + displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = FONT_FAMILY), + displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = FONT_FAMILY), + headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = FONT_FAMILY), + headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = FONT_FAMILY), + headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = FONT_FAMILY), + titleLarge = MaterialTheme.typography.titleLarge.copy(fontFamily = FONT_FAMILY), + titleMedium = MaterialTheme.typography.titleMedium.copy(fontFamily = FONT_FAMILY), + titleSmall = MaterialTheme.typography.titleSmall.copy(fontFamily = FONT_FAMILY), + bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = FONT_FAMILY), + bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = FONT_FAMILY), + bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = FONT_FAMILY), + labelLarge = MaterialTheme.typography.labelLarge.copy(fontFamily = FONT_FAMILY), + labelMedium = MaterialTheme.typography.labelMedium.copy(fontFamily = FONT_FAMILY), + labelSmall = MaterialTheme.typography.labelSmall.copy(fontFamily = FONT_FAMILY), + ) + MaterialTheme( + colorScheme = if (useDarkMode) DesktopColors.DarkTheme else DesktopColors.LightTheme, + typography = typography, + content = content, + ) +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/TerminalViewWrapper.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/TerminalViewWrapper.kt deleted file mode 100644 index 583ba1c93..000000000 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/TerminalViewWrapper.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2023 Slack Technologies, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.slack.sgp.intellij.projectgen - -interface TerminalViewWrapper { - fun executeCommand(command: TerminalCommand) -} - -data class TerminalCommand(val command: String, val projectPath: String?, val tabName: String) diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/UiElement.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/UiElement.kt new file mode 100644 index 000000000..6f9edb919 --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/UiElement.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.projectgen + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +@Stable +internal sealed interface UiElement { + fun reset() {} + + var isVisible: Boolean +} + +@Immutable +internal object DividerElement : UiElement { + override var isVisible: Boolean = true +} + +@Immutable +internal data class SectionElement(val title: String, val description: String) : UiElement { + override var isVisible: Boolean = true +} + +internal class CheckboxElement( + private val initialValue: Boolean, + val name: String, + val hint: String, + val indentLevel: Int = 0, + isVisible: Boolean = true, +) : UiElement { + var isChecked by mutableStateOf(initialValue) + + override fun reset() { + isChecked = initialValue + } + + override var isVisible: Boolean by mutableStateOf(isVisible) +} + +@Suppress("LongParameterList") +internal class TextElement( + private val initialValue: String, + val label: String, + val description: String? = null, + val indentLevel: Int = 0, + val readOnly: Boolean = false, + val prefixTransformation: String? = null, + private val initialVisibility: Boolean = true, + // List of tags this element depends on + private val dependentElements: List = emptyList(), +) : UiElement { + var value by mutableStateOf(initialValue) + + val enabled by derivedStateOf { !readOnly && dependentElements.all { it.isChecked } } + + override fun reset() { + value = initialValue + isVisible = initialVisibility + } + + override var isVisible: Boolean by mutableStateOf(initialVisibility) +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/models.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/models.kt new file mode 100644 index 000000000..cba92a613 --- /dev/null +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/projectgen/models.kt @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.projectgen + +import com.squareup.kotlinpoet.FileSpec +import java.io.File + +internal data class Project( + // Gradle path + val path: String, + val buildFile: BuildFile, + val readMeFile: ReadMeFile, + val features: List, +) { + + fun checkValidPath(rootDir: File): Boolean { + val projectDir = rootDir.resolve(path.removePrefix(":").replace(":", "/")) + return !projectDir.exists() + } + + fun writeTo(rootDir: File) { + val projectDir = rootDir.resolve(path.removePrefix(":").replace(":", "/")).apply { mkdirs() } + buildFile.buildFileSpec(features).writeTo(projectDir) + + readMeFile.writeTo(projectDir) + + for (feature in features) { + feature.renderFiles(projectDir) + } + + val settingsFile = File(rootDir, "settings-all.gradle.kts") + val includedProjects = + settingsFile + .readLines() + .asSequence() + .filter { it.startsWith(" \":") } + .map { it.trim().removeSuffix(",").removeSurrounding("\"") } + .plus(path) + .sorted() + + settingsFile.writeText( + includedProjects.joinToString( + "\n", + prefix = "// Please keep these in alphabetical order!\ninclude(\n", + postfix = "\n)\n", + ) { + " \"$it\"," + } + ) + } +} + +internal data class BuildFile(val dependencies: List) { + fun buildFileSpec(features: List): FileSpec { + val fileSpecBuilder = + FileSpec.scriptBuilder("build.gradle").apply { + // Plugins block + beginControlFlow("plugins") + addStatement("alias(libs.plugins.slack.base)") + // TODO do we ever need to ensure no dupes? + features.filterIsInstance().forEach { it.writeToPlugins(this) } + endControlFlow() + + // android build features + val enabledAndroidBuildFeatures = + features.filterIsInstance().filter { + it.hasAndroidBuildFeaturesToEnable + } + if (enabledAndroidBuildFeatures.isNotEmpty()) { + addStatement("") + beginControlFlow("android") + beginControlFlow("buildFeatures") + enabledAndroidBuildFeatures.forEach { it.writeToAndroidBuildFeatures(this) } + endControlFlow() + endControlFlow() + } + + // slack features + val slackFeatures = features.filterIsInstance() + val slackAndroidFeatures = features.filterIsInstance() + if (slackFeatures.isNotEmpty() || slackAndroidFeatures.isNotEmpty()) { + addStatement("") + beginControlFlow("slack") + if (slackFeatures.isNotEmpty()) { + beginControlFlow("features") + for (feature in slackFeatures) { + feature.writeToSlackFeatures(this) + } + endControlFlow() + } + if (slackAndroidFeatures.isNotEmpty()) { + beginControlFlow("android") + beginControlFlow("features") + slackAndroidFeatures.forEach { it.writeToSlackAndroidFeatures(this) } + endControlFlow() + endControlFlow() + } + endControlFlow() + } + + // dependencies + addStatement("") + beginControlFlow("dependencies") + for (dep in dependencies + DEFAULT_DEPENDENCIES) { + dep.writeTo(this) + } + for (dep in features.filterIsInstance()) { + dep.writeToDependencies(this) + } + endControlFlow() + } + return fileSpecBuilder.build() + } + + companion object { + val DEFAULT_DEPENDENCIES = + listOf( + Dependency("implementation", "libs.androidx.annotation"), + Dependency("testImplementation", "libs.testing.junit"), + Dependency("testImplementation", "libs.testing.truth"), + ) + } +} + +internal data class Dependency( + val configuration: String, + // Either libs.* or ":emoji" path format + val path: String, +) { + private val isLocal: Boolean = !path.startsWith("libs.") + + fun writeTo(builder: FileSpec.Builder) { + val finalPath = + if (isLocal) { + "project(\"$path\")" + } else { + path + } + builder.addStatement("$configuration($finalPath)") + } +} + +internal interface Feature { + fun renderFiles(projectDir: File) {} +} + +internal interface PluginVisitor { + // Callback within plugins { } block + fun writeToPlugins(builder: FileSpec.Builder) +} + +internal interface SlackFeatureVisitor { + // Callback within slack.features { } block + fun writeToSlackFeatures(builder: FileSpec.Builder) +} + +internal interface SlackAndroidFeatureVisitor { + // Callback within slack.android.features { } block + fun writeToSlackAndroidFeatures(builder: FileSpec.Builder) +} + +internal interface AndroidBuildFeatureVisitor { + val hasAndroidBuildFeaturesToEnable: Boolean + get() = true + + // Callback within android.buildFeatures { } block + fun writeToAndroidBuildFeatures(builder: FileSpec.Builder) +} + +internal interface DependenciesVisitor { + // Callback within dependencies { } block + fun writeToDependencies(builder: FileSpec.Builder) +} + +internal data class AndroidLibraryFeature( + val resourcesPrefix: String?, + val viewBindingEnabled: Boolean, + val androidTest: Boolean, + val packageName: String, +) : Feature, PluginVisitor, AndroidBuildFeatureVisitor, SlackAndroidFeatureVisitor { + override fun writeToPlugins(builder: FileSpec.Builder) { + builder.addStatement("alias(libs.plugins.android.library)") + } + + override val hasAndroidBuildFeaturesToEnable: Boolean + get() = resourcesPrefix != null || viewBindingEnabled + + override fun writeToAndroidBuildFeatures(builder: FileSpec.Builder) { + builder.apply { + if (viewBindingEnabled) { + addStatement("viewBinding = true") + } + } + } + + override fun writeToSlackAndroidFeatures(builder: FileSpec.Builder) { + if (androidTest) { + builder.addStatement("androidTest()") + } + resourcesPrefix?.let { builder.addStatement("resources(\"$it\")") } + } + + override fun renderFiles(projectDir: File) { + if (androidTest) { + val androidTestDir = File(projectDir, "src/androidTest") + androidTestDir.mkdirs() + + // Write the manifest file + File(androidTestDir, "AndroidManifest.xml") + // language=XML + .writeText( + """ + + + + + + """ + .trimIndent() + ) + + // Write the placeholder test file + writePlaceholderFileTo(androidTestDir, packageName) + } + } +} + +internal data class KotlinFeature(val packageName: String, val isAndroid: Boolean) : + Feature, PluginVisitor { + override fun writeToPlugins(builder: FileSpec.Builder) { + val marker = if (isAndroid) "android" else "jvm" + builder.addStatement("alias(libs.plugins.kotlin.$marker)") + } + + override fun renderFiles(projectDir: File) { + writePlaceholderFileTo(projectDir.resolve("src/main"), packageName) + } +} + +private fun writePlaceholderFileTo(sourceSetDir: File, packageName: String) { + val mainSrcDir = + sourceSetDir.resolve("kotlin/${packageName.replace(".", "/")}").apply { mkdirs() } + File(mainSrcDir, "Placeholder.kt") + .writeText( + """ + package $packageName + + /** This file exists just to create your new project's directories. Rename or delete this! */ + private abstract class Placeholder + """ + .trimIndent() + ) +} + +internal data class DaggerFeature(val runtimeOnly: Boolean) : Feature, SlackFeatureVisitor { + override fun writeToSlackFeatures(builder: FileSpec.Builder) { + // All these args are false by default, so only add arguments for enabled ones! + val args = + mapOf("runtimeOnly" to runtimeOnly) + .filterValues { it } + .entries + .joinToString(",·") { (k, v) -> "$k·=·$v" } + builder.addStatement("dagger($args)") + } +} + +internal object RobolectricFeature : Feature, SlackAndroidFeatureVisitor { + override fun writeToSlackAndroidFeatures(builder: FileSpec.Builder) { + builder.addStatement("robolectric()") + } +} + +internal object ComposeFeature : Feature, SlackFeatureVisitor { + override fun writeToSlackFeatures(builder: FileSpec.Builder) { + builder.addStatement("compose()") + } +} + +internal class ReadMeFile { + fun writeTo(projectDir: File) { + val projectName = projectDir.name + File(projectDir, "README.md") + .apply { createNewFile() } + .bufferedWriter() + .use { writer -> + val underline = "=".repeat(projectName.length) + writer.write(projectName) + writer.appendLine() + writer.write(underline) + } + } +} diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt index 2ed41173c..b24dcfc8f 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/util/ProjectUtils.kt @@ -29,6 +29,4 @@ fun Project.isTracingEnabled(): Boolean = settings().isTracingEnabled fun Project.isProjectGenMenuActionEnabled(): Boolean = settings().isProjectGenMenuActionEnabled -fun Project.projectGenRunCommand(): String = settings().projectGenRunCommand - fun Project.tracingEndpoint(): String? = settings().tracingEndpoint diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/ProjectGenMenuActionTest.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/ProjectGenMenuActionTest.kt deleted file mode 100644 index 8a665bde9..000000000 --- a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/ProjectGenMenuActionTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2023 Slack Technologies, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.slack.sgp.intellij - -import com.intellij.openapi.components.service -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.slack.sgp.intellij.fakes.FakeTerminalViewWrapper -import com.slack.sgp.intellij.projectgen.ProjectGenMenuAction -import com.slack.sgp.intellij.projectgen.ProjectGenMenuAction.Companion.PROJECT_GEN_TAB_NAME -import com.slack.sgp.intellij.projectgen.TerminalCommand - -class ProjectGenMenuActionTest : BasePlatformTestCase() { - override fun setUp() { - super.setUp() - - // Reset relevant settings. - val settings = project.service() - settings.isProjectGenMenuActionEnabled = true - settings.projectGenRunCommand = "echo Hello World" - } - - fun testCorrectArgumentsPassedIntoTerminalView() { - val fakeTerminalViewWrapper = FakeTerminalViewWrapper() - val action = - ProjectGenMenuAction(terminalViewWrapper = { fakeTerminalViewWrapper }, offline = true) - - // Perform action - myFixture.testAction(action) - - // Verify right arguments are passed into the terminal - val expectedCommand = - TerminalCommand("echo Hello World", project.basePath, PROJECT_GEN_TAB_NAME) - fakeTerminalViewWrapper.assertCommand(expectedCommand) - } - - fun testTerminalViewNotRunningWhenActionDisabled() { - val fakeTerminalViewWrapper = FakeTerminalViewWrapper() - val action = - ProjectGenMenuAction(terminalViewWrapper = { fakeTerminalViewWrapper }, offline = true) - project.service().isProjectGenMenuActionEnabled = false - - // Perform action - myFixture.testAction(action) - - // Verify action didn't run any terminal command - fakeTerminalViewWrapper.assertEmptyCommand() - } -} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/fakes/FakeTerminalViewWrapper.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/fakes/FakeTerminalViewWrapper.kt deleted file mode 100644 index aa1930922..000000000 --- a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/fakes/FakeTerminalViewWrapper.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2023 Slack Technologies, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.slack.sgp.intellij.fakes - -import com.google.common.truth.Truth.assertThat -import com.slack.sgp.intellij.projectgen.TerminalCommand -import com.slack.sgp.intellij.projectgen.TerminalViewWrapper -import org.jetbrains.kotlin.backend.common.push - -class FakeTerminalViewWrapper : TerminalViewWrapper { - private val commands = ArrayDeque() - - override fun executeCommand(command: TerminalCommand) { - commands.push(command) - } - - fun assertCommand(expected: TerminalCommand) { - assertThat(commands.removeFirst()).isEqualTo(expected) - } - - fun assertEmptyCommand() { - assertThat(commands).isEmpty() - } -}