├── a.html ├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ ├── withNodeJs.xml │ │ │ ├── pluginIcon.svg │ │ │ └── pluginIcon_dark.svg │ │ └── icons │ │ │ ├── cap.svg │ │ │ └── cap_dark.svg │ ├── java │ │ └── com │ │ │ └── sap │ │ │ └── cap │ │ │ └── cds │ │ │ └── intellij │ │ │ ├── util │ │ │ ├── CacheKey.java │ │ │ ├── FileUtil.java │ │ │ ├── SupportUtil.java │ │ │ ├── PathUtil.java │ │ │ ├── StackUtil.java │ │ │ ├── StdioLogsUtil.java │ │ │ ├── LoggerScope.java │ │ │ ├── EditorUtil.java │ │ │ ├── CliUtil.java │ │ │ ├── ServerLogsUtil.java │ │ │ ├── Logger.java │ │ │ ├── ReflectionUtil.java │ │ │ └── JsonUtil.java │ │ │ ├── lsp4ij │ │ │ ├── CdsDiagnosticFeature.java │ │ │ ├── CdsSemanticTokensFeature.java │ │ │ ├── CdsWorkspaceSymbolFeature.java │ │ │ ├── CdsProgressFeature.java │ │ │ ├── CdsCustomServerAPI.java │ │ │ ├── CdsFoldingRangeFeature.java │ │ │ ├── CdsHoverFeature.java │ │ │ ├── CdsCodeActionFeature.java │ │ │ ├── CdsLanguageServer.java │ │ │ ├── CdsFormattingFeature.java │ │ │ ├── CdsLanguageServerFactory.java │ │ │ ├── CdsLanguageClient.java │ │ │ └── CdsLspClientFeatures.java │ │ │ ├── textmate │ │ │ ├── CdsTextMateBundle.java │ │ │ └── CdsTextMateBundleService.java │ │ │ ├── lang │ │ │ ├── CdsLanguage.java │ │ │ └── CdsLanguageSubstitutor.java │ │ │ ├── CdsIcons.java │ │ │ ├── codestyle │ │ │ ├── CdsCodeStylePanel.java │ │ │ ├── CdsCodeStylePanelFactory.java │ │ │ ├── CdsPrettierJsonListener.java │ │ │ ├── CdsCodeStyleMainPanel.java │ │ │ ├── CdsPrettierJsonManager.java │ │ │ ├── CdsCodeStyleTabularPanel.java │ │ │ ├── CdsCodeStyleSettingsProvider.java │ │ │ ├── CdsCodeStyleSettingsService.java │ │ │ ├── CdsCodeStylePreviewFormattingService.java │ │ │ ├── CdsCodeStyleOption.java │ │ │ ├── CdsCodeStyleCheckboxesPanel.java │ │ │ └── CdsCodeStyleSettingsBase.java │ │ │ ├── CdsPlugin.java │ │ │ ├── lspServer │ │ │ ├── UserError.java │ │ │ ├── CopyLspServerLogPathAction.java │ │ │ ├── ShowLspServerLogsAction.java │ │ │ └── CdsLspServerDescriptor.java │ │ │ ├── lifecycle │ │ │ ├── IdeLifecycleListener.java │ │ │ └── PluginLifecycleListener.java │ │ │ ├── lsp │ │ │ ├── CopyStdioLogPathAction.java │ │ │ └── ShowStdioLogsAction.java │ │ │ ├── CdsFileType.java │ │ │ ├── usersettings │ │ │ ├── CdsUserSettingsListener.java │ │ │ ├── CdsUserSettingsConfigurable.java │ │ │ ├── CdsUserSettingsJsonManager.java │ │ │ └── CdsUserSettingsService.java │ │ │ ├── settings │ │ │ ├── JsonSettingsService.java │ │ │ ├── AppSettings.java │ │ │ ├── ApplicationSettingsConfigurable.java │ │ │ ├── JsonSettingsFileListener.java │ │ │ ├── AppSettingsComponent.java │ │ │ └── JsonSettingsManager.java │ │ │ ├── command │ │ │ └── AnalyzeDependenciesAction.java │ │ │ └── httpFiles │ │ │ └── HttpFileCompatConversionIntention.java │ └── kotlin │ │ └── com │ │ └── sap │ │ └── cap │ │ └── cds │ │ └── intellij │ │ └── lifecycle │ │ └── ProjectLifecycleListener.kt ├── test │ ├── data │ │ └── serverIntegration │ │ │ ├── diagnostics.cds │ │ │ └── diagnostics.expected.cds │ └── java │ │ └── com │ │ └── sap │ │ └── cap │ │ └── cds │ │ └── intellij │ │ ├── lspServer │ │ ├── ServerIntegrationTest.java │ │ └── CdsLspServerDescriptorTest.java │ │ ├── codestyle │ │ ├── CdsCodeStyleMainPanelTest.java │ │ ├── CdsCodeStyleTabularPanelTest.java │ │ ├── CdsCodeStyleCheckboxesPanelTest.java │ │ ├── CdsCodeStylePanelFactoryTest.java │ │ ├── CdsCodeStyleOptionTest.java │ │ ├── CdsCodeStyleSettingsServiceIdeSettingsTest.java │ │ ├── CdsCodeStyleSettingsTest.java │ │ ├── CdsCodeStyleSettingsServiceProjectSettingsTest.java │ │ └── CdsCodeStyleSettingsServiceTestBase.java │ │ ├── util │ │ └── JsonUtilTest.java │ │ └── TestUtil.java ├── templates │ └── java │ │ └── com │ │ └── sap │ │ └── cap │ │ └── cds │ │ └── intellij │ │ └── codestyle │ │ └── CdsCodeStyleSettings.java └── js │ ├── util │ └── string.js │ └── usersettings │ └── patchUserSettingsJavaSrc.js ├── .gitattributes ├── .assets ├── logo.png ├── outline.png ├── quick_fix.png ├── find_references.png ├── goto_definition.gif ├── code_style_settings.png ├── document_formatting.gif ├── goto_implementation.gif ├── hover_documentation.png ├── highlights+hover+outline.png ├── cds_language_server_settings.png ├── syntax+completion+diagnostics.png └── IntelliJ_IDEA_icon.svg ├── lsp ├── package.json └── mitm.js ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── question.yaml │ ├── feature_request.yaml │ └── bug_report.yaml ├── workflows │ ├── main.yml │ ├── pr.yml │ ├── publish.yml │ ├── build-and-test.yml │ └── release.yml └── actions │ ├── upload-installable-zip │ └── action.yml │ ├── gradle-run │ └── action.yml │ └── prepare-cds-lsp-local │ └── action.yml ├── .gitignore ├── gradle.properties ├── REUSE.toml ├── gradlew.bat └── docs ├── development.md └── features.md /a.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/withNodeJs.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | gradlew text eol=lf 3 | -------------------------------------------------------------------------------- /.assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/logo.png -------------------------------------------------------------------------------- /.assets/outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/outline.png -------------------------------------------------------------------------------- /lsp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@sap/cds-lsp": "9.4.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.assets/quick_fix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/quick_fix.png -------------------------------------------------------------------------------- /.assets/find_references.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/find_references.png -------------------------------------------------------------------------------- /.assets/goto_definition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/goto_definition.gif -------------------------------------------------------------------------------- /.assets/code_style_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/code_style_settings.png -------------------------------------------------------------------------------- /.assets/document_formatting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/document_formatting.gif -------------------------------------------------------------------------------- /.assets/goto_implementation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/goto_implementation.gif -------------------------------------------------------------------------------- /.assets/hover_documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/hover_documentation.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.assets/highlights+hover+outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/highlights+hover+outline.png -------------------------------------------------------------------------------- /.assets/cds_language_server_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/cds_language_server_settings.png -------------------------------------------------------------------------------- /.assets/syntax+completion+diagnostics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cap-js/cds-intellij/HEAD/.assets/syntax+completion+diagnostics.png -------------------------------------------------------------------------------- /src/test/data/serverIntegration/diagnostics.cds: -------------------------------------------------------------------------------- 1 | type T : Integer; 2 | 3 | entity F { 4 | m : T; 5 | n : NotFound; 6 | } 7 | 8 | ntity B {} 9 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/CacheKey.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.openapi.project.Project; 4 | 5 | public record CacheKey(Project project, LoggerScope scope) {} 6 | -------------------------------------------------------------------------------- /src/test/data/serverIntegration/diagnostics.expected.cds: -------------------------------------------------------------------------------- 1 | type T : Integer; 2 | 3 | entity F { 4 | m : T; 5 | n : NotFound; 6 | } 7 | 8 | ntity B {} 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | - package-ecosystem: gradle 9 | directory: / 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsDiagnosticFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.redhat.devtools.lsp4ij.client.features.LSPDiagnosticFeature; 4 | 5 | public class CdsDiagnosticFeature extends LSPDiagnosticFeature { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .intellijPlatform 4 | .kotlin 5 | .qodana 6 | .vscode 7 | 8 | local.properties 9 | 10 | /build 11 | /lsp/node_modules/ 12 | /src/js/.idea/ 13 | 14 | # generated from template 15 | /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettings.java 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundle.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.textmate; 2 | 3 | public class CdsTextMateBundle { 4 | 5 | public static final String LABEL = "TextMate Bundle"; 6 | 7 | static final String RELATIVE_PATH = "cds-lsp/node_modules/@sap/cds-lsp/syntaxes"; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsSemanticTokensFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.redhat.devtools.lsp4ij.client.features.LSPSemanticTokensFeature; 4 | 5 | public class CdsSemanticTokensFeature extends LSPSemanticTokensFeature { 6 | 7 | // Q: anything to do to get our semantic tokens to show up in the IDE? 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lang/CdsLanguage.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lang; 2 | 3 | public class CdsLanguage extends com.intellij.lang.Language { 4 | 5 | public static final CdsLanguage INSTANCE = new CdsLanguage(); 6 | public static final String LABEL = "cds"; 7 | 8 | private CdsLanguage() { 9 | super(LABEL); 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/CdsIcons.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | 5 | import javax.swing.*; 6 | 7 | public class CdsIcons { 8 | 9 | public static final Icon FILE = IconLoader.getIcon("/icons/cap.svg", CdsIcons.class); 10 | 11 | public static final Icon SERVER = CdsIcons.FILE; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePanel.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import java.util.Map; 4 | 5 | public interface CdsCodeStylePanel { 6 | CdsCodeStyleOption.Category getCategory(); 7 | 8 | void addOption(CdsCodeStyleOption option); 9 | 10 | void setOptionsEnablement(Map enablementMap); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/FileUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | public class FileUtil { 8 | 9 | public static Path createTempDir(String prefix) throws IOException { 10 | return Files.createTempDirectory(prefix); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsWorkspaceSymbolFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.redhat.devtools.lsp4ij.client.features.LSPWorkspaceSymbolFeature; 4 | 5 | public class CdsWorkspaceSymbolFeature extends LSPWorkspaceSymbolFeature { 6 | 7 | 8 | @Override 9 | public boolean supportsGotoClass() { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yaml: -------------------------------------------------------------------------------- 1 | name: ❓ Question 2 | description: Ask a question 3 | title: "[QUESTION] " 4 | labels: ["question", "new"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Question 9 | description: What would you like to know? If you encounter unusual behaviour or identified a missing feature, consider opening a bug report instead. 10 | validations: 11 | required: true -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/SupportUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | public class SupportUtil { 4 | 5 | public static boolean isDebugCdsLsp() { 6 | String debug = System.getenv("DEBUG"); 7 | if (debug == null) { 8 | debug = System.getProperty("DEBUG"); 9 | } 10 | return (debug != null) && debug.contains("cds-lsp"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsProgressFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.redhat.devtools.lsp4ij.client.features.LSPProgressFeature; 4 | 5 | public class CdsProgressFeature extends LSPProgressFeature { 6 | 7 | // Q: anything to do to get our progress messages to show up in the IDE? We have it when scanning the workspace and indexing it e.g. findReferences for the first time 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsCustomServerAPI.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import org.eclipse.lsp4j.services.LanguageServer; 4 | 5 | public interface CdsCustomServerAPI extends LanguageServer { 6 | 7 | // @JsonRequest("my/applications") 8 | // CompletableFuture<List<Application>> getApplications(); 9 | 10 | // TODO: add our custom commands/requests here e.g. formatGivenText for config UI 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/CdsPlugin.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij; 2 | 3 | public class CdsPlugin { 4 | 5 | public static final String ID = "com.sap.cap.cds"; // Should not be changed. See plugin.xml 6 | 7 | public static final String PACKAGE = "com.sap.cap.cds.intellij"; 8 | 9 | public static final String LABEL = "cds-intellij"; // Used for logging and other purposes 10 | 11 | public static final String TITLE = "SAP CDS Language Support"; // Used as plugin name in UI 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/PathUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 4 | 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | 8 | import static com.intellij.util.PathUtil.getJarPathForClass; 9 | 10 | public class PathUtil { 11 | 12 | public static String resolve(String relativePath) { 13 | Path basePath = Paths.get(getJarPathForClass(CdsLanguage.class)); 14 | return basePath.getParent().resolve(relativePath).toString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettings.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.psi.codeStyle.CodeStyleSettings; 4 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 5 | 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.*; 10 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.*; 11 | 12 | // NOTE: class body is generated 13 | public class CdsCodeStyleSettings extends CdsCodeStyleSettingsBase { 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: release-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | main-build: 14 | runs-on: ubuntu-latest 15 | container: 16 | image: ubuntu:22.04 17 | steps: 18 | - uses: actions/checkout@v6 19 | 20 | - uses: ./.github/actions/gradle-run 21 | with: 22 | steps: |- 23 | test 24 | build 25 | 26 | - uses: ./.github/actions/upload-installable-zip 27 | with: 28 | retention-days: 7 29 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lspServer/UserError.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lspServer; 2 | 3 | import com.intellij.notification.Notification; 4 | import com.intellij.notification.NotificationType; 5 | import com.intellij.notification.Notifications; 6 | 7 | public class UserError { 8 | /** 9 | * Shows an error message in the IDE. 10 | * @param message The error message to show. 11 | */ 12 | public static void show(String message) { 13 | Notifications.Bus.notify(new Notification("com.sap.cap.cds.intellij.notifications", "CDS language server", message, NotificationType.ERROR)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/util/string.js: -------------------------------------------------------------------------------- 1 | function capitalizeFirstLetter(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | 5 | function toScreamingSnakeCase(string) { 6 | return string 7 | .replace(/-/g, '_') 8 | .replace(/([a-z])([A-Z])/g, '$1_$2') 9 | .toUpperCase(); 10 | } 11 | 12 | function removeMarkdownFormatting(input) { 13 | return input 14 | .replace(/_([^_]+)_/g, '$1') 15 | .replace(/\*\*([^*]+)\*\*/g, "'$1'") 16 | .replace(/\*([^*]+)\*/g, '$1'); 17 | } 18 | 19 | module.exports = { 20 | capitalizeFirstLetter, 21 | toScreamingSnakeCase, 22 | removeMarkdownFormatting 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/StackUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | public class StackUtil { 4 | public static StackTraceElement getCaller(int depth) { 5 | StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 6 | if (stackTrace.length <= depth) { 7 | return null; 8 | } 9 | return stackTrace[depth]; 10 | } 11 | 12 | public static String getMethod(StackTraceElement caller) { 13 | if (caller == null) { 14 | return "(unknown)"; 15 | } 16 | return caller.getClassName() + "." + caller.getMethodName(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lifecycle/IdeLifecycleListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lifecycle; 2 | 3 | import com.intellij.ide.AppLifecycleListener; 4 | import com.sap.cap.cds.intellij.textmate.CdsTextMateBundleService; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.List; 8 | 9 | import static com.intellij.openapi.application.ApplicationManager.getApplication; 10 | 11 | public class IdeLifecycleListener implements AppLifecycleListener { 12 | @Override 13 | public void appFrameCreated(@NotNull List<String> commandLineArgs) { 14 | getApplication().getService(CdsTextMateBundleService.class).registerBundle(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - src/** 7 | - lsp/** 8 | - build.gradle 9 | - gradle.properties 10 | - local.properties 11 | - .github/** 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: build-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | pr-build: 20 | runs-on: ubuntu-latest 21 | container: 22 | image: ubuntu:22.04 23 | env: 24 | CI: true 25 | steps: 26 | - uses: actions/checkout@v6 27 | 28 | - uses: ./.github/actions/gradle-run 29 | with: 30 | steps: |- 31 | test 32 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsFoldingRangeFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.psi.PsiFile; 4 | import com.redhat.devtools.lsp4ij.client.features.LSPFoldingRangeFeature; 5 | import org.eclipse.lsp4j.FoldingRange; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | public class CdsFoldingRangeFeature extends LSPFoldingRangeFeature { 9 | 10 | @Override 11 | public boolean isCollapsedByDefault(@NotNull PsiFile file, @NotNull FoldingRange foldingRange) { 12 | // Idea: collapse usings - maybe client-side user option OR can we do this in server? 13 | return super.isCollapsedByDefault(file, foldingRange); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/lspServer/ServerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lspServer; 2 | 3 | import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase; 4 | 5 | import static com.sap.cap.cds.intellij.TestUtil.checkDiagnostics; 6 | 7 | public class ServerIntegrationTest extends CodeInsightFixtureTestCase { 8 | 9 | @Override 10 | protected void setUp() throws Exception { 11 | super.setUp(); 12 | myFixture.setTestDataPath("src/test/data/serverIntegration"); 13 | } 14 | 15 | public void testDiagnostics() { 16 | myFixture.configureByFile(getTestName(true) + ".cds"); 17 | checkDiagnostics(myFixture); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/StdioLogsUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.openapi.application.PathManager; 4 | import com.sap.cap.cds.intellij.CdsPlugin; 5 | 6 | import java.io.File; 7 | import java.util.Optional; 8 | 9 | public class StdioLogsUtil { 10 | 11 | public static Optional<File> findStdioLogFile() { 12 | String pluginsPath = PathManager.getPluginsPath(); 13 | File stdioLogFile = new File(pluginsPath, "%s/lib/cds-lsp/stdio.json".formatted(CdsPlugin.LABEL)); 14 | if (stdioLogFile.exists()) { 15 | return Optional.of(stdioLogFile); 16 | } 17 | return Optional.empty(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsHoverFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.psi.PsiFile; 4 | import com.redhat.devtools.lsp4ij.client.features.LSPHoverFeature; 5 | import org.eclipse.lsp4j.MarkupContent; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public class CdsHoverFeature extends LSPHoverFeature { 10 | 11 | @Override 12 | public @Nullable String getContent(@NotNull MarkupContent content, @NotNull PsiFile file) { 13 | // TODO: if we cannot hook the analyze-deps command, we could remove the link from the markup for the time being 14 | return super.getContent(content, file); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsCodeActionFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.redhat.devtools.lsp4ij.client.features.LSPCodeActionFeature; 4 | import org.eclipse.lsp4j.CodeAction; 5 | import org.eclipse.lsp4j.Command; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | public class CdsCodeActionFeature extends LSPCodeActionFeature { 10 | 11 | @Override 12 | public @Nullable String getText(@NotNull Command command) { 13 | return super.getText(command); 14 | } 15 | 16 | @Override 17 | public @Nullable String getText(@NotNull CodeAction codeAction) { 18 | return super.getText(codeAction); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: publish-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | publish-jb: 15 | environment: jb-marketplace 16 | runs-on: ubuntu-latest 17 | container: 18 | image: ubuntu:22.04 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - uses: ./.github/actions/gradle-run 23 | with: 24 | steps: |- 25 | test 26 | build 27 | 28 | - uses: ./.github/actions/gradle-run 29 | with: 30 | steps: |- 31 | publish 32 | env: 33 | JB_TOKEN: ${{ secrets.JB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | concurrency: 7 | group: build-and-test-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | build-and-test: 15 | runs-on: ubuntu-latest 16 | container: 17 | image: ubuntu:22.04 18 | steps: 19 | - uses: actions/checkout@v6 20 | 21 | - uses: ./.github/actions/prepare-cds-lsp-local 22 | with: 23 | gh_pat: ${{ secrets.GH_PAT_READ_PUBLIC_REPOS }} 24 | 25 | - uses: ./.github/actions/gradle-run 26 | with: 27 | steps: |- 28 | test 29 | build 30 | 31 | - uses: ./.github/actions/upload-installable-zip 32 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/LoggerScope.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.sap.cap.cds.intellij.CdsPlugin; 4 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsProvider; 5 | import com.sap.cap.cds.intellij.textmate.CdsTextMateBundle; 6 | import com.sap.cap.cds.intellij.usersettings.CdsUserSettingsService; 7 | 8 | public enum LoggerScope { 9 | PLUGIN(CdsPlugin.LABEL), 10 | TM_BUNDLE("%s/%s".formatted(CdsPlugin.LABEL, CdsTextMateBundle.LABEL)), 11 | CODE_STYLE("%s/%s".formatted(CdsPlugin.LABEL, CdsCodeStyleSettingsProvider.LABEL)), 12 | USER_SETTINGS("%s/%s".formatted(CdsPlugin.LABEL, CdsUserSettingsService.LABEL)); 13 | 14 | final String label; 15 | 16 | LoggerScope(String scope) { 17 | this.label = scope; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lang/CdsLanguageSubstitutor.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lang; 2 | 3 | import com.intellij.lang.Language; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | import com.intellij.psi.LanguageSubstitutor; 7 | import com.sap.cap.cds.intellij.CdsFileType; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | public class CdsLanguageSubstitutor extends LanguageSubstitutor { 12 | @Override 13 | public @Nullable Language getLanguage(@NotNull VirtualFile virtualFile, @NotNull Project project) { 14 | if (virtualFile.getName().endsWith(CdsFileType.DOT_EXTENSION)) { 15 | return CdsLanguage.INSTANCE; 16 | } 17 | // TODO? if csn files -> CDS 18 | // TODO? if json files -> inspect and potentially return CDS 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/EditorUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.openapi.fileEditor.FileEditorManager; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.vfs.LocalFileSystem; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.sap.cap.cds.intellij.lspServer.UserError; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.File; 11 | 12 | public class EditorUtil { 13 | 14 | public static void openFileInEditor(@NotNull Project project, @NotNull File file) { 15 | VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); 16 | if (virtualFile != null) { 17 | FileEditorManager.getInstance(project).openFile(virtualFile, true); 18 | } else { 19 | UserError.show("Cannot open file: " + file.getAbsolutePath()); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/sap/cap/cds/intellij/lifecycle/ProjectLifecycleListener.kt: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lifecycle 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.startup.ProjectActivity 5 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService 6 | import com.sap.cap.cds.intellij.util.Logger.logger 7 | import com.sap.cap.cds.intellij.util.LoggerScope.CODE_STYLE 8 | 9 | // Impl in Kotlin is required due to suspension, see https://plugins.jetbrains.com/docs/intellij/plugin-components.html#project-open 10 | class ProjectLifecycleListener : ProjectActivity { 11 | override suspend fun execute(project: Project) { 12 | val service = project.getService(CdsCodeStyleSettingsService::class.java) 13 | if (service.isSettingsFilePresent) { 14 | logger(project, CODE_STYLE).debug("Project with .cdsprettier.json opened") 15 | service.updateProjectSettingsFromFile() 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/CliUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.execution.ExecutionException; 4 | import com.intellij.execution.configurations.GeneralCommandLine; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | import java.util.Optional; 10 | 11 | public class CliUtil { 12 | public static Optional<String> executeCli(String... args) { 13 | try { 14 | Process process = new GeneralCommandLine(args).createProcess(); 15 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 16 | return Optional.ofNullable(reader.readLine()); 17 | } 18 | } catch (ExecutionException | IOException e) { 19 | Logger.PLUGIN.error("Failed to execute [%s]".formatted(String.join(" ", args)), e); 20 | return Optional.empty(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/upload-installable-zip/action.yml: -------------------------------------------------------------------------------- 1 | name: upload-installable-zip 2 | description: Upload build results as a ZIP directly installable in IntelliJ (equivalent to the ZIP built by Gradle) 3 | 4 | # Prerequisites: gradle-run with step build must have run before this action 5 | 6 | inputs: 7 | retention-days: 8 | description: 'The number of days to retain the artifact' 9 | required: false 10 | default: '14' 11 | 12 | runs: 13 | using: composite 14 | 15 | steps: 16 | - name: Install Unzip 17 | shell: bash 18 | # apt-get update should have run elsewhere 19 | run: apt-get install unzip -y 20 | 21 | - name: Extract Zip 22 | shell: bash 23 | run: | 24 | mkdir extracted 25 | unzip build/distributions/cds-intellij.zip -d extracted 26 | 27 | - name: Upload Extracted Files as Artifact 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: cds-intellij 31 | path: extracted/ 32 | retention-days: ${{ inputs.retention-days }} -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanelTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.psi.codeStyle.CodeStyleSettings; 5 | import com.intellij.testFramework.LightPlatformTestCase; 6 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 7 | 8 | public class CdsCodeStyleMainPanelTest extends LightPlatformTestCase { 9 | 10 | CdsCodeStyleMainPanel panel; 11 | 12 | @Override 13 | protected void tearDown() throws Exception { 14 | panel.dispose(); 15 | super.tearDown(); 16 | } 17 | 18 | public void testCdsCodeStyleMainPanelProperties() { 19 | CodeStyleSettings settings = CodeStyle.createTestSettings(); 20 | CodeStyleSettings currentSettings = CodeStyle.createTestSettings(); 21 | panel = new CdsCodeStyleMainPanel(currentSettings, settings); 22 | 23 | assertEquals(CdsLanguage.INSTANCE, panel.getDefaultLanguage()); 24 | assertEquals(CdsCodeStyleSettings.SAMPLE_SRC, panel.getPreviewText()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePanelFactory.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyleAbstractPanel; 4 | import com.intellij.psi.codeStyle.CodeStyleSettings; 5 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 6 | 7 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.BOOLEAN; 8 | 9 | public class CdsCodeStylePanelFactory { 10 | 11 | private final CodeStyleSettings settings; 12 | 13 | CdsCodeStylePanelFactory(CodeStyleSettings settings) { 14 | this.settings = settings; 15 | } 16 | 17 | public CodeStyleAbstractPanel createTabPanel(Category category) { 18 | boolean allBoolean = CdsCodeStyleSettings.OPTIONS.values().stream() 19 | .filter(option -> option.category == category) 20 | .allMatch(option -> option.type == BOOLEAN); 21 | 22 | return allBoolean 23 | ? new CdsCodeStyleCheckboxesPanel(settings, category) 24 | : new CdsCodeStyleTabularPanel(settings, category); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lifecycle/PluginLifecycleListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lifecycle; 2 | 3 | import com.intellij.ide.plugins.DynamicPluginListener; 4 | import com.intellij.ide.plugins.IdeaPluginDescriptor; 5 | import com.sap.cap.cds.intellij.CdsPlugin; 6 | import com.sap.cap.cds.intellij.textmate.CdsTextMateBundleService; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import static com.intellij.openapi.application.ApplicationManager.getApplication; 10 | 11 | public class PluginLifecycleListener implements DynamicPluginListener { 12 | 13 | @Override 14 | public void pluginLoaded(@NotNull IdeaPluginDescriptor pluginDescriptor) { 15 | getApplication().getService(CdsTextMateBundleService.class).registerBundle(); 16 | } 17 | 18 | @Override 19 | public void beforePluginUnload(@NotNull IdeaPluginDescriptor pluginDescriptor, boolean isUpdate) { 20 | if (!CdsPlugin.ID.equals(pluginDescriptor.getPluginId().toString())) { 21 | return; 22 | } 23 | getApplication().getService(CdsTextMateBundleService.class).unregisterBundle(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | description: Request a missing feature 4 | title: "[FEATURE] <title>" 5 | labels: ["feature request", "new"] 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | description: | 11 | A concise description of the feature you're interested in. If possible include use cases, pointers to CAPire, and how it would benefit this tool and its users. 12 | validations: 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Suggested Solution 17 | description: | 18 | Describe your proposed solution. A clear and concise description of what you want to happen. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: Alternatives 24 | description: | 25 | Describe alternatives you can see for this problem. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Additional Context 31 | description: | 32 | Add any other context about the problem here. 33 | validations: 34 | required: false -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lspServer/CopyLspServerLogPathAction.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lspServer; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.awt.datatransfer.StringSelection; 8 | import java.io.File; 9 | import java.util.Optional; 10 | 11 | import static com.sap.cap.cds.intellij.util.ServerLogsUtil.findLspServerLogFile; 12 | 13 | public class CopyLspServerLogPathAction extends AnAction { 14 | 15 | @Override 16 | public void actionPerformed(@NotNull AnActionEvent e) { 17 | Optional<File> logFile = findLspServerLogFile(e.getProject()); 18 | if (logFile.isEmpty()) { 19 | UserError.show("Cannot find LSP server log files."); 20 | return; 21 | } 22 | StringSelection stringSelection = new StringSelection(logFile.get().getAbsolutePath()); 23 | java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); 24 | } 25 | 26 | @Override 27 | public void update(@NotNull AnActionEvent e) { 28 | boolean isProjectOpen = e.getProject() != null; 29 | e.getPresentation().setEnabledAndVisible(isProjectOpen); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp/CopyStdioLogPathAction.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.sap.cap.cds.intellij.lspServer.UserError; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.awt.datatransfer.StringSelection; 9 | import java.io.File; 10 | import java.util.Optional; 11 | 12 | import static com.sap.cap.cds.intellij.util.StdioLogsUtil.findStdioLogFile; 13 | import static com.sap.cap.cds.intellij.util.SupportUtil.isDebugCdsLsp; 14 | 15 | public class CopyStdioLogPathAction extends AnAction { 16 | 17 | @Override 18 | public void actionPerformed(@NotNull AnActionEvent e) { 19 | Optional<File> logFile = findStdioLogFile(); 20 | if (logFile.isEmpty()) { 21 | UserError.show("Stdio log file not found."); 22 | return; 23 | } 24 | 25 | StringSelection stringSelection = new StringSelection(logFile.get().getAbsolutePath()); 26 | java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); 27 | } 28 | 29 | @Override 30 | public void update(@NotNull AnActionEvent e) { 31 | e.getPresentation().setEnabledAndVisible(isDebugCdsLsp()); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lspServer/ShowLspServerLogsAction.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lspServer; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.project.Project; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.File; 9 | import java.util.Optional; 10 | 11 | import static com.sap.cap.cds.intellij.util.EditorUtil.openFileInEditor; 12 | import static com.sap.cap.cds.intellij.util.ServerLogsUtil.findLspServerLogFile; 13 | 14 | public class ShowLspServerLogsAction extends AnAction { 15 | 16 | @Override 17 | public void actionPerformed(@NotNull AnActionEvent e) { 18 | Project project = e.getProject(); 19 | if (project == null) { 20 | return; 21 | } 22 | 23 | Optional<File> logFile = findLspServerLogFile(project); 24 | if (logFile.isEmpty()) { 25 | UserError.show("Cannot find LSP server log files."); 26 | return; 27 | } 28 | 29 | openFileInEditor(project, logFile.get()); 30 | } 31 | 32 | @Override 33 | public void update(@NotNull AnActionEvent e) { 34 | Project project = e.getProject(); 35 | e.getPresentation().setEnabledAndVisible(project != null); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/CdsFileType.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij; 2 | 3 | import com.intellij.openapi.fileTypes.LanguageFileType; 4 | import com.intellij.openapi.util.NlsContexts; 5 | import com.intellij.openapi.util.NlsSafe; 6 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 7 | import org.jetbrains.annotations.NonNls; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.plugins.textmate.TextMateBackedFileType; 10 | 11 | import javax.swing.*; 12 | 13 | public final class CdsFileType extends LanguageFileType implements TextMateBackedFileType { 14 | 15 | public static final CdsFileType INSTANCE = new CdsFileType(); 16 | public static final String EXTENSION = "cds"; 17 | public static final String DOT_EXTENSION = ".cds"; 18 | 19 | private CdsFileType() { 20 | super(CdsLanguage.INSTANCE); 21 | } 22 | 23 | @Override 24 | public @NonNls @NotNull String getName() { 25 | return "cds"; 26 | } 27 | 28 | @Override 29 | public @NlsContexts.Label @NotNull String getDescription() { 30 | return "CDS file"; 31 | } 32 | 33 | @Override 34 | public @NlsSafe @NotNull String getDefaultExtension() { 35 | return EXTENSION; 36 | } 37 | 38 | @Override 39 | public Icon getIcon() { 40 | return CdsIcons.FILE; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.vfs.VirtualFile; 5 | import com.sap.cap.cds.intellij.settings.JsonSettingsFileListener; 6 | import com.sap.cap.cds.intellij.util.LoggerScope; 7 | 8 | import java.util.function.Predicate; 9 | 10 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; 11 | import static com.sap.cap.cds.intellij.util.LoggerScope.CODE_STYLE; 12 | 13 | public class CdsPrettierJsonListener extends JsonSettingsFileListener { 14 | 15 | @Override 16 | protected Predicate<VirtualFile> fileFilter() { 17 | return file -> file.getName().equals(PRETTIER_JSON); 18 | } 19 | 20 | @Override 21 | protected void handleFileChange(Project project) { 22 | CdsCodeStyleSettingsService service = project.getService(CdsCodeStyleSettingsService.class); 23 | if (service.isSettingsFileChanged()) { 24 | service.updateProjectSettingsFromFile(); 25 | } 26 | } 27 | 28 | @Override 29 | protected LoggerScope getLoggerScope() { 30 | return CODE_STYLE; 31 | } 32 | 33 | @Override 34 | protected String getDebugMessage() { 35 | return ".cdsprettier.json changed"; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp/ShowStdioLogsAction.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.project.Project; 6 | import com.sap.cap.cds.intellij.lspServer.UserError; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.File; 10 | import java.util.Optional; 11 | 12 | import static com.sap.cap.cds.intellij.util.EditorUtil.openFileInEditor; 13 | import static com.sap.cap.cds.intellij.util.StdioLogsUtil.findStdioLogFile; 14 | import static com.sap.cap.cds.intellij.util.SupportUtil.isDebugCdsLsp; 15 | 16 | public class ShowStdioLogsAction extends AnAction { 17 | 18 | @Override 19 | public void actionPerformed(@NotNull AnActionEvent e) { 20 | Project project = e.getProject(); 21 | if (project == null) { 22 | return; 23 | } 24 | 25 | Optional<File> logFile = findStdioLogFile(); 26 | if (logFile.isEmpty()) { 27 | UserError.show("Cannot find stdio log file."); 28 | return; 29 | } 30 | 31 | openFileInEditor(project, logFile.get()); 32 | } 33 | 34 | @Override 35 | public void update(@NotNull AnActionEvent e) { 36 | e.getPresentation().setEnabledAndVisible(isDebugCdsLsp()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/usersettings/CdsUserSettingsListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.usersettings; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.vfs.VirtualFile; 5 | import com.sap.cap.cds.intellij.settings.JsonSettingsFileListener; 6 | import com.sap.cap.cds.intellij.util.LoggerScope; 7 | 8 | import java.util.function.Predicate; 9 | 10 | import static com.sap.cap.cds.intellij.usersettings.CdsUserSettings.USER_SETTINGS_JSON; 11 | import static com.sap.cap.cds.intellij.util.LoggerScope.USER_SETTINGS; 12 | 13 | public class CdsUserSettingsListener extends JsonSettingsFileListener { 14 | 15 | @Override 16 | protected Predicate<VirtualFile> fileFilter() { 17 | return file -> file.getPath().endsWith(".settings.json") && file.getPath().contains(".cds-lsp"); 18 | } 19 | 20 | @Override 21 | protected void handleFileChange(Project project) { 22 | CdsUserSettingsService service = project.getService(CdsUserSettingsService.class); 23 | if (service.isSettingsFileChanged()) { 24 | service.updateProjectSettingsFromFile(); 25 | } 26 | } 27 | 28 | @Override 29 | protected LoggerScope getLoggerScope() { 30 | return USER_SETTINGS; 31 | } 32 | 33 | @Override 34 | protected String getDebugMessage() { 35 | return "%s changed".formatted(USER_SETTINGS_JSON); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanel.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.TabbedLanguageCodeStylePanel; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.project.ProjectManager; 6 | import com.intellij.psi.codeStyle.CodeStyleSettings; 7 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | public class CdsCodeStyleMainPanel extends TabbedLanguageCodeStylePanel { 11 | 12 | protected CdsCodeStyleMainPanel(CodeStyleSettings currentSettings, @NotNull CodeStyleSettings settings) { 13 | super(CdsLanguage.INSTANCE, currentSettings, settings); 14 | 15 | // Enable saving of code-style settings 16 | for (Project project : ProjectManager.getInstance().getOpenProjects()) { 17 | project.getService(CdsCodeStyleSettingsService.class); 18 | } 19 | } 20 | 21 | @Override 22 | protected void initTabs(CodeStyleSettings settings) { 23 | CdsCodeStylePanelFactory factory = new CdsCodeStylePanelFactory(settings); 24 | 25 | CdsCodeStyleSettings.CATEGORY_GROUPS.keySet().forEach(category -> { 26 | addTab(factory.createTabPanel(category)); 27 | }); 28 | } 29 | 30 | @Override 31 | protected String getPreviewText() { 32 | return CdsCodeStyleSettings.SAMPLE_SRC; 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanelTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.psi.codeStyle.CodeStyleSettings; 5 | import com.intellij.testFramework.LightPlatformTestCase; 6 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 7 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 8 | 9 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.SPACES; 10 | 11 | public class CdsCodeStyleTabularPanelTest extends LightPlatformTestCase { 12 | 13 | private final Category category = SPACES; 14 | private CdsCodeStyleTabularPanel panel; 15 | 16 | @Override 17 | protected void tearDown() throws Exception { 18 | panel.dispose(); 19 | super.tearDown(); 20 | } 21 | 22 | public void testCdsCodeStyleTabularPanelProperties() { 23 | CodeStyleSettings settings = CodeStyle.createTestSettings(); 24 | panel = new CdsCodeStyleTabularPanel(settings, category); 25 | 26 | assertEquals(category, panel.getCategory()); 27 | assertEquals(CdsLanguage.INSTANCE, panel.getDefaultLanguage()); 28 | assertEquals(CdsCodeStyleSettings.SAMPLE_SRC, panel.getPreviewText()); 29 | assertEquals(category.getSettingsType(), panel.getSettingsType()); 30 | assertEquals(category.getTitle(), panel.getTabTitle()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanelTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.psi.codeStyle.CodeStyleSettings; 5 | import com.intellij.testFramework.LightPlatformTestCase; 6 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 7 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 8 | 9 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.SPACES; 10 | 11 | public class CdsCodeStyleCheckboxesPanelTest extends LightPlatformTestCase { 12 | 13 | private final Category category = SPACES; 14 | private CdsCodeStyleCheckboxesPanel panel; 15 | 16 | @Override 17 | protected void tearDown() throws Exception { 18 | panel.dispose(); 19 | super.tearDown(); 20 | } 21 | 22 | public void testCdsCodeStyleCheckboxesPanelProperties() { 23 | CodeStyleSettings settings = CodeStyle.createTestSettings(); 24 | panel = new CdsCodeStyleCheckboxesPanel(settings, category); 25 | 26 | assertEquals(category, panel.getCategory()); 27 | assertEquals(CdsLanguage.INSTANCE, panel.getDefaultLanguage()); 28 | assertEquals(CdsCodeStyleSettings.SAMPLE_SRC, panel.getPreviewText()); 29 | assertEquals(category.getSettingsType(), panel.getSettingsType()); 30 | assertEquals(category.getTitle(), panel.getTabTitle()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/usersettings/CdsUserSettingsConfigurable.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.usersettings; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import com.intellij.openapi.project.Project; 5 | import org.jetbrains.annotations.Nls; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import javax.swing.*; 9 | 10 | public class CdsUserSettingsConfigurable implements Configurable { 11 | 12 | private final Project project; 13 | private CdsUserSettingsComponent component; 14 | 15 | public CdsUserSettingsConfigurable(Project project) { 16 | this.project = project; 17 | } 18 | 19 | @Override 20 | public @Nls String getDisplayName() { 21 | return "CDS Language Server"; 22 | } 23 | 24 | @Override 25 | public @Nullable JComponent createComponent() { 26 | component = new CdsUserSettingsComponent(project); 27 | return component.getPanel(); 28 | } 29 | 30 | @Override 31 | public boolean isModified() { 32 | return component != null && component.isModified(); 33 | } 34 | 35 | @Override 36 | public void apply() { 37 | if (component != null) { 38 | component.apply(); 39 | } 40 | } 41 | 42 | @Override 43 | public void reset() { 44 | if (component != null) { 45 | component.reset(); 46 | } 47 | } 48 | 49 | @Override 50 | public void disposeUIResources() { 51 | component = null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/util/JsonUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.testFramework.LightPlatformTestCase; 4 | import org.json.JSONObject; 5 | 6 | import static com.sap.cap.cds.intellij.util.JsonUtil.isJsonEqual; 7 | import static com.sap.cap.cds.intellij.util.JsonUtil.toSortedString; 8 | 9 | public class JsonUtilTest extends LightPlatformTestCase { 10 | 11 | public void testIsJsonEqual() { 12 | assertTrue(isJsonEqual("", "")); 13 | assertTrue(isJsonEqual("{}", "{}")); 14 | assertTrue(isJsonEqual("{ \"a\": 1}", "{\"a\": 1}")); 15 | assertTrue(isJsonEqual("{\"a\": 1, \"b\": 2}", "{\"b\": 2, \"a\": 1}")); 16 | 17 | assertFalse(isJsonEqual("{}", "")); 18 | assertFalse(isJsonEqual("", "{}")); 19 | assertFalse(isJsonEqual("{}", "{\"a\": 1}")); 20 | assertFalse(isJsonEqual("{\"a\": 1}", "{\"a\": 2}")); 21 | assertFalse(isJsonEqual("{\"a\": 1}", "{\"b\": 1}")); 22 | assertFalse(isJsonEqual("{\"a\": 1, \"b\": 2}", "{\"a\": 1}")); 23 | } 24 | 25 | public void testToSortedString() { 26 | assertEquals("{}", toSortedString(new JSONObject("{}"))); 27 | assertEquals("{\"a\": 1}", toSortedString(new JSONObject("{\"a\": 1}"))); 28 | assertEquals("{\n \"a\": 1,\n \"b\": 2\n}", toSortedString(new JSONObject("{\"b\": 2, \"a\": 1}"))); 29 | assertEquals("{\n \"a\": 1,\n \"b\": 2\n}", toSortedString(new JSONObject("{\"a\": 1, \"b\": 2}"))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/actions/gradle-run/action.yml: -------------------------------------------------------------------------------- 1 | name: gradle-run 2 | description: Build the plugin using gradle 3 | 4 | inputs: 5 | steps: 6 | description: 'The run steps to perform' 7 | required: true 8 | 9 | runs: 10 | using: composite 11 | 12 | steps: 13 | - name: Install fontconfig 14 | # required for headless gradle 15 | run: | 16 | apt-get -y update 17 | apt-get -y install fontconfig 18 | shell: bash 19 | env: 20 | DEBIAN_FRONTEND: noninteractive 21 | TZ: Etc/UTC 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | 27 | - uses: actions/setup-java@v4 28 | with: 29 | distribution: zulu 30 | java-version: 21 31 | 32 | - name: Test 33 | if: ${{ contains(inputs.steps, 'test') }} 34 | shell: bash 35 | run: | 36 | ./gradlew test --info --no-daemon 37 | 38 | - name: Upload test results 39 | uses: actions/upload-artifact@v4 40 | if: ${{ contains(inputs.steps, 'test') }} 41 | with: 42 | name: test-results 43 | path: ./build/test-results/test 44 | 45 | - name: Build 46 | if: ${{ contains(inputs.steps, 'build') }} 47 | shell: bash 48 | run: | 49 | ./gradlew verifyPlugin buildPlugin --no-daemon 50 | ls -la build/distributions 51 | 52 | - name: Publish 53 | if: ${{ contains(inputs.steps, 'publish') }} 54 | shell: bash 55 | run: | 56 | ./gradlew publishPlugin --info --no-daemon 57 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [release] 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: release-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | release-build: 17 | runs-on: ubuntu-latest 18 | container: 19 | image: ubuntu:22.04 20 | steps: 21 | - uses: actions/checkout@v6 22 | 23 | - uses: ./.github/actions/gradle-run 24 | with: 25 | steps: |- 26 | test 27 | build 28 | 29 | - id: setenv 30 | run: | 31 | echo RELEASE_TAG=$(awk '/pluginVersion/ { print $3 }' gradle.properties) >> $GITHUB_OUTPUT 32 | echo "TODAY_DATE=$(date +%Y-%m-%d)" >> $GITHUB_OUTPUT 33 | echo "CHANGE_LOG=$(awk '/<change-notes>/ { a=1; gsub(/<change-notes>[ \t]*<!\[CDATA\[[ \t]*/, ""); } a==1 && /</ { a=2 } a==2 { if (/<\/change-notes>/) x=1; gsub(/^[ \t]+|\]\].*/, ""); gsub(/[\"\`]/, "\\&"); ORS=" "; print; if (x==1) exit }' src/main/resources/META-INF/plugin.xml)" >> $GITHUB_OUTPUT 34 | 35 | - run: | 36 | echo "${{ steps.setenv.outputs.RELEASE_TAG }}" 37 | echo "${{ steps.setenv.outputs.TODAY_DATE }}" 38 | echo "${{ steps.setenv.outputs.CHANGE_LOG }}" 39 | 40 | - uses: ncipollo/release-action@v1 41 | with: 42 | tag: ${{ steps.setenv.outputs.RELEASE_TAG }} 43 | body: Build ${{ github.run_number }}<br>${{ steps.setenv.outputs.CHANGE_LOG }} 44 | artifacts: build/distributions/cds-intellij.zip 45 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/JsonSettingsService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.sap.cap.cds.intellij.util.Logger; 5 | import com.sap.cap.cds.intellij.util.LoggerScope; 6 | 7 | import static com.sap.cap.cds.intellij.util.Logger.logger; 8 | 9 | public abstract class JsonSettingsService<T> { 10 | 11 | protected final Project project; 12 | protected final Logger logger; 13 | protected final JsonSettingsManager<T> jsonManager; 14 | 15 | protected JsonSettingsService(Project project, String fileName, LoggerScope loggerScope) { 16 | this.project = project; 17 | this.logger = logger(project, loggerScope); 18 | this.jsonManager = createJsonManager(project); 19 | } 20 | 21 | protected abstract JsonSettingsManager<T> createJsonManager(Project project); 22 | protected abstract T getSettings(); 23 | 24 | public boolean isSettingsFilePresent() { 25 | return jsonManager.isJsonFilePresent(); 26 | } 27 | 28 | public boolean isSettingsFileChanged() { 29 | return jsonManager.isSettingsFileChanged(); 30 | } 31 | 32 | public void updateSettingsFile() { 33 | jsonManager.saveSettingsToFile(getSettings()); 34 | } 35 | 36 | public void updateProjectSettingsFromFile() { 37 | if (isSettingsFilePresent()) { 38 | logger.debug("Loading settings from file"); 39 | jsonManager.loadSettingsFromFile(getSettings()); 40 | } else { 41 | logger.debug("No settings file found"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonManager.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.sap.cap.cds.intellij.settings.JsonSettingsManager; 5 | import com.sap.cap.cds.intellij.util.LoggerScope; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.json.JSONException; 8 | 9 | public class CdsPrettierJsonManager extends JsonSettingsManager<CdsCodeStyleSettings> { 10 | 11 | public CdsPrettierJsonManager(Project project) { 12 | super(project, ".cdsprettier.json", LoggerScope.CODE_STYLE); 13 | } 14 | 15 | @Override 16 | public void loadSettingsFromFile(@NotNull CdsCodeStyleSettings settings) { 17 | logger.debug("Loading settings from file"); 18 | String json = readJson(); 19 | if (!json.isEmpty()) { 20 | try { 21 | settings.loadFrom(json); 22 | } catch (JSONException e) { 23 | logger.error("Failed to parse JSON '%s'".formatted(json), e); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void saveSettingsToFile(@NotNull CdsCodeStyleSettings settings) { 30 | if (!isJsonFilePresent() && settings.isDefault()) { 31 | logger.debug("Settings are default, skipping save"); 32 | return; 33 | } 34 | if (!jsonCached.isEmpty() && settings.equals(jsonCached)) { 35 | logger.debug("Settings are equal, skipping save"); 36 | return; 37 | } 38 | String json = settings.getLoadedOrNonDefaultSettings(); 39 | writeJson(json); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | # Plugin name part used in generated ZIP and name of plugin directory in sandbox directory -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-pluginname 4 | # Keep pluginName in sync with the one declared in Java! 5 | archiveName = cds-intellij.zip 6 | pluginRepositoryUrl = https://github.com/cap-js/cds-intellij 7 | # SemVer format -> https://semver.org 8 | pluginVersion = 2.0.1 9 | 10 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 11 | pluginSinceBuild = 251 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = IC 15 | platformVersion = 2025.1 16 | 17 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 18 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 19 | # platformPlugins = 20 | 21 | # Gradle Releases -> https://github.com/gradle/gradle/releases 22 | gradleVersion = 9.1.0 23 | 24 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 25 | org.gradle.configuration-cache = true 26 | 27 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 28 | org.gradle.caching = true 29 | 30 | org.gradle.jvmargs=-Xmx2g 31 | org.jetbrains.intellij.platform.useCacheRedirector=true 32 | 33 | javaVersion = 21 34 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij; 2 | 3 | import com.intellij.openapi.editor.Document; 4 | import com.intellij.openapi.editor.EditorFactory; 5 | import com.intellij.testFramework.ExpectedHighlightingData; 6 | import com.intellij.testFramework.fixtures.CodeInsightTestFixture; 7 | import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | 14 | import static java.nio.file.Files.readString; 15 | 16 | public class TestUtil { 17 | 18 | public static void checkDiagnostics(@NotNull CodeInsightTestFixture fixture) { 19 | try { 20 | String testDataPath = fixture.getTestDataPath(); 21 | String testFileName = fixture.getFile().getName(); 22 | String expectedFileName = testFileName.replace(".cds", ".expected.cds"); 23 | Path expectedFilePath = Paths.get(testDataPath, expectedFileName); 24 | String expectedFileContent = readString(expectedFilePath); 25 | Document expectedDocumentWithMarkup = EditorFactory.getInstance().createDocument(expectedFileContent); 26 | ExpectedHighlightingData expectation = new ExpectedHighlightingData(expectedDocumentWithMarkup); 27 | expectation.init(); 28 | fixture.doHighlighting(); 29 | ((CodeInsightTestFixtureImpl) fixture).collectAndCheckHighlighting(expectation); 30 | 31 | } catch (IOException e) { 32 | throw new RuntimeException("Failed to read expected file for diagnostics: " + fixture.getFile().getName(), e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "cds-intellij" 3 | SPDX-PackageSupplier = "cap@sap.com" 4 | SPDX-PackageDownloadLocation = "https://github.com/cap-js/cds-intellij" 5 | SPDX-PackageComment = "The code in this project may include calls to APIs (\"API Calls\") of\n SAP or third-party products or services developed outside of this project\n (\"External Products\").\n \"APIs\" means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project's code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls." 6 | 7 | [[annotations]] 8 | path = "**" 9 | precedence = "aggregate" 10 | SPDX-FileCopyrightText = "2019-2024 SAP SE or an SAP affiliate company and cds-intellij contributors" 11 | SPDX-License-Identifier = "Apache-2.0" 12 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/AppSettings.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.components.PersistentStateComponent; 5 | import com.intellij.openapi.components.State; 6 | import com.intellij.openapi.components.Storage; 7 | import com.sap.cap.cds.intellij.util.NodeJsUtil; 8 | import groovyjarjarantlr4.v4.runtime.misc.NotNull; 9 | import org.jetbrains.annotations.NonNls; 10 | 11 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.checkInterpreter; 12 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.getInterpreterFromPathOrRegistered; 13 | 14 | @State( 15 | name = "com.sap.cap.cds.intellij.settings.AppSettings", 16 | storages = @Storage("SapCdsLanguageSupportPlugin.xml") 17 | ) 18 | public final class AppSettings 19 | implements PersistentStateComponent<AppSettings.State> { 20 | 21 | private State myState = new State(); 22 | 23 | public static AppSettings getInstance() { 24 | AppSettings instance = ApplicationManager.getApplication().getService(AppSettings.class); 25 | fixNodeJsPath(instance.getState()); 26 | return instance; 27 | } 28 | 29 | private static void fixNodeJsPath(State state) { 30 | if (state != null && checkInterpreter(state.nodeJsPath) != NodeJsUtil.InterpreterStatus.OK) { 31 | state.nodeJsPath = getInterpreterFromPathOrRegistered(); 32 | } 33 | } 34 | 35 | @Override 36 | public State getState() { 37 | return myState; 38 | } 39 | 40 | @Override 41 | public void loadState(@NotNull State state) { 42 | myState = state; 43 | } 44 | 45 | public static class State { 46 | @NonNls @NotNull 47 | public String nodeJsPath = getInterpreterFromPathOrRegistered(); // on first plugin start 48 | public String cdsLspEnv = ""; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /.github/actions/prepare-cds-lsp-local/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Local cds-lsp 2 | description: Conditionally clone, build, and pack a local cds-lsp dependency for npm file-url install. 3 | 4 | inputs: 5 | gh_pat: 6 | description: GitHub Personal Access Token 7 | required: false 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - shell: bash 13 | run: | 14 | apt-get update 15 | apt-get install -y jq 16 | 17 | - shell: bash 18 | run: | 19 | dep=$(jq -r '.dependencies["@sap/cds-lsp"]' lsp/package.json) 20 | branch=$(jq -r '.["cds-lsp-branch"] // "main"' lsp/package.json) 21 | echo "Dependency: $dep" 22 | echo "Branch: $branch" 23 | if [[ $dep == file:* ]]; then 24 | echo "will_prepare=true" >> $GITHUB_ENV 25 | echo "cds_lsp_branch=$branch" >> $GITHUB_ENV 26 | else 27 | echo "will_prepare=false" >> $GITHUB_ENV 28 | fi 29 | 30 | - shell: bash 31 | if: env.will_prepare == 'true' 32 | run: | 33 | apt-get install -y git 34 | 35 | - shell: bash 36 | if: env.will_prepare == 'true' 37 | run: | 38 | git config --global url."https://${GH_PAT}:x-oauth-basic@github.tools.sap/".insteadOf "https://github.tools.sap/" 39 | env: 40 | GH_PAT: ${{ inputs.gh_pat }} 41 | 42 | - uses: actions/setup-node@v4 43 | if: env.will_prepare == 'true' 44 | with: 45 | node-version: 22 46 | 47 | - shell: bash 48 | if: env.will_prepare == 'true' 49 | run: | 50 | git clone -b "${cds_lsp_branch}" https://github.tools.sap/cap/cds-lsp.git ../cds-lsp 51 | cd ../cds-lsp 52 | rm -f .npmrc 53 | npm i --no-package-lock 54 | npm run compile 55 | npm pack 56 | TARBALL=$(echo *.tgz) 57 | cd ../cds-intellij/lsp 58 | jq --arg tgz "file:../../cds-lsp/$TARBALL" '.dependencies["@sap/cds-lsp"]=$tgz' package.json > package.json.tmp && mv package.json.tmp package.json 59 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/command/AnalyzeDependenciesAction.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.command; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.redhat.devtools.lsp4ij.commands.CommandExecutor; 7 | import com.redhat.devtools.lsp4ij.commands.LSPCommandContext; 8 | import com.sap.cap.cds.intellij.lsp4ij.CdsLanguageServer; 9 | import org.eclipse.lsp4j.Command; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.util.Collections; 13 | 14 | public class AnalyzeDependenciesAction extends AnAction { 15 | @Override 16 | public void actionPerformed(@NotNull AnActionEvent anActionEvent) { 17 | Object data = anActionEvent.getDataContext(); 18 | 19 | 20 | // const startModelUri = this.getServerModelUri(uri); 21 | 22 | // const command: CommandName = 'analyze-dependencies'; 23 | // const params: IAnalyzeDependenciesParams = { 24 | // command, 25 | // arguments: [{ 26 | // startModelUri, 27 | // outputFormat: 'svg', 28 | // detailMode 29 | // }] 30 | // }; 31 | // const svg = await this.getClient().sendRequest('workspace/executeCommand', params) as string; 32 | 33 | 34 | var project = anActionEvent.getProject(); 35 | if (project == null) { return; } 36 | 37 | Command command = new Command("Analyze Dependencies", "analyze-dependencies"); 38 | LSPCommandContext commandContext = new LSPCommandContext(command, project); 39 | commandContext.setPreferredLanguageServerId(CdsLanguageServer.ID); 40 | command.setArguments(Collections.singletonList(new JsonObject())); // TODO: Add IAnalyzeDependenciesParams 41 | CommandExecutor.executeCommand(commandContext) 42 | .response() 43 | .thenAccept(r -> { 44 | // Do something with the workspace/executeCommand Object response 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.assets/IntelliJ_IDEA_icon.svg: -------------------------------------------------------------------------------- 1 | <svg fill="none" height="96" viewBox="0 0 96 96" width="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="7.10008" x2="54.8801" y1="54.7099" y2="52.2799"><stop offset=".09" stop-color="#fc801d"/><stop offset=".23" stop-color="#b07e61"/><stop offset=".41" stop-color="#567db2"/><stop offset=".53" stop-color="#1d7ce6"/><stop offset=".59" stop-color="#087cfa"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="84.98" x2="69.05" y1="49.6199" y2="2.02004"><stop offset="0" stop-color="#fe2857"/><stop offset=".08" stop-color="#ca3978"/><stop offset=".16" stop-color="#9d4896"/><stop offset=".25" stop-color="#7556b1"/><stop offset=".34" stop-color="#5362c8"/><stop offset=".44" stop-color="#376bda"/><stop offset=".54" stop-color="#2272e8"/><stop offset=".66" stop-color="#1378f2"/><stop offset=".79" stop-color="#0a7bf8"/><stop offset="1" stop-color="#087cfa"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="14.65" x2="74.7299" y1="22.11" y2="121.49"><stop offset="0" stop-color="#fe2857"/><stop offset=".08" stop-color="#fe295f"/><stop offset=".21" stop-color="#fe2d76"/><stop offset=".3" stop-color="#ff318c"/><stop offset=".38" stop-color="#e93795"/><stop offset=".55" stop-color="#b148ae"/><stop offset=".79" stop-color="#5963d5"/><stop offset="1" stop-color="#087cfa"/></linearGradient><path d="m15.2101 67.71-14.13002-11.1499 8.31-15.3902 12.49002 4.1802z" fill="url(#a)"/><path d="m95.9999 25.5901-1.73 55.6099-36.98 14.8-20.14-13z" fill="#087cfa"/><path d="m96 25.5901-18.3 17.8498-23.49-28.8298 11.6-13.04003z" fill="url(#b)"/><path d="m37.15 83-29.41 10.6299 6.17-21.5698 7.97-26.71-21.88-7.3201 13.91-38.03 31.41 3.69995 32.38 39.73995z" fill="url(#c)"/><path d="m78 18h-60v60h60z" fill="#000"/><path d="m26 70h24v-4h-24zm20.58-44.52v15.98c0 .49-.1.92-.31 1.3-.21.37-.5.6601-.87.8701s-.81.3098-1.3.3098h-3.73v3.9402h4.24c1.23 0 2.31-.25 3.26-.76s1.68-1.2201 2.2-2.1501.78-2 .78-3.22v-16.27zm-8.15 18.57h-3.18v-14.75h3.18v-3.82h-10.58v3.82h3.2v14.75h-3.2v3.8201h10.58z" fill="#fff"/></svg> -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePanelFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyleAbstractPanel; 4 | import com.intellij.testFramework.LightPlatformTestCase; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import static com.intellij.application.options.CodeStyle.createTestSettings; 10 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.ALIGNMENT; 11 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.WRAPPING_AND_BRACES; 12 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.BOOLEAN; 13 | import static org.junit.Assume.assumeTrue; 14 | 15 | public class CdsCodeStylePanelFactoryTest extends LightPlatformTestCase { 16 | 17 | List<CodeStyleAbstractPanel> panels = new ArrayList<>(); 18 | 19 | @Override 20 | protected void tearDown() throws Exception { 21 | panels.forEach(CodeStyleAbstractPanel::dispose); 22 | super.tearDown(); 23 | } 24 | 25 | public void testCdsCodeStylePanelFactoryAllBoolean() { 26 | CdsCodeStyleOption.Category allBooleanCategory = ALIGNMENT; 27 | assumeTrue("Assuming all options in category to be boolean", CdsCodeStyleSettings.OPTIONS.values().stream() 28 | .filter(option -> option.category == allBooleanCategory) 29 | .allMatch(option -> option.type == BOOLEAN)); 30 | 31 | CdsCodeStylePanelFactory factory = new CdsCodeStylePanelFactory(createTestSettings()); 32 | CodeStyleAbstractPanel panel = factory.createTabPanel(allBooleanCategory); 33 | assertEquals(CdsCodeStyleCheckboxesPanel.class, panel.getClass()); 34 | panels.add(panel); 35 | } 36 | 37 | public void testCdsCodeStylePanelFactoryNotAllBoolean() { 38 | CdsCodeStylePanelFactory factory = new CdsCodeStylePanelFactory(createTestSettings()); 39 | CodeStyleAbstractPanel panel = factory.createTabPanel(WRAPPING_AND_BRACES); 40 | assertEquals(CdsCodeStyleTabularPanel.class, panel.getClass()); 41 | panels.add(panel); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleOptionTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.testFramework.LightPlatformTestCase; 4 | 5 | import java.util.List; 6 | 7 | import static com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.OptionAnchor.AFTER; 8 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.*; 9 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.*; 10 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettings.CqlKeywordCapitalization.AS_IS; 11 | import static java.util.Arrays.stream; 12 | import static org.junit.Assert.assertArrayEquals; 13 | 14 | public class CdsCodeStyleOptionTest extends LightPlatformTestCase { 15 | 16 | public void testCdsCodeStyleOptionBoolean() { 17 | CdsCodeStyleOption option = new CdsCodeStyleOption("name", BOOLEAN, false, "label", "group", COMMENTS, "parent", null); 18 | 19 | assertEquals(List.of(), option.children); 20 | assertEquals(AFTER, option.getAnchor()); 21 | assertEquals("parent", option.getAnchorOptionName()); 22 | } 23 | 24 | public void testCdsCodeStyleOptionEnum() { 25 | CdsCodeStyleOption option = new CdsCodeStyleOption("name", ENUM, AS_IS, "label", "group", ALIGNMENT, null, null, CdsCodeStyleSettings.CqlKeywordCapitalization.values()); 26 | 27 | assertEquals(List.of(), option.children); 28 | assertNull(option.getAnchor()); 29 | assertNull(option.getAnchorOptionName()); 30 | assertArrayEquals(stream(CdsCodeStyleSettings.CqlKeywordCapitalization.values()).map(CdsCodeStyleSettings.Enum::getLabel).toArray(String[]::new), option.getValuesLabels()); 31 | assertArrayEquals(stream(CdsCodeStyleSettings.CqlKeywordCapitalization.values()).map(CdsCodeStyleSettings.Enum::getId).mapToInt(Integer::intValue).toArray(), option.getValuesIds()); 32 | } 33 | 34 | public void testCdsCodeStyleOptionInt() { 35 | CdsCodeStyleOption option = new CdsCodeStyleOption("name", INT, 0, "label", "group", BLANK_LINES, null, List.of("child1", "child2")); 36 | 37 | assertEquals(List.of("child1", "child2"), option.children); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsLanguageServer.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.execution.configurations.GeneralCommandLine; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | import com.redhat.devtools.lsp4ij.LanguageServerManager; 7 | import com.redhat.devtools.lsp4ij.server.CannotStartProcessException; 8 | import com.redhat.devtools.lsp4ij.server.OSProcessStreamConnectionProvider; 9 | import com.sap.cap.cds.intellij.usersettings.CdsUserSettingsService; 10 | import com.sap.cap.cds.intellij.util.Logger; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.function.Supplier; 14 | 15 | 16 | public class CdsLanguageServer extends OSProcessStreamConnectionProvider { 17 | 18 | public static final String ID = "cds"; 19 | public static final String LABEL = "Language Server"; 20 | @NotNull 21 | private final Project project; 22 | private final Supplier<GeneralCommandLine> commandLineSupplier; 23 | 24 | public CdsLanguageServer(@NotNull Project project, Supplier<GeneralCommandLine> commandLineSupplier) { 25 | this.project = project; 26 | this.commandLineSupplier = commandLineSupplier; 27 | super.setCommandLine(commandLineSupplier.get()); 28 | } 29 | 30 | public static void restart(@NotNull Project project) { 31 | Logger.SERVER.debug("Restarting CDS Language Server for project: " + project.getName()); 32 | LanguageServerManager lspManager = LanguageServerManager.getInstance(project); 33 | lspManager.stop(ID); 34 | lspManager.start(ID); 35 | } 36 | 37 | public Object getInitializationOptions(VirtualFile rootUri) { 38 | // TODO: in LSP accept cds top-level node but also accept current non-cds second-level nodes. Then use cds here and also adapt vscode to send cds. 39 | // Idea: LSP pulls settings. This allows to pull non-cds settings if needed 40 | // TODO cds.trace.level (off) -> verbose (set env and restart) 41 | // TODO implement ActiveEditorChanged request 42 | 43 | return project.getService(CdsUserSettingsService.class) 44 | .getSettingsStructured(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsFormattingFeature.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.openapi.editor.Editor; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiFile; 6 | import com.redhat.devtools.lsp4ij.client.features.LSPFormattingFeature; 7 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettings; 8 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | public class CdsFormattingFeature extends LSPFormattingFeature { 13 | 14 | @Override 15 | public boolean isFormatOnCloseBrace(@NotNull PsiFile file) { 16 | // Q: do we get this automatically when onTypeFormatting is enabled, or do we need to do something special? 17 | return super.isFormatOnCloseBrace(file); 18 | } 19 | 20 | @Override 21 | public @Nullable String getFormatOnCloseBraceCharacters(@NotNull PsiFile file) { 22 | // Q: '}' ? 23 | // Q: where to define "the language's default close characters" (that is mentioned in the docs)? 24 | return super.getFormatOnCloseBraceCharacters(file); 25 | } 26 | 27 | @Override 28 | public boolean isFormatOnStatementTerminator(@NotNull PsiFile file) { 29 | return super.isFormatOnStatementTerminator(file); 30 | } 31 | 32 | @Override 33 | public @Nullable String getFormatOnStatementTerminatorCharacters(@NotNull PsiFile file) { 34 | // Q: ';' ? 35 | return super.getFormatOnStatementTerminatorCharacters(file); 36 | } 37 | 38 | @Override 39 | public @Nullable Integer getTabSize(@NotNull PsiFile file, @Nullable Editor editor) { 40 | int defaultValue = (int) CdsCodeStyleSettings.OPTIONS.get("tabSize").defaultValue; 41 | if (editor == null) { 42 | return defaultValue; 43 | } 44 | Project project = editor.getProject(); 45 | if (project == null) { 46 | return defaultValue; 47 | } 48 | CdsCodeStyleSettingsService service = project.getService(CdsCodeStyleSettingsService.class); 49 | if (service == null) { 50 | return defaultValue; 51 | } 52 | return service.getSettings().tabSize; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/ServerLogsUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.File; 7 | import java.util.Arrays; 8 | import java.util.Optional; 9 | 10 | import static java.util.Comparator.comparingLong; 11 | 12 | public class ServerLogsUtil { 13 | 14 | /** 15 | * Finds the newest LSP server log file in the project directory or in the temporary directory. 16 | * @param project the current project 17 | * @return an Optional containing the log file if found, otherwise an empty Optional 18 | */ 19 | public static Optional<File> findLspServerLogFile(Project project) { 20 | Optional<File> localFile = ServerLogsUtil.findLogFileInProjectDir(project); 21 | if (localFile.isPresent()) { 22 | return localFile; 23 | } 24 | 25 | return findLogFileInTempDir(); 26 | } 27 | 28 | private static Optional<File> findLogFileInProjectDir(Project project) { 29 | if (project == null || project.getBasePath() == null) { 30 | return Optional.empty(); 31 | } 32 | 33 | File logDir = new File(project.getBasePath(), ".cds-lsp/logs"); 34 | if (!logDir.exists() || !logDir.isDirectory()) { 35 | return Optional.empty(); 36 | } 37 | 38 | return findRelevantLogFile(logDir); 39 | } 40 | 41 | private static Optional<File> findLogFileInTempDir() { 42 | File tempDir = new File(System.getProperty("java.io.tmpdir")); 43 | File logDir = new File(tempDir, "cdxlsp"); 44 | 45 | if (!logDir.exists() || !logDir.isDirectory()) { 46 | return Optional.empty(); 47 | } 48 | 49 | return findRelevantLogFile(logDir); 50 | } 51 | 52 | private static Optional<File> findRelevantLogFile(@NotNull File dir) { 53 | File[] files = dir.listFiles(); 54 | if (files == null || files.length == 0) { 55 | return Optional.empty(); 56 | } 57 | 58 | return Arrays.stream(files) 59 | .filter(File::isFile) 60 | .filter(file -> !file.getName().matches("^(context.*|.*_context)\\.log$")) 61 | .max(comparingLong(File::lastModified)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsLanguageServerFactory.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.redhat.devtools.lsp4ij.LanguageServerFactory; 5 | import com.redhat.devtools.lsp4ij.client.LanguageClientImpl; 6 | import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; 7 | import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider; 8 | import com.sap.cap.cds.intellij.lspServer.CdsLspServerDescriptor; 9 | import org.eclipse.lsp4j.services.LanguageServer; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public class CdsLanguageServerFactory implements LanguageServerFactory { // TODO: check ExtensionLanguageServerDefinition.getIcon etc. 13 | 14 | @Override 15 | public @NotNull StreamConnectionProvider createConnectionProvider(@NotNull Project project) { 16 | return new CdsLanguageServer(project, CdsLspServerDescriptor::getServerCommandLine); 17 | } 18 | 19 | @Override // If you need to provide client specific features 20 | public @NotNull LanguageClientImpl createLanguageClient(@NotNull Project project) { 21 | return new CdsLanguageClient(project); 22 | } 23 | 24 | @Override // If you need to expose a custom server API 25 | public @NotNull Class<? extends LanguageServer> getServerInterface() { 26 | return CdsCustomServerAPI.class; 27 | } 28 | 29 | @Override 30 | public @NotNull LSPClientFeatures createClientFeatures() { 31 | var features = new CdsLspClientFeatures() 32 | // .setServerInstaller() // TODO: install latest cds-lsp on-the-fly, maybe blue/green like for annotation modeler 33 | // .setEditorFeatures() // TODO: API already documented but not yet released 34 | .setWorkspaceSymbolFeature(new CdsWorkspaceSymbolFeature()) 35 | .setSemanticTokensFeature(new CdsSemanticTokensFeature()) 36 | .setProgressFeature(new CdsProgressFeature()) 37 | .setHoverFeature(new CdsHoverFeature()) 38 | .setFormattingFeature(new CdsFormattingFeature()) 39 | .setFoldingRangeFeature(new CdsFoldingRangeFeature()) 40 | .setDiagnosticFeature(new CdsDiagnosticFeature()) 41 | .setCodeActionFeature(new CdsCodeActionFeature()); 42 | return features; 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/usersettings/CdsUserSettingsJsonManager.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.usersettings; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParser; 6 | import com.google.gson.JsonSyntaxException; 7 | import com.intellij.openapi.project.Project; 8 | import com.sap.cap.cds.intellij.settings.JsonSettingsManager; 9 | import com.sap.cap.cds.intellij.util.JsonUtil; 10 | import com.sap.cap.cds.intellij.util.LoggerScope; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.Map; 14 | 15 | import static com.sap.cap.cds.intellij.usersettings.CdsUserSettings.USER_SETTINGS_JSON; 16 | 17 | public class CdsUserSettingsJsonManager extends JsonSettingsManager<Map<String, Object>> { 18 | 19 | public CdsUserSettingsJsonManager(Project project) { 20 | super(project, USER_SETTINGS_JSON, LoggerScope.USER_SETTINGS); 21 | } 22 | 23 | @Override 24 | public void loadSettingsFromFile(@NotNull Map<String, Object> settings) { 25 | logger.debug("Loading user settings from file"); 26 | String json = readJson(); 27 | if (json.isEmpty()) { 28 | return; 29 | } 30 | 31 | try { 32 | JsonElement root = JsonParser.parseString(json); 33 | if (!root.isJsonObject()) { 34 | logger.warn("Invalid JSON structure for user settings: Root is not an object."); 35 | return; 36 | } 37 | JsonObject settingsRoot = root.getAsJsonObject(); 38 | if (settingsRoot.has("settings") && settingsRoot.get("settings").isJsonObject()) { 39 | settingsRoot = settingsRoot.getAsJsonObject("settings"); 40 | } 41 | 42 | Map<String, Object> fileSettings = JsonUtil.flatten(settingsRoot); 43 | settings.putAll(fileSettings); 44 | logger.debug("Loaded and merged user settings from JSON"); 45 | } catch (JsonSyntaxException e) { 46 | logger.error("Failed to parse user settings JSON '%s'".formatted(json), e); 47 | } 48 | } 49 | 50 | @Override 51 | public void saveSettingsToFile(@NotNull Map<String, Object> settings) { 52 | if (settings.isEmpty()) { 53 | writeJson("{}"); 54 | return; 55 | } 56 | writeJson(JsonUtil.nest(settings, "settings").toString()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/Logger.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.sap.cap.cds.intellij.CdsPlugin; 5 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsProvider; 6 | import com.sap.cap.cds.intellij.lsp4ij.CdsLanguageServer; 7 | import com.sap.cap.cds.intellij.textmate.CdsTextMateBundle; 8 | 9 | import java.util.Map; 10 | 11 | import static com.intellij.openapi.diagnostic.Logger.getInstance; 12 | 13 | public class Logger { 14 | 15 | public static final com.intellij.openapi.diagnostic.Logger PLUGIN = getInstance(CdsPlugin.LABEL); 16 | 17 | public static final com.intellij.openapi.diagnostic.Logger TM_BUNDLE = getInstance("%s/%s".formatted(CdsPlugin.LABEL, CdsTextMateBundle.LABEL)); 18 | 19 | public static final com.intellij.openapi.diagnostic.Logger CODE_STYLE = getInstance("%s/%s".formatted(CdsPlugin.LABEL, CdsCodeStyleSettingsProvider.LABEL)); 20 | 21 | public static final com.intellij.openapi.diagnostic.Logger SERVER = getInstance("%s/%s".formatted(CdsPlugin.LABEL, CdsLanguageServer.LABEL)); 22 | 23 | private static final Map<CacheKey, Logger> INSTANCES = new java.util.concurrent.ConcurrentHashMap<>(); 24 | 25 | private final Project project; 26 | private final com.intellij.openapi.diagnostic.Logger logger; 27 | 28 | private Logger(Project project, com.intellij.openapi.diagnostic.Logger ijLogger) { 29 | this.project = project; 30 | this.logger = ijLogger; 31 | } 32 | 33 | public static Logger logger(Project project, LoggerScope scope) { 34 | return INSTANCES.computeIfAbsent(new CacheKey(project, scope), k -> new Logger(project, getInstance(scope.label))); 35 | } 36 | 37 | public void debug(String message) { 38 | logger.debug("[%s] %s".formatted(project.getName(), message)); 39 | } 40 | 41 | public void info(String message) { 42 | logger.info("[%s] %s".formatted(project.getName(), message)); 43 | } 44 | 45 | public void warn(String message) { 46 | logger.warn("[%s] %s".formatted(project.getName(), message)); 47 | } 48 | 49 | public void error(String message) { 50 | logger.error("[%s] %s".formatted(project.getName(), message)); 51 | } 52 | 53 | public void error(String message, Throwable t) { 54 | logger.error("[%s] %s".formatted(project.getName(), message), t); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanel.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.lang.Language; 4 | import com.intellij.openapi.options.ConfigurationException; 5 | import com.intellij.openapi.util.NlsContexts.TabTitle; 6 | import com.intellij.psi.codeStyle.CodeStyleSettings; 7 | import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider; 8 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | /** 13 | * Custom code-style panel for CDS language with a tabular layout. Supports all types of options (boolean, integer, enum). 14 | */ 15 | public class CdsCodeStyleTabularPanel extends CdsCodeStyleTabularPanelBase implements CdsCodeStylePanel { 16 | 17 | private final CdsCodeStyleOption.Category category; 18 | 19 | public CdsCodeStyleTabularPanel(CodeStyleSettings settings, CdsCodeStyleOption.Category category) { 20 | super(settings); 21 | this.category = category; 22 | init(); 23 | getEditor().getSettings().setRightMarginShown(false); 24 | } 25 | 26 | @Override 27 | public CdsCodeStyleOption.Category getCategory() { 28 | return category; 29 | } 30 | 31 | @Override 32 | public LanguageCodeStyleSettingsProvider.SettingsType getSettingsType() { 33 | if (category == null) { 34 | return null; 35 | } 36 | return category.getSettingsType(); 37 | } 38 | 39 | @Override 40 | protected void initTables() { 41 | } 42 | 43 | @Override 44 | protected @TabTitle @NotNull String getTabTitle() { 45 | if (category == null) { 46 | return ""; 47 | } 48 | return category.getTitle(); 49 | } 50 | 51 | @Override 52 | public @Nullable Language getDefaultLanguage() { 53 | return CdsLanguage.INSTANCE; 54 | } 55 | 56 | @Override 57 | protected String getPreviewText() { 58 | return CdsCodeStyleSettings.SAMPLE_SRC; 59 | } 60 | 61 | @Override 62 | public void apply(@NotNull CodeStyleSettings settings) throws ConfigurationException { 63 | super.apply(settings); 64 | CdsCodeStyleSettings cdsSettings = settings.getCustomSettings(CdsCodeStyleSettings.class); 65 | setOptionsEnablement(cdsSettings.getChildOptionsEnablement(category)); 66 | CdsCodeStylePreviewFormattingService.acceptSettings(cdsSettings); 67 | } 68 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: File a bug/issue 3 | title: "[BUG] <title>" 4 | labels: ["bug", "new"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Is there an existing issue for this? 9 | description: Please search to see if an issue already exists for the bug you encountered. 10 | options: 11 | - label: I have searched the existing issues 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Current Behavior 16 | description: A concise description of what you're experiencing. 17 | validations: 18 | required: false 19 | - type: textarea 20 | attributes: 21 | label: Expected Behavior 22 | description: A concise description of what you expected to happen. 23 | validations: 24 | required: false 25 | - type: textarea 26 | attributes: 27 | label: Steps To Reproduce 28 | description: | 29 | Steps to reproduce the behavior. 30 | Tip: You can add CDS code in code fences: 31 | <pre> 32 | ```cds 33 | entity Foo { 34 | bar: String; 35 | baz: Integer; 36 | } 37 | ``` 38 | </pre> 39 | placeholder: | 40 | 1. With this sample model... 41 | 2. I get this error... 42 | validations: 43 | required: false 44 | - type: textarea 45 | attributes: 46 | label: Versions 47 | description: | 48 | | Tool | Version | 49 | | ---- | ------- | 50 | | SAP CDS Language Support for IntelliJ | | 51 | | Node.js | | 52 | validations: 53 | required: false 54 | - type: dropdown 55 | id: environment 56 | attributes: 57 | label: OS / Environment 58 | description: Where does the issue happen? 59 | options: 60 | - Linux 61 | - macOS 62 | - Windows 63 | validations: 64 | required: true 65 | - type: textarea 66 | attributes: 67 | label: Repository Containing a Minimal Reproducible Example 68 | placeholder: https://github.com/my/repository 69 | description: | 70 | Do you have a sample repository where we can observe the reported behaviour? Please include the relevant branch. 71 | validations: 72 | required: false 73 | - type: textarea 74 | attributes: 75 | label: Anything else? 76 | description: | 77 | Links? References? Anything that will give us more context about the issue you are encountering! 78 | 79 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 80 | validations: 81 | required: false -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/usersettings/CdsUserSettingsService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.usersettings; 2 | 3 | import com.intellij.openapi.components.Service; 4 | import com.intellij.openapi.project.Project; 5 | import com.redhat.devtools.lsp4ij.client.SettingsHelper; 6 | import com.sap.cap.cds.intellij.settings.JsonSettingsManager; 7 | import com.sap.cap.cds.intellij.settings.JsonSettingsService; 8 | import com.sap.cap.cds.intellij.util.JsonUtil; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | import static com.sap.cap.cds.intellij.usersettings.CdsUserSettings.USER_SETTINGS_JSON; 15 | import static com.sap.cap.cds.intellij.util.LoggerScope.USER_SETTINGS; 16 | 17 | @Service(Service.Level.PROJECT) 18 | public final class CdsUserSettingsService extends JsonSettingsService<Map<String, Object>> { 19 | 20 | public static final String LABEL = "User Settings"; 21 | public final JsonSettingsManager<Map<String, Object>> jsonManager; 22 | 23 | public CdsUserSettingsService(Project project) { 24 | super(project, USER_SETTINGS_JSON, USER_SETTINGS); 25 | this.jsonManager = createJsonManager(project); 26 | } 27 | 28 | @Override 29 | protected JsonSettingsManager<Map<String, Object>> createJsonManager(Project project) { 30 | return new CdsUserSettingsJsonManager(project); 31 | } 32 | 33 | public void updateSettings(Map<String, Object> newUiState) { 34 | Map<String, Object> currentSettings = getSettings(); 35 | currentSettings.clear(); 36 | currentSettings.putAll(CdsUserSettings.getDefaults()); 37 | currentSettings.putAll(newUiState); 38 | 39 | Map<String, Object> nonDefaultSettings = new HashMap<>(); 40 | Map<String, Object> defaults = CdsUserSettings.getDefaults(); 41 | for (Map.Entry<String, Object> entry : currentSettings.entrySet()) { 42 | if (!Objects.equals(entry.getValue(), defaults.get(entry.getKey()))) { 43 | nonDefaultSettings.put(entry.getKey(), entry.getValue()); 44 | } 45 | } 46 | jsonManager.saveSettingsToFile(nonDefaultSettings); 47 | } 48 | 49 | @Override 50 | public Map<String, Object> getSettings() { 51 | return CdsUserSettings.getInstance(project).getSettings(); 52 | } 53 | 54 | public Object getSettingsStructured() { 55 | String nestedJsonString = JsonUtil.nest(getSettings(), null).toString(); 56 | return SettingsHelper.parseJson(nestedJsonString, false); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/httpFiles/HttpFileCompatConversionIntention.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.httpFiles; 2 | 3 | import com.intellij.codeInsight.intention.impl.BaseIntentionAction; 4 | import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo; 5 | import com.intellij.openapi.command.WriteCommandAction; 6 | import com.intellij.openapi.editor.Document; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.fileTypes.PlainTextFileType; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.vfs.VirtualFile; 11 | import com.intellij.psi.PsiFile; 12 | import com.intellij.util.IncorrectOperationException; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.util.regex.Pattern; 16 | 17 | public class HttpFileCompatConversionIntention extends BaseIntentionAction { 18 | 19 | private static final Pattern PATTERN = Pattern.compile("(?<=\\{\\{username}}):(?=\\{\\{password}})"); 20 | 21 | @NotNull 22 | @Override 23 | public String getText() { 24 | return "Make compatible with Intellij HTTP client"; 25 | } 26 | 27 | @Override 28 | public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) { 29 | if (psiFile == null) { 30 | return false; 31 | } 32 | VirtualFile virtualFile = psiFile.getVirtualFile(); 33 | if (virtualFile == null || !"http".equalsIgnoreCase(virtualFile.getExtension())) { 34 | return false; 35 | } 36 | return PATTERN.matcher(editor.getDocument().getText()).find(); 37 | } 38 | 39 | @Override 40 | public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException { 41 | final Document document = editor.getDocument(); 42 | WriteCommandAction.runWriteCommandAction(project, () -> { 43 | document.setText(replaceAuthSeparator(document.getText())); 44 | }); 45 | } 46 | 47 | @NotNull 48 | @Override 49 | public String getFamilyName() { 50 | return "HTTP file actions"; 51 | } 52 | 53 | @Override 54 | public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { 55 | String text = editor.getDocument().getText(); 56 | return new IntentionPreviewInfo.CustomDiff(PlainTextFileType.INSTANCE, file.getName(), text, replaceAuthSeparator(text)); 57 | } 58 | 59 | private static String replaceAuthSeparator(String text) { 60 | return PATTERN.matcher(text).replaceAll(" "); 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/ReflectionUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import javax.swing.tree.DefaultMutableTreeNode; 6 | import javax.swing.tree.DefaultTreeModel; 7 | import java.lang.reflect.Field; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class ReflectionUtil { 13 | 14 | public static Object getFieldValue(Object obj, String fieldName, @Nullable Class<?> superclass) throws NoSuchFieldException, IllegalAccessException { 15 | Field field = superclass == null ? obj.getClass().getDeclaredField(fieldName) : superclass.getDeclaredField(fieldName); 16 | field.setAccessible(true); 17 | return field.get(obj); 18 | } 19 | 20 | public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { 21 | Field field = obj.getClass().getDeclaredField(fieldName); 22 | field.setAccessible(true); 23 | field.set(obj, value); 24 | } 25 | 26 | public static void setCustomOptionsEnablement(DefaultTreeModel model, Map<String, Boolean> enablementMap) { 27 | DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); 28 | setNodesEnablement(root, model, enablementMap); 29 | } 30 | 31 | private static void setNodesEnablement(DefaultMutableTreeNode root, DefaultTreeModel model, Map<String, Boolean> enablementMap) { 32 | List<Integer> changedChildIndexes = new ArrayList<>(); 33 | for (int i = 0; i < root.getChildCount(); i++) { 34 | DefaultMutableTreeNode child = (DefaultMutableTreeNode) root.getChildAt(i); 35 | if (child.getClass().getSimpleName().equals("MyToggleTreeNode")) { 36 | try { 37 | Object key = getFieldValue(child, "myKey", null); 38 | String optionName = (String) getFieldValue(key, "optionName", key.getClass().getSuperclass().getSuperclass()); 39 | if (enablementMap.containsKey(optionName)) { 40 | setFieldValue(child, "isEnabled", enablementMap.get(optionName)); 41 | changedChildIndexes.add(i); 42 | } 43 | } catch (NoSuchFieldException | IllegalAccessException e) { 44 | Logger.PLUGIN.error(e); 45 | } 46 | } else { 47 | setNodesEnablement(child, model, enablementMap); 48 | } 49 | } 50 | model.nodesChanged(root, changedChildIndexes.stream().mapToInt(Integer::intValue).toArray()); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/ApplicationSettingsConfigurable.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import org.jetbrains.annotations.Nls; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import javax.swing.*; 8 | import java.util.Objects; 9 | 10 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.getInterpreterFromPathOrRegistered; 11 | 12 | /** 13 | * Provides controller functionality for application settings. 14 | */ 15 | final class AppSettingsConfigurable implements Configurable { 16 | 17 | private AppSettingsComponent mySettingsComponent; 18 | 19 | // A default constructor with no arguments is required because 20 | // this implementation is registered as an applicationConfigurable 21 | 22 | @Nls(capitalization = Nls.Capitalization.Title) 23 | @Override 24 | public String getDisplayName() { 25 | return "CAP CDS"; // Q: where is this used? 26 | } 27 | 28 | @Override 29 | public JComponent getPreferredFocusedComponent() { 30 | return mySettingsComponent.getPreferredFocusedComponent(); 31 | } 32 | 33 | @Nullable 34 | @Override 35 | public JComponent createComponent() { 36 | mySettingsComponent = new AppSettingsComponent(); 37 | return mySettingsComponent.getPanel(); 38 | } 39 | 40 | @Override 41 | public boolean isModified() { 42 | AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); 43 | return !(mySettingsComponent.getNodeJsPathText().equals(state.nodeJsPath) && 44 | mySettingsComponent.getCdsLspEnvText().equals(state.cdsLspEnv)); 45 | 46 | } 47 | 48 | @Override 49 | public void apply() { 50 | AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); 51 | String text = mySettingsComponent.getNodeJsPathText(); 52 | state.nodeJsPath = text.isBlank() 53 | ? getInterpreterFromPathOrRegistered() 54 | : text; 55 | state.cdsLspEnv = mySettingsComponent.getCdsLspEnvText(); 56 | } 57 | 58 | @Override 59 | public void reset() { 60 | AppSettings.State state = Objects.requireNonNull(AppSettings.getInstance().getState()); 61 | mySettingsComponent.setNodeJsPathText(state.nodeJsPath); 62 | mySettingsComponent.setCdsLspEnvText(state.cdsLspEnv); 63 | mySettingsComponent.validateAndUpdateNodeJsPath(); 64 | mySettingsComponent.validateAndUpdateEnvMap(); 65 | } 66 | 67 | @Override 68 | public void disposeUIResources() { 69 | mySettingsComponent = null; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsLanguageClient.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.redhat.devtools.lsp4ij.client.LanguageClientImpl; 5 | import com.sap.cap.cds.intellij.usersettings.CdsUserSettingsService; 6 | import org.eclipse.lsp4j.ApplyWorkspaceEditParams; 7 | import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; 8 | 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public class CdsLanguageClient extends LanguageClientImpl /*TODO: try UserDefinedLanguageClient or IndexAwareLanguageClient*/ { 12 | private Project myProject; 13 | 14 | public CdsLanguageClient(Project project) { 15 | super(project); 16 | this.myProject = project; 17 | } 18 | 19 | @Override 20 | public void telemetryEvent(Object object) { 21 | // TODO: can we use this to send telemetry data to our open telemetry cloud? 22 | super.telemetryEvent(object); 23 | } 24 | 25 | @Override 26 | public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) { 27 | // TODO: maintain translation should save the properties file Q: do we need to register for .properties files, too? We want a didChangeFile event to be send to the server 28 | return super.applyEdit(params); 29 | } 30 | 31 | // TODO: derive from UserDefinedLanguageClient and get this for free: sends workspace/didChangeConfiguration after server init 32 | // @Override 33 | // public void handleServerStatusChanged(ServerStatus serverStatus) { 34 | // if (serverStatus == ServerStatus.started) { 35 | // triggerChangeConfiguration(); 36 | // } 37 | // } 38 | 39 | 40 | 41 | // TODO: register command/url handler for analyze dependency's "command:cds.analyzeDependencies", then... 42 | /* 43 | CompletableFuture<List<Application>> applications = 44 | LanguageServerManager.getInstance(project) 45 | .getLanguageServer("myLanguageServerId") 46 | .thenApply(languageServerItem -> 47 | languageServerItem != null ? languageServerItem.getServer() // here getServer is used because we are sure that server is initialized 48 | : null) 49 | .thenCompose(ls -> { 50 | if (ls == null) { 51 | return CompletableFuture.completedFuture(Collections.emptyList()); 52 | } 53 | MyCustomServerAPI myServer = (MyCustomServerAPI) ls; 54 | return myServer.getApplications();} // custom request or other stuff... 55 | ); 56 | */ 57 | 58 | @Override 59 | protected Object createSettings() { 60 | return myProject.getService(CdsUserSettingsService.class) 61 | .getSettingsStructured(); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /lsp/mitm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Man-in-the-middle script that forwards stdio to/from a child command and logs the data. 3 | * Can be used to wrap cds-lsp (CDS LSP server). 4 | */ 5 | 6 | const { spawn } = require('child_process'); 7 | const fs = require('fs'); 8 | const { Transform, Writable } = require('stream'); 9 | 10 | const logged = {}; 11 | 12 | function handle(chunk, prefix) { 13 | const now = new Date().toISOString(); 14 | let key = `${prefix} ${now}`; 15 | if (key in logged) { 16 | key = key.replace(/(?=Z$)/, process.hrtime.bigint().toString().slice(-6)); 17 | } 18 | let content = chunk.toString().trim(); 19 | if (content.startsWith('{') && content.endsWith('}')) { 20 | try { 21 | content = JSON.parse(chunk.toString()); 22 | } catch (error) { 23 | // maybe incomplete JSON 24 | } 25 | } 26 | logged[key] = content; 27 | } 28 | 29 | function writeFile() { 30 | fs.writeFileSync(logFile, JSON.stringify(logged, null, 2)); 31 | } 32 | 33 | function createLogPrefixStream(prefix){ 34 | return new Writable({ 35 | write(chunk, encoding, callback) { 36 | const lines = chunk.toString().trim() 37 | .split('\n') 38 | .map(line => line.trim()) 39 | .filter(line => line.length > 0); 40 | for (const line of lines) { 41 | handle(line, prefix); 42 | } 43 | writeFile(); 44 | callback(); 45 | } 46 | }); 47 | } 48 | 49 | 50 | if (process.argv.length < 4) { 51 | console.error('Usage: node mitm.js LOGFILE COMMAND…'); 52 | process.exit(1); 53 | } 54 | 55 | const logFile = process.argv[2]; 56 | const command = process.argv[3]; 57 | const args = process.argv.slice(4); 58 | 59 | const child = spawn(command, args); 60 | 61 | writeFile(); 62 | 63 | // Redirect stdin 64 | 65 | // client → MITM → child 66 | process.stdin.pipe(child.stdin); 67 | // client → MITM → log 68 | process.stdin.pipe(createLogPrefixStream('>')); 69 | 70 | // Redirect stdout 71 | 72 | // child → MITM → client 73 | child.stdout.pipe(process.stdout); 74 | // child → MITM → log 75 | child.stdout.pipe(createLogPrefixStream('<')); 76 | 77 | // Redirect stderr 78 | 79 | // child → MITM → client 80 | child.stderr.pipe(process.stderr); 81 | // child → MITM → log 82 | child.stderr.pipe(createLogPrefixStream('<[E]')); 83 | 84 | // Log process events 85 | 86 | [process, child].forEach(proc => 87 | ['stdin', 'stdout', 'stderr'].forEach(stream => proc[stream].on('close', () => { 88 | handle(`${proc === child ? 'child' : 'mitm'} ${stream} closed`, '[I]'); 89 | })) 90 | ); 91 | child.on('close', (code) => { 92 | handle(`child process exited with code ${code}`, '[I]'); 93 | process.exit(code); 94 | }); 95 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/JsonSettingsFileListener.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.project.ProjectLocator; 5 | import com.intellij.openapi.vfs.AsyncFileListener; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent; 8 | import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent; 9 | import com.intellij.openapi.vfs.newvfs.events.VFileEvent; 10 | import com.sap.cap.cds.intellij.util.LoggerScope; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.annotations.Nullable; 13 | 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.function.Predicate; 17 | import java.util.stream.Stream; 18 | 19 | import static com.intellij.openapi.application.ApplicationManager.getApplication; 20 | import static com.intellij.openapi.vfs.VfsUtil.collectChildrenRecursively; 21 | import static com.sap.cap.cds.intellij.util.Logger.logger; 22 | 23 | public abstract class JsonSettingsFileListener implements AsyncFileListener { 24 | 25 | protected abstract Predicate<VirtualFile> fileFilter(); 26 | protected abstract void handleFileChange(Project project); 27 | protected abstract LoggerScope getLoggerScope(); 28 | protected abstract String getDebugMessage(); 29 | 30 | private void handle(Stream<? extends @NotNull VFileEvent> stream) { 31 | ProjectLocator projectLocator = getApplication().getService(ProjectLocator.class); 32 | stream 33 | .flatMap(event -> event instanceof VFileCreateEvent e && e.getFile() != null && e.getFile().isDirectory() 34 | ? collectChildrenRecursively(e.getFile()).stream() 35 | : Stream.of(event.getFile())) 36 | .filter(Objects::nonNull) 37 | .filter(fileFilter()) 38 | .map(projectLocator::guessProjectForFile) 39 | .filter(Objects::nonNull) 40 | .distinct() 41 | .forEach(project -> { 42 | logger(project, getLoggerScope()).debug(getDebugMessage()); 43 | handleFileChange(project); 44 | }); 45 | } 46 | 47 | @Override 48 | public @Nullable ChangeApplier prepareChange(@NotNull List<? extends @NotNull VFileEvent> list) { 49 | return new ChangeApplier() { 50 | @Override 51 | public void beforeVfsChange() { 52 | handle(list.stream().filter(event -> event instanceof VFileDeleteEvent)); 53 | } 54 | 55 | @Override 56 | public void afterVfsChange() { 57 | handle(list.stream().filter(event -> !(event instanceof VFileDeleteEvent))); 58 | } 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.textmate; 2 | 3 | 4 | import com.intellij.openapi.components.Service; 5 | import com.intellij.openapi.components.Service.Level; 6 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 7 | import com.sap.cap.cds.intellij.util.Logger; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.plugins.textmate.TextMateServiceImpl; 10 | import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings; 11 | 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | 15 | import static com.intellij.util.PathUtil.getJarPathForClass; 16 | import static com.sap.cap.cds.intellij.util.StackUtil.getCaller; 17 | import static com.sap.cap.cds.intellij.util.StackUtil.getMethod; 18 | 19 | @Service(Level.APP) 20 | public final class CdsTextMateBundleService { 21 | 22 | private volatile Boolean isBundleRegistered = false; 23 | 24 | @NotNull 25 | private static TextMateUserBundlesSettings getBundlesSettings() { 26 | TextMateUserBundlesSettings settings = TextMateUserBundlesSettings.getInstance(); 27 | assert settings != null; 28 | return settings; 29 | } 30 | 31 | private static void logBundlePresent(TextMateUserBundlesSettings settings) { 32 | boolean isBundlePresent = settings.getBundles().values().stream().anyMatch(value -> CdsLanguage.LABEL.equals(value.getName())); 33 | Logger.TM_BUNDLE.info("TextMate bundle present before action: " + isBundlePresent); 34 | } 35 | 36 | @NotNull 37 | private static String getBundlePath() { 38 | Path thisPath = Paths.get(getJarPathForClass(CdsTextMateBundleService.class)); 39 | return thisPath 40 | .getParent() 41 | .resolve(CdsTextMateBundle.RELATIVE_PATH) 42 | .toString(); 43 | } 44 | 45 | public void registerBundle() { 46 | Logger.TM_BUNDLE.info("TextMate bundle registration initiated by " + getMethod(getCaller(3))); 47 | 48 | if (isBundleRegistered) { 49 | Logger.TM_BUNDLE.info("TextMate bundle: registerBundle has been called before"); 50 | return; 51 | } 52 | isBundleRegistered = true; 53 | 54 | TextMateUserBundlesSettings settings = getBundlesSettings(); 55 | logBundlePresent(settings); 56 | settings.addBundle(getBundlePath(), CdsLanguage.LABEL); // Will also update existing bundle. 57 | TextMateServiceImpl.getInstance().reloadEnabledBundles(); 58 | } 59 | 60 | public void unregisterBundle() { 61 | TextMateUserBundlesSettings settings = getBundlesSettings(); 62 | logBundlePresent(settings); 63 | settings.disableBundle(getBundlePath()); 64 | TextMateServiceImpl.getInstance().reloadEnabledBundles(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceIdeSettingsTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | 5 | import java.io.IOException; 6 | 7 | public class CdsCodeStyleSettingsServiceIdeSettingsTest extends CdsCodeStyleSettingsServiceTestBase { 8 | 9 | @Override 10 | protected void setUp() throws Exception { 11 | super.setUp(); 12 | setPerProjectSettings(false); // → CodeStyle.getSettings(project) will return "Default" (IDE) code-style settings via CodeStyleSchemes.getInstance().findPreferredScheme() 13 | } 14 | 15 | public void testDefaultSettings() { 16 | openProject(); 17 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 18 | } 19 | 20 | public void testUsesIdeSettings() { 21 | openProject(); 22 | assertFalse(CodeStyle.usesOwnSettings(project)); 23 | } 24 | 25 | // Direction .cdsprettier.json → settings 26 | 27 | public void testPrettierJsonLifecycle() throws IOException, InterruptedException { 28 | openProject(); 29 | createPrettierJson(); 30 | writePrettierJson("{}"); 31 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 32 | assertFalse(CodeStyle.usesOwnSettings(project)); 33 | 34 | writePrettierJson("{ tabSize: 42 }"); 35 | assertEquals(42, getCdsCodeStyleSettings().tabSize); 36 | assertTrue(CodeStyle.usesOwnSettings(project)); 37 | 38 | deletePrettierJson(); 39 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 40 | assertTrue(CodeStyle.usesOwnSettings(project)); 41 | } 42 | 43 | public void testPrettierJsonDeletedFirst() throws IOException, InterruptedException { 44 | createPrettierJson(); 45 | writePrettierJson("{}"); 46 | openProject(); 47 | assertFalse(CodeStyle.usesOwnSettings(project)); 48 | 49 | deletePrettierJson(); 50 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 51 | assertFalse(CodeStyle.usesOwnSettings(project)); 52 | } 53 | 54 | public void testExistentPrettierJsonWithSameSettings() throws IOException, InterruptedException { 55 | createPrettierJson(); 56 | writePrettierJson("{}"); 57 | openProject(); 58 | assertFalse(CodeStyle.usesOwnSettings(project)); 59 | } 60 | 61 | public void testExistentPrettierJsonWithDifferingSettings() throws IOException, InterruptedException { 62 | createPrettierJson(); 63 | writePrettierJson("{ tabSize: 42 }"); 64 | openProject(); 65 | assertEquals(42, getCdsCodeStyleSettings().tabSize); 66 | assertTrue(CodeStyle.usesOwnSettings(project)); 67 | } 68 | 69 | // TODO Direction settings → .cdsprettier.json 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lsp4ij/CdsLspClientFeatures.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lsp4ij; 2 | 3 | import com.intellij.openapi.vfs.VirtualFile; 4 | import com.intellij.psi.PsiFile; 5 | import com.redhat.devtools.lsp4ij.ServerStatus; 6 | import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; 7 | import org.eclipse.lsp4j.InitializeParams; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.net.URI; 12 | 13 | public class CdsLspClientFeatures extends LSPClientFeatures { 14 | 15 | @Override 16 | public void initializeParams(@NotNull InitializeParams initializeParams) { 17 | // Idea: modify them before `onInitialize` is called 18 | super.initializeParams(initializeParams); 19 | 20 | 21 | // Idea: 22 | // this.getLanguageServer().getTextDocumentService().hover().handleAsync() Q: can we hook this to somehow handle the command:cds.analyzeDependencies URI? 23 | 24 | 25 | } 26 | 27 | @Override 28 | public void handleServerStatusChanged(@NotNull ServerStatus serverStatus) { 29 | // Idea: log 30 | super.handleServerStatusChanged(serverStatus); 31 | } 32 | 33 | @Override 34 | public boolean keepServerAlive() { 35 | // Keep server alive even if all files associated with the language server are closed 36 | return true; 37 | } 38 | 39 | @Override 40 | public boolean isEnabled(@NotNull VirtualFile file) { 41 | // Idea: enable/disable CDS LSP for the file e.g. CSN JSON files vs. other JSON files - But actually the server does this already 42 | return super.isEnabled(file); 43 | } 44 | 45 | @Override 46 | public @Nullable URI getFileUri(@NotNull VirtualFile file) { 47 | // Idea: canonicalize URI like VSCode's uriConverters 48 | URI fileUri = super.getFileUri(file); 49 | return fileUri; 50 | } 51 | 52 | @Override 53 | public @Nullable VirtualFile findFileByUri(@NotNull String fileUri) { 54 | // Idea: canonicalize URI like VSCode's uriConverters 55 | return super.findFileByUri(fileUri); 56 | } 57 | 58 | @Override 59 | public boolean isCaseSensitive(@NotNull PsiFile file) { 60 | // default: false 61 | // Q: return true for CDS files? 62 | return super.isCaseSensitive(file); 63 | } 64 | 65 | // Idea: 66 | /* 67 | String getLineCommentPrefix(PsiFile file) Returns the language grammar line comment prefix for the file. 68 | String getBlockCommentPrefix(PsiFile file) Returns the language grammar block comment prefix for the file. 69 | String getBlockCommentSuffix(PsiFile file) Returns the language grammar block comment suffix for the file. 70 | String getStatementTerminatorCharacters(PsiFile file) Returns the language grammar statement terminator characters for the file. 71 | */ 72 | 73 | } 74 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ### Sandbox IDE 4 | 5 | #### Configuration 6 | 7 | To configure the location of the sandbox IDE, create a file *local.properties* next to *gradle.properties*. 8 | 9 | In this file, you can set a number of optional properties to be used by Gradle. 10 | Note that keys should always start with `local.`. Search *build.gradle* for this prefix to find all known local settings. 11 | 12 | Sample values for Linux: 13 | 14 | ```properties 15 | # All properties should start with 'local.' 16 | local.ideDir=/opt/webstorm 17 | local.ideDirIC=/opt/intellij-community 18 | # to copy your keymaps and various settings to the sandbox IDE on each run: 19 | local.ideConfDir=/home/your_user/.config/JetBrains/WebStorm2025.1 20 | local.ideConfDirIC=/home/your_user/.config/JetBrains/IdeaIC2025.1 21 | ``` 22 | 23 | #### Running the Sandbox IDE 24 | 25 | Depending on the type of IntelliJ-based IDE you have installed locally, run one of the following commands: 26 | 27 | - `./gradlew runWebStorm` for WebStorm 28 | - `./gradlew runIC` for IntelliJ IDEA Community Edition 29 | 30 | This will start the 31 | corresponding [sandbox IDEA instance](https://plugins.jetbrains.com/docs/intellij/ide-development-instance.html) with 32 | the plugin installed. 33 | 34 | ### Debugging the LSP Server 35 | 36 | #### Using Locally-Modified cds-lsp 37 | 38 | To test and debug a local version of `@sap/cds-lsp` in the plugin: 39 | 40 | 1. In the local `cds-lsp` repo: 41 | 1. Make your code modifications. 42 | 2. Run `npm run compile && npm pack` in the same directory. 43 | 2. In this repo: 44 | 1. To have the `lsp/node_modules/@sap/cds-lsp` folder cleaned on each build, thus enabling local development, set 45 | `local.rmCdsLspNodeModules = true` 46 | 2. Reference the path of the output `.tgz` file in the `@sap/cds-lsp` dependency in `lsp/package.json`, with a 47 | `file:` prefix. 48 | 3. Run `./gradlew runIde` (or `./gradlew runWebStorm`) to start the sandbox IDE with the modified LSP server. Each 49 | run will use the newest version of the `.tgz` file. 50 | 51 | #### Debugging 52 | 53 | When LSP debugging is [enabled](./README.md#plugin-debug-mode), the LSP server will start in debug mode, 54 | ready for a debugger to attach. 55 | The server is bundled but features a source map enabling you to set breakpoints in the TypeScript code. 56 | 57 | ### UI Development Tools 58 | 59 | The [Internal Actions UI Submenu](https://plugins.jetbrains.com/docs/intellij/internal-ui-sub.html) provides a set of 60 | tools to develop, debug, and test the plugin UI components. 61 | You may have to enable them in the installed IDE and/or the sandbox IDE. 62 | 63 | ### Building and Testing with cds-lsp form git Branch 64 | 65 | To build and test the plugin with a version of `@sap/cds-lsp` from a git branch, create and push a branch of this repo, changing `lsp/package.json` to read: 66 | 67 | ```json 68 | { 69 | "dependencies": { 70 | "@sap/cds-lsp": "file:../../cds-lsp/cds-lsp-[CDS_LSP_VERSION].tgz" 71 | }, 72 | "cds-lsp-branch": "[CDS_LSP_BRANCH]" 73 | } 74 | ``` 75 | (insert the appropriate version and branch names). 76 | 77 | You can then use the Build and Test action in the plugin repository to build and test the plugin with this version of `@sap/cds-lsp`. -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/lspServer/CdsLspServerDescriptorTest.java: -------------------------------------------------------------------------------- 1 | //package com.sap.cap.cds.intellij.lsp; 2 | // 3 | //import com.intellij.execution.ExecutionException; 4 | //import com.intellij.execution.configurations.GeneralCommandLine; 5 | //import com.intellij.testFramework.fixtures.BasePlatformTestCase; 6 | //import com.intellij.testFramework.fixtures.TempDirTestFixture; 7 | // 8 | //import java.io.BufferedWriter; 9 | //import java.io.File; 10 | //import java.io.FileInputStream; 11 | //import java.io.IOException; 12 | //import java.util.regex.Pattern; 13 | // 14 | //public class CdsLspServerDescriptorTest extends BasePlatformTestCase { 15 | // public void testCommandLine() { 16 | // Process process = null; 17 | // try { 18 | // process = new CdsLspServerDescriptor(getProject(), "name") 19 | // .getServerCommandLine() 20 | // .createProcess(); 21 | // } catch (ExecutionException e) { 22 | // assertNull("unexpected exception", e.getMessage()); 23 | // } 24 | // assertNotNull(process); 25 | // 26 | // String[] args = process.info().arguments().get(); 27 | // String cdsLspPath = args[1]; 28 | // assertTrue(cdsLspPath.contains("/dist/")); 29 | // } 30 | // 31 | // public void testDebugCommandLine() throws IOException, InterruptedException { 32 | // // Enable debug logging 33 | // System.setProperty("DEBUG", "cds-lsp"); 34 | // 35 | // Process process = null; 36 | // GeneralCommandLine commandLine = null; 37 | // try { 38 | // commandLine = new CdsLspServerDescriptor(getProject(), "name").getServerCommandLine(); 39 | // } catch (ExecutionException e) { 40 | // assertNull("unexpected exception", e.getMessage()); 41 | // } 42 | // assertNotNull(commandLine); 43 | // 44 | // // It should be the MITM script 45 | // String[] args = commandLine.getParametersList().getArray(); 46 | // String mitmPath = args[1]; 47 | // assertTrue(mitmPath.contains("mitm")); 48 | // String logPath = args[2]; 49 | // new File(logPath).delete(); 50 | // 51 | // try { 52 | // process = commandLine.createProcess(); 53 | // } catch (ExecutionException e) { 54 | // assertNull("unexpected exception", e.getMessage()); 55 | // } 56 | // assertNotNull(process); 57 | // assertEquals(0, process.getErrorStream().available()); 58 | // 59 | // // Man-in-the-middle should log stdin data 60 | // String data = "FOO INPUT 01234567890123456789"; 61 | // try (BufferedWriter mitmStdin = process.outputWriter()) { 62 | // mitmStdin.write(data); 63 | // mitmStdin.flush(); 64 | // } 65 | // Thread.sleep(500); 66 | // try (FileInputStream stream = new FileInputStream(logPath)) { 67 | // String logged = new String(stream.readAllBytes()).replaceAll("\\d{4}-[\\dTZ:.-]+", "NOW"); 68 | // assertTrue(Pattern.compile("> NOW.*" + data).matcher(logged).find()); 69 | // } 70 | // 71 | // System.clearProperty("DEBUG"); 72 | // } 73 | // 74 | // public void testIsSupportedFile() { 75 | // TempDirTestFixture fixture = this.createTempDirTestFixture(); 76 | // CdsLspServerDescriptor serverDescriptor = new CdsLspServerDescriptor(getProject(), "name"); 77 | // assertTrue(serverDescriptor.isSupportedFile(fixture.createFile("a.cds"))); 78 | // assertFalse(serverDescriptor.isSupportedFile(fixture.createFile("a.txt"))); 79 | // } 80 | //} 81 | -------------------------------------------------------------------------------- /src/js/usersettings/patchUserSettingsJavaSrc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const { readFileSync, writeFileSync } = require('node:fs'); 5 | 6 | const schemaPath = path.resolve(__dirname, '../../../lsp/schemas/user-settings.json'); 7 | const schema = require(schemaPath); 8 | const settingsFromSchema = schema.properties; 9 | 10 | const tgtPath = path.resolve(__dirname, '../../../src/main/java/com/sap/cap/cds/intellij/usersettings/CdsUserSettings.java'); 11 | const tgt = readFileSync(tgtPath, 'utf8'); 12 | 13 | function getDefaultValue(config) { 14 | const type = config.type; 15 | if (type === 'boolean') return config.default; 16 | if (type === 'integer' || type === 'number') return config.default; 17 | if (type === 'array') return `"${Array.isArray(config.default) ? config.default.join(',') : ''}"`; 18 | return `"${(config.default || '').replace(/"/g, '\\"')}"`; 19 | } 20 | 21 | const settings = Object.entries(settingsFromSchema) 22 | .sort(([key1], [key2]) => key1.localeCompare(key2)) 23 | .map(([key, config]) => ({ 24 | key, 25 | defaultValue: getDefaultValue(config), 26 | enumValues: config.enum || null, 27 | label: config.label || null, 28 | description: config.description || null 29 | })); 30 | 31 | const t = ' '; 32 | 33 | // Generate method bodies 34 | const staticInitializerBody = ` 35 | ${t}${t}Map<String, Object> defaults = new HashMap<>(); 36 | ${settings.map(s => `${t}${t}defaults.put("${s.key}", ${s.defaultValue});`).join('\n')} 37 | ${t}${t}DEFAULTS = Collections.unmodifiableMap(defaults);`; 38 | 39 | const getLabelBody = ` 40 | ${t}${t}return switch (settingKey) { 41 | ${settings.filter(s => s.label).map(s => 42 | `${t}${t}${t}case "${s.key}" -> "${s.label.replace(/"/g, '\\"')}";` 43 | ).join('\n')} 44 | ${t}${t}${t}default -> null; 45 | ${t}${t}};`; 46 | 47 | function formatJavaDescription(description) { 48 | if (!description) return 'null'; 49 | const escaped = description.replace(/"/g, '\\"').replace(/\n/g, '\\n'); 50 | return `"${escaped}"`; 51 | } 52 | 53 | const getDescriptionBody = ` 54 | ${t}${t}return switch (settingKey) { 55 | ${settings.filter(s => s.description).map(s => 56 | `${t}${t}${t}case "${s.key}" -> ${formatJavaDescription(s.description)};` 57 | ).join('\n')} 58 | ${t}${t}${t}default -> null; 59 | ${t}${t}};`; 60 | 61 | const getEnumValuesBody = ` 62 | ${t}${t}return switch (settingKey) { 63 | ${settings.filter(s => s.enumValues).map(s => 64 | `${t}${t}${t}case "${s.key}" -> new String[]{${s.enumValues.map(v => `"${v}"`).join(', ')}};` 65 | ).join('\n')} 66 | ${t}${t}${t}default -> null; 67 | ${t}${t}};`; 68 | 69 | const hasEnumValuesBody = ` 70 | ${t}${t}return getEnumValues(settingKey) != null;`; 71 | 72 | let patchedTgt = tgt; 73 | 74 | // Replace method bodies using simplified lookbehind patterns 75 | patchedTgt = patchedTgt.replace( 76 | /(?<=static\s*\{).*?(?=\n })/sm, 77 | staticInitializerBody 78 | ); 79 | 80 | patchedTgt = patchedTgt.replace( 81 | /(?<=\bgetLabel\s*\([^)]*\)\s*\{).*?(?=\n })/sm, 82 | getLabelBody 83 | ); 84 | 85 | patchedTgt = patchedTgt.replace( 86 | /(?<=\bgetDescription\s*\([^)]*\)\s*\{).*?(?=\n })/sm, 87 | getDescriptionBody 88 | ); 89 | 90 | patchedTgt = patchedTgt.replace( 91 | /(?<=\bgetEnumValues\s*\([^)]*\)\s*\{).*?(?=\n })/sm, 92 | getEnumValuesBody 93 | ); 94 | 95 | patchedTgt = patchedTgt.replace( 96 | /(?<=\bhasEnumValues\s*\([^)]*\)\s*\{).*?(?=\n })/sm, 97 | hasEnumValuesBody 98 | ); 99 | 100 | writeFileSync(tgtPath, patchedTgt, 'utf8'); 101 | 102 | console.log('Generated CDS user settings successfully!'); 103 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsProvider.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyleAbstractConfigurable; 4 | import com.intellij.application.options.CodeStyleAbstractPanel; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.psi.PsiFile; 7 | import com.intellij.psi.PsiFileFactory; 8 | import com.intellij.psi.codeStyle.*; 9 | import com.intellij.util.LocalTimeCounter; 10 | import com.sap.cap.cds.intellij.CdsFileType; 11 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | public class CdsCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider { 16 | 17 | public static final String SAMPLE_FILE_NAME = ".cds-codestyle.sample.cds"; 18 | public static final String LABEL = "Code Style"; 19 | 20 | @Override 21 | public CustomCodeStyleSettings createCustomSettings(@NotNull CodeStyleSettings settings) { 22 | return new CdsCodeStyleSettings(settings); 23 | } 24 | 25 | @Override 26 | public String getConfigurableDisplayName() { 27 | return "CDS"; 28 | } 29 | 30 | @Override 31 | public @Nullable String getCodeSample(@NotNull SettingsType settingsType) { 32 | return CdsCodeStyleSettings.SAMPLE_SRC; 33 | } 34 | 35 | @Override 36 | public @NotNull CdsLanguage getLanguage() { 37 | return CdsLanguage.INSTANCE; 38 | } 39 | 40 | /** 41 | * @param initialSettings The base (initial) settings before changes. 42 | * @param modifiedSettings The settings to which UI changes are applied (a.k.a. model/clone settings). 43 | * @see com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider#createConfigurable(com.intellij.psi.codeStyle.CodeStyleSettings, com.intellij.psi.codeStyle.CodeStyleSettings) 44 | */ 45 | @Override 46 | public @NotNull CodeStyleConfigurable createConfigurable(@NotNull CodeStyleSettings initialSettings, @NotNull CodeStyleSettings modifiedSettings) { 47 | return new CdsCodeStyleConfigurable(initialSettings, modifiedSettings); 48 | } 49 | 50 | @Override 51 | public @Nullable PsiFile createFileFromText(@NotNull Project project, @NotNull String text) { 52 | return PsiFileFactory.getInstance(project).createFileFromText(SAMPLE_FILE_NAME, CdsFileType.INSTANCE, text, LocalTimeCounter.currentTime(), false, false); 53 | } 54 | 55 | @Override 56 | public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, @NotNull SettingsType settingsType) { 57 | if (!(consumer instanceof CdsCodeStylePanel panel)) { 58 | return; 59 | } 60 | CdsCodeStyleSettings.OPTIONS.values().stream() 61 | .filter(option -> option.category == panel.getCategory()) 62 | .forEach(panel::addOption); 63 | } 64 | 65 | private static class CdsCodeStyleConfigurable extends CodeStyleAbstractConfigurable { 66 | 67 | /** 68 | * @param initialSettings The base (initial) settings before changes. 69 | * @param modifiedSettings The settings to which UI changes are applied. Initially a clone of the initial settings. 70 | */ 71 | public CdsCodeStyleConfigurable(@NotNull CodeStyleSettings initialSettings, @NotNull CodeStyleSettings modifiedSettings) { 72 | super(initialSettings, modifiedSettings, CdsLanguage.INSTANCE.getDisplayName()); 73 | } 74 | 75 | @Override 76 | protected @NotNull CodeStyleAbstractPanel createPanel(final @NotNull CodeStyleSettings modifiedSettings) { 77 | return new CdsCodeStyleMainPanel(getCurrentSettings(), modifiedSettings); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.openapi.components.Service; 5 | import com.intellij.openapi.fileEditor.FileEditorManager; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.psi.codeStyle.CodeStyleSettings; 8 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager; 9 | import com.sap.cap.cds.intellij.CdsFileType; 10 | import com.sap.cap.cds.intellij.settings.JsonSettingsManager; 11 | import com.sap.cap.cds.intellij.settings.JsonSettingsService; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import static com.intellij.application.options.CodeStyle.getDefaultSettings; 15 | import static com.sap.cap.cds.intellij.util.LoggerScope.CODE_STYLE; 16 | import static java.util.Arrays.stream; 17 | 18 | @Service(Service.Level.PROJECT) 19 | public final class CdsCodeStyleSettingsService extends JsonSettingsService<CdsCodeStyleSettings> { 20 | 21 | public static final String PRETTIER_JSON = ".cdsprettier.json"; 22 | 23 | public CdsCodeStyleSettingsService(Project project) { 24 | super(project, PRETTIER_JSON, CODE_STYLE); 25 | CodeStyleSettingsManager.getInstance(project).subscribe(event -> { 26 | logger.debug("Code-style settings changed"); 27 | if (shouldBeSavedImmediately()) { 28 | updateSettingsFile(); 29 | } 30 | }); 31 | } 32 | 33 | private boolean shouldBeSavedImmediately() { 34 | if (isSettingsFilePresent()) { 35 | return true; 36 | } 37 | 38 | FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); 39 | return stream(fileEditorManager.getOpenFiles()) 40 | .anyMatch(file -> CdsFileType.EXTENSION.equalsIgnoreCase(file.getExtension())); 41 | } 42 | 43 | @Override 44 | protected JsonSettingsManager<CdsCodeStyleSettings> createJsonManager(Project project) { 45 | return new CdsPrettierJsonManager(project); 46 | } 47 | 48 | @Override 49 | public CdsCodeStyleSettings getSettings() { 50 | return CodeStyle.getSettings(project).getCustomSettings(CdsCodeStyleSettings.class); 51 | } 52 | 53 | @Override 54 | public void updateProjectSettingsFromFile() { 55 | if (CodeStyle.usesOwnSettings(project)) { 56 | updateProjectSpecificSettings(); 57 | } else if (isSettingsFilePresent()) { 58 | updateIdeWideSettings(); 59 | } else { 60 | logger.debug("Keeping IDE-wide settings because file is absent"); 61 | } 62 | } 63 | 64 | private void updateProjectSpecificSettings() { 65 | if (isSettingsFilePresent()) { 66 | logger.debug("Loading project-specific settings from file"); 67 | jsonManager.loadSettingsFromFile(getSettings()); 68 | } else { 69 | logger.debug("Resetting project-specific settings because file is absent"); 70 | jsonManager.reset(); 71 | CodeStyle.setMainProjectSettings(project, CodeStyleSettingsManager.getInstance(project).createSettings()); 72 | } 73 | } 74 | 75 | private void updateIdeWideSettings() { 76 | logger.debug("Loading IDE-wide settings from file"); 77 | String prettierJson = jsonManager.readJson(); 78 | if (prettierJson.isEmpty() || getIdeSettings().equals(prettierJson)) { 79 | return; 80 | } 81 | logger.debug("Updating settings"); 82 | CodeStyleSettings projectSettings = CodeStyleSettingsManager.getInstance().createSettings(); 83 | projectSettings.getCustomSettings(CdsCodeStyleSettings.class).loadFrom(prettierJson); 84 | CodeStyle.setMainProjectSettings(project, projectSettings); 85 | } 86 | 87 | private static @NotNull CdsCodeStyleSettings getIdeSettings() { 88 | return getDefaultSettings().getCustomSettings(CdsCodeStyleSettings.class); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/AppSettingsComponent.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.ui.JBColor; 4 | import com.intellij.ui.components.JBLabel; 5 | import com.intellij.ui.components.JBTextField; 6 | import com.intellij.util.ui.FormBuilder; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import javax.swing.*; 10 | import java.awt.event.FocusAdapter; 11 | import java.awt.event.FocusEvent; 12 | 13 | import static com.intellij.ui.JBColor.RED; 14 | import static com.sap.cap.cds.intellij.lspServer.CdsLspServerDescriptor.REQUIRED_NODEJS_VERSION; 15 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.checkInterpreter; 16 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.getCdsLspEnvMap; 17 | 18 | /** 19 | * Supports creating and managing a {@link JPanel} for the Settings Dialog. 20 | */ 21 | public class AppSettingsComponent { 22 | 23 | private final JPanel myMainPanel; 24 | private final JBTextField nodeJsPathText = new JBTextField(); 25 | private final JBTextField cdsLspEnvText = new JBTextField(); 26 | 27 | public AppSettingsComponent() { 28 | myMainPanel = FormBuilder.createFormBuilder() 29 | .addLabeledComponent(new JBLabel("Path to Node.js executable"), nodeJsPathText, 1, false) 30 | .addLabeledComponent(new JBLabel("Additional env for LSP server"), cdsLspEnvText, 10, false) 31 | .addComponentFillVertically(new JPanel(), 0) 32 | .getPanel(); 33 | 34 | nodeJsPathText.addFocusListener(new FocusAdapter() { 35 | @Override 36 | public void focusLost(FocusEvent e) { 37 | validateAndUpdateNodeJsPath(); 38 | } 39 | }); 40 | cdsLspEnvText.addFocusListener(new FocusAdapter() { 41 | @Override 42 | public void focusLost(FocusEvent e) { 43 | validateAndUpdateEnvMap(); 44 | } 45 | }); 46 | } 47 | 48 | public void validateAndUpdateNodeJsPath() { 49 | switch (checkInterpreter(getNodeJsPathText())) { 50 | case OK -> { 51 | nodeJsPathStateHint("found and sufficient", null); 52 | } 53 | case OUTDATED -> 54 | nodeJsPathStateHint("found but outdated (required version: %s)".formatted(REQUIRED_NODEJS_VERSION), RED); 55 | case NOT_FOUND -> nodeJsPathStateHint("not found. Please enter valid path to Node.js executable", RED); 56 | } 57 | } 58 | 59 | public void validateAndUpdateEnvMap() { 60 | try { 61 | getCdsLspEnvMap(getCdsLspEnvText()); 62 | cdsLspEnvStateHint("valid", null); 63 | } catch (Throwable e) { 64 | cdsLspEnvStateHint("invalid: %s".formatted(e.getMessage()), RED); 65 | } 66 | } 67 | 68 | public JPanel getPanel() { 69 | return myMainPanel; 70 | } 71 | 72 | public JComponent getPreferredFocusedComponent() { 73 | return nodeJsPathText; 74 | } 75 | 76 | @NotNull 77 | public String getNodeJsPathText() { 78 | return nodeJsPathText.getText().trim(); 79 | } 80 | 81 | public void setNodeJsPathText(@NotNull String newText) { 82 | nodeJsPathText.setText(newText); 83 | } 84 | 85 | public String getCdsLspEnvText() { 86 | return cdsLspEnvText.getText().trim(); 87 | } 88 | 89 | public void setCdsLspEnvText(@NotNull String newText) { 90 | cdsLspEnvText.setText(newText); 91 | } 92 | 93 | private void nodeJsPathStateHint(String state, JBColor color) { 94 | nodeJsPathText.setToolTipText("Interpreter " + state); 95 | nodeJsPathText.setBackground(color); 96 | } 97 | 98 | private void cdsLspEnvStateHint(String state, JBColor color) { 99 | cdsLspEnvText.setToolTipText("Env setting " + state); 100 | cdsLspEnvText.setBackground(color); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePreviewFormattingService.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.execution.ExecutionException; 4 | import com.intellij.formatting.FormattingRangesInfo; 5 | import com.intellij.formatting.service.FormattingService; 6 | import com.intellij.lang.ImportOptimizer; 7 | import com.intellij.openapi.util.TextRange; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.PsiFile; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.io.IOException; 13 | import java.nio.file.Path; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Set; 17 | 18 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsProvider.SAMPLE_FILE_NAME; 19 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; 20 | import static com.sap.cap.cds.intellij.lspServer.CdsLspServerDescriptor.getFormattingCommandLine; 21 | import static com.sap.cap.cds.intellij.util.FileUtil.createTempDir; 22 | import static java.nio.file.Files.readString; 23 | import static java.nio.file.Files.write; 24 | 25 | public class CdsCodeStylePreviewFormattingService implements FormattingService { 26 | 27 | private static final Map<String, String> formattedByPrettierJson = new HashMap<>(); 28 | private static Path prettierJsonPath; 29 | private static Path tempDir; 30 | private static Path samplePath; 31 | private static String prettierJson = "{}"; 32 | 33 | CdsCodeStylePreviewFormattingService() { 34 | try { 35 | tempDir = createTempDir(CdsCodeStylePreviewFormattingService.class.getName() + "_"); 36 | } catch (IOException e) { 37 | throw new RuntimeException(e); 38 | } 39 | prettierJsonPath = tempDir.resolve(PRETTIER_JSON); 40 | samplePath = tempDir.resolve(SAMPLE_FILE_NAME); 41 | resetSampleSrc(); 42 | } 43 | 44 | public static void acceptSettings(CdsCodeStyleSettings settings) { 45 | prettierJson = settings.getNonDefaultSettings(); 46 | } 47 | 48 | private static void resetSampleSrc() { 49 | try { 50 | write(samplePath, CdsCodeStyleSettings.SAMPLE_SRC.getBytes()); 51 | } catch (IOException e) { 52 | throw new RuntimeException(e); 53 | } 54 | } 55 | 56 | @Override 57 | public @NotNull Set<Feature> getFeatures() { 58 | return Set.of(); 59 | } 60 | 61 | @Override 62 | public boolean canFormat(@NotNull PsiFile psiFile) { 63 | return psiFile.getName().equals(SAMPLE_FILE_NAME); 64 | } 65 | 66 | @Override 67 | public @NotNull PsiElement formatElement(@NotNull PsiElement psiElement, @NotNull TextRange textRange, boolean canChangeWhiteSpaceOnly) { 68 | return this.formatElement(psiElement, canChangeWhiteSpaceOnly); 69 | } 70 | 71 | @Override 72 | public @NotNull PsiElement formatElement(@NotNull PsiElement psiElement, boolean canChangeWhiteSpaceOnly) { 73 | String src = formattedByPrettierJson.get(prettierJson); 74 | if (src == null) { 75 | formattedByPrettierJson.put(prettierJson, src = formatSampleSrc(prettierJson)); 76 | } 77 | psiElement.getContainingFile().getViewProvider().getDocument().setText(src); 78 | return psiElement; 79 | } 80 | 81 | private String formatSampleSrc(String prettierJson) { 82 | resetSampleSrc(); 83 | try { 84 | write(prettierJsonPath, prettierJson.getBytes()); 85 | getFormattingCommandLine(tempDir, samplePath).createProcess().waitFor(); 86 | return readString(samplePath); 87 | } catch (InterruptedException | ExecutionException | IOException e) { 88 | throw new RuntimeException(e); 89 | } 90 | } 91 | 92 | @Override 93 | public void formatRanges(@NotNull PsiFile psiFile, FormattingRangesInfo formattingRangesInfo, boolean canChangeWhiteSpaceOnly, boolean quickFormat) { 94 | } 95 | 96 | @Override 97 | public @NotNull Set<ImportOptimizer> getImportOptimizers(@NotNull PsiFile psiFile) { 98 | return Set.of(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/settings/JsonSettingsManager.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.settings; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.vfs.VirtualFile; 5 | import com.sap.cap.cds.intellij.util.Logger; 6 | import com.sap.cap.cds.intellij.util.LoggerScope; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.File; 10 | import java.io.FileWriter; 11 | import java.io.IOException; 12 | 13 | import static com.intellij.openapi.project.ProjectUtil.guessProjectDir; 14 | import static com.sap.cap.cds.intellij.util.JsonUtil.isJsonEqual; 15 | import static com.sap.cap.cds.intellij.util.Logger.logger; 16 | import static java.nio.file.Files.readString; 17 | 18 | public abstract class JsonSettingsManager<T> { 19 | 20 | protected final Project project; 21 | protected final Logger logger; 22 | protected final File jsonFile; 23 | protected String jsonCached = ""; 24 | 25 | protected JsonSettingsManager(Project project, String fileName, LoggerScope loggerScope) { 26 | this.project = project; 27 | this.logger = logger(project, loggerScope); 28 | this.jsonFile = resolveJsonFile(fileName); 29 | reset(); 30 | } 31 | 32 | private File resolveJsonFile(String fileName) { 33 | VirtualFile guessed = guessProjectDir(project); 34 | String projectDir = guessed != null ? guessed.getPath() : project.getBasePath(); 35 | logger.debug("Project directory: %s".formatted(projectDir)); 36 | 37 | if (projectDir != null) { 38 | File file = new File(projectDir, fileName); 39 | logger.debug("Settings file: %s".formatted(file)); 40 | if (!file.exists()) { 41 | logger.debug("Optional file [%s] does not exist".formatted(file)); 42 | } 43 | return file; 44 | } 45 | return null; 46 | } 47 | 48 | public boolean isJsonFilePresent() { 49 | boolean present = jsonFile != null && jsonFile.exists(); 50 | logger.debug("Settings file present: %s".formatted(present)); 51 | return present; 52 | } 53 | 54 | public boolean isSettingsFileChanged() { 55 | boolean isChanged = !isJsonEqual(jsonCached, readJson()); 56 | if (isChanged) { 57 | logger.debug("Settings file is changed"); 58 | } 59 | return isChanged; 60 | } 61 | 62 | @NotNull 63 | public String readJson() { 64 | logger.debug("Reading settings from file"); 65 | if (!isJsonFilePresent()) { 66 | return ""; 67 | } 68 | try { 69 | jsonCached = readString(jsonFile.toPath()); 70 | logger.debug("Read settings: %s".formatted(jsonCached.replaceAll(" *\n *", " "))); 71 | return jsonCached; 72 | } catch (IOException e) { 73 | logger.error("Failed to read [%s]".formatted(jsonFile), e); 74 | } 75 | return ""; 76 | } 77 | 78 | protected void writeJson(String json) { 79 | logger.debug("Saving settings to file"); 80 | if (jsonFile == null) { 81 | logger.debug("File is null"); 82 | return; 83 | } 84 | if (!jsonFile.getParentFile().exists()) { 85 | logger.debug("Creating directory [%s]".formatted(jsonFile.getParentFile())); 86 | if (!jsonFile.getParentFile().mkdirs()) { 87 | logger.error("Failed to create directory [%s]".formatted(jsonFile.getParentFile())); 88 | return; 89 | } 90 | } 91 | if (!jsonCached.isEmpty() && isJsonEqual(jsonCached, json)) { 92 | logger.debug("Settings are equal, skipping save"); 93 | return; 94 | } 95 | 96 | try (FileWriter writer = new FileWriter(jsonFile)) { 97 | writer.write(json); 98 | jsonCached = json; 99 | logger.debug("Saved settings: %s".formatted(json)); 100 | } catch (IOException e) { 101 | logger.error("Failed to write [%s]".formatted(jsonFile), e); 102 | } 103 | } 104 | 105 | public void reset() { 106 | jsonCached = ""; 107 | } 108 | 109 | public abstract void loadSettingsFromFile(T settings); 110 | public abstract void saveSettingsToFile(T settings); 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleOption.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.OptionAnchor; 4 | import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider.SettingsType; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import static com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider.SettingsType.*; 11 | 12 | public class CdsCodeStyleOption { 13 | /** 14 | * Name of the option (camelCase). Used as configuration key. 15 | */ 16 | public final String name; 17 | /** 18 | * Label of the option (sentence case). Used as UI label. 19 | */ 20 | public final String label; 21 | /** 22 | * Default value of the option. 23 | */ 24 | public final Object defaultValue; 25 | /** 26 | * Group within category (sentence case). Used as collapsable section title in UI. 27 | */ 28 | public final String group; 29 | /** 30 | * Category of the option (title case). Used as tab title in UI. 31 | */ 32 | public final Category category; 33 | /** 34 | * Name of the parent option. 35 | */ 36 | public final String parent; 37 | /** 38 | * Names of the child options. 39 | */ 40 | public final List<String> children; 41 | /** 42 | * Optional values for the option. Used as dropdown values in UI. 43 | */ 44 | public final CdsCodeStyleSettings.Enum[] values; 45 | /** 46 | * Type of the option. 47 | */ 48 | public final Type type; 49 | 50 | public CdsCodeStyleOption(String name, Type type, Object defaultValue, String label, String group, Category category, @Nullable String parent, @Nullable List<String> children, CdsCodeStyleSettings.Enum... values) { 51 | this.name = name; 52 | this.type = type; 53 | this.label = label; 54 | this.defaultValue = defaultValue; 55 | this.group = group; 56 | this.category = category; 57 | this.parent = parent; 58 | this.children = children == null ? List.of() : children; 59 | this.values = values; 60 | } 61 | 62 | public String[] getValuesLabels() { 63 | if (type != Type.ENUM) { 64 | throw new IllegalStateException("Option is not an enum"); 65 | } 66 | return Arrays.stream(values).map(value -> value.getLabel()).toArray(String[]::new); 67 | } 68 | 69 | public int[] getValuesIds() { 70 | if (type != Type.ENUM) { 71 | throw new IllegalStateException("Option is not an enum"); 72 | } 73 | return Arrays.stream(values).mapToInt(value -> value.getId()).toArray(); 74 | } 75 | 76 | public OptionAnchor getAnchor() { 77 | return parent != null ? OptionAnchor.AFTER : null; 78 | } 79 | 80 | public String getAnchorOptionName() { 81 | return parent; 82 | } 83 | 84 | public enum Type { 85 | BOOLEAN(Boolean.class), 86 | ENUM(Integer.class), 87 | INT(Integer.class); 88 | 89 | private final Class<?> fieldType; 90 | 91 | Type(Class<?> fieldType) { 92 | this.fieldType = fieldType; 93 | } 94 | 95 | public Class<?> getFieldType() { 96 | return fieldType; 97 | } 98 | } 99 | 100 | public enum Category { 101 | ALIGNMENT("Alignment", LANGUAGE_SPECIFIC), 102 | BLANK_LINES("Blank Lines", BLANK_LINES_SETTINGS), 103 | COMMENTS("Comments", COMMENTER_SETTINGS), 104 | OTHER("Other", LANGUAGE_SPECIFIC), 105 | SPACES("Spaces", SPACING_SETTINGS), 106 | TABS_AND_INDENTS("Tabs and Indents", INDENT_SETTINGS), 107 | WRAPPING_AND_BRACES("Wrapping and Braces", WRAPPING_AND_BRACES_SETTINGS); 108 | 109 | private final String title; 110 | private final SettingsType settingsType; 111 | 112 | Category(String title, SettingsType settingsType) { 113 | this.title = title; 114 | this.settingsType = settingsType; 115 | } 116 | 117 | public String getTitle() { 118 | return title; 119 | } 120 | 121 | public SettingsType getSettingsType() { 122 | return settingsType; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/lspServer/CdsLspServerDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.lspServer; 2 | 3 | import com.intellij.execution.configurations.GeneralCommandLine; 4 | import org.apache.maven.artifact.versioning.ComparableVersion; 5 | 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.Map; 10 | 11 | import static com.sap.cap.cds.intellij.lspServer.CdsLspServerDescriptor.CommandLineKind.SERVER; 12 | import static com.sap.cap.cds.intellij.lspServer.CdsLspServerDescriptor.CommandLineKind.SERVER_DEBUG; 13 | import static com.sap.cap.cds.intellij.util.JsonUtil.getPropertyAtPath; 14 | import static com.sap.cap.cds.intellij.util.NodeJsUtil.*; 15 | import static com.sap.cap.cds.intellij.util.PathUtil.resolve; 16 | import static com.sap.cap.cds.intellij.util.SupportUtil.isDebugCdsLsp; 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | public class CdsLspServerDescriptor { 20 | 21 | public static final String RELATIVE_SERVER_BASE_PATH = "cds-lsp/node_modules/@sap/cds-lsp/"; 22 | private static final String RELATIVE_SERVER_PATH = RELATIVE_SERVER_BASE_PATH + "dist/main.js"; 23 | private static final String RELATIVE_FORMAT_CLI_PATH = RELATIVE_SERVER_BASE_PATH + "scripts/formatCli.js"; 24 | private static final String RELATIVE_SERVER_PKG_PATH = RELATIVE_SERVER_BASE_PATH + "package.json"; 25 | public static final ComparableVersion REQUIRED_NODEJS_VERSION = getRequiredNodejsVersion(); 26 | private static final String RELATIVE_MITM_PATH = "cds-lsp/mitm.js"; 27 | private static final String RELATIVE_LOG_PATH = "cds-lsp/stdio.json"; 28 | private static final CommandLineKind SERVER_COMMAND_LINE_KIND = getServerCommandLineKind(); 29 | 30 | private static ComparableVersion getRequiredNodejsVersion() { 31 | String serverPkgPath = resolve(RELATIVE_SERVER_PKG_PATH); 32 | String rawVersion; 33 | try { 34 | String serverPkg = new String(Files.readAllBytes(Paths.get(serverPkgPath))); 35 | rawVersion = getPropertyAtPath(serverPkg, new String[]{"engines", "node"}).toString(); 36 | } catch (Exception e) { 37 | throw new RuntimeException("Cannot determine Node.js version required by CDS Language Server at [%s]".formatted(serverPkgPath), e); 38 | } 39 | return extractVersion(rawVersion); 40 | } 41 | 42 | private static CommandLineKind getServerCommandLineKind() { 43 | if (isDebugCdsLsp()) { 44 | return SERVER_DEBUG; 45 | } 46 | return SERVER; 47 | } 48 | 49 | public static GeneralCommandLine getServerCommandLine() { 50 | Map<String, String> envMap = getCdsLspEnvMapFromSetting(); 51 | final String nodeInterpreterPath = getInterpreterFromSetting(); 52 | return switch (SERVER_COMMAND_LINE_KIND) { 53 | case SERVER -> new GeneralCommandLine( 54 | nodeInterpreterPath, 55 | "--enable-source-maps", 56 | resolve(RELATIVE_SERVER_PATH), 57 | "--stdio" 58 | ) 59 | .withEnvironment(envMap) 60 | .withCharset(UTF_8); 61 | 62 | case SERVER_DEBUG -> new GeneralCommandLine( 63 | nodeInterpreterPath, 64 | resolve(RELATIVE_MITM_PATH), 65 | resolve(RELATIVE_LOG_PATH), 66 | nodeInterpreterPath, 67 | "--enable-source-maps", 68 | "--inspect", 69 | resolve(RELATIVE_SERVER_PATH), 70 | "--stdio" 71 | ) 72 | .withEnvironment("CDS_LSP_TRACE_COMPONENTS", "*:verbose") 73 | .withEnvironment(envMap) 74 | .withCharset(UTF_8); 75 | 76 | case CLI_FORMAT -> throw new UnsupportedOperationException("Formatting command line not supported"); 77 | }; 78 | } 79 | 80 | public static GeneralCommandLine getFormattingCommandLine(Path cwd, Path srcPath) { 81 | return new GeneralCommandLine( 82 | getInterpreterFromSetting(), 83 | resolve(RELATIVE_FORMAT_CLI_PATH), 84 | "-f", 85 | srcPath.toString() 86 | ).withWorkDirectory(cwd.toString()); 87 | } 88 | 89 | public enum CommandLineKind { 90 | SERVER, 91 | SERVER_DEBUG, 92 | CLI_FORMAT 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanel.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.codeStyle.OptionTreeWithPreviewPanel; 4 | import com.intellij.openapi.fileTypes.FileType; 5 | import com.intellij.openapi.util.NlsContexts; 6 | import com.intellij.psi.codeStyle.CodeStyleSettings; 7 | import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider; 8 | import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider.SettingsType; 9 | import com.sap.cap.cds.intellij.CdsFileType; 10 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 11 | import com.sap.cap.cds.intellij.lang.CdsLanguage; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.annotations.Nullable; 14 | 15 | import javax.swing.*; 16 | import javax.swing.tree.DefaultTreeModel; 17 | import java.awt.*; 18 | import java.util.Map; 19 | 20 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.BOOLEAN; 21 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsBase.CATEGORY_GROUPS; 22 | import static com.sap.cap.cds.intellij.util.ReflectionUtil.setCustomOptionsEnablement; 23 | 24 | /** 25 | * Custom code-style panel for CDS language with a checkboxes-tree layout. Supports boolean options only. 26 | */ 27 | public class CdsCodeStyleCheckboxesPanel extends OptionTreeWithPreviewPanel implements CdsCodeStylePanel { 28 | 29 | private final Category category; 30 | 31 | public CdsCodeStyleCheckboxesPanel(CodeStyleSettings settings, Category category) { 32 | super(settings); 33 | this.category = category; 34 | init(); 35 | 36 | // Prevent long option labels getting cut off in scroll pane 37 | Dimension minSize = new JLabel("this is quite a long description for a simple setting, eh?").getPreferredSize(); 38 | myPanel.setMinimumSize(minSize); 39 | 40 | getEditor().getSettings().setRightMarginShown(false); 41 | } 42 | 43 | @Override 44 | public Category getCategory() { 45 | return category; 46 | } 47 | 48 | @Override 49 | protected void initTables() { 50 | CATEGORY_GROUPS.get(category).forEach(this::initCustomOptions); 51 | } 52 | 53 | @Override 54 | public SettingsType getSettingsType() { 55 | if (category == null) { 56 | return null; 57 | } 58 | return category.getSettingsType(); 59 | } 60 | 61 | @Override 62 | protected @NotNull FileType getFileType() { 63 | return CdsFileType.INSTANCE; 64 | } 65 | 66 | @Override 67 | public com.intellij.lang.@Nullable Language getDefaultLanguage() { 68 | return CdsLanguage.INSTANCE; 69 | } 70 | 71 | @Override 72 | protected String getPreviewText() { 73 | return CdsCodeStyleSettings.SAMPLE_SRC; 74 | } 75 | 76 | @Override 77 | public void apply(@NotNull CodeStyleSettings settings) { 78 | super.apply(settings); // Applies settings from UI to the settings object 79 | CdsCodeStyleSettings cdsSettings = settings.getCustomSettings(CdsCodeStyleSettings.class); 80 | setOptionsEnablement(cdsSettings.getChildOptionsEnablement(category)); 81 | settings.getIndentOptions().INDENT_SIZE = cdsSettings.tabSize; 82 | CdsCodeStylePreviewFormattingService.acceptSettings(cdsSettings); 83 | } 84 | 85 | @Override 86 | public void setOptionsEnablement(Map<String, Boolean> enablementMap) { 87 | setCustomOptionsEnablement((DefaultTreeModel) myOptionsTree.getModel(), enablementMap); 88 | } 89 | 90 | @Override 91 | protected @NlsContexts.TabTitle @NotNull String getTabTitle() { 92 | if (category == null) { 93 | return ""; 94 | } 95 | return category.getTitle(); 96 | } 97 | 98 | /** 99 | * Show CDS code-style options via the corresponding panel 100 | */ 101 | @Override 102 | protected void customizeSettings() { 103 | LanguageCodeStyleSettingsProvider provider = LanguageCodeStyleSettingsProvider.forLanguage(CdsLanguage.INSTANCE); 104 | if (provider != null) { 105 | provider.customizeSettings(this, getSettingsType()); 106 | } 107 | } 108 | 109 | @Override 110 | public void addOption(CdsCodeStyleOption option) { 111 | if (option.type == BOOLEAN) { 112 | showCustomOption(CdsCodeStyleSettings.class, option.name, option.label, option.group, option.getAnchor(), option.getAnchorOptionName()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.testFramework.LightPlatformTestCase; 4 | 5 | import java.lang.reflect.Field; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | 8 | import static com.intellij.application.options.CodeStyle.createTestSettings; 9 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category.ALIGNMENT; 10 | import static com.sap.cap.cds.intellij.util.ReflectionUtil.setFieldValue; 11 | 12 | public class CdsCodeStyleSettingsTest extends LightPlatformTestCase { 13 | 14 | public void testStaticFields() { 15 | assertNotNull(CdsCodeStyleSettings.SAMPLE_SRC); 16 | assertFalse(CdsCodeStyleSettings.SAMPLE_SRC.isEmpty()); 17 | } 18 | 19 | public void testOptionsConsistency() { 20 | CdsCodeStyleSettings.OPTIONS.forEach((name, option) -> { 21 | assertEquals(name, option.name); 22 | Field field; 23 | try { 24 | field = CdsCodeStyleSettings.class.getDeclaredField(option.name); 25 | } catch (NoSuchFieldException e) { 26 | throw new RuntimeException(e); 27 | } 28 | 29 | assertEquals(getBoxedType(field.getType()), option.type.getFieldType()); 30 | assertEquals(getBoxedType(field.getType()), getBoxedType(option.defaultValue.getClass())); 31 | }); 32 | } 33 | 34 | private Class<?> getBoxedType(Class<?> type) { 35 | if (type == int.class) return Integer.class; 36 | if (type == boolean.class) return Boolean.class; 37 | return type; 38 | } 39 | 40 | public void testGetNonDefaultSettings() throws NoSuchFieldException, IllegalAccessException { 41 | var settings = new CdsCodeStyleSettings(createTestSettings()); 42 | assertEquals("{}", settings.getNonDefaultSettings()); 43 | 44 | var firstBooleanOption = CdsCodeStyleSettings.OPTIONS.values().stream() 45 | .filter(option -> option.type == CdsCodeStyleOption.Type.BOOLEAN) 46 | .findFirst().orElseThrow(); 47 | setFieldValue(settings, firstBooleanOption.name, !(boolean) firstBooleanOption.defaultValue); 48 | assertTrue(settings.getNonDefaultSettings().contains(firstBooleanOption.name)); 49 | } 50 | 51 | public void testToJSON() { 52 | var settings = new CdsCodeStyleSettings(createTestSettings()); 53 | String json = settings.toJSON(); 54 | assertTrue(json.split("\n").length >= CdsCodeStyleSettings.OPTIONS.size() + 2); 55 | } 56 | 57 | public void testEquals() { 58 | var settings = new CdsCodeStyleSettings(createTestSettings()); 59 | String json = settings.getNonDefaultSettings(); 60 | assertTrue(settings.equals(json)); 61 | settings.alignTypes = !settings.alignTypes; 62 | assertFalse(settings.equals(json)); 63 | } 64 | 65 | public void testEqualsSameClass() { 66 | var settings1 = new CdsCodeStyleSettings(createTestSettings()); 67 | var settings2 = new CdsCodeStyleSettings(createTestSettings()); 68 | assertTrue(settings1.equals(settings2)); 69 | settings1.alignAs = !settings1.alignAs; 70 | assertFalse(settings1.equals(settings2)); 71 | } 72 | 73 | public void testChildOptionsEnablement() throws NoSuchFieldException, IllegalAccessException { 74 | var settings = new CdsCodeStyleSettings(createTestSettings()); 75 | CdsCodeStyleOption.Category category = ALIGNMENT; 76 | 77 | var parent = CdsCodeStyleSettings.OPTIONS.values().stream() 78 | .filter(option -> option.category == category) 79 | .filter(option -> option.type == CdsCodeStyleOption.Type.BOOLEAN && !option.children.isEmpty()) 80 | .findFirst().orElseThrow(); 81 | 82 | AtomicBoolean asserted = new AtomicBoolean(); 83 | { 84 | setFieldValue(settings, parent.name, true); 85 | 86 | asserted.set(false); 87 | settings.getChildOptionsEnablement(category).forEach((name, enabled) -> { 88 | if (parent.children.contains(name)) { 89 | assertTrue(enabled); 90 | asserted.set(true); 91 | } 92 | }); 93 | assertTrue(asserted.get()); 94 | } 95 | 96 | { 97 | setFieldValue(settings, parent.name, false); 98 | 99 | asserted.set(false); 100 | settings.getChildOptionsEnablement(category).forEach((name, enabled) -> { 101 | if (parent.children.contains(name)) { 102 | assertFalse(enabled); 103 | asserted.set(true); 104 | } 105 | }); 106 | assertTrue(asserted.get()); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.util; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonPrimitive; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.stream.Stream; 15 | 16 | public class JsonUtil { 17 | 18 | private static final Gson GSON_FOR_SETTINGS = new Gson(); 19 | 20 | public static Object getPropertyAtPath(String json, String[] path) { 21 | JSONObject jsonObject = toJSONObject(json); 22 | for (int i = 0; i < path.length - 1; i++) { 23 | String key = path[i]; 24 | jsonObject = jsonObject.getJSONObject(key); 25 | } 26 | return jsonObject.get(path[path.length - 1]); 27 | } 28 | 29 | public static JSONObject toJSONObject(@NotNull String json) { 30 | return new JSONObject(json.isEmpty() ? "{}" : json); 31 | } 32 | 33 | public static String toSortedString(JSONObject jsonObject) { 34 | String string = jsonObject.toString(JsonUtil.JSON_INDENT); 35 | if (string.trim().matches("\\{\\s*[^,]*}")) { 36 | return string; 37 | } 38 | return "{\n" + 39 | Stream.of(string.split(",?\n")) 40 | .filter(line -> !line.trim().matches("[{}]+")) 41 | .sorted() 42 | .reduce((a, b) -> a + ",\n" + b) 43 | .orElse("") 44 | .replaceFirst("\\s+$", "") 45 | + "\n}"; 46 | } 47 | 48 | public static boolean isJsonEqual(@NotNull String json1, @NotNull String json2) { 49 | if (json1.isEmpty() && json2.isEmpty()) { 50 | return true; 51 | } 52 | if (json1.isEmpty() ^ json2.isEmpty()) { 53 | return false; 54 | } 55 | try { 56 | return new JSONObject(json1).similar(new JSONObject(json2)); 57 | } catch (JSONException e) { 58 | return false; 59 | } 60 | } 61 | 62 | public static JsonObject nest(@NotNull Map<String, Object> flatMap, @Nullable String topLevelKey) { 63 | JsonObject root = new JsonObject(); 64 | JsonObject target = (topLevelKey != null && !topLevelKey.isEmpty()) ? new JsonObject() : root; 65 | if (topLevelKey != null && !topLevelKey.isEmpty()) { 66 | root.add(topLevelKey, target); 67 | } 68 | 69 | for (Map.Entry<String, Object> entry : flatMap.entrySet()) { 70 | String[] parts = entry.getKey().split("\\."); 71 | JsonObject current = target; 72 | for (int i = 0; i < parts.length - 1; i++) { 73 | String part = parts[i]; 74 | if (!current.has(part) || !current.get(part).isJsonObject()) { 75 | current.add(part, new JsonObject()); 76 | } 77 | current = current.getAsJsonObject(part); 78 | } 79 | current.add(parts[parts.length - 1], GSON_FOR_SETTINGS.toJsonTree(entry.getValue())); 80 | } 81 | return root; 82 | } 83 | 84 | public static @NotNull Map<String, Object> flatten(@NotNull JsonObject jsonObject) { 85 | Map<String, Object> flatMap = new HashMap<>(); 86 | flattenRecursively(jsonObject, "", flatMap); 87 | return flatMap; 88 | } 89 | 90 | private static void flattenRecursively(@NotNull JsonObject json, @NotNull String prefix, @NotNull Map<String, Object> result) { 91 | for (Map.Entry<String, JsonElement> entry : json.entrySet()) { 92 | String newKey = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey(); 93 | JsonElement value = entry.getValue(); 94 | 95 | if (value.isJsonObject()) { 96 | flattenRecursively(value.getAsJsonObject(), newKey, result); 97 | } else if (value.isJsonPrimitive()) { 98 | JsonPrimitive primitive = value.getAsJsonPrimitive(); 99 | if (primitive.isBoolean()) { 100 | result.put(newKey, primitive.getAsBoolean()); 101 | } else if (primitive.isString()) { 102 | result.put(newKey, primitive.getAsString()); 103 | } else if (primitive.isNumber()) { 104 | String numStr = primitive.getAsString(); 105 | if (numStr.contains(".")) { 106 | result.put(newKey, primitive.getAsDouble()); 107 | } else { 108 | result.put(newKey, primitive.getAsLong()); 109 | } 110 | } 111 | } else if (value.isJsonArray()) { 112 | result.put(newKey, value.toString()); 113 | } 114 | } 115 | } 116 | 117 | public static final int JSON_INDENT = 2; 118 | } 119 | -------------------------------------------------------------------------------- /src/main/resources/icons/cap.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | id="svg1626" 5 | version="1.1" 6 | height="16" 7 | width="16"> 8 | <path 9 | style="fill:#5a7a94;stroke:none;stroke-width:0.0625" 10 | d="m 3.121576,5.9772812 c -1.7476124,0.3115 -2.45627496,2.595 -1.7929187,4.0624998 0.4587187,1.014813 1.364375,1.545563 2.4179187,1.75 0,-0.253437 0.094463,-0.744375 -0.064237,-0.956125 -0.096444,-0.128625 -0.2988817,-0.162 -0.435763,-0.230875 C 3.0244073,10.490969 2.8097698,10.341656 2.6403823,10.158969 1.7696073,9.2197812 1.9481136,7.741906 3.059076,7.0697188 3.3785948,6.876406 3.759426,6.7485936 4.121576,6.6647812 4.1057,5.895806 3.9629886,5.27955 4.3029012,4.5397812 5.0572384,2.8980937 6.9592636,2.8215624 8.1733884,3.9953687 c 0.4581252,0.4428625 0.57575,0.9861625 0.8231876,1.5444125 0.8719376,-0.2511876 1.579312,-0.97205 2.49975,-0.3594252 0.148688,0.09895 0.281188,0.21885 0.388438,0.3618876 0.237687,0.3168876 0.22225,0.8809124 0.506437,1.1246624 0.195563,0.16775 0.462125,0.2466876 0.667813,0.4059376 0.561874,0.4350624 0.976374,1.18 0.846,1.9044376 -0.103813,0.5770624 -0.431313,1.1086248 -0.908438,1.4506878 -0.194125,0.139187 -0.543938,0.213937 -0.68575,0.4055 -0.158188,0.213687 -0.06425,0.701437 -0.06425,0.956312 0.514188,-0.106125 1.015688,-0.270562 1.4375,-0.595562 1.102875,-0.849813 1.56,-2.4313754 0.980438,-3.7169378 C 14.491576,7.0937188 14.246888,6.715156 13.931638,6.4335936 13.711701,6.237156 13.408451,6.1175936 13.215951,5.8942376 12.943951,5.57865 12.888826,5.1215248 12.602764,4.7900248 12.135826,4.248856 11.349138,3.8737249 10.621576,3.9263062 c -0.421312,0.03045 -0.7886876,0.187175 -1.1875,0.300975 -0.5871248,-2.0380063 -3.5362748,-2.6304188 -5.125,-1.4044188 -0.9855312,0.760525 -1.1875,1.9906624 -1.1875,3.1544188 z" 11 | id="path10" /> 12 | <path 13 | style="fill:#0000ff;stroke:none;stroke-width:0.0625" 14 | d="m 4.996576,2.4147812 0.0625,0.0625 -0.0625,-0.0625 m 4.25,1.375 0.0625,0.0625 -0.0625,-0.0625 m 0.0625,0.125 0.0625,0.0625 -0.0625,-0.0625 m 0.25,0.25 0.0625,0.0625 -0.0625,-0.0625 m -6.3125,0.25 0.0625,0.0625 -0.0625,-0.0625 m 9.4375,0.5 0.0625,0.0625 -0.0625,-0.0625 m -8.625,0.1875 0.0625,0.0625 -0.0625,-0.0625 m -0.0625,0.75 0.0625,0.0625 -0.0625,-0.0625 m 8.125,0.125 0.0625,0.0625 -0.0625,-0.0625 m 1.25,0.0625 0.0625,0.0625 -0.0625,-0.0625 m 0.8125,0.6875 0.0625,0.0625 -0.0625,-0.0625 m -10.625,0.0625 0.0625,0.0625 -0.0625,-0.0625 m -1.9375,0.1875 0.0625,0.0625 -0.0625,-0.0625 m 12.75,0 0.0625,0.0625 -0.0625,-0.0625 m -11.5625,0.1875 0.0625,0.0625 -0.0625,-0.0625 m -1.5625,0.5 0.0625,0.0625 -0.0625,-0.0625 m 12.4375,0.125 0.0625,0.0625 z" 15 | id="path12" /> 16 | <path 17 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 18 | d="M 4.499326,8.587156 C 4.13942,8.7217188 4.1051448,9.184406 4.2714572,9.4772812 c 0.2479188,0.4365624 0.6752812,0.8079378 1.001594,1.1874998 0.1135812,0.132125 0.3356,0.309688 0.3356,0.5 0,0.207313 -0.264,0.418 -0.38855,0.5625 -0.324494,0.376563 -0.8946876,0.8205 -1.0293188,1.3125 -0.096888,0.354 0.1391436,0.80825 0.5557936,0.720125 0.2783876,-0.05881 0.458794,-0.333125 0.634694,-0.532625 0.3846872,-0.43625 0.7744124,-0.869312 1.1511184,-1.3125 C 6.737076,11.673906 6.9534512,11.439906 6.926326,11.102281 6.888202,10.628094 6.3259512,10.192469 6.0326012,9.8522812 5.7286884,9.4998436 5.4315884,9.126906 5.1037324,8.7965936 4.94382,8.6354688 4.7335824,8.4995312 4.499326,8.587156 m 2.6875,-10e-4 C 6.7363884,8.7697188 6.7842012,9.5068436 7.247514,9.637656 7.5328888,9.718281 7.889764,9.66478 8.1840764,9.66478 h 1.9374996 c 0.310812,0 0.7755,0.084062 1.055688,-0.073688 C 11.574201,9.3676548 11.492388,8.68953 11.058138,8.5669048 10.772764,8.48628 10.415888,8.5397808 10.121576,8.5397808 h -1.8125 c -0.3365624,0 -0.80575,-0.082562 -1.12225,0.046375 z" 19 | id="path14" /> 20 | <path 21 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 22 | d="m 4.184076,8.7897812 0.0625,0.0625 -0.0625,-0.0625 m 1,0.125 0.0625,0.0625 -0.0625,-0.0625 m -1.0625,0.125 0.0625,0.0625 -0.0625,-0.0625 m 1.5,0.375 0.0625,0.0625 -0.0625,-0.0625 m -1.3125,0.125 0.0625,0.0625 -0.0625,-0.0625 m 2.8125,0.0625 0.0625,0.0625 -0.0625,-0.0625 m -1.0625,0.3125 0.0625,0.0625 -0.0625,-0.0625 m -1.3125,0.1249998 0.0625,0.0625 z" 23 | id="path16" /> 24 | <path 25 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 26 | d="m 8.0656384,10.648656 c -0.407,0.17275 -0.407,0.8595 0,1.03225 0.21725,0.09219 0.51275,0.04637 0.7434376,0.04637 h 1.5625 c 0.248812,0 0.580188,0.05325 0.805688,-0.07369 0.396937,-0.223438 0.315124,-0.901563 -0.119126,-1.024188 -0.622187,-0.175687 -1.4781868,-0.02712 -2.124062,-0.02712 -0.2648124,0 -0.6195624,-0.05925 -0.8684376,0.04637 z" 27 | id="path18" /> 28 | <path 29 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 30 | d="m 5.621576,11.039781 0.0625,0.0625 z" 31 | id="path20" /> 32 | <path 33 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 34 | d="m 7.186826,12.711156 c -0.4504376,0.183563 -0.4026248,0.920688 0.060688,1.0515 0.2853748,0.08062 0.64225,0.02712 0.9365624,0.02712 h 1.9374996 c 0.310812,0 0.7755,0.08406 1.055688,-0.07369 0.396937,-0.223437 0.315124,-0.901562 -0.119126,-1.024187 -0.285374,-0.08062 -0.64225,-0.02712 -0.936562,-0.02712 H 8.3090764 c -0.3365624,0 -0.80575,-0.08256 -1.12225,0.04638 z" 35 | id="path22" /> 36 | <path 37 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 38 | d="m 4.121576,13.227281 0.0625,0.0625 z" 39 | id="path24" /> 40 | </svg> 41 | -------------------------------------------------------------------------------- /src/main/resources/icons/cap_dark.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | id="svg1626" 5 | version="1.1" 6 | height="16" 7 | width="16"> 8 | <path 9 | style="fill:#9ab1c2;stroke:none;stroke-width:0.0625;fill-opacity:1" 10 | d="m 3.121576,5.9772812 c -1.7476124,0.3115 -2.45627496,2.595 -1.7929187,4.0624998 0.4587187,1.014813 1.364375,1.545563 2.4179187,1.75 0,-0.253437 0.094463,-0.744375 -0.064237,-0.956125 -0.096444,-0.128625 -0.2988817,-0.162 -0.435763,-0.230875 C 3.0244073,10.490969 2.8097698,10.341656 2.6403823,10.158969 1.7696073,9.2197812 1.9481136,7.741906 3.059076,7.0697188 3.3785948,6.876406 3.759426,6.7485936 4.121576,6.6647812 4.1057,5.895806 3.9629886,5.27955 4.3029012,4.5397812 5.0572384,2.8980937 6.9592636,2.8215624 8.1733884,3.9953687 c 0.4581252,0.4428625 0.57575,0.9861625 0.8231876,1.5444125 0.8719376,-0.2511876 1.579312,-0.97205 2.49975,-0.3594252 0.148688,0.09895 0.281188,0.21885 0.388438,0.3618876 0.237687,0.3168876 0.22225,0.8809124 0.506437,1.1246624 0.195563,0.16775 0.462125,0.2466876 0.667813,0.4059376 0.561874,0.4350624 0.976374,1.18 0.846,1.9044376 -0.103813,0.5770624 -0.431313,1.1086248 -0.908438,1.4506878 -0.194125,0.139187 -0.543938,0.213937 -0.68575,0.4055 -0.158188,0.213687 -0.06425,0.701437 -0.06425,0.956312 0.514188,-0.106125 1.015688,-0.270562 1.4375,-0.595562 1.102875,-0.849813 1.56,-2.4313754 0.980438,-3.7169378 C 14.491576,7.0937188 14.246888,6.715156 13.931638,6.4335936 13.711701,6.237156 13.408451,6.1175936 13.215951,5.8942376 12.943951,5.57865 12.888826,5.1215248 12.602764,4.7900248 12.135826,4.248856 11.349138,3.8737249 10.621576,3.9263062 c -0.421312,0.03045 -0.7886876,0.187175 -1.1875,0.300975 -0.5871248,-2.0380063 -3.5362748,-2.6304188 -5.125,-1.4044188 -0.9855312,0.760525 -1.1875,1.9906624 -1.1875,3.1544188 z" 11 | id="path10" /> 12 | <path 13 | style="fill:#0000ff;stroke:none;stroke-width:0.0625" 14 | d="m 4.996576,2.4147812 0.0625,0.0625 -0.0625,-0.0625 m 4.25,1.375 0.0625,0.0625 -0.0625,-0.0625 m 0.0625,0.125 0.0625,0.0625 -0.0625,-0.0625 m 0.25,0.25 0.0625,0.0625 -0.0625,-0.0625 m -6.3125,0.25 0.0625,0.0625 -0.0625,-0.0625 m 9.4375,0.5 0.0625,0.0625 -0.0625,-0.0625 m -8.625,0.1875 0.0625,0.0625 -0.0625,-0.0625 m -0.0625,0.75 0.0625,0.0625 -0.0625,-0.0625 m 8.125,0.125 0.0625,0.0625 -0.0625,-0.0625 m 1.25,0.0625 0.0625,0.0625 -0.0625,-0.0625 m 0.8125,0.6875 0.0625,0.0625 -0.0625,-0.0625 m -10.625,0.0625 0.0625,0.0625 -0.0625,-0.0625 m -1.9375,0.1875 0.0625,0.0625 -0.0625,-0.0625 m 12.75,0 0.0625,0.0625 -0.0625,-0.0625 m -11.5625,0.1875 0.0625,0.0625 -0.0625,-0.0625 m -1.5625,0.5 0.0625,0.0625 -0.0625,-0.0625 m 12.4375,0.125 0.0625,0.0625 z" 15 | id="path12" /> 16 | <path 17 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 18 | d="M 4.499326,8.587156 C 4.13942,8.7217188 4.1051448,9.184406 4.2714572,9.4772812 c 0.2479188,0.4365624 0.6752812,0.8079378 1.001594,1.1874998 0.1135812,0.132125 0.3356,0.309688 0.3356,0.5 0,0.207313 -0.264,0.418 -0.38855,0.5625 -0.324494,0.376563 -0.8946876,0.8205 -1.0293188,1.3125 -0.096888,0.354 0.1391436,0.80825 0.5557936,0.720125 0.2783876,-0.05881 0.458794,-0.333125 0.634694,-0.532625 0.3846872,-0.43625 0.7744124,-0.869312 1.1511184,-1.3125 C 6.737076,11.673906 6.9534512,11.439906 6.926326,11.102281 6.888202,10.628094 6.3259512,10.192469 6.0326012,9.8522812 5.7286884,9.4998436 5.4315884,9.126906 5.1037324,8.7965936 4.94382,8.6354688 4.7335824,8.4995312 4.499326,8.587156 m 2.6875,-10e-4 C 6.7363884,8.7697188 6.7842012,9.5068436 7.247514,9.637656 7.5328888,9.718281 7.889764,9.66478 8.1840764,9.66478 h 1.9374996 c 0.310812,0 0.7755,0.084062 1.055688,-0.073688 C 11.574201,9.3676548 11.492388,8.68953 11.058138,8.5669048 10.772764,8.48628 10.415888,8.5397808 10.121576,8.5397808 h -1.8125 c -0.3365624,0 -0.80575,-0.082562 -1.12225,0.046375 z" 19 | id="path14" /> 20 | <path 21 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 22 | d="m 4.184076,8.7897812 0.0625,0.0625 -0.0625,-0.0625 m 1,0.125 0.0625,0.0625 -0.0625,-0.0625 m -1.0625,0.125 0.0625,0.0625 -0.0625,-0.0625 m 1.5,0.375 0.0625,0.0625 -0.0625,-0.0625 m -1.3125,0.125 0.0625,0.0625 -0.0625,-0.0625 m 2.8125,0.0625 0.0625,0.0625 -0.0625,-0.0625 m -1.0625,0.3125 0.0625,0.0625 -0.0625,-0.0625 m -1.3125,0.1249998 0.0625,0.0625 z" 23 | id="path16" /> 24 | <path 25 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 26 | d="m 8.0656384,10.648656 c -0.407,0.17275 -0.407,0.8595 0,1.03225 0.21725,0.09219 0.51275,0.04637 0.7434376,0.04637 h 1.5625 c 0.248812,0 0.580188,0.05325 0.805688,-0.07369 0.396937,-0.223438 0.315124,-0.901563 -0.119126,-1.024188 -0.622187,-0.175687 -1.4781868,-0.02712 -2.124062,-0.02712 -0.2648124,0 -0.6195624,-0.05925 -0.8684376,0.04637 z" 27 | id="path18" /> 28 | <path 29 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 30 | d="m 5.621576,11.039781 0.0625,0.0625 z" 31 | id="path20" /> 32 | <path 33 | style="fill:#0092d1;stroke:none;stroke-width:0.0625" 34 | d="m 7.186826,12.711156 c -0.4504376,0.183563 -0.4026248,0.920688 0.060688,1.0515 0.2853748,0.08062 0.64225,0.02712 0.9365624,0.02712 h 1.9374996 c 0.310812,0 0.7755,0.08406 1.055688,-0.07369 0.396937,-0.223437 0.315124,-0.901562 -0.119126,-1.024187 -0.285374,-0.08062 -0.64225,-0.02712 -0.936562,-0.02712 H 8.3090764 c -0.3365624,0 -0.80575,-0.08256 -1.12225,0.04638 z" 35 | id="path22" /> 36 | <path 37 | style="fill:#00ffff;stroke:none;stroke-width:0.0625" 38 | d="m 4.121576,13.227281 0.0625,0.0625 z" 39 | id="path24" /> 40 | </svg> 41 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="yes"?> 2 | <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"> 3 | <path 4 | id="path10" 5 | d="m 7.8039401,14.943203 c -4.3690312,0.77875 -6.1406875,6.4875 -4.4822968,10.15625 1.1467968,2.537031 3.4109375,3.863906 6.0447968,4.375 0,-0.633594 0.2361563,-1.860938 -0.1605937,-2.390313 C 8.964737,26.762578 8.4586433,26.67914 8.1164401,26.506953 7.5610183,26.227422 7.0244245,25.85414 6.6009558,25.397422 4.4240183,23.049453 4.8702839,19.354765 7.6476901,17.674297 8.446487,17.191015 9.3985651,16.871484 10.30394,16.661953 c -0.03969,-1.922438 -0.3964686,-3.463078 0.453313,-5.3125 1.885843,-4.1042188 6.640906,-4.295547 9.676218,-1.3610313 1.145313,1.1071563 1.439375,2.4654063 2.057969,3.8610313 2.179844,-0.627969 3.948281,-2.430125 6.249375,-0.898563 0.371719,0.247375 0.702969,0.547125 0.971094,0.904719 0.594219,0.792219 0.555625,2.202281 1.266094,2.811656 0.488906,0.419375 1.155312,0.616719 1.669531,1.014844 1.404687,1.087656 2.440937,2.95 2.115,4.761094 -0.259531,1.442656 -1.078281,2.771562 -2.271094,3.626719 -0.485312,0.347968 -1.359844,0.534843 -1.714375,1.01375 -0.395469,0.534218 -0.160625,1.753593 -0.160625,2.390781 1.285469,-0.265313 2.539219,-0.676406 3.59375,-1.488906 2.757188,-2.124532 3.9,-6.078438 2.451094,-9.292344 -0.432344,-0.958906 -1.044063,-1.905313 -1.832188,-2.609219 -0.549843,-0.491094 -1.307968,-0.79 -1.789218,-1.34839 -0.68,-0.788969 -0.817813,-1.931782 -1.532969,-2.760532 C 30.339565,10.62214 28.372846,9.6843123 26.55394,9.8157654 25.500659,9.8918904 24.582221,10.283703 23.58519,10.568203 22.117378,5.4731873 14.744503,3.992156 10.77269,7.057156 8.308862,8.9584685 7.8039401,12.033812 7.8039401,14.943203 Z" 6 | style="fill:#5a7a94;stroke:none;stroke-width:0.15625" /> 7 | <path 8 | id="path12" 9 | d="m 12.49144,6.0369529 0.15625,0.15625 -0.15625,-0.15625 m 10.625,3.4375 0.15625,0.15625 -0.15625,-0.15625 m 0.15625,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 0.625,0.6250001 0.15625,0.15625 -0.15625,-0.15625 m -15.7812499,0.625 0.15625,0.15625 -0.15625,-0.15625 m 23.5937499,1.25 0.15625,0.15625 -0.15625,-0.15625 m -21.5625,0.46875 0.15625,0.15625 -0.15625,-0.15625 m -0.1562499,1.875 0.1562499,0.15625 -0.1562499,-0.15625 m 20.3124999,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 3.125,0.15625 0.15625,0.15625 -0.15625,-0.15625 m 2.03125,1.71875 0.15625,0.15625 -0.15625,-0.15625 m -26.5624999,0.15625 0.15625,0.15625 -0.15625,-0.15625 m -4.84375,0.46875 0.15625,0.15625 -0.15625,-0.15625 m 31.8749999,0 0.15625,0.15625 -0.15625,-0.15625 m -28.9062499,0.46875 0.15625,0.15625 -0.15625,-0.15625 m -3.90625,1.25 0.15625,0.15625 -0.15625,-0.15625 m 31.0937499,0.3125 0.15625,0.15625 z" 10 | style="fill:#0000ff;stroke:none;stroke-width:0.15625" /> 11 | <path 12 | id="path14" 13 | d="m 11.248315,21.46789 c -0.899765,0.336407 -0.985453,1.493125 -0.569672,2.225313 0.619797,1.091406 1.688203,2.019844 2.503985,2.96875 0.283953,0.330312 0.839,0.774219 0.839,1.25 0,0.518281 -0.66,1.045 -0.971375,1.40625 -0.811235,0.941406 -2.236719,2.05125 -2.573297,3.28125 -0.242219,0.885 0.347859,2.020625 1.389484,1.800312 0.695969,-0.147031 1.146985,-0.832812 1.586735,-1.331562 0.961718,-1.090625 1.936031,-2.173281 2.877796,-3.28125 0.511719,-0.602188 1.052657,-1.187188 0.984844,-2.03125 -0.09531,-1.185469 -1.500937,-2.274531 -2.234312,-3.125 -0.759782,-0.881094 -1.502532,-1.813438 -2.322172,-2.639219 -0.399781,-0.402812 -0.925375,-0.742656 -1.511016,-0.523594 m 6.71875,-0.0025 c -1.126094,0.458907 -1.006562,2.301719 0.151719,2.62875 0.713437,0.201563 1.605625,0.06781 2.341406,0.06781 h 4.84375 c 0.777031,0 1.93875,0.210156 2.639219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -0.713437,-0.201562 -1.605625,-0.06781 -2.341406,-0.06781 h -4.53125 c -0.841406,0 -2.014375,-0.206406 -2.805625,0.115937 z" 14 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" /> 15 | <path 16 | id="path16" 17 | d="m 10.46019,21.974453 0.15625,0.15625 -0.15625,-0.15625 m 2.5,0.3125 0.15625,0.15625 -0.15625,-0.15625 m -2.65625,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 3.75,0.9375 0.15625,0.15625 -0.15625,-0.15625 m -3.28125,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 7.03125,0.15625 0.15625,0.15625 -0.15625,-0.15625 m -2.65625,0.78125 0.15625,0.15625 -0.15625,-0.15625 m -3.28125,0.3125 0.15625,0.15625 z" 18 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" /> 19 | <path 20 | id="path18" 21 | d="m 20.164096,26.62164 c -1.0175,0.431875 -1.0175,2.14875 0,2.580625 0.543125,0.230469 1.281875,0.115938 1.858594,0.115938 h 3.90625 c 0.622031,0 1.450469,0.133125 2.014219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -1.555468,-0.439218 -3.695468,-0.06781 -5.310156,-0.06781 -0.662031,0 -1.548906,-0.148125 -2.171094,0.115937 z" 22 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" /> 23 | <path 24 | id="path20" 25 | d="m 14.05394,27.599453 0.15625,0.15625 z" 26 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" /> 27 | <path 28 | id="path22" 29 | d="m 17.967065,31.77789 c -1.126094,0.458907 -1.006562,2.301719 0.151719,2.62875 0.713437,0.201563 1.605625,0.06781 2.341406,0.06781 h 4.84375 c 0.777031,0 1.93875,0.210156 2.639219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -0.713437,-0.201562 -1.605625,-0.06781 -2.341406,-0.06781 h -4.53125 c -0.841406,0 -2.014375,-0.206406 -2.805625,0.115937 z" 30 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" /> 31 | <path 32 | id="path24" 33 | d="m 10.30394,33.068203 0.15625,0.15625 z" 34 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" /> 35 | </svg> 36 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon_dark.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | id="svg924" 5 | version="1.1" 6 | height="40" 7 | width="40"> 8 | <path 9 | style="fill:#9ab1c2;stroke:none;stroke-width:0.15625;fill-opacity:1" 10 | d="m 7.8039401,14.943203 c -4.3690312,0.77875 -6.1406875,6.4875 -4.4822968,10.15625 1.1467968,2.537031 3.4109375,3.863906 6.0447968,4.375 0,-0.633594 0.2361563,-1.860938 -0.1605937,-2.390313 C 8.964737,26.762578 8.4586433,26.67914 8.1164401,26.506953 7.5610183,26.227422 7.0244245,25.85414 6.6009558,25.397422 4.4240183,23.049453 4.8702839,19.354765 7.6476901,17.674297 8.446487,17.191015 9.3985651,16.871484 10.30394,16.661953 c -0.03969,-1.922438 -0.3964686,-3.463078 0.453313,-5.3125 1.885843,-4.1042188 6.640906,-4.295547 9.676218,-1.3610313 1.145313,1.1071563 1.439375,2.4654063 2.057969,3.8610313 2.179844,-0.627969 3.948281,-2.430125 6.249375,-0.898563 0.371719,0.247375 0.702969,0.547125 0.971094,0.904719 0.594219,0.792219 0.555625,2.202281 1.266094,2.811656 0.488906,0.419375 1.155312,0.616719 1.669531,1.014844 1.404687,1.087656 2.440937,2.95 2.115,4.761094 -0.259531,1.442656 -1.078281,2.771562 -2.271094,3.626719 -0.485312,0.347968 -1.359844,0.534843 -1.714375,1.01375 -0.395469,0.534218 -0.160625,1.753593 -0.160625,2.390781 1.285469,-0.265313 2.539219,-0.676406 3.59375,-1.488906 2.757188,-2.124532 3.9,-6.078438 2.451094,-9.292344 -0.432344,-0.958906 -1.044063,-1.905313 -1.832188,-2.609219 -0.549843,-0.491094 -1.307968,-0.79 -1.789218,-1.34839 -0.68,-0.788969 -0.817813,-1.931782 -1.532969,-2.760532 C 30.339565,10.62214 28.372846,9.6843123 26.55394,9.8157654 25.500659,9.8918904 24.582221,10.283703 23.58519,10.568203 22.117378,5.4731873 14.744503,3.992156 10.77269,7.057156 8.308862,8.9584685 7.8039401,12.033812 7.8039401,14.943203 Z" 11 | id="path10" /> 12 | <path 13 | style="fill:#0000ff;stroke:none;stroke-width:0.15625" 14 | d="m 12.49144,6.0369529 0.15625,0.15625 -0.15625,-0.15625 m 10.625,3.4375 0.15625,0.15625 -0.15625,-0.15625 m 0.15625,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 0.625,0.6250001 0.15625,0.15625 -0.15625,-0.15625 m -15.7812499,0.625 0.15625,0.15625 -0.15625,-0.15625 m 23.5937499,1.25 0.15625,0.15625 -0.15625,-0.15625 m -21.5625,0.46875 0.15625,0.15625 -0.15625,-0.15625 m -0.1562499,1.875 0.1562499,0.15625 -0.1562499,-0.15625 m 20.3124999,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 3.125,0.15625 0.15625,0.15625 -0.15625,-0.15625 m 2.03125,1.71875 0.15625,0.15625 -0.15625,-0.15625 m -26.5624999,0.15625 0.15625,0.15625 -0.15625,-0.15625 m -4.84375,0.46875 0.15625,0.15625 -0.15625,-0.15625 m 31.8749999,0 0.15625,0.15625 -0.15625,-0.15625 m -28.9062499,0.46875 0.15625,0.15625 -0.15625,-0.15625 m -3.90625,1.25 0.15625,0.15625 -0.15625,-0.15625 m 31.0937499,0.3125 0.15625,0.15625 z" 15 | id="path12" /> 16 | <path 17 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" 18 | d="m 11.248315,21.46789 c -0.899765,0.336407 -0.985453,1.493125 -0.569672,2.225313 0.619797,1.091406 1.688203,2.019844 2.503985,2.96875 0.283953,0.330312 0.839,0.774219 0.839,1.25 0,0.518281 -0.66,1.045 -0.971375,1.40625 -0.811235,0.941406 -2.236719,2.05125 -2.573297,3.28125 -0.242219,0.885 0.347859,2.020625 1.389484,1.800312 0.695969,-0.147031 1.146985,-0.832812 1.586735,-1.331562 0.961718,-1.090625 1.936031,-2.173281 2.877796,-3.28125 0.511719,-0.602188 1.052657,-1.187188 0.984844,-2.03125 -0.09531,-1.185469 -1.500937,-2.274531 -2.234312,-3.125 -0.759782,-0.881094 -1.502532,-1.813438 -2.322172,-2.639219 -0.399781,-0.402812 -0.925375,-0.742656 -1.511016,-0.523594 m 6.71875,-0.0025 c -1.126094,0.458907 -1.006562,2.301719 0.151719,2.62875 0.713437,0.201563 1.605625,0.06781 2.341406,0.06781 h 4.84375 c 0.777031,0 1.93875,0.210156 2.639219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -0.713437,-0.201562 -1.605625,-0.06781 -2.341406,-0.06781 h -4.53125 c -0.841406,0 -2.014375,-0.206406 -2.805625,0.115937 z" 19 | id="path14" /> 20 | <path 21 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" 22 | d="m 10.46019,21.974453 0.15625,0.15625 -0.15625,-0.15625 m 2.5,0.3125 0.15625,0.15625 -0.15625,-0.15625 m -2.65625,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 3.75,0.9375 0.15625,0.15625 -0.15625,-0.15625 m -3.28125,0.3125 0.15625,0.15625 -0.15625,-0.15625 m 7.03125,0.15625 0.15625,0.15625 -0.15625,-0.15625 m -2.65625,0.78125 0.15625,0.15625 -0.15625,-0.15625 m -3.28125,0.3125 0.15625,0.15625 z" 23 | id="path16" /> 24 | <path 25 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" 26 | d="m 20.164096,26.62164 c -1.0175,0.431875 -1.0175,2.14875 0,2.580625 0.543125,0.230469 1.281875,0.115938 1.858594,0.115938 h 3.90625 c 0.622031,0 1.450469,0.133125 2.014219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -1.555468,-0.439218 -3.695468,-0.06781 -5.310156,-0.06781 -0.662031,0 -1.548906,-0.148125 -2.171094,0.115937 z" 27 | id="path18" /> 28 | <path 29 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" 30 | d="m 14.05394,27.599453 0.15625,0.15625 z" 31 | id="path20" /> 32 | <path 33 | style="fill:#0092d1;stroke:none;stroke-width:0.15625" 34 | d="m 17.967065,31.77789 c -1.126094,0.458907 -1.006562,2.301719 0.151719,2.62875 0.713437,0.201563 1.605625,0.06781 2.341406,0.06781 h 4.84375 c 0.777031,0 1.93875,0.210156 2.639219,-0.184219 0.992344,-0.558594 0.787812,-2.253906 -0.297813,-2.560469 -0.713437,-0.201562 -1.605625,-0.06781 -2.341406,-0.06781 h -4.53125 c -0.841406,0 -2.014375,-0.206406 -2.805625,0.115937 z" 35 | id="path22" /> 36 | <path 37 | style="fill:#00ffff;stroke:none;stroke-width:0.15625" 38 | d="m 10.30394,33.068203 0.15625,0.15625 z" 39 | id="path24" /> 40 | </svg> 41 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceProjectSettingsTest.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.openapi.fileEditor.FileEditorStateLevel; 5 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager; 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.io.IOException; 10 | 11 | import static com.intellij.testFramework.LoggedErrorProcessor.executeAndReturnLoggedError; 12 | import static com.sap.cap.cds.intellij.util.JsonUtil.isJsonEqual; 13 | 14 | public class CdsCodeStyleSettingsServiceProjectSettingsTest extends CdsCodeStyleSettingsServiceTestBase { 15 | 16 | @Override 17 | protected void setUp() throws Exception { 18 | super.setUp(); 19 | setPerProjectSettings(true); 20 | } 21 | 22 | public void testUsesProjectSettings() { 23 | openProject(); 24 | assertTrue(CodeStyle.usesOwnSettings(project)); 25 | } 26 | 27 | public void testDefaultSettings() { 28 | openProject(); 29 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 30 | } 31 | 32 | // Direction .cdsprettier.json → settings 33 | 34 | public void testPrettierJsonLifecycle() throws IOException { 35 | openProject(); 36 | createPrettierJson(); 37 | writePrettierJson("{}"); 38 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 39 | 40 | writePrettierJson("{ \"tabSize\": %d, \"alignAs\": %b }".formatted(42, !defaults.alignAs)); 41 | assertEquals(42, getCdsCodeStyleSettings().tabSize); 42 | assertEquals(!defaults.alignAs, getCdsCodeStyleSettings().alignAs); 43 | 44 | deletePrettierJson(); 45 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 46 | } 47 | 48 | public void testExistentPrettierJson() throws Exception { 49 | createPrettierJson(); 50 | writePrettierJson("{ tabSize: 42 }"); 51 | openProject(); 52 | assertEquals(42, getCdsCodeStyleSettings().tabSize); 53 | } 54 | 55 | public void testInvalidPrettierJson() { 56 | Throwable exception = executeAndReturnLoggedError(() -> { 57 | createPrettierJson(); 58 | try { 59 | writePrettierJson("invalid JSON"); 60 | } catch (IOException e) { 61 | throw new RuntimeException(e); 62 | } 63 | openProject(); 64 | }); 65 | assertInstanceOf(exception, JSONException.class); 66 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 67 | } 68 | 69 | // Direction settings → .cdsprettier.json 70 | 71 | // TODO test project open with non-project settings → no .cdsprettier.json 72 | 73 | public void testProjectOpenedWithDefaultSettings_noCdsPrettierJsonCreation() throws IOException { 74 | openProject(); 75 | assertFalse(prettierJson.exists()); 76 | } 77 | 78 | // HOT-TODO(reenable): failing due to file is not seen as open in second notify call 79 | // public void testSettingChanged_createsPrettierJsonOnlyIfCdsEditorOpen() throws IOException { 80 | // CodeStyleSettingsManager codeStyleSettingsManager = CodeStyleSettingsManager.getInstance(project); 81 | // 82 | // openProject(); 83 | // getCdsCodeStyleSettings().tabSize = 42; 84 | // codeStyleSettingsManager.notifyCodeStyleSettingsChanged(); 85 | // assertFalse(prettierJson.exists()); 86 | // 87 | // createCdsFile(); 88 | // var editors = openCdsFile(); 89 | // assertEquals(1, editors.length); 90 | // var state = editors[0].getState(FileEditorStateLevel.FULL); 91 | // getCdsCodeStyleSettings().tabSize = 23; 92 | // codeStyleSettingsManager.notifyCodeStyleSettingsChanged(); 93 | // assertTrue(prettierJson.exists()); 94 | // assertEquals(42, new JSONObject(readPrettierJson()).get("tabSize")); 95 | // } 96 | 97 | public void testSettingChangedWithExistingCdsPrettierJson_updatesFile() throws IOException { 98 | openProject(); 99 | createPrettierJson(); 100 | writePrettierJson("{}"); 101 | getCdsCodeStyleSettings().tabSize = 42; 102 | CodeStyleSettingsManager.getInstance(project).notifyCodeStyleSettingsChanged(); 103 | assertEquals(42, new JSONObject(readPrettierJson()).get("tabSize")); 104 | } 105 | 106 | public void testWriteOnlyLoadedOrNonDefaultOptions() throws Exception { 107 | createPrettierJson(); 108 | writePrettierJson("{ \"tabSize\": %d, \"alignAs\": %b }".formatted(defaults.tabSize, !defaults.alignAs)); 109 | openProject(); 110 | assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); 111 | assertEquals(!defaults.alignAs, getCdsCodeStyleSettings().alignAs); 112 | 113 | getCdsCodeStyleSettings().alignTypes = !defaults.alignTypes; 114 | CodeStyleSettingsManager.getInstance(project).notifyCodeStyleSettingsChanged(); 115 | String expected = "{ \"tabSize\": %d, \"alignAs\": %b, \"alignTypes\": %b }".formatted(defaults.tabSize, !defaults.alignAs, !defaults.alignTypes); 116 | assertTrue(isJsonEqual(expected, readPrettierJson())); 117 | } 118 | 119 | public void testNoChanges_doNotReformatExistingPrettierJson() throws Exception { 120 | createPrettierJson(); 121 | String json = "{ \"tabSize\": 19, \"alignAs\": true }"; 122 | writePrettierJson(json); 123 | openProject(); 124 | 125 | CodeStyleSettingsManager.getInstance(project).notifyCodeStyleSettingsChanged(); 126 | assertEquals(json, readPrettierJson()); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceTestBase.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.application.options.CodeStyle; 4 | import com.intellij.openapi.application.WriteAction; 5 | import com.intellij.openapi.fileEditor.FileEditor; 6 | import com.intellij.openapi.fileEditor.FileEditorManager; 7 | import com.intellij.openapi.module.Module; 8 | import com.intellij.openapi.module.ModuleManager; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.openapi.roots.ModuleRootModificationUtil; 11 | import com.intellij.openapi.vfs.LocalFileSystem; 12 | import com.intellij.openapi.vfs.VirtualFile; 13 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager; 14 | import com.intellij.testFramework.HeavyPlatformTestCase; 15 | import com.intellij.testFramework.PlatformTestUtil; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.nio.file.Path; 21 | 22 | import static com.intellij.application.options.CodeStyle.createTestSettings; 23 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; 24 | import static java.util.Objects.requireNonNull; 25 | 26 | @SuppressWarnings("NewClassNamingConvention") 27 | public class CdsCodeStyleSettingsServiceTestBase extends HeavyPlatformTestCase { 28 | 29 | private static final String SOME_CDS = "a.cds"; 30 | 31 | protected CdsCodeStyleSettings defaults; 32 | protected Project project; 33 | protected File prettierJson; 34 | private Path projectDir; 35 | private VirtualFile projectDirVFile; 36 | 37 | protected static void setPerProjectSettings(boolean perProjectSettings) { 38 | CodeStyleSettingsManager.getInstance().USE_PER_PROJECT_SETTINGS = perProjectSettings; 39 | } 40 | 41 | @Override 42 | protected void setUp() throws Exception { 43 | super.setUp(); 44 | defaults = new CdsCodeStyleSettings(createTestSettings()); 45 | File tempDirectory = getTempDirectory(); 46 | projectDir = tempDirectory.toPath(); 47 | projectDirVFile = getVFile(tempDirectory); 48 | prettierJson = projectDir.resolve(PRETTIER_JSON).toFile(); 49 | } 50 | 51 | @Override 52 | protected void tearDown() throws Exception { 53 | if (project != null) { 54 | PlatformTestUtil.forceCloseProjectWithoutSaving(project); 55 | } 56 | super.tearDown(); 57 | } 58 | 59 | private @NotNull File getTempDirectory() throws IOException { 60 | File tempDirectory = createTempDirectory(); 61 | refreshVfsForDirAndChildren(tempDirectory.toPath()); 62 | return tempDirectory; 63 | } 64 | 65 | private @NotNull VirtualFile getVFile(File file) { 66 | return requireNonNull(LocalFileSystem.getInstance().findFileByIoFile(file)); 67 | } 68 | 69 | protected void createPrettierJson() { 70 | try { 71 | WriteAction.computeAndWait(() -> projectDirVFile.findOrCreateChildData(this, PRETTIER_JSON)); 72 | } catch (IOException e) { 73 | throw new RuntimeException(e); 74 | } 75 | } 76 | 77 | protected void createCdsFile() { 78 | try { 79 | WriteAction.computeAndWait(() -> projectDirVFile.findOrCreateChildData(this, SOME_CDS)); 80 | } catch (IOException e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | 85 | protected void deletePrettierJson() throws IOException { 86 | refreshVfsForFile(prettierJson, false); 87 | WriteAction.runAndWait(() -> getVFile(prettierJson).delete(this)); 88 | } 89 | 90 | protected void writePrettierJson(String settings) throws IOException { 91 | if (!prettierJson.exists()) { 92 | throw new IOException(".cdsprettier.json does not exist"); 93 | } 94 | refreshVfsForFile(prettierJson, false); 95 | WriteAction.runAndWait(() -> getVFile(prettierJson).setBinaryContent(settings.getBytes())); 96 | } 97 | 98 | protected String readPrettierJson() throws IOException { 99 | if (!prettierJson.exists()) { 100 | throw new IOException(".cdsprettier.json does not exist"); 101 | } 102 | refreshVfsForFile(prettierJson, true); 103 | return new String(requireNonNull(getVFile(prettierJson).contentsToByteArray())); 104 | } 105 | 106 | protected void refreshVfsForFile(File file, boolean includingContent) { 107 | VirtualFile vFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); 108 | if (includingContent && vFile != null) { 109 | vFile.refresh(false, true); 110 | } 111 | } 112 | 113 | protected void refreshVfsForDirAndChildren(Path dir) { 114 | VirtualFile vFile = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(dir); 115 | if (vFile != null) { 116 | requireNonNull(vFile).refresh(false, true); 117 | } 118 | } 119 | 120 | protected void openProject() { 121 | project = PlatformTestUtil.loadAndOpenProject(projectDir, getTestRootDisposable()); 122 | WriteAction.runAndWait(() -> { 123 | Module module = ModuleManager.getInstance(project).newModule(projectDir.resolve("test.iml").toString(), "ffo"); 124 | ModuleRootModificationUtil.addContentRoot(module, projectDir.toString()); 125 | }); 126 | } 127 | 128 | protected FileEditor[] openCdsFile() { 129 | FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); 130 | return fileEditorManager.openFile(getVFile(projectDir.resolve(SOME_CDS).toFile()), false); 131 | } 132 | 133 | @NotNull 134 | protected CdsCodeStyleSettings getCdsCodeStyleSettings() { 135 | return CodeStyle.getSettings(project).getCustomSettings(CdsCodeStyleSettings.class); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsBase.java: -------------------------------------------------------------------------------- 1 | package com.sap.cap.cds.intellij.codestyle; 2 | 3 | import com.intellij.psi.codeStyle.CodeStyleSettings; 4 | import com.intellij.psi.codeStyle.CodeStyleSettingsManager; 5 | import com.intellij.psi.codeStyle.CustomCodeStyleSettings; 6 | import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Category; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.json.JSONObject; 9 | 10 | import java.util.*; 11 | 12 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.BOOLEAN; 13 | import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleOption.Type.ENUM; 14 | import static com.sap.cap.cds.intellij.util.JsonUtil.toJSONObject; 15 | import static com.sap.cap.cds.intellij.util.JsonUtil.toSortedString; 16 | import static com.sap.cap.cds.intellij.util.ReflectionUtil.getFieldValue; 17 | import static com.sap.cap.cds.intellij.util.ReflectionUtil.setFieldValue; 18 | import static java.util.stream.Collectors.toMap; 19 | 20 | public abstract class CdsCodeStyleSettingsBase extends CustomCodeStyleSettings { 21 | public static final Map<String, CdsCodeStyleOption> OPTIONS = new LinkedHashMap<>(); 22 | public static final Map<Category, Set<String>> CATEGORY_GROUPS = new LinkedHashMap<>(); 23 | protected final List<String> loadedOptions = new ArrayList<>(); 24 | 25 | CdsCodeStyleSettingsBase(@NotNull CodeStyleSettings container) { 26 | super("CDSCodeStyleSettings", container); 27 | } 28 | 29 | private static String getEnumLabel(String name, int id) { 30 | return OPTIONS.get(name).values[id].getLabel(); 31 | } 32 | 33 | private static int getEnumId(CdsCodeStyleOption option, String label) { 34 | return Arrays.stream(option.values).filter(v -> v.getLabel().equals(label)).findFirst().orElseThrow().getId(); 35 | } 36 | 37 | public void loadFrom(String prettierJson) { 38 | loadedOptions.clear(); 39 | var json = toJSONObject(prettierJson); 40 | OPTIONS.forEach((name, option) -> { 41 | if (!json.has(name)) { 42 | return; 43 | } 44 | final var value = json.get(name); 45 | if (value != null) { 46 | loadedOptions.add(name); 47 | try { 48 | if (option.values.length > 0) { 49 | setFieldValue(this, name, getEnumId(option, (String) value)); 50 | } else { 51 | setFieldValue(this, name, value); 52 | } 53 | } catch (NoSuchFieldException | IllegalAccessException e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | }); 58 | } 59 | 60 | public boolean equals(String prettierJson) { 61 | CodeStyleSettings container = CodeStyleSettingsManager.getInstance().createSettings(); 62 | CdsCodeStyleSettings other = container.getCustomSettings(CdsCodeStyleSettings.class); 63 | other.loadFrom(prettierJson); 64 | return this.equals(other); 65 | } 66 | 67 | public boolean equals(CdsCodeStyleSettings other) { 68 | return OPTIONS.keySet().stream() 69 | .allMatch(name -> { 70 | try { 71 | return Objects.equals(getFieldValue(this, name, null), getFieldValue(other, name, null)); 72 | } catch (NoSuchFieldException | IllegalAccessException e) { 73 | throw new RuntimeException(e); 74 | } 75 | }); 76 | } 77 | 78 | public boolean isDefault() { 79 | return OPTIONS.values().stream() 80 | .allMatch(option -> option.defaultValue.equals(getValue(option.name))); 81 | } 82 | 83 | public String getNonDefaultSettings() { 84 | var map = OPTIONS.values().stream() 85 | .filter(option -> !option.defaultValue.equals(getValue(option.name))) 86 | .map(this::getEntry) 87 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 88 | return toSortedString(new JSONObject(map)); 89 | } 90 | 91 | public String getLoadedOrNonDefaultSettings() { 92 | var map = OPTIONS.values().stream() 93 | .filter(option -> loadedOptions.contains(option.name) || !option.defaultValue.equals(getValue(option.name))) 94 | .map(this::getEntry) 95 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 96 | return toSortedString(new JSONObject(map)); 97 | } 98 | 99 | public String toJSON() { 100 | var map = OPTIONS.values().stream() 101 | .map(this::getEntry) 102 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 103 | return toSortedString(new JSONObject(map)); 104 | } 105 | 106 | private Map.Entry<String, ?> getEntry(CdsCodeStyleOption option) { 107 | Object value = getValue(option.name); 108 | return option.type == ENUM 109 | ? Map.entry(option.name, getEnumLabel(option.name, (int) value)) 110 | : Map.entry(option.name, value); 111 | } 112 | 113 | private Object getValue(String name) { 114 | final Object value; 115 | try { 116 | value = getFieldValue(this, name, null); 117 | } catch (NoSuchFieldException | IllegalAccessException e) { 118 | throw new RuntimeException(e); 119 | } 120 | return value; 121 | } 122 | 123 | public Map<String, Boolean> getChildOptionsEnablement(Category category) { 124 | return OPTIONS.values().stream() 125 | .filter(option -> option.category == category) 126 | .filter(option -> option.type == BOOLEAN && !option.children.isEmpty()) 127 | .flatMap(parent -> parent.children.stream() 128 | .map(child -> Map.entry(child, (boolean) getValue(parent.name)))) 129 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | 3 | CDS Language Support for IntelliJ offers the following features based on the LSP4IJ plugin: 4 | 5 | | Feature | LSP4IJ Support | Server Support | LSP Request | Remarks | Tested Working | 6 | | ------------------- | -------------- | :------------: | -------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------- | 7 | | Syntax Highlighting | ✔ | ✔ | (local, based on TextMate) | TM Bundle is automatically registered on plugin installation (and disabled on uninstallation). | ✓ | 8 | | Code Completion | ✔ | ✔ | textDocument/completion | Completing with global identifiers supported with completionItem/resolve | ✓ local, global identifiers | 9 | | Goto Definition | ✔ | ✔ | textDocument/definition | | ✓ *Go declaration or usages* | 10 | | Goto Implementation | ✔ | ✔ | textDocument/implementation | | ✓ | 11 | | Hover Documentation | ✔ | ✔ | textDocument/hover | | ✓ | 12 | | Document Formatting | ✔ | ✔ | textDocument/formatting | | ✓ | 13 | | Range Formatting | ✔ | ✔ | textDocument/rangeFormatting | Format selected text ranges | ✓ | 14 | | Diagnostics | ✔ | ✔ | textDocument/publishDiagnostics | Problems (errors, warnings). | ✓ | 15 | | Quick Fixes | ✔ | ✔ | textDocument/codeAction | Only for Diagnostics, no Intention Actions provided by server yet | ✓ | 16 | | Find References | ✔ | ✔ | textDocument/references | | ✓ | 17 | | Selection Range | ✔ | ✔ | textDocument/selectionRange | Smart selection expansion | ❌ | 18 | | Semantic Tokens | ✔ | (✔) | textDocument/semanticTokens/full | Server supports only textDocument/semanticTokens | n/a | 19 | | Document Highlights | ✔ | ✔ | textDocument/documentHighlight | | ✓ | 20 | | Document Links | ✔ | ✔ | textDocument/documentLink | | ✓ | 21 | | AnalyzeDependencies | ✔ | ✔ | | Statistics for imported path | ✓ | 22 | | Commands | ✔ | ✔ | workspace/executeCommand | | ✓ (implicitly) | 23 | | Code Lens | ✔ | (✔) | textDocument/codeLens | Only used to display statistics | ✓ | 24 | | Outline | ✔ | ✔ | textDocument/documentSymbol | both flat and hierarchical (IJ seems to only support hierarchical) | ✓ hierarchical | 25 | | Workspace Symbols | ✔ | ✔ | workspace/symbol | Workspace-wide symbol search | ✓ | 26 | 27 | ### Examples 28 | 29 | #### Document Highlights, Hover Documentation, Outline 30 | 31 | ![Demo of Document Highlights, Hover Documentation, Outline](../.assets/highlights+hover+outline.png) 32 | 33 | #### Syntax Highlighting, Code Completion, Diagnostics 34 | 35 | ![Demo of Syntax Highlighting, Code Completion, Diagnostics](../.assets/syntax+completion+diagnostics.png) 36 | 37 | #### Goto Definition 38 | 39 | ![Demo of Goto Definition](../.assets/goto_definition.gif) 40 | 41 | #### Goto Implementation 42 | 43 | Navigate to the custom Node.js service implementation: 44 | 45 | ![Demo of Goto Implementation](../.assets/goto_implementation.gif) 46 | 47 | #### Quick Fix 48 | 49 | ![Demo of Quick Fix](../.assets/quick_fix.png) 50 | 51 | #### Hover Documentation 52 | 53 | ![Demo of Hover Documentation](../.assets/hover_documentation.png) 54 | 55 | #### Find References 56 | 57 | ![Demo of Find References](../.assets/find_references.png) 58 | 59 | #### Outline 60 | 61 | ![Demo of Outline](../.assets/outline.png) 62 | 63 | #### Document Formatting 64 | 65 | ![Demo of Document Formatting](../.assets/document_formatting.gif) 66 | 67 | #### Adjust the Code Style 68 | 69 | Changes in the Settings UI will synchronized with `.cdsprettier.json` in the workspace. 70 | 71 | ![Demo of Code Style Settings](../.assets/code_style_settings.png) 72 | 73 | #### Configure the CDS Language Server 74 | 75 | Changes in the Settings UI will synchronized with `.cds-lsp/.settings.json` in the workspace. 76 | 77 | ![Demo of CDS Language Server Settings](../.assets/cds_language_server_settings.png) 78 | 79 | ## Known Issues 80 | 81 | - Maintain Translation quickfix works in principle, but properties file is not saved and thus LSP won't get updated and 82 | still suggests quickfix 83 | - Range Formatting not correctly treating first line of selection 84 | - Document Highlights not shown reliably 85 | --------------------------------------------------------------------------------