Skip to content

Commit

Permalink
Merge branch 'main' into fix/no-cfgs-prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
traeok authored Oct 30, 2024
2 parents dcf3cb2 + 1786fe3 commit 648ed3a
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 48 deletions.
20 changes: 15 additions & 5 deletions packages/zowe-explorer-api/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,14 +611,24 @@ export interface TreeDataProvider<T> {
}

export class Uri {
private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;

public static file(path: string): Uri {
return Uri.parse(path);
}
public static parse(value: string, _strict?: boolean): Uri {
const newUri = new Uri();
newUri.path = value;
const match = Uri._regexp.exec(value);
if (!match) {
return new Uri();
}

return newUri;
return Uri.from({
scheme: match[2] || "",
authority: match[4] || "",
path: match[5] || "",
query: match[7] || "",
fragment: match[9] || "",
});
}

public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri {
Expand Down Expand Up @@ -688,7 +698,7 @@ export class Uri {
/**
* Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`.
*/
path: string;
path: string = "";

/**
* Query is the `query` part of `http://www.example.com/some/path?query#fragment`.
Expand Down Expand Up @@ -720,7 +730,7 @@ export class Uri {
* u.fsPath === '\\server\c$\folder\file.txt'
* ```
*/
fsPath: string;
fsPath: string = "";

public toString(): string {
let result = this.scheme ? `${this.scheme}://` : "";
Expand Down
2 changes: 2 additions & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen

- Update Zowe SDKs to `8.2.0` to get the latest enhancements from Imperative.
- Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175)
- Add a data set or USS resource to a virtual workspace with the new "Add to Workspace" context menu option. [#3265](https://github.com/zowe/zowe-explorer-vscode/issues/3265)
- Power users and developers can now build links to efficiently open mainframe resources in Zowe Explorer. Use the **Copy External Link** option in the context menu to get the URL for a data set or USS resource, or create a link in the format `vscode://Zowe.vscode-extension-for-zowe?<ZoweResourceUri>`. For more information on building resource URIs, see the [FileSystemProvider wiki article](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris). [#3271](https://github.com/zowe/zowe-explorer-vscode/pull/3271)

### Bug fixes

Expand Down
76 changes: 71 additions & 5 deletions packages/zowe-explorer/__tests__/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,47 @@ export interface WebviewViewProvider {
resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Thenable<void> | void;
}

/**
* A uri handler is responsible for handling system-wide {@link Uri uris}.
*
* @see {@link window.registerUriHandler}.
*/
export interface UriHandler {
/**
* Handle the provided system-wide {@link Uri}.
*
* @see {@link window.registerUriHandler}.
*/
handleUri(uri: Uri): ProviderResult<void>;
}

export namespace window {
/**
* Registers a {@link UriHandler uri handler} capable of handling system-wide {@link Uri uris}.
* In case there are multiple windows open, the topmost window will handle the uri.
* A uri handler is scoped to the extension it is contributed from; it will only
* be able to handle uris which are directed to the extension itself. A uri must respect
* the following rules:
*
* - The uri-scheme must be `vscode.env.uriScheme`;
* - The uri-authority must be the extension id (e.g. `my.extension`);
* - The uri-path, -query and -fragment parts are arbitrary.
*
* For example, if the `my.extension` extension registers a uri handler, it will only
* be allowed to handle uris with the prefix `product-name://my.extension`.
*
* An extension can only register a single uri handler in its entire activation lifetime.
*
* * *Note:* There is an activation event `onUri` that fires when a uri directed for
* the current extension is about to be handled.
*
* @param handler The uri handler to register for this extension.
* @returns A {@link Disposable disposable} that unregisters the handler.
*/
export function registerUriHandler(handler: UriHandler): Disposable {
return () => {};
}

/**
* Register a new provider for webview views.
*
Expand Down Expand Up @@ -1320,6 +1360,23 @@ export namespace workspace {
export function onDidOpenTextDocument<T>(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {}
export function onDidSaveTextDocument<T>(listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) {}

export function updateWorkspaceFolders(
start: number,
deleteCount: number | undefined | null,
...workspaceFoldersToAdd: {
/**
* The uri of a workspace folder that's to be added.
*/
readonly uri: Uri;
/**
* The name of a workspace folder that's to be added.
*/
readonly name?: string;
}[]
): boolean {
return false;
}

export function getConfiguration(configuration: string) {
return {
update: () => {
Expand Down Expand Up @@ -1512,14 +1569,23 @@ export interface TextDocument {
}

export class Uri {
private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
public static file(path: string): Uri {
return Uri.parse(path);
}
public static parse(value: string, strict?: boolean): Uri {
const newUri = new Uri();
newUri.path = value;
public static parse(value: string, _strict?: boolean): Uri {
const match = Uri._regexp.exec(value);
if (!match) {
return new Uri();
}

return newUri;
return Uri.from({
scheme: match[2] || "",
authority: match[4] || "",
path: match[5] || "",
query: match[7] || "",
fragment: match[9] || "",
});
}

public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri {
Expand Down Expand Up @@ -1589,7 +1655,7 @@ export class Uri {
/**
* Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`.
*/
path: string;
path: string = "";

/**
* Query is the `query` part of `http://www.example.com/some/path?query#fragment`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async function createGlobalMocks() {
"zowe.saveSearch",
"zowe.addFavorite",
"zowe.removeFavorite",
"zowe.addToWorkspace",
"zowe.removeFavProfile",
"zowe.openWithEncoding",
"zowe.issueTsoCmd",
Expand All @@ -242,6 +243,7 @@ async function createGlobalMocks() {
"zowe.compareWithSelected",
"zowe.compareWithSelectedReadOnly",
"zowe.compareFileStarted",
"zowe.copyExternalLink",
"zowe.placeholderCommand",
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,16 @@ describe("Test src/shared/extension", () => {
expect(remoteLookupDsSpy).not.toHaveBeenCalled();
expect(remoteLookupUssSpy).not.toHaveBeenCalled();
});
it("logs an error if one occurs", async () => {
const fakeEventInfo = getFakeEventInfo([{ uri: vscode.Uri.from({ scheme: ZoweScheme.DS, path: "/lpar.zosmf/TEST.PDS" }) }]);
const sampleError = new Error("issue fetching data set");
const remoteLookupMock = jest.spyOn(DatasetFSProvider.instance, "remoteLookupForResource").mockRejectedValueOnce(sampleError);
const errorMock = jest.spyOn(ZoweLogger, "error").mockImplementation();
await SharedInit.setupRemoteWorkspaceFolders(fakeEventInfo);
expect(errorMock).toHaveBeenCalledWith(sampleError.message);
expect(remoteLookupMock).toHaveBeenCalled();
remoteLookupMock.mockRestore();
});
});

describe("emitZoweEventHook", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import * as vscode from "vscode";
import { createIProfile, createISession, createInstanceOfProfile } from "../../../__mocks__/mockCreators/shared";
import { createDatasetSessionNode } from "../../../__mocks__/mockCreators/datasets";
import { createUSSNode } from "../../../__mocks__/mockCreators/uss";
import { UssFSProvider } from "../../../../src/trees/uss/UssFSProvider";
import { imperative, ProfilesCache, Gui, ZosEncoding, BaseProvider } from "@zowe/zowe-explorer-api";
import { Constants } from "../../../../src/configuration/Constants";
Expand All @@ -25,6 +26,7 @@ import { SharedUtils } from "../../../../src/trees/shared/SharedUtils";
import { ZoweUSSNode } from "../../../../src/trees/uss/ZoweUSSNode";
import { AuthUtils } from "../../../../src/utils/AuthUtils";
import { SharedTreeProviders } from "../../../../src/trees/shared/SharedTreeProviders";
import { MockedProperty } from "../../../__mocks__/mockUtils";

function createGlobalMocks() {
const newMocks = {
Expand Down Expand Up @@ -551,3 +553,96 @@ describe("Shared utils unit tests - function parseFavorites", () => {
expect(warnSpy).toHaveBeenCalledWith("Failed to parse a saved favorite. Attempted to parse: [testProfile]: ");
});
});

describe("Shared utils unit tests - function addToWorkspace", () => {
it("adds a Data Set resource to the workspace", () => {
const datasetNode = new ZoweDatasetNode({
label: "EXAMPLE.DS",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: Constants.DS_DS_CONTEXT,
profile: createIProfile(),
});
const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation();
SharedUtils.addToWorkspace(datasetNode, null as any);
expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { uri: datasetNode.resourceUri, name: datasetNode.label as string });
});
it("adds a USS resource to the workspace", () => {
const ussNode = new ZoweUSSNode({
label: "textFile.txt",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: Constants.USS_TEXT_FILE_CONTEXT,
profile: createIProfile(),
});
const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation();
SharedUtils.addToWorkspace(ussNode, null as any);
expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, { uri: ussNode.resourceUri, name: ussNode.label as string });
});
it("adds a USS session w/ fullPath to the workspace", () => {
const ussNode = new ZoweUSSNode({
label: "sestest",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: Constants.USS_SESSION_CONTEXT,
profile: createIProfile(),
session: createISession(),
});
ussNode.fullPath = "/u/users/smpluser";
const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation();
SharedUtils.addToWorkspace(ussNode, null as any);
expect(updateWorkspaceFoldersMock).toHaveBeenCalledWith(0, null, {
uri: ussNode.resourceUri?.with({ path: `/sestest${ussNode.fullPath}` }),
name: `[${ussNode.label as string}] ${ussNode.fullPath}`,
});
});
it("displays an info message when adding a USS session w/o fullPath", () => {
const ussNode = new ZoweUSSNode({
label: "sestest",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: Constants.USS_SESSION_CONTEXT,
profile: createIProfile(),
session: createISession(),
});
ussNode.fullPath = "";
const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation();
const infoMessageSpy = jest.spyOn(Gui, "infoMessage");
updateWorkspaceFoldersMock.mockClear();
SharedUtils.addToWorkspace(ussNode, null as any);
expect(updateWorkspaceFoldersMock).not.toHaveBeenCalledWith(0, null, {
uri: ussNode.resourceUri?.with({ path: `/sestest${ussNode.fullPath}` }),
name: `[${ussNode.label as string}] ${ussNode.fullPath}`,
});
expect(infoMessageSpy).toHaveBeenCalledWith("A search must be set for sestest before it can be added to a workspace.");
});
it("skips adding a resource that's already in the workspace", () => {
const ussNode = new ZoweUSSNode({
label: "textFile.txt",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: Constants.USS_TEXT_FILE_CONTEXT,
profile: createIProfile(),
});
const workspaceFolders = new MockedProperty(vscode.workspace, "workspaceFolders", {
value: [{ uri: ussNode.resourceUri, name: ussNode.label }],
});
const updateWorkspaceFoldersMock = jest.spyOn(vscode.workspace, "updateWorkspaceFolders").mockImplementation();
updateWorkspaceFoldersMock.mockClear();
SharedUtils.addToWorkspace(ussNode, null as any);
expect(updateWorkspaceFoldersMock).not.toHaveBeenCalledWith(0, null, { uri: ussNode.resourceUri, name: ussNode.label as string });
workspaceFolders[Symbol.dispose]();
});
});

describe("Shared utils unit tests - function copyExternalLink", () => {
it("does nothing for an invalid node or one without a resource URI", async () => {
const copyClipboardMock = jest.spyOn(vscode.env.clipboard, "writeText");
const ussNode = createUSSNode(createISession(), createIProfile());
ussNode.resourceUri = undefined;
await SharedUtils.copyExternalLink({ extension: { id: "Zowe.vscode-extension-for-zowe" } } as any, ussNode);
expect(copyClipboardMock).not.toHaveBeenCalled();
});

it("copies a link for a node with a resource URI", async () => {
const copyClipboardMock = jest.spyOn(vscode.env.clipboard, "writeText");
const ussNode = createUSSNode(createISession(), createIProfile());
await SharedUtils.copyExternalLink({ extension: { id: "Zowe.vscode-extension-for-zowe" } } as any, ussNode);
expect(copyClipboardMock).toHaveBeenCalledWith(`vscode://Zowe.vscode-extension-for-zowe?${ussNode.resourceUri?.toString()}`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/

import { commands, Uri } from "vscode";
import { ZoweUriHandler } from "../../../src/utils/UriHandler";

describe("ZoweUriHandler", () => {
function getBlockMocks() {
return {
executeCommand: jest.spyOn(commands, "executeCommand"),
};
}

it("does nothing if the parsed query does not start with a Zowe scheme", async () => {
const blockMocks = getBlockMocks();
await ZoweUriHandler.getInstance().handleUri(Uri.parse("vscode://Zowe.vscode-extension-for-zowe?blah-some-unknown-query"));
expect(blockMocks.executeCommand).not.toHaveBeenCalled();
});

it("calls vscode.open with the parsed URI if a Zowe resource URI was provided", async () => {
const blockMocks = getBlockMocks();
const uri = Uri.parse("vscode://Zowe.vscode-extension-for-zowe?zowe-ds:/lpar.zosmf/TEST.PS");
await ZoweUriHandler.getInstance().handleUri(uri);
const zoweUri = Uri.parse(uri.query);
expect(blockMocks.executeCommand).toHaveBeenCalledWith("vscode.open", zoweUri, { preview: false });
});
});
Loading

0 comments on commit 648ed3a

Please sign in to comment.