From 3e5d3c9ae9c1ed973b100a8f835b431da6aad996 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:02:56 +0200 Subject: [PATCH 01/46] Fix inverted isEnableInlineProblem checks Both checks were negated the wrong way, which disabled the affected code paths exactly while the plugin was enabled: - DocumentMarkupModelScanner.scanForProblemsManuallyInTextEditor passed an empty problem list when the plugin was enabled, so every problem of the scanned file was removed instead of drawn. This broke "Show only highest severity per line" (the MarkupModelListener delegates to this method for that option) and the HighlightProblemListener. - HighlightProblemListener.accept returned early while the plugin was enabled, so the HighlightProblemListener never triggered a scan at all. (cherry picked from commit d2ab348a48ae4d6494b745b64989cb1a6f959998) --- .../overengineer/inlineproblems/DocumentMarkupModelScanner.java | 2 +- .../inlineproblems/listeners/HighlightProblemListener.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java index 4289373..1795134 100644 --- a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java @@ -127,7 +127,7 @@ public void scanForProblemsManuallyInTextEditor(TextEditor textEditor) { mergingUpdateQueue.queue(new Update("scan") { @Override public void run() { - List problems = settingsState.isEnableInlineProblem() ? List.of() : getProblemsInEditor(textEditor); + List problems = settingsState.isEnableInlineProblem() ? getProblemsInEditor(textEditor) : List.of(); problemManager.updateFromNewActiveProblemsForProjectAndFile( problems, diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java index 267f3c3..b3dfbb9 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java @@ -23,7 +23,7 @@ public class HighlightProblemListener implements HighlightInfoFilter { @Override public boolean accept(@NotNull HighlightInfo highlightInfo, @Nullable PsiFile file) { - if (settingsState.isEnableInlineProblem()) + if (!settingsState.isEnableInlineProblem()) return true; if (settingsState.getEnabledListener() != Listener.HIGHLIGHT_PROBLEMS_LISTENER) return true; From c05562f74f11bc222f69e63aea628bfe17d6b4e0 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:03:13 +0200 Subject: [PATCH 02/46] Fix problem churn in the manual scan with multiple open projects scanForProblemsManually collected the problems of all projects into a single list but called updateFromNewActiveProblems inside the project loop. Since that method diffs against all active problems, the first iteration removed every problem belonging to the other projects and the next iteration drew them again - on every scan interval. The update is now applied once, after all open projects have been collected. (cherry picked from commit e0e83b4f7461d199f0a0934cc2395ba42ee93b8e) --- .../inlineproblems/DocumentMarkupModelScanner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java index 1795134..d1a2cc8 100644 --- a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java @@ -107,8 +107,9 @@ public void scanForProblemsManually() { problems.addAll(getProblemsInEditor(textEditor)); } } - problemManager.updateFromNewActiveProblems(problems); } + + problemManager.updateFromNewActiveProblems(problems); } } From d2c148f7b7c5f74c5578b380836d70b07c28e32e Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:03:23 +0200 Subject: [PATCH 03/46] Honor the "Enable XML unescaping" setting The XML unescaping branch was guarded by isEnableHtmlStripping instead of isEnableXmlUnescaping, so the separate checkbox added in 0.5.6 had no effect and unescaping always followed the HTML stripping switch. (cherry picked from commit 3f1ea0c14142aad629eb6363fbfb4f3e604e327b) --- .../org/overengineer/inlineproblems/entities/InlineProblem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java b/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java index b780efd..436c048 100644 --- a/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java +++ b/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java @@ -87,7 +87,7 @@ private String getTextWithHtmlStrippingAndXmlUnescaping(String text, SettingsSta } if ( - settingsState.isEnableHtmlStripping() && + settingsState.isEnableXmlUnescaping() && text.contains("&") ) { text = StringUtil.unescapeXmlEntities(text); From fa762451485469f7af51f7310473ded968cfd835 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:03:55 +0200 Subject: [PATCH 04/46] Fix settings dialog state handling - isModified did not compare enableInlineProblemsNotifications, so toggling only that checkbox left the Apply button disabled and the change was discarded when the dialog was closed. - reset did not restore maxFileLines while every other field was restored. - The SettingsComponent constructor passed an Optional to JComboBox.setSelectedItem, which is a no-op for a JComboBox. It now uses the index based setter, which also clamps out of range values. (cherry picked from commit 38d73bfe9197df46b9bc0ed61b95c69f3f65b931) --- .../inlineproblems/settings/SettingsComponent.java | 7 +++++-- .../inlineproblems/settings/SettingsConfigurable.java | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java index dd02243..aeab531 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java @@ -17,7 +17,6 @@ import java.awt.*; import java.text.NumberFormat; import java.util.Arrays; -import java.util.Optional; import java.util.stream.Collectors; import java.util.List; @@ -158,7 +157,7 @@ public SettingsComponent() { problemFilterList.setText(settingsState.getProblemFilterList()); fileExtensionBlacklist.setText(settingsState.getFileExtensionBlacklist()); - enabledListener.setSelectedItem(Optional.of(settingsState.getEnabledListener())); + setEnabledListener(settingsState.getEnabledListener()); Dimension enabledListenerDimension = enabledListener.getPreferredSize(); enabledListenerDimension.width += 100; @@ -626,6 +625,10 @@ public int getEnabledListener() { } public void setEnabledListener(int index) { + if (index < 0 || index >= availableListeners.length) { + index = 0; + } + enabledListener.setSelectedIndex(index); } diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java index 49bf284..639ab0c 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java @@ -44,6 +44,7 @@ public boolean isModified() { boolean oldStateEqualsNewState = state.isForceProblemsInSameLine() == settingsComponent.isForceErrorsInSameLine() && state.isEnableInlineProblem() == settingsComponent.isEnableInlineProblem() && + state.isEnableInlineProblemsNotifications() == settingsComponent.isEnableInlineProblemsNotifications() && state.isDrawBoxesAroundErrorLabels() == settingsComponent.getDrawBoxesAroundProblemLabels() && state.isRoundedCornerBoxes() == settingsComponent.isRoundedCornerBoxes() && state.isUseEditorFont() == settingsComponent.isUseEditorFont() && @@ -235,6 +236,7 @@ public void reset() { settingsComponent.setManualScannerDelay(state.getManualScannerDelay()); settingsComponent.setProblemFilterList(state.getProblemFilterList()); settingsComponent.setFileExtensionBlacklist(state.getFileExtensionBlacklist()); + settingsComponent.setMaxFileLines(state.getMaxFileLines()); settingsComponent.setAdditionalInfoSeverities(state.getAdditionalInfoSeveritiesAsString()); settingsComponent.setAdditionalWarningSeverities(state.getAdditionalWarningSeveritiesAsString()); From 788c2a0b6c82e96fcacbe35a0e508cf22add6314 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:04:27 +0200 Subject: [PATCH 05/46] Use the platform ActionManager for the intention popup InlineProblemLabel imported org.jdesktop.swingx.action.ActionManager, so the lookup for ACTION_SHOW_INTENTION_ACTIONS queried the SwingX action registry, which is always empty. The click handler therefore only ever worked through its null fallback, and had the registry ever returned an action the cast to AnAction would have thrown a ClassCastException. The inlay field is also only assigned in paint, so mouseClicked now guards against it being null. (cherry picked from commit bb02730c8cba9d839fd1f62512118e984e050153) --- .../inlineproblems/InlineProblemLabel.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/InlineProblemLabel.java b/src/main/java/org/overengineer/inlineproblems/InlineProblemLabel.java index accaa18..50e220c 100644 --- a/src/main/java/org/overengineer/inlineproblems/InlineProblemLabel.java +++ b/src/main/java/org/overengineer/inlineproblems/InlineProblemLabel.java @@ -4,6 +4,7 @@ import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler; import com.intellij.ide.ui.AntialiasingType; import com.intellij.ide.ui.UISettings; +import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.actionSystem.ex.ActionUtil; @@ -20,7 +21,6 @@ import com.intellij.ui.paint.EffectPainter; import lombok.Getter; import lombok.Setter; -import org.jdesktop.swingx.action.ActionManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.overengineer.inlineproblems.entities.InlineProblem; @@ -196,23 +196,29 @@ public void mouseClicked(@NotNull MouseEvent mouseEvent, @NotNull Point point) { if (!clickableContext) { return; } + Inlay currentInlay = inlay; + if (currentInlay == null) { + return; + } + if (mouseEvent.getButton() == MouseEvent.BUTTON1) { mouseEvent.consume(); - var editor = inlay.getEditor(); + Editor editor = currentInlay.getEditor(); editor.getCaretModel().moveToOffset(actualStartOffset); editor.getScrollingModel().scrollToCaret(ScrollType.CENTER); - var action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS); - if (action == null) { - Project project = editor.getProject(); - if (project == null) return; - PsiFile psiFileInEditor = PsiUtilBase.getPsiFileInEditor(editor, project); - if (psiFileInEditor == null) return; - new ShowIntentionActionsHandler().invoke(project, editor, psiFileInEditor, false); - } else { - ActionUtil.invokeAction((AnAction) action, editor.getComponent(), "EditorInlay", null, null); + AnAction action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS); + if (action != null) { + ActionUtil.invokeAction(action, editor.getComponent(), "EditorInlay", null, null); + return; } + + Project project = editor.getProject(); + if (project == null) return; + PsiFile psiFileInEditor = PsiUtilBase.getPsiFileInEditor(editor, project); + if (psiFileInEditor == null) return; + new ShowIntentionActionsHandler().invoke(project, editor, psiFileInEditor, false); } } From 45ca69e2651925825757dca711e49a5684a0d7c4 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:04:42 +0200 Subject: [PATCH 06/46] Make the color serialization independent of the alpha byte This is hardening, not a fix for an observable bug: the settings use plain ColorPanel instances and never call setSupportTransparency(true), so the color picker has no opacity slider and every color that reaches the converter is opaque. For an opaque color the old implementation was always correct. return "#" + Integer.toHexString(value.getRGB()).substring(2); getRGB() is 0xAARRGGBB, and substring(2) relies on the alpha byte producing exactly the two leading characters. That only holds for an alpha of 0x10 or higher; below that toHexString drops the leading zeros and substring(2) cuts into the red component, or throws StringIndexOutOfBoundsException for a near black color with a zero alpha. Delegating to the platforms ColorUtil.toHex, which zero pads every component, removes that dependency. ColorPanel itself uses the same helper to render its hex label. Verified against the old implementation over all 16777216 opaque colors: identical output and both round trip through Color.decode. They only differ for an alpha below 0x10, where the old one returned a truncated and therefore wrong color. (cherry picked from commit 462125d704379be0eb43287593c94d9f2613ffd8) --- .../overengineer/inlineproblems/utils/ColorConverter.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/overengineer/inlineproblems/utils/ColorConverter.java b/src/main/java/org/overengineer/inlineproblems/utils/ColorConverter.java index be2615d..32e0569 100644 --- a/src/main/java/org/overengineer/inlineproblems/utils/ColorConverter.java +++ b/src/main/java/org/overengineer/inlineproblems/utils/ColorConverter.java @@ -1,5 +1,6 @@ package org.overengineer.inlineproblems.utils; +import com.intellij.ui.ColorUtil; import com.intellij.util.xmlb.Converter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,6 +24,10 @@ public Color fromString(@NotNull String value) { @Override public @Nullable String toString(@NotNull Color value) { - return "#" + Integer.toHexString(value.getRGB()).substring(2); + /* ColorUtil zero pads every component. The previous implementation used + * Integer.toHexString(getRGB()).substring(2), which relies on the alpha byte producing + * the two leading characters. That holds for opaque colors, but for an alpha below 0x10 + * toHexString drops the leading zeros and substring(2) cuts into the red component. */ + return "#" + ColorUtil.toHex(value); } } From 677c48c3d086e467d0cde04c0840512f54831056 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:05:00 +0200 Subject: [PATCH 07/46] Close the readers in the Unity project detection The BufferedReader was never closed, so every scanned .csproj leaked a file handle, and the early return on a match left the reader of that file open as well. Use try-with-resources, drop the redundant exists() check and log through the platform logger instead of printStackTrace. (cherry picked from commit d57179d72afda1d222f789bd20f6a9f38af4e585) --- .../scanners/UnityProjectScanner.java | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/scanners/UnityProjectScanner.java b/src/main/java/org/overengineer/inlineproblems/scanners/UnityProjectScanner.java index 21c3e24..3c91f7c 100644 --- a/src/main/java/org/overengineer/inlineproblems/scanners/UnityProjectScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/scanners/UnityProjectScanner.java @@ -3,12 +3,18 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; public class UnityProjectScanner { - Logger logger = Logger.getInstance(UnityProjectScanner.class); + private final Logger logger = Logger.getInstance(UnityProjectScanner.class); private final String[] unityReferences = { "UnityEngine", @@ -19,37 +25,47 @@ public class UnityProjectScanner { }; public boolean isUnityProject(Project project) { - if (project.getBasePath() == null) + String basePath = project.getBasePath(); + if (basePath == null) return false; - File dir = new File(project.getBasePath()); - - File[] files = dir.listFiles(); + File[] files = new File(basePath).listFiles(); if (files == null) return false; for (File file : files) { - if (file.isFile() && file.exists() && file.getName().endsWith(".csproj")) { - try { - BufferedReader reader = new BufferedReader(new FileReader(file)); - String line = reader.readLine(); - - while (line != null) { - for (String reference : unityReferences) { - if (line.contains(reference)) { - return true; - } - } - - line = reader.readLine(); - } + if (!file.isFile() || !file.getName().endsWith(".csproj")) { + continue; + } + + if (containsUnityReference(file.toPath())) { + return true; + } + } + + return false; + } - } catch (FileNotFoundException ignored) {} catch (IOException e) { - logger.warn("IOException in unity detection"); - e.printStackTrace(); + private boolean containsUnityReference(Path path) { + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + String line = reader.readLine(); + + while (line != null) { + for (String reference : unityReferences) { + if (line.contains(reference)) { + return true; + } } + + line = reader.readLine(); } } + catch (NoSuchFileException ignored) { + // The file disappeared between listing and reading + } + catch (IOException e) { + logger.warn("Unable to read '" + path.getFileName() + "' during Unity project detection", e); + } return false; } From 88b9640545ff9092fbc16980e22cf1ac84972893 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:05:19 +0200 Subject: [PATCH 08/46] Stop creating an AWT Canvas for every drawn problem label drawProblemLabel measured the line text with new Canvas().getFontMetrics(), which builds a heavyweight AWT component per problem and per scan. It is one of the hotspots in the profiler traces of GitHub issue #96. The editor content component provides the same metrics without the allocation, and it is the component the label is actually painted on. (cherry picked from commit 4f9e88a1e32395745ca7a3b82577755c09ef98f5) --- .../org/overengineer/inlineproblems/InlineDrawer.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java index a504bb2..2d0af9e 100644 --- a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java +++ b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java @@ -10,7 +10,7 @@ import org.overengineer.inlineproblems.utils.SeverityUtil; import java.awt.Font; -import java.awt.Canvas; +import java.awt.FontMetrics; import java.util.List; import java.util.Arrays; @@ -52,8 +52,13 @@ public void drawProblemLabel(InlineProblem problem) { Font editorFont = editor.getColorsScheme().getFont(EditorFontType.PLAIN); + /* The font metrics are taken from the editor content component instead of a throwaway + * java.awt.Canvas: instantiating a heavyweight AWT component for every drawn problem + * showed up as a hotspot while scrolling (GitHub issue #96). */ + FontMetrics editorFontMetrics = editor.getContentComponent().getFontMetrics(editorFont); + int problemWidth = inlineProblemLabel.calcWidthInPixels(editor) + - new Canvas().getFontMetrics(editorFont).stringWidth(lineText) + + editorFontMetrics.stringWidth(lineText) + existingInlineElementsWidth; // We add 50 as offset here because the calculation is somehow not exact From 7f02366767d1badfb2ff55346eea0233a3433b1e Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:07:05 +0200 Subject: [PATCH 09/46] Reference the drawn inlays and highlighters directly Problems identified their drawn elements by storing the hash code of the renderer and of the range highlighter. Removing an element therefore meant walking the whole markup model or every inlay of the document: - undrawErrorLineHighlight iterated markupModel.getAllHighlighters() - undrawInlineProblemLabel asked for all block/after-line-end elements of the entire document - removeGutterIconsForLine iterated getAllHighlighters() again Both traces in GitHub issue #96 hit exactly those loops. Matching by hash code is also ambiguous, which can leave a label or a gutter icon behind or dispose a foreign one - the likely cause of issues #38 and #44. InlineProblem now holds the Inlay and the RangeHighlighter it created, so removal is a single dispose/removeHighlighter call. removeGutterIconsForLine uses processRangeHighlightersOverlappingWith when the markup model supports it, and the dead highlightInfoStartOffset field (which held a hash code despite its name, yet took part in equals) is gone. (cherry picked from commit 884ebd26b021fb67db99ec46ddceb578d957fd08) --- .../inlineproblems/InlineDrawer.java | 96 ++++++++++--------- .../entities/InlineProblem.java | 17 ++-- .../listeners/MarkupModelProblemListener.java | 7 +- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java index 2d0af9e..f8e7926 100644 --- a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java +++ b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java @@ -1,9 +1,10 @@ package org.overengineer.inlineproblems; -import com.intellij.openapi.Disposable; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.openapi.editor.ex.MarkupModelEx; import com.intellij.openapi.editor.markup.*; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.TextRange; import org.overengineer.inlineproblems.entities.InlineProblem; import org.overengineer.inlineproblems.settings.SettingsState; @@ -12,7 +13,6 @@ import java.awt.Font; import java.awt.FontMetrics; import java.util.List; -import java.util.Arrays; public class InlineDrawer { @@ -67,13 +67,13 @@ public void drawProblemLabel(InlineProblem problem) { inlineProblemLabel.setBlockElement(true); problem.setBlockElement(true); - inlayModel.addBlockElement( + problem.setInlay(inlayModel.addBlockElement( editor.getDocument().getLineStartOffset(problem.getLine()), false, true, 1, inlineProblemLabel - ); + )); } else { InlayProperties properties = new InlayProperties() @@ -81,14 +81,12 @@ public void drawProblemLabel(InlineProblem problem) { .disableSoftWrapping(true) .priority(1); - inlayModel.addAfterLineEndElement( + problem.setInlay(inlayModel.addAfterLineEndElement( problem.getActualEndOffset(), properties, inlineProblemLabel - ); + )); } - - problem.setInlineProblemLabelHashCode(inlineProblemLabel.hashCode()); } /** Draws the highlighter and the gutter icon for the currently shown problem in the line @@ -135,7 +133,7 @@ public void drawLineHighlighterAndGutterIcon(List problemsInLine) highlighter.setGutterIconRenderer(new GutterRenderer(getGutterText(problemsInLine), drawDetails.getIcon())); } - problem.setProblemLineHighlighterHashCode(highlighter.hashCode()); + problem.setLineHighlighter(highlighter); } /** @@ -144,11 +142,15 @@ public void drawLineHighlighterAndGutterIcon(List problemsInLine) * it still contains the problem itself */ public void undrawErrorLineHighlight(InlineProblem problem, List problemsInLine) { - MarkupModel markupModel = problem.getTextEditor().getEditor().getMarkupModel(); + RangeHighlighter lineHighlighter = problem.getLineHighlighter(); - Arrays.stream(markupModel.getAllHighlighters()) - .filter(h -> h.isValid() && h.hashCode() == problem.getProblemLineHighlighterHashCode()) - .forEach(markupModel::removeHighlighter); + if (lineHighlighter != null) { + problem.setLineHighlighter(null); + + if (lineHighlighter.isValid()) { + problem.getTextEditor().getEditor().getMarkupModel().removeHighlighter(lineHighlighter); + } + } // Gutter icon re-adding if (problemsInLine != null && problemsInLine.size() > 1) { @@ -158,37 +160,20 @@ public void undrawErrorLineHighlight(InlineProblem problem, List } public void undrawInlineProblemLabel(InlineProblem problem) { - Editor editor = problem.getTextEditor().getEditor(); - Document document = editor.getDocument(); + Inlay inlay = problem.getInlay(); - // Here is not checked if single or multi line, both are disposed because we do not have the info here - // We search for all elements because they can move - int documentLineStartOffset = document.getLineStartOffset(0); - int endLine = document.getLineCount() - 1; - if (endLine < 0) endLine = 0; - int documentLineEndOffset = document.getLineEndOffset(endLine); - - if (problem.isBlockElement()) { - editor.getInlayModel() - .getBlockElementsInRange( - documentLineStartOffset, - documentLineEndOffset - ) - .stream() - .filter(e -> problem.getInlineProblemLabelHashCode() == e.getRenderer().hashCode()) - .filter(e -> e.getRenderer() instanceof InlineProblemLabel) - .forEach(Disposable::dispose); + if (inlay == null) { + return; } - else { - editor.getInlayModel() - .getAfterLineEndElementsInRange( - documentLineStartOffset, - documentLineEndOffset - ) - .stream() - .filter(e -> problem.getInlineProblemLabelHashCode() == e.getRenderer().hashCode()) - .filter(e -> e.getRenderer() instanceof InlineProblemLabel) - .forEach(Disposable::dispose); + + problem.setInlay(null); + + /* The inlay moves with the document, so the reference stays correct. Searching all + * elements of the document for a matching renderer hash code, as it was done before, + * is both slow (GitHub issue #96) and ambiguous on a hash collision, which could leave + * the label behind or dispose a foreign one (GitHub issues #38, #44). */ + if (inlay.isValid()) { + Disposer.dispose(inlay); } } @@ -224,13 +209,32 @@ private void removeGutterIconsForLine(Editor editor, int line) { int lineEndOffset = document.getLineEndOffset(line); MarkupModel markupModel = editor.getMarkupModel(); + + /* Only the highlighters overlapping the line are relevant. Materializing every + * highlighter of the editor was one of the hotspots in GitHub issue #96. */ + if (markupModel instanceof MarkupModelEx) { + ((MarkupModelEx) markupModel).processRangeHighlightersOverlappingWith( + lineStartOffset, + lineEndOffset, + highlighter -> { + removeOwnGutterIcon(highlighter); + return true; + } + ); + + return; + } + for (RangeHighlighter highlighter : markupModel.getAllHighlighters()) { if (highlighter.getStartOffset() <= lineEndOffset && highlighter.getEndOffset() >= lineStartOffset) { - GutterIconRenderer gutterIconRenderer = highlighter.getGutterIconRenderer(); - if (gutterIconRenderer instanceof GutterRenderer) { - highlighter.setGutterIconRenderer(null); - } + removeOwnGutterIcon(highlighter); } } } + + private void removeOwnGutterIcon(RangeHighlighter highlighter) { + if (highlighter.getGutterIconRenderer() instanceof GutterRenderer) { + highlighter.setGutterIconRenderer(null); + } + } } diff --git a/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java b/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java index 436c048..07fcb19 100644 --- a/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java +++ b/src/main/java/org/overengineer/inlineproblems/entities/InlineProblem.java @@ -1,6 +1,7 @@ package org.overengineer.inlineproblems.entities; import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; @@ -13,7 +14,7 @@ @Getter @Setter -@EqualsAndHashCode(exclude = {"line", "problemLineHighlighterHashCode", "inlineProblemLabelHashCode", "isBlockElement", "drawDetails"}) +@EqualsAndHashCode(exclude = {"line", "lineHighlighter", "inlay", "isBlockElement", "drawDetails"}) public class InlineProblem { // The line the problem first appeared @@ -35,16 +36,13 @@ public class InlineProblem { private boolean isBlockElement = false; // Set after drawing the line highlight, used to remove it again - private int problemLineHighlighterHashCode; + private RangeHighlighter lineHighlighter; // Set after drawing the inlay, used to remove the inlay again - private int inlineProblemLabelHashCode = 0; + private Inlay inlay; - // Used to determine if the problem has moved - private int highlightInfoStartOffset; - - // Used to identify the problem, should never change even if problem the problem moved - private int rangeHighlighterHashCode; + // Used to identify the problem, should never change even if the problem moved + private final RangeHighlighter rangeHighlighter; public InlineProblem( @@ -67,8 +65,7 @@ public InlineProblem( this.textEditor = textEditor; this.file = filePath; this.project = textEditor.getEditor().getProject(); - this.highlightInfoStartOffset = highlightInfo.hashCode(); - this.rangeHighlighterHashCode = rangeHighlighter.hashCode(); + this.rangeHighlighter = rangeHighlighter; this.actualStartffset = highlightInfo.getStartOffset(); if (highlightInfo.getActualEndOffset() == 0) diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java index cb9611b..0e3f1f5 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java @@ -6,6 +6,7 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ex.MarkupModelEx; import com.intellij.openapi.editor.ex.RangeHighlighterEx; +import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.editor.impl.DocumentMarkupModel; import com.intellij.openapi.editor.impl.event.MarkupModelListener; import com.intellij.openapi.fileEditor.TextEditor; @@ -151,7 +152,7 @@ private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter ); if (type == EventType.CHANGE || type == EventType.REMOVE) { - problemToRemove = findActiveProblemByRangeHighlighterHashCode(highlighter.hashCode()); + problemToRemove = findActiveProblemByRangeHighlighter(highlighter); if (problemToRemove == null) { return; @@ -189,9 +190,9 @@ private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter } } - private InlineProblem findActiveProblemByRangeHighlighterHashCode(int hashCode) { + private InlineProblem findActiveProblemByRangeHighlighter(RangeHighlighter rangeHighlighter) { return problemManager.getActiveProblems().stream() - .filter(p -> p.getRangeHighlighterHashCode() == hashCode) + .filter(p -> p.getRangeHighlighter() == rangeHighlighter) .findFirst() .orElse(null); } From 24b7686749b4ea2412139d96dfdeb7d8a4f531f1 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:07:50 +0200 Subject: [PATCH 10/46] Cache the parsed problem filter list The filter list was split and lowercased again for every single problem in DocumentMarkupModelScanner and for every highlighter event in MarkupModelProblemListener, which can happen several times per millisecond. The parsed and normalized list now lives in ProblemTextFilter and is only rebuilt when the setting itself changes. This also drops blank filter entries. An empty filter list produced a single empty entry through split(";"), and startsWith("") matches everything, so an empty list would have hidden every problem. (cherry picked from commit e049e34679d60c2e9c8724d5c32b677002cfdba0) --- .../DocumentMarkupModelScanner.java | 14 ++-- .../listeners/MarkupModelProblemListener.java | 12 +-- .../utils/ProblemTextFilter.java | 76 +++++++++++++++++++ 3 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java diff --git a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java index d1a2cc8..8cb5d69 100644 --- a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java @@ -20,6 +20,7 @@ import org.overengineer.inlineproblems.listeners.HighlightProblemListener; import org.overengineer.inlineproblems.settings.SettingsState; import org.overengineer.inlineproblems.utils.FileUtil; +import org.overengineer.inlineproblems.utils.ProblemTextFilter; import java.util.ArrayList; import java.util.Arrays; @@ -155,18 +156,15 @@ private List getProblemsInEditor(TextEditor textEditor) { .forDocument(document, editor.getProject(), false) .getAllHighlighters(); - List problemTextBeginningFilterList = new ArrayList<>( - Arrays.asList(SettingsState.getInstance().getProblemFilterList().split(";")) - ); - Arrays.stream(highlighters) .filter(h -> { if (h.isValid() && h.getErrorStripeTooltip() instanceof HighlightInfo) { HighlightInfo highlightInfo = (HighlightInfo) h.getErrorStripeTooltip(); - return highlightInfo.getDescription() != null && - !highlightInfo.getDescription().isEmpty() && - problemTextBeginningFilterList.stream() - .noneMatch(f -> highlightInfo.getDescription().stripLeading().toLowerCase().startsWith(f.toLowerCase())) && + String description = highlightInfo.getDescription(); + + return description != null && + !description.isEmpty() && + !ProblemTextFilter.isFiltered(description) && fileEndOffset >= highlightInfo.getStartOffset(); } diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java index 0e3f1f5..697bbba 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java @@ -17,9 +17,9 @@ import org.overengineer.inlineproblems.entities.InlineProblem; import org.overengineer.inlineproblems.entities.enums.Listener; import org.overengineer.inlineproblems.settings.SettingsState; +import org.overengineer.inlineproblems.utils.ProblemTextFilter; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -159,15 +159,7 @@ private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter } } - List problemTextBeginningFilterList = new ArrayList<>( - Arrays.asList(SettingsState.getInstance().getProblemFilterList().split(";")) - ); - - if ( - newProblem.getText().isEmpty() || - problemTextBeginningFilterList.stream() - .anyMatch(f -> newProblem.getText().toLowerCase().startsWith(f.toLowerCase())) - ) { + if (newProblem.getText().isEmpty() || ProblemTextFilter.isFiltered(newProblem.getText())) { return; } diff --git a/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java b/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java new file mode 100644 index 0000000..4496db5 --- /dev/null +++ b/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java @@ -0,0 +1,76 @@ +package org.overengineer.inlineproblems.utils; + +import org.overengineer.inlineproblems.settings.SettingsState; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; + + +/** + * Matches problem texts against the user configured filter list. + *

+ * The parsed list is cached and only rebuilt when the raw setting changes, because the filter is + * evaluated for every problem of every scan and, with the MarkupModelListener, for every single + * highlighter event. + */ +public final class ProblemTextFilter { + + private static volatile String cachedRawFilterList = null; + private static volatile List cachedFilters = List.of(); + + private ProblemTextFilter() { + } + + /** + * @param problemText the problem description + * @return true if the problem should be ignored because its text starts with one of the filters + */ + public static boolean isFiltered(String problemText) { + if (problemText == null) { + return true; + } + + List filters = getFilters(); + if (filters.isEmpty()) { + return false; + } + + String normalizedText = problemText.stripLeading().toLowerCase(Locale.ROOT); + + for (String filter : filters) { + if (normalizedText.startsWith(filter)) { + return true; + } + } + + return false; + } + + private static List getFilters() { + String rawFilterList = SettingsState.getInstance().getProblemFilterList(); + + if (!Objects.equals(rawFilterList, cachedRawFilterList)) { + cachedFilters = parse(rawFilterList); + cachedRawFilterList = rawFilterList; + } + + return cachedFilters; + } + + private static List parse(String rawFilterList) { + if (rawFilterList == null || rawFilterList.isBlank()) { + return List.of(); + } + + /* Blank entries have to be dropped: an empty filter would match every problem text + * through startsWith("") and hide all problems. */ + return Arrays.stream(rawFilterList.split(";")) + .map(String::trim) + .filter(f -> !f.isEmpty()) + .map(f -> f.toLowerCase(Locale.ROOT)) + .collect(Collectors.toUnmodifiableList()); + } +} From 9d04c3feb9bec60866481e3acd54a03f443b3fe1 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:08:59 +0200 Subject: [PATCH 11/46] Use hash based lookups in the problem manager - updateFromNewActiveProblems ran List.contains inside two stream filters and kept the already processed hash codes in an ArrayList, so every scan round was quadratic in the number of problems. All three lookups now go through hash sets. - getProblemsInLineForProblem and getProblemsInLineForProblemSorted were identical except for the sort; the sorted variant now delegates. The returned list is explicitly an ArrayList because InlineDrawer removes an element from it while re-adding the gutter icon. - Collections.synchronizedList(activeProblems) created a fresh wrapper on every add and remove, so it synchronized on an object nobody else could see. It provided no thread safety at all and only cost an allocation per operation - removed. - findActiveProblemByRangeHighlighter now also matches the editor of the listener. A document markup highlighter is shared by all editors of that document, so in a split view the lookup could return the problem of the other editor and remove the wrong element. (cherry picked from commit c6bc02411aff82277bd565e6dbf670d7a1754d06) --- .../inlineproblems/ProblemManager.java | 38 +++++++++++-------- .../listeners/MarkupModelProblemListener.java | 6 +++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java index c8b877a..4f226c3 100644 --- a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java +++ b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java @@ -15,6 +15,9 @@ public class ProblemManager implements Disposable { + private static final Comparator HIGHEST_SEVERITY_FIRST = + (p1, p2) -> Integer.compare(p2.getSeverity(), p1.getSeverity()); + @Getter private final List activeProblems = new ArrayList<>(); @@ -37,10 +40,9 @@ public void removeProblem(InlineProblem problem) { inlineDrawer.undrawErrorLineHighlight(problem, problemsInLine); inlineDrawer.undrawInlineProblemLabel(problem); - if (!Collections.synchronizedList(activeProblems).remove(problem)) { + if (!activeProblems.remove(problem)) { logger.warn("Removal of problem failed, resetting"); resetForEditor(problem.getTextEditor().getEditor()); - return; } } @@ -54,10 +56,7 @@ public void addProblem(InlineProblem problem) { List problemsInLine = getProblemsInLineForProblem(problem); problemsInLine.add(problem); - - problemsInLine = problemsInLine.stream() - .sorted((p1, p2) -> Integer.compare(p2.getSeverity(), p1.getSeverity())) - .collect(Collectors.toList()); + problemsInLine.sort(HIGHEST_SEVERITY_FIRST); problemsInLine.forEach(p -> { if (p != problem) @@ -84,7 +83,7 @@ private void addProblemPrivate(InlineProblem problem) { } inlineDrawer.drawProblemLabel(problem); - Collections.synchronizedList(activeProblems).add(problem); + activeProblems.add(problem); } public boolean shouldProblemBeIgnored(int severity) { @@ -161,17 +160,21 @@ public void updateFromNewActiveProblemsForProjectAndFile(List pro updateFromNewActiveProblems(problems, activeProblemsSnapShot); } + /** + * @return a mutable list, the caller is allowed to modify it (InlineDrawer removes the + * problem that is being undrawn from it) + */ private List getProblemsInLineForProblem(InlineProblem problem) { return activeProblems.stream() .filter(p -> Objects.equals(p.getTextEditor(), problem.getTextEditor()) && p.getLine() == problem.getLine()) - .collect(Collectors.toList()); + .collect(Collectors.toCollection(ArrayList::new)); } private List getProblemsInLineForProblemSorted(InlineProblem problem) { - return activeProblems.stream() - .filter(p -> Objects.equals(p.getTextEditor(), problem.getTextEditor()) && p.getLine() == problem.getLine()) - .sorted((p1, p2) -> Integer.compare(p2.getSeverity(), p1.getSeverity())) - .collect(Collectors.toList()); + List problemsInLine = getProblemsInLineForProblem(problem); + problemsInLine.sort(HIGHEST_SEVERITY_FIRST); + + return problemsInLine; } /** @@ -180,7 +183,7 @@ private List getProblemsInLineForProblemSorted(InlineProblem prob * this function needs to be used. */ private void updateFromNewActiveProblems(List newProblems, List activeProblemsSnapShot) { - final List processedProblemHashCodes = new ArrayList<>(); + final Set processedProblemHashCodes = new HashSet<>(); List usedProblems; if (settingsState.isShowOnlyHighestSeverityPerLine()) { @@ -206,15 +209,20 @@ private void updateFromNewActiveProblems(List newProblems, List usedProblemSet = new HashSet<>(usedProblems); + final Set activeProblemSet = new HashSet<>(activeProblemsSnapShot); + activeProblemsSnapShot.stream() - .filter(p -> !usedProblems.contains(p)) + .filter(p -> !usedProblemSet.contains(p)) .forEach(p -> { processedProblemHashCodes.add(p.hashCode()); removeProblem(p); }); usedProblems.stream() - .filter(p -> !activeProblemsSnapShot.contains(p) && !processedProblemHashCodes.contains(p.hashCode())) + .filter(p -> !activeProblemSet.contains(p) && !processedProblemHashCodes.contains(p.hashCode())) .forEach(this::addProblem); } } diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java index 697bbba..ad0360c 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java @@ -182,9 +182,15 @@ private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter } } + /** + * The markup model belongs to the document, so a highlighter is shared between all editors + * of that document (split view). The problem of this listeners own editor is the one to + * look for. + */ private InlineProblem findActiveProblemByRangeHighlighter(RangeHighlighter rangeHighlighter) { return problemManager.getActiveProblems().stream() .filter(p -> p.getRangeHighlighter() == rangeHighlighter) + .filter(p -> Objects.equals(p.getTextEditor(), textEditor)) .findFirst() .orElse(null); } From fcdd0ecbe8872f5d7a59a0dd3fe5f7c39636c908 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:10:51 +0200 Subject: [PATCH 12/46] Clean up when editors and projects are closed - MarkupModelProblemListener kept its disposables in a static list that was only cleared in disposeAll(), so every file that was ever opened left a disposable registered on the ProblemManager for the rest of the session. The disposables are now keyed by TextEditor, are released when the editor is gone (disposeInvalid, called from fileClosed) and setup() no longer installs a second listener on an editor that already has one - which used to duplicate every problem event. - Problems of closed editors were only noticed by the next full scan, and removing them meant undrawing on an already disposed editor. ProblemManager.removeObsoleteProblems drops them without undrawing, and ProjectCloseListener now calls resetForProject while the editors are still alive, for every IDE instead of only Rider. - DocumentMarkupModelScanner.dispose only cancelled the merging queue, so the scheduled scan kept running after a plugin unload and the stale static instance made a reload end up with two scans. It now cancels the future and clears the instance. cancelScheduledFuture no longer retries with cancel(true), which could never succeed after cancel(false) failed, and handles a missing future. (cherry picked from commit 621ea07d11edca9d8e5e637ed77e1933b61e8df3) --- .../DocumentMarkupModelScanner.java | 20 ++++++++-- .../inlineproblems/ProblemManager.java | 39 +++++++++++++++++++ .../listeners/FileEditorListener.java | 11 ++++++ .../listeners/MarkupModelProblemListener.java | 37 ++++++++++++++---- .../listeners/ProjectCloseListener.java | 8 ++++ 5 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java index 8cb5d69..422dd97 100644 --- a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java @@ -77,7 +77,14 @@ public static DocumentMarkupModelScanner getInstance() { @Override public void dispose() { + /* Without cancelling the future the scan keeps running after a plugin unload, and the + * stale static instance would make a reload end up with two scheduled scans. */ + cancelScheduledFuture(); mergingUpdateQueue.cancelAllUpdates(); + + if (instance == this) { + instance = null; + } } public void scanForProblemsManually() { @@ -204,10 +211,15 @@ public void setDelayMilliseconds(int newDelayMilliseconds) { } private void cancelScheduledFuture() { - if (!scheduledFuture.cancel(false)) { - if (!scheduledFuture.cancel(true)) { - logger.warn("Unable to cancel scheduledFuture"); - } + ScheduledFuture future = scheduledFuture; + if (future == null) { + return; + } + + scheduledFuture = null; + + if (!future.cancel(false) && !future.isDone()) { + logger.warn("Unable to cancel the scheduled manual scan"); } } diff --git a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java index 4f226c3..b48eba2 100644 --- a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java +++ b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java @@ -140,6 +140,45 @@ public void reset() { activeProblemSnapShot.forEach(this::removeProblem); } + /** + * Removes all problems of the given project including their drawn elements. To be called + * while the project is closing, the editors are still alive at that point. + */ + public void resetForProject(Project project) { + final List activeProblemsSnapShot = List.copyOf(activeProblems); + + activeProblemsSnapShot.stream() + .filter(p -> Objects.equals(p.getProject(), project)) + .forEach(this::removeProblem); + } + + /** + * Drops all problems that belong to an editor or project that is already gone. Their inlays + * and highlighters died with the editor, so nothing has to be undrawn - undrawing would + * even mean touching a disposed editor. + */ + public void removeObsoleteProblems() { + final List obsoleteProblems = activeProblems.stream() + .filter(ProblemManager::isObsolete) + .collect(Collectors.toList()); + + if (obsoleteProblems.isEmpty()) { + return; + } + + activeProblems.removeAll(obsoleteProblems); + logger.debug("Dropped " + obsoleteProblems.size() + " problem(s) of closed editors"); + } + + private static boolean isObsolete(InlineProblem problem) { + Project project = problem.getProject(); + if (project == null || project.isDisposed()) { + return true; + } + + return !problem.getTextEditor().isValid() || problem.getTextEditor().getEditor().isDisposed(); + } + public void resetForEditor(Editor editor) { final List activeProblemsSnapShot = List.copyOf(activeProblems); diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java index 2faba81..58d7d70 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java @@ -1,9 +1,11 @@ package org.overengineer.inlineproblems.listeners; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; +import org.overengineer.inlineproblems.ProblemManager; import org.overengineer.inlineproblems.entities.enums.Listener; import org.overengineer.inlineproblems.settings.SettingsState; import org.overengineer.inlineproblems.utils.FileUtil; @@ -15,6 +17,15 @@ public class FileEditorListener implements FileEditorManagerListener { SettingsState settingsState = SettingsState.getInstance(); + @Override + public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { + MarkupModelProblemListener.disposeInvalid(); + + ApplicationManager.getApplication() + .getService(ProblemManager.class) + .removeObsoleteProblems(); + } + @Override public void fileOpenedSync( @NotNull FileEditorManager source, diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java index ad0360c..c3cbf3e 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java @@ -19,8 +19,9 @@ import org.overengineer.inlineproblems.settings.SettingsState; import org.overengineer.inlineproblems.utils.ProblemTextFilter; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; @@ -29,7 +30,9 @@ public class MarkupModelProblemListener implements MarkupModelListener { private final ProblemManager problemManager; private final TextEditor textEditor; - private static final List disposables = new ArrayList<>(); + /* One listener per TextEditor. The map is also used to avoid installing a second listener + * on an editor that already has one, which would double every problem event. */ + private static final Map disposables = new HashMap<>(); public static final String NAME = "MarkupModelListener (default)"; @@ -69,6 +72,10 @@ public static void setup(TextEditor textEditor) { return; } + if (disposables.containsKey(textEditor)) { + return; + } + Disposable disposable = new MarkupModelProblemListenerDisposable(); Disposer.register(ApplicationManager.getApplication().getService(ProblemManager.class), disposable); @@ -77,17 +84,31 @@ public static void setup(TextEditor textEditor) { new MarkupModelProblemListener(textEditor) ); - disposables.add(disposable); + disposables.put(textEditor, disposable); } - public static void disposeAll() { - List.copyOf(disposables) - .forEach(d -> { - Disposer.dispose(d); - disposables.remove(d); + /** + * Removes the listeners of all editors that are gone, to be called when an editor is closed. + * Without this the disposable stays registered on the ProblemManager for the whole IDE + * session, one per file that was ever opened. + */ + public static void disposeInvalid() { + List.copyOf(disposables.keySet()).stream() + .filter(tE -> !tE.isValid() || tE.getEditor().isDisposed()) + .forEach(tE -> { + Disposable disposable = disposables.remove(tE); + + if (disposable != null) { + Disposer.dispose(disposable); + } }); } + public static void disposeAll() { + List.copyOf(disposables.values()).forEach(Disposer::dispose); + disposables.clear(); + } + private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter) { if (!settingsState.isEnableInlineProblem()) return; diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/ProjectCloseListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/ProjectCloseListener.java index fba10d7..6194d3c 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/ProjectCloseListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/ProjectCloseListener.java @@ -1,9 +1,11 @@ package org.overengineer.inlineproblems.listeners; import com.intellij.openapi.application.ApplicationInfo; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManagerListener; import org.jetbrains.annotations.NotNull; +import org.overengineer.inlineproblems.ProblemManager; import org.overengineer.inlineproblems.UnityProjectManager; import org.overengineer.inlineproblems.entities.IDE; @@ -12,6 +14,12 @@ public class ProjectCloseListener implements ProjectManagerListener { @Override public void projectClosing(@NotNull Project project) { + /* The editors of the project are still alive here, so the drawn elements can be removed + * properly instead of waiting for the next full scan to notice the problems are stale. */ + ApplicationManager.getApplication() + .getService(ProblemManager.class) + .resetForProject(project); + // Only used in Rider because of the Unity projects if (ApplicationInfo.getInstance().getFullApplicationName().startsWith(IDE.RIDER)) { UnityProjectManager.getInstance().projectClosed(project); From fa326f3072dbcfc537f6a78f64e71ff9bc4e650c Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:12:11 +0200 Subject: [PATCH 13/46] Support glob and regex entries in the problem filter list Closes GitHub issue #94. The filter list could only match the beginning of a problem text, which does not work for messages that start with the symbol name, e.g. "'variable' is assigned a value but never used". A filter entry is now interpreted as - a regular expression if it starts with "re:" (partial match) - a glob if it contains * or ? (whole text match) - a problem text beginning otherwise Existing filter lists keep working unchanged. Glob entries quote everything but the wildcards, so filter texts may contain regex meta characters, and an invalid regular expression is logged and skipped instead of breaking the whole list. (cherry picked from commit 92f454230d0af3a8e42c24a03a50852b9c6f4d00) --- .../utils/ProblemTextFilter.java | 124 +++++++++++++++--- .../messages/SettingsBundle.properties | 2 +- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java b/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java index 4496db5..9594163 100644 --- a/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java +++ b/src/main/java/org/overengineer/inlineproblems/utils/ProblemTextFilter.java @@ -1,47 +1,63 @@ package org.overengineer.inlineproblems.utils; +import com.intellij.openapi.diagnostic.Logger; import org.overengineer.inlineproblems.settings.SettingsState; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * Matches problem texts against the user configured filter list. *

+ * A filter entry is interpreted as + *

    + *
  • a regular expression, if it starts with {@code re:} (partial match)
  • + *
  • a glob, if it contains {@code *} or {@code ?} (whole text match)
  • + *
  • a problem text beginning otherwise, which is the legacy behaviour
  • + *
+ * Matching is case insensitive in all three cases. + *

* The parsed list is cached and only rebuilt when the raw setting changes, because the filter is * evaluated for every problem of every scan and, with the MarkupModelListener, for every single * highlighter event. */ public final class ProblemTextFilter { + private static final String REGEX_PREFIX = "re:"; + private static final int PATTERN_FLAGS = Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; + + private static final Logger logger = Logger.getInstance(ProblemTextFilter.class); + private static volatile String cachedRawFilterList = null; - private static volatile List cachedFilters = List.of(); + private static volatile List> cachedFilters = List.of(); private ProblemTextFilter() { } /** * @param problemText the problem description - * @return true if the problem should be ignored because its text starts with one of the filters + * @return true if the problem should be ignored because it matches one of the filters */ public static boolean isFiltered(String problemText) { if (problemText == null) { return true; } - List filters = getFilters(); + List> filters = getFilters(); if (filters.isEmpty()) { return false; } String normalizedText = problemText.stripLeading().toLowerCase(Locale.ROOT); - for (String filter : filters) { - if (normalizedText.startsWith(filter)) { + for (Predicate filter : filters) { + if (filter.test(normalizedText)) { return true; } } @@ -49,7 +65,7 @@ public static boolean isFiltered(String problemText) { return false; } - private static List getFilters() { + private static List> getFilters() { String rawFilterList = SettingsState.getInstance().getProblemFilterList(); if (!Objects.equals(rawFilterList, cachedRawFilterList)) { @@ -60,17 +76,93 @@ private static List getFilters() { return cachedFilters; } - private static List parse(String rawFilterList) { + private static List> parse(String rawFilterList) { if (rawFilterList == null || rawFilterList.isBlank()) { return List.of(); } - /* Blank entries have to be dropped: an empty filter would match every problem text - * through startsWith("") and hide all problems. */ - return Arrays.stream(rawFilterList.split(";")) - .map(String::trim) - .filter(f -> !f.isEmpty()) - .map(f -> f.toLowerCase(Locale.ROOT)) - .collect(Collectors.toUnmodifiableList()); + List> filters = new ArrayList<>(); + + for (String entry : rawFilterList.split(";")) { + String trimmedEntry = entry.trim(); + + /* Blank entries have to be dropped: an empty filter would match every problem text + * through startsWith("") and hide all problems. */ + if (trimmedEntry.isEmpty()) { + continue; + } + + Predicate filter = toFilter(trimmedEntry); + if (filter != null) { + filters.add(filter); + } + } + + return List.copyOf(filters); + } + + private static Predicate toFilter(String entry) { + if (entry.regionMatches(true, 0, REGEX_PREFIX, 0, REGEX_PREFIX.length())) { + return toRegexFilter(entry.substring(REGEX_PREFIX.length()).trim(), entry); + } + + if (entry.indexOf('*') >= 0 || entry.indexOf('?') >= 0) { + return toGlobFilter(entry); + } + + String textBeginning = entry.toLowerCase(Locale.ROOT); + return text -> text.startsWith(textBeginning); + } + + private static Predicate toRegexFilter(String regex, String entry) { + if (regex.isEmpty()) { + return null; + } + + try { + Pattern pattern = Pattern.compile(regex, PATTERN_FLAGS); + return text -> pattern.matcher(text).find(); + } + catch (PatternSyntaxException e) { + logger.warn("Ignoring problem filter '" + entry + "', it is not a valid regular expression", e); + return null; + } + } + + private static Predicate toGlobFilter(String glob) { + Pattern pattern = Pattern.compile(globToRegex(glob), PATTERN_FLAGS); + return text -> pattern.matcher(text).matches(); + } + + /** + * Translates a glob into a regular expression. Everything but {@code *} and {@code ?} is + * quoted, so a filter text can safely contain regex meta characters like {@code (} or + * {@code .}, which problem texts are full of. + */ + private static String globToRegex(String glob) { + StringBuilder regex = new StringBuilder(); + StringBuilder literal = new StringBuilder(); + + for (int i = 0; i < glob.length(); i++) { + char c = glob.charAt(i); + + if (c != '*' && c != '?') { + literal.append(c); + continue; + } + + if (literal.length() > 0) { + regex.append(Pattern.quote(literal.toString())); + literal.setLength(0); + } + + regex.append(c == '*' ? ".*" : "."); + } + + if (literal.length() > 0) { + regex.append(Pattern.quote(literal.toString())); + } + + return regex.toString(); } } diff --git a/src/main/resources/messages/SettingsBundle.properties b/src/main/resources/messages/SettingsBundle.properties index 83b66b0..939b459 100644 --- a/src/main/resources/messages/SettingsBundle.properties +++ b/src/main/resources/messages/SettingsBundle.properties @@ -27,7 +27,7 @@ settings.manualScannerDelayTooltip=Delay between manual scans, only used when Ma settings.inlaySizeDelta=Inlay size delta settings.inlaySizeDeltaTooltip=Used to have smaller font size for the inlays, should be smaller than editor font size settings.problemFilterListLabel=Problem filter list -settings.problemFilterListTooltip=Semicolon separated list of problem text beginnings that will not be handled +settings.problemFilterListTooltip=Semicolon separated list of problems that will not be handled. An entry matches the beginning of the problem text, or the whole text if it contains the wildcards * or ?, or is used as a regular expression if it starts with 're:' settings.fileExtensionBlacklistLabel=File extension blacklist settings.fileExtensionBlacklistTooltip=Semicolon separated list of file extensions to ignore (like ".java;.md") settings.maxFileLinesLabel=Maximum file lines From c37cb260af0861167b952133e61eb6084ceca1de Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:13:08 +0200 Subject: [PATCH 14/46] Redraw the problems when the theme changes Addresses GitHub issue #61. The drawn elements keep the colors they were created with - the inlay renderer copies them in its constructor and the line highlighter reads the default foreground and background from the color scheme that was active while drawing. When the IDE follows the OS theme, the problems therefore stayed in the old theme. ThemeChangeListener subscribes to EditorColorsListener and LafManagerListener and triggers a reset and rescan, guarded so that a switch which fires both topics only redraws once. Note that the configured problem colors themselves are still absolute values, so a user who wants different colors per theme still has to adjust them by hand. (cherry picked from commit 9937ade3c330617594062b31fcde9f2a1ae0df84) --- .../listeners/ThemeChangeListener.java | 55 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 4 ++ 2 files changed, 59 insertions(+) create mode 100644 src/main/java/org/overengineer/inlineproblems/listeners/ThemeChangeListener.java diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/ThemeChangeListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/ThemeChangeListener.java new file mode 100644 index 0000000..fb0749b --- /dev/null +++ b/src/main/java/org/overengineer/inlineproblems/listeners/ThemeChangeListener.java @@ -0,0 +1,55 @@ +package org.overengineer.inlineproblems.listeners; + +import com.intellij.ide.ui.LafManager; +import com.intellij.ide.ui.LafManagerListener; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.colors.EditorColorsListener; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.overengineer.inlineproblems.ListenerManager; +import org.overengineer.inlineproblems.settings.SettingsState; + +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * Redraws all problems when the color scheme or the look and feel changes. + *

+ * The drawn elements keep the colors they were created with: the inlay renderer copies them in + * its constructor and the line highlighter takes the default foreground and background from the + * scheme that was active while drawing. Without a redraw they stay in the old theme until the + * problems happen to be re-created, which is what GitHub issue #61 reports for the automatic + * dark/light switch. + */ +public class ThemeChangeListener implements EditorColorsListener, LafManagerListener { + + /* Static because the platform creates one listener instance per subscribed topic and a + * theme switch usually fires both of them. */ + private static final AtomicBoolean redrawScheduled = new AtomicBoolean(false); + + @Override + public void globalSchemeChange(@Nullable EditorColorsScheme scheme) { + scheduleRedraw(); + } + + @Override + public void lookAndFeelChanged(@NotNull LafManager source) { + scheduleRedraw(); + } + + private void scheduleRedraw() { + if (!SettingsState.getInstance().isEnableInlineProblem()) { + return; + } + + if (!redrawScheduled.compareAndSet(false, true)) { + return; + } + + ApplicationManager.getApplication().invokeLater(() -> { + redrawScheduled.set(false); + ListenerManager.getInstance().resetAndRescan(); + }); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 74b967b..0a475f1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -62,5 +62,9 @@ topic="com.intellij.ide.plugins.DynamicPluginListener"/> + + \ No newline at end of file From c66ccd76973ffd711ce33a8501f330263ee4ff57 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:14:25 +0200 Subject: [PATCH 15/46] Turn the listener selection into a real enum entities/enums/Listener was a class holding three int constants, so the selected listener travelled through the code as an int and SettingsComponent.getEnabledListener returned the raw combo box index. That only mapped to the right listener because the combo box entries happened to be listed in the same order as the constants - inserting or reordering an entry would have silently switched the listener for every user. Listener is now an enum that owns its persisted id and its display name, the settings expose it through getActiveListener/setActiveListener, and the combo box is built from the enum values so index and value cannot drift apart. The persisted setting stays an int id, so existing settings files keep working, and the NAME constants that only existed to label the combo box are gone. (cherry picked from commit ef5428503a480b8caf27429435a64a79393467ad) --- .../DocumentMarkupModelScanner.java | 4 +- .../inlineproblems/ListenerManager.java | 8 ++-- .../inlineproblems/UnityProjectManager.java | 12 +++--- .../entities/enums/Listener.java | 38 +++++++++++++++++-- .../listeners/FileEditorListener.java | 2 +- .../listeners/HighlightProblemListener.java | 5 +-- .../listeners/MarkupModelProblemListener.java | 4 +- .../listeners/PluginListener.java | 2 +- .../settings/SettingsComponent.java | 29 +++++++------- .../settings/SettingsConfigurable.java | 12 +++--- .../settings/SettingsState.java | 20 ++++++++-- 11 files changed, 89 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java index 422dd97..085881f 100644 --- a/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java +++ b/src/main/java/org/overengineer/inlineproblems/DocumentMarkupModelScanner.java @@ -43,8 +43,6 @@ public class DocumentMarkupModelScanner implements Disposable { private ScheduledFuture scheduledFuture; - public static final String NAME = "ManualScanner"; - private DocumentMarkupModelScanner() { Disposer.register(problemManager, this); @@ -61,7 +59,7 @@ private DocumentMarkupModelScanner() { ); SettingsState settingsState = SettingsState.getInstance(); - if (settingsState.getEnabledListener() == Listener.MANUAL_SCANNING) { + if (settingsState.getActiveListener() == Listener.MANUAL_SCANNING) { delayMilliseconds = settingsState.getManualScannerDelay(); } diff --git a/src/main/java/org/overengineer/inlineproblems/ListenerManager.java b/src/main/java/org/overengineer/inlineproblems/ListenerManager.java index 0d8452b..13c9dd2 100644 --- a/src/main/java/org/overengineer/inlineproblems/ListenerManager.java +++ b/src/main/java/org/overengineer/inlineproblems/ListenerManager.java @@ -68,18 +68,18 @@ public void resetMarkupModelProblemListeners() { } public void changeListener() { - if (settings.getEnabledListener() != Listener.MARKUP_MODEL_LISTENER) { + if (settings.getActiveListener() != Listener.MARKUP_MODEL_LISTENER) { MarkupModelProblemListener.disposeAll(); } - if (settings.getEnabledListener() == Listener.MARKUP_MODEL_LISTENER) { + if (settings.getActiveListener() == Listener.MARKUP_MODEL_LISTENER) { documentMarkupModelScanner.setDelayMilliseconds(HighlightProblemListener.ADDITIONAL_MANUAL_SCAN_DELAY_MILLIS); installMarkupModelListenerOnAllProjects(); } - else if (settings.getEnabledListener() == Listener.HIGHLIGHT_PROBLEMS_LISTENER) { + else if (settings.getActiveListener() == Listener.HIGHLIGHT_PROBLEMS_LISTENER) { documentMarkupModelScanner.setDelayMilliseconds(HighlightProblemListener.ADDITIONAL_MANUAL_SCAN_DELAY_MILLIS); } - else if (settings.getEnabledListener() == Listener.MANUAL_SCANNING) { + else if (settings.getActiveListener() == Listener.MANUAL_SCANNING) { documentMarkupModelScanner.setDelayMilliseconds(settings.getManualScannerDelay()); } diff --git a/src/main/java/org/overengineer/inlineproblems/UnityProjectManager.java b/src/main/java/org/overengineer/inlineproblems/UnityProjectManager.java index cce52df..4cb1e8a 100644 --- a/src/main/java/org/overengineer/inlineproblems/UnityProjectManager.java +++ b/src/main/java/org/overengineer/inlineproblems/UnityProjectManager.java @@ -22,7 +22,7 @@ public class UnityProjectManager { private final List projects = new ArrayList<>(); - private int enabledListenerBefore; + private Listener enabledListenerBefore; @Getter private final UnityProjectScanner unityProjectScanner = new UnityProjectScanner(); @@ -34,7 +34,7 @@ public class UnityProjectManager { private static UnityProjectManager instance; private UnityProjectManager() { - enabledListenerBefore = settings.getEnabledListener(); + enabledListenerBefore = settings.getActiveListener(); } public static UnityProjectManager getInstance() { @@ -89,11 +89,11 @@ private boolean isAUnityProjectOpened() { } private void handleUnityProjectOpened(Project project) { - enabledListenerBefore = settings.getEnabledListener(); + enabledListenerBefore = settings.getActiveListener(); if (enabledListenerBefore == Listener.MANUAL_SCANNING) return; - settings.setEnabledListener(Listener.MANUAL_SCANNING); + settings.setActiveListener(Listener.MANUAL_SCANNING); ListenerManager listenerManager = ListenerManager.getInstance(); listenerManager.resetAndRescan(); @@ -105,8 +105,8 @@ private void handleNoMoreUnityProjectsOpened(Project project) { if (enabledListenerBefore == Listener.MANUAL_SCANNING) return; - if (settings.getEnabledListener() == Listener.MANUAL_SCANNING) { - settings.setEnabledListener(enabledListenerBefore); + if (settings.getActiveListener() == Listener.MANUAL_SCANNING) { + settings.setActiveListener(enabledListenerBefore); ListenerManager listenerManager = ListenerManager.getInstance(); listenerManager.resetAndRescan(); diff --git a/src/main/java/org/overengineer/inlineproblems/entities/enums/Listener.java b/src/main/java/org/overengineer/inlineproblems/entities/enums/Listener.java index 0d929d2..bb48ac2 100644 --- a/src/main/java/org/overengineer/inlineproblems/entities/enums/Listener.java +++ b/src/main/java/org/overengineer/inlineproblems/entities/enums/Listener.java @@ -1,7 +1,37 @@ package org.overengineer.inlineproblems.entities.enums; -public class Listener { - public static final int HIGHLIGHT_PROBLEMS_LISTENER = 0; - public static final int MARKUP_MODEL_LISTENER = 1; - public static final int MANUAL_SCANNING = 2; +import lombok.Getter; + + +/** + * The available problem detection strategies. + *

+ * The declaration order defines the order in the settings combo box, the id is what gets + * persisted. Do not change existing ids, they are stored in the users settings file. + */ +@Getter +public enum Listener { + HIGHLIGHT_PROBLEMS_LISTENER(0, "HighlightProblemListener"), + MARKUP_MODEL_LISTENER(1, "MarkupModelListener (default)"), + MANUAL_SCANNING(2, "ManualScanner"); + + public static final Listener DEFAULT = MARKUP_MODEL_LISTENER; + + private final int id; + private final String displayName; + + Listener(int id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public static Listener fromId(int id) { + for (Listener listener : values()) { + if (listener.id == id) { + return listener; + } + } + + return DEFAULT; + } } diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java index 58d7d70..b76cd61 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/FileEditorListener.java @@ -32,7 +32,7 @@ public void fileOpenedSync( @NotNull VirtualFile file, @NotNull Pair editors ) { - if (settingsState.getEnabledListener() != Listener.MARKUP_MODEL_LISTENER) + if (settingsState.getActiveListener() != Listener.MARKUP_MODEL_LISTENER) return; // Precheck only file name, later we check the line count only diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java index b3dfbb9..a5a5779 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/HighlightProblemListener.java @@ -18,14 +18,13 @@ public class HighlightProblemListener implements HighlightInfoFilter { private final DocumentMarkupModelScanner markupModelScanner = DocumentMarkupModelScanner.getInstance(); private final SettingsState settingsState = SettingsState.getInstance(); - public static final String NAME = "HighlightProblemListener"; public static final int ADDITIONAL_MANUAL_SCAN_DELAY_MILLIS = 2000; @Override public boolean accept(@NotNull HighlightInfo highlightInfo, @Nullable PsiFile file) { if (!settingsState.isEnableInlineProblem()) return true; - if (settingsState.getEnabledListener() != Listener.HIGHLIGHT_PROBLEMS_LISTENER) + if (settingsState.getActiveListener() != Listener.HIGHLIGHT_PROBLEMS_LISTENER) return true; if (file == null || !file.isValid()) return true; @@ -43,7 +42,7 @@ public boolean accept(@NotNull HighlightInfo highlightInfo, @Nullable PsiFile fi } public void handleAccept(PsiFile file) { - if (settingsState.getEnabledListener() != Listener.HIGHLIGHT_PROBLEMS_LISTENER) + if (settingsState.getActiveListener() != Listener.HIGHLIGHT_PROBLEMS_LISTENER) return; if (file.getProject().isDisposed() || file.getVirtualFile() == null) diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java index c3cbf3e..00d58e2 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/MarkupModelProblemListener.java @@ -34,8 +34,6 @@ public class MarkupModelProblemListener implements MarkupModelListener { * on an editor that already has one, which would double every problem event. */ private static final Map disposables = new HashMap<>(); - public static final String NAME = "MarkupModelListener (default)"; - private enum EventType { ADD, REMOVE, CHANGE } @@ -113,7 +111,7 @@ private void handleEvent(EventType type, @NotNull RangeHighlighterEx highlighter if (!settingsState.isEnableInlineProblem()) return; - if (settingsState.getEnabledListener() != Listener.MARKUP_MODEL_LISTENER) + if (settingsState.getActiveListener() != Listener.MARKUP_MODEL_LISTENER) return; Editor editor = textEditor.getEditor(); diff --git a/src/main/java/org/overengineer/inlineproblems/listeners/PluginListener.java b/src/main/java/org/overengineer/inlineproblems/listeners/PluginListener.java index e7639cb..e989bc9 100644 --- a/src/main/java/org/overengineer/inlineproblems/listeners/PluginListener.java +++ b/src/main/java/org/overengineer/inlineproblems/listeners/PluginListener.java @@ -26,7 +26,7 @@ public void pluginLoaded(@NotNull IdeaPluginDescriptor descriptor) { projectManager.scanAllOpenProjectsForUnity(); } - if (settingsState.getEnabledListener() == Listener.MARKUP_MODEL_LISTENER) { + if (settingsState.getActiveListener() == Listener.MARKUP_MODEL_LISTENER) { if (descriptor.getPluginId().getIdString().equalsIgnoreCase(PLUGIN_ID)) { listenerManager.installMarkupModelListenerOnAllProjects(); } diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java index aeab531..f0cd5b5 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java @@ -7,10 +7,8 @@ import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.FormBuilder; import lombok.Getter; -import org.overengineer.inlineproblems.DocumentMarkupModelScanner; import org.overengineer.inlineproblems.bundles.SettingsBundle; -import org.overengineer.inlineproblems.listeners.HighlightProblemListener; -import org.overengineer.inlineproblems.listeners.MarkupModelProblemListener; +import org.overengineer.inlineproblems.entities.enums.Listener; import javax.swing.*; import javax.swing.text.NumberFormatter; @@ -77,8 +75,10 @@ public class SettingsComponent { private final JBTextField problemFilterList = new JBTextField(); private final JBTextField fileExtensionBlacklist = new JBTextField(); - private final String[] availableListeners = {HighlightProblemListener.NAME, MarkupModelProblemListener.NAME, DocumentMarkupModelScanner.NAME}; - private final JComboBox enabledListener = new ComboBox<>(availableListeners); + private static final List availableListeners = List.of(Listener.values()); + private final JComboBox enabledListener = new ComboBox<>( + availableListeners.stream().map(Listener::getDisplayName).toArray(String[]::new) + ); private final JBTextField additionalInfoSeverities = new JBTextField(); private final JBTextField additionalWarningSeverities = new JBTextField(); @@ -157,7 +157,7 @@ public SettingsComponent() { problemFilterList.setText(settingsState.getProblemFilterList()); fileExtensionBlacklist.setText(settingsState.getFileExtensionBlacklist()); - setEnabledListener(settingsState.getEnabledListener()); + setEnabledListener(settingsState.getActiveListener()); Dimension enabledListenerDimension = enabledListener.getPreferredSize(); enabledListenerDimension.width += 100; @@ -620,16 +620,19 @@ public void setAdditionalWeakWarningSeverities(final String newText) { additionalWeakWarningSeverities.setText(newText); } - public int getEnabledListener() { - return enabledListener.getSelectedIndex(); - } + public Listener getEnabledListener() { + int index = enabledListener.getSelectedIndex(); - public void setEnabledListener(int index) { - if (index < 0 || index >= availableListeners.length) { - index = 0; + if (index < 0 || index >= availableListeners.size()) { + return Listener.DEFAULT; } - enabledListener.setSelectedIndex(index); + return availableListeners.get(index); + } + + public void setEnabledListener(Listener listener) { + // indexOf returns -1 for an unknown value, which falls back to the first entry + enabledListener.setSelectedIndex(Math.max(0, availableListeners.indexOf(listener))); } private List getSeverityIntegerList(String text) { diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java index 639ab0c..444b60c 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java @@ -86,7 +86,7 @@ public boolean isModified() { state.isHighlightInfos() == settingsComponent.isHighlightInfo() && state.isShowInfosInGutter() == settingsComponent.isShowInfosInGutter() && - state.getEnabledListener() == settingsComponent.getEnabledListener() && + state.getActiveListener() == settingsComponent.getEnabledListener() && state.getManualScannerDelay() == settingsComponent.getManualScannerDelay() && state.getProblemFilterList().equals(settingsComponent.getProblemFilterList()) && @@ -106,7 +106,7 @@ public boolean isModified() { public void apply() { SettingsState state = SettingsState.getInstance(); - boolean listenerChanged = state.getEnabledListener() != settingsComponent.getEnabledListener(); + boolean listenerChanged = state.getActiveListener() != settingsComponent.getEnabledListener(); boolean fileExtensionBlacklistChanged = !Objects.equals(state.getFileExtensionBlacklist(), settingsComponent.getFileExtensionBlacklist()); boolean manualScannerDelayChanged = state.getManualScannerDelay() != settingsComponent.getManualScannerDelay(); boolean maxFileLinesChanged = state.getMaxFileLines() != settingsComponent.getMaxFileLines(); @@ -156,7 +156,7 @@ public void apply() { state.setItalicProblemLabels(settingsComponent.isItalicProblemLabels()); state.setClickableContext(settingsComponent.isClickableContext()); - state.setEnabledListener(settingsComponent.getEnabledListener()); + state.setActiveListener(settingsComponent.getEnabledListener()); state.setManualScannerDelay(settingsComponent.getManualScannerDelay()); state.setProblemFilterList(settingsComponent.getProblemFilterList()); state.setFileExtensionBlacklist(settingsComponent.getFileExtensionBlacklist()); @@ -167,14 +167,14 @@ public void apply() { state.setAdditionalWeakWarningSeverities(settingsComponent.getAdditionalWeakWarningSeveritiesList()); state.setAdditionalErrorSeverities(settingsComponent.getAdditionalErrorSeveritiesList()); - if (manualScannerDelayChanged && state.getEnabledListener() == Listener.MANUAL_SCANNING) { + if (manualScannerDelayChanged && state.getActiveListener() == Listener.MANUAL_SCANNING) { DocumentMarkupModelScanner.getInstance().setDelayMilliseconds(state.getManualScannerDelay()); } listenerManager.resetAndRescan(); // When the blacklist or maxFileLines changes we need to re-apply all MarkupModelProblemListeners - if ((fileExtensionBlacklistChanged || maxFileLinesChanged) && state.getEnabledListener() == Listener.MARKUP_MODEL_LISTENER) { + if ((fileExtensionBlacklistChanged || maxFileLinesChanged) && state.getActiveListener() == Listener.MARKUP_MODEL_LISTENER) { listenerManager.resetMarkupModelProblemListeners(); } @@ -232,7 +232,7 @@ public void reset() { settingsComponent.setItalicProblemLabels(state.isItalicProblemLabels()); settingsComponent.setClickableContext(state.isClickableContext()); - settingsComponent.setEnabledListener(state.getEnabledListener()); + settingsComponent.setEnabledListener(state.getActiveListener()); settingsComponent.setManualScannerDelay(state.getManualScannerDelay()); settingsComponent.setProblemFilterList(state.getProblemFilterList()); settingsComponent.setFileExtensionBlacklist(state.getFileExtensionBlacklist()); diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsState.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsState.java index e585fea..402e564 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsState.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsState.java @@ -97,7 +97,7 @@ public class SettingsState implements PersistentStateComponent { private boolean boldProblemLabels = false; private boolean italicProblemLabels = false; private int problemLineLengthOffsetPixels = 50; - private int enabledListener = Listener.MARKUP_MODEL_LISTENER; + private int enabledListener = Listener.DEFAULT.getId(); private String problemFilterList = "todo;fixme;open in browser"; private String fileExtensionBlacklist = ""; private int maxFileLines = 0; @@ -116,6 +116,20 @@ public class SettingsState implements PersistentStateComponent { private boolean highlightProblemListenerDeprecateMigrationDone = false; private boolean filterListMigrationDone01 = false; + /** + * The persisted {@link #enabledListener} is an id and not the enum itself to stay compatible + * with the settings files of older versions. + */ + @Transient + public Listener getActiveListener() { + return Listener.fromId(enabledListener); + } + + @Transient + public void setActiveListener(Listener listener) { + enabledListener = listener.getId(); + } + public static SettingsState getInstance() { return ApplicationManager.getApplication().getService(SettingsState.class); } @@ -152,8 +166,8 @@ private void migrateState() { // listener if (!highlightProblemListenerDeprecateMigrationDone) { - if (enabledListener == Listener.HIGHLIGHT_PROBLEMS_LISTENER) { - enabledListener = Listener.MARKUP_MODEL_LISTENER; + if (getActiveListener() == Listener.HIGHLIGHT_PROBLEMS_LISTENER) { + setActiveListener(Listener.MARKUP_MODEL_LISTENER); } highlightProblemListenerDeprecateMigrationDone = true; From 901d6cbd2333aa07685223ae6752fe0fcf4d0382 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:15:02 +0200 Subject: [PATCH 16/46] Make the problem line length offset configurable again SettingsState.problemLineLengthOffsetPixels existed but was never read; InlineDrawer had the value hardcoded as "+ 50" with the comment that the width calculation is not exact. Since the deviation depends on the font and the editor, the setting is now actually used and exposed in the settings, next to the inlay font size delta. (cherry picked from commit 28e755ac775c169879b46e7a6ac1e0d6997f41ef) --- .../inlineproblems/InlineDrawer.java | 5 +++-- .../settings/SettingsComponent.java | 19 +++++++++++++++++++ .../settings/SettingsConfigurable.java | 3 +++ .../messages/SettingsBundle.properties | 2 ++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java index f8e7926..8f5b891 100644 --- a/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java +++ b/src/main/java/org/overengineer/inlineproblems/InlineDrawer.java @@ -61,8 +61,9 @@ public void drawProblemLabel(InlineProblem problem) { editorFontMetrics.stringWidth(lineText) + existingInlineElementsWidth; - // We add 50 as offset here because the calculation is somehow not exact - if (problemWidth + 50 > editorWidth && !settings.isForceProblemsInSameLine()) + /* The offset is added because the width calculation is not exact. It is configurable + * because the deviation depends on the font and the editor. */ + if (problemWidth + settings.getProblemLineLengthOffsetPixels() > editorWidth && !settings.isForceProblemsInSameLine()) { inlineProblemLabel.setBlockElement(true); problem.setBlockElement(true); diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java index f0cd5b5..d8c6954 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsComponent.java @@ -64,6 +64,7 @@ public class SettingsComponent { private final JBCheckBox enableHtmlStripping = new JBCheckBox(SettingsBundle.message("settings.enableHtmlStripping")); private final JBCheckBox enableXmlUnescaping = new JBCheckBox(SettingsBundle.message("settings.enableXmlUnescaping")); private final JFormattedTextField inlayFontSizeDeltaText; + private final JFormattedTextField problemLineLengthOffsetPixels; private final JFormattedTextField manualScannerDelay; private final JFormattedTextField maxProblemsPerLine; private final JFormattedTextField maxFileLines; @@ -136,6 +137,9 @@ public SettingsComponent() { inlayFontSizeDeltaText = new JFormattedTextField(numberFormatter); inlayFontSizeDeltaText.setText(Integer.toString(settingsState.getInlayFontSizeDelta())); + problemLineLengthOffsetPixels = new JFormattedTextField(numberFormatter); + problemLineLengthOffsetPixels.setText(Integer.toString(settingsState.getProblemLineLengthOffsetPixels())); + maxProblemsPerLine = new JFormattedTextField(numberFormatter); maxProblemsPerLine.setText(Integer.toString(settingsState.getMaxProblemsPerLine())); @@ -189,6 +193,8 @@ public SettingsComponent() { .addComponent(enableXmlUnescaping, 0) .addLabeledComponent(new JBLabel(SettingsBundle.message("settings.inlaySizeDelta")), inlayFontSizeDeltaText) .addTooltip(SettingsBundle.message("settings.inlaySizeDeltaTooltip")) + .addLabeledComponent(new JBLabel(SettingsBundle.message("settings.problemLineLengthOffsetLabel")), problemLineLengthOffsetPixels) + .addTooltip(SettingsBundle.message("settings.problemLineLengthOffsetTooltip")) .addLabeledComponent(new JLabel(SettingsBundle.message("settings.problemFilterListLabel")), problemFilterList) .addTooltip(SettingsBundle.message("settings.problemFilterListTooltip")) .addLabeledComponent(new JLabel(SettingsBundle.message("settings.fileExtensionBlacklistLabel")), fileExtensionBlacklist) @@ -676,6 +682,19 @@ public void setMaxProblemsPerLine(int max) { maxProblemsPerLine.setText(Integer.toString(Math.max(0, max))); } + public int getProblemLineLengthOffsetPixels() { + try { + return Math.max(Integer.parseInt(problemLineLengthOffsetPixels.getText()), 0); + } + catch (NumberFormatException e) { + return 0; + } + } + + public void setProblemLineLengthOffsetPixels(int offset) { + problemLineLengthOffsetPixels.setText(Integer.toString(Math.max(0, offset))); + } + public int getMaxFileLines() { try { return Math.max(Integer.parseInt(maxFileLines.getText()), 0); diff --git a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java index 444b60c..c192af7 100644 --- a/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java +++ b/src/main/java/org/overengineer/inlineproblems/settings/SettingsConfigurable.java @@ -53,6 +53,7 @@ public boolean isModified() { state.isEnableHtmlStripping() == settingsComponent.isEnableHtmlStripping() && state.isEnableXmlUnescaping() == settingsComponent.isEnableXmlUnescaping() && state.getInlayFontSizeDelta() == settingsComponent.getInlayFontSizeDelta() && + state.getProblemLineLengthOffsetPixels() == settingsComponent.getProblemLineLengthOffsetPixels() && state.isFillProblemLabels() == settingsComponent.isFillProblemLabels() && state.isBoldProblemLabels() == settingsComponent.isBoldProblemLabels() && state.isItalicProblemLabels() == settingsComponent.isItalicProblemLabels() && @@ -151,6 +152,7 @@ public void apply() { state.setEnableHtmlStripping(settingsComponent.isEnableHtmlStripping()); state.setEnableXmlUnescaping(settingsComponent.isEnableXmlUnescaping()); state.setInlayFontSizeDelta(settingsComponent.getInlayFontSizeDelta()); + state.setProblemLineLengthOffsetPixels(settingsComponent.getProblemLineLengthOffsetPixels()); state.setFillProblemLabels(settingsComponent.isFillProblemLabels()); state.setBoldProblemLabels(settingsComponent.isBoldProblemLabels()); state.setItalicProblemLabels(settingsComponent.isItalicProblemLabels()); @@ -227,6 +229,7 @@ public void reset() { settingsComponent.setEnableHtmlStripping(state.isEnableHtmlStripping()); settingsComponent.setEnableXmlUnescaping(state.isEnableXmlUnescaping()); settingsComponent.setInlayFontSizeDelta(state.getInlayFontSizeDelta()); + settingsComponent.setProblemLineLengthOffsetPixels(state.getProblemLineLengthOffsetPixels()); settingsComponent.setFillProblemLabels(state.isFillProblemLabels()); settingsComponent.setBoldProblemLabels(state.isBoldProblemLabels()); settingsComponent.setItalicProblemLabels(state.isItalicProblemLabels()); diff --git a/src/main/resources/messages/SettingsBundle.properties b/src/main/resources/messages/SettingsBundle.properties index 939b459..a062ed3 100644 --- a/src/main/resources/messages/SettingsBundle.properties +++ b/src/main/resources/messages/SettingsBundle.properties @@ -26,6 +26,8 @@ settings.manualScannerDelayLabel=ManualScanner delay in milliseconds settings.manualScannerDelayTooltip=Delay between manual scans, only used when ManualScanner is enabled settings.inlaySizeDelta=Inlay size delta settings.inlaySizeDeltaTooltip=Used to have smaller font size for the inlays, should be smaller than editor font size +settings.problemLineLengthOffsetLabel=Problem line length offset in pixels +settings.problemLineLengthOffsetTooltip=Additional width that is assumed for a line before the problem is drawn below the line instead of behind it. Only used if "Force problems in one line" is disabled settings.problemFilterListLabel=Problem filter list settings.problemFilterListTooltip=Semicolon separated list of problems that will not be handled. An entry matches the beginning of the problem text, or the whole text if it contains the wildcards * or ?, or is used as a regular expression if it starts with 're:' settings.fileExtensionBlacklistLabel=File extension blacklist From 53260626c0a0be80143fc3ef159e509a07816da7 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:17:58 +0200 Subject: [PATCH 17/46] Update Gradle and the build plugins The committed wrapper was still on Gradle 7.5.1 while gradle.properties already claimed 7.6, and 7.5.1 refuses to run on JDK 21 or newer, so the project could not be built with a current default JDK at all. - Gradle 7.5.1 -> 8.10.2 (wrapper regenerated, properties aligned) - org.gradle.unsafe.configuration-cache -> org.gradle.configuration-cache, the old name is gone in Gradle 8 - org.jetbrains.intellij 1.11.0 -> 1.17.4 (last 1.x release) - io.freefair.lombok 6.6 -> 8.6, org.projectlombok:lombok 1.18.24 -> 1.18.34 - org.jetbrains.changelog 2.0.0 -> 2.2.1 - org.jetbrains.qodana 0.1.13 -> 2024.1.5; reportPath, saveReport and showReport no longer exist in the extension, report handling is configured through qodana.yml and the CI action The platform baseline is deliberately untouched: platformVersion stays at 2021.2.4 and the sources still target Java 11, so compilation needs a JDK 17 (or older) to run Gradle. (cherry picked from commit 20c57c7b98ef220d07992b34507b6301ffe32109) --- build.gradle.kts | 16 ++++----- gradle.properties | 4 +-- gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++- gradlew | 44 ++++++++++++++++------- gradlew.bat | 37 ++++++++++--------- 6 files changed, 65 insertions(+), 40 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ac7fe5e..0452015 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,10 +5,10 @@ fun properties(key: String) = project.findProperty(key).toString() plugins { id("java") - id("org.jetbrains.intellij") version "1.11.0" - id("io.freefair.lombok") version "6.6" - id("org.jetbrains.changelog") version "2.0.0" - id("org.jetbrains.qodana") version "0.1.13" + id("org.jetbrains.intellij") version "1.17.4" + id("io.freefair.lombok") version "8.6" + id("org.jetbrains.changelog") version "2.2.1" + id("org.jetbrains.qodana") version "2024.1.5" } group = properties("pluginGroup") @@ -19,7 +19,7 @@ repositories { } dependencies { - implementation("org.projectlombok:lombok:1.18.24") + implementation("org.projectlombok:lombok:1.18.34") } // Configure Gradle IntelliJ Plugin @@ -42,9 +42,9 @@ changelog { // Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin qodana { cachePath.set(file(".qodana").canonicalPath) - reportPath.set(file("build/reports/inspections").canonicalPath) - saveReport.set(true) - showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false) + resultsPath.set(file("build/reports/inspections").canonicalPath) + // reportPath, saveReport and showReport were removed from the plugin, the report handling is + // configured through qodana.yml and the Qodana action in the CI now. } tasks { diff --git a/gradle.properties b/gradle.properties index 6548c4c..00fa3c2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ platformVersion = 2021.2.4 platformPlugins = # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 7.6 +gradleVersion = 8.10.2 # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library # suppress inspection "UnusedProperty" @@ -27,4 +27,4 @@ kotlin.stdlib.default.dependency = false # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html # suppress inspection "UnusedProperty" -org.gradle.unsafe.configuration-cache = true +org.gradle.configuration-cache = true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 60756 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 /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ @@ -205,6 +217,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 0b4638fd5e586226d9b7acc92c066272adf0fce2 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:18:47 +0200 Subject: [PATCH 18/46] Clean up the gitignore and the CI workflows .gitignore additions: - .env, which is currently untracked only by luck and may hold tokens - IMPROVEMENTS.md, local review notes - .claude/, .idea/modules.xml, .idea/copilot*, all generated per machine Workflows: - The build workflow uploaded build/reports/kover/report.xml to Codecov, but the Kover plugin is not applied, so that file never exists. Step removed instead of pretending there is coverage. - Documented that the verify job no longer runs the IntelliJ Plugin Verifier: 9a6304e replaced runPluginVerifier with verifyPlugin, which in gradle-intellij-plugin 1.x only validates the descriptors and the archive structure. The verifier home dir property, its cache step and listProductsReleases have been dead weight since then. Left as a TODO rather than re-enabled, because the commit message suggests it was failing and that needs a look first. - release, beta-release and run-ui-tests still used checkout@v3 and setup-java@v3 while build.yml was already on v4, and the publishing workflows had no Gradle cache. - Added dependabot for gradle and github-actions, the plugin versions had drifted several years behind. (cherry picked from commit 01043256400ecb5f353bf79eabadf7323bf08ec0) --- .github/dependabot.yml | 15 +++++++++++++++ .github/workflows/beta-release.yml | 10 ++++++++-- .github/workflows/build.yml | 26 ++++++++++++++++++-------- .github/workflows/release.yml | 10 ++++++++-- .github/workflows/run-ui-tests.yml | 4 ++-- .gitignore | 15 +++++++++++++++ 6 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2c0f11b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# Keeps the Gradle plugins/dependencies and the GitHub Actions up to date. +# https://docs.github.com/code-security/dependabot/dependabot-version-updates +version: 2 +updates: + - package-ecosystem: gradle + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index a0e8cd4..84893d2 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -14,7 +14,7 @@ jobs: steps: # Check out current repository - name: Fetch Sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Source branch ref: ${{ github.head_ref }} @@ -33,11 +33,17 @@ jobs: # Setup Java 11 environment for the next steps - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 11 + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-home-cache-cleanup: true + # Publish the plugin to the Marketplace, this should automatically use the 'beta' channel because we changed the # version in gradle.properties to version-beta - name: Publish Plugin Beta diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c640af..3cc0812 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,6 @@ # - run 'test' and 'verifyPlugin' tasks, # - run Qodana inspections, # - run 'buildPlugin' task and prepare artifact for the further tests, -# - run 'runPluginVerifier' task, # - create a draft release. # # Workflow is triggered on push and pull_request events. @@ -138,12 +137,6 @@ jobs: name: tests-result path: ${{ github.workspace }}/build/reports/tests - # Upload the Kover report to CodeCov - - name: Upload Code Coverage Report - uses: codecov/codecov-action@v4 - with: - files: ${{ github.workspace }}/build/reports/kover/report.xml - # Run Qodana inspections and provide report inspectCode: name: Inspect code @@ -179,7 +172,24 @@ jobs: with: cache-default-branch-only: true - # Run plugin structure verification along with IntelliJ Plugin Verifier + # Run the plugin structure verification. + # + # NOTE this job used to run the IntelliJ Plugin Verifier. Commit 9a6304e ("Try to fix plugin + # verification task", 2025-04-10) replaced `runPluginVerifier` with `verifyPlugin`, and + # with gradle-intellij-plugin 1.x those are two different tasks: + # verifyPlugin - validates the plugin.xml descriptors and the archive structure + # runPluginVerifier - the actual verifier, checks binary compatibility against IDE builds + # So binary incompatibilities with newer IDE versions are currently not caught in CI. The + # -Dplugin.verifier.home.dir property below, the cache step and the listProductsReleases + # call in the build job are all leftovers that only take effect with runPluginVerifier. + # Locally the verifier is still available through the "Run Verifications" run + # configuration in .run/, which does call runPluginVerifier. + # TODO decide whether to re-enable it. The commit message suggests it was failing back then, + # which is plausible with an open untilBuild plus the internal APIs this plugin uses + # (HighlightInfo, DocumentMarkupModel, FontInfo, ShowIntentionActionsHandler) - it likely + # needs an explicit ideVersions/failureLevel configuration rather than a plain re-add. + # Also note that the task was renamed when the IntelliJ Platform Gradle Plugin 2.x came + # out, so verify the task names again when migrating off 1.x. verify: name: Verify plugin needs: [ build ] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c39feeb..ab9dcdd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,17 +20,23 @@ jobs: # Check out current repository - name: Fetch Sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.release.tag_name }} # Setup Java 11 environment for the next steps - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 11 + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-home-cache-cleanup: true + # Publish the plugin to the Marketplace - name: Publish Plugin env: diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml index 363d9e8..a3f9fbc 100644 --- a/.github/workflows/run-ui-tests.yml +++ b/.github/workflows/run-ui-tests.yml @@ -33,11 +33,11 @@ jobs: # Check out current repository - name: Fetch Sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Setup Java 11 environment for the next steps - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 11 diff --git a/.gitignore b/.gitignore index 737ffc1..9d9789b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,16 @@ .qodana build +### Local only ### +# Environment files with local tokens/secrets +.env +.env.* +# Local review/planning notes +IMPROVEMENTS.md +# AI assistant working directories +.claude/ +.aider* + !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -11,6 +21,11 @@ build *.iml *.ipr out/ +# Generated per machine / per IDE version, not worth tracking +.idea/modules.xml +.idea/copilot* +.idea/shelf/ +.idea/workspace.xml !**/src/main/**/out/ !**/src/test/**/out/ From fafa5990bc6cb9f943745184881c32a5dd73a0b2 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 16:19:27 +0200 Subject: [PATCH 19/46] Fix the README links and document how to build - The license link pointed to LICENSE.txt on the master branch, the file is LICENSE on main - The table of contents link to the beta section had a space instead of a dash, so it did not resolve - Getting Started only said "clone and open"; it now names the JDK requirement that follows from the Java 11 target and the tasks that are actually useful - The roadmap entries that already have issues link to them (cherry picked from commit 885e84905c83387ec66269f5c74499f9388b98f0) --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1c4fad9..0cb82e0 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ You can turn this plugin on and off with the keyboard shortcut `alt+u`, addition

  • Getting Started
  • -
  • Beta Versions
  • +
  • Beta Versions
  • Roadmap
  • License
  • Contact
  • @@ -94,6 +94,12 @@ You can turn this plugin on and off with the keyboard shortcut `alt+u`, addition ## Getting Started - Clone the repository and open it with IntelliJ +- The plugin targets the IntelliJ Platform `2021.2.4` and Java 11 (see `gradle.properties`), + so Gradle has to run on a JDK that still supports a Java 11 target, e.g. + `JAVA_HOME=/path/to/jdk-17 ./gradlew buildPlugin` +- Useful tasks: `buildPlugin` (builds the distribution), `runIde` (starts a sandbox IDE with the + plugin), `verifyPlugin` (plugin structure check) +- Lombok is used, so the Lombok plugin has to be enabled in the development IDE @@ -112,8 +118,10 @@ You can turn this plugin on and off with the keyboard shortcut `alt+u`, addition ## Roadmap - [ ] Multiline ErrorLabels (Inlays) -- [ ] Delay before adding problems -- [ ] Listen to changed width of editor and redraw labels if needed +- [ ] Delay before adding problems ([#60](https://github.com/0verEngineer/InlineProblems/issues/60), [#78](https://github.com/0verEngineer/InlineProblems/issues/78)) +- [ ] Listen to changed width of editor and redraw labels if needed ([#57](https://github.com/0verEngineer/InlineProblems/issues/57)) + +See the [open issues](https://github.com/0verEngineer/InlineProblems/issues) for the full list. @@ -144,7 +152,7 @@ Project Link: [https://github.com/0verEngineer/InlineProblems](https://github.co [issues-shield]: https://img.shields.io/github/issues/0verEngineer/InlineProblems.svg?style=for-the-badge [issues-url]: https://github.com/0verEngineer/InlineProblems/issues [license-shield]: https://img.shields.io/github/license/0verEngineer/InlineProblems.svg?style=for-the-badge -[license-url]: https://github.com/0verEngineer/InlineProblems/blob/master/LICENSE.txt +[license-url]: https://github.com/0verEngineer/InlineProblems/blob/main/LICENSE [plugin-url]: https://plugins.jetbrains.com/plugin/20789-inlineproblems [plugin-version-shield]: https://img.shields.io/jetbrains/plugin/v/20789-inlineproblems.svg?style=for-the-badge [plugin-downloads-shield]: https://img.shields.io/jetbrains/plugin/d/20789-inlineproblems.svg?style=for-the-badge From 19ef9d76ec3b8f98d1e6ddf374ba7b95e1b98644 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 21:10:28 +0200 Subject: [PATCH 20/46] Run the IntelliJ Plugin Verifier in CI again Commit 9a6304e ("Try to fix plugin verification task", 2025-04-10) replaced runPluginVerifier with verifyPlugin in the verify job. With gradle-intellij-plugin 1.x those are different tasks - verifyPlugin only validates the descriptors and the archive structure - so binary compatibility against newer IDE builds has not been checked since then. The verifier home dir property, its cache step and listProductsReleases were left behind as dead weight. The likely reason it was failing is the IDE list rather than the plugin: without an explicit ideVersions, the builds are derived from pluginSinceBuild through listProductsReleases, which with an open untilBuild currently resolves to 14 IDE builds. At 1.8 to 4.1 GB unpacked each that is far more than a runner has after cleanup. The verified build is therefore pinned in gradle.properties (pluginVerifierIdeVersions) and limited to the latest release. The lower bound needs no verifier run: the sources are compiled against platformVersion, so the compiler already guarantees the API exists there. What the verifier adds is catching API that disappeared in a newer IDE. failureLevel is set to the levels that mean real breakage - COMPATIBILITY_PROBLEMS, INVALID_PLUGIN, MISSING_DEPENDENCIES. The deprecation and internal API levels are deliberately not included, because this plugin knowingly builds on internal API and those levels would be permanently red without being actionable; they still show up in the report. The IDE cache key is now the pinned version instead of a hash of gradle.properties, which also holds pluginVersion and would have invalidated a multi gigabyte cache entry on every version bump. listProductsReleases is no longer called since nothing consumes it. Verified locally with the exact command the workflow runs: IU-253.28294.334 Compatible. 3 usages of deprecated API BUILD SUCCESSFUL No compatibility problems. Note that IC-2025.3 resolves to a build that identifies as IU: Community and Ultimate ship as one distribution from 2025.3 on, so the verifier checks against the Ultimate class set. That is harmless here because the plugin only depends on com.intellij.modules.platform. The reported deprecations are HighlightSeverity.INFO in ProblemManager.applyCustomSeverity, ActionUtil.invokeAction in InlineProblemLabel.mouseClicked and the overridden FileEditorManagerListener.fileOpenedSync. (cherry picked from commit 6f96ac9c7838e5d98fa61f2a14fbbca9eed7549a) --- .github/workflows/build.yml | 40 +++++++++++++++++-------------------- build.gradle.kts | 23 +++++++++++++++++++++ gradle.properties | 8 ++++++++ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3cc0812..10255b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ # - run 'test' and 'verifyPlugin' tasks, # - run Qodana inspections, # - run 'buildPlugin' task and prepare artifact for the further tests, +# - run 'runPluginVerifier' task, # - create a draft release. # # Workflow is triggered on push and pull_request events. @@ -31,6 +32,7 @@ jobs: version: ${{ steps.properties.outputs.version }} changelog: ${{ steps.properties.outputs.changelog }} pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} + pluginVerifierIdeVersions: ${{ steps.properties.outputs.pluginVerifierIdeVersions }} steps: # Free GitHub Actions Environment Disk Space @@ -69,16 +71,17 @@ jobs: PROPERTIES="$(./gradlew properties --console=plain -q)" VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" + IDE_VERSIONS="$(echo "$PROPERTIES" | grep "^pluginVerifierIdeVersions:" | cut -f2- -d ' ' | tr -d ' ')" CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT + echo "pluginVerifierIdeVersions=$IDE_VERSIONS" >> $GITHUB_OUTPUT echo "changelog<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier # Build plugin - name: Build plugin @@ -172,24 +175,13 @@ jobs: with: cache-default-branch-only: true - # Run the plugin structure verification. - # - # NOTE this job used to run the IntelliJ Plugin Verifier. Commit 9a6304e ("Try to fix plugin - # verification task", 2025-04-10) replaced `runPluginVerifier` with `verifyPlugin`, and - # with gradle-intellij-plugin 1.x those are two different tasks: - # verifyPlugin - validates the plugin.xml descriptors and the archive structure - # runPluginVerifier - the actual verifier, checks binary compatibility against IDE builds - # So binary incompatibilities with newer IDE versions are currently not caught in CI. The - # -Dplugin.verifier.home.dir property below, the cache step and the listProductsReleases - # call in the build job are all leftovers that only take effect with runPluginVerifier. - # Locally the verifier is still available through the "Run Verifications" run - # configuration in .run/, which does call runPluginVerifier. - # TODO decide whether to re-enable it. The commit message suggests it was failing back then, - # which is plausible with an open untilBuild plus the internal APIs this plugin uses - # (HighlightInfo, DocumentMarkupModel, FontInfo, ShowIntentionActionsHandler) - it likely - # needs an explicit ideVersions/failureLevel configuration rather than a plain re-add. - # Also note that the task was renamed when the IntelliJ Platform Gradle Plugin 2.x came - # out, so verify the task names again when migrating off 1.x. + # Run the plugin structure verification (verifyPlugin) and the IntelliJ Plugin Verifier + # (runPluginVerifier). With gradle-intellij-plugin 1.x these are two different tasks: + # verifyPlugin - validates the plugin.xml descriptors and the archive structure + # runPluginVerifier - checks binary compatibility against the IDE build pinned in + # gradle.properties (pluginVerifierIdeVersions) + # Note that the task names changed with the IntelliJ Platform Gradle Plugin 2.x, so they have to + # be revisited when migrating off 1.x. verify: name: Verify plugin needs: [ build ] @@ -220,16 +212,20 @@ jobs: with: gradle-home-cache-cleanup: true - # Cache Plugin Verifier IDEs + # Cache Plugin Verifier IDEs. Keyed on the pinned IDE build itself and not on + # gradle.properties, because that file also holds pluginVersion - keying on the file would + # invalidate a multi gigabyte cache entry on every version bump. One IDE is roughly 3.5 GB + # unpacked, so keep pluginVerifierIdeVersions short: GitHub allows 10 GB of caches per + # repository in total, shared with the Gradle cache. - name: Setup Plugin Verifier IDEs Cache uses: actions/cache@v4 with: path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides - key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} + key: plugin-verifier-${{ needs.build.outputs.pluginVerifierIdeVersions }} # Run Verify Plugin task and IntelliJ Plugin Verifier tool - name: Run Plugin Verification tasks - run: ./gradlew verifyPlugin -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} + run: ./gradlew verifyPlugin runPluginVerifier -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} # Collect Plugin Verifier Result - name: Collect Plugin Verifier Result diff --git a/build.gradle.kts b/build.gradle.kts index 0452015..77d9cc9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML +import org.jetbrains.intellij.tasks.RunPluginVerifierTask.FailureLevel fun properties(key: String) = project.findProperty(key).toString() @@ -129,4 +130,26 @@ tasks { maxHeapSize = "4g" minHeapSize = "2g" } + + runPluginVerifier { + /* Without an explicit list the IDE builds are derived from pluginSinceBuild through the + * listProductsReleases task. With an open untilBuild that currently resolves to 14 builds + * at 1.8 to 3.5 GB unpacked each - more than a CI runner has after cleanup, which is the + * likely reason the task was failing before. */ + ideVersions.set( + properties("pluginVerifierIdeVersions").split(',').map(String::trim).filter(String::isNotEmpty) + ) + + /* Only genuine breakage fails the build. The plugin knowingly builds on internal API + * (HighlightInfo, DocumentMarkupModel, FontInfo, ShowIntentionActionsHandler), so + * INTERNAL_API_USAGES and the deprecation levels would be permanently red without saying + * anything actionable. They are still listed in the report under build/reports/pluginVerifier. */ + failureLevel.set( + listOf( + FailureLevel.COMPATIBILITY_PROBLEMS, + FailureLevel.INVALID_PLUGIN, + FailureLevel.MISSING_DEPENDENCIES, + ) + ) + } } diff --git a/gradle.properties b/gradle.properties index 00fa3c2..ecc1f9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,14 @@ platformVersion = 2021.2.4 # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 platformPlugins = +# IDE build the IntelliJ Plugin Verifier checks against (runPluginVerifier). +# Only the latest release: the sources are compiled against platformVersion, so the lower bound is +# already covered by the compiler, and what the verifier adds is catching API that disappeared in a +# newer IDE. See the comment on the task in build.gradle.kts. +# Bump this when a new IDE version is released - `./gradlew listProductsReleases` prints the +# currently available builds, newest first. A comma separated list is supported. +pluginVerifierIdeVersions = IC-2025.3 + # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion = 8.10.2 From cc04d4ab71532ee2a6a45aca39a1ca8a78061c74 Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 21:31:16 +0200 Subject: [PATCH 21/46] Map additional info severities to INFORMATION, not INFO applyCustomSeverity assigned HighlightSeverity.INFO.myVal for the severities configured under "Additional severities" in the info section. INFO is deprecated in favour of WEAK_WARNING, and it carries the same value: INFORMATION = 10 INFO = 200 (deprecated, "use WEAK_WARNING") WEAK_WARNING = 200 WARNING = 300 ERROR = 400 Every classification in the plugin - shouldProblemBeIgnored, DrawDetails and SeverityUtil - dispatches on `severity >= WEAK_WARNING.myVal` before it reaches the INFORMATION branch. A problem remapped through the info list was therefore rendered as a weak warning: weak warning colors, and hidden or shown by the weak warning toggle instead of the info one. INFORMATION is the value the three classifications actually mean by "info", and it also removes the last deprecation warning from the build. --- .../java/org/overengineer/inlineproblems/ProblemManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java index b48eba2..f869cfe 100644 --- a/src/main/java/org/overengineer/inlineproblems/ProblemManager.java +++ b/src/main/java/org/overengineer/inlineproblems/ProblemManager.java @@ -129,7 +129,7 @@ public void applyCustomSeverity(InlineProblem problem) { for (int additionalSeverity : settingsState.getAdditionalInfoSeverities()) { if (additionalSeverity == severity) { - problem.setSeverity(HighlightSeverity.INFO.myVal); + problem.setSeverity(HighlightSeverity.INFORMATION.myVal); return; } } From 76ec0aac0a7c59612c40fc3dab6c7299835fd2be Mon Sep 17 00:00:00 2001 From: 0verEngineer Date: Fri, 4 Sep 2026 21:57:40 +0200 Subject: [PATCH 22/46] Raise the platform baseline to 2025.1 and migrate the build The plugin was compiled against the IntelliJ Platform 2021.2.4 with pluginSinceBuild 212.5, which kept every API added after 2021.2 out of reach and made the platform log a PluginException on every start of a recent IDE ("Migrate ProjectStartupActivity to ProjectActivity"). New baseline: platformVersion 2025.1.7, pluginSinceBuild 251, Java 21. 2025.1 is the earliest 2025 line, so it keeps the widest user base among the 2025 releases while unlocking everything this plugin actually wants - ProjectActivity (2023.1), AnAction.getActionUpdateThread (2022.3), the non-deprecated FileEditorManagerListener and ActionUtil overloads, and Java 16+ language features such as pattern matching for instanceof and records. Nothing in 2025.2 or 2025.3 adds anything for this plugin, and going higher would only cut users. That baseline forces two toolchain moves, both required rather than optional: - gradle-intellij-plugin 1.x refuses to build against 2024.2+ ("does not support building plugins against the IntelliJ Platform 2024.2+ (242+)"), so the build moves to the IntelliJ Platform Gradle Plugin 2.18.1. The whole build script is restructured accordingly: the intellij block becomes intellijPlatform with pluginConfiguration, signing, publishing and pluginVerification, the platform is declared as a dependency, and patchPluginXml is configured through pluginConfiguration instead. - The 2.x plugin from 2.14.0 on requires Gradle 9, so the wrapper moves from 8.10.2 to 9.7.1. Side effect worth having: Gradle 9.7.1 runs on current JDKs, so the build no longer needs a JAVA_HOME override - it works with a JDK 25 default. Task renames that come with the 2.x plugin, applied to the workflow and the run configuration: verifyPlugin (1.x) -> verifyPluginStructure runPluginVerifier (1.x) -> verifyPlugin listProductsReleases -> printProductsReleases The verifier no longer needs its own IDE cache either, because 2.x resolves the verified IDE as a regular Gradle dependency, so it lands in the Gradle cache the setup-gradle action already handles. pluginVerifierIdeVersions moves from IC-2025.3 to IU-2025.3: IntelliJ IDEA Community is not published separately any more starting with 253, the plugin resolution fails with "IC is no longer published since 2025.3". Community and Ultimate ship as one distribution now. Verified with the default JDK 25: - ./gradlew buildPlugin - BUILD SUCCESSFUL, since-build="251", no until-build - ./gradlew verifyPluginProjectConfiguration - no issues - ./gradlew verifyPluginStructure verifyPlugin - IU-253.28294.334 "Compatible. 2 usages of deprecated API", no compatibility problems The two remaining deprecations are addressed in the next commit, together with the rest of the modernization the new baseline allows. Also in this commit because the new tooling requires it: .intellijPlatform added to .gitignore (asked for by verifyPluginProjectConfiguration), qodana projectJDK 11 -> 21, and Java 21 in all workflows. The UI test workflow is marked with a TODO: runIdeForUiTests does not exist in 2.x and there are no UI tests to run anyway. --- .github/workflows/beta-release.yml | 4 +- .github/workflows/build.yml | 45 ++--- .github/workflows/release.yml | 4 +- .github/workflows/run-ui-tests.yml | 8 +- .gitignore | 1 + .run/Run Plugin Verification.run.xml | 3 +- README.md | 14 +- build.gradle.kts | 206 ++++++++++++----------- gradle.properties | 19 ++- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 47505 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 18 +- gradlew.bat | 36 ++-- qodana.yml | 2 +- 14 files changed, 179 insertions(+), 185 deletions(-) diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 84893d2..1d3fb36 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -31,12 +31,12 @@ jobs: sed -i "/pluginVersion = / s/ = .*/ = $VERSION_WITH_BUILDNUMBER/" gradle.properties echo "Version: $VERSION_WITH_BUILDNUMBER" - # Setup Java 11 environment for the next steps + # Setup Java 21 environment for the next steps - name: Setup Java uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Setup Gradle - name: Setup Gradle diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 10255b5..7e8c9ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,8 +31,6 @@ jobs: outputs: version: ${{ steps.properties.outputs.version }} changelog: ${{ steps.properties.outputs.changelog }} - pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} - pluginVerifierIdeVersions: ${{ steps.properties.outputs.pluginVerifierIdeVersions }} steps: # Free GitHub Actions Environment Disk Space @@ -50,12 +48,12 @@ jobs: - name: Gradle Wrapper Validation uses: gradle/wrapper-validation-action@v2 - # Setup Java 11 environment for the next steps + # Setup Java 21 environment for the next steps - name: Setup Java uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Setup Gradle - name: Setup Gradle @@ -71,13 +69,10 @@ jobs: PROPERTIES="$(./gradlew properties --console=plain -q)" VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" - IDE_VERSIONS="$(echo "$PROPERTIES" | grep "^pluginVerifierIdeVersions:" | cut -f2- -d ' ' | tr -d ' ')" CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" echo "version=$VERSION" >> $GITHUB_OUTPUT echo "name=$NAME" >> $GITHUB_OUTPUT - echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT - echo "pluginVerifierIdeVersions=$IDE_VERSIONS" >> $GITHUB_OUTPUT echo "changelog<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT @@ -120,7 +115,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Setup Gradle - name: Setup Gradle @@ -175,13 +170,13 @@ jobs: with: cache-default-branch-only: true - # Run the plugin structure verification (verifyPlugin) and the IntelliJ Plugin Verifier - # (runPluginVerifier). With gradle-intellij-plugin 1.x these are two different tasks: - # verifyPlugin - validates the plugin.xml descriptors and the archive structure - # runPluginVerifier - checks binary compatibility against the IDE build pinned in - # gradle.properties (pluginVerifierIdeVersions) - # Note that the task names changed with the IntelliJ Platform Gradle Plugin 2.x, so they have to - # be revisited when migrating off 1.x. + # Run the plugin structure verification and the IntelliJ Plugin Verifier. Note the task names, + # they were swapped by the move to the IntelliJ Platform Gradle Plugin 2.x: + # verifyPluginStructure - validates the plugin.xml descriptors and the archive structure + # (this was called verifyPlugin with the 1.x plugin) + # verifyPlugin - the IntelliJ Plugin Verifier, checks binary compatibility against + # the IDE build pinned in gradle.properties (pluginVerifierIdeVersions) + # (this was called runPluginVerifier with the 1.x plugin) verify: name: Verify plugin needs: [ build ] @@ -204,7 +199,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Setup Gradle - name: Setup Gradle @@ -212,20 +207,12 @@ jobs: with: gradle-home-cache-cleanup: true - # Cache Plugin Verifier IDEs. Keyed on the pinned IDE build itself and not on - # gradle.properties, because that file also holds pluginVersion - keying on the file would - # invalidate a multi gigabyte cache entry on every version bump. One IDE is roughly 3.5 GB - # unpacked, so keep pluginVerifierIdeVersions short: GitHub allows 10 GB of caches per - # repository in total, shared with the Gradle cache. - - name: Setup Plugin Verifier IDEs Cache - uses: actions/cache@v4 - with: - path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides - key: plugin-verifier-${{ needs.build.outputs.pluginVerifierIdeVersions }} - - # Run Verify Plugin task and IntelliJ Plugin Verifier tool + # No dedicated IDE cache: the 2.x plugin resolves the verified IDE as a regular Gradle + # dependency, so it lands in the Gradle cache that setup-gradle already handles. One IDE is + # roughly 4 GB, so keep pluginVerifierIdeVersions short - GitHub allows 10 GB of caches per + # repository in total. - name: Run Plugin Verification tasks - run: ./gradlew verifyPlugin runPluginVerifier -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} + run: ./gradlew verifyPluginStructure verifyPlugin # Collect Plugin Verifier Result - name: Collect Plugin Verifier Result diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab9dcdd..7749d06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,12 +24,12 @@ jobs: with: ref: ${{ github.event.release.tag_name }} - # Setup Java 11 environment for the next steps + # Setup Java 21 environment for the next steps - name: Setup Java uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Setup Gradle - name: Setup Gradle diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml index a3f9fbc..8aa6647 100644 --- a/.github/workflows/run-ui-tests.yml +++ b/.github/workflows/run-ui-tests.yml @@ -7,6 +7,10 @@ # # Workflow is triggered manually. +# TODO this workflow is broken since the move to the IntelliJ Platform Gradle Plugin 2.x: the +# runIdeForUiTests task does not exist any more, UI tests are driven by testIdeUi now. There +# are also no UI tests in the project, so the workflow never had anything to run. It is kept +# as a starting point, it only runs on manual dispatch. name: Run UI Tests on: workflow_dispatch @@ -35,12 +39,12 @@ jobs: - name: Fetch Sources uses: actions/checkout@v4 - # Setup Java 11 environment for the next steps + # Setup Java 21 environment for the next steps - name: Setup Java uses: actions/setup-java@v4 with: distribution: zulu - java-version: 11 + java-version: 21 # Run IDEA prepared for UI testing - name: Run IDE diff --git a/.gitignore b/.gitignore index 9d9789b..9654ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle .qodana +.intellijPlatform build ### Local only ### diff --git a/.run/Run Plugin Verification.run.xml b/.run/Run Plugin Verification.run.xml index 3a8d688..cab5204 100644 --- a/.run/Run Plugin Verification.run.xml +++ b/.run/Run Plugin Verification.run.xml @@ -11,7 +11,8 @@