Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: open Zowe Explorer resources using VSCode URLs #3271

Merged
merged 13 commits into from
Oct 30, 2024
Merged
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
1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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
59 changes: 54 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 @@ -1529,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 @@ -1606,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 @@ -243,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 @@ -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 Down Expand Up @@ -628,3 +629,20 @@ describe("Shared utils unit tests - function addToWorkspace", () => {
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 });
});
});
52 changes: 26 additions & 26 deletions packages/zowe-explorer/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,32 +208,6 @@
"Profile auth error": "Profile auth error",
"Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue",
"Retrieving response from USS list API": "Retrieving response from USS list API",
"The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.",
"Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI",
"Profile does not exist for this file.": "Profile does not exist for this file.",
"Saving USS file...": "Saving USS file...",
"Renaming {0} failed due to API error: {1}/File pathError message": {
"message": "Renaming {0} failed due to API error: {1}",
"comment": [
"File path",
"Error message"
]
},
"Deleting {0} failed due to API error: {1}/File nameError message": {
"message": "Deleting {0} failed due to API error: {1}",
"comment": [
"File name",
"Error message"
]
},
"No error details given": "No error details given",
"Error fetching destination {0} for paste action: {1}/USS pathError message": {
"message": "Error fetching destination {0} for paste action: {1}",
"comment": [
"USS path",
"Error message"
]
},
"Downloaded: {0}/Download time": {
"message": "Downloaded: {0}",
"comment": [
Expand Down Expand Up @@ -304,6 +278,32 @@
"initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove",
"File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.",
"Pulling from Mainframe...": "Pulling from Mainframe...",
"The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.",
"Could not list USS files: Empty path provided in URI": "Could not list USS files: Empty path provided in URI",
"Profile does not exist for this file.": "Profile does not exist for this file.",
"Saving USS file...": "Saving USS file...",
"Renaming {0} failed due to API error: {1}/File pathError message": {
"message": "Renaming {0} failed due to API error: {1}",
"comment": [
"File path",
"Error message"
]
},
"Deleting {0} failed due to API error: {1}/File nameError message": {
"message": "Deleting {0} failed due to API error: {1}",
"comment": [
"File name",
"Error message"
]
},
"No error details given": "No error details given",
"Error fetching destination {0} for paste action: {1}/USS pathError message": {
"message": "Error fetching destination {0} for paste action: {1}",
"comment": [
"USS path",
"Error message"
]
},
"{0} location/Node type": {
"message": "{0} location",
"comment": [
Expand Down
19 changes: 11 additions & 8 deletions packages/zowe-explorer/l10n/poeditor.json
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@
"openWithEncoding": {
"Open with Encoding": ""
},
"copyExternalLink": {
"Copy External Link": ""
},
"zowe.history.deprecationMsg": {
"Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage.": ""
},
Expand Down Expand Up @@ -528,14 +531,6 @@
"Profile auth error": "",
"Profile is not authenticated, please log in to continue": "",
"Retrieving response from USS list API": "",
"The 'move' function is not implemented for this USS API.": "",
"Could not list USS files: Empty path provided in URI": "",
"Profile does not exist for this file.": "",
"Saving USS file...": "",
"Renaming {0} failed due to API error: {1}": "",
"Deleting {0} failed due to API error: {1}": "",
"No error details given": "",
"Error fetching destination {0} for paste action: {1}": "",
"Downloaded: {0}": "",
"Encoding: {0}": "",
"Binary": "",
Expand Down Expand Up @@ -564,6 +559,14 @@
"initializeUSSFavorites.error.buttonRemove": "",
"File does not exist. It may have been deleted.": "",
"Pulling from Mainframe...": "",
"The 'move' function is not implemented for this USS API.": "",
"Could not list USS files: Empty path provided in URI": "",
"Profile does not exist for this file.": "",
"Saving USS file...": "",
"Renaming {0} failed due to API error: {1}": "",
"Deleting {0} failed due to API error: {1}": "",
"No error details given": "",
"Error fetching destination {0} for paste action: {1}": "",
"{0} location": "",
"Choose a location to create the {0}": "",
"Name of file or directory": "",
Expand Down
24 changes: 22 additions & 2 deletions packages/zowe-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,11 @@
"title": "%copyName%",
"category": "Zowe Explorer"
},
{
"command": "zowe.copyExternalLink",
"title": "%copyExternalLink%",
"category": "Zowe Explorer"
},
{
"command": "zowe.uss.addSession",
"title": "%uss.addSession%",
Expand Down Expand Up @@ -834,6 +839,11 @@
"command": "zowe.uss.copyRelativePath",
"group": "002_zowe_ussSystemSpecific@5"
},
{
"when": "view == zowe.uss.explorer && viewItem =~ /^(?!(directory|favorite|profile_fav|ussSession))/ && !listMultiSelection",
"command": "zowe.copyExternalLink",
"group": "002_zowe_ussSystemSpecific@6"
},
{
"when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)(textFile.*|binaryFile.*|directory.*)/",
"command": "zowe.addFavorite",
Expand Down Expand Up @@ -1034,15 +1044,20 @@
"command": "zowe.ds.copyName",
"group": "098_zowe_dsMisc@0"
},
{
"when": "view == zowe.ds.explorer && viewItem =~ /^(ds|member).*/ && !listMultiSelection",
"command": "zowe.copyExternalLink",
"group": "098_zowe_dsMisc@1"
},
{
"when": "view == zowe.ds.explorer && viewItem =~ /^(pds|session).*/ && !listMultiSelection",
"command": "zowe.ds.filterBy",
"group": "098_zowe_dsMisc@1"
"group": "098_zowe_dsMisc@2"
},
{
"when": "view == zowe.ds.explorer && viewItem =~ /^(pds|session).*/ && !listMultiSelection",
"command": "zowe.ds.sortBy",
"group": "098_zowe_dsMisc@2"
"group": "098_zowe_dsMisc@3"
},
{
"when": "view == zowe.ds.explorer && viewItem =~ /^fileError.*/",
Expand Down Expand Up @@ -1219,6 +1234,11 @@
"command": "zowe.jobs.copyName",
"group": "003_zowe_jobsMisc@0"
},
{
"when": "view == zowe.jobs.explorer && viewItem =~ /^spool.*/",
"command": "zowe.copyExternalLink",
"group": "003_zowe_jobsMisc@1"
},
{
"when": "view == zowe.jobs.explorer && viewItem =~ /^spool.*/",
"command": "zowe.jobs.refreshSpool",
Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,6 @@
"compareWithSelected": "Compare with Selected",
"compareWithSelectedReadOnly": "Compare with Selected (Read-Only)",
"openWithEncoding": "Open with Encoding",
"copyExternalLink": "Copy External Link",
"zowe.history.deprecationMsg": "Changes made here will not be reflected in Zowe Explorer, use right-click Edit History option to access information from local storage."
}
2 changes: 1 addition & 1 deletion packages/zowe-explorer/src/configuration/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { imperative, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api";
import type { Profiles } from "./Profiles";

export class Constants {
public static readonly COMMAND_COUNT = 100;
public static readonly COMMAND_COUNT = 101;
public static readonly MAX_SEARCH_HISTORY = 5;
public static readonly MAX_FILE_HISTORY = 10;
public static readonly MS_PER_SEC = 1000;
Expand Down
Loading
Loading