├── CONTRIBUTION-GUIDE.md ├── plugin-test ├── maven │ ├── multi-module │ │ ├── child1 │ │ │ ├── test.yaml │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ └── resources │ │ │ │ │ │ └── test.yaml │ │ │ │ └── test │ │ │ │ │ └── resources │ │ │ │ │ └── test.yml │ │ │ ├── build.gradle │ │ │ └── pom.xml │ │ ├── child2 │ │ │ ├── test.yaml │ │ │ ├── src │ │ │ │ ├── test │ │ │ │ │ └── java │ │ │ │ │ │ └── test.yaml │ │ │ │ └── main │ │ │ │ │ ├── java │ │ │ │ │ └── model │ │ │ │ │ │ ├── TopLevelEnum.java │ │ │ │ │ │ ├── ChildNode.java │ │ │ │ │ │ ├── RootNode2.java │ │ │ │ │ │ └── RootNode1.java │ │ │ │ │ └── resources │ │ │ │ │ ├── test.yaml │ │ │ │ │ └── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ └── pom.xml │ │ ├── src │ │ │ └── main │ │ │ │ └── resources │ │ │ │ ├── application.properties │ │ │ │ └── test.yml │ │ ├── .mvn │ │ │ └── wrapper │ │ │ │ ├── maven-wrapper.properties │ │ │ │ └── maven-wrapper.jar │ │ ├── .gitignore │ │ └── pom.xml │ ├── single-module │ │ ├── src │ │ │ ├── main │ │ │ │ ├── resources │ │ │ │ │ ├── application.properties │ │ │ │ │ └── test.yml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── singlemodule │ │ │ │ │ └── SingleModuleApplication.java │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── singlemodule │ │ │ │ └── SingleModuleApplicationTests.java │ │ ├── .mvn │ │ │ └── wrapper │ │ │ │ ├── maven-wrapper.properties │ │ │ │ └── maven-wrapper.jar │ │ ├── .gitignore │ │ └── pom.xml │ ├── spring-boot-example │ │ ├── demo-app │ │ │ ├── src │ │ │ │ └── main │ │ │ │ │ ├── resources │ │ │ │ │ ├── banner.txt │ │ │ │ │ ├── my.properties │ │ │ │ │ ├── db │ │ │ │ │ │ └── change-log │ │ │ │ │ │ │ └── master.yaml │ │ │ │ │ ├── abc.yml │ │ │ │ │ ├── application.properties │ │ │ │ │ ├── application-negative-cases.yaml │ │ │ │ │ └── application.yaml │ │ │ │ │ └── java │ │ │ │ │ └── dev │ │ │ │ │ └── flikas │ │ │ │ │ └── MyApp.java │ │ │ └── pom.xml │ │ ├── demo-lib │ │ │ ├── src │ │ │ │ └── main │ │ │ │ │ ├── java │ │ │ │ │ └── dev │ │ │ │ │ │ └── flikas │ │ │ │ │ │ ├── LombokPojo.java │ │ │ │ │ │ └── MyProperties.java │ │ │ │ │ └── resources │ │ │ │ │ └── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ └── pom.xml │ │ └── pom.xml │ └── spring-boot-example-2 │ │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── dev │ │ │ │ └── flikas │ │ │ │ └── demo │ │ │ │ └── DemoApp.java │ │ │ └── resources │ │ │ └── application.yaml │ │ └── pom.xml └── gradle │ ├── multi-module │ ├── module1-spring-only │ │ ├── test.yaml │ │ ├── src │ │ │ ├── main │ │ │ │ └── resources │ │ │ │ │ ├── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ │ │ └── application.yaml │ │ │ └── test │ │ │ │ └── resources │ │ │ │ └── application.yml │ │ └── build.gradle │ ├── module2-additional-metadata │ │ ├── test.yaml │ │ ├── src │ │ │ ├── main │ │ │ │ ├── java │ │ │ │ │ └── model │ │ │ │ │ │ ├── TopLevelEnum.java │ │ │ │ │ │ ├── ChildNode.java │ │ │ │ │ │ ├── RootNode2.java │ │ │ │ │ │ └── RootNode1.java │ │ │ │ └── resources │ │ │ │ │ ├── application.yaml │ │ │ │ │ └── META-INF │ │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ └── test │ │ │ │ └── java │ │ │ │ └── application-test.yaml │ │ └── build.gradle │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── resources │ │ │ │ ├── spring-banner.txt │ │ │ │ └── application.yaml │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── acme │ │ │ │ └── Main.java │ │ └── build.gradle.kts │ ├── buildSrc │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ └── groovy │ │ │ └── multi-project.conventions.gradle │ ├── src │ │ └── main │ │ │ └── resources │ │ │ └── application.yaml │ ├── settings.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── build.gradle │ └── gradlew.bat │ └── single-project │ ├── src │ └── main │ │ ├── resources │ │ ├── customProperties.yml │ │ ├── application-negative-cases.yaml │ │ └── application.yaml │ │ └── java │ │ └── com │ │ └── acme │ │ └── model │ │ ├── TopLevelEnum.java │ │ ├── ChildNode.java │ │ ├── DynamicEnum.java │ │ ├── DynamicChild.java │ │ ├── RootNode2.java │ │ ├── RootNode1.java │ │ └── DynamicRoot.java │ ├── settings.gradle │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── build.gradle │ └── gradlew.bat ├── lombok.config ├── plugin-docs └── demo-completion.gif ├── plugin └── src │ ├── test │ ├── resources │ │ ├── META-INF │ │ │ └── spring-configuration-metadata.json │ │ └── application.yaml │ └── java │ │ └── in │ │ └── oneton │ │ └── idea │ │ └── spring │ │ └── assistant │ │ └── plugin │ │ └── misc │ │ └── GenericUtilTest.java │ └── main │ ├── resources │ ├── icons │ │ ├── boot.png │ │ ├── boot@2x.png │ │ └── springBoot.svg │ ├── inspectionDescriptions │ │ ├── InvalidValue.html │ │ ├── PropertyDeprecated.html │ │ ├── PropertyRemoved.html │ │ └── KeyNotDefined.html │ └── META-INF │ │ └── pluginIcon.svg │ └── java │ ├── icons │ └── Icons.java │ ├── dev │ └── flikas │ │ └── spring │ │ └── boot │ │ └── assistant │ │ └── idea │ │ └── plugin │ │ ├── misc │ │ ├── MutableReference.java │ │ ├── ModuleRootUtils.java │ │ ├── PsiMethodUtils.java │ │ ├── CompositeIconUtils.java │ │ └── PsiElementUtils.java │ │ ├── metadata │ │ ├── index │ │ │ ├── AbstractMetadataSource.java │ │ │ ├── MetadataHint.java │ │ │ ├── MetadataGroup.java │ │ │ ├── MetadataSource.java │ │ │ ├── PsiElementMetadataSource.java │ │ │ ├── ConfigurationMetadataIndex.java │ │ │ ├── MetadataItem.java │ │ │ ├── MetadataProperty.java │ │ │ ├── hint │ │ │ │ ├── provider │ │ │ │ │ ├── ValueProvider.java │ │ │ │ │ ├── AbstractValueProvider.java │ │ │ │ │ └── ClassReferenceValueProvider.java │ │ │ │ ├── value │ │ │ │ │ └── ValueHint.java │ │ │ │ └── Hint.java │ │ │ ├── FileMetadataSource.java │ │ │ ├── MetadataHintImpl.java │ │ │ ├── HomonymProperties.java │ │ │ ├── MetadataIndex.java │ │ │ ├── NameTreeNode.java │ │ │ └── MetadataGroupImpl.java │ │ ├── service │ │ │ ├── ModuleMetadataService.java │ │ │ ├── ProjectMetadataService.java │ │ │ └── MetadataFileContainer.java │ │ └── source │ │ │ └── MetadataFileIndexConfigurator.java │ │ ├── completion │ │ ├── yaml │ │ │ ├── SpringYamlCompletionContributor.java │ │ │ └── YamlCompletionProvider.java │ │ ├── properties │ │ │ ├── SpringPropertiesCompletionContributor.java │ │ │ ├── PropertiesValueInsertHandler.java │ │ │ └── PropertiesCompletionProvider.java │ │ └── SourceContainer.java │ │ ├── navigation │ │ ├── PropertiesToPsiReference.java │ │ ├── forward │ │ │ ├── AbstractReferenceProvider.java │ │ │ ├── YamlToCodeReferenceContributor.java │ │ │ └── PropertiesToCodeReferenceContributor.java │ │ ├── YamlToPsiReference.java │ │ ├── backward │ │ │ └── PsiToSpringPropertyReferenceSearcher.java │ │ ├── SpringPropertyToPsiReference.java │ │ └── SpringPropertyReadWriteAccessDetector.java │ │ ├── inspection │ │ ├── properties │ │ │ └── PropertiesPropertyUsageProvider.java │ │ └── yaml │ │ │ ├── PropertyRemovedInspection.java │ │ │ ├── PropertyDeprecatedInspection.java │ │ │ ├── YamlInspectionBase.java │ │ │ ├── PropertyDeprecatedInspectionBase.java │ │ │ └── KeyNotDefinedInspection.java │ │ ├── filetype │ │ ├── SpringBootConfigurationYamlFileType.java │ │ └── SpringBootConfigurationPropertiesFileType.java │ │ ├── documentation │ │ └── YamlDocumentationProvider.java │ │ └── editing │ │ ├── YamlJoinLinesHandler.java │ │ └── YamlSplitKeyProcessor.java │ └── in │ └── oneton │ └── LICENSE.1ton ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .idea ├── codeStyles │ └── codeStyleConfig.xml └── .gitignore ├── .gitignore ├── .gitattributes ├── RELEASE-STEPS.md ├── ROADMAP.md ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── FUNDING.yml └── workflows │ └── publish-master-to-eap.yml ├── gradle.properties └── gradlew.bat /CONTRIBUTION-GUIDE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child1/test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module1-spring-only/test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child1/src/main/resources/test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/resources/customProperties.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/app/src/main/resources/spring-banner.txt: -------------------------------------------------------------------------------- 1 | Banner -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/my.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'single-project' -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/src/main/resources/test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | devtools: 3 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/src/main/resources/test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | devtools: 3 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/db/change-log/master.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'groovy-gradle-plugin' 3 | } -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | # This file is generated by the 'io.freefair.lombok' Gradle plugin 2 | config.stopBubbling = true 3 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/abc.yml: -------------------------------------------------------------------------------- 1 | sp: 2 | aas: 3 | - bb 4 | - dd 5 | oos 6 | -------------------------------------------------------------------------------- /plugin-docs/demo-completion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-docs/demo-completion.gif -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | devtools: 3 | livereload: 4 | enabled: true 5 | -------------------------------------------------------------------------------- /plugin/src/test/resources/META-INF/spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": [], 3 | "groups": [], 4 | "properties": [] 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/test/java/test.yaml: -------------------------------------------------------------------------------- 1 | autogenerated: 2 | rootnode2: 3 | charlie: hello 4 | custom: 5 | replacement: fresh -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'multi-project' 2 | include 'app', 'module1-spring-only', 'module2-additional-metadata' 3 | -------------------------------------------------------------------------------- /plugin/src/main/resources/icons/boot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin/src/main/resources/icons/boot.png -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/java/model/TopLevelEnum.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | public enum TopLevelEnum { 4 | enum1, enum2, enum3 5 | } 6 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/resources/test.yaml: -------------------------------------------------------------------------------- 1 | autogenerated: 2 | rootnode2: 3 | charlie: hello 4 | custom: 5 | replacement: newval 6 | -------------------------------------------------------------------------------- /plugin/src/main/resources/icons/boot@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin/src/main/resources/icons/boot@2x.png -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/TopLevelEnum.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | public enum TopLevelEnum { 4 | enum1, enum2, enum3 5 | } 6 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip 2 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip 2 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/java/model/TopLevelEnum.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | public enum TopLevelEnum { 4 | enum1, enum2, enum3 5 | } 6 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/test/java/application-test.yaml: -------------------------------------------------------------------------------- 1 | autogenerated: 2 | rootnode2: 3 | charlie: hello 4 | custom: 5 | replacement: fresh 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") 3 | } 4 | rootProject.name = "idea-spring-boot-assistant" 5 | include("plugin") -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/maven/multi-module/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /plugin-test/maven/single-module/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/maven/single-module/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/gradle/multi-module/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/gradle/single-project/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/java/model/ChildNode.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ChildNode { 7 | private String childStringProperty; 8 | } 9 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/ChildNode.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ChildNode { 7 | private String childStringProperty; 8 | } 9 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/java/model/ChildNode.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ChildNode { 7 | private String childStringProperty; 8 | } 9 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child1/src/test/resources/test.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | spring: 4 | application: 5 | name: helloworld 6 | test: 7 | mockmvc: 8 | print: SYSTEM_OUT 9 | aop: 10 | auto: false -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-lib/src/main/java/dev/flikas/LombokPojo.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/maven/spring-boot-example/demo-lib/src/main/java/dev/flikas/LombokPojo.java -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-lib/src/main/java/dev/flikas/MyProperties.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flikas/archived-idea-spring-boot-assistant/HEAD/plugin-test/maven/spring-boot-example/demo-lib/src/main/java/dev/flikas/MyProperties.java -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module1-spring-only/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "custom.boolean-value", 5 | "type": "java.lang.Boolean" 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /plugin/src/main/java/icons/Icons.java: -------------------------------------------------------------------------------- 1 | package icons; 2 | 3 | import javax.swing.*; 4 | 5 | import static com.intellij.openapi.util.IconLoader.getIcon; 6 | 7 | public class Icons { 8 | public static final Icon SpringBoot = getIcon("/icons/springBoot.svg", Icons.class); 9 | } 10 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | autogenerated: 2 | rootnode1: 3 | enum-property: val1 4 | rootnode2: 5 | charlie: hello 6 | top-level-enum: enum2 7 | new-property1: false 8 | custom: 9 | replacement: newval 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | target/ 4 | out/ 5 | .project 6 | .classpath 7 | /bootstrap.xml 8 | /font-awesome.xml 9 | *.*~ 10 | **/*.iml 11 | *.ipl 12 | **/*.ipl 13 | *.iwl 14 | **/*.iwl 15 | /chain.crt 16 | /private.pem 17 | /cache 18 | /.intellijPlatform 19 | .idea 20 | !/.idea/codeStyles 21 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module1-spring-only/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | spring: 4 | application: 5 | name: helloworld 6 | test: 7 | mockmvc: 8 | print: SYSTEM_OUT 9 | aop: 10 | auto: false 11 | management.endpoints.web.exposure.include: '*' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/app/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | autogenerated: 2 | rootnode1: 3 | enum-property: val1 4 | rootnode2: 5 | new-property: true 6 | custom: 7 | choice: 8 | nodefault: a 9 | boolean-value: true 10 | spring: 11 | devtools: 12 | livereload: 13 | enabled: true 14 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/DynamicEnum.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | /** 4 | * Enum documentation (from within) 5 | */ 6 | public enum DynamicEnum { 7 | /** 8 | * Val1 documentation 9 | */ 10 | val1, 11 | /** 12 | * Val2 documentation 13 | */ 14 | VAL2 15 | } 16 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | # Binary files should be left untouched 11 | *.jar binary 12 | 13 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module1-spring-only/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | spring: 4 | application: 5 | name: helloworld 6 | test: 7 | mockmvc: 8 | print: SYSTEM_OUT 9 | aop: 10 | auto: false 11 | management.endpoints.web.exposure.include: '*' 12 | custom: 13 | boolean-value: 14 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'multi-project.conventions' 3 | id 'io.freefair.lombok' 4 | } 5 | 6 | dependencies { 7 | implementation "org.springframework.boot:spring-boot-starter" 8 | annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" 9 | } 10 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | # Others 10 | /deployment.xml 11 | /git_toolbox_prj.xml 12 | /misc.xml 13 | /vcs.xml 14 | /encodings.xml 15 | /kotlinScripting.xml 16 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/DynamicChild.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * dynamic child documentation 8 | */ 9 | public class DynamicChild { 10 | /** 11 | * child prop documentation 12 | */ 13 | @Getter 14 | @Setter 15 | private int childProp; 16 | } 17 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/app/src/main/java/com/acme/Main.java: -------------------------------------------------------------------------------- 1 | package com.acme; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Main { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Main.class); 10 | } 11 | } -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /plugin-test/maven/single-module/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example-2/src/main/java/dev/flikas/demo/DemoApp.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApp { 8 | public static void main(String[] args) { 9 | SpringApplication.run(DemoApp.class); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/src/main/java/com/example/singlemodule/SingleModuleApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.singlemodule; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SingleModuleApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(SingleModuleApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RELEASE-STEPS.md: -------------------------------------------------------------------------------- 1 | - Bump version in `gradle.properties` file 2 | - Update `CHANGELOG.md` with release notes 3 | - `./gradlew clean buildPlugin` 4 | - `git add .` will stage all local changes for commit 5 | - `git commit -m ` will commit to local git repo 6 | - `git tag -a -m ""` will create tag with the latest version 7 | - `git push && git push origin ` will push both the code & the tags to GitHub 8 | - `./gradlew publishPlugin` will publish to jetbrains plugin repo 9 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/src/test/java/com/example/singlemodule/SingleModuleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.singlemodule; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class SingleModuleApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module1-spring-only/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'multi-project.conventions' 3 | } 4 | 5 | dependencies { 6 | implementation 'org.springframework.boot:spring-boot-starter' 7 | 8 | annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 9 | // annotationProcessor 'org.projectlombok:lombok' 10 | // testImplementation 'org.springframework.boot:spring-boot-starter-test' 11 | // testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 12 | } 13 | -------------------------------------------------------------------------------- /plugin/src/test/java/in/oneton/idea/spring/assistant/plugin/misc/GenericUtilTest.java: -------------------------------------------------------------------------------- 1 | package in.oneton.idea.spring.assistant.plugin.misc; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | class GenericUtilTest { 6 | 7 | @Test 8 | void updateClassNameAsJavadocHtml() { 9 | StringBuilder s = new StringBuilder(); 10 | GenericUtil.updateClassNameAsJavadocHtml( 11 | s, 12 | "java.util.Map>" 13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-lib/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": [ 3 | { 4 | "name": "oo", 5 | "type": "dev.flikas.LombokPojo", 6 | "sourceType": "dev.flikas.MyApp", 7 | "sourceMethod": "lombokPojo()" 8 | } 9 | ], 10 | "properties": [ 11 | { 12 | "name": "oo.b", 13 | "type": "java.lang.String", 14 | "description": "中文说明", 15 | "sourceType": "dev.flikas.LombokPojo" 16 | } 17 | ], 18 | "hints": [] 19 | } -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/java/model/RootNode2.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("autogenerated.rootnode2") 8 | public class RootNode2 { 9 | /** 10 | * Charlie description 11 | */ 12 | private String charlie; 13 | /** 14 | * Custom description 15 | */ 16 | private int delta; 17 | /** 18 | * Enum description 19 | */ 20 | private TopLevelEnum topLevelEnum; 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/MutableReference.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | @FunctionalInterface 6 | public interface MutableReference { 7 | static MutableReference immutable(R obj) { 8 | return () -> obj; 9 | } 10 | 11 | @Nullable T dereference(); 12 | 13 | /** 14 | * Refresh the inner object, this may cause later {@link #dereference()} returns {@code null}. 15 | */ 16 | default void refresh() {} 17 | } 18 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/RootNode2.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("autogenerated.rootnode2") 8 | public class RootNode2 { 9 | /** 10 | * Charlie fresh comment 11 | */ 12 | private String charlie; 13 | /** 14 | * Delta comment 15 | */ 16 | private int delta; 17 | /** 18 | * Top level enum comment 19 | */ 20 | private TopLevelEnum topLevelEnum; 21 | } 22 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/java/model/RootNode2.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("autogenerated.rootnode2") 8 | public class RootNode2 { 9 | /** 10 | * Charlie description 11 | */ 12 | private String charlie; 13 | /** 14 | * Custom description 15 | */ 16 | private int delta; 17 | /** 18 | * Enum description 19 | */ 20 | private TopLevelEnum topLevelEnum; 21 | 22 | private boolean newProperty; 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/resources/inspectionDescriptions/InvalidValue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Reports invalid values in Spring Boot configuration files. 4 |

5 | The schema is based on 6 | 7 | Spring Boot configuration metadata 8 | . 9 |

10 |

11 | See 12 | 13 | Spring Boot document 14 | for more information. 15 |

16 | 17 | 18 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # ROADMAP 2 | 3 | - Inspection: add `spring-boot-configuration-processor` 4 | - Convert between `*.properties` & `*.yaml` files. 5 | - Support `@PropertySource`, `@TestPropertySource` annotation. 6 | - Support `@Value` annotation. 7 | - Support `@ConditionalOnProperty` annotation. 8 | - Generate metadata for ConditionalOnProperty.(Or leave this to an annotation processor?) 9 | - Quick-fix for deprecated properties. 10 | - Comply with [Trusted Project](https://plugins.jetbrains.com/docs/intellij/trusted-projects.html), eg, while value 11 | validation. 12 | - Go to placeholders. 13 | - More inspection for `.properties` file. 14 | - Documentation for `.properties` file. -------------------------------------------------------------------------------- /plugin/src/main/resources/inspectionDescriptions/PropertyDeprecated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Report deprecated properties in Spring Boot configuration files. 4 |

5 | A deprecated property is deprecated property with "warning" deprecation level, 6 | refer to Spring 8 | Boot Document, 9 | it should still be bound in the environment. 10 |

11 |

12 | A deprecated property is still supported, but it is suggested to be replaced. 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example-2/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | banner: 3 | charset: goog 4 | application: 5 | name: 6 | - abcdef 7 | - xyzdef 8 | profiles: 9 | active: 10 | - abc 11 | - def 12 | - ggg 13 | jpa: 14 | hibernate: 15 | naming: 16 | implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy 17 | properties: 18 | abc: def 19 | hibernate: 20 | archive: 21 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner 22 | logging: 23 | log4j2: 24 | config: 25 | override: abc 26 | level: 27 | root: info 28 | 29 | -------------------------------------------------------------------------------- /plugin/src/main/resources/inspectionDescriptions/PropertyRemoved.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Report removed properties in Spring Boot configuration files. 4 |

5 | A removed property is deprecated property with "error" deprecation level, 6 | refer to Spring 8 | Boot Document, 9 | it is no longer managed and is not bound in the environment. 10 |

11 |

12 | A removed property is no longer supported and should be removed or replaced immediately. 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/AbstractMetadataSource.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | public abstract class AbstractMetadataSource implements MetadataSource { 4 | private long lastModificationStamp = -1; 5 | 6 | 7 | @Override 8 | public void markSynchronized() { 9 | this.lastModificationStamp = getSource().getModificationCount(); 10 | } 11 | 12 | 13 | @Override 14 | public boolean isChanged() { 15 | return isValid() && this.lastModificationStamp != getSource().getModificationCount(); 16 | } 17 | 18 | 19 | @Override 20 | public String toString() { 21 | return getPresentation(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataHint.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider.ValueProvider; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.value.ValueHint; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Hint; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.List; 9 | 10 | public interface MetadataHint { 11 | Hint getMetadata(); 12 | 13 | @NotNull 14 | List getValues(); 15 | 16 | @NotNull 17 | List getProviders(); 18 | } 19 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataGroup.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.psi.PsiMethod; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Group; 6 | import kotlin.Pair; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import javax.swing.*; 10 | import java.util.Optional; 11 | 12 | public interface MetadataGroup extends MetadataItem { 13 | 14 | @Override 15 | default @NotNull Pair getIcon() { 16 | return new Pair<>("AllIcons.Nodes.Folder", AllIcons.Nodes.Folder); 17 | } 18 | 19 | Optional getSourceMethod(); 20 | 21 | Group getMetadata(); 22 | } 23 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/service/ModuleMetadataService.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.service; 2 | 3 | import com.intellij.openapi.module.Module; 4 | import com.intellij.openapi.project.Project; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataIndex; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public interface ModuleMetadataService { 9 | 10 | static ModuleMetadataService getInstance(Module module) { 11 | return module.getService(ModuleMetadataService.class); 12 | } 13 | 14 | /** 15 | * @return Merged spring configuration metadata in this module and its libraries, or {@linkplain MetadataIndex#empty(Project) EMPTY}. 16 | */ 17 | @NotNull MetadataIndex getIndex(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/buildSrc/src/main/groovy/multi-project.conventions.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'java-library' 4 | id 'org.springframework.boot' 5 | id 'io.spring.dependency-management' 6 | } 7 | 8 | group = 'com.acme' 9 | version = '0.0.1-SNAPSHOT' 10 | 11 | java { 12 | toolchain { 13 | languageVersion = JavaLanguageVersion.of(8) 14 | } 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | ext { 22 | set('springCloudVersion', "2021.0.9") 23 | } 24 | 25 | configurations { 26 | compileOnly { 27 | extendsFrom annotationProcessor 28 | } 29 | } 30 | 31 | dependencyManagement { 32 | imports { 33 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 34 | } 35 | } 36 | 37 | tasks.test { 38 | useJUnitPlatform() 39 | } -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("application") 3 | id("multi-project.conventions") 4 | } 5 | 6 | dependencies { 7 | implementation("org.springframework.boot:spring-boot-starter-webflux") 8 | implementation("org.springframework.boot:spring-boot-starter-actuator") 9 | // implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 10 | // implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' 11 | // compileOnly 'org.projectlombok:lombok' 12 | developmentOnly("org.springframework.boot:spring-boot-devtools") 13 | implementation(project(":module1-spring-only")) 14 | implementation(project(":module2-additional-metadata")) 15 | testImplementation(platform("org.junit:junit-bom:5.10.0")) 16 | testImplementation("org.junit.jupiter:junit-jupiter") 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## What happened 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Screenshots 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## Version information 27 | - OS: [e.g. iOS, Windows] 28 | - IDEA Version: [e.g. IC-2022.1.1] 29 | - Plugin Version: [e.g. 0.13.0] 30 | 31 | ## Exception 32 | Copy and paste all the stack traces from IDE. 33 | 34 | ## Additional context 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child1/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.0.M6' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url 'http://repo.spring.io/plugins-release' } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'org.springframework.boot' 17 | apply plugin: 'io.spring.dependency-management' 18 | 19 | sourceCompatibility = 1.8 20 | 21 | repositories { 22 | mavenCentral() 23 | maven { url 'http://repo.spring.io/plugins-release' } 24 | maven { url "https://repo.spring.io/milestone" } 25 | } 26 | 27 | dependencies { 28 | testCompile 'org.springframework.boot:spring-boot-test-autoconfigure' 29 | } -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.7.6' apply false 3 | id 'io.spring.dependency-management' version '1.1.7' apply false 4 | id 'io.freefair.lombok' version '8.12' apply false 5 | id 'multi-project.conventions' 6 | } 7 | 8 | dependencies { 9 | // implementation 'org.springframework.boot:spring-boot-starter-actuator' 10 | // implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 11 | // implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' 12 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 13 | // annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 14 | // testImplementation 'org.springframework.boot:spring-boot-starter-test' 15 | // testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 16 | } 17 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataSource.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.util.ModificationTracker; 4 | 5 | public interface MetadataSource { 6 | 7 | /** 8 | * Get the presentation string of this source 9 | */ 10 | String getPresentation(); 11 | 12 | /** 13 | * Get source element of this metadata 14 | */ 15 | ModificationTracker getSource(); 16 | 17 | /** 18 | * @return true if the source is still isValid. 19 | */ 20 | boolean isValid(); 21 | 22 | /** 23 | * @return true if this source has changed since last {@link #markSynchronized()} 24 | */ 25 | boolean isChanged(); 26 | 27 | /** 28 | * Set the current state of source has been synchronized. 29 | * After marked, {@link #isChanged()} should return {@code true}. 30 | */ 31 | void markSynchronized(); 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/yaml/SpringYamlCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.yaml; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 7 | import org.jetbrains.yaml.YAMLLanguage; 8 | 9 | import static com.intellij.patterns.PlatformPatterns.virtualFile; 10 | 11 | public class SpringYamlCompletionContributor extends CompletionContributor { 12 | public SpringYamlCompletionContributor() { 13 | extend( 14 | CompletionType.BASIC, 15 | PlatformPatterns.psiElement().withLanguage(YAMLLanguage.INSTANCE) 16 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationYamlFileType.INSTANCE)), 17 | new YamlCompletionProvider() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/java/model/RootNode1.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("autogenerated.rootnode1") 8 | public class RootNode1 { 9 | private ChildNode childNode; 10 | private ChildNode11 childNode11; 11 | private ChildNode12 childNode12; 12 | private char[] charArrayProperty; 13 | private String stringProperty; 14 | private int intProperty; 15 | private double doubleProperty; 16 | private byte byteProperty; 17 | private Integer integerWrapperProperty; 18 | private Double doubleWrapperProperty; 19 | private Enum1 enumProperty; 20 | private int newProperty; 21 | 22 | 23 | private enum Enum1 { 24 | val1, val2, val3 25 | } 26 | 27 | 28 | @Data 29 | private class ChildNode11 { 30 | private String childProperty11; 31 | } 32 | 33 | 34 | @Data 35 | private class ChildNode12 { 36 | private boolean childProperty12; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | # buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | # thanks_dev: # Replace with a single thanks.dev username 15 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | custom: 'https://PayPal.Me/flikas248' 17 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/java/model/RootNode1.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties("autogenerated.rootnode1") 8 | public class RootNode1 { 9 | private ChildNode childNode; 10 | private ChildNode11 childNode11; 11 | private ChildNode12 childNode12; 12 | private char[] charArrayProperty; 13 | private String stringProperty; 14 | private int intProperty; 15 | private double doubleProperty; 16 | private byte byteProperty; 17 | private Integer integerWrapperProperty; 18 | private Double doubleWrapperProperty; 19 | private Enum1 enumProperty; 20 | private int newProperty; 21 | 22 | 23 | private enum Enum1 { 24 | val1, val2, val3 25 | } 26 | 27 | 28 | @Data 29 | private class ChildNode11 { 30 | private String childProperty11; 31 | } 32 | 33 | 34 | @Data 35 | private class ChildNode12 { 36 | private boolean childProperty12; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/properties/SpringPropertiesCompletionContributor.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.properties; 2 | 3 | import com.intellij.codeInsight.completion.CompletionContributor; 4 | import com.intellij.codeInsight.completion.CompletionType; 5 | import com.intellij.lang.properties.PropertiesLanguage; 6 | import com.intellij.patterns.PlatformPatterns; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationPropertiesFileType; 8 | 9 | import static com.intellij.patterns.PlatformPatterns.virtualFile; 10 | 11 | public class SpringPropertiesCompletionContributor extends CompletionContributor { 12 | public SpringPropertiesCompletionContributor() { 13 | extend( 14 | CompletionType.BASIC, 15 | PlatformPatterns.psiElement().withLanguage(PropertiesLanguage.INSTANCE) 16 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationPropertiesFileType.INSTANCE)), 17 | new PropertiesCompletionProvider() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example-2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | spring-boot-starter-parent 9 | org.springframework.boot 10 | 2.7.18 11 | 12 | 13 | org.example 14 | spring-boot-example-2 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-webflux 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-jpa 25 | 26 | 27 | com.h2database 28 | h2 29 | 30 | 31 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Completion Test 2 | ## Most common: Property of type String 3 | spring.application.name=my-first-project 4 | ## Hint:Value 5 | spring.jpa.hibernate.ddl-auto=create 6 | ## Hint:Type:enum 7 | spring.data.cassandra.request.throttler.type=CONCURRENCY_LIMITING 8 | ## Hint:Type:boolean 9 | spring.devtools.add-properties=true 10 | ## Hint:Provider:class-reference 11 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 12 | ## Hint:Provider:handle-as:Resource(List) 13 | spring.sql.init.data-locations[0]=classpath:banner.txt 14 | spring.sql.init.data-locations[1]=classpath:my.properties 15 | ## Hint:Provider:handle-as:Resource 16 | spring.liquibase.change-log=classpath:banner.txt 17 | ## Hint:Provider:handle-as:MimeType 18 | spring.freemarker.content-type=application/json 19 | ## Hint:Provider:handle-as:Charset 20 | spring.banner.charset=UTF-8 21 | ## Hint:Provider:handle-as:Locale 22 | server.tomcat.accesslog.locale=en_US 23 | ## Hint:Value:for Map key 24 | logging.level.root=debug 25 | ## Hint:Value:for Map value 26 | logging.level.com.citicbank=off 27 | ## Key completion: Map 28 | logging.group.abc=dcf -------------------------------------------------------------------------------- /plugin/src/main/java/in/oneton/LICENSE.1ton: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 1Ton Technologies, https://1ton.in 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/RootNode1.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import java.util.Set; 7 | 8 | @Data 9 | @ConfigurationProperties("autogenerated.rootnode1") 10 | public class RootNode1 { 11 | private ChildNode childNode; 12 | private ChildNode11 childNode11; 13 | private ChildNode12 childNode12; 14 | private char[] charArrayProperty; 15 | private String stringProperty; 16 | private int intProperty; 17 | private double doubleProperty; 18 | private byte byteProperty; 19 | private Integer integerWrapperProperty; 20 | private Double doubleWrapperProperty; 21 | private Enum1 enumProperty; 22 | private Enum2 anotherEnumProperty; 23 | private Set enumValues; 24 | 25 | 26 | public enum Enum1 { 27 | val1, val2, val3 28 | } 29 | 30 | public static enum Enum2 { 31 | val1, val2, val3 32 | } 33 | 34 | @Data 35 | private class ChildNode11 { 36 | private String childProperty11; 37 | } 38 | 39 | 40 | @Data 41 | private class ChildNode12 { 42 | private boolean childProperty12; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-boot-example 7 | org.example 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | demo-lib 13 | 14 | 15 | GBK 16 | GBK 17 | 18 | 19 | 20 | 21 | org.projectlombok 22 | lombok 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-configuration-processor 27 | 28 | 29 | org.springframework.boot 30 | spring-boot 31 | 32 | 33 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/ModuleRootUtils.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc; 2 | 3 | import com.intellij.openapi.module.Module; 4 | import com.intellij.openapi.roots.ModuleRootManager; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | import lombok.experimental.UtilityClass; 7 | 8 | @UtilityClass 9 | public class ModuleRootUtils { 10 | public static VirtualFile[] getClassRootsWithoutLibrariesRecursively(Module module) { 11 | return ModuleRootManager.getInstance(module) 12 | .orderEntries().recursively().withoutLibraries().withoutSdk().productionOnly().getClassesRoots(); 13 | } 14 | 15 | 16 | public static VirtualFile[] getClassRootsRecursively(Module module) { 17 | return ModuleRootManager.getInstance(module).orderEntries() 18 | .recursively().withoutSdk().productionOnly().getClassesRoots(); 19 | } 20 | 21 | 22 | public static VirtualFile[] getClassRootsWithoutLibraries(Module module) { 23 | // We must use OrderEnumerator but not CompilerModuleExtension, 24 | // because Gradle uses OrderEnumerationHandler to add custom roots. 25 | return ModuleRootManager.getInstance(module).orderEntries() 26 | .withoutSdk().withoutLibraries().productionOnly().getClassesRoots(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | icon-spring-boot 9 | 10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | spring-boot-starter-parent 9 | org.springframework.boot 10 | 2.7.18 11 | 12 | 13 | org.example 14 | spring-boot-example 15 | 1.0-SNAPSHOT 16 | 17 | pom 18 | 19 | demo-app 20 | demo-lib 21 | 22 | 23 | 24 | 2022.0.5 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-dependencies 32 | ${spring.cloud-version} 33 | pom 34 | import 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '2.7.6' 4 | id 'io.spring.dependency-management' version '1.1.7' 5 | } 6 | 7 | group = 'com.acme' 8 | version = '0.0.1-SNAPSHOT' 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(8) 13 | } 14 | } 15 | 16 | configurations { 17 | compileOnly { 18 | extendsFrom annotationProcessor 19 | } 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | ext { 27 | set('springCloudVersion', "2021.0.9") 28 | } 29 | 30 | dependencies { 31 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 32 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 33 | implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' 34 | annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' 35 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 36 | testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 37 | } 38 | 39 | dependencyManagement { 40 | imports { 41 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 42 | } 43 | } 44 | 45 | tasks.named('test') { 46 | useJUnitPlatform() 47 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/icons/springBoot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | icon-spring-boot 10 | 11 | 12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/PsiElementMetadataSource.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.lang.java.JavaLanguage; 4 | import com.intellij.openapi.util.ModificationTracker; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.util.PsiModificationTracker; 7 | import lombok.Getter; 8 | 9 | public class PsiElementMetadataSource extends AbstractMetadataSource { 10 | private final PsiElement sourceElement; 11 | @Getter private final ModificationTracker source; 12 | 13 | 14 | public PsiElementMetadataSource(PsiElement sourceElement) { 15 | this.sourceElement = sourceElement; 16 | //TODO this modification tracker is not for this PsiElement only, but for the all project. 17 | // If there is not an alternative solution for tracking this PsiElement's modification, 18 | // let's remove the modification tracking from the MetadataSource interface. 19 | this.source = PsiModificationTracker.getInstance(sourceElement.getProject()).forLanguage(JavaLanguage.INSTANCE); 20 | } 21 | 22 | 23 | @Override 24 | public String getPresentation() { 25 | return sourceElement.toString(); 26 | } 27 | 28 | 29 | @Override 30 | public boolean isValid() { 31 | return this.sourceElement.isValid(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/PropertiesToPsiReference.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation; 2 | 3 | import com.intellij.lang.properties.psi.Property; 4 | import com.intellij.lang.properties.psi.impl.PropertyKeyImpl; 5 | import com.intellij.psi.util.PsiTreeUtil; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.Iterator; 10 | 11 | class PropertiesToPsiReference extends SpringPropertyToPsiReference { 12 | PropertiesToPsiReference(@NotNull PropertyKeyImpl source) { 13 | super(source); 14 | } 15 | 16 | @Override 17 | protected Iterator candidateKeys(PropertyKeyImpl property) { 18 | String key = PsiTreeUtil.getParentOfType(property, Property.class).getUnescapedKey(); 19 | assert key != null; 20 | PropertyName pn = PropertyName.adapt(key); 21 | return new Iterator<>() { 22 | private PropertyName next = pn; 23 | 24 | @Override 25 | public boolean hasNext() { 26 | return !next.isEmpty(); 27 | } 28 | 29 | @Override 30 | public String next() { 31 | PropertyName n = next; 32 | next = next.getParent(); 33 | return n.toString(); 34 | } 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/PsiMethodUtils.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc; 2 | 3 | import com.intellij.psi.PsiClass; 4 | import com.intellij.psi.PsiMethod; 5 | import com.intellij.psi.impl.light.LightMethodBuilder; 6 | import lombok.experimental.UtilityClass; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | import java.util.Optional; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | @UtilityClass 14 | public class PsiMethodUtils { 15 | private static final Pattern METHOD_SIGNATURE = Pattern.compile("([\\w$]+)\\s*\\((.*)\\)"); 16 | 17 | 18 | public static Optional findMethodBySignature(PsiClass containingClass, String methodSignature) { 19 | Matcher matcher = METHOD_SIGNATURE.matcher(methodSignature); 20 | if (!matcher.matches()) { 21 | return Optional.empty(); 22 | } 23 | String name = matcher.group(1); 24 | String[] params = matcher.group(2).split(","); 25 | LightMethodBuilder patternMethod = new LightMethodBuilder(containingClass.getManager(), name); 26 | for (int i = 0; i < params.length; i++) { 27 | if (StringUtils.isBlank(params[i])) continue; 28 | patternMethod.addParameter("param" + i, params[i]); 29 | } 30 | return Optional.ofNullable(containingClass.findMethodBySignature(patternMethod, true)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/ConfigurationMetadataIndex.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.psi.PsiElement; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | /** 12 | * An index created from a {@link ConfigurationMetadata} 13 | */ 14 | @SuppressWarnings("LombokGetterMayBeUsed") 15 | public class ConfigurationMetadataIndex extends MetadataIndexBase { 16 | private final MetadataSource source; 17 | 18 | 19 | public ConfigurationMetadataIndex( 20 | @NotNull ConfigurationMetadata metadata, @NotNull PsiElement sourceElement, @NotNull Project project) { 21 | super(project); 22 | add(sourceElement.toString(), metadata); 23 | this.source = new PsiElementMetadataSource(sourceElement); 24 | this.source.markSynchronized(); 25 | } 26 | 27 | 28 | public ConfigurationMetadataIndex(@NotNull FileMetadataSource source, @NotNull Project project) throws IOException { 29 | super(project); 30 | add(source.getPresentation(), source.getContent()); 31 | this.source = source; 32 | } 33 | 34 | 35 | @Override 36 | public @NotNull List getSource() { 37 | return List.of(source); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataItem.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.psi.PsiClass; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 6 | import kotlin.Pair; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import javax.swing.*; 10 | import java.util.Optional; 11 | 12 | /** 13 | * A spring configuration metadata property or group 14 | */ 15 | public interface MetadataItem { 16 | /** 17 | * @see ConfigurationMetadata.Property#getName() 18 | * @see ConfigurationMetadata.Group#getName() 19 | */ 20 | @NotNull String getNameStr(); 21 | 22 | @NotNull 23 | default PropertyName getName() { 24 | return PropertyName.of(getNameStr()); 25 | } 26 | 27 | /** 28 | * @see ConfigurationMetadata.Property#getType() 29 | * @see ConfigurationMetadata.Group#getType() 30 | */ 31 | Optional getType(); 32 | 33 | /** 34 | * @see ConfigurationMetadata.Property#getSourceType() 35 | * @see ConfigurationMetadata.Group#getSourceType() 36 | */ 37 | Optional getSourceType(); 38 | 39 | @NotNull Pair getIcon(); 40 | 41 | /** 42 | * @return Rendered(HTML) description for this item 43 | */ 44 | @NotNull 45 | String getRenderedDescription(); 46 | 47 | MetadataIndex getIndex(); 48 | } 49 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/resources/application-negative-cases.yaml: -------------------------------------------------------------------------------- 1 | # Inspection::KeyNotDefined(negative) 2 | an-undefined-key: 1 3 | # Inspection::KeyNotDefined(negative) 4 | spring: 5 | undefined: 2 6 | profiles.include: 7 | # Inspection::InvalidValue(negative), Inspection::KeyNotDefined(negative) 8 | - invalid-key: 9 9 | cloud.discovery.client.simple.instances: 10 | c: 11 | # Inspection::KeyNotDefined(negative, Map>) 12 | - undefined: xxy 13 | resilience4j.circuitbreaker.instances: 14 | "backendA": 15 | # Inspection::KeyNotDefined(negative, Map) 16 | undefined: null 17 | --- 18 | spring: 19 | application: 20 | # Inspection::InvalidValue(negative) 21 | name: 22 | - name1 23 | - name2 24 | server: 25 | servlet: 26 | encoding: 27 | "mapping": 28 | # Inspection::InvalidValue(negative) 29 | "en-US": "ASCII2" 30 | jetty: 31 | # Inspection::InvalidValue(negative) 32 | connection-idle-timeout: abcd 33 | logging: 34 | # Inspection::InvalidValue(negative) 35 | level: xxys 36 | logback: 37 | rolling-policy: 38 | # Inspection::InvalidValue(negative) 39 | max-file-size: 100M 40 | --- 41 | spring: 42 | # Inspection::Deprecated(negative) 43 | profiles: 44 | - abc 45 | logging: 46 | pattern: 47 | # Inspection::Deprecated(negative) 48 | rolling-file-name: "xx" 49 | server: 50 | # Inspection::Deprecated_Unsupported(negative) 51 | use-forward-headers: true 52 | 53 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/AbstractReferenceProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.intellij.psi.PsiReference; 5 | import com.intellij.psi.PsiReferenceProvider; 6 | import com.intellij.psi.PsiReferenceService; 7 | import com.intellij.util.ProcessingContext; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.navigation.ReferenceService; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | abstract class AbstractReferenceProvider extends PsiReferenceProvider { 13 | @Override 14 | @NotNull 15 | public PsiReference @NotNull [] getReferencesByElement( 16 | @NotNull PsiElement element, @NotNull ProcessingContext context) { 17 | PsiElement source = getRefSource(element, context); 18 | if (source != null) { 19 | var svc = ReferenceService.getInstance(element.getProject()); 20 | return new PsiReference[]{svc.forwardReference(source)}; 21 | } else { 22 | return PsiReference.EMPTY_ARRAY; 23 | } 24 | } 25 | 26 | @Override 27 | public boolean acceptsHints(@NotNull PsiElement element, PsiReferenceService.@NotNull Hints hints) { 28 | // if (hints == PsiReferenceService.Hints.HIGHLIGHTED_REFERENCES) return false; 29 | return super.acceptsHints(element, hints); 30 | } 31 | 32 | @Nullable 33 | protected abstract PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context); 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/properties/PropertiesPropertyUsageProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.properties; 2 | 3 | import com.intellij.lang.properties.codeInspection.unused.ImplicitPropertyUsageProvider; 4 | import com.intellij.lang.properties.psi.Property; 5 | import com.intellij.openapi.module.Module; 6 | import com.intellij.openapi.module.ModuleUtil; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public class PropertiesPropertyUsageProvider implements ImplicitPropertyUsageProvider { 13 | @Override 14 | public boolean isUsed(@NotNull Property property) { 15 | Module module = ModuleUtil.findModuleForPsiElement(property); 16 | if (module == null) return false; 17 | var service = ModuleMetadataService.getInstance(module); 18 | String key = property.getUnescapedKey(); 19 | if (StringUtils.isBlank(key)) return false; 20 | PropertyName propertyName = PropertyName.adapt(key); 21 | if (propertyName.isLastElementIndexed()) { 22 | key = propertyName.getParent().toString(); 23 | } 24 | if (service.getIndex().getProperty(key) != null) return true; 25 | var nearestParent = service.getIndex().getNearestParentProperty(key); 26 | return nearestParent != null && nearestParent.canBind(key); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyRemovedInspection.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml; 2 | 3 | import com.intellij.codeInspection.ProblemsHolder; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 6 | import org.jetbrains.yaml.psi.YAMLKeyValue; 7 | 8 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation.Level.ERROR; 9 | 10 | /** 11 | * Report deprecated properties whose deprecation level is error, which means that the property is completely unsupported. 12 | *

13 | * refer to Spring Boot Document 14 | */ 15 | public class PropertyRemovedInspection extends PropertyDeprecatedInspectionBase { 16 | @Override 17 | protected void foundDeprecatedKey( 18 | YAMLKeyValue keyValue, MetadataProperty property, 19 | ConfigurationMetadata.Property.Deprecation deprecation, ProblemsHolder holder, 20 | boolean isOnTheFly 21 | ) { 22 | if (deprecation.getLevel() == ERROR) { 23 | assert keyValue.getKey() != null; 24 | holder.registerProblem( 25 | keyValue.getKey(), 26 | "Property \"" + property.getNameStr() + "\" is deprecated and no longer supported." 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataProperty.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.psi.PsiField; 4 | import com.intellij.psi.PsiType; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.util.Optional; 10 | 11 | /** 12 | * A spring configuration metadata property 13 | */ 14 | public interface MetadataProperty extends MetadataItem { 15 | /** 16 | * The PsiType include type arguments, for example, {@code Map}. 17 | * 18 | * @see ConfigurationMetadata.Property#getType() 19 | */ 20 | Optional getFullType(); 21 | 22 | /** 23 | * @return the field that this property will be bound to, null if not present. 24 | */ 25 | Optional getSourceField(); 26 | 27 | /** 28 | * get hint or value hint for this property. 29 | */ 30 | Optional getHint(); 31 | 32 | /** 33 | * get key hint for this property if it is a Map. 34 | */ 35 | Optional getKeyHint(); 36 | 37 | /** 38 | * @return whether the specified key can be bound to this property. 39 | */ 40 | boolean canBind(@NotNull String key); 41 | 42 | default boolean isMapType() { 43 | return getFullType().filter(p -> PsiTypeUtils.isMap(getIndex().project(), p)).isPresent(); 44 | } 45 | 46 | ConfigurationMetadata.Property getMetadata(); 47 | } 48 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/java/dev/flikas/MyApp.java: -------------------------------------------------------------------------------- 1 | package dev.flikas; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.ApplicationArguments; 5 | import org.springframework.boot.ApplicationRunner; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | 13 | import java.nio.charset.Charset; 14 | 15 | /** 16 | * {@link org.springframework.boot.context.config.ConfigFileApplicationListener} 17 | */ 18 | @SpringBootApplication 19 | public class MyApp implements ApplicationRunner { 20 | 21 | @Autowired private MyProperties properties; 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(MyApp.class); 25 | } 26 | 27 | @GetMapping("/hello") 28 | public String hello() { 29 | return "world"; 30 | } 31 | 32 | @Bean 33 | public String bean() { 34 | return "A"; 35 | } 36 | 37 | @Bean 38 | @ConfigurationProperties("example.server") 39 | public LombokPojo lombokPojo() { 40 | return new LombokPojo(); 41 | } 42 | 43 | @Override 44 | public void run(ApplicationArguments args) throws Exception { 45 | System.out.println(properties.getKeyStore()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyDeprecatedInspection.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml; 2 | 3 | import com.intellij.codeInspection.ProblemsHolder; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 6 | import org.jetbrains.yaml.psi.YAMLKeyValue; 7 | 8 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation.Level.WARNING; 9 | 10 | /** 11 | * Report deprecated properties whose deprecation level is warning, which means that the property is still be bound in the environment. 12 | *

13 | * refer to Spring Boot Document 14 | */ 15 | public class PropertyDeprecatedInspection extends PropertyDeprecatedInspectionBase { 16 | @Override 17 | protected void foundDeprecatedKey( 18 | YAMLKeyValue keyValue, MetadataProperty property, 19 | ConfigurationMetadata.Property.Deprecation deprecation, ProblemsHolder holder, 20 | boolean isOnTheFly 21 | ) { 22 | if (deprecation.getLevel() == null || deprecation.getLevel() == WARNING) { 23 | assert keyValue.getKey() != null; 24 | holder.registerProblem( 25 | keyValue.getKey(), 26 | "Property \"" + property.getNameStr() + "\" is deprecated." 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Available idea versions: 2 | # https://www.jetbrains.com/intellij-repository/releases 3 | # https://www.jetbrains.com/intellij-repository/snapshots 4 | 5 | vendor.name=PENG FEI 6 | vendor.email=flikas@outlook.com 7 | vendor.url=https://github.com/flikas 8 | 9 | plugin.name=Spring Boot Assistant 10 | plugin.id=dev.flikas.idea.spring.boot.assistant.plugin 11 | # MUST use semver (https://semver.org/) 12 | # The pre-release version of plugin.version will be used as JetBrains plugin release channel. 13 | # Common release channels: stable(no pre-release version),alpha,beta,eap. 14 | # https://plugins.jetbrains.com/docs/marketplace/custom-release-channels.html#configure-a-custom-channel 15 | plugin.version=601.0.0 16 | 17 | plugin.source-url=https://github.com/flikas/idea-spring-boot-assistant 18 | # This will be added to plugin's version number as 'build metadata', like '1.0.0-beta+242'. 19 | plugin.since-build=242 20 | #plugin.until-build= 21 | 22 | platform.type=IC 23 | platform.version=2024.3.3 24 | # https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html#platformVersions 25 | # 242+ -> Java 21 26 | # 222+ -> Java 17 27 | # 203+ -> Java 11 28 | platform.java-version=21 29 | 30 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 31 | kotlin.stdlib.default.dependency=false 32 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 33 | org.gradle.configuration-cache=true 34 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 35 | org.gradle.caching=true 36 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/application-negative-cases.yaml: -------------------------------------------------------------------------------- 1 | # Inspection: Negative test cases, here should be 14 warnings ------------> 2 | 3 | # Inspection::KeyNotDefined(negative) 4 | an-undefined-key: 1 5 | # Inspection::KeyNotDefined(negative) 6 | spring: 7 | undefined: 2 8 | profiles.include: 9 | # Inspection::InvalidValue(negative), Inspection::KeyNotDefined(negative) 10 | - invalid-key: 9 11 | cloud.discovery.client.simple.instances: 12 | c: 13 | # Inspection::KeyNotDefined(negative, Map>) 14 | - undefined: xxy 15 | resilience4j.circuitbreaker.instances: 16 | "backendA": 17 | # Inspection::KeyNotDefined(negative, Map) 18 | undefined: null 19 | --- 20 | spring: 21 | application: 22 | # Inspection::InvalidValue(negative) 23 | name: 24 | - name1 25 | - name2 26 | server: 27 | servlet: 28 | encoding: 29 | "mapping": 30 | # Inspection::InvalidValue(negative) 31 | "en-US": "ASCII2" 32 | jetty: 33 | # Inspection::InvalidValue(negative) 34 | connection-idle-timeout: abcd 35 | logging: 36 | # Inspection::InvalidValue(negative) 37 | level: xxys 38 | logback: 39 | rolling-policy: 40 | # Inspection::InvalidValue(negative) 41 | max-file-size: 100M 42 | --- 43 | spring: 44 | # Inspection::Deprecated(negative) 45 | profiles: 46 | - abc 47 | logging: 48 | pattern: 49 | # Inspection::Deprecated(negative) 50 | rolling-file-name: "xx" 51 | server: 52 | # Inspection::Deprecated_Unsupported(negative) 53 | use-forward-headers: true 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/publish-master-to-eap.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 3 | 4 | name: Publish to JetBrains Marketplace 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | build: 14 | environment: JetBrains Marketplace 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: write 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up JDK 21 # This jdk is just for running Gradle, the compiler will be downloaded according to Gradle's build script. 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: '21' 26 | distribution: 'temurin' 27 | cache: gradle 28 | 29 | - name: Build with Gradle 30 | run: ./gradlew buildPlugin 31 | 32 | - name: Run Plugin Verifier 33 | run: ./gradlew verifyPlugin 34 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 35 | # the publishing section of your build.gradle 36 | - name: Publish to JetBrains Marketplace 37 | run: ./gradlew publishPlugin 38 | env: 39 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 40 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 41 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 42 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 43 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/filetype/SpringBootConfigurationYamlFileType.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.filetype; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.fileTypes.LanguageFileType; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.CompositeIconUtils; 6 | import icons.Icons; 7 | import org.jetbrains.annotations.Nls; 8 | import org.jetbrains.annotations.NonNls; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.jetbrains.yaml.YAMLLanguage; 12 | 13 | import javax.swing.*; 14 | 15 | public class SpringBootConfigurationYamlFileType extends LanguageFileType { 16 | public static final SpringBootConfigurationYamlFileType INSTANCE = new SpringBootConfigurationYamlFileType(); 17 | 18 | 19 | private SpringBootConfigurationYamlFileType() { 20 | super(YAMLLanguage.INSTANCE, true); 21 | } 22 | 23 | 24 | @Override 25 | public @NonNls @NotNull String getName() { 26 | return "spring-boot-properties-yaml"; 27 | } 28 | 29 | 30 | @Override 31 | public @Nls @NotNull String getDisplayName() { 32 | return "Spring Boot Configuration YAML"; 33 | } 34 | 35 | 36 | @Override 37 | public @NotNull String getDescription() { 38 | return "Spring configuration yaml file"; 39 | } 40 | 41 | 42 | @Override 43 | public @NotNull String getDefaultExtension() { 44 | return "yaml"; 45 | } 46 | 47 | 48 | @Override 49 | public @Nullable Icon getIcon() { 50 | return CompositeIconUtils.createWithModifier(Icons.SpringBoot, AllIcons.FileTypes.Yaml); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin/src/main/resources/inspectionDescriptions/KeyNotDefined.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Reports undefined keys in Spring Boot configuration files. 4 |

5 | The definition of keys is based on 6 | 7 | Spring Boot configuration metadata 8 | . 9 |

10 |

11 | An undefined key can be fixed by either: 12 |

30 |

31 | See 32 | 33 | Spring Boot document 34 | for more information. 35 |

36 | 37 | 38 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/ValueProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.PrefixMatcher; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.util.Collection; 11 | 12 | /** 13 | * Spring configuration metadata hint, value providers. 14 | * 15 | * @see Spring docs 16 | */ 17 | public interface ValueProvider { 18 | static ValueProvider create(ConfigurationMetadata.Hint.ValueProvider metadata) { 19 | return switch (metadata.getName()) { 20 | case ANY -> null; 21 | case CLASS_REFERENCE -> new ClassReferenceValueProvider(metadata); 22 | case HANDLE_AS -> new HandleAsValueProvider(metadata); 23 | case LOGGER_NAME -> new LoggerNameValueProvider(metadata); 24 | case SPRING_BEAN_REFERENCE -> null; 25 | case SPRING_PROFILE_NAME -> null; 26 | }; 27 | } 28 | 29 | default ConfigurationMetadata.Hint.ValueProvider.Type getType() { 30 | return getMetadata().getName(); 31 | } 32 | 33 | ConfigurationMetadata.Hint.ValueProvider getMetadata(); 34 | 35 | Collection provideValues( 36 | @NotNull CompletionParameters completionParameters, 37 | @Nullable PrefixMatcher prefixMatcher 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/filetype/SpringBootConfigurationPropertiesFileType.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.filetype; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.lang.properties.PropertiesLanguage; 5 | import com.intellij.openapi.fileTypes.LanguageFileType; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.CompositeIconUtils; 7 | import icons.Icons; 8 | import org.jetbrains.annotations.Nls; 9 | import org.jetbrains.annotations.NonNls; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import javax.swing.*; 14 | 15 | public class SpringBootConfigurationPropertiesFileType extends LanguageFileType { 16 | public static final SpringBootConfigurationPropertiesFileType INSTANCE = new SpringBootConfigurationPropertiesFileType(); 17 | 18 | 19 | private SpringBootConfigurationPropertiesFileType() { 20 | super(PropertiesLanguage.INSTANCE, true); 21 | } 22 | 23 | 24 | @Override 25 | public @NonNls @NotNull String getName() { 26 | return "sba-spring-boot-configuration-properties"; 27 | } 28 | 29 | 30 | @Override 31 | public @Nls @NotNull String getDisplayName() { 32 | return "Spring Boot Configuration Properties"; 33 | } 34 | 35 | 36 | @Override 37 | public @NotNull String getDescription() { 38 | return "Spring Boot configuration properties file"; 39 | } 40 | 41 | 42 | @Override 43 | public @NotNull String getDefaultExtension() { 44 | return "properties"; 45 | } 46 | 47 | 48 | @Override 49 | public @Nullable Icon getIcon() { 50 | return CompositeIconUtils.createWithModifier(Icons.SpringBoot, AllIcons.FileTypes.Properties); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/YamlToPsiReference.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation; 2 | 3 | import com.intellij.openapi.util.TextRange; 4 | import com.intellij.psi.PsiElement; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.yaml.YAMLUtil; 8 | import org.jetbrains.yaml.psi.YAMLKeyValue; 9 | 10 | import java.util.Iterator; 11 | 12 | class YamlToPsiReference extends SpringPropertyToPsiReference { 13 | YamlToPsiReference(@NotNull YAMLKeyValue source) { 14 | super(source); 15 | } 16 | 17 | @Override 18 | protected Iterator candidateKeys(YAMLKeyValue key) { 19 | PropertyName fullKey = PropertyName.adapt(YAMLUtil.getConfigFullName(key)); 20 | PropertyName thisKey = PropertyName.adapt(key.getKeyText()); 21 | assert fullKey.toString().endsWith(thisKey.toString()); 22 | PropertyName prefix = fullKey.chop(fullKey.getNumberOfElements() - thisKey.getNumberOfElements()); 23 | return new Iterator<>() { 24 | private PropertyName next = thisKey; 25 | 26 | @Override 27 | public boolean hasNext() { 28 | return !next.isEmpty(); 29 | } 30 | 31 | @Override 32 | public String next() { 33 | PropertyName n = next; 34 | next = next.getParent(); 35 | return prefix.append(n).toString(); 36 | } 37 | }; 38 | } 39 | 40 | 41 | @Override 42 | protected TextRange calculateDefaultRangeInElement() { 43 | PsiElement key = getElement().getKey(); 44 | if (key != null) { 45 | return key.getTextRangeInParent(); 46 | } 47 | return super.calculateDefaultRangeInElement(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/YamlToCodeReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.intellij.psi.PsiReferenceContributor; 5 | import com.intellij.psi.PsiReferenceRegistrar; 6 | import com.intellij.util.ProcessingContext; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.yaml.YAMLLanguage; 10 | import org.jetbrains.yaml.psi.YAMLKeyValue; 11 | 12 | import static com.intellij.patterns.PlatformPatterns.psiElement; 13 | import static com.intellij.patterns.PsiJavaPatterns.virtualFile; 14 | 15 | 16 | /** 17 | * Provides references from Spring configuration file (application.yaml) to code. 18 | */ 19 | public class YamlToCodeReferenceContributor extends PsiReferenceContributor { 20 | 21 | //TODO refactor by com.intellij.psi.search.searches.DefinitionsScopedSearch.EP and 22 | // com.intellij.psi.search.searches.ReferencesSearch.EP_NAME 23 | 24 | 25 | @Override 26 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 27 | registrar.registerReferenceProvider( 28 | psiElement(YAMLKeyValue.class) 29 | .withLanguage(YAMLLanguage.INSTANCE) 30 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationYamlFileType.INSTANCE)), 31 | new AbstractReferenceProvider() { 32 | @Override 33 | protected PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context) { 34 | return element instanceof YAMLKeyValue yamlKeyValue ? yamlKeyValue : null; 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/backward/PsiToSpringPropertyReferenceSearcher.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.backward; 2 | 3 | import com.intellij.openapi.application.QueryExecutorBase; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiReference; 7 | import com.intellij.psi.search.GlobalSearchScope; 8 | import com.intellij.psi.search.SearchScope; 9 | import com.intellij.psi.search.searches.ReferencesSearch; 10 | import com.intellij.util.Processor; 11 | import dev.flikas.spring.boot.assistant.idea.plugin.navigation.ReferenceService; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | public class PsiToSpringPropertyReferenceSearcher 15 | extends QueryExecutorBase { 16 | protected PsiToSpringPropertyReferenceSearcher() { 17 | super(true); 18 | } 19 | 20 | 21 | @Override 22 | public void processQuery( 23 | @NotNull ReferencesSearch.SearchParameters queryParameters, @NotNull Processor consumer) { 24 | if (!queryParameters.areValid()) return; 25 | SearchScope searchScope = queryParameters.getScopeDeterminedByUser(); 26 | if (!(searchScope instanceof GlobalSearchScope)) return; 27 | PsiElement element = queryParameters.getElementToSearch(); 28 | 29 | Project project = element.getProject(); 30 | ReferenceService service = ReferenceService.getInstance(project); 31 | service.backwardReference(element) 32 | .stream() 33 | .filter(ref -> ref.getElement().isValid()) 34 | .filter(ref -> searchScope.contains(ref.getElement().getContainingFile().getVirtualFile())) 35 | .forEach(consumer::process); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/source/MetadataFileIndexConfigurator.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.source; 2 | 3 | import com.intellij.openapi.module.Module; 4 | import com.intellij.openapi.module.ModuleManager; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.util.indexing.IndexableSetContributor; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.ModuleRootUtils; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | 14 | /** 15 | * Add project's {@code /META-INF/(additional-)spring-configuration-metadata.json} files into file-based index, for {@link MetadataFileIndex}. 16 | */ 17 | public class MetadataFileIndexConfigurator extends IndexableSetContributor { 18 | @Override 19 | public @NotNull Set getAdditionalRootsToIndex() { 20 | return Set.of(); 21 | } 22 | 23 | 24 | @Override 25 | public @NotNull Set getAdditionalProjectRootsToIndex(@NotNull Project project) { 26 | Set files = new HashSet<>(); 27 | for (Module module : ModuleManager.getInstance(project).getModules()) { 28 | for (VirtualFile classRoot : ModuleRootUtils.getClassRootsWithoutLibraries(module)) { 29 | VirtualFile metaDir = classRoot.findChild(MetadataFileIndex.META_FILE_DIR); 30 | if (metaDir == null) continue; 31 | VirtualFile f = metaDir.findChild(MetadataFileIndex.METADATA_FILE_NAME); 32 | if (f != null) files.add(f); 33 | f = metaDir.findChild(MetadataFileIndex.ADDITIONAL_METADATA_FILE_NAME); 34 | if (f != null) files.add(f); 35 | } 36 | } 37 | return files; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin/src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | jetty: 3 | connection-idle-timeout: 4 | address: 5 | reactive: 6 | session: 7 | timeout: 8 | tomcat: 9 | accept-count: 10 | connection-timeout: 60 11 | threads: 12 | max: 13 | accesslog: 14 | buffered: 15 | basedir: 16 | port: xxf 17 | abc: x 18 | spring: 19 | devtools: 20 | add-properties: 21 | remote: 22 | application: 23 | name: 24 | cloud.discovery.client.simple: 25 | instances: 26 | a: 27 | - port: 1 28 | host: 127.0.0.1 29 | - host: 127.0.0.1 30 | serviceId: service 31 | port: 12 32 | b: 33 | - host: 127.0.0.1 34 | port: 12 35 | activemq: 36 | packages: 37 | trust-all: 38 | artemis: 39 | embedded: 40 | data-directory: 41 | logging: 42 | charset: 43 | console: utf-8 44 | pattern: 45 | rolling-file-name: "xx" 46 | level: 47 | root: info 48 | logback: 49 | rollingpolicy: 50 | max-file-size: 100MB 51 | 52 | 53 | resilience4j: 54 | circuitbreaker: 55 | instances: 56 | backendA: 57 | permittedNumberOfCallsInHalfOpenState: 58 | exponentialBackoffMultiplier: 59 | allowHealthIndicatorToFail: 60 | baseConfig: woha 61 | backendB: 62 | baseConfig: o 63 | failureRateThreshold: 64 | 65 | 66 | --- 67 | example: 68 | server: 69 | port: 70 | key: value 71 | - name: python 72 | id: 1 73 | - name: "c++" 74 | id: 2 75 | client: 76 | - client1 77 | - "client2" 78 | longString: | 79 | abcidosie 80 | sjigoes 81 | jsdfiwoeige 82 | longString2: >- 83 | jsaidfoei 84 | sdjfoiwe 85 | sjfoeiwreow 86 | 87 | 88 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/value/ValueHint.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.value; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public class ValueHint { 10 | private final ConfigurationMetadata.Hint.ValueHint metadata; 11 | 12 | 13 | public ValueHint(ConfigurationMetadata.Hint.ValueHint metadata) { 14 | this.metadata = metadata; 15 | } 16 | 17 | 18 | /** 19 | * @see ConfigurationMetadata.Hint.ValueHint#getValue() 20 | */ 21 | @NotNull 22 | public Object getValue() { 23 | return metadata.getValue(); 24 | } 25 | 26 | 27 | /** 28 | * @see ConfigurationMetadata.Hint.ValueHint#getDescription() 29 | */ 30 | @Nullable 31 | public String getDescription() { 32 | return metadata.getDescription(); 33 | } 34 | 35 | 36 | public Hint toHint() { 37 | return new Hint( 38 | String.valueOf(getValue()), 39 | getFirstLine(getDescription()), 40 | getDescription(), 41 | AllIcons.Nodes.Field); 42 | } 43 | 44 | 45 | private String getFirstLine(@Nullable String paragraph) { 46 | if (paragraph == null) return null; 47 | int dot = paragraph.indexOf('.'); 48 | int ls = paragraph.indexOf('\n'); 49 | int end; 50 | if (dot > 0 && ls > 0) { 51 | end = Math.min(dot, ls); 52 | } else if (dot > 0) { 53 | end = dot; 54 | } else if (ls > 0) { 55 | end = ls; 56 | } else { 57 | return paragraph; 58 | } 59 | return paragraph.substring(0, end); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/forward/PropertiesToCodeReferenceContributor.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation.forward; 2 | 3 | import com.intellij.lang.properties.PropertiesLanguage; 4 | import com.intellij.lang.properties.psi.impl.PropertyKeyImpl; 5 | import com.intellij.patterns.PlatformPatterns; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiReferenceContributor; 8 | import com.intellij.psi.PsiReferenceRegistrar; 9 | import com.intellij.util.ProcessingContext; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationPropertiesFileType; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import static com.intellij.patterns.PsiJavaPatterns.virtualFile; 14 | 15 | 16 | /** 17 | * Provides references from Spring configuration file (application.properties) to code. 18 | */ 19 | public class PropertiesToCodeReferenceContributor extends PsiReferenceContributor { 20 | 21 | //TODO refactor by com.intellij.psi.search.searches.DefinitionsScopedSearch.EP and 22 | // com.intellij.psi.search.searches.ReferencesSearch.EP_NAME 23 | 24 | 25 | @Override 26 | public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { 27 | registrar.registerReferenceProvider( 28 | PlatformPatterns.psiElement(PropertyKeyImpl.class) 29 | .withLanguage(PropertiesLanguage.INSTANCE) 30 | .inVirtualFile(virtualFile().ofType(SpringBootConfigurationPropertiesFileType.INSTANCE)), 31 | new AbstractReferenceProvider() { 32 | @Override 33 | protected PsiElement getRefSource(@NotNull PsiElement element, @NotNull ProcessingContext context) { 34 | return element instanceof PropertyKeyImpl key ? key : null; 35 | } 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/Hint.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import javax.swing.*; 8 | 9 | public record Hint( 10 | @NotNull String value, 11 | @Nullable String oneLineDescription, 12 | @Nullable String description, 13 | @Nullable Icon icon, 14 | @Nullable PsiElement psiElement, 15 | @Nullable Integer priorityGroup 16 | ) implements Comparable { 17 | public Hint(@NotNull String value, @NotNull PsiElement psiElement) { 18 | this(value, null, null, null, psiElement, null); 19 | } 20 | 21 | 22 | public Hint(@NotNull String value, @NotNull PsiElement psiElement, int priorityGroup) { 23 | this(value, null, null, null, psiElement, priorityGroup); 24 | } 25 | 26 | 27 | public Hint(@NotNull String value, @NotNull Icon icon) { 28 | this(value, null, null, icon, null, null); 29 | } 30 | 31 | 32 | public Hint(@NotNull String value, @NotNull Icon icon, int priorityGroup) { 33 | this(value, null, null, icon, null, priorityGroup); 34 | } 35 | 36 | 37 | public Hint( 38 | @NotNull String value, @Nullable String oneLineDescription, @Nullable String description, @NotNull Icon icon) { 39 | this(value, oneLineDescription, description, icon, null, null); 40 | } 41 | 42 | 43 | public Hint( 44 | @NotNull String value, @Nullable String oneLineDescription, @Nullable String description, @NotNull Icon icon, 45 | int priorityGroup 46 | ) { 47 | this(value, oneLineDescription, description, icon, null, priorityGroup); 48 | } 49 | 50 | 51 | @Override 52 | public int compareTo(@NotNull Hint o) { 53 | return this.value.compareTo(o.value); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/SpringPropertyToPsiReference.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation; 2 | 3 | import com.intellij.codeInspection.reference.PsiMemberReference; 4 | import com.intellij.openapi.module.Module; 5 | import com.intellij.openapi.module.ModuleUtil; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiReferenceBase; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.util.Iterator; 15 | 16 | abstract class SpringPropertyToPsiReference 17 | extends PsiReferenceBase implements PsiMemberReference { 18 | 19 | SpringPropertyToPsiReference(@NotNull T source) { 20 | super(source, true); 21 | } 22 | 23 | @Override 24 | public @Nullable PsiElement resolve() { 25 | Module module = ModuleUtil.findModuleForPsiElement(getElement()); 26 | if (module == null) return null; 27 | 28 | Iterator candidates = candidateKeys(getElement()); 29 | 30 | ModuleMetadataService metadataService = ModuleMetadataService.getInstance(module); 31 | while (candidates.hasNext()) { 32 | String key = candidates.next(); 33 | MetadataItem propertyOrGroup = metadataService.getIndex().getPropertyOrGroup(key); 34 | if (propertyOrGroup == null) continue; 35 | if (propertyOrGroup instanceof MetadataProperty property) { 36 | return property.getSourceField().map(f -> (PsiElement) f).or(property::getSourceType).orElse(null); 37 | } else { 38 | return propertyOrGroup.getSourceType().orElse(null); 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | protected abstract Iterator candidateKeys(T source); 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/FileMetadataSource.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.google.gson.Gson; 4 | import com.intellij.openapi.vfs.VirtualFile; 5 | import com.intellij.openapi.vfs.VirtualFileManager; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 7 | import lombok.Getter; 8 | 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.io.Reader; 12 | 13 | @SuppressWarnings("LombokGetterMayBeUsed") 14 | public class FileMetadataSource extends AbstractMetadataSource { 15 | private static final ThreadLocal gson = ThreadLocal.withInitial(Gson::new); 16 | @Getter private VirtualFile source; 17 | 18 | 19 | public FileMetadataSource(VirtualFile source) { 20 | this.source = source; 21 | } 22 | 23 | 24 | /** 25 | * If current source {@link VirtualFile} is invalid, try to recreate it with the same URL, 26 | * if succeeded, return true. 27 | * 28 | * @return true if current source file is invalid and the recreation is succeeded. 29 | */ 30 | public boolean tryReloadIfInvalid() { 31 | if (!isValid()) { 32 | VirtualFile vf = VirtualFileManager.getInstance().refreshAndFindFileByUrl(this.source.getUrl()); 33 | if (vf != null) { 34 | this.source = vf; 35 | return true; 36 | } 37 | } 38 | return false; 39 | } 40 | 41 | 42 | @Override 43 | public boolean isValid() { 44 | return source.isValid(); 45 | } 46 | 47 | 48 | @Override 49 | public String getPresentation() { 50 | return source.toString(); 51 | } 52 | 53 | 54 | public ConfigurationMetadata getContent() throws IOException { 55 | try (Reader reader = new InputStreamReader(source.getInputStream(), source.getCharset())) { 56 | ConfigurationMetadata metadata = gson.get().fromJson(reader, ConfigurationMetadata.class); 57 | markSynchronized(); 58 | return metadata; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataHintImpl.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider.HandleAsValueProvider; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider.ValueProvider; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.value.ValueHint; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | 12 | import java.util.List; 13 | import java.util.Objects; 14 | import java.util.stream.Stream; 15 | 16 | @EqualsAndHashCode(of = "metadata") 17 | @ToString(of = "metadata") 18 | @Getter 19 | class MetadataHintImpl implements MetadataHint { 20 | private static final Logger LOG = Logger.getInstance(HandleAsValueProvider.class); 21 | private final ConfigurationMetadata.Hint metadata; 22 | private final List values; 23 | private final List providers; 24 | 25 | 26 | public MetadataHintImpl(ConfigurationMetadata.Hint metadata) { 27 | this.metadata = metadata; 28 | 29 | ConfigurationMetadata.Hint.ValueHint[] values = metadata.getValues(); 30 | if (values == null) { 31 | this.values = List.of(); 32 | } else { 33 | this.values = Stream.of(values).map(ValueHint::new).toList(); 34 | } 35 | 36 | ConfigurationMetadata.Hint.ValueProvider[] providers = metadata.getProviders(); 37 | if (providers == null) { 38 | this.providers = List.of(); 39 | } else { 40 | this.providers = Stream.of(providers).map(m -> { 41 | try { 42 | return ValueProvider.create(m); 43 | } catch (Exception e) { 44 | LOG.warn("Invalid hint configuration, ignored: " + metadata.getName(), e); 45 | return null; 46 | } 47 | }).filter(Objects::nonNull).toList(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/YamlInspectionBase.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml; 2 | 3 | import com.intellij.codeInspection.LocalInspectionTool; 4 | import com.intellij.codeInspection.LocalInspectionToolSession; 5 | import com.intellij.codeInspection.ProblemsHolder; 6 | import com.intellij.openapi.fileTypes.FileTypeManager; 7 | import com.intellij.openapi.module.Module; 8 | import com.intellij.openapi.module.ModuleUtil; 9 | import com.intellij.openapi.progress.ProgressIndicatorProvider; 10 | import com.intellij.openapi.vfs.VirtualFile; 11 | import com.intellij.psi.PsiElementVisitor; 12 | import com.intellij.psi.PsiFile; 13 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.yaml.psi.YAMLKeyValue; 16 | import org.jetbrains.yaml.psi.YamlPsiElementVisitor; 17 | 18 | public abstract class YamlInspectionBase extends LocalInspectionTool { 19 | @Override 20 | public boolean isAvailableForFile(@NotNull PsiFile file) { 21 | VirtualFile virtualFile = file.getVirtualFile(); 22 | if (virtualFile == null) { 23 | return false; 24 | } 25 | FileTypeManager ftm = FileTypeManager.getInstance(); 26 | return ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE); 27 | } 28 | 29 | 30 | @Override 31 | public @NotNull PsiElementVisitor buildVisitor( 32 | @NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session 33 | ) { 34 | Module module = ModuleUtil.findModuleForFile(session.getFile()); 35 | if (module == null) return PsiElementVisitor.EMPTY_VISITOR; 36 | 37 | return new YamlPsiElementVisitor() { 38 | @Override 39 | public void visitKeyValue(@NotNull YAMLKeyValue keyValue) { 40 | ProgressIndicatorProvider.checkCanceled(); 41 | YamlInspectionBase.this.visitKeyValue(module, keyValue, holder, isOnTheFly); 42 | } 43 | }; 44 | } 45 | 46 | 47 | protected abstract void visitKeyValue( 48 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-boot-example 7 | org.example 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | demo-app 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-starter-webflux 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-circuitbreaker-resilience4j 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-jpa 26 | 27 | 28 | com.h2database 29 | h2 30 | 31 | 32 | com.mysql 33 | mysql-connector-j 34 | 35 | 36 | com.oracle.database.jdbc 37 | ojdbc11 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-batch 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-configuration-processor 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-devtools 50 | 51 | 52 | org.springframework.session 53 | spring-session-data-redis 54 | 55 | 56 | org.example 57 | demo-lib 58 | 1.0-SNAPSHOT 59 | 60 | 61 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child1/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.acme 8 | multi-module 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | child1 13 | child1 14 | Child1 15 | 16 | 17 | UTF-8 18 | UTF-8 19 | 1.8 20 | Finchley.M4 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-test-autoconfigure 27 | test 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-maven-plugin 36 | 37 | 38 | 39 | 40 | 41 | 42 | spring-snapshots 43 | Spring Snapshots 44 | https://repo.spring.io/snapshot 45 | 46 | true 47 | 48 | 49 | 50 | spring-milestones 51 | Spring Milestones 52 | https://repo.spring.io/milestone 53 | 54 | false 55 | 56 | 57 | 58 | 59 | 60 | 61 | spring-snapshots 62 | Spring Snapshots 63 | https://repo.spring.io/snapshot 64 | 65 | true 66 | 67 | 68 | 69 | spring-milestones 70 | Spring Milestones 71 | https://repo.spring.io/milestone 72 | 73 | false 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/HomonymProperties.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import lombok.experimental.Delegate; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.AbstractMap; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.ConcurrentMap; 11 | 12 | /** 13 | * Aggregate properties which have the same name. 14 | */ 15 | public class HomonymProperties extends AbstractMap implements MetadataProperty { 16 | private static final Logger LOG = Logger.getInstance(HomonymProperties.class); 17 | 18 | private final ConcurrentMap items = new ConcurrentHashMap<>(); 19 | @Delegate 20 | private MetadataProperty mainProperty; 21 | 22 | 23 | public HomonymProperties(String source, MetadataProperty property) { 24 | add(source, property); 25 | } 26 | 27 | 28 | @Override 29 | public @NotNull Set> entrySet() { 30 | return items.entrySet(); 31 | } 32 | 33 | 34 | public void add(@NotNull String source, MetadataProperty property) { 35 | MetadataProperty current = items.putIfAbsent(property.getNameStr(), property); 36 | if (current != null && !current.equals(property)) { 37 | LOG.warn("Duplicate property " + property.getNameStr() + " in " + source + ", ignored"); 38 | } else if (current == null) { 39 | if (this.mainProperty == null) { 40 | this.mainProperty = property; 41 | } else if (this.mainProperty.getMetadata().getDeprecation() != null 42 | && property.getMetadata().getDeprecation() == null) { 43 | this.mainProperty = property; 44 | } else if (property.getMetadata().getDeprecation() == null) { 45 | LOG.warn("Duplicate property '" + property.getNameStr() + "' & '" 46 | + this.mainProperty.getNameStr() + "' in " + source + ", ignored"); 47 | } 48 | } 49 | } 50 | 51 | 52 | public void addAll(String source, Iterable properties) { 53 | for (MetadataProperty property : properties) { 54 | add(source, property); 55 | } 56 | } 57 | 58 | 59 | public HomonymProperties merge(String source, HomonymProperties properties) { 60 | addAll(source, properties.values()); 61 | return this; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/PropertyDeprecatedInspectionBase.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml; 2 | 3 | import com.intellij.codeInspection.ProblemsHolder; 4 | import com.intellij.openapi.module.Module; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata.Property.Deprecation; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.yaml.YAMLUtil; 10 | import org.jetbrains.yaml.psi.YAMLAlias; 11 | import org.jetbrains.yaml.psi.YAMLKeyValue; 12 | import org.jetbrains.yaml.psi.YAMLMapping; 13 | import org.jetbrains.yaml.psi.YAMLValue; 14 | 15 | public abstract class PropertyDeprecatedInspectionBase extends YamlInspectionBase { 16 | @Override 17 | protected void visitKeyValue( 18 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly 19 | ) { 20 | YAMLValue yamlValue = keyValue.getValue(); 21 | if (yamlValue == null) return; 22 | if (yamlValue instanceof YAMLAlias) return; //TODO Support YAML alias. 23 | 24 | String propertyName = YAMLUtil.getConfigFullName(keyValue); 25 | ModuleMetadataService service = module.getService(ModuleMetadataService.class); 26 | MetadataProperty property = service.getIndex().getProperty(propertyName); 27 | if (property == null) return; 28 | 29 | if (yamlValue instanceof YAMLMapping && !property.isMapType()) { 30 | // Property isValid, its value in YAML is a mapping, but the property's type is not a Map: this may happen on 31 | // property deprecation, for example, "spring.profiles" & "spring.profiles.active/group/include/...". 32 | // If it happens, we should only prompt deprecation while the actual value type coincides with the property's type. 33 | return; 34 | } 35 | Deprecation deprecation = property.getMetadata().getDeprecation(); 36 | if (deprecation != null) { 37 | foundDeprecatedKey(keyValue, property, deprecation, holder, isOnTheFly); 38 | } 39 | } 40 | 41 | 42 | protected abstract void foundDeprecatedKey( 43 | YAMLKeyValue keyValue, MetadataProperty property, Deprecation deprecation, 44 | ProblemsHolder holder, boolean isOnTheFly 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/service/ProjectMetadataService.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.service; 2 | 3 | import com.intellij.openapi.Disposable; 4 | import com.intellij.openapi.components.Service; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.roots.ModuleRootModel; 7 | import com.intellij.openapi.vfs.VirtualFile; 8 | import com.intellij.task.ProjectTaskListener; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataIndex; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.MutableReference; 11 | import lombok.Getter; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.concurrent.ConcurrentHashMap; 15 | import java.util.concurrent.ConcurrentMap; 16 | 17 | import static com.intellij.openapi.compiler.CompilerTopics.COMPILATION_STATUS; 18 | 19 | /** 20 | * Service that generates {@link MetadataIndex} from one {@linkplain ModuleRootModel#getSourceRoots() SourceRoot}. 21 | *

22 | * It searches and generate index from Spring Configuration Files 23 | * in the source root and watches them for automatically update the index. 24 | */ 25 | @Service(Service.Level.PROJECT) 26 | final class ProjectMetadataService implements Disposable { 27 | private final Project project; 28 | private final ConcurrentMap metadataFiles = new ConcurrentHashMap<>(); 29 | @Getter private final MetadataIndex emptyIndex; 30 | 31 | 32 | public ProjectMetadataService(Project project) { 33 | this.project = project; 34 | this.emptyIndex = MetadataIndex.empty(this.project); 35 | CompilationListener compilationListener = new CompilationListener(project); 36 | project.getMessageBus().connect().subscribe(COMPILATION_STATUS, compilationListener); 37 | // For gradle delegated build 38 | project.getMessageBus().connect().subscribe(ProjectTaskListener.TOPIC, compilationListener); 39 | } 40 | 41 | 42 | public MutableReference getIndexForMetaFile(@NotNull VirtualFile metadataFile) { 43 | return getIndex(metadataFile); 44 | } 45 | 46 | 47 | @Override 48 | public void dispose() { 49 | // This is a parent disposable for FileWatcher. 50 | } 51 | 52 | 53 | private MetadataFileContainer getIndex(@NotNull VirtualFile metadataFile) { 54 | return metadataFiles.computeIfAbsent(metadataFile.getUrl(), 55 | url -> new MetadataFileContainer(metadataFile, project)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/properties/PropertiesValueInsertHandler.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.properties; 2 | 3 | import com.intellij.codeInsight.completion.InsertHandler; 4 | import com.intellij.codeInsight.completion.InsertionContext; 5 | import com.intellij.codeInsight.lookup.LookupElement; 6 | import com.intellij.lang.properties.psi.PropertiesResourceBundleUtil; 7 | import com.intellij.lang.properties.psi.Property; 8 | import com.intellij.lang.properties.psi.PropertyKeyValueFormat; 9 | import com.intellij.lang.properties.psi.codeStyle.PropertiesCodeStyleSettings; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.psi.PsiElement; 12 | import com.intellij.psi.util.PsiTreeUtil; 13 | import net.jcip.annotations.ThreadSafe; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | @ThreadSafe 17 | class PropertiesValueInsertHandler implements InsertHandler { 18 | public static final PropertiesValueInsertHandler INSTANCE = new PropertiesValueInsertHandler(); 19 | 20 | 21 | private PropertiesValueInsertHandler() {} 22 | 23 | 24 | @Override 25 | public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement lookupElement) { 26 | Project project = context.getProject(); 27 | PsiElement currentElement = context.getFile().findElementAt(context.getStartOffset()); 28 | assert currentElement != null : "no element at " + context.getStartOffset(); 29 | Property property = PsiTreeUtil.getParentOfType(currentElement, Property.class); 30 | if (property == null) return; 31 | 32 | String escaped = escapeValue(project, lookupElement.getLookupString()); 33 | if (!escaped.equals(lookupElement.getLookupString())) { 34 | property.setValue(lookupElement.getLookupString(), PropertyKeyValueFormat.MEMORY); 35 | } 36 | 37 | //TODO Add '\n' if the value is 'complete'(for example, 'classpath:' is not complete. 38 | // if (context.getCompletionChar() == '\n') { 39 | // Editor editor = context.getEditor(); 40 | // editor.getCaretModel().moveToOffset(property.getTextOffset() + property.getTextLength()); 41 | // EditorModificationUtil.insertStringAtCaret(editor, "\n", false, true); 42 | // } 43 | } 44 | 45 | 46 | private String escapeValue(Project project, String value) { 47 | char delimiter = PropertiesCodeStyleSettings.getInstance(project).getDelimiter(); 48 | return PropertiesResourceBundleUtil.convertValueToFileFormat(value, delimiter, PropertyKeyValueFormat.MEMORY); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/navigation/SpringPropertyReadWriteAccessDetector.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.navigation; 2 | 3 | import com.intellij.codeInsight.highlighting.JavaReadWriteAccessDetector; 4 | import com.intellij.lang.properties.parsing.PropertiesTokenTypes; 5 | import com.intellij.lang.properties.psi.Property; 6 | import com.intellij.openapi.fileTypes.FileTypeManager; 7 | import com.intellij.openapi.vfs.VirtualFile; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.PsiField; 10 | import com.intellij.psi.PsiFile; 11 | import com.intellij.psi.PsiReference; 12 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationPropertiesFileType; 13 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.yaml.psi.YAMLKeyValue; 16 | 17 | import java.util.Optional; 18 | 19 | /** 20 | * This detector have to extends {@link JavaReadWriteAccessDetector}, 21 | * because KotlinReadWriteAccessDetector decorate it. 22 | */ 23 | public class SpringPropertyReadWriteAccessDetector extends JavaReadWriteAccessDetector { 24 | @Override 25 | public boolean isReadWriteAccessible(@NotNull PsiElement element) { 26 | return element instanceof PsiField || super.isReadWriteAccessible(element); 27 | } 28 | 29 | @Override 30 | public @NotNull Access getReferenceAccess(@NotNull PsiElement referencedElement, @NotNull PsiReference reference) { 31 | return reference instanceof SpringPropertyToPsiReference 32 | ? Access.Write 33 | : super.getReferenceAccess(referencedElement, reference); 34 | } 35 | 36 | @Override 37 | public @NotNull Access getExpressionAccess(@NotNull PsiElement expression) { 38 | return isSpringProperty(expression) 39 | ? Access.Write 40 | : super.getExpressionAccess(expression); 41 | } 42 | 43 | private boolean isSpringProperty(PsiElement element) { 44 | VirtualFile vf = Optional.ofNullable(element.getContainingFile()).map(PsiFile::getVirtualFile).orElse(null); 45 | if (vf == null) return false; 46 | var vfm = FileTypeManager.getInstance(); 47 | return (vfm.isFileOfType(vf, SpringBootConfigurationYamlFileType.INSTANCE) && element instanceof YAMLKeyValue) 48 | || (vfm.isFileOfType(vf, SpringBootConfigurationPropertiesFileType.INSTANCE) 49 | && (element instanceof Property 50 | || element.getNode().getElementType() == PropertiesTokenTypes.KEY_CHARACTERS)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/SourceContainer.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion; 2 | 3 | import com.intellij.lang.Language; 4 | import com.intellij.navigation.ItemPresentation; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.util.NlsSafe; 7 | import com.intellij.psi.PsiManager; 8 | import com.intellij.psi.impl.light.LightElement; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint; 11 | import kotlin.Pair; 12 | import lombok.ToString; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import javax.swing.*; 17 | import java.util.Optional; 18 | 19 | /** 20 | * A PsiElement that carrying a source object. 21 | *

22 | * Created by Completion for each LookupElement, useful for InsertHandler, Documentation, etc. 23 | */ 24 | @ToString(of = "source") 25 | public class SourceContainer extends LightElement { 26 | private final Object source; 27 | 28 | 29 | SourceContainer(@NotNull MetadataItem metadata, @NotNull Project project) { 30 | this(metadata, PsiManager.getInstance(project)); 31 | } 32 | 33 | 34 | SourceContainer(@NotNull Hint metadata, @NotNull Project project) { 35 | this(metadata, PsiManager.getInstance(project)); 36 | } 37 | 38 | 39 | private SourceContainer(@NotNull Object metadata, @NotNull PsiManager psiManager) { 40 | super(psiManager, Language.ANY); 41 | this.source = metadata; 42 | assert metadata instanceof MetadataItem || metadata instanceof Hint; 43 | } 44 | 45 | 46 | @Override 47 | public ItemPresentation getPresentation() { 48 | return new ItemPresentation() { 49 | @Nullable 50 | @Override 51 | public @NlsSafe String getPresentableText() { 52 | return getSourceMetadataItem().map(MetadataItem::getNameStr).orElseGet(() -> 53 | getSourceHint().map(Hint::value).orElse("")); 54 | } 55 | 56 | 57 | @Override 58 | public @Nullable Icon getIcon(boolean unused) { 59 | return getSourceMetadataItem().map(MetadataItem::getIcon).map(Pair::getSecond).orElseGet(() -> 60 | getSourceHint().map(Hint::icon).orElse(null)); 61 | } 62 | }; 63 | } 64 | 65 | 66 | public Optional getSourceMetadataItem() { 67 | return source instanceof MetadataItem mi ? Optional.of(mi) : Optional.empty(); 68 | } 69 | 70 | 71 | public Optional getSourceHint() { 72 | return source instanceof Hint h ? Optional.of(h) : Optional.empty(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/inspection/yaml/KeyNotDefinedInspection.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.inspection.yaml; 2 | 3 | import com.intellij.codeInspection.ProblemsHolder; 4 | import com.intellij.openapi.module.Module; 5 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.NameTreeNode; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.jetbrains.yaml.YAMLBundle; 12 | import org.jetbrains.yaml.YAMLUtil; 13 | import org.jetbrains.yaml.psi.YAMLAlias; 14 | import org.jetbrains.yaml.psi.YAMLKeyValue; 15 | import org.jetbrains.yaml.psi.YAMLMapping; 16 | import org.jetbrains.yaml.psi.YAMLScalar; 17 | import org.jetbrains.yaml.psi.YAMLSequence; 18 | import org.jetbrains.yaml.psi.YAMLValue; 19 | 20 | 21 | public class KeyNotDefinedInspection extends YamlInspectionBase { 22 | @Override 23 | protected void visitKeyValue( 24 | @NotNull Module module, @NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder, boolean isOnTheFly 25 | ) { 26 | if (keyValue.getKey() == null) return; 27 | ModuleMetadataService service = module.getService(ModuleMetadataService.class); 28 | String fullName = YAMLUtil.getConfigFullName(keyValue); 29 | if (StringUtils.isBlank(fullName)) return; 30 | YAMLValue value = keyValue.getValue(); 31 | if (value instanceof YAMLScalar || value instanceof YAMLSequence || value == null) { 32 | MetadataProperty property = service.getIndex().getProperty(fullName); 33 | if (property != null) return; 34 | } else if (value instanceof YAMLMapping) { 35 | NameTreeNode tn = service.getIndex().findInNameTrie(fullName); 36 | if (tn != null) return; 37 | } else if (value instanceof YAMLAlias) { 38 | return; //We do not support alias for now 39 | } 40 | // Property is not defined, but maybe its parent has a Map or Properties type. 41 | @Nullable MetadataProperty property = service.getIndex().getNearestParentProperty(fullName); 42 | if (property == null || !property.canBind(fullName)) { 43 | registerProblem(keyValue, holder); 44 | } 45 | } 46 | 47 | 48 | private static void registerProblem(@NotNull YAMLKeyValue keyValue, @NotNull ProblemsHolder holder) { 49 | holder.registerProblem( 50 | keyValue.getKey(), 51 | YAMLBundle.message("YamlUnknownKeysInspectionBase.unknown.key", keyValue.getKeyText()) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": [ 3 | { 4 | "name": "custom.choice.nodefault", 5 | "values": [ 6 | { 7 | "value": "a", 8 | "description": "custom a" 9 | }, 10 | { 11 | "value": "b" 12 | } 13 | ] 14 | }, 15 | { 16 | "name": "custom.choice.with.default", 17 | "values": [ 18 | { 19 | "value": "a", 20 | "description": "custom a" 21 | }, 22 | { 23 | "value": "b" 24 | } 25 | ] 26 | } 27 | ], 28 | "groups": [ 29 | { 30 | "sourceType": "com.acme.Custom", 31 | "name": "custom", 32 | "type": "com.acme.Custom", 33 | "description": "Custom group long long long long long long description" 34 | } 35 | ], 36 | "properties": [ 37 | { 38 | "deprecated": true, 39 | "name": "custom.deprecated.warning", 40 | "type": "java.lang.String", 41 | "deprecation": { 42 | "level": "warning", 43 | "replacement": "custom.replacement" 44 | } 45 | }, 46 | { 47 | "deprecated": true, 48 | "name": "custom.deprecated.autowarning", 49 | "type": "java.lang.String", 50 | "deprecation": { 51 | "replacement": "custom.replacement" 52 | } 53 | }, 54 | { 55 | "deprecated": true, 56 | "name": "custom.deprecated.error", 57 | "type": "com.acme.CustomProperty", 58 | "description": "Should never be shown to anyone", 59 | "deprecation": { 60 | "level": "error" 61 | } 62 | }, 63 | { 64 | "defaultValue": "fresh", 65 | "name": "custom.replacement", 66 | "description": "I'm new, why dont you use me instead of deprecated property", 67 | "type": "java.lang.String" 68 | }, 69 | { 70 | "defaultValue": "defaultvalue", 71 | "name": "custom.choice.with.default", 72 | "description": "Custom choice with default", 73 | "type": "java.lang.String" 74 | }, 75 | { 76 | "name": "custom.choice.nodefault", 77 | "description": "No default present", 78 | "type": "java.lang.String" 79 | }, 80 | { 81 | "name": "custom.choice.values.from.default", 82 | "description": "With choices", 83 | "type": "java.lang.String", 84 | "defaultValue": [ 85 | "val1", 86 | "val2" 87 | ] 88 | }, 89 | { 90 | "name": "newgroup.property.withdescription", 91 | "description": "I have description", 92 | "type": "java.lang.String" 93 | }, 94 | { 95 | "name": "newgroup.property.nodesription", 96 | "type": "java.lang.Boolean" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /plugin-test/gradle/multi-module/module2-additional-metadata/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": [ 3 | { 4 | "name": "custom.choice.nodefault", 5 | "values": [ 6 | { 7 | "value": "a", 8 | "description": "custom a" 9 | }, 10 | { 11 | "value": "b" 12 | } 13 | ] 14 | }, 15 | { 16 | "name": "custom.choice.with.default", 17 | "values": [ 18 | { 19 | "value": "a", 20 | "description": "custom a" 21 | }, 22 | { 23 | "value": "b" 24 | } 25 | ] 26 | } 27 | ], 28 | "groups": [ 29 | { 30 | "sourceType": "com.acme.Custom", 31 | "name": "custom", 32 | "type": "com.acme.Custom", 33 | "description": "Custom group long long long long long long description" 34 | } 35 | ], 36 | "properties": [ 37 | { 38 | "deprecated": true, 39 | "name": "custom.deprecated.warning", 40 | "type": "java.lang.String", 41 | "deprecation": { 42 | "level": "warning", 43 | "replacement": "custom.replacement" 44 | } 45 | }, 46 | { 47 | "deprecated": true, 48 | "name": "custom.deprecated.autowarning", 49 | "type": "java.lang.String", 50 | "deprecation": { 51 | "replacement": "custom.replacement" 52 | } 53 | }, 54 | { 55 | "deprecated": true, 56 | "name": "custom.deprecated.error", 57 | "type": "com.acme.CustomProperty", 58 | "description": "Should never be shown to anyone", 59 | "deprecation": { 60 | "level": "error" 61 | } 62 | }, 63 | { 64 | "defaultValue": "fresh", 65 | "name": "custom.replacement", 66 | "description": "I'm new, why dont you use me instead of deprecated property", 67 | "type": "java.lang.String" 68 | }, 69 | { 70 | "defaultValue": "defaultvalue", 71 | "name": "custom.choice.with.default", 72 | "description": "Custom choice with default", 73 | "type": "java.lang.String" 74 | }, 75 | { 76 | "name": "custom.choice.nodefault", 77 | "description": "No default present", 78 | "type": "java.lang.String" 79 | }, 80 | { 81 | "name": "custom.choice.values.from.default", 82 | "description": "With choices", 83 | "type": "java.lang.String", 84 | "defaultValue": [ 85 | "val1", 86 | "val2" 87 | ] 88 | }, 89 | { 90 | "name": "newgroup.property.withdescription", 91 | "description": "I have description", 92 | "type": "java.lang.String" 93 | }, 94 | { 95 | "name": "newgroup.property.nodesription", 96 | "type": "java.lang.Boolean" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/documentation/YamlDocumentationProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.documentation; 2 | 3 | import com.intellij.lang.documentation.AbstractDocumentationProvider; 4 | import com.intellij.openapi.module.Module; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.util.PsiTreeUtil; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.SourceContainer; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.documentation.service.DocumentationService; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataItem; 11 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.service.ModuleMetadataService; 12 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiElementUtils; 13 | import org.jetbrains.annotations.Nls; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | import org.jetbrains.yaml.YAMLUtil; 17 | import org.jetbrains.yaml.psi.YAMLKeyValue; 18 | 19 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement; 20 | 21 | public class YamlDocumentationProvider extends AbstractDocumentationProvider { 22 | @Nullable 23 | @Nls 24 | @Override 25 | public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { 26 | if (element instanceof SourceContainer ce) { 27 | return generateDocumentHtml(ce); 28 | } 29 | 30 | if (originalElement != null) element = originalElement; 31 | if (!PsiElementUtils.isInFileOfType(element, SpringBootConfigurationYamlFileType.INSTANCE)) { 32 | return null; 33 | } 34 | Module module = findModuleForPsiElement(element); 35 | if (module == null) { 36 | return null; 37 | } 38 | // Find context YAMLKeyValue, stop if context is not at the same line. 39 | YAMLKeyValue keyValue = PsiTreeUtil.getContextOfType(element, false, YAMLKeyValue.class); 40 | if (keyValue == null) return null; 41 | if (!YAMLUtil.psiAreAtTheSameLine(element, keyValue)) return null; 42 | 43 | String propertyName = YAMLUtil.getConfigFullName(keyValue); 44 | ModuleMetadataService service = module.getService(ModuleMetadataService.class); 45 | @Nullable MetadataItem propertyOrGroup = service.getIndex().getPropertyOrGroup(propertyName); 46 | if (propertyOrGroup == null) return null; 47 | 48 | return DocumentationService.getInstance(module.getProject()).generateDoc(propertyOrGroup); 49 | } 50 | 51 | 52 | @NotNull 53 | private String generateDocumentHtml(SourceContainer sc) { 54 | DocumentationService docSvc = DocumentationService.getInstance(sc.getProject()); 55 | return sc.getSourceMetadataItem().map(docSvc::generateDoc).orElseGet(() -> 56 | sc.getSourceHint().map(docSvc::generateDoc).orElseThrow()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/AbstractValueProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.PrefixMatcher; 5 | import com.intellij.openapi.module.Module; 6 | import com.intellij.openapi.module.ModuleUtilCore; 7 | import com.intellij.openapi.project.Project; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 9 | import org.jetbrains.annotations.Contract; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | import org.springframework.boot.convert.ApplicationConversionService; 13 | import org.springframework.core.convert.ConversionService; 14 | 15 | import java.util.Map; 16 | import java.util.Objects; 17 | 18 | abstract class AbstractValueProvider implements ValueProvider { 19 | private static final ConversionService conversionService = ApplicationConversionService.getSharedInstance(); 20 | private final ConfigurationMetadata.Hint.ValueProvider metadata; 21 | 22 | 23 | AbstractValueProvider(ConfigurationMetadata.Hint.ValueProvider metadata) { 24 | this.metadata = metadata; 25 | } 26 | 27 | 28 | @Override 29 | public ConfigurationMetadata.Hint.ValueProvider getMetadata() { 30 | return this.metadata; 31 | } 32 | 33 | 34 | @NotNull 35 | protected PrefixMatcher getPrefixMatcher(@Nullable PrefixMatcher prefixMatcher) { 36 | return Objects.requireNonNullElse(prefixMatcher, PrefixMatcher.ALWAYS_TRUE); 37 | } 38 | 39 | 40 | @NotNull 41 | protected Project getProject(@NotNull CompletionParameters completionParameters) { 42 | return completionParameters.getPosition().getProject(); 43 | } 44 | 45 | 46 | @NotNull 47 | protected Module getModule(@NotNull CompletionParameters completionParameters) { 48 | return Objects.requireNonNull(ModuleUtilCore.findModuleForPsiElement(completionParameters.getPosition())); 49 | } 50 | 51 | 52 | @Contract("_,_,!null->!null") 53 | @Nullable 54 | protected T getParameter(String key, Class type, @Nullable T defaultValue) { 55 | Map parameters = metadata.getParameters(); 56 | if (parameters != null) { 57 | Object value = parameters.get(key); 58 | if (value == null) return defaultValue; 59 | if (conversionService.canConvert(value.getClass(), type)) { 60 | return conversionService.convert(value, type); 61 | } else { 62 | throw new IllegalArgumentException("Cannot convert value [" + value + "] to required type: " + type); 63 | } 64 | } else { 65 | return defaultValue; 66 | } 67 | } 68 | 69 | 70 | protected T getRequiredParameter(String key, Class type) { 71 | return Objects.requireNonNull(getParameter(key, type, null), 72 | "Parameter " + key + " is mandatory"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataIndex.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public interface MetadataIndex { 13 | static MetadataIndex empty(Project project) { 14 | return new Empty(project); 15 | } 16 | 17 | boolean isEmpty(); 18 | 19 | @NotNull Project project(); 20 | 21 | /** 22 | * Source file url or source type FQN, maybe empty string. 23 | */ 24 | @NotNull List getSource(); 25 | 26 | @NotNull Map getGroups(); 27 | 28 | @NotNull Map getProperties(); 29 | 30 | @NotNull Map getHints(); 31 | 32 | @Nullable MetadataGroup getGroup(String name); 33 | 34 | @Nullable MetadataProperty getProperty(String name); 35 | 36 | @Nullable MetadataProperty getNearestParentProperty(String name); 37 | 38 | @Nullable MetadataHint getHint(String name); 39 | 40 | @Nullable MetadataItem getPropertyOrGroup(String name); 41 | 42 | @Nullable NameTreeNode findInNameTrie(String prefix); 43 | 44 | //region empty implement 45 | record Empty(Project project) implements MetadataIndex { 46 | @Override 47 | public boolean isEmpty() { 48 | return true; 49 | } 50 | 51 | 52 | @Override 53 | public @NotNull Project project() { 54 | return project; 55 | } 56 | 57 | 58 | @Override 59 | public @NotNull List getSource() { 60 | return Collections.emptyList(); 61 | } 62 | 63 | 64 | @Override 65 | public @Nullable MetadataGroup getGroup(String name) { 66 | return null; 67 | } 68 | 69 | 70 | @Override 71 | public MetadataProperty getProperty(String name) { 72 | return null; 73 | } 74 | 75 | 76 | @Override 77 | public MetadataProperty getNearestParentProperty(String name) { 78 | return null; 79 | } 80 | 81 | 82 | @Override 83 | public MetadataHint getHint(String name) { 84 | return null; 85 | } 86 | 87 | 88 | @Override 89 | public @NotNull Map getGroups() { 90 | return Map.of(); 91 | } 92 | 93 | 94 | @Override 95 | public @NotNull Map getProperties() { 96 | return Map.of(); 97 | } 98 | 99 | 100 | @Override 101 | public @NotNull Map getHints() { 102 | return Map.of(); 103 | } 104 | 105 | 106 | @Override 107 | public MetadataItem getPropertyOrGroup(String name) { 108 | return null; 109 | } 110 | 111 | 112 | @Override 113 | public @Nullable NameTreeNode findInNameTrie(String prefix) { 114 | return null; 115 | } 116 | } 117 | //endregion 118 | } 119 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.acme 7 | multi-module 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | multi-module 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.M6 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Finchley.M4 26 | 27 | 28 | 29 | child1 30 | child2 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-devtools 37 | test 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.springframework.cloud 45 | spring-cloud-dependencies 46 | ${spring-cloud.version} 47 | pom 48 | import 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-maven-plugin 58 | 59 | 60 | 61 | 62 | 63 | 64 | spring-snapshots 65 | Spring Snapshots 66 | https://repo.spring.io/snapshot 67 | 68 | true 69 | 70 | 71 | 72 | spring-milestones 73 | Spring Milestones 74 | https://repo.spring.io/milestone 75 | 76 | false 77 | 78 | 79 | 80 | 81 | 82 | 83 | spring-snapshots 84 | Spring Snapshots 85 | https://repo.spring.io/snapshot 86 | 87 | true 88 | 89 | 90 | 91 | spring-milestones 92 | Spring Milestones 93 | https://repo.spring.io/milestone 94 | 95 | false 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /plugin-test/maven/multi-module/child2/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | com.acme 8 | multi-module 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | child2 13 | child2 14 | Child2 15 | 16 | 17 | UTF-8 18 | UTF-8 19 | 1.8 20 | Finchley.M4 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-configuration-processor 31 | true 32 | 33 | 34 | org.projectlombok 35 | lombok 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-maven-plugin 45 | 46 | 47 | 48 | 49 | 50 | 51 | spring-snapshots 52 | Spring Snapshots 53 | https://repo.spring.io/snapshot 54 | 55 | true 56 | 57 | 58 | 59 | spring-milestones 60 | Spring Milestones 61 | https://repo.spring.io/milestone 62 | 63 | false 64 | 65 | 66 | 67 | 68 | 69 | 70 | spring-snapshots 71 | Spring Snapshots 72 | https://repo.spring.io/snapshot 73 | 74 | true 75 | 76 | 77 | 78 | spring-milestones 79 | Spring Milestones 80 | https://repo.spring.io/milestone 81 | 82 | false 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /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/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/hint/provider/ClassReferenceValueProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.provider; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.JavaInheritorsGetter; 5 | import com.intellij.codeInsight.completion.PrefixMatcher; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.psi.PsiClass; 8 | import com.intellij.psi.PsiClassType; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.hint.Hint; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 11 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils; 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | /** 23 | * @see ConfigurationMetadata.Hint.ValueProvider.Type#CLASS_REFERENCE 24 | */ 25 | public class ClassReferenceValueProvider extends AbstractValueProvider { 26 | private final String targetFQN; 27 | /** 28 | * Specify whether only concrete classes are to be considered as valid candidates. Default: true. 29 | */ 30 | private final boolean concrete; 31 | 32 | 33 | ClassReferenceValueProvider(ConfigurationMetadata.Hint.ValueProvider metadata) { 34 | super(metadata); 35 | 36 | this.targetFQN = getParameter("target", String.class, null); 37 | this.concrete = getParameter("concrete", Boolean.class, true); 38 | } 39 | 40 | 41 | /** 42 | * The fully qualified name of the class that should be assignable to the chosen value. 43 | * Typically used to filter out non-candidate classes. 44 | * Note that this information can be provided by the type itself by exposing a class with the appropriate upper bound. 45 | */ 46 | public Optional getUpperBoundClass(Project project) { 47 | return Optional.ofNullable(targetFQN) 48 | .map(fqn -> PsiTypeUtils.getJavaTypeByName(project, fqn)); 49 | } 50 | 51 | 52 | @Override 53 | public Collection provideValues( 54 | @NotNull CompletionParameters completionParameters, @Nullable PrefixMatcher prefixMatcher 55 | ) { 56 | return getUpperBoundClass(completionParameters.getEditor().getProject()).map(baseClass -> { 57 | List values = new ArrayList<>(); 58 | // We do not give the specified prefixMatcher to JavaInheritorsGetter, because it will use it with the simple name 59 | // of the class, that will filter out too many candidates we need. 60 | JavaInheritorsGetter.processInheritors(completionParameters, Collections.singleton(baseClass), 61 | PrefixMatcher.ALWAYS_TRUE, 62 | t -> { 63 | PsiClass c = PsiTypeUtils.resolveClassInType(t); 64 | if (c != null) { 65 | if (this.concrete && !PsiTypeUtils.isConcrete(t)) return; 66 | String name = c.getQualifiedName(); 67 | if (StringUtils.isBlank(name)) return; 68 | if (prefixMatcher != null && !prefixMatcher.prefixMatches(name)) return; 69 | values.add(new Hint(name, c)); 70 | } 71 | }); 72 | return values; 73 | }).orElseGet(Collections::emptyList); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/service/MetadataFileContainer.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.service; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.AggregatedMetadataIndex; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.ConfigurationMetadataIndex; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.FileMetadataSource; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataIndex; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.index.MetadataProperty; 11 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 12 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.MutableReference; 13 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.annotations.Nullable; 16 | 17 | import java.io.IOException; 18 | import java.util.Optional; 19 | 20 | /** 21 | * A container of a loaded metadata file, can auto-reload while the file has changed or removed. 22 | */ 23 | class MetadataFileContainer implements MutableReference { 24 | private static final Logger LOG = Logger.getInstance(ProjectMetadataService.class); 25 | @NotNull private final FileMetadataSource source; 26 | @NotNull private final Project project; 27 | private MetadataIndex metadata; 28 | 29 | 30 | MetadataFileContainer(@NotNull VirtualFile metadataFile, @NotNull Project project) { 31 | this.source = new FileMetadataSource(metadataFile); 32 | this.project = project; 33 | refresh(); 34 | } 35 | 36 | 37 | @Override 38 | public @Nullable MetadataIndex dereference() { 39 | refresh(); 40 | return this.metadata; 41 | } 42 | 43 | 44 | @Override 45 | public synchronized void refresh() { 46 | if (!this.source.isValid()) { 47 | if (!this.source.tryReloadIfInvalid()) { 48 | this.metadata = null; 49 | return; 50 | } 51 | } else if (!this.source.isChanged()) { 52 | return; 53 | } 54 | try { 55 | AggregatedMetadataIndex index = new AggregatedMetadataIndex( 56 | new ConfigurationMetadataIndex(this.source, this.project)); 57 | // Spring does not create metadata for types in collections, we should create it by ourselves and expand our index, 58 | // to better support code-completion, documentation, navigation, etc. 59 | for (MetadataProperty property : index.getProperties().values()) { 60 | resolvePropertyType(property).ifPresent(index::addFirst); 61 | } 62 | this.metadata = index; 63 | } catch (IOException e) { 64 | LOG.warn("Read metadata file " + this.source.getPresentation() + " failed", e); 65 | } 66 | } 67 | 68 | 69 | /** 70 | * @see ConfigurationMetadata.Property#getType() 71 | */ 72 | @NotNull 73 | private Optional resolvePropertyType(@NotNull MetadataProperty property) { 74 | return property.getFullType().filter(t -> PsiTypeUtils.isCollectionOrMap(project, t)) 75 | .flatMap(t -> project.getService(ProjectClassMetadataService.class).getMetadata(property.getNameStr(), t)); 76 | } 77 | 78 | 79 | @Override 80 | public String toString() { 81 | return "Metadata index form " + this.source.getPresentation(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/properties/PropertiesCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.properties; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.CompletionProvider; 5 | import com.intellij.codeInsight.completion.CompletionResultSet; 6 | import com.intellij.lang.properties.psi.Property; 7 | import com.intellij.openapi.application.ReadAction; 8 | import com.intellij.openapi.module.Module; 9 | import com.intellij.openapi.project.DumbModeBlockedFunctionality; 10 | import com.intellij.openapi.project.DumbService; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.psi.PsiComment; 13 | import com.intellij.psi.PsiElement; 14 | import com.intellij.psi.util.PsiTreeUtil; 15 | import com.intellij.util.ProcessingContext; 16 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.CompletionService; 17 | import org.apache.commons.lang3.StringUtils; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.annotations.Nullable; 20 | 21 | import static com.intellij.codeInsight.completion.CompletionUtil.DUMMY_IDENTIFIER_TRIMMED; 22 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement; 23 | 24 | class PropertiesCompletionProvider extends CompletionProvider { 25 | @Override 26 | protected void addCompletions( 27 | @NotNull CompletionParameters completionParameters, 28 | @NotNull ProcessingContext processingContext, 29 | @NotNull CompletionResultSet resultSet 30 | ) { 31 | PsiElement element = completionParameters.getPosition(); 32 | if (element instanceof PsiComment) return; 33 | 34 | Project project = element.getProject(); 35 | if (ReadAction.compute(() -> DumbService.isDumb(project))) { 36 | DumbService.getInstance(project).showDumbModeNotificationForFunctionality( 37 | "Spring configuration completion", DumbModeBlockedFunctionality.CodeCompletion); 38 | return; 39 | } 40 | Module module = findModuleForPsiElement(element); 41 | if (module == null) return; 42 | 43 | // Find context YAMLPsiElement, stop if context is not at the same line. 44 | @Nullable Property context = PsiTreeUtil.getParentOfType(element, Property.class, false); 45 | if (context == null) return; 46 | 47 | String originKey = context.getUnescapedKey(); 48 | String originValue = context.getUnescapedValue(); 49 | 50 | CompletionService service = CompletionService.getInstance(project); 51 | if (originKey != null && originKey.contains(DUMMY_IDENTIFIER_TRIMMED)) { 52 | // User is asking completion for property key 53 | //TODO Map key completion 54 | String queryString = StringUtils.truncate(originKey, originKey.indexOf(DUMMY_IDENTIFIER_TRIMMED)); 55 | if (service.findSuggestionForKey(completionParameters, resultSet, "", queryString, 56 | PropertiesKeyInsertHandler.INSTANCE)) { 57 | resultSet.stopHere(); 58 | } 59 | } else if (originValue != null && originValue.contains(DUMMY_IDENTIFIER_TRIMMED)) { 60 | // Value completion 61 | String queryString = StringUtils.truncate(originValue, originValue.indexOf(DUMMY_IDENTIFIER_TRIMMED)); 62 | if (StringUtils.isBlank(originKey)) return; 63 | if (service.findSuggestionForValue(completionParameters, resultSet, originKey, queryString, 64 | PropertiesValueInsertHandler.INSTANCE)) { 65 | resultSet.stopHere(); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin-test/maven/single-module/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | single-module 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | single-module 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.M6 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | Finchley.M4 26 | 27 | 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-starter-gateway 32 | 33 | 34 | org.springframework.cloud 35 | spring-cloud-starter-netflix-hystrix 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-test 41 | test 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-devtools 47 | test 48 | 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.cloud 55 | spring-cloud-dependencies 56 | ${spring-cloud.version} 57 | pom 58 | import 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | 70 | 71 | 72 | 73 | 74 | spring-snapshots 75 | Spring Snapshots 76 | https://repo.spring.io/snapshot 77 | 78 | true 79 | 80 | 81 | 82 | spring-milestones 83 | Spring Milestones 84 | https://repo.spring.io/milestone 85 | 86 | false 87 | 88 | 89 | 90 | 91 | 92 | 93 | spring-snapshots 94 | Spring Snapshots 95 | https://repo.spring.io/snapshot 96 | 97 | true 98 | 99 | 100 | 101 | spring-milestones 102 | Spring Milestones 103 | https://repo.spring.io/milestone 104 | 105 | false 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/editing/YamlJoinLinesHandler.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.editing; 2 | 3 | import com.intellij.codeInsight.editorActions.JoinRawLinesHandlerDelegate; 4 | import com.intellij.openapi.editor.Document; 5 | import com.intellij.openapi.fileTypes.FileTypeManager; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.psi.PsiDocumentManager; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.PsiFile; 10 | import com.intellij.psi.codeStyle.CodeStyleManager; 11 | import com.intellij.psi.util.PsiTreeUtil; 12 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.yaml.YAMLTokenTypes; 15 | import org.jetbrains.yaml.psi.YAMLKeyValue; 16 | 17 | import java.util.Collections; 18 | 19 | /** 20 | *

{@code
21 |  * server:
22 |  *   port: 8080
23 |  * ==>
24 |  * spring.port: 8080
25 |  * }
26 | */ 27 | public class YamlJoinLinesHandler implements JoinRawLinesHandlerDelegate { 28 | @Override 29 | public int tryJoinLines(@NotNull Document document, @NotNull PsiFile file, int start, int end) { 30 | return CANNOT_JOIN; 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | * 36 | * @param start offset right after the last non-space char of first line; 37 | * @param end offset of first non-space char since the next line. 38 | */ 39 | @Override 40 | public int tryJoinRawLines(@NotNull Document document, @NotNull PsiFile file, int start, int end) { 41 | // Take effects only in Spring YAML Configuration files. 42 | VirtualFile virtualFile = file.getVirtualFile(); 43 | if (virtualFile == null) return CANNOT_JOIN; 44 | FileTypeManager ftm = FileTypeManager.getInstance(); 45 | if (!ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE)) return CANNOT_JOIN; 46 | // Take effects only when first line represents a key 47 | PsiElement elementAtStartLineEnd = file.findElementAt(start - 1); 48 | if (elementAtStartLineEnd == null || !YAMLTokenTypes.COLON.equals(elementAtStartLineEnd.getNode().getElementType())) 49 | return CANNOT_JOIN; 50 | // Take effects only when next line represents a key 51 | PsiElement elementAtEndLineStart = file.findElementAt(end); 52 | if (elementAtEndLineStart == null || 53 | !YAMLTokenTypes.SCALAR_KEY.equals(elementAtEndLineStart.getNode().getElementType())) { 54 | return CANNOT_JOIN; 55 | } 56 | // Take effects only when the key at next line is child of first line 57 | YAMLKeyValue parentKeyValue = PsiTreeUtil.getParentOfType(elementAtStartLineEnd, YAMLKeyValue.class); 58 | if (parentKeyValue == null) return CANNOT_JOIN; 59 | YAMLKeyValue childKeyValue = PsiTreeUtil.getParentOfType(elementAtEndLineStart, YAMLKeyValue.class); 60 | if (childKeyValue == null) return CANNOT_JOIN; 61 | if (!PsiTreeUtil.isAncestor(parentKeyValue, childKeyValue, false)) { 62 | return CANNOT_JOIN; 63 | } 64 | 65 | // Join parent and child key 66 | start = elementAtStartLineEnd.getTextOffset(); 67 | document.replaceString(start, end, "."); 68 | 69 | // Reformat joined key value 70 | PsiDocumentManager pdm = PsiDocumentManager.getInstance(file.getProject()); 71 | pdm.commitDocument(document); 72 | YAMLKeyValue joinedKeyValue = PsiTreeUtil.getParentOfType(file.findElementAt(start), YAMLKeyValue.class); 73 | if (joinedKeyValue != null) { 74 | CodeStyleManager csm = CodeStyleManager.getInstance(joinedKeyValue.getManager()); 75 | csm.reformatText(file, Collections.singletonList(joinedKeyValue.getTextRange())); 76 | pdm.doPostponedOperationsAndUnblockDocument(document); 77 | } 78 | 79 | return start; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | # Completion test 2 | spring: 3 | jpa.hibernate: 4 | # Completion: hint-values 5 | ddl-auto: 'create-drop' 6 | data.cassandra.request.throttler: 7 | # Completion: enum 8 | type: CONCURRENCY_LIMITING 9 | datasource: 10 | # Completion: class-reference 11 | driver-class-name: org.h2.Driver 12 | # Completion: List 13 | sql.init.data-locations: 14 | - 'classpath:banner.txt' 15 | - 'classpath:db/change-log/master.yaml' 16 | - 'classpath*:application.yaml' 17 | # Completion: handle-as org.springframework.core.io.Resource 18 | liquibase.change-log: 'classpath:banner.txt' 19 | # Completion: handle-as org.springframework.util.MimeType 20 | freemarker.content-type: application/json 21 | # Completion: handle-as Charset 22 | server: 23 | servlet.encoding.charset: 'UTF-8' 24 | tomcat.accesslog: 25 | encoding: 'IBM-Thai' 26 | # Completion: handle-as Locale 27 | locale: en_US 28 | logging: 29 | group: 30 | app: 31 | - 'dev.flikas' 32 | spring: 33 | - 'org.springframework' 34 | level: 35 | root: info 36 | org.springframework: debug 37 | --- 38 | # In project metadata 39 | oo: 40 | b: c 41 | example: 42 | server: 43 | address: 44 | port: 8080 45 | value: 123 46 | my: 47 | keys1: 48 | - foo 49 | - bar 50 | jobs: 51 | key: 52 | name: 12 # Needs Lombok plugin, key from Map 53 | cron: xxf # Needs Lombok plugin, key from Map 54 | --- 55 | # Inspection:KeyNotDefined(positive) 56 | logging.level: 57 | # hint: enum 58 | root: debug 59 | # hint: logger-name 60 | org.your-company: info 61 | resilience4j.circuitbreaker.instances: 62 | "backendA": 63 | # In Type: Map 64 | allowHealthIndicatorToFail: false 65 | server: 66 | servlet.encoding: 67 | "mapping": 68 | # In Type: Map 69 | "en-US": "UTF-8" 70 | # For Type: List 71 | jetty.accesslog.ignore-paths: 72 | - p1 73 | - p2 74 | - p3 75 | spring: 76 | profiles: 77 | # Pure key 78 | active: dev 79 | datasource.hikari.data-source-properties: 80 | # For Type: java.util.Properties 81 | any-property: should-be-valid 82 | # For Type: java.util.Properties, can map any depth of keys 83 | jpa.properties: 84 | hibernate: 85 | archive: 86 | # Inspection::KeyNotDefined(positive) 87 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner 88 | --- 89 | #Inspection::InvalidValue(positive) 90 | # Type: Charset 91 | logging.charset.console: utf-8 92 | server: 93 | # Type: List 94 | jetty.accesslog.ignore-paths: 95 | - p1 96 | - p2 97 | - p3 98 | tomcat: 99 | # Type: int 100 | accept-count: 1 101 | # Type: Duration 102 | connection-timeout: 3m 103 | # variable substitute 104 | threads.max: ${spring.cloud-version} 105 | access-log: 106 | # Type: boolean 107 | buffered: false 108 | spring: 109 | # Type org.springframework.core.io.Resource 110 | banner.location: 'classpath:banner.txt' 111 | profiles: 112 | # Type: List -- String 113 | active: dev 114 | # Type: java.util.Properties 115 | datasource.hikari.data-source-properties: 116 | any-property: should-be-valid 117 | # Type: Map -- any depth of keys 118 | jpa.properties: 119 | hibernate: 120 | archive: 121 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner 122 | --- 123 | # Inspection::Deprecated(positive) 124 | # Deprecated property but available sub-property 125 | spring: 126 | profiles: 127 | active: dev 128 | -------------------------------------------------------------------------------- /plugin-test/maven/spring-boot-example/demo-app/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | # Positive test cases, here should no warnings -----------------> 2 | 3 | # Completion test 4 | spring: 5 | # Completion: type boolean 6 | devtools.add-properties: true 7 | jpa.hibernate: 8 | # Completion: hint-values 9 | ddl-auto: 'create-drop' 10 | # Completion: type enum 11 | data.cassandra.request.throttler.type: CONCURRENCY_LIMITING 12 | datasource: 13 | # Completion: class-reference 14 | driver-class-name: org.h2.Driver 15 | # Completion: List 16 | sql.init.data-locations: 17 | - 'classpath:banner.txt' 18 | - 'classpath:db/change-log/master.yaml' 19 | # Completion: handle-as org.springframework.core.io.Resource 20 | liquibase.change-log: 'classpath:banner.txt' 21 | # Completion: handle-as org.springframework.util.MimeType 22 | freemarker.content-type: application/json 23 | # Completion: handle-as Charset 24 | server: 25 | servlet.encoding.charset: 'UTF-8' 26 | tomcat.accesslog: 27 | encoding: 'IBM-Thai' 28 | # Completion: handle-as Locale 29 | locale: en_US 30 | logging: 31 | group: 32 | app: 33 | - 'dev.flikas' 34 | spring: 35 | - 'org.springframework' 36 | level: 37 | root: info 38 | org.springframework: debug 39 | --- 40 | # In project metadata 41 | oo: 42 | b: c 43 | example: 44 | server: 45 | address: 46 | port: 8080 47 | value: 123 48 | my: 49 | keys1: 50 | - foo 51 | - bar 52 | jobs: 53 | key: 54 | name: 12 # Needs Lombok plugin, key from Map 55 | cron: xxf # Needs Lombok plugin, key from Map 56 | --- 57 | # Inspection:KeyNotDefined(positive) 58 | logging.level: 59 | # hint: enum 60 | root: debug 61 | # hint: logger-name 62 | org.your-company: info 63 | resilience4j.circuitbreaker.instances: 64 | "backendA": 65 | # In Type: Map 66 | allowHealthIndicatorToFail: false 67 | server: 68 | servlet.encoding: 69 | "mapping": 70 | # In Type: Map 71 | "en-US": "UTF-8" 72 | # For Type: List 73 | jetty.accesslog.ignore-paths: 74 | - p1 75 | - p2 76 | - p3 77 | spring: 78 | profiles: 79 | # Pure key 80 | active: dev 81 | datasource.hikari.data-source-properties: 82 | # For Type: java.util.Properties 83 | any-property: should-be-valid 84 | # For Type: java.util.Properties, can map any depth of keys 85 | jpa.properties: 86 | hibernate: 87 | archive: 88 | # Inspection::KeyNotDefined(positive) 89 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner 90 | --- 91 | #Inspection::InvalidValue(positive) 92 | # Type: Charset 93 | logging.charset.console: utf-8 94 | server: 95 | # Type: List 96 | jetty.accesslog.ignore-paths: 97 | - p1 98 | - p2 99 | - p3 100 | tomcat: 101 | # Type: int 102 | accept-count: 1 103 | # Type: Duration 104 | connection-timeout: 3m 105 | # variable substitute 106 | threads.max: ${spring.cloud-version} 107 | access-log: 108 | # Type: boolean 109 | buffered: false 110 | spring: 111 | # Type org.springframework.core.io.Resource 112 | banner.location: 'classpath:banner.txt' 113 | profiles: 114 | # Type: List -- String 115 | active: dev 116 | # Type: java.util.Properties 117 | datasource.hikari.data-source-properties: 118 | any-property: should-be-valid 119 | # Type: Map -- any depth of keys 120 | jpa.properties: 121 | hibernate: 122 | archive: 123 | scanner: org.hibernate.boot.archive.scan.internal.StandardScanner 124 | --- 125 | # Inspection::Deprecated(positive) 126 | # Deprecated property but available sub-property 127 | spring: 128 | profiles: 129 | active: dev 130 | -------------------------------------------------------------------------------- /plugin-test/gradle/single-project/src/main/java/com/acme/model/DynamicRoot.java: -------------------------------------------------------------------------------- 1 | package com.acme.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Collection; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | /** 12 | * Dynamic root documentation, Intentionally written so long that this should not fit into the suggestions. After previous dot, this section should not be visible in the documentation section 13 | */ 14 | public class DynamicRoot { 15 | 16 | public int invisiblePublicProperty; 17 | protected int invisibleProtectedProperty; 18 | int invisiblePackageScopedProperty; 19 | @Getter 20 | int invisibleGetterOnlyProperty; 21 | private int invisiblePrivateProperty; 22 | 23 | /** 24 | * Boolean documentation 25 | */ 26 | @Getter 27 | @Setter 28 | private boolean boolProp; 29 | /** 30 | * Byte documentation 31 | */ 32 | @Getter 33 | @Setter 34 | private byte byteProp; 35 | /** 36 | * Short documentation 37 | */ 38 | @Getter 39 | @Setter 40 | private short shortProp; 41 | /** 42 | * Int documentation 43 | */ 44 | @Getter 45 | @Setter 46 | private int intProp; 47 | /** 48 | * Long documentation 49 | */ 50 | @Getter 51 | @Setter 52 | private long longProp; 53 | /** 54 | * Float documentation 55 | */ 56 | @Getter 57 | @Setter 58 | private float floatProp; 59 | /** 60 | * Double documentation 61 | */ 62 | @Getter 63 | @Setter 64 | private double doubleProp; 65 | /** 66 | * Big decimal documentation 67 | */ 68 | @Getter 69 | @Setter 70 | private BigDecimal bigDecimalProp; 71 | /** 72 | * Char documentation 73 | */ 74 | @Getter 75 | @Setter 76 | private char charProp; 77 | /** 78 | * String documentation 79 | */ 80 | @Getter 81 | @Setter 82 | private String stringProp; 83 | /** 84 | * Enum documentation 85 | */ 86 | @Getter 87 | @Setter 88 | private DynamicEnum enumProp; 89 | /** 90 | * Primitive key -> value map documentation 91 | */ 92 | @Getter 93 | @Setter 94 | private Map primitiveKeyToPrimitiveValueMap; 95 | /** 96 | * Enum key -> primitive value documentation 97 | */ 98 | @Getter 99 | @Setter 100 | private Map enumKeyToPrimitiveValueMap; 101 | /** 102 | * Primitive collection documentation 103 | */ 104 | @Getter 105 | @Setter 106 | private Collection primitiveCollection; 107 | /** 108 | * Enum collection documentation 109 | */ 110 | @Getter 111 | @Setter 112 | private Collection enumCollection; 113 | /** 114 | * Dynamic child documentation 115 | */ 116 | @Getter 117 | @Setter 118 | private DynamicChild dynamicChild; 119 | /** 120 | * Primitive key -> dynamic child documentation 121 | */ 122 | @Getter 123 | @Setter 124 | private Map primitiveKeyToDynamicChildValueMap; 125 | /** 126 | * enum key -> dynamic child documentation 127 | */ 128 | @Getter 129 | @Setter 130 | private Map enumKeyToDynamicChildValueMap; 131 | /** 132 | * dynamic child collection documentation 133 | */ 134 | @Getter 135 | @Setter 136 | private Collection childCollection; 137 | 138 | private void setAnotherInvisiblePrivateProperty(int anotherInvisiblePrivateProperty) { 139 | // does not matter 140 | } 141 | 142 | protected void setAnotherInvisibleProtectedProperty(int anotherInvisibleProtectedProperty) { 143 | // does not matter 144 | } 145 | 146 | void setAnotherInvisiblePackageScopedProperty(int anotherInvisiblePackageScopedProperty) { 147 | // does not matter 148 | } 149 | 150 | public void setPropertyViaSetter(int propertyViaSetter) { 151 | // does not matter 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/editing/YamlSplitKeyProcessor.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.editing; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter; 5 | import com.intellij.openapi.actionSystem.DataContext; 6 | import com.intellij.openapi.editor.Document; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.editor.actionSystem.EditorActionHandler; 9 | import com.intellij.openapi.fileTypes.FileTypeManager; 10 | import com.intellij.openapi.util.Ref; 11 | import com.intellij.openapi.util.TextRange; 12 | import com.intellij.openapi.vfs.VirtualFile; 13 | import com.intellij.psi.PsiDocumentManager; 14 | import com.intellij.psi.PsiElement; 15 | import com.intellij.psi.PsiFile; 16 | import com.intellij.psi.impl.source.codeStyle.IndentHelperImpl; 17 | import com.intellij.psi.util.PsiTreeUtil; 18 | import dev.flikas.spring.boot.assistant.idea.plugin.filetype.SpringBootConfigurationYamlFileType; 19 | import org.jetbrains.annotations.NotNull; 20 | import org.jetbrains.yaml.YAMLTextUtil; 21 | import org.jetbrains.yaml.YAMLTokenTypes; 22 | import org.jetbrains.yaml.psi.YAMLCompoundValue; 23 | import org.jetbrains.yaml.psi.YAMLKeyValue; 24 | import org.jetbrains.yaml.psi.YAMLValue; 25 | 26 | import static com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result.Continue; 27 | 28 | public class YamlSplitKeyProcessor extends EnterHandlerDelegateAdapter { 29 | 30 | @Override 31 | public Result preprocessEnter(@NotNull PsiFile file, @NotNull Editor editor, @NotNull Ref caretOffset, 32 | @NotNull Ref caretAdvance, @NotNull DataContext dataContext, 33 | EditorActionHandler originalHandler) { 34 | VirtualFile virtualFile = file.getVirtualFile(); 35 | if (virtualFile == null) return Continue; 36 | FileTypeManager ftm = FileTypeManager.getInstance(); 37 | if (!ftm.isFileOfType(virtualFile, SpringBootConfigurationYamlFileType.INSTANCE)) 38 | return Continue; 39 | if (caretOffset.get() <= 0) 40 | return Continue; 41 | PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument()); 42 | PsiElement elementAtCaret = file.findElementAt(caretOffset.get()); 43 | if (elementAtCaret == null) 44 | return Continue; 45 | 46 | if (YAMLTokenTypes.SCALAR_KEY.equals(elementAtCaret.getNode().getElementType())) { 47 | Document document = editor.getDocument(); 48 | int offset = caretOffset.get(); 49 | char c = document.getText().charAt(offset); 50 | if (c != '.') { 51 | //If caret is right after the dot, it should work as well. 52 | c = document.getText().charAt(--offset); 53 | } 54 | if (c == '.') { 55 | int indentSize = CodeStyle.getIndentSize(file); 56 | //Indent children 57 | YAMLKeyValue keyValue = PsiTreeUtil.getParentOfType(elementAtCaret, YAMLKeyValue.class); 58 | if (keyValue != null) { 59 | YAMLValue valueElement = keyValue.getValue(); 60 | if (valueElement instanceof YAMLCompoundValue) { 61 | TextRange range = valueElement.getTextRange(); 62 | document.replaceString(range.getStartOffset(), range.getEndOffset(), 63 | YAMLTextUtil.indentText(valueElement.getText(), indentSize)); 64 | } 65 | } 66 | //Split the key 67 | String space = IndentHelperImpl.fillIndent(CodeStyle.getIndentOptions(file), indentSize); 68 | document.replaceString(offset, offset + 1, ":" + space); 69 | caretOffset.set(offset + 1); 70 | } 71 | } 72 | 73 | return Continue; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/completion/yaml/YamlCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.completion.yaml; 2 | 3 | import com.intellij.codeInsight.completion.CompletionParameters; 4 | import com.intellij.codeInsight.completion.CompletionProvider; 5 | import com.intellij.codeInsight.completion.CompletionResultSet; 6 | import com.intellij.openapi.application.ReadAction; 7 | import com.intellij.openapi.module.Module; 8 | import com.intellij.openapi.project.DumbModeBlockedFunctionality; 9 | import com.intellij.openapi.project.DumbService; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.psi.PsiComment; 12 | import com.intellij.psi.PsiElement; 13 | import com.intellij.psi.util.PsiTreeUtil; 14 | import com.intellij.util.ProcessingContext; 15 | import dev.flikas.spring.boot.assistant.idea.plugin.completion.CompletionService; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.jetbrains.annotations.NotNull; 18 | import org.jetbrains.annotations.Nullable; 19 | import org.jetbrains.yaml.YAMLUtil; 20 | import org.jetbrains.yaml.psi.YAMLKeyValue; 21 | import org.jetbrains.yaml.psi.YAMLPsiElement; 22 | import org.jetbrains.yaml.psi.YAMLScalar; 23 | import org.jetbrains.yaml.psi.YAMLSequenceItem; 24 | 25 | import static com.intellij.codeInsight.completion.CompletionUtil.DUMMY_IDENTIFIER_TRIMMED; 26 | import static com.intellij.openapi.module.ModuleUtilCore.findModuleForPsiElement; 27 | 28 | class YamlCompletionProvider extends CompletionProvider { 29 | @Override 30 | protected void addCompletions( 31 | @NotNull final CompletionParameters completionParameters, 32 | @NotNull final ProcessingContext processingContext, 33 | @NotNull final CompletionResultSet resultSet 34 | ) { 35 | PsiElement element = completionParameters.getPosition(); 36 | if (element instanceof PsiComment) { 37 | return; 38 | } 39 | Project project = element.getProject(); 40 | if (ReadAction.compute(() -> DumbService.isDumb(project))) { 41 | DumbService.getInstance(project).showDumbModeNotificationForFunctionality("Spring configuration completion", 42 | DumbModeBlockedFunctionality.CodeCompletion); 43 | return; 44 | } 45 | Module module = findModuleForPsiElement(element); 46 | if (module == null) { 47 | return; 48 | } 49 | 50 | // Find context YAMLPsiElement, stop if context is not at the same line. 51 | @Nullable YAMLPsiElement context = PsiTreeUtil.getParentOfType(element, YAMLPsiElement.class, false); 52 | if (context == null) return; 53 | if (!YAMLUtil.psiAreAtTheSameLine(element, context)) return; 54 | 55 | String queryString = element.getText(); 56 | String ancestorKeys = YAMLUtil.getConfigFullName(context); 57 | 58 | ancestorKeys = StringUtils.removeEnd(ancestorKeys, queryString); 59 | // use chars before the completion point as query string, ignore the remains, 60 | // besides, if user press Tab for completion, we should delete thr remains chars. 61 | queryString = StringUtils.truncate(queryString, queryString.indexOf(DUMMY_IDENTIFIER_TRIMMED)); 62 | CompletionService service = CompletionService.getInstance(project); 63 | YAMLKeyValue nearestKeyValue = PsiTreeUtil.getParentOfType(context, YAMLKeyValue.class, false); 64 | YAMLSequenceItem nearestSequenceItem = PsiTreeUtil.getParentOfType(context, YAMLSequenceItem.class, false); 65 | if (((nearestKeyValue != null && YAMLUtil.psiAreAtTheSameLine(nearestKeyValue, context)) 66 | || (nearestSequenceItem != null && YAMLUtil.psiAreAtTheSameLine(nearestSequenceItem, context))) 67 | && context instanceof YAMLScalar) { 68 | // User is asking completion for property value 69 | service.findSuggestionForValue(completionParameters, resultSet, ancestorKeys, queryString, 70 | YamlValueInsertHandler.INSTANCE); 71 | } else { 72 | // Key completion 73 | service.findSuggestionForKey(completionParameters, resultSet, ancestorKeys, queryString, 74 | YamlKeyInsertHandler.INSTANCE); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/NameTreeNode.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.PropertyName; 5 | import lombok.AccessLevel; 6 | import lombok.Data; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import org.apache.commons.collections4.Trie; 10 | import org.apache.commons.collections4.trie.PatriciaTrie; 11 | import org.apache.commons.collections4.trie.UnmodifiableTrie; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.util.Collections; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | 18 | import static dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationPropertyName.Form.UNIFORM; 19 | 20 | @Data 21 | public class NameTreeNode { 22 | private static final Logger LOG = Logger.getInstance(NameTreeNode.class); 23 | private final PatriciaTrie children = new PatriciaTrie<>(); 24 | private final List data = new LinkedList<>(); 25 | @Getter(AccessLevel.NONE) 26 | @Setter(AccessLevel.NONE) 27 | private IndexedType indexedType = IndexedType.NONE; 28 | 29 | 30 | public static NameTreeNode merge(NameTreeNode n1, NameTreeNode n2) { 31 | NameTreeNode dst = new NameTreeNode(); 32 | if (!n1.children.isEmpty() && !n2.children.isEmpty()) { 33 | assert n1.indexedType == n2.indexedType; 34 | dst.indexedType = n1.indexedType; 35 | } else if (!n1.children.isEmpty()) { 36 | dst.indexedType = n1.indexedType; 37 | } else if (!n2.children.isEmpty()) { 38 | dst.indexedType = n2.indexedType; 39 | } 40 | dst.data.addAll(n1.data); 41 | dst.data.addAll(n2.data); 42 | n1.children.forEach((k, v) -> dst.children.merge(k, v, NameTreeNode::merge)); 43 | n2.children.forEach((k, v) -> dst.children.merge(k, v, NameTreeNode::merge)); 44 | return dst; 45 | } 46 | 47 | 48 | public Trie getChildren() { 49 | return UnmodifiableTrie.unmodifiableTrie(children); 50 | } 51 | 52 | 53 | public boolean isIndexed() { 54 | return this.indexedType != IndexedType.NONE; 55 | } 56 | 57 | 58 | @Nullable 59 | public NameTreeNode findChild(PropertyName name) { 60 | if (name.isEmpty()) return this; 61 | NameTreeNode child; 62 | if (this.indexedType == IndexedType.NON_NUMERIC) { 63 | assert this.children.size() == 1; 64 | child = this.children.values().iterator().next(); 65 | } else if (this.indexedType == IndexedType.NUMERIC) { 66 | assert this.children.size() == 1; 67 | if (name.isNumericIndex(0)) { 68 | child = this.children.values().iterator().next(); 69 | } else { 70 | return null; 71 | } 72 | } else { 73 | child = this.children.get(name.getElement(0, UNIFORM)); 74 | } 75 | if (child == null) { 76 | return null; 77 | } else { 78 | return child.findChild(name.subName(1)); 79 | } 80 | } 81 | 82 | 83 | public void addChild(PropertyName name, MetadataItem value) { 84 | if (name.isEmpty()) { 85 | this.data.add(value); 86 | return; 87 | } 88 | String key; 89 | if (name.isAnyNonNumericIndex(0)) { 90 | key = "*"; 91 | this.indexedType = IndexedType.NON_NUMERIC; 92 | ensureAtMostOneChild(key); 93 | } else if (name.isAnyNumericIndex(0)) { 94 | key = "#"; 95 | this.indexedType = IndexedType.NUMERIC; 96 | ensureAtMostOneChild(key); 97 | } else { 98 | key = name.getElement(0, UNIFORM); 99 | } 100 | NameTreeNode child = this.children.computeIfAbsent(key, k -> new NameTreeNode()); 101 | child.addChild(name.subName(1), value); 102 | } 103 | 104 | 105 | private void ensureAtMostOneChild(String key) { 106 | if (!this.children.isEmpty() && !this.children.keySet().equals(Collections.singleton(key))) { 107 | LOG.warn("There should be at most one child of key \"" + key + "\", but children are: " + this.children); 108 | this.children.clear(); 109 | } 110 | } 111 | 112 | 113 | enum IndexedType {NUMERIC, NON_NUMERIC, NONE} 114 | } 115 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/CompositeIconUtils.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc; 2 | 3 | import com.intellij.ui.IconManager; 4 | import com.intellij.ui.JBColor; 5 | import com.intellij.ui.OffsetIcon; 6 | import com.intellij.ui.RetrievableIcon; 7 | import com.intellij.ui.icons.IconReplacer; 8 | import com.intellij.util.IconUtil; 9 | import com.intellij.util.ui.ColorIcon; 10 | import com.intellij.util.ui.JBCachingScalableIcon; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import javax.swing.*; 14 | import java.awt.*; 15 | import java.util.Objects; 16 | 17 | import static com.intellij.ui.scale.ScaleType.OBJ_SCALE; 18 | import static java.lang.Math.ceil; 19 | import static java.lang.Math.floor; 20 | 21 | public interface CompositeIconUtils { 22 | static Icon createWithModifier(Icon baseIcon, Icon modifierIcon) { 23 | IconManager im = IconManager.getInstance(); 24 | int halfWidth = OffsetIcon.REGULAR_OFFSET / 2; 25 | Icon modifierBackground = new ColorIcon(modifierIcon.getIconWidth(), modifierIcon.getIconHeight(), 26 | modifierIcon.getIconWidth(), modifierIcon.getIconHeight(), 27 | JBColor.background(), false); 28 | Icon modifierWithBackground = im.createLayered(modifierBackground, modifierIcon); 29 | var maskLayer = new ModifierIcon(IconUtil.resizeSquared(modifierWithBackground, halfWidth)); 30 | return im.createLayered(baseIcon, maskLayer); 31 | } 32 | 33 | 34 | class ModifierIcon extends JBCachingScalableIcon implements RetrievableIcon { 35 | private final double factor = 0.8; 36 | private final Icon myIcon; 37 | private int myWidth; 38 | private int myHeight; 39 | private Icon myScaledIcon; 40 | private int myScaledXOffset; 41 | private int myScaledYOffset; 42 | 43 | { 44 | getScaleContext().addUpdateListener(this::updateSize); 45 | setAutoUpdateScaleContext(false); 46 | } 47 | 48 | 49 | public ModifierIcon(@NotNull Icon icon) { 50 | myIcon = icon; 51 | updateSize(); 52 | } 53 | 54 | private ModifierIcon(@NotNull ModifierIcon icon) { 55 | super(icon); 56 | myWidth = icon.myWidth; 57 | myHeight = icon.myHeight; 58 | myIcon = icon.myIcon; 59 | myScaledIcon = null; 60 | myScaledXOffset = icon.myScaledXOffset; 61 | myScaledYOffset = icon.myScaledYOffset; 62 | } 63 | 64 | @Override 65 | public @NotNull ModifierIcon copy() { 66 | return new ModifierIcon(this); 67 | } 68 | 69 | public @NotNull Icon getIcon() { 70 | return myIcon; 71 | } 72 | 73 | public int hashCode() { 74 | return myIcon.hashCode(); 75 | } 76 | 77 | public boolean equals(Object obj) { 78 | if (obj == this) return true; 79 | if (obj instanceof ModifierIcon icon) { 80 | return Objects.equals(icon.myIcon, myIcon); 81 | } 82 | return false; 83 | } 84 | 85 | @Override 86 | public void paintIcon(Component c, Graphics g, int x, int y) { 87 | getScaleContext().update(); 88 | if (myScaledIcon == null) { 89 | float scale = getScale(); 90 | myScaledIcon = scale == 1f ? myIcon : IconUtil.scale(myIcon, null, scale); 91 | } 92 | myScaledIcon.paintIcon(c, g, myScaledXOffset + x, myScaledYOffset + y); 93 | } 94 | 95 | @Override 96 | public int getIconWidth() { 97 | getScaleContext().update(); 98 | return (int) ceil(scaleVal(myWidth, OBJ_SCALE) * (1 + factor)); 99 | } 100 | 101 | @Override 102 | public int getIconHeight() { 103 | getScaleContext().update(); 104 | return (int) ceil(scaleVal(myHeight, OBJ_SCALE) * (1 + factor)); 105 | } 106 | 107 | @Override 108 | public @NotNull Icon replaceBy(@NotNull IconReplacer replacer) { 109 | return new ModifierIcon(replacer.replaceIcon(myIcon)); 110 | } 111 | 112 | private void updateSize() { 113 | myWidth = myIcon.getIconWidth(); 114 | myHeight = myIcon.getIconHeight(); 115 | myScaledXOffset = (int) floor(scaleVal(myWidth, OBJ_SCALE) * factor); 116 | myScaledYOffset = (int) floor(scaleVal(myHeight, OBJ_SCALE) * factor); 117 | } 118 | 119 | @Override 120 | public String toString() { 121 | return "ModifierIcon: icon=" + myIcon; 122 | } 123 | 124 | @Override 125 | public @NotNull Icon retrieveIcon() { 126 | return getIcon(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/metadata/index/MetadataGroupImpl.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.metadata.index; 2 | 3 | import com.intellij.lang.documentation.DocumentationMarkup; 4 | import com.intellij.openapi.util.text.HtmlBuilder; 5 | import com.intellij.psi.PsiClass; 6 | import com.intellij.psi.PsiMethod; 7 | import dev.flikas.spring.boot.assistant.idea.plugin.metadata.source.ConfigurationMetadata; 8 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiElementUtils; 9 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiMethodUtils; 10 | import dev.flikas.spring.boot.assistant.idea.plugin.misc.PsiTypeUtils; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.Getter; 13 | import lombok.ToString; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.util.Optional; 18 | 19 | @EqualsAndHashCode(of = "metadata") 20 | @ToString(of = "metadata") 21 | class MetadataGroupImpl implements MetadataGroup { 22 | private final MetadataIndex index; 23 | @Getter 24 | private final ConfigurationMetadata.Group metadata; 25 | private volatile String renderedDocument = null; 26 | 27 | 28 | MetadataGroupImpl(MetadataIndex index, ConfigurationMetadata.Group metadata) { 29 | this.index = index; 30 | this.metadata = metadata; 31 | } 32 | 33 | 34 | @Override 35 | public @NotNull String getNameStr() { 36 | return metadata.getName(); 37 | } 38 | 39 | 40 | /** 41 | * @see ConfigurationMetadata.Group#getType() 42 | */ 43 | @Override 44 | public Optional getType() { 45 | return Optional.ofNullable(metadata.getType()) 46 | .filter(StringUtils::isNotBlank) 47 | .map(type -> PsiTypeUtils.findClass(index.project(), type)); 48 | } 49 | 50 | 51 | /** 52 | * @see ConfigurationMetadata.Group#getSourceType() 53 | */ 54 | @Override 55 | public Optional getSourceType() { 56 | return Optional.ofNullable(metadata.getSourceType()) 57 | .filter(StringUtils::isNotBlank) 58 | .map(type -> PsiTypeUtils.findClass(index.project(), type)); 59 | } 60 | 61 | 62 | @Override 63 | public @NotNull String getRenderedDescription() { 64 | if (this.renderedDocument != null) { 65 | return this.renderedDocument; 66 | } 67 | synchronized (this) { 68 | if (this.renderedDocument != null) { 69 | return this.renderedDocument; 70 | } 71 | HtmlBuilder doc = new HtmlBuilder(); 72 | String desc = metadata.getDescription(); 73 | //Unfortunately, even though there is a 'description' field for the group metadata, `spring boot configuration processor` will never fill it. 74 | //Here we use group class/method's document instead. 75 | String descFrom = null; 76 | if (StringUtils.isBlank(desc)) { 77 | desc = getSourceMethod().map(PsiElementUtils::getDocument).orElse(null); 78 | descFrom = getSourceMethod().map(PsiElementUtils::createLinkForDoc).orElse(null); 79 | } 80 | if (StringUtils.isBlank(desc)) { 81 | desc = getType().map(PsiElementUtils::getDocument).orElse(null); 82 | descFrom = getType().map(PsiElementUtils::createLinkForDoc).orElse(null); 83 | } 84 | if (StringUtils.isBlank(desc)) { 85 | desc = getSourceType().map(PsiElementUtils::getDocument).orElse(null); 86 | descFrom = getSourceType().map(PsiElementUtils::createLinkForDoc).orElse(null); 87 | } 88 | if (StringUtils.isNotBlank(desc)) { 89 | if (StringUtils.isNotBlank(descFrom)) { 90 | doc.append(DocumentationMarkup.GRAYED_ELEMENT 91 | .addText("(Doc below is copied from ") 92 | .addRaw(descFrom) 93 | .addText(")\n")); 94 | } 95 | doc.appendRaw(desc); 96 | } 97 | this.renderedDocument = doc.toString(); 98 | } 99 | return this.renderedDocument; 100 | } 101 | 102 | 103 | @Override 104 | public MetadataIndex getIndex() { 105 | return index; 106 | } 107 | 108 | 109 | /** 110 | * @see ConfigurationMetadata.Group#getSourceMethod() 111 | */ 112 | @Override 113 | public Optional getSourceMethod() { 114 | String sourceMethod = metadata.getSourceMethod(); 115 | if (StringUtils.isBlank(sourceMethod)) return Optional.empty(); 116 | return getSourceType().flatMap(sourceClass -> PsiMethodUtils.findMethodBySignature(sourceClass, sourceMethod)); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /plugin/src/main/java/dev/flikas/spring/boot/assistant/idea/plugin/misc/PsiElementUtils.java: -------------------------------------------------------------------------------- 1 | package dev.flikas.spring.boot.assistant.idea.plugin.misc; 2 | 3 | import com.intellij.codeInsight.documentation.DocumentationManagerUtil; 4 | import com.intellij.codeInsight.javadoc.JavaDocInfoGenerator; 5 | import com.intellij.lang.documentation.DocumentationMarkup; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | import com.intellij.openapi.fileTypes.FileType; 8 | import com.intellij.openapi.fileTypes.FileTypeManager; 9 | import com.intellij.openapi.vfs.VirtualFile; 10 | import com.intellij.psi.PsiClass; 11 | import com.intellij.psi.PsiDocCommentOwner; 12 | import com.intellij.psi.PsiElement; 13 | import com.intellij.psi.PsiJavaDocumentedElement; 14 | import com.intellij.psi.PsiJvmMember; 15 | import com.intellij.psi.PsiMethod; 16 | import com.intellij.psi.PsiModifierList; 17 | import com.intellij.psi.javadoc.PsiDocComment; 18 | import com.intellij.psi.util.PsiUtil; 19 | import lombok.experimental.UtilityClass; 20 | import org.jetbrains.annotations.NotNull; 21 | import org.jetbrains.annotations.Nullable; 22 | 23 | @UtilityClass 24 | public class PsiElementUtils { 25 | private static final Logger LOG = Logger.getInstance(PsiElementUtils.class); 26 | 27 | 28 | public static boolean isInFileOfType(PsiElement element, FileType fileType) { 29 | VirtualFile virtualFile = PsiUtil.getVirtualFile(element); 30 | if (virtualFile == null) { 31 | return false; 32 | } 33 | FileTypeManager ftm = FileTypeManager.getInstance(); 34 | return ftm.isFileOfType(virtualFile, fileType); 35 | } 36 | 37 | 38 | public static String getDocument(PsiJavaDocumentedElement element) { 39 | JavaDocInfoGenerator generator = new JavaDocInfoGenerator(element.getProject(), element); 40 | PsiDocComment comment = getDocComment(element); 41 | if (comment == null) return null; 42 | StringBuilder doc = new StringBuilder(); 43 | generator.generateCommonSection(doc, comment); 44 | // We use the CONTENT part of document generated by JavaDocInfoGenerator.generateCommonSection only, 45 | // because the SECTIONS part is not complete, and we don't want it influence user. 46 | int idx = doc.indexOf(DocumentationMarkup.CONTENT_END); 47 | if (idx >= 0) { 48 | doc.delete(idx, doc.length()); 49 | idx = doc.indexOf(DocumentationMarkup.CONTENT_START); 50 | assert idx >= 0; 51 | doc.delete(idx, idx + DocumentationMarkup.CONTENT_START.length()); 52 | } else { 53 | return null; 54 | } 55 | return doc.toString().strip(); 56 | } 57 | 58 | 59 | public static String createLinkForDoc(@NotNull PsiJvmMember member) { 60 | String label; 61 | String ref; 62 | PsiClass containingClass = member.getContainingClass(); 63 | if (containingClass == null) { 64 | if (member instanceof PsiClass psiClass) { 65 | label = psiClass.getQualifiedName(); 66 | ref = psiClass.getQualifiedName(); 67 | } else { 68 | label = member.getName(); 69 | ref = member.getName(); 70 | } 71 | } else { 72 | label = containingClass.getQualifiedName() + "." 73 | + member.getName() + (member instanceof PsiMethod ? "()" : ""); 74 | ref = containingClass.getQualifiedName() + "#" + member.getName(); 75 | } 76 | return createHyperLink(ref, label); 77 | } 78 | 79 | 80 | private static @NotNull String createHyperLink(String ref, String label) { 81 | StringBuilder buffer = new StringBuilder(); 82 | DocumentationManagerUtil.createHyperlink(buffer, ref, label, false); 83 | return buffer.toString(); 84 | } 85 | 86 | 87 | private static @Nullable PsiDocComment getDocComment(PsiJavaDocumentedElement docOwner) { 88 | PsiElement navElement = docOwner.getNavigationElement(); 89 | if (!(navElement instanceof PsiJavaDocumentedElement)) { 90 | LOG.info("Wrong navElement: " + navElement + "; original = " + docOwner + " of class " + docOwner.getClass()); 91 | return null; 92 | } 93 | PsiDocComment comment = ((PsiJavaDocumentedElement) navElement).getDocComment(); 94 | if (comment == null) { //check for non-normalized fields 95 | PsiModifierList modifierList = docOwner instanceof PsiDocCommentOwner 96 | ? ((PsiDocCommentOwner) docOwner).getModifierList() : null; 97 | if (modifierList != null) { 98 | PsiElement parent = modifierList.getParent(); 99 | if (parent instanceof PsiDocCommentOwner && parent.getNavigationElement() instanceof PsiDocCommentOwner) { 100 | return ((PsiDocCommentOwner) parent.getNavigationElement()).getDocComment(); 101 | } 102 | } 103 | } 104 | return comment; 105 | } 106 | } 107 | --------------------------------------------------------------------------------