├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── kotlinc.xml ├── vcs.xml ├── .gitignore ├── compiler.xml ├── encodings.xml ├── gradle.xml ├── misc.xml └── jarRepositories.xml ├── plugin-api-test-client ├── settings.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ ├── test │ │ ├── java │ │ │ └── com │ │ │ │ └── plugin │ │ │ │ └── testviews │ │ │ │ ├── Role.java │ │ │ │ ├── UserRepository.java │ │ │ │ ├── CustomButton.java │ │ │ │ ├── HelloWorldView.java │ │ │ │ ├── CustomView.java │ │ │ │ ├── SecurityConfig.java │ │ │ │ ├── User.java │ │ │ │ └── UserDetailsServiceImpl.java │ │ └── resources │ │ │ └── samples │ │ │ └── image.png │ └── main │ │ └── java │ │ └── com │ │ └── vaadin │ │ └── plugin │ │ ├── TestApplication.java │ │ ├── Message.java │ │ └── Client.java ├── .idea │ ├── kotlinc.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── compiler.xml │ ├── gradle.xml │ └── jarRepositories.xml ├── .gitignore ├── build.gradle.kts └── gradlew.bat ├── plugin ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ ├── vaadin-with-microservices-jvm.xml │ │ │ │ ├── vaadin-injections.xml │ │ │ │ ├── vaadin-with-ultimate.xml │ │ │ │ ├── vaadin-with-microservices.xml │ │ │ │ ├── vaadin-with-javascript.xml │ │ │ │ └── pluginIcon.svg │ │ │ ├── icon.png │ │ │ ├── hotswap-agent.jar │ │ │ └── vaadin │ │ │ │ ├── flow-language-injections.xml │ │ │ │ └── icons │ │ │ │ ├── module.svg │ │ │ │ ├── vaadin.svg │ │ │ │ ├── vaadin_dark.svg │ │ │ │ ├── rerun.svg │ │ │ │ ├── rerun_dark.svg │ │ │ │ ├── hilla.svg │ │ │ │ ├── debug.svg │ │ │ │ └── debug_dark.svg │ │ └── kotlin │ │ │ └── com │ │ │ └── vaadin │ │ │ └── plugin │ │ │ ├── copilot │ │ │ ├── handler │ │ │ │ ├── Handler.kt │ │ │ │ ├── HandlerResponse.kt │ │ │ │ ├── RefreshHandler.kt │ │ │ │ ├── GetModulePathsHandler.kt │ │ │ │ ├── RedoHandler.kt │ │ │ │ ├── HeartbeatHandler.kt │ │ │ │ ├── GetVaadinRoutesHandler.kt │ │ │ │ ├── ReloadMavenModuleHandler.kt │ │ │ │ ├── WriteBase64FileHandler.kt │ │ │ │ ├── CompileFilesHandler.kt │ │ │ │ ├── GetVaadinEntitiesHandler.kt │ │ │ │ ├── GetVaadinComponentsHandler.kt │ │ │ │ ├── GetVaadinSecurityHandler.kt │ │ │ │ ├── RestartApplicationHandler.kt │ │ │ │ ├── DeleteFileHandler.kt │ │ │ │ ├── ShowInIdeHandler.kt │ │ │ │ ├── GetVaadinVersionHandler.kt │ │ │ │ ├── UndoHandler.kt │ │ │ │ ├── AbstractHandler.kt │ │ │ │ └── WriteFileHandler.kt │ │ │ ├── CommandRequest.kt │ │ │ ├── service │ │ │ │ ├── CopilotDotfileService.kt │ │ │ │ ├── CopilotRestService.kt │ │ │ │ ├── CompilationStatusManagerService.kt │ │ │ │ ├── CopilotDotfileServiceImpl.kt │ │ │ │ └── CopilotUndoManager.kt │ │ │ ├── RestUtil.kt │ │ │ ├── listeners │ │ │ │ ├── CopilotVaadinProjectListener.kt │ │ │ │ └── CopilotDynamicPluginListener.kt │ │ │ ├── DefaultProgramPatcher.kt │ │ │ └── CopilotErrorHandler.kt │ │ │ ├── endpoints │ │ │ ├── Entity.kt │ │ │ ├── VaadinRoute.kt │ │ │ ├── VaadinSecurity.kt │ │ │ ├── VaadinUserDetails.kt │ │ │ ├── VaadinComponent.kt │ │ │ ├── VaadinHillaEndpointsProvider.kt │ │ │ ├── VaadinImplicitUsageProvider.kt │ │ │ ├── VaadinEndpointsProvider.kt │ │ │ ├── VaadinFlowEndpointsProvider.kt │ │ │ ├── VaadinReferenceContributor.kt │ │ │ └── VaadinUrlResolver.kt │ │ │ ├── starter │ │ │ ├── DownloadableModel.kt │ │ │ ├── StarterSupportMatrixElement.kt │ │ │ ├── StarterSupport.kt │ │ │ ├── HelloWorldModel.kt │ │ │ └── StarterProjectModel.kt │ │ │ ├── utils │ │ │ ├── ArtifactIdUtil.kt │ │ │ ├── VaadinIcons.kt │ │ │ ├── VaadinProjectUtil.kt │ │ │ ├── AmpliUtil.kt │ │ │ └── DownloadUtil.kt │ │ │ ├── listeners │ │ │ └── VaadinProjectListener.kt │ │ │ ├── actions │ │ │ ├── VaadinOnSaveInfoProvider.kt │ │ │ ├── InstallOrUpdateHotSwapAction.kt │ │ │ ├── DebugUsingHotSwapAgentAction.kt │ │ │ ├── VaadinCompileOnSaveActionInfo.kt │ │ │ └── VaadinCompileOnSaveAction.kt │ │ │ ├── ui │ │ │ ├── VaadinStatusBarWidgetFactory.kt │ │ │ ├── settings │ │ │ │ ├── VaadinSettings.kt │ │ │ │ ├── VaadinSettingsConfigurable.kt │ │ │ │ └── VaadinSettingsComponent.kt │ │ │ └── VaadinStatusBarWidget.kt │ │ │ ├── module │ │ │ ├── StarterProjectPanel.kt │ │ │ ├── VaadinProjectWizardStep.kt │ │ │ ├── VaadinProjectWizard.kt │ │ │ ├── VaadinProjectBuilderAdapter.kt │ │ │ └── HelloWorldPanel.kt │ │ │ ├── symbols │ │ │ ├── HillaSymbolReferenceProvider.kt │ │ │ ├── HillaSymbol.kt │ │ │ └── HillaSymbolReference.kt │ │ │ ├── hotswapagent │ │ │ ├── BadJBRFoundDialog.kt │ │ │ ├── HotswapAgentExecutor.kt │ │ │ ├── HotswapAgentProgramPatcher.kt │ │ │ └── HotswapAgentRunner.kt │ │ │ ├── VaadinProjectDetector.kt │ │ │ └── psi │ │ │ └── HillaReferenceSearcher.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── vaadin │ │ └── plugin │ │ ├── hotswapagent │ │ └── JdkUtilTest.kt │ │ └── starter │ │ ├── HelloWorldModelTest.kt │ │ └── StarterProjectModelTest.kt ├── settings.gradle.kts ├── ampli.json ├── .gitignore └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── gradle.properties ├── .gitignore ├── README.md ├── .run └── Run IDE with Plugin.run.xml ├── gradlew.bat └── .github └── workflows └── validation.yml /.idea/.name: -------------------------------------------------------------------------------- 1 | intellij-plugin-parent -------------------------------------------------------------------------------- /plugin-api-test-client/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "plugin-api-test-client" 2 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/vaadin-with-microservices-jvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaadin/intellij-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin/src/main/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaadin/intellij-plugin/HEAD/plugin/src/main/resources/icon.png -------------------------------------------------------------------------------- /plugin/src/main/resources/hotswap-agent.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaadin/intellij-plugin/HEAD/plugin/src/main/resources/hotswap-agent.jar -------------------------------------------------------------------------------- /plugin-api-test-client/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaadin/intellij-plugin/HEAD/plugin-api-test-client/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/Role.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | public enum Role { 4 | USER, ADMIN; 5 | } -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/resources/samples/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vaadin/intellij-plugin/HEAD/plugin-api-test-client/src/test/resources/samples/image.png -------------------------------------------------------------------------------- /plugin/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "intellij-plugin" 9 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/Handler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | interface Handler { 4 | 5 | fun run(): HandlerResponse 6 | } 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/CommandRequest.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot 2 | 3 | data class CommandRequest(val command: String, val projectBasePath: String?, val data: Map) 4 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "intellij-plugin-parent" 9 | include("plugin") 10 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/main/java/com/vaadin/plugin/TestApplication.java: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class TestApplication { 7 | } 8 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/vaadin-injections.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/HandlerResponse.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import io.netty.handler.codec.http.HttpResponseStatus 4 | 5 | data class HandlerResponse(val status: HttpResponseStatus, val data: Map? = null) 6 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/Entity.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.psi.HierarchicalMethodSignature 4 | 5 | class Entity(val className: String, val visibleMethods: Collection, val path: String) {} 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface UserRepository extends JpaRepository { 6 | User findByUsername(String username); 7 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/vaadin-with-ultimate.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinRoute.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.psi.PsiAnchor 4 | 5 | class VaadinRoute(val urlMapping: String, val locationString: String, val anchor: PsiAnchor) { 6 | fun isValid(): Boolean = anchor.retrieve() != null 7 | } 8 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/starter/DownloadableModel.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | import com.intellij.openapi.project.Project 4 | 5 | interface DownloadableModel { 6 | 7 | fun getDownloadLink(project: Project): String 8 | 9 | fun getProjectType(): String 10 | } 11 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/starter/StarterSupportMatrixElement.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | data class StarterSupportMatrixElement( 4 | val framework: String, 5 | val languages: Collection, 6 | val architectures: Collection, 7 | val buildTools: Collection, 8 | ) 9 | -------------------------------------------------------------------------------- /plugin-api-test-client/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/CustomButton.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import com.vaadin.flow.component.button.Button; 4 | 5 | /** 6 | * A simple Vaadin custom component. 7 | */ 8 | public class CustomButton extends Button { 9 | 10 | public CustomButton() { 11 | setText("My Custom Button"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinSecurity.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.psi.PsiAnchor 4 | 5 | class VaadinSecurity( 6 | val className: String, 7 | val origin: String, 8 | val source: String, 9 | val path: String, 10 | val anchor: PsiAnchor, 11 | val loginView: String? = null, 12 | ) { 13 | fun isValid(): Boolean = anchor.retrieve() != null 14 | } 15 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinUserDetails.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.psi.PsiAnchor 4 | 5 | class VaadinUserDetails( 6 | val className: String, 7 | val origin: String, 8 | val source: String, 9 | val path: String, 10 | val anchor: PsiAnchor, 11 | val entity: List? = null, 12 | ) { 13 | fun isValid(): Boolean = anchor.retrieve() != null 14 | } 15 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/utils/ArtifactIdUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.utils 2 | 3 | fun toArtifactId(name: String): String { 4 | return name 5 | .trim() 6 | .replace(Regex("([a-z])([A-Z])"), "$1-$2") // camelCase to kebab-case 7 | .replace(Regex("[\\s_]+"), "-") // spaces/underscores to hyphen 8 | .replace(Regex("[^a-zA-Z0-9-]"), "") // remove invalid chars 9 | .lowercase() 10 | } 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 2 | kotlin.stdlib.default.dependency=false 3 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 4 | org.gradle.configuration-cache=true 5 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 6 | org.gradle.caching=true 7 | 8 | kotlin.code.style=official -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotDotfileService.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.service 2 | 3 | import java.io.IOException 4 | import java.nio.file.Path 5 | 6 | interface CopilotDotfileService { 7 | 8 | fun isActive(): Boolean 9 | 10 | @Throws(IOException::class) fun removeDotfile() 11 | 12 | @Throws(IOException::class) fun createDotfile(content: String) 13 | 14 | fun getDotfilePath(): Path? 15 | } 16 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/RefreshHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.VirtualFileManager 5 | 6 | class RefreshHandler(project: Project) : AbstractHandler(project) { 7 | 8 | override fun run(): HandlerResponse { 9 | VirtualFileManager.getInstance().asyncRefresh {} 10 | return RESPONSE_OK 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/HelloWorldView.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 4 | import com.vaadin.flow.router.Route; 5 | 6 | /** 7 | * A simple Vaadin view that can be used for testing get routes tool within this project. 8 | * This view is mapped to the route "HelloWorld". 9 | */ 10 | @Route("HelloWorld") 11 | public class HelloWorldView extends VerticalLayout { 12 | } 13 | -------------------------------------------------------------------------------- /plugin/ampli.json: -------------------------------------------------------------------------------- 1 | { 2 | "Zone": "us", 3 | "OrgId": "62972", 4 | "WorkspaceId": "418f2561-c6f7-4d6c-9e33-459672266311", 5 | "SourceId": "755ed219-73a5-4bf7-bd2f-48cf12072780", 6 | "Runtime": "jre:kotlin-ampli", 7 | "Platform": "JRE", 8 | "Language": "Kotlin", 9 | "SDK": "com.amplitude:java-sdk:1.+", 10 | "Path": "./src/main/kotlin/com/amplitude/ampli", 11 | "Branch": "main", 12 | "Version": "1.0.0", 13 | "VersionId": "ac62c3f9-e92b-4502-8a50-3fd8f13c4103" 14 | } -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinComponent.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.psi.HierarchicalMethodSignature 4 | import com.intellij.psi.PsiAnchor 5 | 6 | class VaadinComponent( 7 | val className: String, 8 | val origin: String, 9 | val source: String, 10 | val path: String, 11 | val anchor: PsiAnchor, 12 | val visibleMethods: Collection 13 | ) { 14 | fun isValid(): Boolean = anchor.retrieve() != null 15 | } 16 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/RestUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot 2 | 3 | import java.util.UUID 4 | import org.jetbrains.ide.BuiltInServerManager 5 | 6 | class RestUtil { 7 | 8 | companion object { 9 | 10 | private val serviceName = "copilot-" + UUID.randomUUID() 11 | 12 | fun getServiceName(): String = serviceName 13 | 14 | fun getEndpoint(): String { 15 | val port = BuiltInServerManager.getInstance().port 16 | return "http://127.0.0.1:$port/api/" + getServiceName() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/listeners/VaadinProjectListener.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.listeners 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.util.messages.Topic 5 | import com.intellij.util.messages.Topic.ProjectLevel 6 | import java.util.* 7 | 8 | interface VaadinProjectListener : EventListener { 9 | 10 | companion object { 11 | @ProjectLevel 12 | val TOPIC: Topic = 13 | Topic.create("Vaadin project detected", VaadinProjectListener::class.java) 14 | } 15 | 16 | fun vaadinProjectDetected(project: Project) {} 17 | } 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/actions/VaadinOnSaveInfoProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.actions 2 | 3 | import com.intellij.ide.actionsOnSave.ActionOnSaveContext 4 | import com.intellij.ide.actionsOnSave.ActionOnSaveInfo 5 | import com.intellij.ide.actionsOnSave.ActionOnSaveInfoProvider 6 | 7 | class VaadinOnSaveInfoProvider : ActionOnSaveInfoProvider() { 8 | 9 | override fun getActionOnSaveInfos(p0: ActionOnSaveContext): MutableCollection { 10 | val info: ArrayList = ArrayList() 11 | info.add(VaadinCompileOnSaveActionInfo(p0)) 12 | return info 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/vaadin-with-microservices.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/CustomView.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import com.flowingcode.vaadin.addons.dayofweekselector.DayOfWeekSelector; 4 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 5 | import com.vaadin.flow.router.Route; 6 | 7 | 8 | /** 9 | * A simple Vaadin view that can be used for testing get routes tool within this project. 10 | * This view is mapped to the route "custom". 11 | */ 12 | @Route(value = "custom", autoLayout = false) 13 | public class CustomView extends VerticalLayout { 14 | public CustomView() { 15 | add(new DayOfWeekSelector()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/ui/VaadinStatusBarWidgetFactory.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.ui 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.wm.StatusBarWidget 5 | import com.intellij.openapi.wm.StatusBarWidgetFactory 6 | 7 | class VaadinStatusBarWidgetFactory : StatusBarWidgetFactory { 8 | 9 | override fun getId(): String { 10 | return "VaadinStatusBarWidgetFactory" 11 | } 12 | 13 | override fun getDisplayName(): String { 14 | return "Vaadin" 15 | } 16 | 17 | override fun createWidget(project: Project): StatusBarWidget { 18 | return VaadinStatusBarWidget(project) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/vaadin-with-javascript.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | .intellijPlatform 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | 45 | 46 | .idea/.copilot-plugin -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | .intellijPlatform 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | 45 | 46 | .idea/.copilot-plugin -------------------------------------------------------------------------------- /plugin-api-test-client/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | .intellijPlatform 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | 45 | 46 | .idea/.copilot-plugin -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetModulePathsHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.project.guessProjectDir 5 | import com.vaadin.plugin.copilot.CopilotPluginUtil 6 | import com.vaadin.plugin.copilot.CopilotPluginUtil.Companion.getModulesInfo 7 | import io.netty.handler.codec.http.HttpResponseStatus 8 | 9 | class GetModulePathsHandler(project: Project) : AbstractHandler(project) { 10 | 11 | override fun run(): HandlerResponse { 12 | val modules = getModulesInfo(project) 13 | val projectInfo = CopilotPluginUtil.ProjectInfo(project.guessProjectDir()?.path, modules) 14 | val data = mapOf("project" to projectInfo) 15 | return HandlerResponse(HttpResponseStatus.OK, data) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/utils/VaadinIcons.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.utils 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | class VaadinIcons { 6 | 7 | companion object { 8 | 9 | private const val RESOURCE_PATH = "/vaadin/icons" 10 | 11 | val VAADIN_BLUE = IconLoader.getIcon("$RESOURCE_PATH/module.svg", VaadinIcons::class.java.classLoader) 12 | 13 | val VAADIN = IconLoader.getIcon("$RESOURCE_PATH/vaadin.svg", VaadinIcons::class.java.classLoader) 14 | 15 | val HILLA = IconLoader.getIcon("$RESOURCE_PATH/hilla.svg", VaadinIcons::class.java.classLoader) 16 | 17 | val DEBUG_HOTSWAP = IconLoader.getIcon("$RESOURCE_PATH/debug.svg", VaadinIcons::class.java.classLoader) 18 | 19 | val RERUN_HOTSWAP = IconLoader.getIcon("$RESOURCE_PATH/rerun.svg", VaadinIcons::class.java.classLoader) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/module/StarterProjectPanel.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.module 2 | 3 | import com.intellij.ui.dsl.builder.bindSelected 4 | import com.intellij.ui.dsl.builder.panel 5 | import com.vaadin.plugin.starter.StarterProjectModel 6 | 7 | class StarterProjectPanel(private val model: StarterProjectModel) { 8 | 9 | private val MAX_LINE_LENGTH = 60 10 | 11 | val root = panel { 12 | row("Vaadin Version") { 13 | segmentedButton(setOf(false, true)) { this.text = if (it) "Prerelease" else "Stable" } 14 | .bind(model.usePrereleaseProperty) 15 | } 16 | row { text("Include sample view").bold() } 17 | row { text("A sample view built fully in Java, front to back.", MAX_LINE_LENGTH) } 18 | row("Include sample view") { checkBox("").bindSelected(model.includeFlowProperty) } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/vaadin/plugin/hotswapagent/JdkUtilTest.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.hotswapagent 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertNull 6 | 7 | class JdkUtilTest { 8 | 9 | @Test 10 | fun parseJavaVersionSupportsCommonFormats() { 11 | val cases = 12 | mapOf( 13 | "17" to 17, 14 | " 17 " to 17, 15 | "1.8" to 8, 16 | "1.8.0_292" to 8, 17 | "11.0.4" to 11, 18 | "21-ea" to 21, 19 | ) 20 | 21 | cases.forEach { (input, expected) -> 22 | assertEquals(expected, JdkUtil.parseJavaVersion(input), "Failed for input '$input'") 23 | } 24 | } 25 | 26 | @Test 27 | fun parseJavaVersionReturnsNullWhenNotParsable() { 28 | assertNull(JdkUtil.parseJavaVersion("abc")) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/module/VaadinProjectWizardStep.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.module 2 | 3 | import com.intellij.ide.util.projectWizard.WizardContext 4 | import com.intellij.ide.wizard.NewProjectWizardStep 5 | import com.intellij.openapi.observable.properties.PropertyGraph 6 | import com.intellij.openapi.util.UserDataHolder 7 | import com.intellij.openapi.util.UserDataHolderBase 8 | import com.intellij.ui.dsl.builder.Panel 9 | 10 | class VaadinProjectWizardStep(override val context: WizardContext, override val propertyGraph: PropertyGraph) : 11 | NewProjectWizardStep { 12 | 13 | override val data: UserDataHolder 14 | get() = UserDataHolderBase() 15 | 16 | override val keywords: NewProjectWizardStep.Keywords 17 | get() = NewProjectWizardStep.Keywords() 18 | 19 | override fun setupUI(builder: Panel) { 20 | VaadinPanel(propertyGraph, context, builder) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/listeners/CopilotVaadinProjectListener.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.listeners 2 | 3 | import com.intellij.openapi.components.service 4 | import com.intellij.openapi.project.DumbService 5 | import com.intellij.openapi.project.Project 6 | import com.vaadin.plugin.copilot.CopilotPluginUtil.Companion.saveDotFile 7 | import com.vaadin.plugin.copilot.service.CopilotDotfileService 8 | import com.vaadin.plugin.listeners.VaadinProjectListener 9 | import com.vaadin.plugin.ui.VaadinStatusBarWidget 10 | 11 | class CopilotVaadinProjectListener : VaadinProjectListener { 12 | 13 | override fun vaadinProjectDetected(project: Project) { 14 | if (!project.isDisposed && !project.service().isActive()) { 15 | saveDotFile(project) 16 | DumbService.getInstance(project).smartInvokeLater { VaadinStatusBarWidget.update(project) } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vaadin IntelliJ Plugin 2 | 3 | Official Vaadin plugin for JetBrains IntelliJ. 4 | 5 | Improves developer experience while working with [Vaadin](https://vaadin.com) framework.

6 | - Helps to create new Vaadin projects using configurable Hello World starters. 7 | - Boosts [Vaadin Copilot](https://vaadin.com/copilot) experience. 8 | - Helps to set up and run projects using HotSwap Agent. 9 | - Provides navigation to [Flow](https://vaadin.com/flow) @Route views in Endpoints tool window. 10 | 11 | Available at [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/23758-vaadin). 12 | 13 | Thanks to [HotSwapHelper](https://github.com/gejun123456/HotSwapHelper) for inspiration and ideas on the Hotswap Agent integration. 14 | 15 | ## API test application 16 | 17 | `plugin-api-test-client` is a simple Spring boot app with tests allowing to easily debug plugin endpoint API calls. 18 | 19 | Please remember to run development IDE (Run Plugin) before running the tests. 20 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/RedoHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.command.undo.UndoManager 4 | import com.intellij.openapi.fileEditor.FileEditor 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.vfs.VirtualFile 7 | 8 | class RedoHandler(project: Project, data: Map) : UndoHandler(project, data) { 9 | 10 | override fun getOpsCount(vfsFile: VirtualFile): Int { 11 | return getCopilotUndoManager().getRedoCount(vfsFile) 12 | } 13 | 14 | override fun before(vfsFile: VirtualFile) { 15 | getCopilotUndoManager().redoStart(vfsFile) 16 | } 17 | 18 | override fun runManagerAction(undoManager: UndoManager, editor: FileEditor) { 19 | undoManager.redo(editor) 20 | } 21 | 22 | override fun after(vfsFile: VirtualFile) { 23 | getCopilotUndoManager().redoDone(vfsFile) 24 | LOG.info("$vfsFile redo performed") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/actions/InstallOrUpdateHotSwapAction.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.actions 2 | 3 | import com.intellij.notification.NotificationType 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.vaadin.plugin.copilot.CopilotPluginUtil 7 | import com.vaadin.plugin.utils.VaadinHomeUtil 8 | 9 | class InstallOrUpdateHotSwapAction : AnAction() { 10 | 11 | override fun actionPerformed(p0: AnActionEvent) { 12 | val version = VaadinHomeUtil.updateOrInstallHotSwapJar() 13 | if (version != null) { 14 | CopilotPluginUtil.notify("hotswap-agent.jar:$version installed", NotificationType.INFORMATION, p0.project) 15 | } else { 16 | CopilotPluginUtil.notify( 17 | "Installation of hotswap-agent.jar failed, see logs for details", 18 | NotificationType.ERROR, 19 | p0.project, 20 | ) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/HeartbeatHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.vaadin.plugin.copilot.service.CompilationStatusManagerService 5 | import io.netty.handler.codec.http.HttpResponseStatus 6 | 7 | class HeartbeatHandler(project: Project) : AbstractHandler(project) { 8 | companion object { 9 | const val HAS_COMPILATION_ERROR = "hasCompilationError" 10 | const val FILES_CONTAIN_COMPILATION_ERROR = "filesContainCompilationError" 11 | } 12 | 13 | override fun run(): HandlerResponse { 14 | val data: MutableMap = mutableMapOf() 15 | val compilationStatusManagerService = project.getService(CompilationStatusManagerService::class.java) 16 | data[HAS_COMPILATION_ERROR] = compilationStatusManagerService.hasCompilationError() 17 | data[FILES_CONTAIN_COMPILATION_ERROR] = compilationStatusManagerService.getErrorFiles() 18 | return HandlerResponse(HttpResponseStatus.OK, data) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetVaadinRoutesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.search.GlobalSearchScope 6 | import com.vaadin.plugin.endpoints.findFlowRoutes 7 | import io.netty.handler.codec.http.HttpResponseStatus 8 | 9 | class GetVaadinRoutesHandler(project: Project) : AbstractHandler(project) { 10 | 11 | override fun run(): HandlerResponse { 12 | return ApplicationManager.getApplication().runReadAction { 13 | val flowViews = findFlowRoutes(project, GlobalSearchScope.allScope(project)) 14 | 15 | val mapFlowRoute = 16 | flowViews.map { route -> mapOf("route" to route.urlMapping, "classname" to route.locationString) } 17 | 18 | LOG.info("Flow Routes detected: $flowViews") 19 | 20 | HandlerResponse(status = HttpResponseStatus.OK, data = mapOf("routes" to mapFlowRoute)) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/actions/DebugUsingHotSwapAgentAction.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.actions 2 | 3 | import com.intellij.execution.Executor 4 | import com.intellij.execution.ExecutorRegistry 5 | import com.intellij.execution.dashboard.actions.ExecutorAction 6 | import com.intellij.openapi.actionSystem.AnActionEvent 7 | import com.vaadin.plugin.hotswapagent.HotswapAgentExecutor 8 | import com.vaadin.plugin.utils.VaadinIcons 9 | 10 | class DebugUsingHotSwapAgentAction : ExecutorAction() { 11 | override fun update(event: AnActionEvent, isRunning: Boolean) { 12 | if (isRunning) { 13 | event.presentation.text = "Rerun using HotSwapAgent" 14 | event.presentation.icon = VaadinIcons.RERUN_HOTSWAP 15 | } else { 16 | event.presentation.text = "Debug using HotSwapAgent" 17 | event.presentation.icon = VaadinIcons.DEBUG_HOTSWAP 18 | } 19 | } 20 | 21 | override fun getExecutor(): Executor { 22 | return ExecutorRegistry.getInstance().getExecutorById(HotswapAgentExecutor.ID)!! 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/DefaultProgramPatcher.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot 2 | 3 | import com.intellij.execution.Executor 4 | import com.intellij.execution.configurations.JavaParameters 5 | import com.intellij.execution.configurations.RunConfiguration 6 | import com.intellij.execution.configurations.RunProfile 7 | import com.intellij.execution.runners.JavaProgramPatcher 8 | import com.vaadin.plugin.copilot.service.CopilotDotfileService 9 | 10 | class DefaultProgramPatcher : JavaProgramPatcher() { 11 | 12 | override fun patchJavaParameters(executor: Executor?, runProfile: RunProfile?, javaParameters: JavaParameters?) { 13 | 14 | if (javaParameters == null) { 15 | return 16 | } 17 | val paramsList = javaParameters.vmParametersList 18 | 19 | if (runProfile is RunConfiguration) { 20 | runProfile.project.getService(CopilotDotfileService::class.java).getDotfilePath()?.let { 21 | paramsList.add("-Dvaadin.copilot.pluginDotFilePath=${it}") 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import com.vaadin.flow.spring.security.VaadinWebSecurity; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.crypto.password.NoOpPasswordEncoder; 9 | import org.springframework.security.crypto.password.PasswordEncoder; 10 | 11 | @Configuration 12 | @EnableWebSecurity 13 | public class SecurityConfig extends VaadinWebSecurity { 14 | 15 | @Override 16 | protected void configure(HttpSecurity http) throws Exception { 17 | super.configure(http); 18 | // Set the Login view to use 19 | setLoginView(http, HelloWorldView.class); 20 | } 21 | 22 | @Bean 23 | public PasswordEncoder passwordEncoder() { 24 | return NoOpPasswordEncoder.getInstance(); 25 | } 26 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/symbols/HillaSymbolReferenceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.symbols 2 | 3 | import com.intellij.lang.javascript.psi.JSReferenceExpression 4 | import com.intellij.model.Symbol 5 | import com.intellij.model.psi.PsiExternalReferenceHost 6 | import com.intellij.model.psi.PsiSymbolReference 7 | import com.intellij.model.psi.PsiSymbolReferenceHints 8 | import com.intellij.model.psi.PsiSymbolReferenceProvider 9 | import com.intellij.model.search.SearchRequest 10 | import com.intellij.openapi.project.Project 11 | 12 | internal class HillaSymbolReferenceProvider : PsiSymbolReferenceProvider { 13 | 14 | override fun getReferences( 15 | host: PsiExternalReferenceHost, 16 | hints: PsiSymbolReferenceHints 17 | ): Collection { 18 | if (host !is JSReferenceExpression) { 19 | return emptyList() 20 | } 21 | 22 | return listOf(HillaSymbolReference(host)) 23 | } 24 | 25 | override fun getSearchRequests(project: Project, symbol: Symbol): Collection { 26 | return emptyList() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/ui/settings/VaadinSettings.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.ui.settings 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.PersistentStateComponent 5 | import com.intellij.openapi.components.State 6 | import com.intellij.openapi.components.Storage 7 | import org.jetbrains.annotations.NotNull 8 | 9 | @State(name = "com.vaadin.plugin.ui.settings.VaadinSettings", storages = [Storage("VaadinSettings.xml")]) 10 | internal class VaadinSettings : PersistentStateComponent { 11 | internal class State { 12 | var userId: String? = null 13 | var sendUsageStatistics: Boolean = true 14 | } 15 | 16 | private var myState = State() 17 | 18 | override fun getState(): State { 19 | return myState 20 | } 21 | 22 | override fun loadState(@NotNull state: State) { 23 | myState = state 24 | } 25 | 26 | companion object { 27 | val instance: VaadinSettings 28 | get() = ApplicationManager.getApplication().getService(VaadinSettings::class.java) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/listeners/CopilotDynamicPluginListener.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.listeners 2 | 3 | import com.intellij.ide.plugins.DynamicPluginListener 4 | import com.intellij.ide.plugins.IdeaPluginDescriptor 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.openapi.project.Project 7 | import com.vaadin.plugin.copilot.CopilotPluginUtil 8 | import com.vaadin.plugin.copilot.service.CopilotDotfileService 9 | 10 | class CopilotDynamicPluginListener(private val project: Project) : DynamicPluginListener { 11 | 12 | private val LOG: Logger = Logger.getInstance(CopilotDynamicPluginListener::class.java) 13 | 14 | override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) { 15 | project.getService(CopilotDotfileService::class.java).removeDotfile() 16 | LOG.debug("Plugin is going to be unloaded, .copilot-plugin removed") 17 | } 18 | 19 | override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) { 20 | CopilotPluginUtil.saveDotFile(project) 21 | LOG.debug("Plugin loaded, .copilot-plugin created") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin-api-test-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.springframework.boot") version "3.4.0" 3 | id("io.spring.dependency-management") version "1.1.6" 4 | id("java") 5 | } 6 | 7 | group = "com.vaadin.plugin" 8 | version = "unspecified" 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation("org.springframework.boot:spring-boot-starter-web:3.4.2") 16 | implementation("org.springframework.boot:spring-boot-starter-security:3.4.2") 17 | implementation("org.springframework.boot:spring-boot-starter-data-jpa:3.4.2") 18 | implementation("com.vaadin:vaadin-spring:24.6.5") 19 | implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2") 20 | implementation("com.vaadin:vaadin-core:24.6.5") 21 | implementation("jakarta.persistence:jakarta.persistence-api:3.2.0") 22 | implementation("com.flowingcode.vaadin.addons:day-of-week-selector-addon:1.1.0") 23 | testImplementation("org.springframework.boot:spring-boot-starter-test:3.4.2") 24 | } 25 | 26 | tasks.test { 27 | useJUnitPlatform() 28 | testLogging { 29 | events("PASSED", "FAILED", "SKIPPED") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/ReloadMavenModuleHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.runInEdt 4 | import com.intellij.openapi.project.Project 5 | import org.jetbrains.idea.maven.project.MavenProjectsManager 6 | 7 | class ReloadMavenModuleHandler(project: Project, moduleName: String) : AbstractHandler(project) { 8 | 9 | private val moduleName: String = moduleName as String 10 | 11 | override fun run(): HandlerResponse { 12 | runInEdt { 13 | val mavenProjectsManager = MavenProjectsManager.getInstance(project) 14 | mavenProjectsManager.projects 15 | .firstOrNull { it.displayName == moduleName } 16 | ?.let { mavenProject -> 17 | LOG.debug("Reloading ${mavenProject.displayName} (${project.name})") 18 | mavenProjectsManager.scheduleForceUpdateMavenProject(mavenProject) 19 | return@runInEdt 20 | } 21 | LOG.debug("Reloading of $moduleName failed - content not found") 22 | } 23 | return RESPONSE_OK 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/main/java/com/vaadin/plugin/Message.java: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin; 2 | 3 | import java.util.List; 4 | 5 | public class Message { 6 | 7 | record Command(String command, Object data) { 8 | } 9 | 10 | record CopilotRestRequest(String command, String projectBasePath, Object data) { 11 | } 12 | 13 | record WriteFileMessage(String file, String undoLabel, String content) { 14 | } 15 | 16 | record UndoRedoMessage(List files) { 17 | } 18 | 19 | record ShowInIdeMessage(String file, Integer line, Integer column) { 20 | } 21 | 22 | record RefreshMessage() { 23 | } 24 | 25 | record RestartApplicationMessage() { 26 | } 27 | 28 | record CompileMessage(List files) { 29 | } 30 | 31 | record DeleteMessage(String file) { 32 | } 33 | 34 | record GetVaadinRoutesMessage() { 35 | } 36 | 37 | record GetVaadinVersionMessage() { 38 | } 39 | 40 | record GetVaadinComponentsMessage(boolean includeMethods) { 41 | } 42 | 43 | record GetVaadinPersistenceMessage(boolean includeMethods) { 44 | } 45 | 46 | record GetVaadinSecurityMessage() { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/symbols/HillaSymbol.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.symbols 2 | 3 | import com.intellij.model.Pointer 4 | import com.intellij.model.Symbol 5 | import com.intellij.navigation.NavigatableSymbol 6 | import com.intellij.navigation.SymbolNavigationService 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.platform.backend.navigation.NavigationTarget 9 | import com.intellij.psi.PsiElement 10 | 11 | class HillaSymbol(private val target: PsiElement) : NavigatableSymbol { 12 | 13 | override fun createPointer(): Pointer { 14 | return Pointer.hardPointer(this) 15 | } 16 | 17 | override fun getNavigationTargets(project: Project): Collection { 18 | if (target.project != project) return emptyList() 19 | return listOf(SymbolNavigationService.getInstance().psiElementNavigationTarget(target)) 20 | } 21 | 22 | override fun equals(other: Any?): Boolean { 23 | if (this === other) return true 24 | if (javaClass != other?.javaClass) return false 25 | 26 | other as HillaSymbol 27 | 28 | return target == other.target 29 | } 30 | 31 | override fun hashCode(): Int { 32 | return target.hashCode() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteBase64FileHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.editor.Document 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.task.ProjectTaskManager 7 | import java.util.Base64 8 | 9 | class WriteBase64FileHandler(project: Project, data: Map) : WriteFileHandler(project, data) { 10 | 11 | override fun doWrite(vfsFile: VirtualFile?, doc: Document?, content: String) { 12 | vfsFile?.setBinaryContent(Base64.getDecoder().decode(content)) 13 | } 14 | 15 | override fun postSave(vfsFile: VirtualFile) { 16 | LOG.info("File $vfsFile created") 17 | notifyUndoManager(vfsFile) 18 | // there is no Document associated with binary VirtualFile, call "compile" always to process 19 | // resource 20 | processResource(vfsFile) 21 | openFileInEditor(vfsFile) 22 | } 23 | 24 | private fun processResource(vfsFile: VirtualFile) { 25 | ProjectTaskManager.getInstance(project).compile(vfsFile).then { 26 | if (it.hasErrors()) { 27 | LOG.warn("Cannot process $vfsFile") 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/starter/StarterSupport.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | class StarterSupport { 4 | 5 | companion object { 6 | 7 | val languages = linkedMapOf("java" to "Java", "kotlin" to "Kotlin") 8 | 9 | val architectures = 10 | linkedMapOf( 11 | "springboot" to "Spring Boot", 12 | "quarkus" to "Quarkus", 13 | "jakartaee" to "Jakarta EE", 14 | "servlet" to "Servlet", 15 | ) 16 | 17 | val buildTools = linkedMapOf("maven" to "Maven", "gradle" to "Gradle") 18 | 19 | fun isSupportedArchitecture(model: HelloWorldModel, architecture: String): Boolean { 20 | if (model.buildTool == "gradle") { 21 | return setOf("springboot", "servlet").contains(architecture) 22 | } else if (model.language == "kotlin") { 23 | return architecture == "springboot" 24 | } 25 | return true 26 | } 27 | 28 | fun isSupportedBuildTool(model: HelloWorldModel, buildTool: String): Boolean { 29 | if (model.language == "kotlin") { 30 | return buildTool == "maven" 31 | } 32 | return true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/CompileFilesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.vfs.VfsUtil 5 | import com.intellij.openapi.vfs.findDocument 6 | import com.vaadin.plugin.actions.VaadinCompileOnSaveActionInfo 7 | import java.io.File 8 | 9 | class CompileFilesHandler(project: Project, data: Map) : AbstractHandler(project) { 10 | 11 | private val ioFile = data["files"] as Collection 12 | 13 | override fun run(): HandlerResponse { 14 | 15 | val documents = 16 | ioFile 17 | .map { File(it) } 18 | .filter { isFileInsideProject(project, it) } 19 | .mapNotNull { VfsUtil.findFileByIoFile(it, true) } 20 | .mapNotNull { it.findDocument() } 21 | .toTypedArray() 22 | 23 | if (documents.isNotEmpty()) { 24 | VaadinCompileOnSaveActionInfo.getAction().processDocuments(project, documents) 25 | LOG.debug("Files compilation initiated: $documents") 26 | return RESPONSE_OK 27 | } else { 28 | LOG.warn("No project files found in $ioFile") 29 | return RESPONSE_BAD_REQUEST 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/module/VaadinProjectWizard.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.module 2 | 3 | import com.intellij.ide.util.projectWizard.WizardContext 4 | import com.intellij.ide.wizard.GeneratorNewProjectWizard 5 | import com.intellij.ide.wizard.NewProjectWizardStep 6 | import com.intellij.openapi.observable.properties.PropertyGraph 7 | import com.vaadin.plugin.starter.DownloadableModel 8 | import com.vaadin.plugin.utils.VaadinIcons 9 | import com.vaadin.plugin.utils.VaadinProjectUtil.Companion.PROJECT_MODEL_PROP_KEY 10 | import javax.swing.Icon 11 | 12 | class VaadinProjectWizard : GeneratorNewProjectWizard { 13 | 14 | override val icon: Icon 15 | get() = VaadinIcons.VAADIN_BLUE 16 | 17 | override val id: String 18 | get() = "Vaadin" 19 | 20 | override val name: String 21 | get() = "Vaadin" 22 | 23 | private val propertyGraph: PropertyGraph 24 | get() = PropertyGraph("Vaadin project") 25 | 26 | private val projectModelProperty = propertyGraph.property(null) 27 | 28 | val projectModel: DownloadableModel? by projectModelProperty 29 | 30 | override fun createStep(context: WizardContext): NewProjectWizardStep { 31 | context.putUserData(PROJECT_MODEL_PROP_KEY, projectModelProperty) 32 | return VaadinProjectWizardStep(context, propertyGraph) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinHillaEndpointsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.microservices.endpoints.EndpointsFilter 4 | import com.intellij.microservices.endpoints.FrameworkPresentation 5 | import com.intellij.microservices.endpoints.ModuleEndpointsFilter 6 | import com.intellij.microservices.endpoints.presentation.HttpUrlPresentation 7 | import com.intellij.navigation.ItemPresentation 8 | import com.intellij.openapi.project.Project 9 | import com.vaadin.plugin.utils.VaadinIcons 10 | import com.vaadin.plugin.utils.hasVaadin 11 | 12 | internal class VaadinHillaEndpointsProvider : VaadinEndpointsProvider() { 13 | 14 | override val presentation: FrameworkPresentation = 15 | FrameworkPresentation("Vaadin-Hilla", "Vaadin Hilla", VaadinIcons.HILLA) 16 | 17 | override fun getEndpointGroups(project: Project, filter: EndpointsFilter): Iterable { 18 | if (filter !is ModuleEndpointsFilter) return emptyList() 19 | if (!hasVaadin(filter.module)) return emptyList() 20 | 21 | return findHillaEndpoints(project, filter.transitiveSearchScope) 22 | } 23 | 24 | override fun getEndpointPresentation(group: VaadinRoute, endpoint: VaadinRoute): ItemPresentation { 25 | return HttpUrlPresentation(group.urlMapping, group.locationString, VaadinIcons.HILLA) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/actions/VaadinCompileOnSaveActionInfo.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.actions 2 | 3 | import com.intellij.codeInsight.actions.onSave.ActionOnSaveInfoBase 4 | import com.intellij.ide.actionsOnSave.ActionOnSaveComment 5 | import com.intellij.ide.actionsOnSave.ActionOnSaveContext 6 | import com.intellij.ide.util.PropertiesComponent 7 | import com.intellij.openapi.actionSystem.ActionManager 8 | import com.intellij.openapi.project.Project 9 | 10 | class VaadinCompileOnSaveActionInfo(context: ActionOnSaveContext) : 11 | ActionOnSaveInfoBase(context, NAME, PROPERTY, DEFAULT) { 12 | 13 | companion object { 14 | const val ID = "VaadinCompileOnSaveAction" 15 | const val NAME = "Compile Java files" 16 | const val PROPERTY = "vaadin.compileOnSave" 17 | const val DEFAULT = true 18 | const val DESCRIPTION = "Compiles *.java while debugging" 19 | 20 | fun isEnabledForProject(project: Project): Boolean { 21 | return PropertiesComponent.getInstance(project).getBoolean(PROPERTY, DEFAULT) 22 | } 23 | 24 | fun getAction(): VaadinCompileOnSaveAction { 25 | return ActionManager.getInstance().getAction(ID) as VaadinCompileOnSaveAction 26 | } 27 | } 28 | 29 | override fun getComment(): ActionOnSaveComment? { 30 | return ActionOnSaveComment.info(DESCRIPTION) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinImplicitUsageProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.codeInsight.AnnotationUtil 4 | import com.intellij.codeInsight.daemon.ImplicitUsageProvider 5 | import com.intellij.lang.jvm.JvmModifier 6 | import com.intellij.psi.PsiClass 7 | import com.intellij.psi.PsiElement 8 | import com.intellij.psi.PsiField 9 | import com.intellij.psi.util.InheritanceUtil 10 | 11 | internal class VaadinImplicitUsageProvider : ImplicitUsageProvider { 12 | override fun isImplicitUsage(element: PsiElement): Boolean { 13 | return element is PsiClass && 14 | !element.isInterface && 15 | !element.isEnum && 16 | !element.hasModifier(JvmModifier.ABSTRACT) && 17 | !element.isAnnotationType && 18 | (AnnotationUtil.isAnnotated(element, VAADIN_ROUTE, 0) || 19 | AnnotationUtil.isAnnotated(element, VAADIN_TAG, 0) || 20 | AnnotationUtil.isAnnotated(element, HILLA_BROWSER_CALLABLE, 0) || 21 | InheritanceUtil.isInheritor(element, VAADIN_APP_SHELL_CONFIGURATOR)) 22 | } 23 | 24 | override fun isImplicitRead(element: PsiElement): Boolean { 25 | return false 26 | } 27 | 28 | override fun isImplicitWrite(element: PsiElement): Boolean { 29 | return element is PsiField && AnnotationUtil.isAnnotated(element, VAADIN_ID, 0) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/User.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import jakarta.persistence.*; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | @Entity 9 | @Table(name = "app_user") 10 | public class User { 11 | 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Long id; 15 | 16 | @Column(nullable = false, unique = true) 17 | private String username; 18 | 19 | // Store the encoded password 20 | @Column(nullable = false) 21 | private String password; 22 | 23 | @ElementCollection(fetch = FetchType.EAGER) 24 | @CollectionTable(name = "app_user_roles", joinColumns = @JoinColumn(name = "user_id")) 25 | @Column(name = "role") 26 | private Set roles = new HashSet<>(); 27 | 28 | public Long getId() { 29 | return id; 30 | } 31 | 32 | public String getUsername() { 33 | return username; 34 | } 35 | 36 | public void setUsername(String username) { 37 | this.username = username; 38 | } 39 | 40 | public String getPassword() { 41 | return password; 42 | } 43 | 44 | public void setPassword(String password) { 45 | this.password = password; 46 | } 47 | 48 | public Set getRoles() { 49 | return roles; 50 | } 51 | 52 | public void setRoles(Set roles) { 53 | this.roles = roles; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/flow-language-injections.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Element.executeJs (com.vaadin.flow.dom) 4 | 5 | 6 | 7 | 8 | Html (com.vaadin.flow.component) 9 | 10 | 11 | 12 | 13 | 14 | TextField.setPattern (com.vaadin.flow.component.textfield) 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/vaadin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/vaadin_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/rerun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/rerun_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/vaadin/plugin/starter/HelloWorldModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | import com.intellij.openapi.observable.properties.PropertyGraph 4 | import com.intellij.openapi.project.Project 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | import org.mockito.kotlin.mock 8 | import org.mockito.kotlin.whenever 9 | 10 | class HelloWorldModelTest { 11 | @Test 12 | fun defaultGroupIdIsComExampleApplication() { 13 | val groupIdProperty = PropertyGraph().property("com.example.application") 14 | val model = HelloWorldModel(groupIdProperty) 15 | assertEquals("com.example.application", model.groupIdProperty.get()) 16 | } 17 | 18 | @Test 19 | fun groupIdCanBeChanged() { 20 | val groupIdProperty = PropertyGraph().property("com.example.application") 21 | val model = HelloWorldModel(groupIdProperty) 22 | groupIdProperty.set("org.example") 23 | assertEquals("org.example", model.groupIdProperty.get()) 24 | } 25 | 26 | @Test 27 | fun groupIdIsIncludedInDownloadUrl() { 28 | val groupIdProperty = PropertyGraph().property("com.example.application") 29 | val model = HelloWorldModel(groupIdProperty) 30 | groupIdProperty.set("org.test") 31 | val project = mock() 32 | whenever(project.name).thenReturn("my-app") 33 | val url = model.getDownloadLink(project) 34 | assertTrue(url.contains("groupId=org.test")) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/com/vaadin/plugin/starter/StarterProjectModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | import com.intellij.openapi.observable.properties.PropertyGraph 4 | import com.intellij.openapi.project.Project 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | import org.mockito.kotlin.mock 8 | import org.mockito.kotlin.whenever 9 | 10 | class StarterProjectModelTest { 11 | @Test 12 | fun defaultGroupIdIsComVaadinApplication() { 13 | val groupIdProperty = PropertyGraph().property("com.vaadin.application") 14 | val model = StarterProjectModel(groupIdProperty) 15 | assertEquals("com.vaadin.application", model.groupIdProperty.get()) 16 | } 17 | 18 | @Test 19 | fun groupIdCanBeChanged() { 20 | val groupIdProperty = PropertyGraph().property("com.vaadin.application") 21 | val model = StarterProjectModel(groupIdProperty) 22 | groupIdProperty.set("org.example") 23 | assertEquals("org.example", model.groupIdProperty.get()) 24 | } 25 | 26 | @Test 27 | fun groupIdIsIncludedInDownloadUrl() { 28 | val groupIdProperty = PropertyGraph().property("com.vaadin.application") 29 | val model = StarterProjectModel(groupIdProperty) 30 | groupIdProperty.set("org.test") 31 | val project = mock() 32 | whenever(project.name).thenReturn("demo") 33 | val url = model.getDownloadLink(project) 34 | assertTrue(url.contains("groupId=org.test")) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/hotswapagent/BadJBRFoundDialog.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.hotswapagent 2 | 3 | import com.intellij.openapi.ui.DialogWrapper 4 | import javax.swing.Action 5 | import javax.swing.JComponent 6 | import javax.swing.JPanel 7 | import javax.swing.JTextArea 8 | 9 | class BadJBRFoundDialog() : DialogWrapper(true) { 10 | 11 | private val message: String 12 | 13 | override fun createActions(): Array { 14 | return arrayOf(myOKAction) 15 | } 16 | 17 | init { 18 | message = buildString { 19 | append( 20 | "HotswapAgent requires running with a JetBrains Runtime JDK which implements the low level hotswap functionality.\n\n") 21 | append( 22 | "Your IntelliJ IDEA installation includes a bundled JetBrains Runtime JDK which is known not work for this purpose.\n\n") 23 | append("You can resolve this by one of the following:\n") 24 | append("- Upgrade IntelliJ IDEA to 2024.2.4 or newer\n") 25 | append( 26 | "- Download a newer JetBrains runtime from https://github.com/JetBrains/JetBrainsRuntime/releases and set your run configuration to use it.") 27 | append("\n") 28 | } 29 | title = "Unable to Find a Suitable JDK" 30 | init() 31 | } 32 | 33 | override fun createCenterPanel(): JComponent { 34 | val textArea = JTextArea(message) 35 | textArea.isEditable = false 36 | return JPanel().apply { add(textArea) } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetVaadinEntitiesHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.search.GlobalSearchScope 6 | import com.vaadin.plugin.endpoints.findEntities 7 | import com.vaadin.plugin.endpoints.signatureToString 8 | import io.netty.handler.codec.http.HttpResponseStatus 9 | 10 | class GetVaadinEntitiesHandler(project: Project, includeMethods: Boolean) : AbstractHandler(project) { 11 | 12 | private val includeMethods: Boolean = includeMethods 13 | 14 | override fun run(): HandlerResponse { 15 | return ApplicationManager.getApplication().runReadAction { 16 | val entities = findEntities(project, GlobalSearchScope.allScope(project)) 17 | 18 | val mapEntities = 19 | entities.map { entity -> 20 | if (includeMethods) { 21 | mapOf( 22 | "classname" to entity.className, 23 | "methods" to entity.visibleMethods.joinToString(",") { signatureToString(it) }, 24 | "path" to entity.path) 25 | } else { 26 | mapOf("classname" to entity.className, "path" to entity.path) 27 | } 28 | } 29 | 30 | LOG.info("Entities detected: $entities") 31 | 32 | HandlerResponse(status = HttpResponseStatus.OK, data = mapOf("entities" to mapEntities)) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/ui/settings/VaadinSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.ui.settings 2 | 3 | import com.intellij.openapi.options.Configurable 4 | import java.util.Objects 5 | import javax.swing.JComponent 6 | 7 | /** Provides controller functionality for application settings. */ 8 | internal class VaadinSettingsConfigurable : Configurable { 9 | 10 | private var mySettingsComponent: VaadinSettingsComponent? = null 11 | 12 | override fun getPreferredFocusedComponent(): JComponent { 13 | return mySettingsComponent!!.preferredFocusedComponent 14 | } 15 | 16 | override fun createComponent(): JComponent { 17 | mySettingsComponent = VaadinSettingsComponent() 18 | return mySettingsComponent!!.panel 19 | } 20 | 21 | override fun isModified(): Boolean { 22 | val state: VaadinSettings.State = Objects.requireNonNull(VaadinSettings.instance.state) 23 | return mySettingsComponent!!.sendUsageStatisticsStatus != state.sendUsageStatistics 24 | } 25 | 26 | override fun apply() { 27 | val state: VaadinSettings.State = Objects.requireNonNull(VaadinSettings.instance.state) 28 | state.sendUsageStatistics = mySettingsComponent!!.sendUsageStatisticsStatus 29 | } 30 | 31 | override fun reset() { 32 | val state: VaadinSettings.State = Objects.requireNonNull(VaadinSettings.instance.state) 33 | mySettingsComponent!!.sendUsageStatisticsStatus = state.sendUsageStatistics 34 | } 35 | 36 | override fun disposeUIResources() { 37 | mySettingsComponent = null 38 | } 39 | 40 | override fun getDisplayName(): String { 41 | return "Vaadin" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/test/java/com/plugin/testviews/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.plugin.testviews; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | @Service 14 | public class UserDetailsServiceImpl implements UserDetailsService { 15 | 16 | private final UserRepository userRepository; 17 | 18 | public UserDetailsServiceImpl(UserRepository userRepository) { 19 | this.userRepository = userRepository; 20 | } 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 24 | User user = userRepository.findByUsername(username); 25 | if (user == null) { 26 | throw new UsernameNotFoundException("No user present with username: " + username); 27 | } else { 28 | return new org.springframework.security.core.userdetails.User( 29 | user.getUsername(), 30 | user.getPassword(), 31 | getAuthorities(user) 32 | ); 33 | } 34 | } 35 | 36 | private static List getAuthorities(User user) { 37 | return user.getRoles().stream() 38 | .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) 39 | .collect(Collectors.toList()); 40 | } 41 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/starter/HelloWorldModel.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | import com.intellij.openapi.observable.properties.PropertyGraph 4 | import com.intellij.openapi.project.Project 5 | import com.vaadin.plugin.utils.toArtifactId 6 | import java.net.URLEncoder 7 | import java.nio.charset.StandardCharsets 8 | 9 | class HelloWorldModel(val groupIdProperty: com.intellij.openapi.observable.properties.GraphProperty) : 10 | DownloadableModel { 11 | 12 | private val graph = PropertyGraph() 13 | val languageProperty = graph.property(StarterSupport.languages.keys.first()) 14 | val buildToolProperty = graph.property(StarterSupport.buildTools.keys.first()) 15 | val architectureProperty = graph.property(StarterSupport.architectures.keys.first()) 16 | 17 | val language by languageProperty 18 | val buildTool by buildToolProperty 19 | val architecture by architectureProperty 20 | val groupId by groupIdProperty 21 | 22 | override fun getDownloadLink(project: Project): String { 23 | val params = 24 | mapOf( 25 | "framework" to "flow", 26 | "language" to language, 27 | "buildtool" to buildTool, 28 | "stack" to architecture, 29 | "artifactId" to toArtifactId(project.name), 30 | "groupId" to groupId, 31 | "ref" to "intellij-plugin") 32 | val query = 33 | params.entries.joinToString("&") { (key, value) -> 34 | "${URLEncoder.encode(key, StandardCharsets.UTF_8)}=${URLEncoder.encode(value, StandardCharsets.UTF_8)}" 35 | } 36 | 37 | return "https://start.vaadin.com/helloworld?${query}" 38 | } 39 | 40 | override fun getProjectType(): String { 41 | return buildTool 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/utils/VaadinProjectUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.utils 2 | 3 | import com.intellij.ide.plugins.IdeaPluginDescriptor 4 | import com.intellij.ide.plugins.PluginManager 5 | import com.intellij.ide.plugins.PluginManagerCore 6 | import com.intellij.java.library.JavaLibraryUtil.hasLibraryClass 7 | import com.intellij.openapi.application.ApplicationInfo 8 | import com.intellij.openapi.extensions.PluginId 9 | import com.intellij.openapi.observable.properties.GraphProperty 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.util.Key 12 | import com.vaadin.plugin.listeners.VaadinProjectListener 13 | import com.vaadin.plugin.starter.DownloadableModel 14 | 15 | internal const val VAADIN_SERVICE = "com.vaadin.flow.server.VaadinService" 16 | 17 | internal const val ENDPOINTS_PLUGIN_ID = "com.intellij.microservices.ui" 18 | 19 | class VaadinProjectUtil { 20 | 21 | companion object { 22 | 23 | val PROJECT_MODEL_PROP_KEY = Key>("vaadin_project_model") 24 | } 25 | } 26 | 27 | internal fun doNotifyAboutVaadinProject(project: Project) { 28 | val publisher: VaadinProjectListener = project.messageBus.syncPublisher(VaadinProjectListener.TOPIC) 29 | publisher.vaadinProjectDetected(project) 30 | } 31 | 32 | internal fun hasVaadin(project: Project): Boolean = hasLibraryClass(project, VAADIN_SERVICE) 33 | 34 | internal fun hasVaadin(module: com.intellij.openapi.module.Module): Boolean = hasLibraryClass(module, VAADIN_SERVICE) 35 | 36 | internal fun hasEndpoints(): Boolean = 37 | PluginId.findId(ENDPOINTS_PLUGIN_ID)?.let { PluginManager.isPluginInstalled(it) } ?: false 38 | 39 | internal fun isUltimate(): Boolean = ApplicationInfo.getInstance().apiVersion.startsWith("IU-") 40 | 41 | internal fun getVaadinPluginDescriptor(): IdeaPluginDescriptor = 42 | PluginManagerCore.getPlugin(PluginId.getId("com.vaadin.intellij-plugin"))!! 43 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetVaadinComponentsHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.search.GlobalSearchScope 6 | import com.vaadin.plugin.endpoints.findComponents 7 | import com.vaadin.plugin.endpoints.signatureToString 8 | import io.netty.handler.codec.http.HttpResponseStatus 9 | 10 | class GetVaadinComponentsHandler(project: Project, includeMethods: Boolean) : AbstractHandler(project) { 11 | 12 | private val includeMethods: Boolean = includeMethods 13 | 14 | override fun run(): HandlerResponse { 15 | return ApplicationManager.getApplication().runReadAction { 16 | val components = findComponents(project, GlobalSearchScope.allScope(project)) 17 | 18 | val mapComponents = 19 | components.map { component -> 20 | if (includeMethods) { 21 | mapOf( 22 | "class" to component.className, 23 | "origin" to component.origin, 24 | "source" to component.source, 25 | "path" to component.path, 26 | "methods" to component.visibleMethods.joinToString(",") { signatureToString(it) }) 27 | } else { 28 | mapOf( 29 | "class" to component.className, 30 | "origin" to component.origin, 31 | "source" to component.source, 32 | "path" to component.path) 33 | } 34 | } 35 | 36 | LOG.info("Components detected: $components") 37 | 38 | HandlerResponse(status = HttpResponseStatus.OK, data = mapOf("components" to mapComponents)) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinEndpointsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.microservices.endpoints.EndpointType 4 | import com.intellij.microservices.endpoints.EndpointsFilter 5 | import com.intellij.microservices.endpoints.EndpointsProvider 6 | import com.intellij.microservices.endpoints.HTTP_SERVER_TYPE 7 | import com.intellij.microservices.endpoints.ModuleEndpointsFilter 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.ModificationTracker 10 | import com.intellij.psi.PsiElement 11 | import com.intellij.uast.UastModificationTracker 12 | import com.vaadin.plugin.utils.hasVaadin 13 | 14 | abstract class VaadinEndpointsProvider : EndpointsProvider { 15 | 16 | override val endpointType: EndpointType = HTTP_SERVER_TYPE 17 | 18 | override fun getStatus(project: Project): EndpointsProvider.Status { 19 | if (hasVaadin(project)) return EndpointsProvider.Status.HAS_ENDPOINTS 20 | return EndpointsProvider.Status.UNAVAILABLE 21 | } 22 | 23 | override fun getModificationTracker(project: Project): ModificationTracker { 24 | return UastModificationTracker.getInstance(project) 25 | } 26 | 27 | override fun getEndpointGroups(project: Project, filter: EndpointsFilter): Iterable { 28 | if (filter !is ModuleEndpointsFilter) return emptyList() 29 | if (!hasVaadin(filter.module)) return emptyList() 30 | 31 | return findFlowRoutes(project, filter.transitiveSearchScope) 32 | } 33 | 34 | override fun getEndpoints(group: VaadinRoute): Iterable { 35 | return listOf(group) 36 | } 37 | 38 | override fun isValidEndpoint(group: VaadinRoute, endpoint: VaadinRoute): Boolean { 39 | return group.isValid() 40 | } 41 | 42 | override fun getDocumentationElement(group: VaadinRoute, endpoint: VaadinRoute): PsiElement? { 43 | return endpoint.anchor.retrieve() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/VaadinProjectDetector.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.application.ReadAction 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.project.DumbService 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.project.ProjectManager 10 | import com.intellij.openapi.roots.ModuleRootEvent 11 | import com.intellij.openapi.roots.ModuleRootListener 12 | import com.intellij.openapi.startup.ProjectActivity 13 | import com.vaadin.plugin.copilot.service.CopilotDotfileService 14 | import com.vaadin.plugin.utils.doNotifyAboutVaadinProject 15 | import com.vaadin.plugin.utils.hasVaadin 16 | 17 | class VaadinProjectDetector : ModuleRootListener, ProjectActivity, DumbService.DumbModeListener { 18 | 19 | private val LOG: Logger = Logger.getInstance(VaadinProjectDetector::class.java) 20 | 21 | override fun rootsChanged(event: ModuleRootEvent) { 22 | if (event.project.isOpen) { 23 | detectAndNotifyAboutVaadinProject { event.project } 24 | } 25 | } 26 | 27 | override suspend fun execute(project: Project) { 28 | detectAndNotifyAboutVaadinProject { project } 29 | } 30 | 31 | override fun exitDumbMode() { 32 | detectAndNotifyAboutVaadinProject { ProjectManager.getInstance().openProjects.first() } 33 | } 34 | 35 | private fun detectAndNotifyAboutVaadinProject(projectProvider: () -> Project) { 36 | ApplicationManager.getApplication().executeOnPooledThread { 37 | ReadAction.run { 38 | val project = projectProvider.invoke() 39 | if (!project.service().isActive() && hasVaadin(project)) { 40 | doNotifyAboutVaadinProject(project) 41 | LOG.info("Vaadin project detected " + project.name) 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinFlowEndpointsProvider.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.microservices.endpoints.EndpointsFilter 4 | import com.intellij.microservices.endpoints.FrameworkPresentation 5 | import com.intellij.microservices.endpoints.ModuleEndpointsFilter 6 | import com.intellij.microservices.endpoints.presentation.HttpUrlPresentation 7 | import com.intellij.microservices.url.UrlPath 8 | import com.intellij.navigation.ItemPresentation 9 | import com.intellij.openapi.project.Project 10 | import com.vaadin.plugin.utils.VaadinIcons 11 | import com.vaadin.plugin.utils.hasVaadin 12 | 13 | internal class VaadinFlowEndpointsProvider : VaadinEndpointsProvider() { 14 | 15 | override val presentation: FrameworkPresentation = 16 | FrameworkPresentation("Vaadin-Flow", "Vaadin Flow", VaadinIcons.VAADIN_BLUE) 17 | 18 | override fun getEndpointGroups(project: Project, filter: EndpointsFilter): Iterable { 19 | if (filter !is ModuleEndpointsFilter) return emptyList() 20 | if (!hasVaadin(filter.module)) return emptyList() 21 | 22 | return findFlowRoutes(project, filter.transitiveSearchScope) 23 | } 24 | 25 | override fun getEndpointPresentation(group: VaadinRoute, endpoint: VaadinRoute): ItemPresentation { 26 | return HttpUrlPresentation(normalizeUrl(group.urlMapping), group.locationString, VaadinIcons.VAADIN_BLUE) 27 | } 28 | 29 | private fun normalizeUrl(urlMapping: String): String { 30 | val urlString = run { 31 | if (urlMapping.isBlank()) return@run "/" 32 | if (!urlMapping.startsWith("/")) return@run "/$urlMapping" 33 | return@run urlMapping 34 | } 35 | 36 | return parseVaadinUrlMapping(urlString).getPresentation(VaadinUrlRenderer) 37 | } 38 | 39 | private object VaadinUrlRenderer : UrlPath.PathSegmentRenderer { 40 | override fun visitVariable(variable: UrlPath.PathSegment.Variable): String { 41 | return "{${variable.variableName}}" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetVaadinSecurityHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.search.GlobalSearchScope 6 | import com.vaadin.plugin.endpoints.findSecurityConfig 7 | import com.vaadin.plugin.endpoints.findUserDetails 8 | import io.netty.handler.codec.http.HttpResponseStatus 9 | 10 | class GetVaadinSecurityHandler(project: Project) : AbstractHandler(project) { 11 | 12 | override fun run(): HandlerResponse { 13 | return ApplicationManager.getApplication().runReadAction { 14 | val security = findSecurityConfig(project, GlobalSearchScope.allScope(project)) 15 | val userDetails = findUserDetails(project, GlobalSearchScope.allScope(project)) 16 | 17 | val mapSecurity = 18 | security.map { component -> 19 | mapOf( 20 | "class" to component.className, 21 | "origin" to component.origin, 22 | "source" to component.source, 23 | "path" to component.path, 24 | "loginView" to component.loginView) 25 | } 26 | val mapUserDetails = 27 | userDetails.filterNot({ component -> component.origin == "library" }).map { component -> 28 | mapOf( 29 | "class" to component.className, 30 | "origin" to component.origin, 31 | "source" to component.source, 32 | "path" to component.path, 33 | "entity" to component.entity?.joinToString(",")) 34 | } 35 | 36 | LOG.info("Security detected: $security") 37 | 38 | HandlerResponse( 39 | status = HttpResponseStatus.OK, 40 | data = mapOf("security" to mapSecurity, "userDetails" to mapUserDetails)) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/RestartApplicationHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.execution.process.BaseOSProcessHandler 4 | import com.intellij.execution.runners.ExecutionUtil 5 | import com.intellij.execution.ui.RunContentDescriptor 6 | import com.intellij.execution.ui.RunContentManager 7 | import com.intellij.openapi.application.runInEdt 8 | import com.intellij.openapi.project.Project 9 | 10 | /** 11 | * Handler for restarting the application. If the main class is provided, the handler will restart the application with 12 | * the provided main class. 13 | */ 14 | class RestartApplicationHandler(project: Project, data: Map?) : AbstractHandler(project) { 15 | 16 | private val expectedMainClass: String? = data?.get("mainClass") as String? 17 | 18 | override fun run(): HandlerResponse { 19 | val contentManager = RunContentManager.getInstance(project) 20 | 21 | val descriptor = findMatchingDescriptor(contentManager) ?: return RESPONSE_BAD_REQUEST 22 | 23 | runInEdt { ExecutionUtil.restart(descriptor) } 24 | 25 | return RESPONSE_OK 26 | } 27 | 28 | private fun findMatchingDescriptor(runContentManager: RunContentManager): RunContentDescriptor? { 29 | // If the main class is not provided, use default descriptor 30 | if (expectedMainClass == null) { 31 | return runContentManager.selectedContent 32 | } 33 | 34 | // Find descriptor with matching main class 35 | for (descriptor in runContentManager.allDescriptors) { 36 | val processHandler = descriptor.processHandler 37 | 38 | // Support only BaseOSProcessHandler 39 | if (processHandler !is BaseOSProcessHandler) { 40 | continue 41 | } 42 | 43 | val mainClassName = processHandler.commandLine.split(" ").last() 44 | if (mainClassName != expectedMainClass) { 45 | continue 46 | } 47 | 48 | return descriptor 49 | } 50 | 51 | return null 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/starter/StarterProjectModel.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.starter 2 | 3 | import com.intellij.openapi.components.BaseState 4 | import com.intellij.openapi.observable.properties.PropertyGraph 5 | import com.intellij.openapi.project.Project 6 | import com.vaadin.plugin.utils.toArtifactId 7 | import java.net.URLEncoder 8 | import java.nio.charset.StandardCharsets 9 | 10 | class StarterProjectModel(val groupIdProperty: com.intellij.openapi.observable.properties.GraphProperty) : 11 | BaseState(), DownloadableModel { 12 | 13 | private val graph: PropertyGraph = PropertyGraph() 14 | val usePrereleaseProperty = graph.property(false) 15 | val includeFlowProperty = graph.property(true) 16 | val includeHillaProperty = graph.property(false) 17 | 18 | private val usePrerelease by usePrereleaseProperty 19 | private val includeFlow by includeFlowProperty 20 | private val includeHilla by includeHillaProperty 21 | private val groupId by groupIdProperty 22 | 23 | override fun getDownloadLink(project: Project): String { 24 | val frameworks = 25 | when { 26 | includeFlow && includeHilla -> "flow,hilla" 27 | includeFlow -> "flow" 28 | includeHilla -> "hilla" 29 | else -> "empty" 30 | } 31 | 32 | val platformVersion = if (usePrerelease) "pre" else "latest" 33 | 34 | val params = 35 | mapOf( 36 | "frameworks" to frameworks, 37 | "platformVersion" to platformVersion, 38 | "artifactId" to toArtifactId(project.name), 39 | "groupId" to groupId, 40 | "ref" to "intellij-plugin") 41 | val query = 42 | params.entries.joinToString("&") { (key, value) -> 43 | "${URLEncoder.encode(key, StandardCharsets.UTF_8)}=${URLEncoder.encode(value, StandardCharsets.UTF_8)}" 44 | } 45 | 46 | return "https://start.vaadin.com/skeleton?${query}" 47 | } 48 | 49 | override fun getProjectType(): String { 50 | return "maven" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin-api-test-client/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/DeleteFileHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.runInEdt 4 | import com.intellij.openapi.command.CommandProcessor 5 | import com.intellij.openapi.command.UndoConfirmationPolicy 6 | import com.intellij.openapi.command.WriteCommandAction 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.vfs.ReadonlyStatusHandler 9 | import com.intellij.openapi.vfs.VfsUtil 10 | import java.io.File 11 | 12 | class DeleteFileHandler(project: Project, data: Map) : AbstractHandler(project) { 13 | 14 | private val ioFile: File = File(data["file"] as String) 15 | 16 | override fun run(): HandlerResponse { 17 | if (!isFileInsideProject(project, ioFile)) { 18 | LOG.warn("File $ioFile is not a part of a project") 19 | return RESPONSE_BAD_REQUEST 20 | } 21 | 22 | val vfsFile = VfsUtil.findFileByIoFile(ioFile, true) ?: return RESPONSE_ERROR 23 | if (!vfsFile.exists()) { 24 | LOG.warn("File $ioFile does not exist") 25 | return RESPONSE_ERROR 26 | } 27 | 28 | runInEdt { 29 | if (!ReadonlyStatusHandler.ensureFilesWritable(project, vfsFile)) { 30 | LOG.warn("File $ioFile is not writable, cannot delete") 31 | return@runInEdt 32 | } 33 | 34 | CommandProcessor.getInstance() 35 | .executeCommand( 36 | project, 37 | { 38 | WriteCommandAction.runWriteCommandAction(project) { 39 | try { 40 | vfsFile.delete(this) 41 | LOG.info("File $ioFile deleted") 42 | } catch (e: Exception) { 43 | LOG.error("Failed to delete file $ioFile", e) 44 | } 45 | } 46 | }, 47 | "Vaadin Copilot Delete File", 48 | null, 49 | UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION) 50 | } 51 | 52 | return RESPONSE_OK 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/CopilotErrorHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.application.ApplicationInfo 5 | import com.intellij.openapi.diagnostic.ErrorReportSubmitter 6 | import com.intellij.openapi.diagnostic.IdeaLoggingEvent 7 | import com.intellij.openapi.diagnostic.SubmittedReportInfo 8 | import com.intellij.util.Consumer 9 | import java.awt.Component 10 | import java.lang.management.ManagementFactory 11 | 12 | class CopilotErrorHandler : ErrorReportSubmitter() { 13 | 14 | private val ghMaxBodyLength = 5000 15 | 16 | private val url = "https://github.com/vaadin/intellij-plugin/issues/new" 17 | 18 | override fun getReportActionText(): String { 19 | return "Report on GitHub" 20 | } 21 | 22 | override fun submit( 23 | events: Array, 24 | additionalInfo: String?, 25 | parentComponent: Component, 26 | consumer: Consumer, 27 | ): Boolean { 28 | var throwableText = events.iterator().next().throwableText 29 | val firstLine = throwableText.split("\\n".toRegex(), 2)[0].trim() 30 | val appName = ApplicationInfo.getInstance().fullApplicationName 31 | 32 | if (throwableText.length > ghMaxBodyLength) { 33 | throwableText = throwableText.substring(0, ghMaxBodyLength) 34 | } 35 | 36 | val runtimeMXBean = ManagementFactory.getRuntimeMXBean() 37 | var body = 38 | "Plugin version: **${pluginDescriptor.version}**\n" + 39 | "IDE version: **$appName**\n" + 40 | "VM: **${runtimeMXBean.vmName + " " + runtimeMXBean.vmVersion + " " + runtimeMXBean.vmVendor}**\n" + 41 | "OS: **${System.getProperty("os.name") + " " + System.getProperty("os.version")}**\n\n" 42 | 43 | if (additionalInfo != null) { 44 | body += "Additional info:\n" + "> ${additionalInfo.replace("\\n+".toRegex(), "\n")}" + "\n\n" 45 | } 46 | 47 | body += "Stacktrace:\n```\n$throwableText\n```" 48 | 49 | val title = "[Error report] $firstLine" 50 | BrowserUtil.browse("$url?title=$title&body=$body") 51 | return true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/ShowInIdeHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.runInEdt 4 | import com.intellij.openapi.editor.LogicalPosition 5 | import com.intellij.openapi.editor.ScrollType 6 | import com.intellij.openapi.fileEditor.FileEditorManager 7 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.vfs.VfsUtil 10 | import com.vaadin.plugin.utils.IdeUtil 11 | import java.io.File 12 | 13 | class ShowInIdeHandler(project: Project, data: Map) : AbstractHandler(project) { 14 | 15 | private val ioFile: File = File(data["file"] as String) 16 | private val line: Int = (data["line"] as Int?) ?: 0 17 | private val column: Int = (data["column"] as Int?) ?: 0 18 | 19 | override fun run(): HandlerResponse { 20 | if (line < 0 || column < 0) { 21 | LOG.warn("Invalid line or column number ($line:$column)") 22 | return RESPONSE_BAD_REQUEST 23 | } 24 | if (isFileInsideProject(project, ioFile)) { 25 | val result = 26 | VfsUtil.findFileByIoFile(ioFile, true)?.let { file -> 27 | val openFileDescriptor = OpenFileDescriptor(project, file) 28 | runInEdt { 29 | FileEditorManager.getInstance(project).openTextEditor(openFileDescriptor, true)?.let { editor -> 30 | editor.selectionModel.removeSelection() 31 | editor.caretModel.currentCaret.moveToLogicalPosition(LogicalPosition(line, column)) 32 | editor.scrollingModel.scrollToCaret(ScrollType.CENTER) 33 | } 34 | IdeUtil.bringToFront(project) 35 | } 36 | LOG.info("File $ioFile opened at $line:$column") 37 | true 38 | } 39 | if (result != true) { 40 | LOG.warn("Cannot open $ioFile at $line:$column, file does not exist or is not readable") 41 | return RESPONSE_ERROR 42 | } 43 | 44 | return RESPONSE_OK 45 | } else { 46 | LOG.warn("File $ioFile is not a part of a project") 47 | return RESPONSE_BAD_REQUEST 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/symbols/HillaSymbolReference.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.symbols 2 | 3 | import com.intellij.model.Symbol 4 | import com.intellij.model.psi.PsiSymbolReference 5 | import com.intellij.openapi.util.TextRange 6 | import com.intellij.psi.JavaPsiFacade 7 | import com.intellij.psi.PsiElement 8 | import com.intellij.psi.search.GlobalSearchScope 9 | import com.intellij.psi.search.ProjectScope 10 | import com.intellij.psi.search.searches.AnnotatedElementsSearch 11 | import com.vaadin.plugin.endpoints.HILLA_BROWSER_CALLABLE 12 | 13 | class HillaSymbolReference(private val element: PsiElement) : PsiSymbolReference { 14 | 15 | override fun resolveReference(): Collection { 16 | val hillaBrowserCallableClass = 17 | JavaPsiFacade.getInstance(element.project) 18 | .findClass(HILLA_BROWSER_CALLABLE, ProjectScope.getLibrariesScope(element.project)) ?: return emptySet() 19 | 20 | val scope = GlobalSearchScope.allScope(element.project) 21 | 22 | var className = element.text 23 | var methodName: String? = null 24 | 25 | // both ClassName and ClassName.methodName are JSReferenceExpressions 26 | if (element.textContains('.')) { 27 | element.text.split('.').let { 28 | className = it[0] 29 | methodName = it[1] 30 | } 31 | } 32 | 33 | val psiClasses = 34 | AnnotatedElementsSearch.searchPsiClasses(hillaBrowserCallableClass, scope).filter { 35 | it.name?.endsWith(className) == true 36 | } 37 | 38 | if (psiClasses.isEmpty()) { 39 | return emptySet() 40 | } 41 | 42 | if (methodName != null) { 43 | return psiClasses 44 | .mapNotNull { it.findMethodsByName(methodName, true).firstOrNull() } 45 | .map { HillaSymbol(it) } 46 | } 47 | 48 | return psiClasses.map { HillaSymbol(it) } 49 | } 50 | 51 | override fun getElement(): PsiElement { 52 | return element 53 | } 54 | 55 | override fun getRangeInElement(): TextRange { 56 | val indexOfDot = element.text.indexOf('.') 57 | if (indexOfDot != -1) { 58 | return TextRange.from(indexOfDot + 1, element.text.length - indexOfDot) 59 | } 60 | return TextRange.allOf(element.text) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinReferenceContributor.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.codeInsight.highlighting.HighlightedReference 4 | import com.intellij.lang.properties.references.PropertyReference 5 | import com.intellij.microservices.jvm.url.uastUrlPathReferenceInjectorForScheme 6 | import com.intellij.microservices.jvm.url.uastUrlReferenceProvider 7 | import com.intellij.microservices.url.HTTP_SCHEMES 8 | import com.intellij.patterns.PsiJavaPatterns.psiMethod 9 | import com.intellij.patterns.uast.callExpression 10 | import com.intellij.patterns.uast.injectionHostUExpression 11 | import com.intellij.psi.* 12 | import com.intellij.util.ProcessingContext 13 | import org.jetbrains.uast.UElement 14 | import org.jetbrains.uast.expressions.UInjectionHost 15 | 16 | internal class VaadinReferenceContributor : PsiReferenceContributor() { 17 | override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { 18 | registrar.registerUastReferenceProvider( 19 | injectionHostUExpression().annotationParam(VAADIN_ROUTE, "value"), 20 | uastUrlReferenceProvider(uastUrlPathReferenceInjectorForScheme(HTTP_SCHEMES, vaadinUrlPksParser))) 21 | 22 | registrar.registerUastReferenceProvider( 23 | injectionHostUExpression() 24 | .callParameter( 25 | 0, 26 | callExpression() 27 | .withMethodName("getTranslation") 28 | .withAnyResolvedMethod( 29 | psiMethod() 30 | .withName("getTranslation") 31 | .definedInClass("com.vaadin.flow.component.Component"))), 32 | object : UastReferenceProvider() { 33 | override fun getReferencesByElement( 34 | element: UElement, 35 | context: ProcessingContext 36 | ): Array { 37 | if (element !is UInjectionHost) return PsiReference.EMPTY_ARRAY 38 | val key = element.evaluateToString() ?: return PsiReference.EMPTY_ARRAY 39 | val sourcePsi = element.sourcePsi ?: return PsiReference.EMPTY_ARRAY 40 | 41 | return arrayOf(object : PropertyReference(key, sourcePsi, null, false), HighlightedReference {}) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/hotswapagent/HotswapAgentExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.hotswapagent 2 | 3 | import com.intellij.execution.JavaRunConfigurationBase 4 | import com.intellij.execution.RunManager 5 | import com.intellij.execution.configurations.RunConfiguration 6 | import com.intellij.execution.executors.DefaultDebugExecutor 7 | import com.intellij.openapi.project.Project 8 | import com.vaadin.plugin.utils.VaadinIcons 9 | import javax.swing.Icon 10 | 11 | class HotswapAgentExecutor : DefaultDebugExecutor() { 12 | 13 | companion object { 14 | const val ID = "Vaadin.HotswapAgentExecutor" 15 | } 16 | 17 | override fun getDescription(): String { 18 | return "Debug using HotswapAgent" 19 | } 20 | 21 | override fun getId(): String { 22 | return ID 23 | } 24 | 25 | override fun getToolWindowId(): String { 26 | return id 27 | } 28 | 29 | override fun getActionName(): String { 30 | return description 31 | } 32 | 33 | override fun getStartActionText(): String { 34 | return description 35 | } 36 | 37 | override fun getStartActionText(configurationName: String): String { 38 | return description 39 | } 40 | 41 | override fun getContextActionId(): String { 42 | return "$id-action" 43 | } 44 | 45 | override fun getIcon(): Icon { 46 | return VaadinIcons.DEBUG_HOTSWAP 47 | } 48 | 49 | override fun getToolWindowIcon(): Icon { 50 | return VaadinIcons.DEBUG_HOTSWAP 51 | } 52 | 53 | override fun getDisabledIcon(): Icon { 54 | return super.getDisabledIcon() 55 | } 56 | 57 | override fun isApplicable(project: Project): Boolean { 58 | val selectedConfiguration = RunManager.getInstance(project).selectedConfiguration?.configuration 59 | return isCurrentFile(selectedConfiguration) || 60 | (selectedConfiguration is JavaRunConfigurationBase && 61 | (!isMaven(selectedConfiguration) || !isGradle(selectedConfiguration))) 62 | } 63 | 64 | private fun isCurrentFile(configuration: RunConfiguration?): Boolean { 65 | return configuration == null 66 | } 67 | 68 | private fun isMaven(configuration: RunConfiguration): Boolean { 69 | return configuration.javaClass.name.contains("MavenRunConfiguration") 70 | } 71 | 72 | private fun isGradle(configuration: RunConfiguration): Boolean { 73 | return configuration.javaClass.name.contains("GradleRunConfiguration") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/GetVaadinVersionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.JarFileSystem 6 | import com.intellij.openapi.vfs.VirtualFile 7 | import com.intellij.psi.JavaPsiFacade 8 | import com.intellij.psi.PsiClass 9 | import com.intellij.psi.search.GlobalSearchScope 10 | import com.vaadin.plugin.utils.VAADIN_SERVICE 11 | import io.netty.handler.codec.http.HttpResponseStatus 12 | 13 | class GetVaadinVersionHandler(project: Project) : AbstractHandler(project) { 14 | 15 | override fun run(): HandlerResponse { 16 | return ApplicationManager.getApplication().runReadAction { 17 | val psiClass: PsiClass = 18 | JavaPsiFacade.getInstance(project).findClass(VAADIN_SERVICE, GlobalSearchScope.allScope(project)) 19 | ?: return@runReadAction HandlerResponse( 20 | status = HttpResponseStatus.NOT_FOUND, data = mapOf("error" to "VaadinService class not found")) 21 | 22 | LOG.info("Vaadin Service psiClass: ${psiClass.qualifiedName}") 23 | 24 | val classFile: VirtualFile = 25 | psiClass.containingFile.virtualFile 26 | ?: return@runReadAction HandlerResponse( 27 | status = HttpResponseStatus.NOT_FOUND, 28 | data = mapOf("error" to "VaadinService class file not found")) 29 | 30 | LOG.info("Vaadin Service class file: ${classFile.path}") 31 | 32 | val jarFile = JarFileSystem.getInstance().getVirtualFileForJar(classFile) 33 | val version = 34 | if (jarFile != null) { 35 | extractArtifactVersionFromJarPath(jarFile.path) 36 | } else { 37 | "N/A" 38 | } 39 | 40 | LOG.info("Vaadin Version detected: $version") 41 | 42 | HandlerResponse(status = HttpResponseStatus.OK, data = mapOf("version" to version) as Map?) 43 | } 44 | } 45 | 46 | fun extractArtifactVersionFromJarPath(jarPath: String): String? { 47 | val jarName = jarPath.substringAfterLast('/') 48 | if (!jarName.endsWith(".jar")) return null 49 | 50 | val baseName = jarName.removeSuffix(".jar") 51 | 52 | // Heuristic: version starts at the last dash, assuming format like artifactId-version.jar 53 | val dashIndex = baseName.lastIndexOf('-') 54 | if (dashIndex == -1 || dashIndex == baseName.length - 1) return null 55 | 56 | return baseName.substring(dashIndex + 1) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/hilla.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/psi/HillaReferenceSearcher.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.psi 2 | 3 | import com.intellij.lang.javascript.JSElementTypes 4 | import com.intellij.openapi.application.QueryExecutorBase 5 | import com.intellij.openapi.application.ReadAction 6 | import com.intellij.openapi.fileTypes.FileTypeManager 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.psi.PsiElement 9 | import com.intellij.psi.PsiReference 10 | import com.intellij.psi.PsiReferenceBase 11 | import com.intellij.psi.search.GlobalSearchScope 12 | import com.intellij.psi.search.PsiSearchHelper 13 | import com.intellij.psi.search.UsageSearchContext 14 | import com.intellij.psi.search.searches.MethodReferencesSearch 15 | import com.intellij.psi.util.elementType 16 | import com.intellij.util.Processor 17 | import com.vaadin.plugin.endpoints.HILLA_BROWSER_CALLABLE 18 | 19 | class HillaReferenceSearcher : QueryExecutorBase() { 20 | 21 | private val tsFileType = FileTypeManager.getInstance().getFileTypeByExtension("ts") 22 | private val tsxFileType = FileTypeManager.getInstance().getFileTypeByExtension("tsx") 23 | private val jsFileType = FileTypeManager.getInstance().getFileTypeByExtension("js") 24 | private val jsxFileType = FileTypeManager.getInstance().getFileTypeByExtension("jsx") 25 | 26 | override fun processQuery( 27 | queryParameters: MethodReferencesSearch.SearchParameters, 28 | consumer: Processor 29 | ) { 30 | val psiMethod = queryParameters.method 31 | 32 | ReadAction.run { 33 | if (psiMethod.containingClass?.hasAnnotation(HILLA_BROWSER_CALLABLE) != true) { 34 | return@run 35 | } 36 | 37 | val searchHelper = PsiSearchHelper.getInstance(queryParameters.project) 38 | val scope = GlobalSearchScope.projectScope(queryParameters.project) 39 | 40 | val filteredScope = 41 | GlobalSearchScope.getScopeRestrictedByFileTypes(scope, tsFileType, tsxFileType, jsFileType, jsxFileType) 42 | searchHelper.processElementsWithWord( 43 | { psiElement, offset -> 44 | if (psiElement.elementType == JSElementTypes.REFERENCE_EXPRESSION) { 45 | val range = TextRange(offset, offset + psiMethod.name.length) 46 | val reference = 47 | object : PsiReferenceBase(psiElement, range) { 48 | override fun resolve(): PsiElement = psiElement 49 | } 50 | consumer.process(reference) 51 | } 52 | true // return false to stop the search early 53 | }, 54 | filteredScope, 55 | psiMethod.name, 56 | UsageSearchContext.IN_CODE, 57 | true) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotRestService.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.service 2 | 3 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.readValue 5 | import com.google.gson.Gson 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.project.ProjectManager 8 | import com.vaadin.plugin.copilot.CommandRequest 9 | import com.vaadin.plugin.copilot.CopilotPluginUtil 10 | import com.vaadin.plugin.copilot.RestUtil 11 | import io.netty.buffer.ByteBuf 12 | import io.netty.buffer.Unpooled 13 | import io.netty.channel.ChannelHandlerContext 14 | import io.netty.handler.codec.http.* 15 | import java.nio.charset.Charset 16 | import java.nio.file.Path 17 | import org.jetbrains.ide.RestService 18 | import org.jetbrains.io.response 19 | 20 | class CopilotRestService : RestService() { 21 | 22 | private val LOG: Logger = Logger.getInstance(CopilotRestService::class.java) 23 | 24 | override fun getServiceName(): String { 25 | return RestUtil.getServiceName() 26 | } 27 | 28 | override fun execute( 29 | urlDecoder: QueryStringDecoder, 30 | request: FullHttpRequest, 31 | context: ChannelHandlerContext, 32 | ): String? { 33 | val charset = HttpUtil.getCharset(request, Charset.defaultCharset()) 34 | val copilotRequest: CommandRequest = jacksonObjectMapper().readValue(request.content().toString(charset)) 35 | 36 | if (copilotRequest.projectBasePath == null) { 37 | sendStatus(HttpResponseStatus.BAD_REQUEST, false, context.channel()) 38 | return null 39 | } 40 | 41 | val projectBasePath = Path.of(copilotRequest.projectBasePath).toRealPath() 42 | val project = 43 | ProjectManager.getInstance().openProjects.find { 44 | Path.of(it.basePath!!).toRealPath().equals(projectBasePath) 45 | } 46 | 47 | if (project == null) { 48 | LOG.error( 49 | "Requested project location $projectBasePath does not match any of opened projects: " + 50 | ProjectManager.getInstance().openProjects.mapNotNull { it.basePath }.joinToString { it }) 51 | sendStatus(HttpResponseStatus.BAD_REQUEST, false, context.channel()) 52 | return null 53 | } 54 | 55 | val handlerResponse = 56 | CopilotPluginUtil.createCommandHandler(copilotRequest.command, project, copilotRequest.data).run() 57 | if (handlerResponse.data == null) { 58 | sendStatus(handlerResponse.status, HttpUtil.isKeepAlive(request), context.channel()) 59 | } else { 60 | val json = Gson().toJson(handlerResponse.data) 61 | val buff: ByteBuf = Unpooled.copiedBuffer(json, Charset.forName("UTF-8")) 62 | sendResponse(request, context, response("application/json", buff)) 63 | } 64 | return null 65 | } 66 | 67 | override fun isMethodSupported(method: HttpMethod): Boolean { 68 | return method === HttpMethod.POST 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/debug.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugin/src/main/resources/vaadin/icons/debug_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/service/CompilationStatusManagerService.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.service 2 | 3 | import com.intellij.openapi.compiler.CompilationStatusListener 4 | import com.intellij.openapi.compiler.CompileContext 5 | import com.intellij.openapi.compiler.CompilerMessageCategory 6 | import com.intellij.openapi.compiler.CompilerTopics 7 | import com.intellij.openapi.components.Service 8 | import com.intellij.openapi.project.Project 9 | 10 | @Service(Service.Level.PROJECT) 11 | class CompilationStatusManagerService(private val project: Project) : CompilationStatusListener { 12 | 13 | /** 14 | * Holds compilation error state for a given project, including: 15 | * - [hasCompilationError]: whether the last compilation attempt had errors 16 | * - [errorFiles]: a set of file paths (as strings) that caused the compilation errors 17 | */ 18 | data class CompilationErrorInfo( 19 | var hasCompilationError: Boolean = false, 20 | val errorFiles: MutableSet = mutableSetOf() 21 | ) 22 | 23 | private var compilationErrorInfo: CompilationErrorInfo? = null 24 | 25 | /** Subscribes to project message bus to listen compilation results */ 26 | fun subscribeToCompilationStatus() { 27 | project.messageBus.connect().subscribe(CompilerTopics.COMPILATION_STATUS, this) 28 | } 29 | 30 | override fun compilationFinished(aborted: Boolean, errors: Int, warnings: Int, compileContext: CompileContext) { 31 | val filePaths = mutableSetOf() 32 | if (errors > 0) { 33 | val messages = compileContext.getMessages(CompilerMessageCategory.ERROR) 34 | for (message in messages) { 35 | message.virtualFile?.let { virtualFile -> filePaths.add(virtualFile.path) } 36 | } 37 | } 38 | setHasCompilationError(errors > 0, filePaths) 39 | super.compilationFinished(aborted, errors, warnings, compileContext) 40 | } 41 | 42 | /** 43 | * Checks if the given [project] currently has compilation errors. 44 | * 45 | * @return `true` if the project has errors, otherwise `false`. 46 | */ 47 | fun hasCompilationError(): Boolean { 48 | return compilationErrorInfo?.hasCompilationError ?: false 49 | } 50 | 51 | /** 52 | * Retrieves the set of file names that caused compilation errors for the given [project]. 53 | * 54 | * @return A [Set] of file paths that had errors, or an empty set if none are recorded. 55 | */ 56 | fun getErrorFiles(): Set { 57 | return compilationErrorInfo?.errorFiles ?: emptySet() 58 | } 59 | 60 | /** 61 | * Updates the compilation error status for the given [project]. 62 | * 63 | * @param hasError Whether the project has compilation errors. 64 | * @param files The set of file names (or paths) that caused compilation errors. 65 | */ 66 | private fun setHasCompilationError(hasError: Boolean, files: Set = emptySet()) { 67 | val info = compilationErrorInfo ?: CompilationErrorInfo() 68 | info.hasCompilationError = hasError 69 | info.errorFiles.clear() 70 | info.errorFiles.addAll(files) 71 | this.compilationErrorInfo = info 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /plugin-api-test-client/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/ui/settings/VaadinSettingsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.ui.settings 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.ui.ColorUtil 5 | import com.intellij.ui.HyperlinkAdapter 6 | import com.intellij.ui.JBColor 7 | import com.intellij.ui.components.JBCheckBox 8 | import com.intellij.ui.scale.JBUIScale.scale 9 | import com.intellij.util.ui.FormBuilder 10 | import com.intellij.util.ui.JBUI 11 | import com.intellij.util.ui.SwingHelper 12 | import com.intellij.util.ui.UIUtil 13 | import java.awt.GridBagConstraints 14 | import java.awt.GridBagLayout 15 | import javax.swing.JComponent 16 | import javax.swing.JPanel 17 | import javax.swing.event.HyperlinkEvent 18 | import javax.swing.text.html.HTMLDocument 19 | 20 | /** Supports creating and managing a [JPanel] for the Settings Dialog. */ 21 | class VaadinSettingsComponent { 22 | val panel: JPanel 23 | private val sendUsageStatistics = JBCheckBox("Send usage statistics") 24 | 25 | init { 26 | val pane = JPanel(GridBagLayout()) 27 | val viewer = SwingHelper.createHtmlViewer(true, null, JBColor.WHITE, JBColor.BLACK) 28 | viewer.isOpaque = false 29 | viewer.isFocusable = false 30 | UIUtil.doNotScrollToCaret(viewer) 31 | viewer.addHyperlinkListener( 32 | object : HyperlinkAdapter() { 33 | override fun hyperlinkActivated(e: HyperlinkEvent) { 34 | e.url?.let { BrowserUtil.browse(it) } 35 | } 36 | }) 37 | viewer.text = 38 | "" + 39 | "Help us improve Vaadin plugin by sending anonymous usage statistics.

" + 40 | "Please note that this will not include personal data or any sensitive information,
" + 41 | "such as source code, file names, etc. The data sent complies with the Vaadin Privacy Policy." + 42 | "" 43 | val styleSheet = (viewer.document as HTMLDocument).styleSheet 44 | styleSheet.addRule( 45 | "body {font-size: " + 46 | UIUtil.getFontSize(UIUtil.FontSize.SMALL) + 47 | "; color: #" + 48 | ColorUtil.toHex(UIUtil.getContextHelpForeground()) + 49 | ";}") 50 | pane.add( 51 | viewer, 52 | GridBagConstraints( 53 | 0, 54 | GridBagConstraints.RELATIVE, 55 | 1, 56 | 1, 57 | 1.0, 58 | 0.0, 59 | GridBagConstraints.NORTHWEST, 60 | GridBagConstraints.BOTH, 61 | JBUI.insets(0, scale(24), 0, 0), 62 | 0, 63 | 0)) 64 | 65 | panel = 66 | FormBuilder.createFormBuilder() 67 | .addComponent(sendUsageStatistics, 1) 68 | .addComponent(pane) 69 | .addComponentFillVertically(JPanel(), 0) 70 | .panel 71 | } 72 | 73 | val preferredFocusedComponent: JComponent 74 | get() = sendUsageStatistics 75 | 76 | var sendUsageStatisticsStatus: Boolean 77 | get() = sendUsageStatistics.isSelected 78 | set(newStatus) { 79 | sendUsageStatistics.isSelected = newStatus 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/utils/AmpliUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.utils 2 | 3 | import ai.grazie.utils.mpp.UUID 4 | import com.amplitude.ampli.Ampli 5 | import com.amplitude.ampli.DebugWithHotswap 6 | import com.amplitude.ampli.EventOptions 7 | import com.amplitude.ampli.LoadOptions 8 | import com.amplitude.ampli.ManualCopilotRestart 9 | import com.amplitude.ampli.PluginInitialized 10 | import com.amplitude.ampli.ProjectCreated 11 | import com.amplitude.ampli.ampli 12 | import com.intellij.openapi.application.ApplicationInfo 13 | import com.intellij.util.io.DigestUtil 14 | import com.vaadin.plugin.copilot.CopilotPluginUtil 15 | import com.vaadin.plugin.ui.settings.VaadinSettings 16 | import com.vaadin.pro.licensechecker.LocalProKey 17 | import elemental.json.Json 18 | import java.io.IOException 19 | import java.nio.charset.Charset 20 | 21 | private val eventOptions = 22 | EventOptions( 23 | platform = ApplicationInfo.getInstance().fullApplicationName, 24 | deviceModel = if (isUltimate()) "ultimate" else "community", 25 | language = System.getProperty("user.language"), 26 | country = System.getProperty("user.country"), 27 | region = System.getProperty("user.region"), 28 | osName = System.getProperty("os.name"), 29 | osVersion = System.getProperty("os.version"), 30 | appVersion = CopilotPluginUtil.getPluginVersion()) 31 | 32 | private var userId: String? = null 33 | 34 | private var vaadiner: Boolean? = null 35 | 36 | private fun getUserId(): String { 37 | if (userId == null) { 38 | userId = 39 | try { 40 | VaadinHomeUtil.getUserKey() 41 | } catch (e: IOException) { 42 | "user-" + UUID.random().text 43 | } 44 | ampli.load(LoadOptions(Ampli.Environment.IDEPLUGINS)) 45 | ampli.identify(userId, eventOptions) 46 | } 47 | return userId!! 48 | } 49 | 50 | private fun isVaadiner(): Boolean { 51 | if (vaadiner == null) { 52 | val proKey = LocalProKey.get() 53 | if (proKey != null) { 54 | val json = Json.parse(proKey.toJson()) 55 | vaadiner = if (json.hasKey("username")) json.getString("username").endsWith("@vaadin.com") else false 56 | } else { 57 | vaadiner = false 58 | } 59 | } 60 | return vaadiner!! 61 | } 62 | 63 | private fun getProKeyDigest(): String? { 64 | val proKey = LocalProKey.get() 65 | return if (proKey != null) { 66 | DigestUtil.sha256Hex(proKey.proKey.toByteArray(Charset.defaultCharset())) 67 | } else { 68 | null 69 | } 70 | } 71 | 72 | private val enabled: Boolean 73 | get() = VaadinSettings.instance.state.sendUsageStatistics 74 | 75 | internal fun trackPluginInitialized() { 76 | if (enabled) { 77 | ampli.pluginInitialized(getUserId(), PluginInitialized(isVaadiner(), getProKeyDigest())) 78 | } 79 | } 80 | 81 | internal fun trackProjectCreated(downloadUrl: String) { 82 | if (enabled) { 83 | ampli.projectCreated(getUserId(), ProjectCreated(isVaadiner(), downloadUrl)) 84 | } 85 | } 86 | 87 | internal fun trackManualCopilotRestart() { 88 | if (enabled) { 89 | ampli.manualCopilotRestart(getUserId(), ManualCopilotRestart(isVaadiner())) 90 | } 91 | } 92 | 93 | internal fun trackDebugWithHotswap() { 94 | if (enabled) { 95 | ampli.debugWithHotswap(getUserId(), DebugWithHotswap(isVaadiner())) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/module/VaadinProjectBuilderAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.module 2 | 3 | import com.intellij.ide.actions.NewProjectAction 4 | import com.intellij.ide.util.projectWizard.WizardContext 5 | import com.intellij.ide.wizard.GeneratorNewProjectWizardBuilderAdapter 6 | import com.intellij.ide.wizard.NewProjectWizardStep 7 | import com.intellij.openapi.fileEditor.FileEditorManager 8 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.util.io.FileUtil 11 | import com.intellij.openapi.vfs.VfsUtil 12 | import com.intellij.util.io.ZipUtil 13 | import com.vaadin.plugin.utils.DownloadUtil 14 | import com.vaadin.plugin.utils.IdeUtil 15 | import com.vaadin.plugin.utils.IdeUtil.getIdeaDirectoryPath 16 | import com.vaadin.plugin.utils.getVaadinPluginDescriptor 17 | import com.vaadin.plugin.utils.trackProjectCreated 18 | import java.io.File 19 | import java.nio.file.Files 20 | import java.nio.file.Path 21 | 22 | class VaadinProjectBuilderAdapter(private val vaadinWizard: VaadinProjectWizard = VaadinProjectWizard()) : 23 | GeneratorNewProjectWizardBuilderAdapter(vaadinWizard) { 24 | 25 | override fun createStep(context: WizardContext): NewProjectWizardStep { 26 | return vaadinWizard.createStep(context) 27 | } 28 | 29 | override fun createProject(name: String?, path: String?): Project? { 30 | return super.createProject(name, path)?.let { project -> 31 | val outputPath = Path.of(path!!) 32 | val downloadUrl = vaadinWizard.projectModel!!.getDownloadLink(project) 33 | val tempFile = FileUtil.generateRandomTemporaryPath("project", ".zip") 34 | DownloadUtil.download(project, downloadUrl, tempFile.toPath(), "Vaadin Project") 35 | .thenRun { 36 | // project zip contains single root directory, move contents to parent after 37 | // extracting 38 | val zipRoot = DownloadUtil.getZipRootFolder(tempFile)!! 39 | ZipUtil.extract(tempFile.toPath(), outputPath, null, true) 40 | IdeUtil.moveDirectoryContentsRobustly(outputPath.resolve(zipRoot).toFile(), outputPath.toFile()) 41 | FileUtil.delete(tempFile) 42 | copyVaadinIcon(project) 43 | } 44 | .thenRun { 45 | afterProjectCreated(project) 46 | trackProjectCreated(downloadUrl) 47 | } 48 | project 49 | } 50 | } 51 | 52 | // Show wizard only for new projects, not modules 53 | override fun isAvailable(): Boolean { 54 | return Thread.currentThread().stackTrace.any { element -> 55 | element.className.contains(NewProjectAction::class.java.name) 56 | } 57 | } 58 | 59 | private fun afterProjectCreated(project: Project) { 60 | VfsUtil.findFileByIoFile(File(project.basePath, "README.md"), true)?.let { 61 | val descriptor = OpenFileDescriptor(project, it) 62 | descriptor.isUsePreviewTab = true 63 | FileEditorManager.getInstance(project).openEditor(descriptor, true) 64 | } 65 | } 66 | 67 | private fun copyVaadinIcon(project: Project) { 68 | val ideaDir = getIdeaDirectoryPath(project) 69 | if (ideaDir != null) { 70 | FileUtil.createIfDoesntExist(ideaDir.toFile()) 71 | val classLoader = getVaadinPluginDescriptor().classLoader 72 | classLoader.getResourceAsStream("icon.png")?.use { inputStream -> 73 | Files.copy(inputStream, ideaDir.resolve("icon.png")) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/UndoHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.WriteAction 4 | import com.intellij.openapi.application.runInEdt 5 | import com.intellij.openapi.command.impl.UndoManagerImpl 6 | import com.intellij.openapi.command.undo.UndoManager 7 | import com.intellij.openapi.fileEditor.FileEditor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.vfs.VfsUtil 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.openapi.vfs.findDocument 12 | import io.netty.handler.codec.http.HttpResponseStatus 13 | import java.io.File 14 | 15 | open class UndoHandler(project: Project, data: Map) : AbstractHandler(project) { 16 | 17 | private val vfsFiles: ArrayList = ArrayList() 18 | 19 | init { 20 | val paths = data["files"] as Collection 21 | for (path in paths) { 22 | val file = File(path) 23 | if (isFileInsideProject(project, file)) { 24 | var vfsFile = VfsUtil.findFileByIoFile(file, true) 25 | if (vfsFile != null) { 26 | vfsFiles.add(vfsFile) 27 | } else { 28 | // if we want to undo file removal we need to create empty virtual file to write 29 | // content 30 | runInEdt { 31 | WriteAction.run { 32 | val parent = VfsUtil.createDirectories(file.parent) 33 | vfsFile = parent.createChildData(this, file.name) 34 | vfsFiles.add(vfsFile!!) 35 | } 36 | } 37 | } 38 | } else { 39 | LOG.warn("File $path is not a part of a project") 40 | } 41 | } 42 | } 43 | 44 | override fun run(): HandlerResponse { 45 | var performed = false 46 | for (vfsFile in vfsFiles) { 47 | val count = getOpsCount(vfsFile) 48 | if (count == 0) { 49 | continue 50 | } 51 | 52 | performed = true 53 | 54 | runInEdt { 55 | getEditorWrapper(vfsFile).use { wrapper -> 56 | val undoManager = UndoManagerImpl.getInstance(project) 57 | val editor = wrapper.getFileEditor() 58 | WriteAction.run { 59 | try { 60 | before(vfsFile) 61 | var i = 0 62 | while (i++ < count) { 63 | runManagerAction(undoManager, editor) 64 | } 65 | commitAndFlush(vfsFile.findDocument()) 66 | } finally { 67 | after(vfsFile) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | val data = mapOf("performed" to performed) 75 | return HandlerResponse(HttpResponseStatus.OK, data) 76 | } 77 | 78 | open fun getOpsCount(vfsFile: VirtualFile): Int { 79 | return getCopilotUndoManager().getUndoCount(vfsFile) 80 | } 81 | 82 | open fun before(vfsFile: VirtualFile) { 83 | getCopilotUndoManager().undoStart(vfsFile) 84 | } 85 | 86 | open fun runManagerAction(undoManager: UndoManager, editor: FileEditor) { 87 | undoManager.undo(editor) 88 | } 89 | 90 | open fun after(vfsFile: VirtualFile) { 91 | getCopilotUndoManager().undoDone(vfsFile) 92 | LOG.info("$vfsFile undo performed") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/AbstractHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.editor.Document 5 | import com.intellij.openapi.fileEditor.FileDocumentManager 6 | import com.intellij.openapi.fileEditor.FileEditor 7 | import com.intellij.openapi.fileEditor.FileEditorManager 8 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.openapi.vfs.findDocument 12 | import com.intellij.psi.PsiDocumentManager 13 | import com.vaadin.plugin.copilot.CopilotPluginUtil 14 | import com.vaadin.plugin.copilot.CopilotPluginUtil.Companion.getBaseDirectoriesForProject 15 | import com.vaadin.plugin.copilot.service.CopilotUndoManager 16 | import io.netty.handler.codec.http.HttpResponseStatus 17 | import java.io.File 18 | 19 | abstract class AbstractHandler(val project: Project) : Handler { 20 | 21 | val LOG: Logger = Logger.getInstance(CopilotPluginUtil::class.java) 22 | 23 | val RESPONSE_OK = HandlerResponse(HttpResponseStatus.OK) 24 | 25 | val RESPONSE_BAD_REQUEST = HandlerResponse(HttpResponseStatus.BAD_REQUEST) 26 | 27 | val RESPONSE_ERROR = HandlerResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR) 28 | 29 | class FileEditorWrapper( 30 | private val fileEditor: FileEditor, 31 | private val project: Project, 32 | private val closable: Boolean, 33 | ) : AutoCloseable { 34 | 35 | fun getFileEditor(): FileEditor { 36 | return fileEditor 37 | } 38 | 39 | override fun close() { 40 | if (closable) { 41 | FileEditorManager.getInstance(project).closeFile(fileEditor.file) 42 | } 43 | } 44 | } 45 | 46 | fun isFileInsideProject(project: Project, file: File): Boolean { 47 | if (file.exists()) { 48 | val path = file.toPath() 49 | return getBaseDirectoriesForProject(project).values.flatten().any { baseDirectory -> 50 | path.startsWith(baseDirectory) 51 | } 52 | } else { 53 | // New file 54 | return isFileInsideProject(project, file.parentFile) 55 | } 56 | } 57 | 58 | fun getEditorWrapper(vfsFile: VirtualFile): FileEditorWrapper { 59 | val manager = FileEditorManager.getInstance(project) 60 | val editors = manager.getEditors(vfsFile) 61 | if (editors.isEmpty()) { 62 | return FileEditorWrapper(manager.openFile(vfsFile, false).first(), project, true) 63 | } 64 | return FileEditorWrapper(editors.first(), project, false) 65 | } 66 | 67 | open fun postSave(vfsFile: VirtualFile) { 68 | LOG.info("File $vfsFile contents saved") 69 | notifyUndoManager(vfsFile) 70 | commitAndFlush(vfsFile.findDocument()) 71 | openFileInEditor(vfsFile) 72 | } 73 | 74 | fun openFileInEditor(vfsFile: VirtualFile) { 75 | val openFileDescriptor = OpenFileDescriptor(project, vfsFile) 76 | FileEditorManager.getInstance(project).openTextEditor(openFileDescriptor, false) 77 | } 78 | 79 | fun notifyUndoManager(vfsFile: VirtualFile) { 80 | getCopilotUndoManager().fileWritten(vfsFile) 81 | } 82 | 83 | fun commitAndFlush(vfsDoc: Document?) { 84 | if (vfsDoc != null) { 85 | PsiDocumentManager.getInstance(project).commitDocument(vfsDoc) 86 | FileDocumentManager.getInstance().saveDocuments(vfsDoc::equals) 87 | } 88 | } 89 | 90 | fun getCopilotUndoManager(): CopilotUndoManager { 91 | return project.getService(CopilotUndoManager::class.java) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotDotfileServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.service 2 | 3 | import com.intellij.openapi.application.WriteAction 4 | import com.intellij.openapi.application.runInEdt 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.project.ProjectManager 7 | import com.intellij.openapi.project.ProjectManagerListener 8 | import com.intellij.openapi.vfs.VfsUtil 9 | import com.intellij.openapi.vfs.VirtualFileManager 10 | import com.intellij.openapi.vfs.newvfs.BulkFileListener 11 | import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent 12 | import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent 13 | import com.intellij.openapi.vfs.newvfs.events.VFileEvent 14 | import com.vaadin.plugin.utils.IdeUtil.getIdeaDirectoryPath 15 | import java.io.IOException 16 | import java.nio.file.Files 17 | import java.nio.file.Path 18 | import kotlinx.coroutines.flow.MutableStateFlow 19 | import kotlinx.coroutines.flow.StateFlow 20 | import kotlinx.coroutines.flow.asStateFlow 21 | 22 | class CopilotDotfileServiceImpl(private val project: Project) : CopilotDotfileService { 23 | 24 | private val DOTFILE = ".copilot-plugin" 25 | 26 | private val _fileExists = MutableStateFlow(false) 27 | private val fileExists: StateFlow = _fileExists.asStateFlow() 28 | 29 | init { 30 | val connection = project.messageBus.connect() 31 | connection.subscribe( 32 | VirtualFileManager.VFS_CHANGES, 33 | object : BulkFileListener { 34 | override fun after(events: List) { 35 | for (event in events) { 36 | when (event) { 37 | is VFileCreateEvent -> 38 | if (Path.of(event.path).equals(getDotfilePath())) _fileExists.value = true 39 | is VFileDeleteEvent -> 40 | if (Path.of(event.path).equals(getDotfilePath())) _fileExists.value = false 41 | } 42 | } 43 | } 44 | }) 45 | 46 | // Initial state 47 | _fileExists.value = getDotfilePath()?.let { Files.exists(it) } ?: false 48 | 49 | ProjectManager.getInstance() 50 | .addProjectManagerListener( 51 | project, 52 | object : ProjectManagerListener { 53 | override fun projectClosing(project: Project) { 54 | removeDotfile() 55 | } 56 | }, 57 | ) 58 | } 59 | 60 | override fun isActive(): Boolean { 61 | return fileExists.value 62 | } 63 | 64 | override fun getDotfilePath(): Path? { 65 | return getIdeaDirectoryPath(project)?.resolve(DOTFILE) 66 | } 67 | 68 | @Throws(IOException::class) 69 | override fun removeDotfile() { 70 | runInEdt { WriteAction.run { getDotfilePath()?.let { VfsUtil.findFile(it, false)?.delete(this) } } } 71 | } 72 | 73 | @Throws(IOException::class) 74 | override fun createDotfile(content: String) { 75 | runInEdt { 76 | WriteAction.run { 77 | val dotfileDirectory = 78 | getIdeaDirectoryPath(project) ?: throw IOException("Could not find the .idea directory") 79 | val vfsDotfileDirectory = 80 | VfsUtil.createDirectoryIfMissing(dotfileDirectory.toString()) 81 | ?: throw IOException("Could not create .idea directory") 82 | val dotfile = vfsDotfileDirectory.createChildData(this, DOTFILE) 83 | VfsUtil.saveText(dotfile, content) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/service/CopilotUndoManager.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.service 2 | 3 | import com.intellij.openapi.components.Service 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.openapi.vfs.VirtualFileManager 7 | import com.intellij.openapi.vfs.newvfs.BulkFileListener 8 | import com.intellij.openapi.vfs.newvfs.events.VFileEvent 9 | import java.util.Stack 10 | import java.util.concurrent.locks.ReentrantLock 11 | 12 | @Service(Service.Level.PROJECT) 13 | class CopilotUndoManager(val project: Project) : BulkFileListener { 14 | 15 | companion object { 16 | const val ACTIONS_ON_SAVE_WINDOW = 1000 // ms window for actions on save 17 | } 18 | 19 | class Batch() { 20 | private val time = System.currentTimeMillis() 21 | private var count = 0 22 | 23 | fun getCount(): Int { 24 | return count 25 | } 26 | 27 | fun increment() { 28 | count += 1 29 | } 30 | 31 | fun isInProgress(): Boolean { 32 | return System.currentTimeMillis() - time <= ACTIONS_ON_SAVE_WINDOW 33 | } 34 | } 35 | 36 | private val undoStack: MutableMap> = mutableMapOf() 37 | private val redoStack: MutableMap> = mutableMapOf() 38 | private val locks: MutableMap = mutableMapOf() 39 | 40 | fun subscribeToVfsChanges() { 41 | project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this) 42 | } 43 | 44 | // increments latest batch for file if is current batch 45 | // locking prevents modifying stack during undo / redo 46 | override fun after(events: MutableList) { 47 | events 48 | .filter { ev -> ev.isFromSave } 49 | .filter { ev -> locks[ev.path] == null || !locks[ev.path]!!.isLocked } 50 | .forEach { 51 | val stack = undoStack[it.path] 52 | if (stack != null) { 53 | if (stack.peek().isInProgress()) { 54 | stack.peek().increment() 55 | } else { 56 | undoStack.remove(it.path) 57 | } 58 | } 59 | } 60 | } 61 | 62 | fun fileWritten(file: VirtualFile) { 63 | undoStack.getOrPut(file.path) { Stack() }.push(Batch()) 64 | } 65 | 66 | fun getUndoCount(file: VirtualFile): Int { 67 | return undoStack[file.path]?.peek()?.getCount() ?: 0 68 | } 69 | 70 | fun getRedoCount(file: VirtualFile): Int { 71 | return redoStack[file.path]?.peek()?.getCount() ?: 0 72 | } 73 | 74 | fun undoStart(file: VirtualFile) { 75 | locks.getOrPut(file.path) { ReentrantLock() }.lock() 76 | } 77 | 78 | fun redoStart(file: VirtualFile) { 79 | locks.getOrPut(file.path) { ReentrantLock() }.lock() 80 | } 81 | 82 | fun undoDone(file: VirtualFile) { 83 | popAndPush(file, undoStack, redoStack) 84 | locks[file.path]?.unlock() 85 | } 86 | 87 | fun redoDone(file: VirtualFile) { 88 | popAndPush(file, redoStack, undoStack) 89 | locks[file.path]?.unlock() 90 | } 91 | 92 | private fun popAndPush( 93 | file: VirtualFile, 94 | fromStacksMap: MutableMap>, 95 | targetStacksMap: MutableMap> 96 | ) { 97 | val batch = fromStacksMap[file.path]?.pop() 98 | targetStacksMap.getOrPut(file.path) { Stack() }.push(batch) 99 | if (fromStacksMap[file.path]?.isEmpty() == true) { 100 | fromStacksMap.remove(file.path) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/endpoints/VaadinUrlResolver.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.endpoints 2 | 3 | import com.intellij.microservices.jvm.cache.ModuleCacheValueHolder 4 | import com.intellij.microservices.jvm.cache.SourceLibSearchProvider 5 | import com.intellij.microservices.jvm.cache.UastCachedSearchUtils.sequenceWithCache 6 | import com.intellij.microservices.url.* 7 | import com.intellij.microservices.url.UrlPath.PathSegment 8 | import com.intellij.microservices.url.references.UrlPksParser 9 | import com.intellij.openapi.module.Module 10 | import com.intellij.openapi.module.ModuleManager 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.psi.PsiAnchor 13 | import com.intellij.psi.PsiElement 14 | import com.intellij.psi.util.PartiallyKnownString 15 | import com.intellij.psi.util.SplitEscaper 16 | import com.vaadin.plugin.utils.VaadinIcons 17 | import com.vaadin.plugin.utils.hasVaadin 18 | import javax.swing.Icon 19 | 20 | internal class VaadinUrlResolverFactory : UrlResolverFactory { 21 | override fun forProject(project: Project): UrlResolver? { 22 | return if (hasVaadin(project)) VaadinUrlResolver(project) else null 23 | } 24 | } 25 | 26 | internal class VaadinUrlResolver(private val project: Project) : UrlResolver { 27 | override val supportedSchemes: List 28 | get() = HTTP_SCHEMES 29 | 30 | override fun getVariants(): Iterable { 31 | return getAllModuleVariants(project).asIterable() 32 | } 33 | 34 | override fun resolve(request: UrlResolveRequest): Iterable { 35 | if (request.method != HttpMethods.GET) return emptyList() 36 | 37 | val allModuleVariants = getAllModuleVariants(project).toList() 38 | 39 | return UrlPath.combinations(request.path) 40 | .flatMap { path -> allModuleVariants.asSequence().filter { it.path.isCompatibleWith(path) } } 41 | .asIterable() 42 | } 43 | } 44 | 45 | internal val VAADIN_ROUTES_SEARCH: SourceLibSearchProvider, Module> = 46 | SourceLibSearchProvider("VAADIN_ROUTES") { p, _, scope -> findFlowRoutes(p, scope).toList() } 47 | 48 | private fun getAllModuleVariants(project: Project): Sequence { 49 | val modules = ModuleManager.getInstance(project).modules 50 | 51 | return modules.asSequence().flatMap(::getVariants).map(::VaadinUrlTargetInfo) 52 | } 53 | 54 | private fun getVariants(module: Module): Sequence { 55 | if (!hasVaadin(module)) return emptySequence() 56 | 57 | return sequenceWithCache(ModuleCacheValueHolder(module), VAADIN_ROUTES_SEARCH) 58 | } 59 | 60 | private class VaadinUrlTargetInfo(route: VaadinRoute) : UrlTargetInfo { 61 | private val anchor: PsiAnchor = route.anchor 62 | 63 | override val authorities: List 64 | get() = emptyList() 65 | 66 | override val path: UrlPath = parseVaadinUrlMapping(route.urlMapping) 67 | 68 | override val icon: Icon 69 | get() = VaadinIcons.VAADIN_BLUE 70 | 71 | override val schemes: List 72 | get() = HTTP_SCHEMES 73 | 74 | override fun resolveToPsiElement(): PsiElement? = anchor.retrieve() 75 | } 76 | 77 | internal val vaadinUrlPksParser: UrlPksParser = 78 | UrlPksParser( 79 | splitEscaper = { _, _ -> SplitEscaper.AcceptAll }, 80 | customPathSegmentExtractor = { part -> 81 | if (part.startsWith(":")) { 82 | val varName = part.removePrefix(":").substringBefore("?").substringBefore("(") 83 | PathSegment.Variable(varName) 84 | } else { 85 | PathSegment.Exact(part) 86 | } 87 | }) 88 | 89 | internal fun parseVaadinUrlMapping(urlMapping: String): UrlPath { 90 | return vaadinUrlPksParser.parseUrlPath(PartiallyKnownString(urlMapping)).urlPath 91 | } 92 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/ui/VaadinStatusBarWidget.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.ui 2 | 3 | import com.intellij.openapi.project.DumbService 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.ui.popup.JBPopupFactory 6 | import com.intellij.openapi.util.Disposer 7 | import com.intellij.openapi.wm.StatusBarWidget 8 | import com.intellij.openapi.wm.WindowManager 9 | import com.intellij.ui.IconManager 10 | import com.intellij.ui.awt.RelativePoint 11 | import com.intellij.util.Consumer 12 | import com.intellij.util.ui.JBUI.CurrentTheme.IconBadge 13 | import com.intellij.util.ui.UIUtil 14 | import com.vaadin.plugin.copilot.service.CopilotDotfileService 15 | import com.vaadin.plugin.utils.VaadinIcons 16 | import com.vaadin.plugin.utils.hasEndpoints 17 | import java.awt.Point 18 | import java.awt.event.MouseEvent 19 | import javax.swing.Icon 20 | 21 | class VaadinStatusBarWidget(private val project: Project) : StatusBarWidget, StatusBarWidget.IconPresentation { 22 | 23 | companion object { 24 | const val ID = "VaadinStatusBarPanel" 25 | 26 | fun update(project: Project) { 27 | WindowManager.getInstance().getStatusBar(project)?.updateWidget(ID) 28 | } 29 | } 30 | 31 | private var clicked: Boolean = false 32 | 33 | init { 34 | project.messageBus 35 | .connect() 36 | .subscribe( 37 | DumbService.DUMB_MODE, 38 | object : DumbService.DumbModeListener { 39 | override fun enteredDumbMode() { 40 | update(project) 41 | } 42 | 43 | override fun exitDumbMode() { 44 | update(project) 45 | } 46 | }) 47 | } 48 | 49 | override fun ID(): String { 50 | return ID 51 | } 52 | 53 | override fun getPresentation(): StatusBarWidget.WidgetPresentation { 54 | return this 55 | } 56 | 57 | override fun getClickConsumer(): Consumer { 58 | return Consumer { 59 | clicked = true 60 | showPopup(it) 61 | update(project) 62 | } 63 | } 64 | 65 | private fun showPopup(e: MouseEvent) { 66 | val panel = VaadinStatusBarInfoPopupPanel(project) 67 | val popup = JBPopupFactory.getInstance().createComponentPopupBuilder(panel, null).createPopup() 68 | panel.closePopup = { popup.cancel() } 69 | val dimension = popup.content.preferredSize 70 | val at = Point(0, -dimension.height) 71 | popup.show(RelativePoint(e.component, at)) 72 | // destroy popup on unexpected project close 73 | Disposer.register(this, popup) 74 | } 75 | 76 | override fun getTooltipText(): String { 77 | if (isActive() && hasEndpoints()) { 78 | return "Vaadin plugin is active, all features are available" 79 | } 80 | 81 | if (DumbService.isDumb(project)) { 82 | return "Indexing is in progress, please wait" 83 | } 84 | 85 | return "Not all features are available, click to see details" 86 | } 87 | 88 | override fun getIcon(): Icon { 89 | if (clicked) { 90 | return VaadinIcons.VAADIN 91 | } 92 | 93 | if (isActive() && hasEndpoints()) { 94 | return VaadinIcons.VAADIN 95 | } 96 | 97 | if (DumbService.isDumb(project)) { 98 | return IconManager.getInstance().withIconBadge(VaadinIcons.VAADIN, UIUtil.getLabelForeground()) 99 | } 100 | 101 | return IconManager.getInstance().withIconBadge(VaadinIcons.VAADIN, IconBadge.WARNING) 102 | } 103 | 104 | private fun isActive(): Boolean { 105 | return project.getService(CopilotDotfileService::class.java).isActive() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType 2 | import org.jetbrains.intellij.platform.gradle.TestFrameworkType 3 | import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask 4 | 5 | plugins { 6 | id("java") 7 | id("org.jetbrains.kotlin.jvm") version "1.9.21" 8 | id("org.jetbrains.intellij.platform") version "2.10.4" 9 | id("com.diffplug.spotless") version "7.2.1" 10 | 11 | id("com.adarshr.test-logger") version "4.0.0" 12 | } 13 | 14 | // version for building plugin 15 | val buildVersion = "2024.2" 16 | 17 | // compatibility range 18 | val sinceBuildProperty = "242" 19 | val untilBuildProperty = "253.*" 20 | 21 | // version for verifying plugin, check validation.yml 22 | val verifyVersion = 23 | if (hasProperty("verifyVersion")) { 24 | property("verifyVersion") as String 25 | } else { 26 | buildVersion 27 | } 28 | 29 | group = "com.vaadin" 30 | 31 | val publishChannel = 32 | if (hasProperty("publishChannel")) { 33 | property("publishChannel") as String 34 | } else { 35 | "Stable" 36 | } 37 | 38 | java { 39 | sourceCompatibility = JavaVersion.VERSION_21 40 | targetCompatibility = JavaVersion.VERSION_21 41 | } 42 | 43 | kotlin { jvmToolchain(21) } 44 | 45 | repositories { 46 | mavenCentral() 47 | intellijPlatform { defaultRepositories() } 48 | } 49 | 50 | dependencies { 51 | implementation("com.amplitude:java-sdk:[1.8.0,2.0)") 52 | implementation("org.json:json:20201115") 53 | implementation("com.vaadin:license-checker:1.13.3") { 54 | exclude(group = "net.java.dev.jna", module = "jna") 55 | exclude(group = "net.java.dev.jna", module = "jna-platform") 56 | } 57 | 58 | intellijPlatform { 59 | intellijIdeaUltimate(buildVersion) 60 | bundledPlugin("com.intellij.java") 61 | bundledPlugin("org.jetbrains.idea.maven") 62 | bundledPlugin("com.intellij.properties") 63 | bundledPlugin("com.intellij.microservices.jvm") 64 | bundledPlugin("JavaScript") 65 | 66 | pluginVerifier() 67 | zipSigner() 68 | 69 | testFramework(TestFrameworkType.Platform) 70 | } 71 | 72 | testImplementation(kotlin("test")) 73 | testImplementation("junit:junit:4.13.2") 74 | testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1") 75 | } 76 | 77 | intellijPlatform { 78 | pluginConfiguration { 79 | version = 80 | if (hasProperty("projVersion")) { 81 | property("projVersion") as String 82 | } else { 83 | "1.0-SNAPSHOT" 84 | } 85 | } 86 | pluginVerification { 87 | ides { create(IntelliJPlatformType.IntellijIdea, verifyVersion) } 88 | verificationReportsFormats = listOf(VerifyPluginTask.VerificationReportsFormats.MARKDOWN) 89 | } 90 | } 91 | 92 | configure { 93 | kotlin { 94 | // by default the target is every '.kt' and '.kts` file in the java sourcesets 95 | ktfmt("0.51").googleStyle().configure { 96 | it.setMaxWidth(120) 97 | it.setBlockIndent(4) 98 | it.setContinuationIndent(4) 99 | it.setRemoveUnusedImports(true) 100 | it.setManageTrailingCommas(false) 101 | } 102 | } 103 | kotlinGradle { 104 | target("*.gradle.kts") // default target for kotlinGradle 105 | ktfmt() 106 | } 107 | } 108 | 109 | tasks { 110 | patchPluginXml { 111 | sinceBuild.set(sinceBuildProperty) 112 | untilBuild.set(untilBuildProperty) 113 | } 114 | 115 | signPlugin { 116 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 117 | privateKey.set(System.getenv("PRIVATE_KEY")) 118 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 119 | } 120 | 121 | publishPlugin { 122 | channels.set(listOf(publishChannel)) 123 | token.set(System.getenv("PUBLISH_TOKEN")) 124 | } 125 | 126 | test { useJUnitPlatform() } 127 | } 128 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/hotswapagent/HotswapAgentProgramPatcher.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.hotswapagent 2 | 3 | import com.intellij.execution.Executor 4 | import com.intellij.execution.JavaRunConfigurationBase 5 | import com.intellij.execution.configurations.JavaParameters 6 | import com.intellij.execution.configurations.RunProfile 7 | import com.intellij.execution.runners.JavaProgramPatcher 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.vaadin.plugin.utils.VaadinHomeUtil 10 | 11 | class HotswapAgentProgramPatcher : JavaProgramPatcher() { 12 | 13 | private val LOG: Logger = Logger.getInstance(HotswapAgentProgramPatcher::class.java) 14 | 15 | override fun patchJavaParameters(executor: Executor?, runProfile: RunProfile?, javaParameters: JavaParameters?) { 16 | if (executor !is HotswapAgentExecutor) { 17 | return 18 | } 19 | if (runProfile !is JavaRunConfigurationBase) { 20 | return 21 | } 22 | if (javaParameters == null) { 23 | return 24 | } 25 | val module = runProfile.configurationModule?.module ?: return 26 | 27 | if (runProfile.javaClass.simpleName == "SpringBootApplicationRunConfiguration") { 28 | turnOffFrameDeactivationPolicy(runProfile) 29 | } 30 | if (!JdkUtil.isJetbrainsRuntime(javaParameters.jdk)) { 31 | // Use the bundled Jetbrains Runtime 32 | try { 33 | javaParameters.jdk = 34 | JdkUtil.getCompatibleJetbrainsJdk(module) 35 | ?: throw IllegalArgumentException("The bundled JBR is not compatible with the project JDK") 36 | } catch (e: BrokenJbrException) { 37 | throw IllegalArgumentException("The bundled JBR is known to be broken") 38 | } 39 | } 40 | val agentInHome = VaadinHomeUtil.getHotSwapAgentJar() 41 | 42 | val paramsList = javaParameters.vmParametersList 43 | 44 | val addOpens = "--add-opens" 45 | paramsList.add(addOpens) 46 | paramsList.add("java.base/sun.nio.ch=ALL-UNNAMED") 47 | paramsList.add(addOpens) 48 | paramsList.add("java.base/java.lang=ALL-UNNAMED") 49 | paramsList.add(addOpens) 50 | paramsList.add("java.base/java.lang.reflect=ALL-UNNAMED") 51 | paramsList.add(addOpens) 52 | paramsList.add("java.base/java.io=ALL-UNNAMED") 53 | paramsList.add(addOpens) 54 | paramsList.add("java.base/sun.security.action=ALL-UNNAMED") 55 | paramsList.add(addOpens) 56 | paramsList.add("java.desktop/java.beans=ALL-UNNAMED") 57 | paramsList.add(addOpens) 58 | paramsList.add("java.desktop/com.sun.beans=ALL-UNNAMED") 59 | paramsList.add(addOpens) 60 | paramsList.add("java.desktop/com.sun.beans.introspect=ALL-UNNAMED") 61 | paramsList.add(addOpens) 62 | paramsList.add("java.desktop/com.sun.beans.util=ALL-UNNAMED") 63 | paramsList.add(addOpens) 64 | paramsList.add("java.base/jdk.internal.loader=ALL-UNNAMED") 65 | 66 | paramsList.add("-XX:+AllowEnhancedClassRedefinition") 67 | paramsList.add("-XX:+ClassUnloading") 68 | 69 | paramsList.add("-javaagent:$agentInHome") 70 | } 71 | 72 | private fun turnOffFrameDeactivationPolicy(runProfile: JavaRunConfigurationBase) { 73 | try { 74 | val getOptions = runProfile.javaClass.getDeclaredMethod("getOptions") 75 | getOptions.trySetAccessible() 76 | val options = getOptions.invoke(runProfile) 77 | val policy = options.javaClass.getDeclaredField("frameDeactivationUpdatePolicy\$delegate") 78 | policy.trySetAccessible() 79 | 80 | val prop = policy.get(options) 81 | 82 | prop.javaClass.getMethod("parseAndSetValue", String::class.java).invoke(prop, null) 83 | } catch (e: Exception) { 84 | LOG.debug("Failed to turn off frame deactivation policy", e) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/hotswapagent/HotswapAgentRunner.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.hotswapagent 2 | 3 | import com.intellij.debugger.impl.GenericDebuggerRunner 4 | import com.intellij.execution.JavaRunConfigurationBase 5 | import com.intellij.execution.configurations.JavaCommandLine 6 | import com.intellij.execution.configurations.RunProfile 7 | import com.intellij.execution.runners.ExecutionEnvironment 8 | import com.intellij.notification.Notification 9 | import com.intellij.notification.NotificationAction 10 | import com.intellij.notification.NotificationType 11 | import com.intellij.notification.Notifications 12 | import com.intellij.openapi.application.ApplicationManager 13 | import com.intellij.openapi.application.invokeLater 14 | import com.vaadin.plugin.copilot.CopilotPluginUtil.Companion.NOTIFICATION_GROUP 15 | import com.vaadin.plugin.utils.trackDebugWithHotswap 16 | 17 | class HotswapAgentRunner : GenericDebuggerRunner() { 18 | 19 | override fun getRunnerId(): String { 20 | return "vaadin-hotswapagent-runner" 21 | } 22 | 23 | override fun canRun(executorId: String, profile: RunProfile): Boolean { 24 | return executorId.equals(HotswapAgentExecutor.ID) 25 | } 26 | 27 | override fun execute(environment: ExecutionEnvironment) { 28 | ApplicationManager.getApplication().executeOnPooledThread { 29 | if (environment.runProfile !is JavaRunConfigurationBase) { 30 | Notification( 31 | NOTIFICATION_GROUP, 32 | "To launch, open the Spring Boot application class and press \"Debug using Hotswap Agent\". " + 33 | "Do not launch through Maven or Gradle.", 34 | NotificationType.WARNING) 35 | .setTitle("Only Spring Boot applications are supported") 36 | .notify(environment.project) 37 | return@executeOnPooledThread 38 | } 39 | 40 | val runProfile = environment.runProfile as JavaRunConfigurationBase 41 | val javaCommandLine = 42 | environment.state as? JavaCommandLine 43 | ?: throw IllegalStateException("$runnerId needs a JavaCommandLine") 44 | val module = 45 | runProfile.configurationModule?.module ?: throw IllegalStateException("$runnerId needs a module") 46 | 47 | val javaParameters = javaCommandLine.javaParameters 48 | try { 49 | val jdkOk = 50 | JdkUtil.isJetbrainsRuntime(javaParameters.jdk) || JdkUtil.getCompatibleJetbrainsJdk(module) != null 51 | 52 | if (jdkOk) { 53 | trackDebugWithHotswap() 54 | invokeLater { super.execute(environment) } 55 | } else { 56 | invokeLater { 57 | val action = 58 | NotificationAction.create("Setup JetBrains Runtime...") { event, notification -> 59 | JdkUtil.createSdkPopupBuilder(environment.project) 60 | .onSdkSelected({ _ -> notification.hideBalloon() }) 61 | .buildPopup() 62 | .showPopup(event) 63 | } 64 | Notifications.Bus.notify( 65 | Notification( 66 | NOTIFICATION_GROUP, 67 | "Current SDK is not a JetBrains Runtime. Set up JetBrains Runtime to enable Debug with HotSwap.", 68 | NotificationType.WARNING) 69 | .addAction(action), 70 | environment.project, 71 | ) 72 | } 73 | } 74 | } catch (_: BrokenJbrException) { 75 | invokeLater { BadJBRFoundDialog().show() } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.github/workflows/validation.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: "${{ github.workflow }}-${{ github.ref }}" 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | 12 | codeChecks: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | cache: gradle 22 | - name: Check Spotless 23 | run: ./gradlew spotlessCheck 24 | - name: Run Unit Tests 25 | run: ./gradlew test 26 | 27 | buildPlugin: 28 | runs-on: ubuntu-latest 29 | needs: codeChecks 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up JDK 21 33 | uses: actions/setup-java@v4 34 | with: 35 | java-version: '21' 36 | distribution: 'temurin' 37 | cache: gradle 38 | - name: Build plugin 39 | run: ./gradlew --no-configuration-cache buildPlugin 40 | - uses: actions/upload-artifact@v4.3.3 41 | id: artifact 42 | with: 43 | name: distributions 44 | path: plugin/build/distributions 45 | - uses: mshick/add-pr-comment@v2 46 | with: 47 | message-id: download-artifact-comment 48 | refresh-message-position: true 49 | message: | 50 | Artifact build on last commit: [distributions.zip](${{ steps.artifact.outputs.artifact-url }}). 51 | For MacOS users: there is a zip inside this zip and Finder unzips them both at once. Use `unzip distributions.zip` from Terminal or [check solution for Archive Manager](https://apple.stackexchange.com/questions/443607/is-there-a-way-to-prevent-macoss-archive-utility-from-unarchiving-inner-zip-fil). 52 | 53 | runPluginVerifier: 54 | runs-on: macos-latest 55 | needs: codeChecks 56 | strategy: 57 | fail-fast: false 58 | matrix: 59 | version: [ "253.28294.169" ] # Use EAP for 2025.3 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Set up JDK 21 63 | uses: actions/setup-java@v4 64 | with: 65 | java-version: '21' 66 | distribution: 'temurin' 67 | cache: gradle 68 | - name: Verify plugin 69 | run: ./gradlew --no-configuration-cache -PverifyVersion=${{ matrix.version }} verifyPlugin 70 | - uses: actions/upload-artifact@v4 71 | with: 72 | name: pluginVerifierReports-${{ matrix.version }} 73 | path: plugin/build/reports/pluginVerifier 74 | 75 | waitForResults: 76 | runs-on: ubuntu-latest 77 | needs: runPluginVerifier 78 | steps: 79 | - uses: actions/download-artifact@v4 80 | - name: Get result directories 81 | run: | 82 | DIRECTORIES=$(find . -name 'I*' | jq -R -s -c 'split("\n")[:-1]') 83 | echo "DIRECTORIES=$DIRECTORIES" >> $GITHUB_ENV 84 | outputs: 85 | directories: ${{ env.DIRECTORIES }} 86 | 87 | addComments: 88 | runs-on: ubuntu-latest 89 | needs: waitForResults 90 | strategy: 91 | fail-fast: false 92 | matrix: 93 | dir: ${{fromJson(needs.waitForResults.outputs.directories)}} 94 | steps: 95 | - uses: actions/download-artifact@v4 96 | - run: | 97 | cat ${{ matrix.dir }}/report.md | head -n 3 | sed s/#/###/ > ${{ matrix.dir }}/report.short.md 98 | - name: Publish report 99 | uses: mshick/add-pr-comment@v2 100 | with: 101 | message-id: verify-plugin-comment-${{ matrix.dir }} 102 | refresh-message-position: true 103 | message-path: | 104 | ${{ matrix.dir }}/report.short.md 105 | - name: Fail on not compatible 106 | run: | 107 | if grep -q "Compatible." ${{ matrix.dir }}/report.short.md; then 108 | echo "✅ Compatible." 109 | else 110 | echo "❌ Not compatible!" 111 | exit 1 112 | fi 113 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/copilot/handler/WriteFileHandler.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.copilot.handler 2 | 3 | import com.intellij.openapi.application.runInEdt 4 | import com.intellij.openapi.command.CommandProcessor 5 | import com.intellij.openapi.command.UndoConfirmationPolicy 6 | import com.intellij.openapi.command.WriteCommandAction 7 | import com.intellij.openapi.editor.Document 8 | import com.intellij.openapi.editor.actionSystem.DocCommandGroupId 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.util.text.Strings 11 | import com.intellij.openapi.vfs.ReadonlyStatusHandler 12 | import com.intellij.openapi.vfs.VfsUtil 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.openapi.vfs.findDocument 15 | import com.vaadin.plugin.utils.IdeUtil 16 | import io.netty.handler.codec.http.HttpResponseStatus 17 | import java.io.File 18 | 19 | open class WriteFileHandler(project: Project, data: Map) : AbstractHandler(project) { 20 | 21 | private val content: String = data["content"] as String 22 | private val undoLabel: String? = data["undoLabel"] as String? 23 | private val ioFile: File = File(data["file"] as String) 24 | 25 | override fun run(): HandlerResponse { 26 | var response = RESPONSE_OK 27 | if (isFileInsideProject(project, ioFile)) { 28 | // file exists, write content 29 | val vfsFile = VfsUtil.findFileByIoFile(ioFile, true) 30 | if (vfsFile?.exists() == true) { 31 | runInEdt { 32 | if (ReadonlyStatusHandler.ensureFilesWritable(project, vfsFile)) { 33 | writeAndFlush(vfsFile) 34 | } else { 35 | LOG.warn("File $ioFile is not writable") 36 | } 37 | } 38 | } else { 39 | // file does not exist, create new one 40 | LOG.info("File $ioFile does not exist, creating new file") 41 | create() 42 | if (IdeUtil.willVcsPopupBeShown(project)) { 43 | IdeUtil.bringToFront(project) 44 | response = HandlerResponse(HttpResponseStatus.OK, mapOf("blockingPopup" to "true")) 45 | } 46 | } 47 | 48 | return response 49 | } else { 50 | LOG.warn("File $ioFile is not a part of a project") 51 | return RESPONSE_BAD_REQUEST 52 | } 53 | } 54 | 55 | private fun writeAndFlush(vfsFile: VirtualFile) { 56 | vfsFile.findDocument()?.let { 57 | CommandProcessor.getInstance() 58 | .executeCommand( 59 | project, 60 | { 61 | WriteCommandAction.runWriteCommandAction(project) { 62 | doWrite(vfsFile, it, content) 63 | postSave(vfsFile) 64 | } 65 | }, 66 | undoLabel ?: "Vaadin Copilot Write File", 67 | DocCommandGroupId.noneGroupId(it), 68 | UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, 69 | ) 70 | } 71 | } 72 | 73 | private fun create() { 74 | runInEdt { 75 | CommandProcessor.getInstance() 76 | .executeCommand( 77 | project, 78 | { 79 | WriteCommandAction.runWriteCommandAction(project) { 80 | val vfsFile = doCreate(ioFile, content) 81 | postSave(vfsFile) 82 | } 83 | }, 84 | undoLabel ?: "Vaadin Copilot Write File", 85 | null, 86 | UndoConfirmationPolicy.DO_NOT_REQUEST_CONFIRMATION, 87 | ) 88 | } 89 | } 90 | 91 | open fun doCreate(file: File, content: String): VirtualFile { 92 | val parent = VfsUtil.createDirectories(file.parent) 93 | val vfsFile = parent.createChildData(this, file.name) 94 | doWrite(vfsFile, vfsFile.findDocument(), content) 95 | return vfsFile 96 | } 97 | 98 | open fun doWrite(vfsFile: VirtualFile?, doc: Document?, content: String) { 99 | doc?.setText(Strings.convertLineSeparators(content)) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/module/HelloWorldPanel.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.module 2 | 3 | import com.intellij.openapi.observable.properties.GraphProperty 4 | import com.intellij.openapi.observable.properties.PropertyGraph 5 | import com.intellij.openapi.ui.DialogPanel 6 | import com.intellij.ui.dsl.builder.Row 7 | import com.intellij.ui.dsl.builder.SegmentedButton 8 | import com.intellij.ui.dsl.builder.bindText 9 | import com.intellij.ui.dsl.builder.panel 10 | import com.vaadin.plugin.starter.HelloWorldModel 11 | import com.vaadin.plugin.starter.StarterSupport 12 | 13 | class HelloWorldPanel(private val model: HelloWorldModel) { 14 | 15 | private class SegmentModel( 16 | val values: LinkedHashMap, 17 | val property: GraphProperty, 18 | val supported: ((HelloWorldModel, String) -> Boolean) 19 | ) { 20 | var component: SegmentedButton? = null 21 | 22 | fun reset() { 23 | property.set(values.keys.first()) 24 | } 25 | 26 | fun value(): String { 27 | return property.get() 28 | } 29 | 30 | fun label(): String { 31 | return values[value()]!! 32 | } 33 | 34 | fun label(key: String): String { 35 | return values[key]!! 36 | } 37 | 38 | fun update() { 39 | component?.let { values.forEach { v -> it.update(v.key) } } 40 | } 41 | } 42 | 43 | private class ViewModel(val model: HelloWorldModel) { 44 | val language = SegmentModel(StarterSupport.languages, model.languageProperty, { _, _ -> true }) 45 | val buildTool = 46 | SegmentModel(StarterSupport.buildTools, model.buildToolProperty, StarterSupport::isSupportedBuildTool) 47 | val architecture = 48 | SegmentModel( 49 | StarterSupport.architectures, model.architectureProperty, StarterSupport::isSupportedArchitecture) 50 | 51 | fun all(): List { 52 | return listOf(language, buildTool, architecture) 53 | } 54 | 55 | fun isSupported(segmentModel: SegmentModel, value: String): Boolean { 56 | return segmentModel.supported.let { it(model, value) } ?: true 57 | } 58 | } 59 | 60 | private val viewModel: ViewModel = ViewModel(model) 61 | 62 | private val graph: PropertyGraph = PropertyGraph() 63 | private val kotlinInfoVisibleProperty = graph.property(false) 64 | private val notAllArchitecturesVisibleProperty = graph.property(false) 65 | private val notAllArchitecturesSupportedMessage = graph.property("") 66 | 67 | init { 68 | viewModel.language.property.afterChange { refreshSegments("language") } 69 | viewModel.buildTool.property.afterChange { refreshSegments("buildTool") } 70 | viewModel.architecture.property.afterChange { refreshSegments("architecture") } 71 | notAllArchitecturesSupportedMessage.afterChange { notAllArchitecturesVisibleProperty.set(it != "") } 72 | } 73 | 74 | private fun buildSegment(row: Row, segmentModel: SegmentModel) { 75 | segmentModel.component = 76 | row.segmentedButton(segmentModel.values.keys) { 77 | this.text = segmentModel.label(it) 78 | this.enabled = viewModel.isSupported(segmentModel, it) 79 | } 80 | .bind(segmentModel.property) 81 | } 82 | 83 | val root: DialogPanel = panel { 84 | buildSegment(row("Language") {}, viewModel.language) 85 | row("") { text("Kotlin support uses a community add-on.") }.visibleIf(kotlinInfoVisibleProperty) 86 | buildSegment(row("Build tool") {}, viewModel.buildTool) 87 | buildSegment(row("Architecture") {}, viewModel.architecture) 88 | row("") { text("").bindText(notAllArchitecturesSupportedMessage) }.visibleIf(notAllArchitecturesVisibleProperty) 89 | } 90 | 91 | private fun refreshSegments(source: String) { 92 | if (source != "language") { 93 | viewModel.language.update() 94 | } 95 | if (source != "buildTool") { 96 | viewModel.buildTool.update() 97 | } 98 | if (source != "architecture") { 99 | viewModel.architecture.update() 100 | } 101 | refreshKotlinInfo() 102 | fallbackToFirstEnabled() 103 | } 104 | 105 | private fun refreshKotlinInfo() { 106 | kotlinInfoVisibleProperty.set(viewModel.language.value() == "kotlin") 107 | } 108 | 109 | private fun fallbackToFirstEnabled() { 110 | viewModel.all().filter { !viewModel.isSupported(it, it.value()) }.forEach { it.reset() } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/actions/VaadinCompileOnSaveAction.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.actions 2 | 3 | import com.intellij.debugger.DebuggerManagerEx 4 | import com.intellij.debugger.ui.HotSwapUI 5 | import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener 6 | import com.intellij.openapi.application.ReadAction 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.editor.Document 9 | import com.intellij.openapi.fileEditor.FileDocumentManager 10 | import com.intellij.openapi.progress.ProgressIndicator 11 | import com.intellij.openapi.progress.ProgressManager 12 | import com.intellij.openapi.progress.Task 13 | import com.intellij.openapi.project.Project 14 | import com.intellij.openapi.roots.ProjectFileIndex 15 | import com.intellij.openapi.vfs.VirtualFile 16 | import com.intellij.task.ProjectTaskListener 17 | import com.intellij.task.ProjectTaskManager 18 | import com.vaadin.plugin.copilot.CopilotPluginUtil 19 | import java.util.concurrent.Executors 20 | import java.util.concurrent.Semaphore 21 | 22 | /** Action run after Document has been saved. Is not run for binary files. */ 23 | class VaadinCompileOnSaveAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { 24 | 25 | private val LOG: Logger = Logger.getInstance(CopilotPluginUtil::class.java) 26 | private var compileLock = Semaphore(1) 27 | 28 | override fun isEnabledForProject(project: Project): Boolean { 29 | return VaadinCompileOnSaveActionInfo.isEnabledForProject(project) 30 | } 31 | 32 | override fun processDocuments(project: Project, documents: Array) { 33 | val task = 34 | object : Task.Backgroundable(project, "Vaadin: compiling...") { 35 | override fun run(indicator: ProgressIndicator) { 36 | 37 | LOG.info("Processing ${documents.size} document(s)") 38 | 39 | val fileIndex = ProjectFileIndex.getInstance(project) 40 | val vfsFiles = 41 | ReadAction.compute, Exception> { 42 | documents 43 | .mapNotNull { FileDocumentManager.getInstance().getFile(it) } 44 | .filter { fileIndex.isInSourceContent(it) } 45 | } 46 | 47 | if (vfsFiles.isEmpty()) { 48 | return 49 | } 50 | 51 | val javaFiles = vfsFiles.filter { it.extension == "java" } 52 | if (javaFiles.isNotEmpty()) { 53 | val session = DebuggerManagerEx.getInstanceEx(project).context.debuggerSession 54 | if (session != null) { 55 | val executorService = Executors.newSingleThreadExecutor() 56 | executorService.submit { 57 | // Wait for compile lock so only one compile task is running at a 58 | // time 59 | val timeout: Long = 30 60 | if (!compileLock.tryAcquire(timeout, java.util.concurrent.TimeUnit.SECONDS)) { 61 | // We did not get the compile lock. Assume something is wrong 62 | // and create a new lock so we won't block all further compiles 63 | LOG.warn( 64 | "Unable to acquire the compile lock in $timeout seconds. Creating a new lock and proceeding.") 65 | compileLock = Semaphore(1) 66 | compileLock.acquire() 67 | } 68 | 69 | val thisLock = compileLock 70 | 71 | LOG.debug("Compile starting for $javaFiles") 72 | 73 | val messageBusConnection = getProject().messageBus.connect() 74 | val listener = 75 | object : ProjectTaskListener { 76 | override fun finished(result: ProjectTaskManager.Result) { 77 | LOG.debug("Compile completed for $javaFiles") 78 | thisLock.release() 79 | messageBusConnection.disconnect() 80 | } 81 | } 82 | 83 | messageBusConnection.subscribe(ProjectTaskListener.TOPIC, listener) 84 | 85 | ReadAction.run { 86 | HotSwapUI.getInstance(project).compileAndReload(session, *javaFiles.toTypedArray()) 87 | } 88 | } 89 | executorService.shutdown() 90 | } 91 | return 92 | } 93 | 94 | val nonJavaFiles = vfsFiles.filter { it.extension != "java" } 95 | if (nonJavaFiles.isNotEmpty()) { 96 | ProjectTaskManager.getInstance(project).compile(*nonJavaFiles.toTypedArray()) 97 | } 98 | } 99 | } 100 | ProgressManager.getInstance().run(task) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /plugin-api-test-client/src/main/java/com/vaadin/plugin/Client.java: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.web.client.RestClient; 8 | 9 | import java.io.IOException; 10 | import java.net.URI; 11 | import java.net.http.HttpClient; 12 | import java.net.http.HttpRequest; 13 | import java.net.http.HttpResponse; 14 | import java.nio.file.Path; 15 | import java.util.Collections; 16 | import java.util.Optional; 17 | 18 | public class Client { 19 | 20 | private final String endpoint; 21 | 22 | private final String projectBasePath; 23 | 24 | private final ObjectMapper objectMapper = new ObjectMapper(); 25 | 26 | public Client(String endpoint, String projectBasePath) { 27 | this.endpoint = endpoint; 28 | this.projectBasePath = projectBasePath; 29 | } 30 | 31 | public RestClient.ResponseSpec undo(Path path) throws IOException { 32 | return send("undo", new Message.UndoRedoMessage(Collections.singletonList(path.toString()))); 33 | } 34 | 35 | public RestClient.ResponseSpec redo(Path path) throws IOException { 36 | return send("redo", new Message.UndoRedoMessage(Collections.singletonList(path.toString()))); 37 | } 38 | 39 | public RestClient.ResponseSpec write(Path path, String content) throws IOException { 40 | return send("write", new Message.WriteFileMessage(path.toString(), null, content)); 41 | } 42 | 43 | public RestClient.ResponseSpec restartApplication() throws IOException { 44 | return send("restartApplication", new Message.RestartApplicationMessage()); 45 | } 46 | 47 | public RestClient.ResponseSpec writeBinary(Path path, String content) throws IOException { 48 | return send("writeBase64", new Message.WriteFileMessage(path.toString(), null, content)); 49 | } 50 | 51 | public RestClient.ResponseSpec showInIde(Path path) throws IOException { 52 | return send("showInIde", new Message.ShowInIdeMessage(path.toString(), -10, -2)); 53 | } 54 | 55 | public RestClient.ResponseSpec refresh() throws IOException { 56 | return send("refresh", new Message.RefreshMessage()); 57 | } 58 | 59 | public RestClient.ResponseSpec delete(Path path) throws IOException { 60 | return send("delete", new Message.DeleteMessage(path.toString())); 61 | } 62 | 63 | public Optional getVaadinRoutes() throws IOException { 64 | return sendRestSync("getVaadinRoutes", new Message.GetVaadinRoutesMessage()); 65 | } 66 | 67 | public Optional getVaadinVersion() throws IOException { 68 | return sendRestSync("getVaadinVersion", new Message.GetVaadinVersionMessage()); 69 | } 70 | 71 | public Optional getVaadinComponents(boolean includeMethods) throws IOException { 72 | return sendRestSync("getVaadinComponents", new Message.GetVaadinComponentsMessage(includeMethods)); 73 | } 74 | 75 | public Optional getVaadinEntities(boolean includeMethods) throws IOException { 76 | return sendRestSync("getVaadinEntities", new Message.GetVaadinPersistenceMessage(includeMethods)); 77 | } 78 | 79 | public Optional getVaadinSecurity() throws IOException { 80 | return sendRestSync("getVaadinSecurity", new Message.GetVaadinSecurityMessage()); 81 | } 82 | 83 | private RestClient.ResponseSpec send(String command, Object data) throws JsonProcessingException { 84 | Message.CopilotRestRequest message = new Message.CopilotRestRequest(command, projectBasePath, data); 85 | String body = new ObjectMapper().writeValueAsString(message); 86 | org.springframework.web.client.RestClient.ResponseSpec response = org.springframework.web.client.RestClient.create().post() 87 | .uri(endpoint).contentType(MediaType.APPLICATION_JSON) 88 | .body(body).retrieve(); 89 | return response; 90 | } 91 | 92 | // rest client 93 | private Optional sendRestSync(String command, Object dataCommand) { 94 | try { 95 | Message.CopilotRestRequest message = new Message.CopilotRestRequest(command, projectBasePath, dataCommand); 96 | byte[] data = objectMapper.writeValueAsBytes(message); 97 | HttpRequest request = HttpRequest.newBuilder().uri(URI.create(endpoint)) 98 | .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofByteArray(data)) 99 | .build(); 100 | HttpResponse response = HttpClient.newHttpClient().send(request, 101 | HttpResponse.BodyHandlers.ofString()); 102 | if (response.statusCode() != 200) { 103 | throw new IOException("Unexpected response (" + response.statusCode() 104 | + ") communicating with the IDE plugin: " + response.body()); 105 | } 106 | if (response.body() != null && !response.body().isEmpty()) { 107 | JsonNode responseJson = objectMapper.readTree(response.body()); 108 | return Optional.of(responseJson); 109 | } 110 | } catch (IOException e) { 111 | e.printStackTrace(); 112 | } catch (InterruptedException e) { 113 | Thread.currentThread().interrupt(); 114 | } 115 | 116 | return Optional.empty(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/vaadin/plugin/utils/DownloadUtil.kt: -------------------------------------------------------------------------------- 1 | package com.vaadin.plugin.utils 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.util.Pair 6 | import com.intellij.openapi.util.io.FileUtil 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import com.intellij.util.download.DownloadableFileDescription 9 | import com.intellij.util.download.DownloadableFileService 10 | import com.intellij.util.net.HttpConfigurable 11 | import java.io.File 12 | import java.io.FileInputStream 13 | import java.io.FileOutputStream 14 | import java.io.IOException 15 | import java.net.InetSocketAddress 16 | import java.net.PasswordAuthentication 17 | import java.net.Proxy 18 | import java.net.URL 19 | import java.nio.file.Path 20 | import java.util.concurrent.CompletableFuture 21 | import java.util.zip.ZipFile 22 | import org.apache.commons.compress.archivers.tar.TarArchiveInputStream 23 | import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream 24 | 25 | object DownloadUtil { 26 | 27 | private val LOG: Logger = Logger.getInstance(DownloadUtil::class.java) 28 | 29 | /** 30 | * Open connection to given url using IDE proxy if configured 31 | * 32 | * @param urlStr resource URL 33 | * @return content as String 34 | */ 35 | fun openUrlWithProxy(urlStr: String): String { 36 | val config = HttpConfigurable.getInstance() 37 | val url = URL(urlStr) 38 | 39 | val connection = 40 | if (config.USE_HTTP_PROXY && !config.isProxyException(url.toURI())) { 41 | val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(config.PROXY_HOST, config.PROXY_PORT)) 42 | 43 | if (config.PROXY_AUTHENTICATION) { 44 | java.net.Authenticator.setDefault( 45 | object : java.net.Authenticator() { 46 | override fun getPasswordAuthentication(): PasswordAuthentication { 47 | return PasswordAuthentication( 48 | config.proxyLogin, config.plainProxyPassword?.toCharArray()) 49 | } 50 | }) 51 | } 52 | 53 | url.openConnection(proxy) 54 | } else { 55 | url.openConnection() 56 | } 57 | 58 | return connection.getInputStream().bufferedReader().use { it.readText() } 59 | } 60 | 61 | /** 62 | * Downloads resource from given url in background with progress indicator 63 | * 64 | * @param project current project 65 | * @param url resource url 66 | * @param targetFile download target file 67 | * @param downloaderLabel label for download indicator progress widget 68 | * @return list of virtual files and descriptors as completable future 69 | */ 70 | fun download( 71 | project: Project, 72 | url: String, 73 | targetFile: Path, 74 | downloaderLabel: String 75 | ): CompletableFuture?>?> { 76 | LOG.info("Downloading $url") 77 | val description = 78 | DownloadableFileService.getInstance().createFileDescription(url, targetFile.fileName.toString()) 79 | val downloader = DownloadableFileService.getInstance().createDownloader(listOf(description), downloaderLabel) 80 | return downloader.downloadWithBackgroundProgress(targetFile.parent.toString(), project) 81 | } 82 | 83 | /** 84 | * Extracts tar gz archive 85 | * 86 | * @param tarGzPath archive file 87 | * @param outputDirPath output dir 88 | */ 89 | fun extractTarGz(tarGzPath: File, outputDirPath: File) { 90 | val buffer = ByteArray(4096) 91 | val input = TarArchiveInputStream(GzipCompressorInputStream(FileInputStream(tarGzPath))) 92 | 93 | var entry = input.nextEntry 94 | while (entry != null) { 95 | val outFile = outputDirPath.resolve(entry.name) 96 | if (entry.isDirectory) { 97 | FileUtil.createDirectory(outFile) 98 | } else { 99 | FileUtil.createDirectory(outFile.parentFile) 100 | FileOutputStream(outFile).use { out -> 101 | var len: Int 102 | while (input.read(buffer).also { len = it } != -1) { 103 | out.write(buffer, 0, len) 104 | } 105 | } 106 | } 107 | entry = input.nextEntry 108 | } 109 | input.close() 110 | } 111 | 112 | /** 113 | * Finds root directory in zip archive if present 114 | * 115 | * @param zip input file 116 | * @return root directory name if present 117 | */ 118 | @Throws(IOException::class) 119 | fun getZipRootFolder(zip: File): String? { 120 | ZipFile(zip).use { zipFile -> 121 | val en = zipFile.entries() 122 | while (en.hasMoreElements()) { 123 | val zipEntry = en.nextElement() 124 | // we do not necessarily get a separate entry for the subdirectory when the file 125 | // in the ZIP archive is placed in a subdirectory, so we need to check if the 126 | // slash 127 | // is found anywhere in the path 128 | val indexOf = zipEntry.name.indexOf('/') 129 | if (indexOf >= 0) { 130 | return zipEntry.name.substring(0, indexOf) 131 | } 132 | } 133 | return null 134 | } 135 | } 136 | } 137 | --------------------------------------------------------------------------------