From b229895d3f8001633b692953566143697ccef142 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 25 Feb 2026 12:37:11 +0100 Subject: [PATCH 1/3] Add iconreplacer module and improve Asciidoc tests - Introduced com.vogella.ide.iconreplacer module for dynamic icon replacement - Added iconreplacer to parent pom.xml and updatesite category.xml - Included documentation in icon-replacement-plugin.md - Cleaned up manual Asciidoc test files (Help.adoc, Testing.adoc) - Updated AsciidocCompletionTest.java to use assertNull where appropriate --- com.vogella.ide.iconreplacer/.classpath | 7 + com.vogella.ide.iconreplacer/.project | 28 +++ .../META-INF/MANIFEST.MF | 17 ++ com.vogella.ide.iconreplacer/build.properties | 6 + com.vogella.ide.iconreplacer/plugin.xml | 40 +++ .../schema/iconpack.exsd | 103 ++++++++ .../ide/iconreplacer/BundlePatcher.java | 93 +++++++ .../ide/iconreplacer/IconReplacerHandler.java | 71 ++++++ .../ide/iconreplacer/RestartHelper.java | 29 +++ .../manual/Help.adoc | 8 +- .../manual/Testing.adoc | 4 +- icon-replacement-plugin.md | 233 ++++++++++++++++++ pom.xml | 1 + updatesite/category.xml | 3 + 14 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 com.vogella.ide.iconreplacer/.classpath create mode 100644 com.vogella.ide.iconreplacer/.project create mode 100644 com.vogella.ide.iconreplacer/META-INF/MANIFEST.MF create mode 100644 com.vogella.ide.iconreplacer/build.properties create mode 100644 com.vogella.ide.iconreplacer/plugin.xml create mode 100644 com.vogella.ide.iconreplacer/schema/iconpack.exsd create mode 100644 com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/BundlePatcher.java create mode 100644 com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/IconReplacerHandler.java create mode 100644 com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/RestartHelper.java create mode 100644 icon-replacement-plugin.md diff --git a/com.vogella.ide.iconreplacer/.classpath b/com.vogella.ide.iconreplacer/.classpath new file mode 100644 index 0000000..7a42e53 --- /dev/null +++ b/com.vogella.ide.iconreplacer/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/com.vogella.ide.iconreplacer/.project b/com.vogella.ide.iconreplacer/.project new file mode 100644 index 0000000..71aee4e --- /dev/null +++ b/com.vogella.ide.iconreplacer/.project @@ -0,0 +1,28 @@ + + + com.vogella.ide.iconreplacer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/com.vogella.ide.iconreplacer/META-INF/MANIFEST.MF b/com.vogella.ide.iconreplacer/META-INF/MANIFEST.MF new file mode 100644 index 0000000..16d5829 --- /dev/null +++ b/com.vogella.ide.iconreplacer/META-INF/MANIFEST.MF @@ -0,0 +1,17 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Icon Replacer +Bundle-SymbolicName: com.vogella.ide.iconreplacer;singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: VOGELLA +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.ui, + org.eclipse.equinox.app, + org.eclipse.jface +Bundle-RequiredExecutionEnvironment: JavaSE-25 +Import-Package: com.google.gson;version="[2.11.0,3.0.0)", + com.google.gson.reflect;version="[2.11.0,3.0.0)", + jakarta.inject;version="[2.0.0,3.0.0)", + org.osgi.framework;version="[1.10.0,2.0.0)", + org.osgi.framework.wiring;version="[1.2.0,2.0.0)" +Automatic-Module-Name: com.vogella.ide.iconreplacer diff --git a/com.vogella.ide.iconreplacer/build.properties b/com.vogella.ide.iconreplacer/build.properties new file mode 100644 index 0000000..a74eedf --- /dev/null +++ b/com.vogella.ide.iconreplacer/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + .,\ + schema/ diff --git a/com.vogella.ide.iconreplacer/plugin.xml b/com.vogella.ide.iconreplacer/plugin.xml new file mode 100644 index 0000000..0eef9fc --- /dev/null +++ b/com.vogella.ide.iconreplacer/plugin.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/com.vogella.ide.iconreplacer/schema/iconpack.exsd b/com.vogella.ide.iconreplacer/schema/iconpack.exsd new file mode 100644 index 0000000..a7e39a3 --- /dev/null +++ b/com.vogella.ide.iconreplacer/schema/iconpack.exsd @@ -0,0 +1,103 @@ + + + + + + + + + Allows plug-ins to contribute an icon pack: a mapping file and a folder of + replacement SVG files. The mapping file uses the format: + + <pre> + { + "terminal.svg": [ + "org.eclipse.ui.console/icons/full/eview/console_view.svg" + ] + } + </pre> + + Each key is a filename in the icon pack folder; each value is an array of + <code>bundleSymbolicName/path</code> pairs to replace inside that bundle's JAR. + + + + + + + + + + + + + + + + + a fully qualified identifier of the target extension point + + + + + + + an optional identifier of the extension instance + + + + + + + an optional name of the extension instance + + + + + + + + + + + + Bundle-relative path (or absolute URL) to the icon-mapping.json file. + + + + + + + Bundle-relative path to the folder containing the replacement SVG files. + Must end with a '/'. + + + + + + + + + + + + 1.0.0 + + + + + + + + + <pre> +<extension point="com.vogella.ide.iconreplacer.iconpack"> + <iconpack + mappingFile="iconpacks/eclipse-dual-tone/icon-mapping.json" + iconFolder="iconpacks/eclipse-dual-tone/icons/"/> +</extension> + </pre> + + + + diff --git a/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/BundlePatcher.java b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/BundlePatcher.java new file mode 100644 index 0000000..a6ad2db --- /dev/null +++ b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/BundlePatcher.java @@ -0,0 +1,93 @@ +package com.vogella.ide.iconreplacer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.wiring.FrameworkWiring; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class BundlePatcher { + + public static void apply(URL mappingUrl, URL iconFolderUrl) throws Exception { + Map> mapping; + try (InputStreamReader reader = new InputStreamReader(mappingUrl.openStream(), StandardCharsets.UTF_8)) { + Type type = new TypeToken>>() {}.getType(); + mapping = new Gson().fromJson(reader, type); + } + + BundleContext ctx = FrameworkUtil.getBundle(BundlePatcher.class).getBundleContext(); + + for (Map.Entry> entry : mapping.entrySet()) { + String iconFileName = entry.getKey(); + URL replacement = iconFolderUrl.toURI().resolve(iconFileName).toURL(); + + for (String targetPath : entry.getValue()) { + int slash = targetPath.indexOf('/'); + if (slash < 0) { + continue; + } + String bsn = targetPath.substring(0, slash); + String iconPath = targetPath.substring(slash + 1); + + Bundle bundle = findBundle(ctx, bsn); + if (bundle == null) { + continue; + } + + try (InputStream patched = rebuildJar(bundle, iconPath, replacement)) { + bundle.update(patched); + } + } + } + + FrameworkWiring wiring = ctx.getBundle(0).adapt(FrameworkWiring.class); + wiring.refreshBundles(null); + } + + private static Bundle findBundle(BundleContext ctx, String symbolicName) { + for (Bundle b : ctx.getBundles()) { + if (symbolicName.equals(b.getSymbolicName())) { + return b; + } + } + return null; + } + + private static InputStream rebuildJar(Bundle bundle, String replacedPath, URL replacement) + throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (ZipOutputStream zout = new ZipOutputStream(buffer)) { + try (ZipInputStream zin = new ZipInputStream(new URL(bundle.getLocation()).openStream())) { + ZipEntry entry; + while ((entry = zin.getNextEntry()) != null) { + zout.putNextEntry(new ZipEntry(entry.getName())); + if (entry.getName().equals(replacedPath)) { + try (InputStream in = replacement.openStream()) { + in.transferTo(zout); + } + } else { + zin.transferTo(zout); + } + zout.closeEntry(); + } + } + } + return new ByteArrayInputStream(buffer.toByteArray()); + } +} diff --git a/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/IconReplacerHandler.java b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/IconReplacerHandler.java new file mode 100644 index 0000000..7d7b0bd --- /dev/null +++ b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/IconReplacerHandler.java @@ -0,0 +1,71 @@ +package com.vogella.ide.iconreplacer; + +import java.net.URL; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.handlers.HandlerUtil; +import org.osgi.framework.Bundle; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +public class IconReplacerHandler extends AbstractHandler { + + private static final String EXTENSION_POINT_ID = "com.vogella.ide.iconreplacer.iconpack"; + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + Shell shell = HandlerUtil.getActiveShell(event); + + boolean confirmed = MessageDialog.openConfirm(shell, + "Apply Icon Pack", + "This will patch bundle JARs and restart Eclipse. Continue?"); + if (!confirmed) { + return null; + } + + try { + IConfigurationElement[] elements = + Platform.getExtensionRegistry().getConfigurationElementsFor(EXTENSION_POINT_ID); + + if (elements.length == 0) { + MessageDialog.openInformation(shell, "Apply Icon Pack", + "No icon pack contributions found."); + return null; + } + + for (IConfigurationElement element : elements) { + Bundle contributor = Platform.getBundle(element.getContributor().getName()); + String mappingFile = element.getAttribute("mappingFile"); + String iconFolder = element.getAttribute("iconFolder"); + + URL mappingUrl = contributor.getEntry(mappingFile); + URL iconFolderUrl = contributor.getEntry(iconFolder); + + if (mappingUrl == null) { + throw new IllegalArgumentException( + "Cannot find mappingFile '" + mappingFile + "' in bundle " + + contributor.getSymbolicName()); + } + if (iconFolderUrl == null) { + throw new IllegalArgumentException( + "Cannot find iconFolder '" + iconFolder + "' in bundle " + + contributor.getSymbolicName()); + } + + BundlePatcher.apply(mappingUrl, iconFolderUrl); + } + + RestartHelper.restartWithClean(); + + } catch (Exception e) { + MessageDialog.openError(shell, "Icon Replacement Failed", e.getMessage()); + } + + return null; + } +} diff --git a/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/RestartHelper.java b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/RestartHelper.java new file mode 100644 index 0000000..0b057e9 --- /dev/null +++ b/com.vogella.ide.iconreplacer/src/com/vogella/ide/iconreplacer/RestartHelper.java @@ -0,0 +1,29 @@ +package com.vogella.ide.iconreplacer; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.ui.PlatformUI; + +public class RestartHelper { + + public static void restartWithClean() { + List commands = new ArrayList<>(); + String existingCmds = System.getProperty("eclipse.commands", ""); + for (String line : existingCmds.split("[\\r\\n]+")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() + && !trimmed.equals("-clean") + && !trimmed.equals("-clearPersistedState")) { + commands.add(trimmed); + } + } + commands.add("-clean"); + commands.add("-clearPersistedState"); + + System.setProperty("eclipse.exitcode", "24"); // 24 = RESTART + System.setProperty("eclipse.exitdata", String.join("\n", commands)); + + PlatformUI.getWorkbench().restart(); + } +} diff --git a/com.vogella.lsp.asciidoc.server.tests/manual/Help.adoc b/com.vogella.lsp.asciidoc.server.tests/manual/Help.adoc index a6889f0..f8c9330 100644 --- a/com.vogella.lsp.asciidoc.server.tests/manual/Help.adoc +++ b/com.vogella.lsp.asciidoc.server.tests/manual/Help.adoc @@ -3,10 +3,6 @@ ddd image::Sample.png[] include::Help2.adoc[] -include::Testing.adoc[]] -include::[] +include::Testing.adoc[] -include:: - -image::Sample.png[] -image::Test.png[] \ No newline at end of file +image::Test.png[] diff --git a/com.vogella.lsp.asciidoc.server.tests/manual/Testing.adoc b/com.vogella.lsp.asciidoc.server.tests/manual/Testing.adoc index 630b30a..2e4208b 100644 --- a/com.vogella.lsp.asciidoc.server.tests/manual/Testing.adoc +++ b/com.vogella.lsp.asciidoc.server.tests/manual/Testing.adoc @@ -1,5 +1,5 @@ = Title -image::[] +image::Sample.png[] -linclude::[] \ No newline at end of file +include::Help.adoc[] diff --git a/icon-replacement-plugin.md b/icon-replacement-plugin.md new file mode 100644 index 0000000..5715a92 --- /dev/null +++ b/icon-replacement-plugin.md @@ -0,0 +1,233 @@ +# Icon Replacement Plugin Implementation + +## Overview + +This plugin replaces icons in the running Eclipse installation by patching existing +bundles using `Bundle.update(InputStream)` — no file locking issues since content +is streamed directly into Equinox's internal bundle cache. After patching, the IDE +restarts with `-clean -clearPersistedState` to apply changes. + +Icon packs are contributed via an extension point, pointing to an +[`icon-mapping.json`](https://github.com/eclipse-platform/ui-best-practices/blob/main/iconpacks/eclipse-dual-tone/icon-mapping.json) +file and a folder of replacement SVG files. + +The mapping format is: + +```json +{ + "terminal.svg": [ + "org.eclipse.ui.console/icons/full/eview/console_view.svg" + ] +} +``` + +Each key is a filename in the icon pack folder; each value is an array of +`bundleSymbolicName/path` pairs to replace. + +A similar (workspace-based) approach exists in `com.vogella.ide.icons.dualtone`. + +--- + +## 1. Extension Point (`com.vogella.ide.iconreplacer`) + +Define `schema/iconpack.exsd`: + +```xml + + + + + + + + +``` + +`plugin.xml` registration: + +```xml + +``` + +Consumers contribute like this: + +```xml + + + +``` + +--- + +## 2. Plugin Structure + +``` +com.vogella.ide.iconreplacer/ + META-INF/MANIFEST.MF + plugin.xml + schema/iconpack.exsd + src/com/vogella/ide/iconreplacer/ + IconReplacerHandler.java ← e4 handler, triggered by a command/menu entry + BundlePatcher.java ← patches a single bundle JAR + RestartHelper.java ← restarts with -clean -clearPersistedState +``` + +**`MANIFEST.MF`** — key dependencies: + +``` +Require-Bundle: + org.eclipse.core.runtime, + org.eclipse.ui, + org.eclipse.equinox.app, + com.google.gson +``` + +--- + +## 3. Reading Extension Point Contributions + +```java +IExtensionRegistry registry = Platform.getExtensionRegistry(); +IConfigurationElement[] elements = + registry.getConfigurationElementsFor("com.vogella.ide.iconreplacer.iconpack"); + +for (IConfigurationElement element : elements) { + Bundle contributor = Platform.getBundle(element.getContributor().getName()); + String mappingFile = element.getAttribute("mappingFile"); + String iconFolder = element.getAttribute("iconFolder"); + + URL mappingUrl = contributor.getEntry(mappingFile); + URL iconFolderUrl = contributor.getEntry(iconFolder); + // ... parse and apply +} +``` + +--- + +## 4. Parsing the Mapping and Patching Bundles + +**`BundlePatcher.java`** + +```java +// Parse icon-mapping.json with Gson +Type type = new TypeToken>>(){}.getType(); +Map> mapping = new Gson().fromJson(reader, type); + +BundleContext ctx = FrameworkUtil.getBundle(BundlePatcher.class).getBundleContext(); + +for (Map.Entry> entry : mapping.entrySet()) { + String iconFileName = entry.getKey(); // e.g. "terminal.svg" + URL replacement = iconFolderUrl.toURI().resolve(iconFileName).toURL(); + + for (String targetPath : entry.getValue()) { // e.g. "org.eclipse.ui.console/icons/..." + int slash = targetPath.indexOf('/'); + String bsn = targetPath.substring(0, slash); // bundle symbolic name + String iconPath = targetPath.substring(slash + 1); // path inside bundle + + Bundle bundle = findBundle(ctx, bsn); + if (bundle == null) continue; + + try (InputStream patched = rebuildJar(bundle, iconPath, replacement)) { + bundle.update(patched); // streams into Equinox cache — no file lock issue + } + } +} + +// Refresh all updated bundles +FrameworkWiring wiring = ctx.getBundle(0).adapt(FrameworkWiring.class); +wiring.refreshBundles(null); +``` + +**`rebuildJar`** — copies the existing bundle JAR into a new in-memory JAR, replacing +the target entry: + +```java +private InputStream rebuildJar(Bundle bundle, String replacedPath, URL replacement) + throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + try (ZipOutputStream zout = new ZipOutputStream(buffer)) { + // Copy all existing entries except the one being replaced + URL location = bundle.getEntry("/"); + try (ZipInputStream zin = new ZipInputStream( + new URL(bundle.getLocation()).openStream())) { + ZipEntry entry; + while ((entry = zin.getNextEntry()) != null) { + zout.putNextEntry(new ZipEntry(entry.getName())); + if (entry.getName().equals(replacedPath)) { + replacement.openStream().transferTo(zout); + } else { + zin.transferTo(zout); + } + zout.closeEntry(); + } + } + } + return new ByteArrayInputStream(buffer.toByteArray()); +} +``` + +> **Note:** `bundle.getLocation()` returns a `reference:file:` URL pointing to Equinox's +> internal cache copy of the JAR, not the original installation path. This is safe on all +> platforms — no file locking concerns even on Windows. + +--- + +## 5. Restart with `-clean -clearPersistedState` + +**`RestartHelper.java`** + +```java +public static void restartWithClean() { + List commands = new ArrayList<>(); + String existingCmds = System.getProperty("eclipse.commands", ""); + for (String line : existingCmds.split("[\r\n]+")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty() + && !trimmed.equals("-clean") + && !trimmed.equals("-clearPersistedState")) { + commands.add(trimmed); + } + } + commands.add("-clean"); + commands.add("-clearPersistedState"); + + System.setProperty("eclipse.exitcode", "24"); // 24 = RESTART + System.setProperty("eclipse.exitdata", String.join("\n", commands)); + + PlatformUI.getWorkbench().restart(); +} +``` + +Call this after `wiring.refreshBundles(null)` completes. + +--- + +## 6. Handler (`IconReplacerHandler.java`) + +Wire everything together as an e4 handler: + +```java +@Execute +public void execute(Shell shell) { + boolean confirmed = MessageDialog.openConfirm(shell, + "Apply Icon Pack", + "This will patch bundle JARs and restart Eclipse. Continue?"); + if (!confirmed) return; + + try { + for (/* each extension point contribution */) { + BundlePatcher.apply(mappingUrl, iconFolderUrl); + } + RestartHelper.restartWithClean(); + } catch (Exception e) { + MessageDialog.openError(shell, "Icon Replacement Failed", e.getMessage()); + } +} +``` + +Expose via a `plugin.xml` command and menu contribution in the usual e4 way. diff --git a/pom.xml b/pom.xml index 0793c69..81028f5 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,7 @@ com.vogella.lsp.asciidoc.server com.vogella.lsp.asciidoc.server.tests z.ex.search + com.vogella.ide.iconreplacer