diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index 20400d7ccd..9b780c83f0 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t - Added the `BaseProfileAuthOptions` interface to define base profile authentication options for SSO login and logout. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) - Deprecated the methods `ZoweVsCodeExtension.loginWithBaseProfile` and `ZoweVsCodeExtension.logoutWithBaseProfile`. Use `ZoweVsCodeExtension.ssoLogin` and `ZoweVsCodeExtension.ssoLogout` instead, which use the `BaseProfileAuthOptions` interface and allow you to choose whether the token value in the base profile should have precedence in case there are conflicts. [#3076](https://github.com/zowe/zowe-explorer-vscode/pull/3076) - Fixed bug in `ProfilesCache` class where old profiles were still accessible after deleting a Team configuration file. [#3124](https://github.com/zowe/zowe-explorer-vscode/issues/3124) +- Added `extensionRemovedFromPath` private function to the `DsEntry` class to allow removing the extension from a data set before making API calls. [#3121](https://github.com/zowe/zowe-explorer-vscode/issues/3121) ### Bug fixes diff --git a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts index 7825970886..8597cabf0e 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/fs/types/datasets.unit.test.ts @@ -51,4 +51,23 @@ describe("DsEntryMetadata", () => { }); expect(pdsEntryMeta.dsName).toBe("TEST.PDS(MEMBER)"); }); + + describe("extensionRemovedFromPath", () => { + it("returns a path without its extension", () => { + const fakeProfile: any = { name: "testProfile" }; + const entryMeta = new DsEntryMetadata({ + profile: fakeProfile, + path: "/testProfile/TEST.COBOL.DS.cbl", + }); + expect((entryMeta as any).extensionRemovedFromPath()).toBe("/testProfile/TEST.COBOL.DS"); + }); + it("returns a path as-is if no extension is detected", () => { + const fakeProfile: any = { name: "testProfile" }; + const entryMeta = new DsEntryMetadata({ + profile: fakeProfile, + path: "/testProfile/TEST.SOME.DS", + }); + expect((entryMeta as any).extensionRemovedFromPath()).toBe("/testProfile/TEST.SOME.DS"); + }); + }); }); diff --git a/packages/zowe-explorer-api/src/fs/types/datasets.ts b/packages/zowe-explorer-api/src/fs/types/datasets.ts index 32d8291804..e809f9c6c9 100644 --- a/packages/zowe-explorer-api/src/fs/types/datasets.ts +++ b/packages/zowe-explorer-api/src/fs/types/datasets.ts @@ -17,6 +17,20 @@ interface DsEntryProps { stats: Types.DatasetStats; } +export const DS_EXTENSION_MAP: Map = new Map([ + [".c", ["C"]], + [".jcl", ["JCL", "JCLLIB", "CNTL", "PROC", "PROCLIB"]], + [".cbl", ["COBOL", "CBL", "COB", "SCBL"]], + [".cpy", ["COPYBOOK", "COPY", "CPY", "COBCOPY"]], + [".inc", ["INC", "INCLUDE", "PLINC"]], + [".pli", ["PLI", "PL1", "PLX", "PCX"]], + [".sh", ["SH", "SHELL"]], + [".rexx", ["REXX", "REXEC", "EXEC"]], + [".xml", ["XML"]], + [".asm", ["ASM", /ASSEMBL/]], + [".log", ["LOG", /SPFLOG/]], +]); + export class DsEntry extends FileEntry implements DsEntryProps { public metadata: DsEntryMetadata; @@ -47,8 +61,21 @@ export class DsEntryMetadata implements EntryMetadata { this.path = metadata.path; } + /** + * @returns the data set's file system path without the extension + */ + public extensionRemovedFromPath(): string { + for (const ext of DS_EXTENSION_MAP.keys()) { + if (this.path.endsWith(ext)) { + return this.path.replace(ext, ""); + } + } + + return this.path; + } + public get dsName(): string { - const segments = this.path.split("/").filter(Boolean); + const segments = this.extensionRemovedFromPath().split("/").filter(Boolean); return segments[1] ? `${segments[0]}(${segments[1]})` : segments[0]; } } diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index ae2bcaf3b8..aade4fd1aa 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -12,6 +12,8 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed behavior of logout action when token is defined in both base profile and parent profile. [#3076](https://github.com/zowe/zowe-explorer-vscode/issues/3076) - Fixed bug that displayed obsolete profiles in the Zowe Explorer tree views after the associated team configuration file was deleted. [#3124](https://github.com/zowe/zowe-explorer-vscode/issues/3124) - Fix issue with extender profiles not being included in fresh team configuration file. [#3122](https://github.com/zowe/zowe-explorer-vscode/issues/3122) +- Fixed issue where file extensions were removed from data sets, causing language detection to sometimes fail for Zowe Explorer extenders. [#3121](https://github.com/zowe/zowe-explorer-vscode/issues/3121) +- Fixed an issue where copying and pasting a file/folder in the USS tree would fail abruptly, displaying an error. [#3128](https://github.com/zowe/zowe-explorer-vscode/issues/3128) ## `3.0.0-next.202409132122` diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index a14bd051c1..2af16a7253 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -9,38 +9,50 @@ * */ -import { Disposable, FilePermission, FileSystemError, FileType, languages, TextDocument, TextEditor, Uri } from "vscode"; +import { Disposable, FilePermission, FileSystemError, FileType, TextEditor, Uri } from "vscode"; import { createIProfile } from "../../../__mocks__/mockCreators/shared"; -import { DirEntry, DsEntry, FileEntry, FilterEntry, FsAbstractUtils, Gui, PdsEntry, ZoweScheme } from "@zowe/zowe-explorer-api"; +import { + DirEntry, + DsEntry, + DsEntryMetadata, + FileEntry, + FilterEntry, + FsAbstractUtils, + FsDatasetsUtils, + Gui, + PdsEntry, + ZoweScheme, +} from "@zowe/zowe-explorer-api"; import { MockedProperty } from "../../../__mocks__/mockUtils"; import { DatasetFSProvider } from "../../../../src/trees/dataset/DatasetFSProvider"; import { ZoweExplorerApiRegister } from "../../../../src/extending/ZoweExplorerApiRegister"; -import { ZoweLogger } from "../../../../src/tools/ZoweLogger"; const dayjs = require("dayjs"); const testProfile = createIProfile(); const testEntries = { ps: { ...new DsEntry("USER.DATA.PS", false), - metadata: { + metadata: new DsEntryMetadata({ profile: testProfile, path: "/USER.DATA.PS", - }, + }), etag: "OLDETAG", + isMember: false, } as DsEntry, pds: { ...new PdsEntry("USER.DATA.PDS"), - metadata: { + metadata: new DsEntryMetadata({ profile: testProfile, path: "/USER.DATA.PDS", - }, + }), } as PdsEntry, pdsMember: { ...new DsEntry("MEMBER1", true), - metadata: { + metadata: new DsEntryMetadata({ profile: testProfile, path: "/USER.DATA.PDS/MEMBER1", - }, + }), + isMember: true, } as DsEntry, session: { ...new FilterEntry("sestest"), @@ -743,7 +755,7 @@ describe("fetchDataset", () => { mvsApiMock.mockRestore(); }); - it("existing URI - PS", async () => { + it("existing URI", async () => { const fakePs = Object.assign(Object.create(Object.getPrototypeOf(testEntries.ps)), testEntries.ps); const lookupMock = jest.spyOn(DatasetFSProvider.instance, "lookup").mockReturnValue(fakePs); const writeFileSpy = jest.spyOn(DatasetFSProvider.instance as any, "writeFile"); @@ -841,6 +853,26 @@ describe("delete", () => { mvsApiMock.mockRestore(); }); + it("successfully deletes a PDS", async () => { + const fakePds = { ...testEntries.pds }; + const mockMvsApi = { + deleteDataSet: jest.fn(), + }; + const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); + const _lookupMock = jest.spyOn(DatasetFSProvider.instance as any, "lookup").mockReturnValueOnce(fakePds); + const _fireSoonMock = jest.spyOn(DatasetFSProvider.instance as any, "_fireSoon").mockImplementation(); + const isPdsEntry = jest.spyOn(FsDatasetsUtils, "isPdsEntry").mockReturnValueOnce(true); + jest.spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory").mockReturnValueOnce({ ...testEntries.session }); + + await DatasetFSProvider.instance.delete(testUris.pds, { recursive: false }); + expect(mockMvsApi.deleteDataSet).toHaveBeenCalledWith(fakePds.name, { responseTimeout: undefined }); + expect(_lookupMock).toHaveBeenCalledWith(testUris.pds, false); + expect(_fireSoonMock).toHaveBeenCalled(); + + mvsApiMock.mockRestore(); + isPdsEntry.mockRestore(); + }); + it("throws an error if it could not delete an entry", async () => { const fakePs = { ...testEntries.ps }; const fakeSession = { ...testEntries.session, entries: new Map() }; @@ -879,7 +911,7 @@ describe("rename", () => { it("renames a PS", async () => { const oldPs = { ...testEntries.ps }; const mockMvsApi = { - renameDataSetMember: jest.fn(), + renameDataSet: jest.fn(), }; const mvsApiMock = jest.spyOn(ZoweExplorerApiRegister, "getMvsApi").mockReturnValueOnce(mockMvsApi as any); const _lookupMock = jest @@ -889,7 +921,7 @@ describe("rename", () => { .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") .mockReturnValueOnce({ ...testEntries.session }); await DatasetFSProvider.instance.rename(testUris.ps, testUris.ps.with({ path: "/USER.DATA.PS2" }), { overwrite: true }); - expect(mockMvsApi.renameDataSetMember).toHaveBeenCalledWith("", "USER.DATA.PS", "USER.DATA.PS2"); + expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PS", "USER.DATA.PS2"); _lookupMock.mockRestore(); mvsApiMock.mockRestore(); _lookupParentDirectoryMock.mockRestore(); @@ -946,72 +978,3 @@ describe("rename", () => { _lookupParentDirectoryMock.mockRestore(); }); }); - -describe("onDidOpenTextDocument", () => { - const setTextDocLanguage = jest.spyOn(languages, "setTextDocumentLanguage"); - - afterEach(() => { - setTextDocLanguage.mockClear(); - }); - - it("handles ZoweScheme.DS documents", async () => { - const dsUri = Uri.from({ - path: "/profile/USER.WONDROUS.C/AMAZING", - scheme: ZoweScheme.DS, - }); - const doc = { - uri: dsUri, - languageId: undefined, - } as unknown as TextDocument; - await DatasetFSProvider.onDidOpenTextDocument(doc); - expect(setTextDocLanguage).toHaveBeenCalledWith(doc, "c"); - }); - - it("returns early if the language ID could not be identified", async () => { - const dsUri = Uri.from({ - path: "/profile/TEST.DS/AMAZING", - scheme: ZoweScheme.DS, - }); - const doc = { - uri: dsUri, - languageId: undefined, - } as unknown as TextDocument; - await DatasetFSProvider.onDidOpenTextDocument(doc); - expect(setTextDocLanguage).not.toHaveBeenCalled(); - }); - - it("returns early if the scheme is not ZoweScheme.DS", async () => { - const fileUri = Uri.from({ - path: "/var/www/AMAZING.txt", - scheme: "file", - }); - const doc = { - uri: fileUri, - languageId: "plaintext", - } as unknown as TextDocument; - await DatasetFSProvider.onDidOpenTextDocument(doc); - expect(setTextDocLanguage).not.toHaveBeenCalled(); - expect(doc.languageId).toBe("plaintext"); - }); - - it("handles an error when setting the language ID", async () => { - setTextDocLanguage.mockImplementationOnce(() => { - throw new Error("Not available"); - }); - const dsUri = Uri.from({ - path: "/profile/TEST.C.DS/MISSING", - scheme: ZoweScheme.DS, - }); - const doc = { - fileName: "MISSING", - uri: dsUri, - languageId: "rust", - } as unknown as TextDocument; - - const warnSpy = jest.spyOn(ZoweLogger, "warn"); - await DatasetFSProvider.onDidOpenTextDocument(doc); - expect(setTextDocLanguage).toHaveBeenCalled(); - expect(warnSpy).toHaveBeenCalledWith("Could not set document language for MISSING - tried languageId 'c'"); - expect(doc.languageId).toBe("rust"); - }); -}); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetUtils.unit.test.ts index f92e532578..7f79ff5e82 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetUtils.unit.test.ts @@ -11,27 +11,29 @@ import { DatasetUtils } from "../../../../src/trees/dataset/DatasetUtils"; -describe("Dataset utils unit tests - function getLanguageId", () => { - it("returns the proper language ID", () => { +describe("Dataset utils unit tests - function getExtension", () => { + it("returns the proper file extension", () => { const pairs = [ - { name: "TEST.DS.C", languageId: "c" }, - { name: "TEST.PDS.C(MEMBER)", languageId: "c" }, - { name: "TEST.DS.JCL", languageId: "jcl" }, - { name: "TEST.DS.CBL", languageId: "cobol" }, - { name: "TEST.PDS.CPY(M1)", languageId: "copybook" }, - { name: "TEST.DS.INCLUDE", languageId: "inc" }, - { name: "TEST.DS.PLX", languageId: "pli" }, - { name: "TEST.DS.SHELL", languageId: "shellscript" }, - { name: "TEST.DS.EXEC", languageId: "rexx" }, - { name: "TEST.DS.XML", languageId: "xml" }, - { name: "TEST.DS.ASM", languageId: "asm" }, - { name: "TEST.DS.LOG", languageId: "log" }, + { name: "TEST.DS.C", extension: ".c" }, + { name: "TEST.PDS.C(MEMBER)", extension: ".c" }, + { name: "TEST.DS.JCL", extension: ".jcl" }, + { name: "TEST.DS.CBL", extension: ".cbl" }, + { name: "TEST.PDS.CPY(M1)", extension: ".cpy" }, + { name: "TEST.DS.INCLUDE", extension: ".inc" }, + { name: "TEST.DS.PLX", extension: ".pli" }, + { name: "TEST.DS.SHELL", extension: ".sh" }, + { name: "TEST.DS.EXEC", extension: ".rexx" }, + { name: "TEST.DS.XML", extension: ".xml" }, + { name: "TEST.DS.ASM", extension: ".asm" }, + { name: "TEST.DS.ASSEMBLY", extension: ".asm" }, + { name: "TEST.DS.LOG", extension: ".log" }, + { name: "TEST.DS.SPFLOGS", extension: ".log" }, ]; for (const pair of pairs) { - expect(DatasetUtils.getLanguageId(pair.name)).toBe(pair.languageId); + expect(DatasetUtils.getExtension(pair.name)).toBe(pair.extension); } }); - it("returns null if no language ID was found", () => { - expect(DatasetUtils.getLanguageId("TEST.DS")).toBe(null); + it("returns null if no language was detected", () => { + expect(DatasetUtils.getExtension("TEST.DS")).toBe(null); }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index c43dca7f86..446f20b28f 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -18,7 +18,6 @@ import { UssFSProvider } from "../../../../src/trees/uss/UssFSProvider"; import { USSFileStructure } from "../../../../src/trees/uss/USSFileStructure"; const testProfile = createIProfile(); -const testProfileB = { ...createIProfile(), name: "sestest2", profile: { ...testProfile.profile, host: "fake2" } }; type TestUris = Record>; const testUris: TestUris = { @@ -1029,204 +1028,309 @@ describe("buildFileName", () => { }); describe("copyTree", () => { - describe("copying a file tree - same profiles (copy API)", () => { - it("with naming collisions", async () => { - const getInfoFromUri = jest - .spyOn(UssFSProvider.instance as any, "_getInfoFromUri") - // destination info + const getBlockMocks = (hasCopy: boolean = false) => { + const fileList = jest.fn(); + const copy = jest.fn(); + const create = jest.fn(); + const uploadFromBuffer = jest.fn(); + const ussApi = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue( + hasCopy + ? { + fileList, + copy, + create, + uploadFromBuffer, + } + : ({ fileList, create, uploadFromBuffer } as any) + ); + const getInfoFromUri = jest.spyOn(UssFSProvider.instance as any, "_getInfoFromUri"); + + return { + profile: createIProfile(), + profile2: { ...createIProfile(), name: "sestest2" }, + getInfoFromUri, + ussApi, + apiFuncs: { + fileList, + copy, + create, + uploadFromBuffer, + }, + }; + }; + it("throws error if file list for destination path was unsuccessful", async () => { + const blockMocks = getBlockMocks(); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/file", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderB", + }); + blockMocks.apiFuncs.fileList.mockReturnValueOnce({ + success: false, + errorMessage: "Unknown server-side error", + }); + await expect((UssFSProvider.instance as any).copyTree(sourceUri, destUri)).rejects.toThrow( + "Error fetching destination /folderB for paste action: Unknown server-side error" + ); + }); + describe("same profiles", () => { + it("copies a file into a destination folder - no collisions", async () => { + const blockMocks = getBlockMocks(true); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/file", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderB", + }); + blockMocks.getInfoFromUri .mockReturnValueOnce({ - profile: testProfile, - path: "/bFile.txt", + profile: blockMocks.profile, + path: "/folderB", }) - // source info .mockReturnValueOnce({ - profile: testProfile, - path: "/aFile.txt", + profile: blockMocks.profile, + path: "/folderA/file", }); - const mockUssApi = { - copy: jest.fn(), - create: jest.fn(), - fileList: jest.fn().mockResolvedValueOnce({ - apiResponse: { - items: [{ name: "bFile.txt" }], - }, - }), - uploadFromBuffer: jest.fn(), - }; - jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); - await (UssFSProvider.instance as any).copyTree( - testUris.file, - testUris.file.with({ - path: "/sestest/bFile.txt", - }), - { tree: { type: USSFileStructure.UssFileType.File } } - ); - expect(mockUssApi.copy).toHaveBeenCalledWith("/bFile (1).txt", { - from: "/aFile.txt", + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/file", + baseName: "file", + sessionName: "lpar.zosmf", + type: USSFileStructure.UssFileType.File, + }, + }); + expect(blockMocks.apiFuncs.copy).toHaveBeenCalledWith("/folderB/file", { + from: "/folderA/file", recursive: false, overwrite: true, }); - getInfoFromUri.mockRestore(); }); - - it("without naming collisions", async () => { - const getInfoFromUri = jest - .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") - // destination info + it("copies a file into a destination folder - collision", async () => { + const blockMocks = getBlockMocks(true); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/file", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderB", + }); + blockMocks.getInfoFromUri .mockReturnValueOnce({ - profile: testProfile, - path: "/bFile.txt", + profile: blockMocks.profile, + path: "/folderB", }) - // source info .mockReturnValueOnce({ - profile: testProfile, - path: "/aFile.txt", + profile: blockMocks.profile, + path: "/folderA/file", }); - const mockUssApi = { - copy: jest.fn(), - create: jest.fn(), - fileList: jest.fn().mockResolvedValueOnce({ - apiResponse: { - items: [{ name: "aFile.txt" }], - }, - }), - uploadFromBuffer: jest.fn(), - }; - jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); - await (UssFSProvider.instance as any).copyTree( - testUris.file, - testUris.file.with({ - path: "/sestest/bFile.txt", - }), - { tree: { type: USSFileStructure.UssFileType.File } } - ); - expect(mockUssApi.copy).toHaveBeenCalledWith("/bFile.txt", { - from: "/aFile.txt", + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [{ name: "file" }] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/file", + baseName: "file", + sessionName: "lpar.zosmf", + type: USSFileStructure.UssFileType.File, + }, + }); + expect(blockMocks.apiFuncs.copy).toHaveBeenCalledWith("/folderB/file (1)", { + from: "/folderA/file", recursive: false, overwrite: true, }); - getInfoFromUri.mockRestore(); + }); + it("copies a folder into a destination folder - no collisions", async () => { + const blockMocks = getBlockMocks(true); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/innerFolder", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderB", + }); + blockMocks.getInfoFromUri + .mockReturnValueOnce({ + profile: blockMocks.profile, + path: "/folderB", + }) + .mockReturnValueOnce({ + profile: blockMocks.profile, + path: "/folderA/innerFolder", + }); + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/innerFolder", + sessionName: "lpar.zosmf", + type: USSFileStructure.UssFileType.Directory, + }, + }); + expect(blockMocks.apiFuncs.copy).toHaveBeenCalledWith("/folderB/innerFolder", { + from: "/folderA/innerFolder", + recursive: true, + overwrite: true, + }); + }); + it("copies a folder into a destination folder - collision", async () => { + const blockMocks = getBlockMocks(true); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/innerFolder", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderB", + }); + blockMocks.getInfoFromUri + .mockReturnValueOnce({ + profile: blockMocks.profile, + path: "/folderB", + }) + .mockReturnValueOnce({ + profile: blockMocks.profile, + path: "/folderA/innerFolder", + }); + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [{ name: "innerFolder" }] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/innerFolder", + sessionName: "lpar.zosmf", + type: USSFileStructure.UssFileType.Directory, + }, + }); + expect(blockMocks.apiFuncs.copy).toHaveBeenCalledWith("/folderB/innerFolder (1)", { + from: "/folderA/innerFolder", + recursive: true, + overwrite: true, + }); }); }); - - describe("copying - different profiles", () => { - it("file: with naming collisions", async () => { - const getInfoFromUri = jest - .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") - // destination info + describe("different profiles", () => { + it("copies a file into a destination folder - collision", async () => { + const blockMocks = getBlockMocks(); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/file", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest2/folderB", + }); + blockMocks.getInfoFromUri .mockReturnValueOnce({ - profile: testProfileB, - path: "/aFile.txt", + profile: blockMocks.profile2, + path: "/folderB", }) - // source info .mockReturnValueOnce({ - profile: testProfile, - path: "/aFile.txt", + profile: blockMocks.profile, + path: "/folderA/file", }); - const mockUssApi = { - copy: jest.fn(), - create: jest.fn(), - fileList: jest.fn().mockResolvedValueOnce({ - apiResponse: { - items: [{ name: "bFile.txt" }], - }, - }), - uploadFromBuffer: jest.fn(), - }; - jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); - jest.spyOn((UssFSProvider as any).instance, "lookup").mockReturnValueOnce(testEntries.file); - jest.spyOn((UssFSProvider as any).instance, "readFile").mockResolvedValueOnce(testEntries.file.data); - await (UssFSProvider.instance as any).copyTree( - testUris.file, - testUris.file.with({ - path: "/sestest2/bFile.txt", - }), - { tree: { type: USSFileStructure.UssFileType.File } } - ); - expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(testEntries.file.data ?? []), "/aFile.txt"); - getInfoFromUri.mockRestore(); + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [{ name: "file" }] } }); + const lookupMock = jest.spyOn(UssFSProvider.instance, "lookup").mockReturnValue({ + name: "file", + type: FileType.File, + metadata: { + path: "/sestest/folderA/file", + profile: blockMocks.profile, + }, + wasAccessed: false, + data: new Uint8Array(), + ctime: 0, + mtime: 0, + size: 0, + }); + const readFileMock = jest.spyOn(UssFSProvider.instance, "readFile").mockResolvedValue(new Uint8Array([1, 2, 3])); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/file", + baseName: "file", + sessionName: "sestest", + type: USSFileStructure.UssFileType.File, + }, + }); + expect(lookupMock).toHaveBeenCalledWith(sourceUri); + expect(readFileMock).toHaveBeenCalledWith(sourceUri); + lookupMock.mockRestore(); + readFileMock.mockRestore(); }); - it("file: without naming collisions", async () => { - const getInfoFromUri = jest - .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") - // destination info + it("copies a folder into a destination folder - collision", async () => { + const blockMocks = getBlockMocks(); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/innerFolder", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest2/folderB", + }); + blockMocks.getInfoFromUri .mockReturnValueOnce({ - profile: testProfileB, - path: "/aFile.txt", + profile: blockMocks.profile2, + path: "/folderB", }) - // source info .mockReturnValueOnce({ - profile: testProfile, - path: "/aFile.txt", + profile: blockMocks.profile, + path: "/folderA/innerFolder", }); - const mockUssApi = { - copy: jest.fn(), - create: jest.fn(), - fileList: jest.fn().mockResolvedValueOnce({ - apiResponse: { - items: [{ name: "aFile.txt" }], - }, - }), - uploadFromBuffer: jest.fn(), - }; - jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValueOnce(mockUssApi as any); - jest.spyOn((UssFSProvider as any).instance, "lookup").mockReturnValueOnce(testEntries.file); - jest.spyOn((UssFSProvider as any).instance, "readFile").mockResolvedValueOnce(testEntries.file.data); - await (UssFSProvider.instance as any).copyTree( - testUris.file, - testUris.file.with({ - path: "/sestest2/aFile.txt", - }), - { tree: { type: USSFileStructure.UssFileType.File } } - ); - expect(mockUssApi.uploadFromBuffer).toHaveBeenCalledWith(Buffer.from(testEntries.file.data ?? []), "/aFile (1).txt"); - getInfoFromUri.mockRestore(); + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [{ name: "innerFolder" }] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/innerFolder", + sessionName: "lpar.zosmf", + type: USSFileStructure.UssFileType.Directory, + }, + }); + expect(blockMocks.apiFuncs.create).toHaveBeenCalledWith("/folderB/innerFolder (1)", "directory"); }); - it("folder", async () => { - const getInfoFromUri = jest - .spyOn((UssFSProvider as any).prototype, "_getInfoFromUri") - // destination info + it("copies a folder into a destination folder - no collision", async () => { + const blockMocks = getBlockMocks(); + const sourceUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest/folderA/innerFolder", + }); + const destUri = Uri.from({ + scheme: ZoweScheme.USS, + path: "/sestest2/folderB", + }); + blockMocks.getInfoFromUri .mockReturnValueOnce({ - profile: testProfileB, - path: "/aFolder", + profile: blockMocks.profile2, + path: "/folderB", }) - // source info .mockReturnValueOnce({ - profile: testProfile, - path: "/aFolder", + profile: blockMocks.profile, + path: "/folderA/innerFolder", }); - const mockUssApi = { - create: jest.fn(), - fileList: jest.fn().mockResolvedValue({ - apiResponse: { - items: [{ name: "aFile.txt" }], - }, - }), - uploadFromBuffer: jest.fn(), - }; - const ussApiMock = jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue(mockUssApi as any); - - const copyTreeSpy = jest.spyOn(UssFSProvider.instance as any, "copyTree"); - const fileInPathTree = { - baseName: "someFile.txt", - localUri: Uri.from({ scheme: ZoweScheme.USS, path: "/sestest/aFolder/someFile.txt" }), - type: USSFileStructure.UssFileType.File, - }; - const ussFileTree = { - type: USSFileStructure.UssFileType.Directory, - children: [fileInPathTree], - }; - await (UssFSProvider.instance as any).copyTree( - testUris.folder, - testUris.folder.with({ - path: "/sestest2/aFolder", - }), - { tree: ussFileTree } - ); - expect(mockUssApi.create).toHaveBeenCalledWith("/aFolder", "directory"); - expect(copyTreeSpy).toHaveBeenCalledTimes(2); - getInfoFromUri.mockRestore(); - ussApiMock.mockRestore(); + blockMocks.apiFuncs.fileList.mockResolvedValueOnce({ success: true, apiResponse: { items: [] } }); + await (UssFSProvider.instance as any).copyTree(sourceUri, destUri, { + overwrite: true, + tree: { + localUri: sourceUri, + ussPath: "/folderA/innerFolder", + sessionName: "sestest", + type: USSFileStructure.UssFileType.Directory, + }, + }); + expect(blockMocks.apiFuncs.create).toHaveBeenCalledWith("/folderB/innerFolder", "directory"); }); }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index 14f20dab26..a478e1e650 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -190,6 +190,14 @@ "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": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 2c32d7577d..7b0ed474b4 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -501,6 +501,8 @@ "$(sync~spin) 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": "", diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index 0846cf4809..bf6ddc1455 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -28,11 +28,11 @@ import { FileEntry, } from "@zowe/zowe-explorer-api"; import { IZosFilesResponse } from "@zowe/zos-files-for-zowe-sdk"; -import { DatasetUtils } from "./DatasetUtils"; import { Profiles } from "../../configuration/Profiles"; import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister"; import { ZoweLogger } from "../../tools/ZoweLogger"; import * as dayjs from "dayjs"; +import { DatasetUtils } from "./DatasetUtils"; export class DatasetFSProvider extends BaseProvider implements vscode.FileSystemProvider { private static _instance: DatasetFSProvider; @@ -55,29 +55,6 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem return DatasetFSProvider._instance; } - /** - * onDidOpenTextDocument event listener for the dataset provider. - * Updates the opened document with the correct language ID. - * @param doc The document received from the onDidOpenTextDocument event. - */ - public static async onDidOpenTextDocument(this: void, doc: vscode.TextDocument): Promise { - if (doc.uri.scheme !== ZoweScheme.DS) { - return; - } - - const parentPath = path.posix.basename(path.posix.join(doc.uri.path, "..")); - const languageId = DatasetUtils.getLanguageId(parentPath); - if (languageId == null) { - return; - } - - try { - await vscode.languages.setTextDocumentLanguage(doc, languageId); - } catch (err) { - ZoweLogger.warn(`Could not set document language for ${doc.fileName} - tried languageId '${languageId}'`); - } - } - public watch(_uri: vscode.Uri, _options: { readonly recursive: boolean; readonly excludes: readonly string[] }): vscode.Disposable { // ignore, fires for all changes... return new vscode.Disposable(() => {}); @@ -118,8 +95,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const pds = this._lookupParentDirectory(uri); resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(pds.name, { attributes: true }); } else { - // PDS or Data Set - resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.posix.basename(uri.path), { attributes: true }); + // Data Set + resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(path.parse(uri.path).name, { attributes: true }); } // Attempt to parse a successful API response and update the data set's cached stats. @@ -166,24 +143,24 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem for (const ds of resp.apiResponse?.items ?? resp.apiResponse ?? []) { let tempEntry = profileEntry.entries.get(ds.dsname); if (tempEntry == null) { + let name = ds.dsname; if (ds.dsorg === "PO" || ds.dsorg === "PO-E") { // Entry is a PDS tempEntry = new PdsEntry(ds.dsname); } else if (ds.dsorg === "VS") { // TODO: Add VSAM and ZFS support in Zowe Explorer continue; - } else if (ds.migr?.toUpperCase() === "YES") { - // migrated - tempEntry = new DsEntry(ds.dsname, false); } else { - // PS - tempEntry = new DsEntry(ds.dsname, false); + // PS or migrated + const extension = DatasetUtils.getExtension(ds.dsname); + name = extension ? (ds.dsname as string).concat(extension) : ds.dsname; + tempEntry = new DsEntry(name, false); } tempEntry.metadata = new DsEntryMetadata({ ...profileEntry.metadata, - path: path.posix.join(profileEntry.metadata.path, ds.dsname), + path: path.posix.join(profileEntry.metadata.path, name), }); - profileEntry.entries.set(ds.dsname, tempEntry); + profileEntry.entries.set(name, tempEntry); } } } @@ -193,13 +170,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem private async fetchEntriesForDataset(entry: PdsEntry, uri: vscode.Uri, uriInfo: UriFsInfo): Promise { const members = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).allMembers(path.posix.basename(uri.path)); + const pdsExtension = DatasetUtils.getExtension(entry.name); for (const ds of members.apiResponse?.items || []) { - let tempEntry = entry.entries.get(ds.member); + const fullMemberName = `${ds.member as string}${pdsExtension ?? ""}`; + let tempEntry = entry.entries.get(fullMemberName); if (tempEntry == null) { - tempEntry = new DsEntry(ds.member, true); - tempEntry.metadata = new DsEntryMetadata({ ...entry.metadata, path: path.posix.join(entry.metadata.path, ds.member) }); - entry.entries.set(ds.member, tempEntry); + tempEntry = new DsEntry(fullMemberName, true); + tempEntry.metadata = new DsEntryMetadata({ ...entry.metadata, path: path.posix.join(entry.metadata.path, fullMemberName) }); + entry.entries.set(fullMemberName, tempEntry); } } } @@ -220,10 +199,13 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const entryExists = entry != null; let entryIsDir = entry != null ? entry.type === vscode.FileType.Directory : false; + // /DATA.SET/MEMBER const uriPath = uri.path.substring(uriInfo.slashAfterProfilePos + 1).split("/"); const pdsMember = uriPath.length === 2; if (!entryExists) { - const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(uriPath[0], { attributes: true }); + const resp = await ZoweExplorerApiRegister.getMvsApi(uriInfo.profile).dataSet(entryIsDir ? uriPath[0] : path.parse(uriPath[0]).name, { + attributes: true, + }); if (resp.success) { const dsorg: string = resp.apiResponse?.items?.[0]?.dsorg; entryIsDir = pdsMember ? false : dsorg?.startsWith("PO") ?? false; @@ -446,13 +428,11 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem private async uploadEntry(parent: DirEntry, entry: DsEntry, content: Uint8Array, forceUpload?: boolean): Promise { const statusMsg = Gui.setStatusBarMessage(vscode.l10n.t("$(sync~spin) Saving data set...")); - const isPdsMember = FsDatasetsUtils.isPdsEntry(parent) && !FsAbstractUtils.isFilterEntry(parent); - const fullName = isPdsMember ? `${parent.name}(${entry.name})` : entry.name; let resp: IZosFilesResponse; try { const mvsApi = ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile); const profileEncoding = entry.encoding ? null : entry.metadata.profile.profile?.encoding; - resp = await mvsApi.uploadFromBuffer(Buffer.from(content), fullName, { + resp = await mvsApi.uploadFromBuffer(Buffer.from(content), entry.metadata.dsName, { binary: entry.encoding?.kind === "binary", encoding: entry.encoding?.kind === "other" ? entry.encoding.codepage : profileEncoding, etag: forceUpload ? undefined : entry.etag, @@ -495,7 +475,8 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem try { if (!entry) { - entry = new DsEntry(basename, FsDatasetsUtils.isPdsEntry(parent)); + const isPdsMember = FsDatasetsUtils.isPdsEntry(parent); + entry = new DsEntry(basename, isPdsMember); entry.data = content; const profInfo = parent.metadata ? new DsEntryMetadata({ @@ -561,13 +542,16 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem } public async delete(uri: vscode.Uri, _options: { readonly recursive: boolean }): Promise { - const entry = this.lookup(uri, false); + const entry = this.lookup(uri, false) as DsEntry | PdsEntry; const parent = this._lookupParentDirectory(uri); let fullName: string = ""; if (FsDatasetsUtils.isPdsEntry(parent)) { - fullName = `${parent.name}(${entry.name})`; - } else { + // PDS member + fullName = (entry as DsEntry).metadata.dsName; + } else if (FsDatasetsUtils.isPdsEntry(entry)) { fullName = entry.name; + } else { + fullName = entry.metadata.dsName; } try { @@ -604,11 +588,15 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const newName = path.posix.basename(newUri.path); try { - if (FsDatasetsUtils.isPdsEntry(entry)) { + if (FsDatasetsUtils.isPdsEntry(entry) || !entry.isMember) { await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).renameDataSet(oldName, newName); } else { const pdsName = path.basename(path.posix.join(entry.metadata.path, "..")); - await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).renameDataSetMember(pdsName, oldName, newName); + await ZoweExplorerApiRegister.getMvsApi(entry.metadata.profile).renameDataSetMember( + pdsName, + path.parse(oldName).name, + path.parse(newName).name + ); } } catch (err) { await Gui.errorMessage( diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts b/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts index 58fc048597..3575bf2404 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetInit.ts @@ -182,7 +182,6 @@ export class DatasetInit { await datasetProvider.onDidChangeConfiguration(e); }) ); - context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(DatasetFSProvider.onDidOpenTextDocument)); SharedInit.initSubscribers(context, datasetProvider); return datasetProvider; diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetUtils.ts b/packages/zowe-explorer/src/trees/dataset/DatasetUtils.ts index d93365a807..1483cf89a0 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetUtils.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetUtils.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { Types } from "@zowe/zowe-explorer-api"; +import { DS_EXTENSION_MAP, Types } from "@zowe/zowe-explorer-api"; import { Constants } from "../../configuration/Constants"; import { ZoweLogger } from "../../tools/ZoweLogger"; @@ -67,45 +67,23 @@ export class DatasetUtils { } /** - * Get the language ID of a Data Set for use with `vscode.languages.setTextDocumentLanguage` + * Get the file extension for a Data Set (or data set member) based on its name or its PDS name. */ - public static getLanguageId(label: string): string | null { + public static getExtension(label: string): string | null { const limit = 5; const bracket = label.indexOf("("); const split = bracket > -1 ? label.substring(0, bracket).split(".", limit) : label.split(".", limit); for (let i = split.length - 1; i > 0; i--) { - if (split[i] === "C") { - return "c"; - } - if (["JCL", "JCLLIB", "CNTL", "PROC", "PROCLIB"].includes(split[i])) { - return "jcl"; - } - if (["COBOL", "CBL", "COB", "SCBL"].includes(split[i])) { - return "cobol"; - } - if (["COPYBOOK", "COPY", "CPY", "COBCOPY"].includes(split[i])) { - return "copybook"; - } - if (["INC", "INCLUDE", "PLINC"].includes(split[i])) { - return "inc"; - } - if (["PLI", "PL1", "PLX", "PCX"].includes(split[i])) { - return "pli"; - } - if (["SH", "SHELL"].includes(split[i])) { - return "shellscript"; - } - if (["REXX", "REXEC", "EXEC"].includes(split[i])) { - return "rexx"; - } - if (split[i] === "XML") { - return "xml"; - } - if (split[i] === "ASM" || split[i].indexOf("ASSEMBL") > -1) { - return "asm"; - } - if (split[i] === "LOG" || split[i].indexOf("SPFLOG") > -1) { - return "log"; + for (const [ext, matches] of DS_EXTENSION_MAP.entries()) { + for (const match of matches) { + if (match instanceof RegExp) { + if (match.test(split[i])) { + return ext; + } + } else if (match.includes(split[i])) { + return ext; + } + } } } return null; diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index 4c55b307b3..cd5b9b93bf 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -39,6 +39,7 @@ import { AuthUtils } from "../../utils/AuthUtils"; import type { Definitions } from "../../configuration/Definitions"; import type { DatasetTree } from "./DatasetTree"; import { SharedTreeProviders } from "../shared/SharedTreeProviders"; +import { DatasetUtils } from "./DatasetUtils"; /** * A type of TreeItem used to represent sessions and data sets @@ -117,6 +118,8 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod path: `/${sessionLabel}/${this.label as string}`, }); if (this.contextValue === Constants.DS_DS_CONTEXT) { + const extension = DatasetUtils.getExtension(this.label as string); + this.resourceUri = this.resourceUri.with({ path: `${this.resourceUri.path}${extension ?? ""}` }); this.command = { command: "vscode.open", title: "", @@ -124,9 +127,10 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod }; } } else if (this.contextValue === Constants.DS_MEMBER_CONTEXT) { + const extension = DatasetUtils.getExtension(this.getParent().label as string); this.resourceUri = vscode.Uri.from({ scheme: ZoweScheme.DS, - path: `/${sessionLabel}/${this.getParent().label as string}/${this.label as string}`, + path: `/${sessionLabel}/${this.getParent().label as string}/${this.label as string}${extension ?? ""}`, }); this.command = { command: "vscode.open", diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index 747200b04e..d2a830f85d 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -619,11 +619,22 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const hasCopyApi = api.copy != null; - const apiResponse = await api.fileList(path.posix.join(destInfo.path, "..")); + // Get the up-to-date list of files in the destination directory + const apiResponse = await api.fileList(path.posix.join(destInfo.path)); + if (!apiResponse.success) { + throw vscode.FileSystemError.Unavailable( + vscode.l10n.t({ + message: "Error fetching destination {0} for paste action: {1}", + args: [destInfo.path, apiResponse.errorMessage ?? vscode.l10n.t("No error details given")], + comment: ["USS path", "Error message"], + }) + ); + } const fileList = apiResponse.apiResponse?.items; - const fileName = this.buildFileName(fileList, path.basename(destInfo.path)); - const outputPath = path.posix.join(destInfo.path, "..", fileName); + // Build the name of the destination file/folder, handling any potential name collisions + const fileName = this.buildFileName(fileList, path.basename(sourceInfo.path)); + const outputPath = path.posix.join(destInfo.path, fileName); if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) { await api.copy(outputPath, { @@ -648,11 +659,7 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv } } } else { - const fileEntry = this.lookup(source, true); - if (fileEntry == null) { - return; - } - + const fileEntry = this.lookup(source); if (!fileEntry.wasAccessed) { // must fetch contents of file first before pasting in new path await this.readFile(source); diff --git a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts index 4d5c496c02..0fe4354e75 100644 --- a/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/trees/uss/ZoweUSSNode.ts @@ -655,8 +655,13 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { if (!uss.api.fileList || (!hasCopy && !hasUploadFromBuffer)) { throw new Error(vscode.l10n.t("Required API functions for pasting (fileList and copy/uploadFromBuffer) were not found.")); } + const localUri = vscode.Uri.from({ + scheme: ZoweScheme.USS, + path: uss.tree.localUri.path, + query: `tree=${encodeURIComponent(JSON.stringify(uss.tree))}`, + }); - await UssFSProvider.instance.copy(uss.tree.localUri.with({ query: `tree=${encodeURIComponent(JSON.stringify(uss.tree))}` }), destUri, { + await UssFSProvider.instance.copy(localUri, destUri, { overwrite: true, }); }