Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The full list of changes for this release can be found in https://github.com/dev

== 2026.09.001

* https://github.com/devonfw/IDEasy/issues/2293[#2293]: Rework spyder to have same features as IdeToolCommandlets
Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/1525[#1525]: Document known issue and workaround for lombok plugin in Eclipse
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.devonfw.tools.ide.property.ToolProperty;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.tool.ToolCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginBasedCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginFeatures;

/**
* {@link Commandlet} to install a tool.
Expand Down Expand Up @@ -49,7 +49,7 @@ protected void doRun() {
ToolCommandlet commandlet = this.tool.getValue();
String plugin = this.plugin.getValue();

if (commandlet instanceof PluginBasedCommandlet cmd) {
if (commandlet instanceof PluginFeatures cmd) {
Step step = context.newStep("Install plugin: " + plugin);
step.run(() -> cmd.installPlugin(cmd.getPlugin(plugin), step));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.devonfw.tools.ide.property.PluginProperty;
import com.devonfw.tools.ide.property.ToolProperty;
import com.devonfw.tools.ide.tool.ToolCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginBasedCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginFeatures;

/**
* {@link Commandlet} to install a tool.
Expand Down Expand Up @@ -48,7 +48,7 @@ protected void doRun() {
ToolCommandlet commandlet = this.tool.getValue();
String plugin = this.plugin.getValue();

if (commandlet instanceof PluginBasedCommandlet cmd) {
if (commandlet instanceof PluginFeatures cmd) {
cmd.uninstallPlugin(cmd.getPlugin(plugin));
} else {
LOG.warn("Tool {} does not support plugins.", tool.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.devonfw.tools.ide.property.RepositoryProperty;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.tool.ToolCommandlet;
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
import com.devonfw.tools.ide.tool.ide.IdeFeatures;

/**
* {@link Commandlet} to setup one or multiple GIT repositories for development.
Expand Down Expand Up @@ -278,8 +278,8 @@ private void importRepository(RepositoryConfig repositoryConfig, Path repository
String displayName = (ide == null || ide.isBlank()) ? "<empty>" : "'" + ide + "'";
step.error("Cannot import repository '{}'. Required IDE '{}' not found. Please check your repository's imports configuration.", repositoryId,
displayName);
} else if (commandlet instanceof IdeToolCommandlet ideCommandlet) {
ideCommandlet.importRepository(repositoryPath);
} else if (commandlet instanceof IdeFeatures ideFeatures) {
ideFeatures.importRepository(repositoryPath);
} else {
step.error("Repository {} has import {} configured that is not an IDE!", repositoryId, ide);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public DirectoryMerger(IdeContext context) {
this.extension2mergerMap.put("launch", xmlMerger); // Eclipse specific
JsonMerger jsonMerger = new JsonMerger(context);
this.extension2mergerMap.put("json", jsonMerger);
IniMerger iniMerger = new IniMerger(context);
this.extension2mergerMap.put("ini", iniMerger);
TextMerger textMerger = new TextMerger(context);
this.extension2mergerMap.put("name", textMerger); // intellij specific
this.extension2mergerMap.put("editorconfig", textMerger);
Expand Down
189 changes: 189 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/merge/IniMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package com.devonfw.tools.ide.merge;

import java.nio.file.Files;
import java.nio.file.Path;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.ini.IniFile;
import com.devonfw.tools.ide.io.ini.IniFileImpl;
import com.devonfw.tools.ide.io.ini.IniSection;

/**
* Implementation of {@link FileMerger} for {@code .ini} files.
*/
public class IniMerger extends FileMerger {

private static final Logger LOG = LoggerFactory.getLogger(IniMerger.class);

/**
* The constructor.
*
* @param context the {@link #context}.
*/
public IniMerger(IdeContext context) {
super(context);
}

@Override
protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) {

FileAccess fileAccess = this.context.getFileAccess();
IniFile mergedIni = new IniFileImpl();
boolean updateFileExists = Files.exists(update);
Path template = setup;
if (Files.exists(workspace)) {
if (!updateFileExists) {
LOG.trace("Nothing to do as update file does not exist: {}", update);
return; // nothing to do ...
}
fileAccess.readIniFile(workspace, mergedIni);
} else if (Files.exists(setup)) {
fileAccess.readIniFile(setup, mergedIni);
}
if (updateFileExists) {
IniFile updateIni = new IniFileImpl();
fileAccess.readIniFile(update, updateIni);
mergeIniInto(updateIni, mergedIni);
template = update;
}

resolve(mergedIni, resolver, template.toString());
fileAccess.writeIniFile(mergedIni, workspace, true);
LOG.trace("Saved merged ini to: {}", workspace);
}

/**
* Merge the properties from {@code source} into {@code target}. Keys that exist in both: target gets overwritten with source. Keys that exist only in target:
* preserved (user modifications).
*
* @param source the source INI (update template).
* @param target the target INI (workspace or setup base).
*/
private void mergeIniInto(IniFile source, IniFile target) {
for (String sectionName : source.getSectionNames()) {
IniSection srcSection = source.getSection(sectionName);
IniSection tgtSection = target.getOrCreateSection(sectionName);
for (String key : srcSection.getPropertyKeys()) {
tgtSection.setProperty(key, srcSection.getPropertyValue(key));
}
}
IniSection srcInitial = source.getInitialSection();
IniSection tgtInitial = target.getOrCreateSection("");
for (String key : srcInitial.getPropertyKeys()) {
tgtInitial.setProperty(key, srcInitial.getPropertyValue(key));
}
}

private void resolve(IniFile iniFile, EnvironmentVariables resolver, String src) {

for (String sectionName : iniFile.getSectionNames()) {
IniSection section = iniFile.getSection(sectionName);
for (String key : section.getPropertyKeys()) {
String value = section.getPropertyValue(key);
String resolved = resolver.resolve(value, src, this.legacySupport);
section.setProperty(key, resolved);
}
}
IniSection initial = iniFile.getInitialSection();
for (String key : initial.getPropertyKeys()) {
String value = initial.getPropertyValue(key);
String resolved = resolver.resolve(value, src, this.legacySupport);
initial.setProperty(key, resolved);
}
}

@Override
public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) {

if (!Files.exists(workspace)) {
LOG.trace("Workspace file does not exist: {}", workspace);
return;
}
if (!Files.exists(update)) {
LOG.trace("Update file does not exist: {}", update);
return;
}
Object src = workspace.getFileName();
FileAccess fileAccess = this.context.getFileAccess();
IniFile updateIni = fileAccess.readIniFile(update);
IniFile workspaceIni = fileAccess.readIniFile(workspace);
IniFile mergedIni = new IniFileImpl();
copyIniInto(updateIni, mergedIni);
boolean updated = false;

for (String sectionName : workspaceIni.getSectionNames()) {
IniSection wsSection = workspaceIni.getSection(sectionName);
IniSection mergedSection = mergedIni.getOrCreateSection(sectionName);
for (String key : wsSection.getPropertyKeys()) {
String wsValue = wsSection.getPropertyValue(key);
String updateValue = mergedSection.getPropertyValue(key);
if ((updateValue != null) || addNewProperties) {
String updateValueResolved = updateValue != null
? variables.resolve(updateValue, src, this.legacySupport)
: null;
if (!wsValue.equals(updateValueResolved)) {
String wsValueInverseResolved = variables.inverseResolve(wsValue, src);
mergedSection.setProperty(key, wsValueInverseResolved);
updated = true;
}
}
}
}

IniSection wsInitial = workspaceIni.getInitialSection();
IniSection mergedInitial = mergedIni.getOrCreateSection("");
for (String key : wsInitial.getPropertyKeys()) {
String wsValue = wsInitial.getPropertyValue(key);
String updateValue = mergedInitial.getPropertyValue(key);
if ((updateValue != null) || addNewProperties) {
String updateValueResolved = updateValue != null
? variables.resolve(updateValue, src, this.legacySupport)
: null;
if (!wsValue.equals(updateValueResolved)) {
String wsValueInverseResolved = variables.inverseResolve(wsValue, src);
mergedInitial.setProperty(key, wsValueInverseResolved);
updated = true;
}
}
}

if (updated) {
fileAccess.writeIniFile(mergedIni, update, true);
LOG.debug("Saved changes from: {} to: {}", workspace.getFileName(), update);
} else {
LOG.trace("No changes for: {}", update);
}
}

@Override
protected boolean doUpgrade(Path workspaceFile) throws Exception {

return doUpgradeTextContent(workspaceFile);
}

/**
* Copy all sections and properties from source INI to target INI.
*
* @param src the source INI.
* @param tgt the target INI.
*/
private void copyIniInto(IniFile src, IniFile tgt) {
for (String sectionName : src.getSectionNames()) {
IniSection srcSection = src.getSection(sectionName);
IniSection tgtSection = tgt.getOrCreateSection(sectionName);
for (String key : srcSection.getPropertyKeys()) {
tgtSection.setProperty(key, srcSection.getPropertyValue(key));
}
}
IniSection srcInit = src.getInitialSection();
IniSection tgtInit = tgt.getOrCreateSection("");
for (String key : srcInit.getPropertyKeys()) {
tgtInit.setProperty(key, srcInit.getPropertyValue(key));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.tool.ToolCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginBasedCommandlet;
import com.devonfw.tools.ide.tool.plugin.PluginFeatures;
import com.devonfw.tools.ide.tool.plugin.ToolPluginDescriptor;
import com.devonfw.tools.ide.tool.plugin.ToolPlugins;
import com.devonfw.tools.ide.validation.PropertyValidator;

/**
* {@link Property} representing the plugin of a {@link PluginBasedCommandlet}.
* {@link Property} representing the plugin of a {@link PluginFeatures tool that supports plugins}.
*/
public class PluginProperty extends Property<String> {

Expand Down Expand Up @@ -56,7 +56,7 @@ public String parse(String valueAsString, IdeContext context) {
protected void completeValue(String arg, IdeContext context, Commandlet commandlet, CompletionCandidateCollector collector) {

ToolCommandlet cmd = commandlet.getToolForCompletion();
if (cmd instanceof PluginBasedCommandlet pbc) {
if (cmd instanceof PluginFeatures pbc) {
ToolPlugins plugins = pbc.getPlugins();
for (ToolPluginDescriptor pluginDescriptor : plugins.getPlugins()) {
if (pluginDescriptor.name().toLowerCase().startsWith(arg.toLowerCase())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, Lis
}

@Override
protected boolean isPluginUrlNeeded() {
public boolean isPluginUrlNeeded() {

return true;
}
Expand Down
32 changes: 32 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/tool/ide/IdeFeatures.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.devonfw.tools.ide.tool.ide;

import java.nio.file.Path;

/**
* Interface for IDE-specific features that are independent of the installation mechanism (binary vs. package manager).
* <p>
* This allows tools installed via package managers (like pip for Spyder) to still benefit from IDEasy's IDE features such as workspace configuration, metadata
* management, and repository import.
*/
public interface IdeFeatures {

/**
* Configures (initializes or updates) the workspace for this IDE using the templates from the settings.
*/
void configureWorkspace();

/**
* @return the {@link Path} to the IDE-specific metadata folder for the current workspace, located at {@code $IDE_HOME/.ide/«toolName»/«workspace»}. Unlike
* {@link com.devonfw.tools.ide.context.IdeContext#getWorkspacePath() the workspace path} (which holds the projects to open), this folder keeps
* IDE-specific metadata (e.g. {@code .vmoptions} or {@code *.properties} files) out of the workspace so it stays clean and independent of the IDE being
* used.
*/
Path getIdeMetadataPath();

/**
* Imports the repository specified by the given {@link Path} into the IDE managed by this {@link IdeFeatures}.
*
* @param repositoryPath the {@link Path} to the repository directory to import.
*/
void importRepository(Path repositoryPath);
}
Loading