Skip to content

Commit 05b2ced

Browse files
committed
fix: avoid spurious 'restart terminal' prompts on window reload
VS Code shows a 'restart terminal to apply environment variable changes' prompt whenever an extension's EnvironmentVariableCollection differs from what is already applied to running terminals. The previous No-Config Debug activation flow called collection.clear() followed by replace() / append() unconditionally, which VS Code always interprets as a change even when the resulting values are byte-identical, so the prompt fired on every window reload. Refactor the activation to diff-aware helpers (applyReplaceIfChanged, applyAppendIfChanged, deleteIfPresent) that only write to the collection when the existing mutator's type, value, or options actually differ from what we want. The hot path on a normal reload becomes a sequence of no-op get() comparisons, so VS Code never observes a change and the prompt no longer appears. Additional notes: * Compare normalized EnvironmentVariableMutatorOptions (applyAtProcessCreation / applyAtShellIntegration) so future options changes still trigger a write. * When the Java Language Server cannot resolve a Java home, keep the previously stored VSCODE_JAVA_EXEC instead of deleting it, to avoid churn caused by transient startup failures. * Explicitly clean up legacy keys (currently JAVA_TOOL_OPTIONS) that older versions of this extension used to set but no longer manage. * Set a human-readable description on the collection so users can see the source in VS Code's environment variable UI. Fixes #1647 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9758e1d commit 05b2ced

3 files changed

Lines changed: 376 additions & 8 deletions

File tree

src/envVarSync.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as vscode from "vscode";
5+
6+
/**
7+
* Helpers that update a {@link vscode.EnvironmentVariableCollection} only when
8+
* the resulting mutation would actually change the collection.
9+
*
10+
* Background: VS Code shows a "Restart terminal to apply environment variable
11+
* changes" prompt whenever an extension's collection differs from what has
12+
* already been applied to running terminals. Calling `replace()` / `append()`
13+
* with the same value, or calling `clear()` followed by re-adding identical
14+
* entries, still counts as a change and re-triggers the prompt on every
15+
* window reload. See issue #1647.
16+
*
17+
* These helpers compare the existing mutator (type + value + options) against
18+
* the desired one and skip the write entirely when they match.
19+
*/
20+
21+
const DEFAULT_OPTIONS: Required<vscode.EnvironmentVariableMutatorOptions> = {
22+
applyAtProcessCreation: true,
23+
applyAtShellIntegration: false,
24+
};
25+
26+
function normalizeOptions(
27+
options?: vscode.EnvironmentVariableMutatorOptions,
28+
): Required<vscode.EnvironmentVariableMutatorOptions> {
29+
return { ...DEFAULT_OPTIONS, ...options };
30+
}
31+
32+
function sameOptions(
33+
existing: vscode.EnvironmentVariableMutator,
34+
desired?: vscode.EnvironmentVariableMutatorOptions,
35+
): boolean {
36+
const e = normalizeOptions(existing.options);
37+
const d = normalizeOptions(desired);
38+
return e.applyAtProcessCreation === d.applyAtProcessCreation
39+
&& e.applyAtShellIntegration === d.applyAtShellIntegration;
40+
}
41+
42+
/**
43+
* Calls `collection.replace(variable, value, options)` only when the existing
44+
* mutator (if any) does not already match.
45+
*
46+
* @returns `true` if the collection was actually written to.
47+
*/
48+
export function applyReplaceIfChanged(
49+
collection: vscode.EnvironmentVariableCollection,
50+
variable: string,
51+
value: string,
52+
options?: vscode.EnvironmentVariableMutatorOptions,
53+
): boolean {
54+
const existing = collection.get(variable);
55+
if (existing
56+
&& existing.type === vscode.EnvironmentVariableMutatorType.Replace
57+
&& existing.value === value
58+
&& sameOptions(existing, options)) {
59+
return false;
60+
}
61+
if (options) {
62+
collection.replace(variable, value, options);
63+
} else {
64+
collection.replace(variable, value);
65+
}
66+
return true;
67+
}
68+
69+
/**
70+
* Calls `collection.append(variable, value, options)` only when the existing
71+
* mutator (if any) does not already match.
72+
*
73+
* @returns `true` if the collection was actually written to.
74+
*/
75+
export function applyAppendIfChanged(
76+
collection: vscode.EnvironmentVariableCollection,
77+
variable: string,
78+
value: string,
79+
options?: vscode.EnvironmentVariableMutatorOptions,
80+
): boolean {
81+
const existing = collection.get(variable);
82+
if (existing
83+
&& existing.type === vscode.EnvironmentVariableMutatorType.Append
84+
&& existing.value === value
85+
&& sameOptions(existing, options)) {
86+
return false;
87+
}
88+
if (options) {
89+
collection.append(variable, value, options);
90+
} else {
91+
collection.append(variable, value);
92+
}
93+
return true;
94+
}
95+
96+
/**
97+
* Deletes the mutator for `variable` only when one is currently present.
98+
*
99+
* @returns `true` if a mutator was deleted.
100+
*/
101+
export function deleteIfPresent(
102+
collection: vscode.EnvironmentVariableCollection,
103+
variable: string,
104+
): boolean {
105+
if (collection.get(variable)) {
106+
collection.delete(variable);
107+
return true;
108+
}
109+
return false;
110+
}

src/noConfigDebugInit.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import * as vscode from 'vscode';
99
import { sendInfo, sendError } from "vscode-extension-telemetry-wrapper";
1010
import { getJavaHome } from "./utility";
1111
import { buildNoConfigPathAppendValue } from "./pathUtil";
12+
import { applyAppendIfChanged, applyReplaceIfChanged, deleteIfPresent } from "./envVarSync";
13+
14+
// Environment variables that older versions of this extension contributed but
15+
// no longer manage. They are cleaned up explicitly on activation so they do
16+
// not linger in the persistent EnvironmentVariableCollection. See issue #1647.
17+
const LEGACY_ENV_VARS = ["JAVA_TOOL_OPTIONS"];
18+
19+
const ENV_VAR_COLLECTION_DESCRIPTION = "Java No-Config Debug";
1220

1321
/**
1422
* Registers the configuration-less debugging setup for the extension.
@@ -21,7 +29,7 @@ import { buildNoConfigPathAppendValue } from "./pathUtil";
2129
*
2230
* Environment Variables:
2331
* - `VSCODE_JDWP_ADAPTER_ENDPOINTS`: Path to the file containing the debugger adapter endpoint.
24-
* - `JAVA_TOOL_OPTIONS`: JDWP configuration for automatic debugging.
32+
* - `VSCODE_JAVA_EXEC`: Path to the java executable from the Java Language Server (when available).
2533
* - `PATH`: Appends the path to the noConfigScripts directory.
2634
*/
2735
export async function registerNoConfigDebug(
@@ -69,30 +77,46 @@ export async function registerNoConfigDebug(
6977
}
7078
}
7179

72-
// clear the env var collection to remove any existing env vars
73-
collection.clear();
80+
// Surface a description in VS Code's environment variable UI so users can
81+
// see which extension is contributing these variables.
82+
if (collection.description !== ENV_VAR_COLLECTION_DESCRIPTION) {
83+
collection.description = ENV_VAR_COLLECTION_DESCRIPTION;
84+
}
85+
86+
// Remove any variables that older versions of this extension used to set
87+
// but no longer manage. Doing this idempotently (only when present) keeps
88+
// subsequent reloads from triggering the "restart terminal" prompt.
89+
for (const legacy of LEGACY_ENV_VARS) {
90+
deleteIfPresent(collection, legacy);
91+
}
7492

75-
// Add env var for VSCODE_JDWP_ADAPTER_ENDPOINTS
93+
// Apply our managed variables using diff-aware helpers. On a typical
94+
// window reload the values are unchanged and these calls are no-ops, so
95+
// VS Code does not prompt the user to restart their existing terminals.
96+
// See issue #1647.
97+
//
7698
// Note: We do NOT set JAVA_TOOL_OPTIONS globally to avoid affecting all Java processes
7799
// (javac, maven, gradle, language server, etc.). Instead, JAVA_TOOL_OPTIONS is set
78100
// only in the debugjava wrapper scripts (debugjava.ps1, debugjava.bat, debugjava)
79-
collection.replace('VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath);
101+
applyReplaceIfChanged(collection, 'VSCODE_JDWP_ADAPTER_ENDPOINTS', tempFilePath);
80102

81103
// Try to get Java executable from Java Language Server
82-
// This ensures we use the same Java version as the project is compiled with
104+
// This ensures we use the same Java version as the project is compiled with.
105+
// If detection fails or returns nothing, we deliberately keep any previously
106+
// set VSCODE_JAVA_EXEC to avoid churn from transient startup failures.
83107
try {
84108
const javaHome = await getJavaHome();
85109
if (javaHome) {
86110
const javaExec = path.join(javaHome, 'bin', 'java');
87-
collection.replace('VSCODE_JAVA_EXEC', javaExec);
111+
applyReplaceIfChanged(collection, 'VSCODE_JAVA_EXEC', javaExec);
88112
}
89113
} catch (error) {
90114
// If we can't get Java from Language Server, that's okay
91115
// The wrapper script will fall back to JAVA_HOME or PATH
92116
}
93117

94118
const noConfigScriptsDir = path.join(extPath, 'bundled', 'scripts', 'noConfigScripts');
95-
collection.append('PATH', buildNoConfigPathAppendValue(noConfigScriptsDir));
119+
applyAppendIfChanged(collection, 'PATH', buildNoConfigPathAppendValue(noConfigScriptsDir));
96120

97121
// create file system watcher for the debuggerAdapterEndpointFolder for when the communication port is written
98122
const fileSystemWatcher = vscode.workspace.createFileSystemWatcher(

0 commit comments

Comments
 (0)