Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions packages/backend/src/panels/YeomanUIPanel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isEmpty, get, isNil, assign } from "lodash";
import { isEmpty, get, assign } from "lodash";
import { join } from "path";
import * as vscode from "vscode";
import { YeomanUI } from "../yeomanui";
Expand All @@ -16,6 +16,7 @@ import { NpmCommand } from "../utils/npm";
import { Constants } from "../utils/constants";
import { notifyGeneratorsInstallationProgress } from "../utils/generators-installation-progress";
import messages from "../messages";
import { getWorkspaceFolders, getFileSchemeWorkspaceFolders } from "../utils/workspaceFolders";

export class YeomanUIPanel extends AbstractWebviewPanel {
public static YEOMAN_UI = "Application Wizard";
Expand Down Expand Up @@ -46,7 +47,32 @@ export class YeomanUIPanel extends AbstractWebviewPanel {
this.yeomanui = null;
}

private async ensureValidTargetFolder(): Promise<{ originalTargetFolder: string; wasModified: boolean }> {
const originalTargetFolder = vscode.workspace.getConfiguration()?.get<string>("ApplicationWizard.TargetFolder");
const supportedFileWorkspaceFolders = getWorkspaceFolders();

// If there are in-memory folders, ensure targetFolder is a valid physical path that will load yeoman UI
const hasInMemoryFolders =
(vscode.workspace.workspaceFolders?.length ?? 0) !== (supportedFileWorkspaceFolders?.length ?? 0);

if (hasInMemoryFolders) {
// Using logical OR to ensure we don't set empty string, which is an invalid state
const workspaceFolder = originalTargetFolder?.trim() || supportedFileWorkspaceFolders?.[0]?.trim() || "";
const targetFolder = workspaceFolder || join(homedir(), "projects");

await vscode.workspace
.getConfiguration()
.update("ApplicationWizard.TargetFolder", targetFolder, vscode.ConfigurationTarget.Global);

return { originalTargetFolder, wasModified: true };
}

return { originalTargetFolder, wasModified: false };
}

public async loadWebviewPanel(uiOptions?: any): Promise<void> {
// Ensure valid target folder when virtual workspaces are present
await this.ensureValidTargetFolder();
if (!Constants.IS_IN_BAS && (await NpmCommand.getNodeProcessVersions()).node === undefined) {
void vscode.window.showErrorMessage(messages.nodejs_install_not_found);
}
Expand Down Expand Up @@ -142,10 +168,8 @@ export class YeomanUIPanel extends AbstractWebviewPanel {
}
uri = vscode.Uri.file(currentPath);
} catch (e) {
uri = get(vscode, "workspace.workspaceFolders[0].uri");
if (isNil(uri)) {
uri = vscode.Uri.file(join(homedir()));
}
const workspaceFolders = getFileSchemeWorkspaceFolders();
uri = workspaceFolders.length > 0 ? workspaceFolders[0].uri : vscode.Uri.file(join(homedir()));
}

try {
Expand Down
32 changes: 32 additions & 0 deletions packages/backend/src/utils/workspaceFolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { workspace, WorkspaceFolder } from "vscode";

/**
* Returns an array of file system paths for workspace folders with 'file' URI scheme.
* Filters out virtual workspaces (remote, SSH, WSL, etc.) that don't have valid fsPath.
* @returns Array of file system paths, or empty array if no file-scheme workspaces exist
*/
export function getWorkspaceFolders(): string[] {
return workspace.workspaceFolders
? workspace.workspaceFolders.filter((ws) => ws.uri.scheme === "file").map((ws) => ws.uri.fsPath)
: [];
}

/**
* Returns an array of WorkspaceFolder objects with 'file' URI scheme.
* Useful when you need the full WorkspaceFolder metadata (name, uri, etc.).
* @returns Array of WorkspaceFolder objects with file scheme, or empty array
*/
export function getFileSchemeWorkspaceFolders(): WorkspaceFolder[] {
return workspace.workspaceFolders ? workspace.workspaceFolders.filter((folder) => folder.uri.scheme === "file") : [];
}

/**
* Returns the first file-scheme workspace path, or fallback if none exist.
* Convenience method for cases where you need a single path with a default.
* @param fallback The fallback path to use if no file-scheme workspaces exist
* @returns The first file-scheme workspace path or the fallback value
*/
export function getFirstWorkspacePath(fallback: string): string {
const paths = getWorkspaceFolders();
return paths.length > 0 ? paths[0] : fallback;
}
7 changes: 4 additions & 3 deletions packages/backend/src/vscode-youi-events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as vscode from "vscode";
import { isEmpty, size, isNil, set } from "lodash";
import { isEmpty, isNil, set } from "lodash";
import { YouiEvents } from "./youi-events";
import { IRpc } from "@sap-devx/webview-rpc/out.ext/rpc-common";
import { GeneratorOutput } from "./vscode-output";
Expand All @@ -9,6 +9,7 @@ import { getImage } from "./images/messageImages";
import { AppWizard, MessageType, Severity, IBannerProps } from "@sap-devx/yeoman-ui-types";
import { FolderUriConfig, getFolderUri, getValidFolderUri, WorkspaceFile, WsFoldersToAdd } from "./utils/workspaceFile";
import { Constants } from "./utils/constants";
import { getFileSchemeWorkspaceFolders } from "./utils/workspaceFolders";

class YoUiAppWizard extends AppWizard {
constructor(private readonly events: VSCodeYouiEvents) {
Expand Down Expand Up @@ -240,7 +241,7 @@ export class VSCodeYouiEvents implements YouiEvents {
}

private getUniqueProjectName(baseName: string): string {
const existingNames = vscode.workspace.workspaceFolders?.map((folder) => folder.name) || [];
const existingNames = getFileSchemeWorkspaceFolders().map((folder) => folder.name);
if (!existingNames.includes(baseName)) {
return baseName;
}
Expand All @@ -257,7 +258,7 @@ export class VSCodeYouiEvents implements YouiEvents {
}

private addOrCreateProjectWorkspace(wsFoldersToAdd: WsFoldersToAdd) {
const wsFoldersQuantity = size(vscode.workspace.workspaceFolders);
const wsFoldersQuantity = getFileSchemeWorkspaceFolders().length;
vscode.workspace.updateWorkspaceFolders(wsFoldersQuantity, null, wsFoldersToAdd);
}

Expand Down
5 changes: 2 additions & 3 deletions packages/backend/src/yeomanui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Questions } from "yeoman-environment/lib/adapter";
import { State } from "./utils/promise";
import { Constants } from "./utils/constants";
import { isUriFlow } from "./utils/workspaceFile";
import { getFirstWorkspacePath } from "./utils/workspaceFolders";

export interface IQuestionsPrompt extends IPrompt {
questions: any[];
Expand Down Expand Up @@ -363,9 +364,7 @@ export class YeomanUI {
return targetFolderConfig;
}

return Constants.IS_IN_BAS
? Constants.HOMEDIR_PROJECTS
: _.get(vscode, "workspace.workspaceFolders[0].uri.fsPath", Constants.HOMEDIR_PROJECTS);
return Constants.IS_IN_BAS ? Constants.HOMEDIR_PROJECTS : getFirstWorkspacePath(Constants.HOMEDIR_PROJECTS);
}

public async showPrompt(questions: Questions<any>): Promise<inquirer.Answers> {
Expand Down
81 changes: 81 additions & 0 deletions packages/backend/test/panels/YeomanUIPanel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,87 @@ describe("YeomanUIPanel unit test", () => {
.throws(new Error("unexpected"));
expect(await panel["showOpenDialog"](required, canSelectFiles)).to.equal(required);
});

it("showOpenDialog - empty path, only remote workspaces, uses homedir fallback", async () => {
const canSelectFiles = false;
const mockFolders = [
{ uri: { scheme: "vscode-remote", fsPath: "" }, name: "remote" },
{ uri: { scheme: "ssh", fsPath: "" }, name: "ssh" },
];
sandbox.stub(vscode.workspace, "workspaceFolders").value(mockFolders);
windowMock
.expects("showOpenDialog")
.withExactArgs({
canSelectFiles,
canSelectFolders: !canSelectFiles,
defaultUri: vscode.Uri.file(join(homedir())),
})
.resolves([selected]);
expect(await panel["showOpenDialog"]("", canSelectFiles)).to.equal(selected.fsPath);
});

it("showOpenDialog - empty path, mixed schemes, uses first file-scheme workspace", async () => {
const canSelectFiles = true;
const fileWorkspacePath = "/local/path";
const mockFolders = [
{ uri: { scheme: "vscode-remote", fsPath: "" }, name: "remote" },
{ uri: { scheme: "file", fsPath: fileWorkspacePath }, name: "local" },
];
sandbox.stub(vscode.workspace, "workspaceFolders").value(mockFolders);
windowMock
.expects("showOpenDialog")
.withExactArgs({
canSelectFiles,
canSelectFolders: !canSelectFiles,
defaultUri: vscode.Uri.file(fileWorkspacePath),
})
.resolves([selected]);
expect(await panel["showOpenDialog"]("", canSelectFiles)).to.equal(selected.fsPath);
});
});

describe("ensureValidTargetFolder", () => {
it("detects in-memory folders and updates config", async () => {
const mockFolders = [
{ uri: { scheme: "vscode-remote", fsPath: "" }, name: "remote" },
{ uri: { scheme: "file", fsPath: "/local/path" }, name: "local" },
];
sandbox.stub(vscode.workspace, "workspaceFolders").value(mockFolders);
wsConfigMock.expects("get").withExactArgs("ApplicationWizard.TargetFolder").returns(undefined);
wsConfigMock
.expects("update")
.withArgs("ApplicationWizard.TargetFolder", "/local/path", vscode.ConfigurationTarget.Global)
.resolves();

const result = await panel["ensureValidTargetFolder"]();
expect(result.wasModified).to.be.true;
});

it("does not modify config when no in-memory folders exist", async () => {
const mockFolders = [
{ uri: { scheme: "file", fsPath: "/local/path1" }, name: "local1" },
{ uri: { scheme: "file", fsPath: "/local/path2" }, name: "local2" },
];
sandbox.stub(vscode.workspace, "workspaceFolders").value(mockFolders);
wsConfigMock.expects("get").withExactArgs("ApplicationWizard.TargetFolder").returns(undefined);
wsConfigMock.expects("update").never();

const result = await panel["ensureValidTargetFolder"]();
expect(result.wasModified).to.be.false;
});

it("uses homedir fallback when no file-scheme workspaces exist", async () => {
const mockFolders = [{ uri: { scheme: "vscode-remote", fsPath: "" }, name: "remote" }];
sandbox.stub(vscode.workspace, "workspaceFolders").value(mockFolders);
wsConfigMock.expects("get").withExactArgs("ApplicationWizard.TargetFolder").returns(undefined);
wsConfigMock
.expects("update")
.withArgs("ApplicationWizard.TargetFolder", join(homedir(), "projects"), vscode.ConfigurationTarget.Global)
.resolves();

const result = await panel["ensureValidTargetFolder"]();
expect(result.wasModified).to.be.true;
});
});

describe("dispose", () => {
Expand Down
Loading
Loading