From a6de1e52c3aaac325bcff2df12f2fcd6b87866c8 Mon Sep 17 00:00:00 2001 From: Tim Schulze-Hartung <108271660+tim-sh@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:20:36 +0100 Subject: [PATCH] feat: sync settings with .cdsprettier.json changes (#98) Also includes: * chore: deduplicate textmate-bundle registration * chore: avoid redundant settings reload --- .gitignore | 1 + build.gradle | 7 + .../intellij/CdsSyntaxHighlighterFactory.java | 20 --- .../CdsCodeStyleCheckboxesPanel.java | 2 +- .../codestyle/CdsCodeStyleMainPanel.java | 5 +- ...CdsCodeStylePreviewFormattingService.java} | 8 +- .../CdsCodeStyleSettingsService.java | 151 ++++++++++++++++++ .../codestyle/CdsCodeStyleTabularPanel.java | 2 +- .../CdsDefaultCodeStyleSettingsService.java | 21 --- .../codestyle/CdsPrettierJsonListener.java | 45 ++++-- .../codestyle/CdsPrettierJsonService.java | 68 -------- .../CdsProjectCodeStyleSettingsService.java | 38 ----- .../lifecycle/IdeLifecycleListener.java | 16 ++ .../lifecycle/PluginLifecycleListener.java | 28 ++++ .../lifecycle/ProjectLifecycleListener.kt | 16 ++ ...ger.java => CdsTextMateBundleService.java} | 56 +------ .../com/sap/cap/cds/intellij/util/Logger.java | 26 +++ src/main/resources/META-INF/plugin.xml | 39 ++--- ...yleSettingsServiceProjectSettingsTest.java | 58 +++++++ .../CdsCodeStyleSettingsServiceTestBase.java | 97 +++++++++++ .../lsp/CdsLspServerDescriptorTest.java | 4 - 21 files changed, 463 insertions(+), 245 deletions(-) delete mode 100644 src/main/java/com/sap/cap/cds/intellij/CdsSyntaxHighlighterFactory.java rename src/main/java/com/sap/cap/cds/intellij/codestyle/{CdsPreviewFormattingService.java => CdsCodeStylePreviewFormattingService.java} (91%) create mode 100644 src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsService.java delete mode 100644 src/main/java/com/sap/cap/cds/intellij/codestyle/CdsDefaultCodeStyleSettingsService.java delete mode 100644 src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonService.java delete mode 100644 src/main/java/com/sap/cap/cds/intellij/codestyle/CdsProjectCodeStyleSettingsService.java create mode 100644 src/main/java/com/sap/cap/cds/intellij/lifecycle/IdeLifecycleListener.java create mode 100644 src/main/java/com/sap/cap/cds/intellij/lifecycle/PluginLifecycleListener.java create mode 100644 src/main/java/com/sap/cap/cds/intellij/lifecycle/ProjectLifecycleListener.kt rename src/main/java/com/sap/cap/cds/intellij/textmate/{CdsTextMateBundleManager.java => CdsTextMateBundleService.java} (54%) create mode 100644 src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceProjectSettingsTest.java create mode 100644 src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceTestBase.java diff --git a/.gitignore b/.gitignore index 698439c..cc9b7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle .idea .intellijPlatform +.kotlin .qodana local.properties diff --git a/build.gradle b/build.gradle index 22e3c75..90a85a7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,7 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType plugins { id 'java' id 'org.jetbrains.intellij.platform' version '2.0.1' + id 'org.jetbrains.kotlin.jvm' version '2.0.21' } def hasLocalProperties = provider { @@ -30,6 +31,7 @@ repositories { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation 'org.jetbrains:annotations:26.0.1' implementation 'org.json:json:20240303' implementation 'org.apache.maven:maven-artifact:3.9.9' @@ -108,6 +110,11 @@ tasks { test { dependsOn 'copyLspToInstrumented' + exclude '**/*TestBase.class' + } + + jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } publishPlugin { diff --git a/src/main/java/com/sap/cap/cds/intellij/CdsSyntaxHighlighterFactory.java b/src/main/java/com/sap/cap/cds/intellij/CdsSyntaxHighlighterFactory.java deleted file mode 100644 index 54b1598..0000000 --- a/src/main/java/com/sap/cap/cds/intellij/CdsSyntaxHighlighterFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.sap.cap.cds.intellij; - -import com.intellij.openapi.fileTypes.SyntaxHighlighter; -import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.testFramework.LightVirtualFile; -import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettings; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.plugins.textmate.language.syntax.highlighting.TextMateSyntaxHighlighterFactory; - -// TODO remove? currently useless - -public class CdsSyntaxHighlighterFactory extends SyntaxHighlighterFactory { - @Override - public @NotNull SyntaxHighlighter getSyntaxHighlighter(@Nullable Project project, @Nullable VirtualFile virtualFile) { - return new TextMateSyntaxHighlighterFactory().getSyntaxHighlighter(project, new LightVirtualFile("/sample.cds", CdsFileType.INSTANCE, CdsCodeStyleSettings.SAMPLE_SRC)); - } -} diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanel.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanel.java index a888ff2..e9f2bf7 100644 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanel.java +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleCheckboxesPanel.java @@ -78,7 +78,7 @@ public void apply(@NotNull CodeStyleSettings settings) { super.apply(settings); // Applies settings from UI to the settings object CdsCodeStyleSettings cdsSettings = settings.getCustomSettings(CdsCodeStyleSettings.class); setOptionsEnablement(cdsSettings.getChildOptionsEnablement(category)); - CdsPreviewFormattingService.acceptSettings(cdsSettings); + CdsCodeStylePreviewFormattingService.acceptSettings(cdsSettings); } @Override diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanel.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanel.java index 4e041ed..575e275 100644 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanel.java +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleMainPanel.java @@ -7,8 +7,6 @@ import com.sap.cap.cds.intellij.lang.CdsLanguage; import org.jetbrains.annotations.NotNull; -import static com.intellij.openapi.application.ApplicationManager.getApplication; - public class CdsCodeStyleMainPanel extends TabbedLanguageCodeStylePanel { protected CdsCodeStyleMainPanel(CodeStyleSettings currentSettings, @NotNull CodeStyleSettings settings) { @@ -16,9 +14,8 @@ protected CdsCodeStyleMainPanel(CodeStyleSettings currentSettings, @NotNull Code // Enable saving of code-style settings for (Project project : ProjectManager.getInstance().getOpenProjects()) { - project.getService(CdsProjectCodeStyleSettingsService.class); + project.getService(CdsCodeStyleSettingsService.class); } - getApplication().getService(CdsDefaultCodeStyleSettingsService.class); } @Override diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPreviewFormattingService.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePreviewFormattingService.java similarity index 91% rename from src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPreviewFormattingService.java rename to src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePreviewFormattingService.java index 2f9b28a..701b3ee 100644 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPreviewFormattingService.java +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStylePreviewFormattingService.java @@ -16,13 +16,13 @@ import java.util.Set; import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsProvider.SAMPLE_FILE_NAME; -import static com.sap.cap.cds.intellij.codestyle.CdsPrettierJsonService.PRETTIER_JSON; +import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; import static com.sap.cap.cds.intellij.lsp.CdsLspServerDescriptor.getFormattingCommandLine; import static com.sap.cap.cds.intellij.util.FileUtil.createTempDir; import static java.nio.file.Files.readString; import static java.nio.file.Files.write; -public class CdsPreviewFormattingService implements FormattingService { +public class CdsCodeStylePreviewFormattingService implements FormattingService { private static final Map formattedByPrettierJson = new HashMap<>(); private static Path prettierJsonPath; @@ -30,9 +30,9 @@ public class CdsPreviewFormattingService implements FormattingService { private static Path samplePath; private static String prettierJson = "{}"; - CdsPreviewFormattingService() { + CdsCodeStylePreviewFormattingService() { try { - tempDir = createTempDir(CdsPreviewFormattingService.class.getName() + "_"); + tempDir = createTempDir(CdsCodeStylePreviewFormattingService.class.getName() + "_"); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsService.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsService.java new file mode 100644 index 0000000..3617fd0 --- /dev/null +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsService.java @@ -0,0 +1,151 @@ +package com.sap.cap.cds.intellij.codestyle; + +import com.intellij.application.options.CodeStyle; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.codeStyle.CodeStyleSettingsChangeEvent; +import com.intellij.psi.codeStyle.CodeStyleSettingsListener; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static com.intellij.openapi.project.ProjectUtil.guessProjectDir; +import static com.sap.cap.cds.intellij.util.Logger.logger; +import static java.nio.file.Files.readString; + +@Service(Service.Level.PROJECT) +public final class CdsCodeStyleSettingsService { + + public static final String PRETTIER_JSON = ".cdsprettier.json"; + private final Project project; + private final com.intellij.openapi.diagnostic.Logger logger; + private final CdsPrettierJsonManager prettierJsonManager; + + public CdsCodeStyleSettingsService(Project project) { + this.project = project; + this.logger = logger(project).CODE_STYLE(); + prettierJsonManager = new CdsPrettierJsonManager(); + CodeStyleSettingsManager.getInstance(project).subscribe(new CodeStyleSettingsListener() { + @Override + public void codeStyleSettingsChanged(@NotNull CodeStyleSettingsChangeEvent event) { + updateSettingsFile(); + } + }); + } + + public boolean isSettingsFilePresent() { + return prettierJsonManager.isJsonFilePresent(); + } + + public boolean isSettingsReallyChanged() { + return prettierJsonManager.isSettingsReallyChanged(); + } + + public void updateSettingsFile() { + prettierJsonManager.saveSettingsToFile(getSettings()); + } + + public void updateProjectSettingsFromFile() { + if (CodeStyle.usesOwnSettings(project)) { + if (isSettingsFilePresent()) { + prettierJsonManager.loadSettingsFromFile(getSettings()); + } else { + prettierJsonManager.reset(); + CodeStyle.setMainProjectSettings(project, CodeStyleSettingsManager.getInstance(project).createSettings()); + } + } + } + + private CdsCodeStyleSettings getSettings() { + return CodeStyle.getSettings(project).getCustomSettings(CdsCodeStyleSettings.class); + } + + private final class CdsPrettierJsonManager { + + static final int JSON_INDENT = 2; + + File jsonFile; + String jsonWritten; + + CdsPrettierJsonManager() { + // assuming no changes to project directory + VirtualFile guessed = guessProjectDir(project); + String projectDir = guessed != null + ? guessed.getPath() + : project.getBasePath(); + if (projectDir != null) { + jsonFile = getJsonFile(projectDir); + } + reset(); + } + + boolean isJsonFilePresent() { + return jsonFile != null && jsonFile.exists(); + } + + void loadSettingsFromFile(@NotNull CdsCodeStyleSettings settings) { + String json = readJson(); + if (json != null) { + try { + settings.loadFrom(new JSONObject(json)); + } catch (JSONException e) { + logger.error("Failed to parse JSON '%s'".formatted(json), e); + } + } + } + + private String readJson() { + if (!isJsonFilePresent()) { + return null; + } + try { + return readString(jsonFile.toPath()); + } catch (IOException e) { + logger.error("Failed to read [%s]".formatted(jsonFile), e); + } + return null; + } + + void saveSettingsToFile(@NotNull CdsCodeStyleSettings settings) { + if (jsonFile == null) { + return; + } + String json = settings.getNonDefaultSettings().toString(JSON_INDENT); + if (json.equals(jsonWritten)) { + return; + } + if (!jsonFile.getParentFile().exists()) { + logger.debug("Directory [%s] does not exist".formatted(jsonFile.getParentFile())); + return; + } + try (FileWriter writer = new FileWriter(jsonFile)) { + writer.write(json); + jsonWritten = json; + } catch (IOException e) { + logger.error("Failed to write [%s]".formatted(jsonFile), e); + } + } + + void reset() { + jsonWritten = null; + } + + @NotNull File getJsonFile(String projectDir) { + File file = new File(projectDir, PRETTIER_JSON); + if (!file.exists()) { + logger.debug("Optional file [%s] does not exist".formatted(file)); + } + return file; + } + + public boolean isSettingsReallyChanged() { + return jsonWritten == null || !jsonWritten.equals(readJson()); + } + } +} diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanel.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanel.java index 5988612..da43349 100644 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanel.java +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleTabularPanel.java @@ -64,6 +64,6 @@ public void apply(@NotNull CodeStyleSettings settings) throws ConfigurationExcep super.apply(settings); CdsCodeStyleSettings cdsSettings = settings.getCustomSettings(CdsCodeStyleSettings.class); setOptionsEnablement(cdsSettings.getChildOptionsEnablement(category)); - CdsPreviewFormattingService.acceptSettings(cdsSettings); + CdsCodeStylePreviewFormattingService.acceptSettings(cdsSettings); } } \ No newline at end of file diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsDefaultCodeStyleSettingsService.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsDefaultCodeStyleSettingsService.java deleted file mode 100644 index d2770d6..0000000 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsDefaultCodeStyleSettingsService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.sap.cap.cds.intellij.codestyle; - -import com.intellij.openapi.components.Service; -import com.intellij.psi.codeStyle.*; -import org.jetbrains.annotations.NotNull; - -@Service -public final class CdsDefaultCodeStyleSettingsService { - - // TODO apply default settings to new projects - - public CdsDefaultCodeStyleSettingsService() { - CodeStyleSettingsManager codeStyleManager = CodeStyleSettingsManager.getInstance(); - codeStyleManager.subscribe(new CodeStyleSettingsListener() { - @Override - public void codeStyleSettingsChanged(@NotNull CodeStyleSettingsChangeEvent event) { - // TODO implement - } - }); - } -} diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonListener.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonListener.java index da483d1..503e9bb 100644 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonListener.java +++ b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonListener.java @@ -2,31 +2,52 @@ import com.intellij.openapi.project.ProjectLocator; import com.intellij.openapi.vfs.AsyncFileListener; -import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent; +import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent; import com.intellij.openapi.vfs.newvfs.events.VFileEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import static com.intellij.openapi.application.ApplicationManager.getApplication; -import static com.sap.cap.cds.intellij.codestyle.CdsPrettierJsonService.PRETTIER_JSON; +import static com.intellij.openapi.vfs.VfsUtil.collectChildrenRecursively; +import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; public class CdsPrettierJsonListener implements AsyncFileListener { - @Override - public @Nullable ChangeApplier prepareChange(@NotNull List list) { - list.stream() - // NOTE this is also triggered by programmatic changes to the file - .filter(event -> event instanceof VFileContentChangeEvent && ((VFileContentChangeEvent) event).getFile().getName().equals(PRETTIER_JSON)) - .map(event -> getApplication().getService(ProjectLocator.class).guessProjectForFile(event.getFile())) + private static void handle(Stream stream) { + ProjectLocator projectLocator = getApplication().getService(ProjectLocator.class); + stream + .flatMap(event -> event instanceof VFileCreateEvent e && e.getFile() != null && e.getFile().isDirectory() + ? collectChildrenRecursively(e.getFile()).stream() + : Stream.of(event.getFile())) + .filter(Objects::nonNull) + .filter(file -> file.getName().equals(PRETTIER_JSON)) + .map(projectLocator::guessProjectForFile) .filter(Objects::nonNull) .distinct() .forEach(project -> { - getApplication().invokeLater(() -> { - project.getService(CdsProjectCodeStyleSettingsService.class).updateSettingsFromFile(); - }); + CdsCodeStyleSettingsService service = project.getService(CdsCodeStyleSettingsService.class); + if (service.isSettingsReallyChanged()) { + service.updateProjectSettingsFromFile(); + } }); - return null; + } + + @Override + public @Nullable ChangeApplier prepareChange(@NotNull List list) { + return new ChangeApplier() { + @Override + public void beforeVfsChange() { + handle(list.stream().filter(event -> event instanceof VFileDeleteEvent)); + } + + @Override + public void afterVfsChange() { + handle(list.stream().filter(event -> !(event instanceof VFileDeleteEvent))); + } + }; } } diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonService.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonService.java deleted file mode 100644 index 291c323..0000000 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsPrettierJsonService.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.sap.cap.cds.intellij.codestyle; - -import com.intellij.openapi.components.Service; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.vfs.VirtualFile; -import com.sap.cap.cds.intellij.util.Logger; -import org.jetbrains.annotations.NotNull; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -import static com.intellij.openapi.project.ProjectUtil.guessProjectDir; -import static java.nio.file.Files.readString; - -@Service(Service.Level.PROJECT) -public final class CdsPrettierJsonService { - - public static final String PRETTIER_JSON = ".cdsprettier.json"; - private static final int JSON_INDENT = 2; - - private File jsonFile; - private String jsonWritten; - - public CdsPrettierJsonService(Project project) { - // assuming no changes to project directory - VirtualFile projectDir = guessProjectDir(project); - if (projectDir == null) { - // TODO handle IDE settings - } else { - jsonFile = getJsonFile(projectDir.getPath()); - } - } - - public void loadSettingsFromFile(@NotNull CdsCodeStyleSettings settings) { - if (!jsonFile.exists()) { - return; - } - try { - JSONObject json = new JSONObject(readString(jsonFile.toPath())); - settings.loadFrom(json); - } catch (IOException e) { - Logger.CODE_STYLE.error("Failed to read [%s]".formatted(jsonFile), e); - } - } - - public void saveSettingsToFile(@NotNull CdsCodeStyleSettings settings) { - String json = settings.getNonDefaultSettings().toString(JSON_INDENT); - if (!json.equals(jsonWritten)) { - try (FileWriter writer = new FileWriter(jsonFile)) { - writer.write(json); - jsonWritten = json; - } catch (IOException e) { - Logger.CODE_STYLE.error("Failed to write [%s]".formatted(jsonFile), e); - } - } - } - - private @NotNull File getJsonFile(String projectDir) { - File file = new File(projectDir, PRETTIER_JSON); - if (!file.exists()) { - Logger.CODE_STYLE.debug("Optional file [%s] does not exist".formatted(file)); - } - return file; - } - -} diff --git a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsProjectCodeStyleSettingsService.java b/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsProjectCodeStyleSettingsService.java deleted file mode 100644 index 9fb24ca..0000000 --- a/src/main/java/com/sap/cap/cds/intellij/codestyle/CdsProjectCodeStyleSettingsService.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.sap.cap.cds.intellij.codestyle; - -import com.intellij.application.options.CodeStyle; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.project.Project; -import com.intellij.psi.codeStyle.CodeStyleSettingsChangeEvent; -import com.intellij.psi.codeStyle.CodeStyleSettingsListener; -import com.intellij.psi.codeStyle.CodeStyleSettingsManager; -import org.jetbrains.annotations.NotNull; - -// TODO test .cdsprettier.json reading/updating on a project level - -@Service(Service.Level.PROJECT) -public final class CdsProjectCodeStyleSettingsService { - - private final Project project; - - public CdsProjectCodeStyleSettingsService(Project project) { - this.project = project; - CdsPrettierJsonService prettierJsonService = project.getService(CdsPrettierJsonService.class); - CodeStyleSettingsManager manager = CodeStyleSettingsManager.getInstance(project); - manager.subscribe(new CodeStyleSettingsListener() { - @Override - public void codeStyleSettingsChanged(@NotNull CodeStyleSettingsChangeEvent event) { - prettierJsonService.saveSettingsToFile(getSettings()); - } - }); - } - - public CdsCodeStyleSettings getSettings() { - return CodeStyle.getSettings(project).getCustomSettings(CdsCodeStyleSettings.class); - } - - public void updateSettingsFromFile() { - CdsPrettierJsonService prettierJsonService = project.getService(CdsPrettierJsonService.class); - prettierJsonService.loadSettingsFromFile(getSettings()); - } -} diff --git a/src/main/java/com/sap/cap/cds/intellij/lifecycle/IdeLifecycleListener.java b/src/main/java/com/sap/cap/cds/intellij/lifecycle/IdeLifecycleListener.java new file mode 100644 index 0000000..d3d5984 --- /dev/null +++ b/src/main/java/com/sap/cap/cds/intellij/lifecycle/IdeLifecycleListener.java @@ -0,0 +1,16 @@ +package com.sap.cap.cds.intellij.lifecycle; + +import com.intellij.ide.AppLifecycleListener; +import com.sap.cap.cds.intellij.textmate.CdsTextMateBundleService; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static com.intellij.openapi.application.ApplicationManager.getApplication; + +public class IdeLifecycleListener implements AppLifecycleListener { + @Override + public void appFrameCreated(@NotNull List commandLineArgs) { + getApplication().getService(CdsTextMateBundleService.class).registerBundle(); + } +} diff --git a/src/main/java/com/sap/cap/cds/intellij/lifecycle/PluginLifecycleListener.java b/src/main/java/com/sap/cap/cds/intellij/lifecycle/PluginLifecycleListener.java new file mode 100644 index 0000000..7ea3f11 --- /dev/null +++ b/src/main/java/com/sap/cap/cds/intellij/lifecycle/PluginLifecycleListener.java @@ -0,0 +1,28 @@ +package com.sap.cap.cds.intellij.lifecycle; + +import com.intellij.ide.plugins.DynamicPluginListener; +import com.intellij.ide.plugins.IdeaPluginDescriptor; +import com.sap.cap.cds.intellij.CdsPlugin; +import com.sap.cap.cds.intellij.textmate.CdsTextMateBundleService; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.openapi.application.ApplicationManager.getApplication; + +public class PluginLifecycleListener implements DynamicPluginListener { + + @Override + public void pluginLoaded(@NotNull IdeaPluginDescriptor pluginDescriptor) { + getApplication().getService(CdsTextMateBundleService.class).registerBundle(); + } + + // TODO also on plugin disablement + @Override + public void beforePluginUnload(@NotNull IdeaPluginDescriptor pluginDescriptor, boolean isUpdate) { + if (!CdsPlugin.PACKAGE.equals(pluginDescriptor.getPluginId().toString())) { + return; + } + + getApplication().getService(CdsTextMateBundleService.class).unregisterBundle(); + } + +} diff --git a/src/main/java/com/sap/cap/cds/intellij/lifecycle/ProjectLifecycleListener.kt b/src/main/java/com/sap/cap/cds/intellij/lifecycle/ProjectLifecycleListener.kt new file mode 100644 index 0000000..5a6d824 --- /dev/null +++ b/src/main/java/com/sap/cap/cds/intellij/lifecycle/ProjectLifecycleListener.kt @@ -0,0 +1,16 @@ +package com.sap.cap.cds.intellij.lifecycle + +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService + +class ProjectLifecycleListener : ProjectActivity { + override suspend fun execute(project: Project) { + val service = project.getService(CdsCodeStyleSettingsService::class.java) + if (service.isSettingsFilePresent) { + service.updateProjectSettingsFromFile() + } else { + service.updateSettingsFile() + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleManager.java b/src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleService.java similarity index 54% rename from src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleManager.java rename to src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleService.java index 86da5d4..db8e5b4 100644 --- a/src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleManager.java +++ b/src/main/java/com/sap/cap/cds/intellij/textmate/CdsTextMateBundleService.java @@ -1,33 +1,23 @@ package com.sap.cap.cds.intellij.textmate; -import com.intellij.ide.AppLifecycleListener; -import com.intellij.ide.plugins.DynamicPluginListener; -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.startup.ProjectActivity; -import com.intellij.openapi.startup.StartupActivity; -import com.intellij.util.PathUtil; -import com.sap.cap.cds.intellij.CdsPlugin; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.components.Service.Level; import com.sap.cap.cds.intellij.lang.CdsLanguage; import com.sap.cap.cds.intellij.util.Logger; -import kotlin.Unit; -import kotlin.coroutines.Continuation; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.textmate.TextMateServiceImpl; import org.jetbrains.plugins.textmate.configuration.TextMateUserBundlesSettings; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; +import static com.intellij.util.PathUtil.getJarPathForClass; import static com.sap.cap.cds.intellij.util.StackUtil.getCaller; import static com.sap.cap.cds.intellij.util.StackUtil.getMethod; -public class CdsTextMateBundleManager implements AppLifecycleListener, /*obsolete*/ StartupActivity, /* replacement*/ ProjectActivity, DynamicPluginListener { - - public static final CdsTextMateBundleManager INSTANCE = new CdsTextMateBundleManager(); +@Service(Level.APP) +public final class CdsTextMateBundleService { private volatile Boolean isBundleRegistered = false; @@ -40,12 +30,12 @@ private static TextMateUserBundlesSettings getBundlesSettings() { private static void logBundlePresent(TextMateUserBundlesSettings settings) { boolean isBundlePresent = settings.getBundles().values().stream().anyMatch(value -> CdsLanguage.LABEL.equals(value.getName())); - Logger.PLUGIN.info("TextMate bundle present: " + isBundlePresent); + Logger.TM_BUNDLE.info("TextMate bundle present before action: " + isBundlePresent); } @NotNull private static String getBundlePath() { - Path thisPath = Paths.get(PathUtil.getJarPathForClass(CdsTextMateBundleManager.class)); + Path thisPath = Paths.get(getJarPathForClass(CdsTextMateBundleService.class)); return thisPath .getParent() .resolve(CdsTextMateBundle.RELATIVE_PATH) @@ -67,40 +57,10 @@ public void registerBundle() { TextMateServiceImpl.getInstance().reloadEnabledBundles(); } - private void unregisterBundle() { + public void unregisterBundle() { TextMateUserBundlesSettings settings = getBundlesSettings(); logBundlePresent(settings); settings.disableBundle(getBundlePath()); TextMateServiceImpl.getInstance().reloadEnabledBundles(); } - - /** - * Overridden listener methods - */ - - @Override - public void appFrameCreated(@NotNull List commandLineArgs) { - registerBundle(); - } - - @Override - public void runActivity(@NotNull Project project) { - registerBundle(); - } - - @Nullable - @Override - public Object execute(@NotNull Project project, @NotNull Continuation continuation) { - registerBundle(); - return null; - } - - @Override - public void beforePluginUnload(@NotNull IdeaPluginDescriptor pluginDescriptor, boolean isUpdate) { - if (isUpdate || !CdsPlugin.PACKAGE.equals(pluginDescriptor.getPluginId().toString())) { - return; - } - - unregisterBundle(); - } } diff --git a/src/main/java/com/sap/cap/cds/intellij/util/Logger.java b/src/main/java/com/sap/cap/cds/intellij/util/Logger.java index ff1e5bc..24b34ad 100644 --- a/src/main/java/com/sap/cap/cds/intellij/util/Logger.java +++ b/src/main/java/com/sap/cap/cds/intellij/util/Logger.java @@ -1,8 +1,10 @@ package com.sap.cap.cds.intellij.util; +import com.intellij.openapi.project.Project; import com.sap.cap.cds.intellij.CdsPlugin; import com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsProvider; import com.sap.cap.cds.intellij.textmate.CdsTextMateBundle; +import org.jetbrains.annotations.NotNull; import static com.intellij.openapi.diagnostic.Logger.getInstance; @@ -14,4 +16,28 @@ public class Logger { public static final com.intellij.openapi.diagnostic.Logger CODE_STYLE = getInstance("%s/%s".formatted(CdsPlugin.LABEL, CdsCodeStyleSettingsProvider.LABEL)); + private String project; + + private Logger() {} + + Logger(Project project) { + this.project = " [%s]".formatted(project.getName()); + } + + public static Logger logger(Project project) { + return new Logger(project); + } + + public com.intellij.openapi.diagnostic.@NotNull Logger PLUGIN() { + return getInstance("%s%s".formatted(CdsPlugin.LABEL, project)); + } + + public com.intellij.openapi.diagnostic.@NotNull Logger TM_BUNDLE() { + return getInstance("%s/%s%s".formatted(CdsPlugin.LABEL, CdsTextMateBundle.LABEL, project)); + } + + public com.intellij.openapi.diagnostic.@NotNull Logger CODE_STYLE() { + return getInstance("%s/%s%s".formatted(CdsPlugin.LABEL, CdsCodeStyleSettingsProvider.LABEL, project)); + } + } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d276f3a..2ec897e 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -20,8 +20,6 @@ The set of features will grow with future releases, aligned with the development NodeJS - - - - - + + + + - - - - Added
    -
  • CDS Formatting Options can now be configured under Settings > Editor > Code Style > CDS. Options depending on a switched-off option are disabled.
  • +
  • CDS Formatting Options can now be configured under Settings > Editor > Code Style > CDS. +
      +
    • A .cdsprettier.json file in the project root is kept in sync with the options for consumption by the cds-lsp server. +
    • Options depending on a switched-off option are disabled. +
    +

Changed

  • include @sap/cds-lsp 8.5.1 with the following changes since 8.4.3:
      -
    • enabling `newParser` via `cds.env` will no longer interfere with language server -
    • improved telemetry messages -
    • With `@sap/cds 8.5.0` one of the default locations for translation folders (`assets/i18n`) was removed +
    • enabling `newParser` via `cds.env` will no longer interfere with language server +
    • improved telemetry messages +
    • With `@sap/cds 8.5.0` one of the default locations for translation folders (`assets/i18n`) was removed
]]>
diff --git a/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceProjectSettingsTest.java b/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceProjectSettingsTest.java new file mode 100644 index 0000000..b21e703 --- /dev/null +++ b/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceProjectSettingsTest.java @@ -0,0 +1,58 @@ +package com.sap.cap.cds.intellij.codestyle; + +import org.json.JSONException; + +import java.io.IOException; + +import static com.intellij.testFramework.LoggedErrorProcessor.executeAndReturnLoggedError; + +public class CdsCodeStyleSettingsServiceProjectSettingsTest extends CdsCodeStyleSettingsServiceTestBase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + setPerProjectSettings(true); + } + + // TODO test other direction + + // Direction .cdsprettier.json → settings + + public void testPrettierJsonLifecycle() throws IOException, InterruptedException { + loadProject(); + assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); + + createPrettierJson(); + + writePrettierJson("{}"); + assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); + + writePrettierJson("{ tabSize: 42 }"); + assertEquals(42, getCdsCodeStyleSettings().tabSize); + + deletePrettierJson(); + assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); + } + + public void testExistentPrettierJson() throws Exception { + createPrettierJson(); + writePrettierJson("{ tabSize: 42 }"); + loadProject(); + assertEquals(42, getCdsCodeStyleSettings().tabSize); + } + + public void testInvalidPrettierJson() { + Throwable exception = executeAndReturnLoggedError(() -> { + createPrettierJson(); + try { + writePrettierJson("invalid JSON"); + } catch (IOException e) { + throw new RuntimeException(e); + } + loadProject(); + }); + assertInstanceOf(exception, JSONException.class); + assertEquals(defaults.tabSize, getCdsCodeStyleSettings().tabSize); + } + +} diff --git a/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceTestBase.java b/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceTestBase.java new file mode 100644 index 0000000..5607edb --- /dev/null +++ b/src/test/java/com/sap/cap/cds/intellij/codestyle/CdsCodeStyleSettingsServiceTestBase.java @@ -0,0 +1,97 @@ +package com.sap.cap.cds.intellij.codestyle; + +import com.intellij.application.options.CodeStyle; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootModificationUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.testFramework.HeavyPlatformTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import static com.intellij.application.options.CodeStyle.createTestSettings; +import static com.sap.cap.cds.intellij.codestyle.CdsCodeStyleSettingsService.PRETTIER_JSON; +import static java.util.Objects.requireNonNull; + +@SuppressWarnings("NewClassNamingConvention") +public class CdsCodeStyleSettingsServiceTestBase extends HeavyPlatformTestCase { + protected CdsCodeStyleSettings defaults; + protected Project project; + private File prettierJson; + private Path projectDir; + private VirtualFile projectDirVFile; + + protected static void setPerProjectSettings(boolean perProjectSettings) { + CodeStyleSettingsManager.getInstance().USE_PER_PROJECT_SETTINGS = perProjectSettings; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + defaults = new CdsCodeStyleSettings(createTestSettings()); + File tempDirectory = getTempDirectory(); + projectDir = tempDirectory.toPath(); + projectDirVFile = getVFile(tempDirectory); + prettierJson = projectDir.resolve(PRETTIER_JSON).toFile(); + } + + private @NotNull File getTempDirectory() throws IOException { + File tempDirectory = createTempDirectory(); + refreshVfsFor(tempDirectory); + return tempDirectory; + } + + private @NotNull VirtualFile getVFile(File file) { + return requireNonNull(LocalFileSystem.getInstance().findFileByIoFile(file)); + } + + protected void createPrettierJson() { + try { + WriteAction.computeAndWait(() -> projectDirVFile.findOrCreateChildData(this, PRETTIER_JSON)); + } catch (IOException e) { + throw new RuntimeException(e); + } + refreshVfsFor(prettierJson); + } + + private void refreshVfsFor(File file) { + LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); + } + + protected void writePrettierJson(String settings) throws IOException { + if (!isPrettierJsonPresent()) { + throw new IOException("Invalid test setup: create .cdsprettier.json first"); + } + WriteAction.runAndWait(() -> getVFile(prettierJson).setBinaryContent(settings.getBytes())); + } + + private boolean isPrettierJsonPresent() { + return prettierJson.exists(); + } + + protected void deletePrettierJson() throws IOException { + WriteAction.runAndWait(() -> getVFile(prettierJson).delete(this)); + refreshVfsFor(prettierJson); + } + + protected void loadProject() { + project = PlatformTestUtil.loadAndOpenProject(projectDir, getTestRootDisposable()); + WriteAction.runAndWait(() -> { + Module module = ModuleManager.getInstance(project).newModule(projectDir.resolve("test.iml").toString(), "ffo"); + ModuleRootModificationUtil.addContentRoot(module, projectDir.toString()); + }); + } + + @NotNull + protected CdsCodeStyleSettings getCdsCodeStyleSettings() { + return CodeStyle.getProjectOrDefaultSettings(project).getCustomSettings(CdsCodeStyleSettings.class); + } +} diff --git a/src/test/java/com/sap/cap/cds/intellij/lsp/CdsLspServerDescriptorTest.java b/src/test/java/com/sap/cap/cds/intellij/lsp/CdsLspServerDescriptorTest.java index 01c3af2..529f1ba 100644 --- a/src/test/java/com/sap/cap/cds/intellij/lsp/CdsLspServerDescriptorTest.java +++ b/src/test/java/com/sap/cap/cds/intellij/lsp/CdsLspServerDescriptorTest.java @@ -11,10 +11,6 @@ import java.io.IOException; import java.util.regex.Pattern; -/* - * TODO try to write model-level functional tests instead of unit tests (cf. https://plugins.jetbrains.com/docs/intellij/testing-plugins.html) - */ - public class CdsLspServerDescriptorTest extends BasePlatformTestCase { public void testCommandLine() { Process process = null;