Skip to content

Commit 5869bdd

Browse files
committed
fix: restore manually-added projects after Save Workspace As
1 parent 5e143bc commit 5869bdd

4 files changed

Lines changed: 173 additions & 25 deletions

File tree

src/extension.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
4545
item.tooltip = localize("liberty.ls.starting");
4646
toggleItem(window.activeTextEditor, item);
4747

48+
// Run after getJavaExtensionAPI so VS Code APIs (findFiles etc.) are ready,
49+
// but before startLangServer so we don't wait for LS startup.
50+
handleWorkspaceSaveInProgress(context).catch(err => console.error('[handleWorkspaceSaveInProgress] uncaught error:', err));
51+
4852
resolveLclsRequirements(api).then().catch((error => {
4953
window.showErrorMessage(error.message, error.label).then((selection) => {
5054
if (error.label && error.label === selection && error.openUrl) {
@@ -60,7 +64,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
6064
item.text = localize("liberty.ls.thumbs.up");
6165
item.tooltip = localize("liberty.ls.started");
6266
toggleItem(window.activeTextEditor, item);
63-
handleWorkspaceSaveInProgress(context);
6467
registerCommands(context);
6568
}, (error: any) => {
6669
console.log("Liberty client was not ready. Did not initialize");
@@ -107,6 +110,10 @@ function registerCommands(context: ExtensionContext) {
107110
if (vscode.workspace.workspaceFolders !== undefined) {
108111
registerFileWatcher(projectProvider);
109112
vscode.window.registerTreeDataProvider("liberty-dev", projectProvider);
113+
// Re-run refresh so that any project written to workspaceState by
114+
// handleWorkspaceSaveInProgress (which ran before the tree was registered)
115+
// is picked up and displayed immediately.
116+
projectProvider.refresh();
110117
}
111118

112119
context.subscriptions.push(
@@ -303,13 +310,21 @@ async function getJavaExtensionAPI(): Promise<JavaExtensionAPI> {
303310
return Promise.resolve(api);
304311
}
305312

306-
function handleWorkspaceSaveInProgress(context: vscode.ExtensionContext) {
307-
let projectProvider = getProjectProvider(context);
308-
if (projectProvider.getContext().globalState.get('workspaceSaveInProgress') &&
309-
projectProvider.getContext().globalState.get('selectedProject') !== undefined) {
310-
devCommands.addProjectsToTheDashBoard(projectProvider, projectProvider.getContext().globalState.get('selectedProject') as string);
313+
async function handleWorkspaceSaveInProgress(context: vscode.ExtensionContext): Promise<boolean> {
314+
const projectProvider = getProjectProvider(context);
315+
const wip = projectProvider.getContext().globalState.get('workspaceSaveInProgress');
316+
const sel = projectProvider.getContext().globalState.get('selectedProject');
317+
if (wip && sel !== undefined) {
318+
// Wait for the initial refresh() to finish so the project map is fully populated.
319+
await projectProvider.initialRefresh;
320+
// Write the project into workspaceState. Do NOT call fireChangeEvent here —
321+
// the tree data provider may not be registered yet. registerCommands() will
322+
// call refresh() after registering the tree, which will display it.
323+
await projectProvider.addUserSelectedPath(sel as string, projectProvider.getProjects());
311324
helperUtil.clearDataSavedInGlobalState(projectProvider.getContext());
325+
return true;
312326
}
327+
return false;
313328
}
314329

315330
function getProjectProvider(context: vscode.ExtensionContext): ProjectProvider {

src/liberty/libertyProject.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as gradleUtil from "../util/gradleUtil";
99
import * as mavenUtil from "../util/mavenUtil";
1010
import * as util from "../util/helperUtil";
1111
import { localize } from "../util/i18nUtil";
12-
import { EXCLUDED_DIR_PATTERN, LIBERTY_GRADLE_PROJECT, LIBERTY_GRADLE_PROJECT_CONTAINER, LIBERTY_MAVEN_PROJECT, LIBERTY_MAVEN_PROJECT_CONTAINER, UNTITLED_WORKSPACE } from "../definitions/constants";
12+
import { EXCLUDED_DIR_PATTERN, LIBERTY_GRADLE_PROJECT, LIBERTY_GRADLE_PROJECT_CONTAINER, LIBERTY_MAVEN_PROJECT, LIBERTY_MAVEN_PROJECT_CONTAINER } from "../definitions/constants";
1313
import { BuildFileImpl, GradleBuildFile } from "../util/buildFile";
1414
import { DashboardData } from "./dashboard";
1515
import { BaseLibertyProject } from "./baseLibertyProject";
@@ -31,11 +31,15 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
3131

3232
private _context: vscode.ExtensionContext;
3333

34+
// Resolves when the first refresh() completes. Callers that need a stable
35+
// project map before acting (e.g. handleWorkspaceSaveInProgress) must await this.
36+
public readonly initialRefresh: Promise<void>;
37+
3438
constructor(context: vscode.ExtensionContext) {
3539
this._context = context;
3640
this._onDidChangeTreeData = new vscode.EventEmitter<LibertyProject | undefined>();
3741
this.onDidChangeTreeData = this._onDidChangeTreeData.event;
38-
this.refresh();
42+
this.initialRefresh = this.refresh();
3943
}
4044

4145
public getContext(): vscode.ExtensionContext {
@@ -212,20 +216,20 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
212216
'Save Workspace'
213217
).then(async (selection) => {
214218
if (selection === 'Save Workspace') {
219+
await this._context.globalState.update('workspaceSaveInProgress', true);
220+
console.log('[checkUntitledWorkspaceAndSaveIt] wrote workspaceSaveInProgress=true, selectedProject=' + this._context.globalState.get('selectedProject'));
221+
// Do NOT clear globalState here: the new session needs workspaceSaveInProgress
222+
// and selectedProject to restore the manually-added project to the dashboard.
223+
await vscode.commands.executeCommand('workbench.action.saveWorkspaceAs');
224+
console.log('[checkUntitledWorkspaceAndSaveIt] saveWorkspaceAs command returned');
225+
} else {
215226
/**
216-
* setting workspaceSaveInProgress to true and storing it in globalstate for identifyting that the
217-
* workspace is saved and needs to save the manually added projects to the dashboard
227+
* If the user cancels saving the workspace and exits without saving, the data stays in the global state,
228+
* which is shared across all VS Code instances. To prevent this data from being mistakenly used in other
229+
* sessions and added to the dashboard, it should be cleared if the user cancels the save.
218230
*/
219-
await this._context.globalState.update('workspaceSaveInProgress', true);
220-
//opens the saveWorkspace as dialog box
221-
await vscode.commands.executeCommand('workbench.action.saveWorkspaceAs');
231+
util.clearDataSavedInGlobalState(this._context);
222232
}
223-
/**
224-
* If the user cancels saving the workspace and exits without saving, the data stays in the global state,
225-
* which is shared across all VS Code instances. To prevent this data from being mistakenly used in other
226-
* sessions and added to the dashboard, it should be cleared if the user cancels the save.
227-
*/
228-
util.clearDataSavedInGlobalState(this._context);
229233
resolve();
230234
});
231235
} catch (error) {
@@ -237,12 +241,15 @@ export class ProjectProvider implements vscode.TreeDataProvider<LibertyProject>
237241
}
238242

239243
/*
240-
This method identifies a workspace that is untitled and contains more than one project
241-
*/
244+
* Returns true when the workspace has not yet been saved to a .code-workspace file,
245+
* meaning workspaceState will be lost if the user does "Save Workspace As...".
246+
* This covers both plain single-folder windows (workspace.name == folder name)
247+
* and VS Code's auto-created "Untitled (Workspace)" multi-root workspaces.
248+
*/
242249
public isMultiProjectUntitledWorkspace(): boolean {
243250
const workspaceFolders = vscode.workspace.workspaceFolders;
244-
if ((workspaceFolders && workspaceFolders.length > 1
245-
&& vscode.workspace.name === UNTITLED_WORKSPACE)) {
251+
if (workspaceFolders && workspaceFolders.length >= 1
252+
&& vscode.workspace.workspaceFile === undefined) {
246253
return true;
247254
}
248255
return false;

src/test/unit/fakeVscode.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,40 @@ export interface FakeVscode {
3131
workspace: {
3232
workspaceFolders: any;
3333
name: any;
34+
workspaceFile: any;
3435
};
3536
}
3637

37-
export function installFakeVscode(windowOverrides: Partial<FakeVscode["window"]> = {}): FakeVscode {
38+
// Paths that must be evicted from Node's require cache so that re-importing
39+
// libertyProject (or anything that transitively imports it) picks up the new
40+
// fake instead of a version bound to a previously-installed fake.
41+
const EXTENSION_CACHE_KEYS = [
42+
"liberty/libertyProject",
43+
"liberty/dashboard",
44+
"liberty/baseLibertyProject",
45+
"util/helperUtil",
46+
"util/gradleUtil",
47+
"util/mavenUtil",
48+
"util/buildFile",
49+
"util/i18nUtil",
50+
"definitions/constants",
51+
];
52+
53+
/**
54+
* @param windowOverrides Optional overrides for window methods (e.g. a sinon stub).
55+
* @param clearCache Pass true to evict all cached extension modules before
56+
* installing the fake. Required when another test file may
57+
* have already loaded these modules against a different fake.
58+
* Defaults to false so existing callers are unaffected.
59+
*/
60+
export function installFakeVscode(windowOverrides: Partial<FakeVscode["window"]> = {}, clearCache = false): FakeVscode {
61+
if (clearCache) {
62+
Object.keys(require.cache).forEach(key => {
63+
if (EXTENSION_CACHE_KEYS.some(segment => key.includes(segment))) {
64+
delete require.cache[key];
65+
}
66+
});
67+
}
3868
const fakeVscode: FakeVscode = {
3969
EventEmitter: class { event = () => {}; fire() {} },
4070
TreeItemCollapsibleState: { None: 0 },
@@ -54,7 +84,8 @@ export function installFakeVscode(windowOverrides: Partial<FakeVscode["window"]>
5484
},
5585
workspace: {
5686
workspaceFolders: undefined,
57-
name: undefined
87+
name: undefined,
88+
workspaceFile: undefined
5889
}
5990
};
6091

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* IBM Confidential
3+
* Copyright IBM Corp. 2026
4+
*/
5+
6+
// This test runs in plain Node, not inside a real VS Code window.
7+
import { installFakeVscode, FakeVscode } from "./fakeVscode";
8+
9+
// Install the fake and clear any previously cached extension modules so this
10+
// file's import of libertyProject binds to our fake, regardless of test order.
11+
const fakeVscode: FakeVscode = installFakeVscode({}, true);
12+
13+
import * as assert from "assert";
14+
import { ProjectProvider } from "../../liberty/libertyProject";
15+
16+
// Minimal fake ExtensionContext — ProjectProvider needs one to construct.
17+
const fakeContext: any = {
18+
workspaceState: {
19+
get: () => undefined,
20+
update: () => Promise.resolve()
21+
},
22+
globalState: {
23+
get: () => undefined,
24+
update: () => Promise.resolve()
25+
}
26+
};
27+
28+
// Helper: build a fake workspaceFolders array with n entries.
29+
function makeFolders(n: number) {
30+
return Array.from({ length: n }, (_, i) => ({ uri: { fsPath: `/project${i}` } }));
31+
}
32+
33+
describe("isMultiProjectUntitledWorkspace", () => {
34+
35+
let provider: ProjectProvider;
36+
37+
before(() => {
38+
provider = new ProjectProvider(fakeContext);
39+
});
40+
41+
// resets fake back to clean slate so test behavior is not affected by last test
42+
afterEach(() => {
43+
fakeVscode.workspace.workspaceFolders = undefined;
44+
fakeVscode.workspace.name = undefined;
45+
fakeVscode.workspace.workspaceFile = undefined;
46+
});
47+
48+
// ─── Test 1 ───────────────────────────────────────────────────────────────
49+
// Plain single-folder window (the video scenario) where workspace.name is the
50+
// folder name (like gradleappadd) and no workspaceFile is saved. workspaceState
51+
// is lost when user does "Save Workspace As..." so isMultiProjectUntitledWorkspace()
52+
// must return true to trigger the globalState update and prevent the project from being lost.
53+
it("returns true for a plain single-folder window (no .code-workspace file)", () => {
54+
fakeVscode.workspace.workspaceFolders = makeFolders(1);
55+
fakeVscode.workspace.name = "gradleappadd";
56+
fakeVscode.workspace.workspaceFile = undefined;
57+
58+
assert.strictEqual(provider.isMultiProjectUntitledWorkspace(), true,
59+
"A plain single-folder window loses workspaceState on Save Workspace As " +
60+
"so the safe-path must trigger.");
61+
});
62+
63+
// ─── Test 2 ───────────────────────────────────────────────────────────────
64+
// VS Code auto-created "Untitled (Workspace)" (multiple folders not yet saved):
65+
// workspaceFile is undefined here too, so must return true.
66+
it("returns true for an auto-created untitled workspace (multiple folders)", () => {
67+
fakeVscode.workspace.workspaceFolders = makeFolders(2);
68+
fakeVscode.workspace.name = "Untitled (Workspace)";
69+
fakeVscode.workspace.workspaceFile = undefined;
70+
71+
assert.strictEqual(provider.isMultiProjectUntitledWorkspace(), true);
72+
});
73+
74+
// ─── Test 3 ───────────────────────────────────────────────────────────────
75+
// Workspace already saved to a .code-workspace file: workspaceState is
76+
// stable, no prompt needed.
77+
it("returns false when workspace is already saved to a .code-workspace file", () => {
78+
fakeVscode.workspace.workspaceFolders = makeFolders(1);
79+
fakeVscode.workspace.name = "testIssue";
80+
fakeVscode.workspace.workspaceFile = { fsPath: "/home/user/testIssue.code-workspace" };
81+
82+
assert.strictEqual(provider.isMultiProjectUntitledWorkspace(), false);
83+
});
84+
85+
// ─── Test 4 ───────────────────────────────────────────────────────────────
86+
// No folders open at all: nothing to protect, should return false.
87+
it("returns false when there are no folders open", () => {
88+
fakeVscode.workspace.workspaceFolders = undefined;
89+
fakeVscode.workspace.name = undefined;
90+
fakeVscode.workspace.workspaceFile = undefined;
91+
92+
assert.strictEqual(provider.isMultiProjectUntitledWorkspace(), false);
93+
});
94+
95+
});

0 commit comments

Comments
 (0)