Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Release with new features and bugfixes:

* https://github.com/devonfw/IDEasy/issues/2176[#2176]: Support 7z archive extraction
* https://github.com/devonfw/IDEasy/issues/2100[#2100]: Fix Python not available for Mac x64
* https://github.com/devonfw/IDEasy/issues/2142[#2142]: Move IDE-specific metadata (.idea, .vscode) out of workspace

* https://github.com/devonfw/IDEasy/issues/2134[#2134]: Add auto-completion for icd command

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/48?closed=1[milestone 2026.08.001].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.migration.v2025.Mig202502001;
import com.devonfw.tools.ide.migration.v2025.Mig202510001;
import com.devonfw.tools.ide.migration.v2026.Mig202608001;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.version.IdeVersion;
import com.devonfw.tools.ide.version.VersionIdentifier;
Expand All @@ -31,7 +32,7 @@ public class IdeMigrator implements IdeMigration {
public IdeMigrator() {

// migrations must be strictly in ascending order (from oldest to newest version)
this(List.of(new Mig202502001(), new Mig202510001()));
this(List.of(new Mig202502001(), new Mig202510001(), new Mig202608001()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.devonfw.tools.ide.migration.v2026;

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

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

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.migration.IdeVersionMigration;

/**
* Migration to 2026.08.001. Moves the VSCode user-data folder ({@code .vscode/.userdata}) out of each workspace into the dedicated
* {@code $IDE_HOME/.ide/vscode/«workspace»/config} folder so workspaces stay clean and independent of the IDE being used. See
* <a href="https://github.com/devonfw/IDEasy/issues/2142">#2142</a>.
*/
public class Mig202608001 extends IdeVersionMigration {

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

/**
* The constructor.
*/
public Mig202608001() {

super("2026.08.001");
}

@Override
public void run(IdeContext context) {

Path workspacesPath = context.getWorkspacesBasePath();
if (workspacesPath == null) {
return;
}
FileAccess fileAccess = context.getFileAccess();
Path vscodeMetaPath = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode");
List<Path> workspaces = fileAccess.listChildren(workspacesPath, Files::isDirectory);
for (Path workspace : workspaces) {
Path oldUserData = workspace.resolve(".vscode").resolve(".userdata");
if (fileAccess.isExpectedFolder(oldUserData)) {
Path target = vscodeMetaPath.resolve(workspace.getFileName().toString()).resolve("config");
if (Files.exists(target)) {
LOG.warn("Skipping migration of {} since target already exists: {}", oldUserData, target);
continue;
}
fileAccess.mkdirs(target.getParent());
fileAccess.move(oldUserData, target);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ public ToolInstallation install(ToolInstallRequest request) {
return super.install(request);
}

/**
* @return the {@link Path} to the IDE-specific metadata folder for the {@link IdeContext#getWorkspaceName() current workspace}, located at
* {@code $IDE_HOME/.ide/«ide»/«workspace»}. Unlike {@link 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.
*/
protected Path getIdeMetadataPath() {

return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(getName()).resolve(this.context.getWorkspaceName());
}

/**
* Configure (initialize or update) the workspace for this IDE using the templates from the settings.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, Lis
if (this.context.getSystemInfo().isWsl()) {
pc.withEnvVar("DONT_PROMPT_WSL_INSTALL", "1");
}
Path vsCodeConf = this.context.getWorkspacePath().resolve(".vscode/.userdata");
Path vsCodeConf = getIdeMetadataPath().resolve("config");
pc.addArg("--new-window");
pc.addArg("--user-data-dir=" + vsCodeConf);
Path vsCodeExtensionFolder = this.context.getIdeHome().resolve("plugins/vscode");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.devonfw.tools.ide.migration.v2026;

import java.nio.file.Path;

import org.junit.jupiter.api.Test;

import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeTestContext;
import com.devonfw.tools.ide.io.FileAccess;

/**
* Test of {@link Mig202608001}.
*/
class Mig202608001Test extends AbstractIdeContextTest {

/**
* Tests that an existing {@code .vscode/.userdata} folder is moved out of the workspace into {@code $IDE_HOME/.ide/vscode/«workspace»/config}.
*/
@Test
void testMovesVscodeUserDataOutOfWorkspace() {

// arrange
IdeTestContext context = newContext("vscode");
FileAccess fileAccess = context.getFileAccess();
Path workspace = context.getWorkspacePath();
Path oldUserData = workspace.resolve(".vscode").resolve(".userdata");
fileAccess.mkdirs(oldUserData);
fileAccess.writeFileContent("dummy", oldUserData.resolve("state.json"));
// act
new Mig202608001().run(context);
// assert
Path newConfig = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
assertThat(newConfig.resolve("state.json")).exists().hasContent("dummy");
assertThat(oldUserData).doesNotExist();
}

/**
* Tests that the migration is a no-op (and does not fail) when no {@code .vscode/.userdata} folder exists.
*/
@Test
void testDoesNothingWhenNoUserData() {

// arrange
IdeTestContext context = newContext("vscode");
Path vscodeMeta = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode");
// act
new Mig202608001().run(context);
// assert
assertThat(vscodeMeta).doesNotExist();
}

/**
* Tests that the migration skips a workspace whose target folder already exists, without failing and without overwriting the existing data.
*/
@Test
void testSkipsWhenTargetAlreadyExists() {

// arrange
IdeTestContext context = newContext("vscode");
FileAccess fileAccess = context.getFileAccess();
Path oldUserData = context.getWorkspacePath().resolve(".vscode").resolve(".userdata");
fileAccess.mkdirs(oldUserData);
fileAccess.writeFileContent("old", oldUserData.resolve("state.json"));
Path target = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
fileAccess.mkdirs(target);
fileAccess.writeFileContent("new", target.resolve("state.json"));
// act
new Mig202608001().run(context);
// assert
assertThat(oldUserData).exists();
assertThat(target.resolve("state.json")).exists().hasContent("new");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,20 @@ void testConfigureWorkspace() {
assertThat(workspace.resolve(".intellij/config/idea.key")).exists();
assertThat(workspace.resolve("user.properties")).exists().content().contains("ijversion=2023.3.3");
}

/**
* Tests that {@link IdeToolCommandlet#getIdeMetadataPath()} resolves to {@code $IDE_HOME/.ide/«ide»/«workspace»} instead of the workspace itself.
*/
@Test
void testGetIdeMetadataPath() {
// arrange
IdeContext context = newContext("intellij");
IdeToolCommandlet ide = context.getCommandletManager().getCommandlet(Intellij.class);
// act
Path metadataPath = ide.getIdeMetadataPath();
// assert
assertThat(metadataPath).startsWithRaw(context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE));
assertThat(metadataPath).endsWithRaw(Path.of("intellij", context.getWorkspaceName()));
assertThat(metadataPath).isNotEqualTo(context.getWorkspacePath());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.junit.jupiter.api.Test;

import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeTestContext;
import com.devonfw.tools.ide.context.ProcessContextTestImpl;
import com.devonfw.tools.ide.os.SystemInfoMock;
Expand Down Expand Up @@ -150,6 +151,26 @@ void testConfigureToolArgsDoesNotSetWslEnvVarOnNonWsl() {
assertThat(pc.getEnvVar("DONT_PROMPT_WSL_INSTALL")).isNull();
}

/**
* Tests that {@link Vscode#configureToolArgs(ProcessContext, ProcessMode, List)} points {@code --user-data-dir} to the IDE metadata folder
* ({@code $IDE_HOME/.ide/vscode/«workspace»/config}) instead of a {@code .vscode} folder inside the workspace.
*/
@Test
void testConfigureToolArgsUsesIdeMetadataPathForUserData() {

// arrange
IdeTestContext context = newContext(PROJECT_VSCODE);
context.setSystemInfo(SystemInfoMock.LINUX_X64);
Vscode commandlet = new Vscode(context);
ArgCapturingProcessContext pc = new ArgCapturingProcessContext(context);
// act
commandlet.configureToolArgs(pc, ProcessMode.DEFAULT, List.of());
// assert
Path expectedUserData = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
assertThat(pc.capturedArgs).contains("--user-data-dir=" + expectedUserData);
assertThat(pc.capturedArgs).noneMatch(arg -> arg.contains(".vscode"));
}

@Test
void testVscodiumInstall() {

Expand Down Expand Up @@ -258,6 +279,26 @@ String getEnvVar(String key) {
}
}

/**
* {@link ProcessContextTestImpl} subclass that captures the CLI arguments added via {@link #addArg(String)} for test assertions.
*/
private static class ArgCapturingProcessContext extends ProcessContextTestImpl {

private final List<String> capturedArgs = new ArrayList<>();

private ArgCapturingProcessContext(IdeTestContext context) {

super(context);
}

@Override
public ProcessContext addArg(String arg) {

this.capturedArgs.add(arg);
return super.addArg(arg);
}
}

private void checkVscodiumInstallation(IdeTestContext context) {

assertThat(context.getSoftwarePath().resolve("vscode/bin/codium.cmd")).exists().hasContent("@echo test for windows");
Expand Down
Loading