diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3f8dd2748..8de5e0ac7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Use Node.js 18 + - name: Use Node.js 20 uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 registry-url: 'https://registry.npmjs.org' - uses: actions/checkout@v3 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 8f6e46cda..855742ee6 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -6,9 +6,12 @@ jobs: runs-on: ubuntu-latest if: contains(github.event.pull_request.labels.*.name, 'build') steps: - - uses: actions/setup-node@v2 + - name: Use Node.js 20 + uses: actions/setup-node@v3 with: - node-version: '16' + node-version: 20 + registry-url: 'https://registry.npmjs.org' + - uses: actions/checkout@v2 with: fetch-depth: 1 diff --git a/.github/workflows/prerelease.yaml b/.github/workflows/prerelease.yaml new file mode 100644 index 000000000..47d613211 --- /dev/null +++ b/.github/workflows/prerelease.yaml @@ -0,0 +1,51 @@ +on: workflow_dispatch + +jobs: + publish_prerelease: + runs-on: ubuntu-latest + + steps: + - name: Use Node.js 20 + uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://registry.npmjs.org' + + - uses: actions/checkout@v3 + with: + ref: master + + - name: Install dependencies + run: npm install + + - name: Update version number for prelease + run: | + npm version --no-git-tag-version patch + cd types && npm version --no-git-tag-version patch + + - name: get-npm-version + id: package-version + uses: martinbeentjes/npm-get-version-action@v1.3.1 + + - name: Package + run: npx @vscode/vsce package --pre-release + + - name: Publish + run: | + npx @vscode/vsce publish --skip-duplicate --packagePath code-for-ibmi-${{ steps.package-version.outputs.current-version}}.vsix --pat ${{ secrets.PUBLISHER_TOKEN }} + npx ovsx publish --skip-duplicate --packagePath code-for-ibmi-${{ steps.package-version.outputs.current-version}}.vsix --pat ${{ secrets.OPENVSX_TOKEN }} + + - name: Bump version number for next dev cycle + run: | + npm version --no-git-tag-version prerelease --preid dev + cd types && npm version --no-git-tag-version prerelease --preid dev + - name: get-npm-version + id: devcycle-version + uses: martinbeentjes/npm-get-version-action@v1.3.1 + + - name: Commit version bump + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git commit --allow-empty -a -m "Pre-release ${{ steps.devcycle-version.outputs.current-version}}" + git push \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 68eaed732..9269ad77a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-for-ibmi", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "code-for-ibmi", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "license": "MIT", "dependencies": { "@bendera/vscode-webview-elements": "^0.12.0", diff --git a/package.json b/package.json index e6fdf3cc4..ecc6ce03e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "icon": "icon.png", "displayName": "Code for IBM i", "description": "Maintain your RPGLE, CL, COBOL, C/CPP on IBM i right from Visual Studio Code.", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "keywords": [ "ibmi", "rpgle", @@ -1249,20 +1249,20 @@ { "command": "code-for-ibmi.refreshIFSBrowser", "enablement": "code-for-ibmi:connected", - "title": "Refresh IFS List", + "title": "Refresh", "category": "IBM i", "icon": "$(refresh)" }, { "command": "code-for-ibmi.refreshIFSBrowserItem", "enablement": "code-for-ibmi:connected", - "title": "Refresh IFS List item", + "title": "Refresh List item", "category": "IBM i" }, { "command": "code-for-ibmi.revealInIFSBrowser", "enablement": "code-for-ibmi:connected", - "title": "Reveal IFS Browser Item", + "title": "Reveal Browser Item", "category": "IBM i" }, { @@ -1275,7 +1275,7 @@ { "command": "code-for-ibmi.deleteIFS", "enablement": "code-for-ibmi:connected", - "title": "Delete Object", + "title": "Delete", "category": "IBM i" }, { @@ -1287,7 +1287,7 @@ { "command": "code-for-ibmi.moveIFS", "enablement": "code-for-ibmi:connected", - "title": "Rename or Move Object", + "title": "Rename/Move", "category": "IBM i", "icon": "$(files)" }, @@ -1301,21 +1301,21 @@ { "command": "code-for-ibmi.searchIFS", "enablement": "code-for-ibmi:connected", - "title": "Search Directory", + "title": "Search", "category": "IBM i", "icon": "$(search)" }, { "command": "code-for-ibmi.createDirectory", "enablement": "code-for-ibmi:connected", - "title": "Create Directory", + "title": "New Directory", "category": "IBM i", "icon": "$(new-folder)" }, { "command": "code-for-ibmi.createStreamfile", "enablement": "code-for-ibmi:connected", - "title": "Create Streamfile", + "title": "New File", "category": "IBM i", "icon": "$(new-file)" }, @@ -1349,49 +1349,49 @@ { "command": "code-for-ibmi.addIFSShortcut", "enablement": "code-for-ibmi:connected", - "title": "Add IFS Shortcut", + "title": "Add Shortcut", "category": "IBM i", "icon": "$(add)" }, { "command": "code-for-ibmi.removeIFSShortcut", "enablement": "code-for-ibmi:connected", - "title": "Remove IFS Shortcut", + "title": "Remove Shortcut", "category": "IBM i", "icon": "$(remove)" }, { "command": "code-for-ibmi.sortIFSShortcuts", "enablement": "code-for-ibmi:connected", - "title": "Sort IFS Shortcuts", + "title": "Sort Shortcuts", "category": "IBM i", "icon": "$(list-ordered)" }, { "command": "code-for-ibmi.moveIFSShortcutDown", "enablement": "code-for-ibmi:connected", - "title": "Move IFS Shortcut Down", + "title": "Move Shortcut Down", "category": "IBM i", "icon": "$(arrow-down)" }, { "command": "code-for-ibmi.moveIFSShortcutUp", "enablement": "code-for-ibmi:connected", - "title": "Move IFS Shortcut Up", + "title": "Move Shortcut Up", "category": "IBM i", "icon": "$(arrow-up)" }, { "command": "code-for-ibmi.moveIFSShortcutToTop", "enablement": "code-for-ibmi:connected", - "title": "Move IFS Shortcut to Top", + "title": "Move Shortcut to Top", "category": "IBM i", "icon": "$(arrow-up)" }, { "command": "code-for-ibmi.moveIFSShortcutToBottom", "enablement": "code-for-ibmi:connected", - "title": "Move IFS Shortcut to Bottom", + "title": "Move Shortcut to Bottom", "category": "IBM i", "icon": "$(arrow-up)" }, @@ -1468,7 +1468,7 @@ { "command": "code-for-ibmi.refreshObjectBrowser", "enablement": "code-for-ibmi:connected", - "title": "Refresh Object Browser", + "title": "Refresh", "category": "IBM i", "icon": "$(refresh)" }, @@ -1574,7 +1574,7 @@ }, { "command": "code-for-ibmi.openTerminalHere", - "title": "Open terminal here", + "title": "Open Terminal Here", "category": "IBM i" }, { @@ -2362,35 +2362,35 @@ "group": "1_workspace@1" }, { - "command": "code-for-ibmi.deleteIFS", - "when": "view == ifsBrowser && !(viewItem =~ /^.*_protected$/)", - "group": "2_ifsStuff@4" + "command": "code-for-ibmi.createStreamfile", + "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", + "group": "2_ifsStuff@1" }, { - "command": "code-for-ibmi.moveIFS", - "when": "view == ifsBrowser && !listMultiSelection && !(viewItem =~ /^.*_protected$/)", + "command": "code-for-ibmi.createDirectory", + "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", "group": "2_ifsStuff@2" }, { "command": "code-for-ibmi.copyIFS", "when": "view == ifsBrowser && !listMultiSelection && !(viewItem =~ /^.*_protected$/)", - "group": "2_ifsStuff@2" + "group": "2_ifsStuff@3" }, { - "command": "code-for-ibmi.createDirectory", - "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", - "group": "2_ifsStuff@2" + "command": "code-for-ibmi.moveIFS", + "when": "view == ifsBrowser && !listMultiSelection && !(viewItem =~ /^.*_protected$/)", + "group": "2_ifsStuff@4" + }, + { + "command": "code-for-ibmi.deleteIFS", + "when": "view == ifsBrowser && !(viewItem =~ /^.*_protected$/)", + "group": "2_ifsStuff@5" }, { "submenu": "code-for-ibmi.sortIFSFiles", "when": "view == ifsBrowser && !listMultiSelection", "group": "5_ifsStuff@1" }, - { - "command": "code-for-ibmi.createStreamfile", - "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", - "group": "2_ifsStuff@1" - }, { "command": "code-for-ibmi.searchIFS", "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^directory.*$/", @@ -2407,14 +2407,14 @@ "group": "3_ifsStuff@3" }, { - "command": "code-for-ibmi.createDirectory", + "command": "code-for-ibmi.createStreamfile", "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^shortcut.*$/", - "group": "2_ifsStuff@2" + "group": "2_ifsStuff@1" }, { - "command": "code-for-ibmi.createStreamfile", + "command": "code-for-ibmi.createDirectory", "when": "view == ifsBrowser && !listMultiSelection && viewItem =~ /^shortcut.*$/", - "group": "2_ifsStuff@1" + "group": "2_ifsStuff@2" }, { "command": "code-for-ibmi.searchIFS", diff --git a/src/api/IBMi.ts b/src/api/IBMi.ts index 763881ba5..7ef974e07 100644 --- a/src/api/IBMi.ts +++ b/src/api/IBMi.ts @@ -7,7 +7,6 @@ import path from 'path'; import { instance } from "../instantiate"; import { CommandData, CommandResult, ConnectionData, IBMiMember, RemoteCommand } from "../typings"; import { CompileTools } from "./CompileTools"; -import IBMiContent from "./IBMiContent"; import { CachedServerSettings, GlobalStorage } from './Storage'; import { Tools } from './Tools'; import * as configVars from './configVars'; @@ -15,8 +14,9 @@ import * as configVars from './configVars'; export interface MemberParts extends IBMiMember { basename: string } +const CCSID_SYSVAL = -2; -let remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!! +const remoteApps = [ // All names MUST also be defined as key in 'remoteFeatures' below!! { path: `/usr/bin/`, names: [`setccsid`, `iconv`, `attr`, `tar`, `ls`] @@ -50,7 +50,8 @@ export default class IBMi { defaultUserLibraries: string[]; outputChannel?: vscode.OutputChannel; aspInfo: { [id: number]: string }; - qccsid: number | null; + qccsid: number; + defaultCCSID: number; remoteFeatures: { [name: string]: string | undefined }; variantChars: { american: string, local: string }; lastErrors: object[]; @@ -74,8 +75,8 @@ export default class IBMi { * the root of the IFS, thus why we store it. */ this.aspInfo = {}; - - this.qccsid = null; + this.qccsid = CCSID_SYSVAL; + this.defaultCCSID = 0; this.remoteFeatures = { git: undefined, @@ -428,7 +429,7 @@ export default class IBMi { }) this.sendCommand({ - command: `rm -f ${path.posix.join(this.config.tempDir, `vscodetemp*`)}` + command: `rm -rf ${path.posix.join(this.config.tempDir, `vscodetemp*`)}` }) .then(result => { // All good! @@ -560,8 +561,20 @@ export default class IBMi { } if (this.remoteFeatures[`QZDFMDB2.PGM`]) { - let statement; - let output; + //Temporary function to run SQL + const runSQL = async (statement: string) => { + const output = await this.sendCommand({ + command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, + stdin: statement + }); + + if (output.code === 0) { + return Tools.db2Parse(output.stdout); + } + else { + throw new Error(output.stdout); + } + }; // Check for ASP information? if (quickConnect === true && cachedServerSettings?.aspInfo) { @@ -574,7 +587,7 @@ export default class IBMi { //This is mostly a nice to have. We grab the ASP info so user's do //not have to provide the ASP in the settings. try { - const resultSet = await new IBMiContent(this).runSQL(`SELECT * FROM QSYS2.ASP_INFO`); + const resultSet = await runSQL(`SELECT * FROM QSYS2.ASP_INFO`); resultSet.forEach(row => { if (row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME && row.DEVICE_DESCRIPTION_NAME !== `null`) { this.aspInfo[Number(row.ASP_NUMBER)] = String(row.DEVICE_DESCRIPTION_NAME); @@ -589,9 +602,10 @@ export default class IBMi { } // Fetch conversion values? - if (quickConnect === true && cachedServerSettings?.qccsid !== null && cachedServerSettings?.variantChars) { + if (quickConnect === true && cachedServerSettings?.qccsid !== null && cachedServerSettings?.variantChars && cachedServerSettings?.defaultCCSID) { this.qccsid = cachedServerSettings.qccsid; this.variantChars = cachedServerSettings.variantChars; + this.defaultCCSID = cachedServerSettings.defaultCCSID; } else { progress.report({ message: `Fetching conversion values.` @@ -599,34 +613,32 @@ export default class IBMi { // Next, we're going to see if we can get the CCSID from the user or the system. // Some things don't work without it!!! - try { - const CCSID_SYSVAL = -2; - statement = `select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`; - output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); + try { + const [userInfo] = await runSQL(`select CHARACTER_CODE_SET_ID from table( QSYS2.QSYUSRINFO( USERNAME => upper('${this.currentUser}') ) )`); + if (userInfo.CHARACTER_CODE_SET_ID !== `null` && typeof userInfo.CHARACTER_CODE_SET_ID === 'number') { + this.qccsid = userInfo.CHARACTER_CODE_SET_ID; + } - if (output.stdout) { - const [row] = Tools.db2Parse(output.stdout); - if (row && row.CHARACTER_CODE_SET_ID !== `null` && typeof row.CHARACTER_CODE_SET_ID === 'number') { - this.qccsid = row.CHARACTER_CODE_SET_ID; + if (!this.qccsid || this.qccsid === CCSID_SYSVAL) { + const [systemCCSID] = await runSQL(`select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`); + if (typeof systemCCSID.CURRENT_NUMERIC_VALUE === 'number') { + this.qccsid = systemCCSID.CURRENT_NUMERIC_VALUE; } } - if (this.qccsid === undefined || this.qccsid === CCSID_SYSVAL) { - statement = `select SYSTEM_VALUE_NAME, CURRENT_NUMERIC_VALUE from QSYS2.SYSTEM_VALUE_INFO where SYSTEM_VALUE_NAME = 'QCCSID'`; - output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); - - if (output.stdout) { - const rows = Tools.db2Parse(output.stdout); - const ccsid = rows.find(row => row.SYSTEM_VALUE_NAME === `QCCSID`); - if (ccsid && typeof ccsid.CURRENT_NUMERIC_VALUE === 'number') { - this.qccsid = ccsid.CURRENT_NUMERIC_VALUE; - } + try { + const [activeJob] = await runSQL(`Select DEFAULT_CCSID From Table(QSYS2.ACTIVE_JOB_INFO( JOB_NAME_FILTER => '*', DETAILED_INFO => 'ALL' ))`); + this.defaultCCSID = Number(activeJob.DEFAULT_CCSID); + } + catch (error) { + const [defaultCCSID] = (await this.runCommand({ command: "DSPJOB OPTION(*DFNA)" })) + .stdout + .split("\n") + .filter(line => line.includes("DFTCCSID")); + + const defaultCCSCID = Number(defaultCCSID.split("DFTCCSID").at(1)?.trim()); + if (defaultCCSCID && !isNaN(defaultCCSCID)) { + this.defaultCCSID = defaultCCSCID; } } @@ -639,24 +651,15 @@ export default class IBMi { message: `Fetching local encoding values.` }); - statement = `with VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + const [variants] = await runSQL(`With VARIANTS ( HASH, AT, DOLLARSIGN ) as (` + ` values ( cast( x'7B' as varchar(1) )` + ` , cast( x'7C' as varchar(1) )` + ` , cast( x'5B' as varchar(1) ) )` + `)` - + `select HASH concat AT concat DOLLARSIGN as LOCAL` - + ` from VARIANTS; `; - output = await this.sendCommand({ - command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i')"`, - stdin: statement - }); - if (output.stdout) { - const [row] = Tools.db2Parse(output.stdout); - if (row && row.LOCAL !== `null` && typeof row.LOCAL === 'string') { - this.variantChars.local = row.LOCAL; - } - } else { - throw new Error(`There was an error running the SQL statement.`); + + `Select HASH concat AT concat DOLLARSIGN as LOCAL from VARIANTS`); + + if (typeof variants.LOCAL === 'string' && variants.LOCAL !== `null`) { + this.variantChars.local = variants.LOCAL; } } catch (e) { // Oh well! @@ -665,7 +668,6 @@ export default class IBMi { } } else { // Disable it if it's not found - if (this.config.enableSQL) { progress.report({ message: `SQL program not installed. Disabling SQL.` @@ -674,6 +676,9 @@ export default class IBMi { } } + if((this.qccsid < 1 || this.qccsid === 65535)){ + this.outputChannel?.appendLine(`\nUser CCSID is ${this.qccsid}; falling back to using default CCSID ${this.defaultCCSID}\n`); + } // give user option to set bash as default shell. if (this.remoteFeatures[`bash`]) { @@ -866,7 +871,8 @@ export default class IBMi { }, badDataAreasChecked: true, libraryListValidated: true, - pathChecked: true + pathChecked: true, + defaultCCSID: this.defaultCCSID }); return { diff --git a/src/api/IBMiContent.ts b/src/api/IBMiContent.ts index e66bb244c..5888a6fe1 100644 --- a/src/api/IBMiContent.ts +++ b/src/api/IBMiContent.ts @@ -25,7 +25,7 @@ export type SortOptions = { } export default class IBMiContent { - + private chgJobCCSID: string | undefined = undefined; constructor(readonly ibmi: IBMi) { } private get config(): ConnectionConfiguration.Parameters { @@ -169,7 +169,7 @@ export default class IBMiContent { switch (messageID) { case "CPDA08A": //We need to try again after we delete the temp remote - const result = await this.ibmi.sendCommand({ command: `rm -f ${tempRmt}`, directory: `.` }); + const result = await this.ibmi.sendCommand({ command: `rm -rf ${tempRmt}`, directory: `.` }); retry = !result.code || result.code === 0; break; case "CPFA0A9": @@ -246,25 +246,24 @@ export default class IBMiContent { } /** - * Run an SQL statement - * @param statement + * Run SQL statements. + * Each statement must be separated by a semi-colon and a new line (i.e. ;\n). + * If a statement starts with @, it will be run as a CL command. + * + * @param statements * @returns a Result set */ - async runSQL(statement: string): Promise { + async runSQL(statements: string): Promise { const { 'QZDFMDB2.PGM': QZDFMDB2 } = this.ibmi.remoteFeatures; if (QZDFMDB2) { - // Well, the fun part about db2 is that it always writes to standard out. - // It does not write to standard error at all. - - // if comments present in sql statement, sql string needs to be checked - if (statement.search(`--`) > -1) { - statement = this.fixCommentsInSQLString(statement); - } + if (this.chgJobCCSID === undefined) { + this.chgJobCCSID = (this.ibmi.qccsid < 1 || this.ibmi.qccsid === 65535) && this.ibmi.defaultCCSID > 0 ? `@CHGJOB CCSID(${this.ibmi.defaultCCSID});\n` : ''; + } const output = await this.ibmi.sendCommand({ command: `LC_ALL=EN_US.UTF-8 system "call QSYS/QZDFMDB2 PARM('-d' '-i' '-t')"`, - stdin: statement, + stdin: Tools.fixSQL(`${this.chgJobCCSID}${statements}`) }) if (output.stdout) { @@ -345,7 +344,7 @@ export default class IBMiContent { if (this.config.autoClearTempData) { Promise.allSettled([ - this.ibmi.sendCommand({ command: `rm -f ${tempRmt}`, directory: `.` }), + this.ibmi.sendCommand({ command: `rm -rf ${tempRmt}`, directory: `.` }), deleteTable ? this.ibmi.runCommand({ command: `DLTOBJ OBJ(${library}/${file}) OBJTYPE(*FILE)`, noLibList: true }) : Promise.resolve() ]); } @@ -376,34 +375,9 @@ export default class IBMiContent { * @returns : the table's content */ async getQTempTable(prepareQueries: string[], table: string): Promise { - let temporaryFile: string | undefined; - prepareQueries.push(`Select * From QTEMP.${table}`); - - try { - const fullQuery = prepareQueries.map(query => query.endsWith(';') ? query : `${query};`).join("\n"); - const result = await this.runSQL(fullQuery); - if (temporaryFile) { - return parse(await this.downloadStreamfile(temporaryFile), { - columns: true, - skip_empty_lines: true, - cast: true, - onRecord(record) { - for (const key of Object.keys(record)) { - record[key] = record[key] === ` ` ? `` : record[key]; - } - return record; - } - }); - } - else { - return result; - } - } - finally { - if (this.config.autoClearTempData && temporaryFile) { - await this.ibmi.sendCommand({ command: `rm -f ${temporaryFile}` }); - } - } + prepareQueries.push(`Select * From QTEMP.${table}`); + const fullQuery = prepareQueries.map(query => query.endsWith(';') ? query : `${query};`).join("\n"); + return await this.runSQL(fullQuery); } /** @@ -412,9 +386,6 @@ export default class IBMiContent { * @returns an array of libraries as IBMiObject */ async getLibraryList(libraries: string[]): Promise { - const config = this.ibmi.config; - const tempLib = this.config.tempLibrary; - const TempName = Tools.makeid(); let results: Tools.DB2Row[]; if (this.config.enableSQL) { @@ -498,7 +469,7 @@ export default class IBMiContent { } async getLibraries(filters: { library: string; filterType?: FilterType }) { - return this.getObjectList({ library: "QSYS", object: filters.library, types: ["*LIB"], filterType:filters.filterType }); + return this.getObjectList({ library: "QSYS", object: filters.library, types: ["*LIB"], filterType: filters.filterType }); } /** @@ -525,11 +496,11 @@ export default class IBMiContent { const queries: string[] = []; if (!sourceFilesOnly) { - queries.push(`CALL QSYS2.QCMDEXC('DSPOBJD OBJ(${library}/${object}) OBJTYPE(${type}) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IOBJD)')`); + queries.push(`@DSPOBJD OBJ(${library}/${object}) OBJTYPE(${type}) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IOBJD)`); } if (withSourceFiles) { - queries.push(`CALL QSYS2.QCMDEXC('DSPFD FILE(${library}/${object}) TYPE(*ATR) FILEATR(*PF) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IFD)')`); + queries.push(`@DSPFD FILE(${library}/${object}) TYPE(*ATR) FILEATR(*PF) OUTPUT(*OUTFILE) OUTFILE(QTEMP/CODE4IFD)`); } let createOBJLIST; @@ -601,10 +572,10 @@ export default class IBMiContent { const sourceFile = filter.sourceFile.toUpperCase(); const memberFilter = parseFilter(filter.members, filter.filterType); - const singleMember = memberFilter.noFilter && filter.members && !filter.members.includes(",") ? filter.members.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; + const singleMember = memberFilter.noFilter && filter.members && !filter.members.includes(",") ? filter.members.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; const memberExtensionFilter = parseFilter(filter.extensions, filter.filterType); - const singleMemberExtension = memberExtensionFilter.noFilter && filter.extensions && !filter.extensions.includes(",") ? filter.extensions.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; + const singleMemberExtension = memberExtensionFilter.noFilter && filter.extensions && !filter.extensions.includes(",") ? filter.extensions.toLocaleUpperCase().replace(/[*]/g, `%`) : undefined; const statement = `With MEMBERS As ( @@ -615,7 +586,7 @@ export default class IBMiContent { rtrim(cast(a.system_table_name as char(10) for bit data)) AS SOURCE_FILE, rtrim(cast(b.system_table_member as char(10) for bit data)) as NAME, coalesce(rtrim(cast(b.source_type as varchar(10) for bit data)), '') as TYPE, - coalesce(rtrim(b.partition_text), '') as TEXT, + coalesce(rtrim(varchar(b.partition_text)), '') as TEXT, b.NUMBER_ROWS as LINES, extract(epoch from (b.CREATE_TIMESTAMP))*1000 as CREATED, extract(epoch from (b.LAST_SOURCE_UPDATE_TIMESTAMP))*1000 as CHANGED @@ -631,15 +602,8 @@ export default class IBMiContent { ${singleMemberExtension ? `And TYPE Like '${singleMemberExtension}'` : ''} Order By ${sort.order === 'name' ? 'NAME' : 'CHANGED'} ${!sort.ascending ? 'DESC' : 'ASC'}`; - let results: Tools.DB2Row[]; - if (this.config.enableSQL) { - results = await this.runSQL(statement); - } - else { - results = await this.getQTempTable([`Create Table QTEMP.MEMBERSLST As (${statement}) With DATA`], "MEMBERSLST"); - } - - if(results.length){ + const results = await this.runSQL(statement); + if (results.length) { const asp = this.ibmi.aspInfo[Number(results[0].ASP)]; return results.map(result => ({ asp, @@ -653,10 +617,10 @@ export default class IBMiContent { created: new Date(result.CREATED ? Number(result.CREATED) : 0), changed: new Date(result.CHANGED ? Number(result.CHANGED) : 0) } as IBMiMember)) - .filter(member => memberFilter.test(member.name)) - .filter(member => memberExtensionFilter.test(member.extension)); + .filter(member => memberFilter.test(member.name)) + .filter(member => memberExtensionFilter.test(member.extension)); } - else{ + else { return []; } } @@ -820,34 +784,6 @@ export default class IBMiContent { return undefined; } - /** - * Fix Comments in an SQL string so that the comments always start at position 0 of the line. - * Required to work with QZDFMDB2. - * @param inSql; sql statement - * @returns correctly formattted sql string containing comments - */ - private fixCommentsInSQLString(inSql: string): string { - const newLine: string = `\n`; - let parsedSql: string = ``; - - inSql.split(newLine) - .forEach(item => { - let goodLine = item + newLine; - - const pos = item.search(`--`); - if (pos > 0) { - goodLine = item.slice(0, pos) + - newLine + - item.slice(pos) + - newLine; - } - parsedSql += goodLine; - - }); - - return parsedSql; - } - /** * @param errorsString; several lines of `code:text`... * @returns errors @@ -914,7 +850,7 @@ export default class IBMiContent { * @param parameters A key/value object of parameters * @returns Formatted CL string */ - static toCl(command: string, parameters: {[parameter: string]: string|number|undefined}) { + static toCl(command: string, parameters: { [parameter: string]: string | number | undefined }) { let cl = command; for (const [key, value] of Object.entries(parameters)) { @@ -938,4 +874,4 @@ export default class IBMiContent { return cl; } -} +} \ No newline at end of file diff --git a/src/api/Storage.ts b/src/api/Storage.ts index 44ad5f35c..017616fe4 100644 --- a/src/api/Storage.ts +++ b/src/api/Storage.ts @@ -45,6 +45,7 @@ export type CachedServerSettings = { badDataAreasChecked: boolean | null, libraryListValidated: boolean | null, pathChecked?: boolean + defaultCCSID: number | null; } | undefined; export class GlobalStorage extends Storage { diff --git a/src/api/Tools.ts b/src/api/Tools.ts index d18b5e018..c651a32a2 100644 --- a/src/api/Tools.ts +++ b/src/api/Tools.ts @@ -37,10 +37,11 @@ export namespace Tools { const trimmed = line.trim(); return trimmed !== `DB2>` && !trimmed.startsWith(`DB20`) && // Notice messages + !/COMMAND .+ COMPLETED WITH EXIT STATUS \d+/.test(trimmed) && // @CL command execution output trimmed !== `?>`; }); - if(!data[data.length-1]){ + if (!data[data.length - 1]) { data.pop(); } @@ -242,20 +243,42 @@ export namespace Tools { } } - export function parseQSysPath(path: string) : QsysPath{ + export function parseQSysPath(path: string): QsysPath { const parts = path.split('/'); - if(parts.length > 3){ + if (parts.length > 3) { return { asp: parts[0], library: parts[1], name: parts[2] } } - else{ + else { return { library: parts[0], name: parts[1] } } } + + /** + * Fixes an SQL statement to make it compatible with db2 CLI program QZDFMDB2. + * - Changes `@clCommand` statements into Call `QSYS2.QCMDEX('clCommand')` procedure calls + * - Makes sure each comment (`--`) starts on a new line + * @param statement the statement to fix + * @returns statement compatible with QZDFMDB2 + */ + export function fixSQL(statement: string) { + return statement.split("\n").map(line => { + if (line.startsWith('@')) { + //- Escape all ' + //- Remove any trailing ; + //- Put the command in a Call QSYS2.QCMDEXC statement + line = `Call QSYS2.QCMDEXC('${line.substring(1, line.endsWith(";") ? line.length - 1 : undefined).replaceAll("'", "''")}');`; + } + + //Make each comment start on a new line + return line.replaceAll("--", "\n--"); + } + ).join("\n"); + } } \ No newline at end of file diff --git a/src/api/local/deployment.ts b/src/api/local/deployment.ts index d7f9f2882..be07dd9e3 100644 --- a/src/api/local/deployment.ts +++ b/src/api/local/deployment.ts @@ -198,7 +198,7 @@ export namespace Deployment { export async function deleteFiles(parameters: DeploymentParameters, toDelete: string[]) { if (toDelete.length) { Deployment.deploymentLog.appendLine(`\nDeleted:\n\t${toDelete.join('\n\t')}\n`); - await Deployment.getConnection().sendCommand({ directory: parameters.remotePath, command: `rm -f ${toDelete.join(' ')}` }); + await Deployment.getConnection().sendCommand({ directory: parameters.remotePath, command: `rm -rf ${toDelete.join(' ')}` }); } } diff --git a/src/testing/content.ts b/src/testing/content.ts index 32a507c44..d50a77ac0 100644 --- a/src/testing/content.ts +++ b/src/testing/content.ts @@ -591,5 +591,15 @@ export const ContentSuite: TestSuite = { assert.ok(exists); } }, + { + name: `Test @clCommand + select statement`, test: async () => { + const content = instance.getContent()!; + + const [result] = await content.runSQL(`@CRTSAVF FILE(QTEMP/UNITTEST) TEXT('Code for i test');\nSelect * From Table(QSYS2.OBJECT_STATISTICS('QTEMP', '*FILE')) Where OBJATTRIBUTE = 'SAVF';`); + + assert.deepStrictEqual(result.OBJNAME, "UNITTEST"); + assert.deepStrictEqual(result.OBJTEXT, "Code for i test"); + } + }, ] }; diff --git a/src/testing/tools.ts b/src/testing/tools.ts index e96648844..e1501b089 100644 --- a/src/testing/tools.ts +++ b/src/testing/tools.ts @@ -1,37 +1,53 @@ import assert from "assert"; import { TestSuite } from "."; -import { instance } from "../instantiate"; import { Tools } from "../api/Tools"; export const ToolsSuite: TestSuite = { name: `Tools API tests`, tests: [ - {name: `unqualifyPath (In a named library)`, test: async () => { - const qualifiedPath = `/QSYS.LIB/MYLIB.LIB/DEVSRC.FILE/THINGY.MBR`; - const simplePath = Tools.unqualifyPath(qualifiedPath); - - assert.strictEqual(simplePath, `/MYLIB/DEVSRC/THINGY.MBR`); - }}, - - {name: `unqualifyPath (In QSYS)`, test: async () => { - const qualifiedPath = `/QSYS.LIB/DEVSRC.FILE/THINGY.MBR`; - const simplePath = Tools.unqualifyPath(qualifiedPath); - - assert.strictEqual(simplePath, `/QSYS/DEVSRC/THINGY.MBR`); - }}, - - {name: `unqualifyPath (In an ASP)`, test: async () => { - const qualifiedPath = `/myasp/QSYS.LIB/MYLIB.LIB/DEVSRC.FILE/THINGY.MBR`; - const simplePath = Tools.unqualifyPath(qualifiedPath); - - assert.strictEqual(simplePath, `/myasp/MYLIB/DEVSRC/THINGY.MBR`); - }}, - - {name: `sanitizeLibraryNames ($ and #)`, test: async () => { - const rawLibraryNames = [`QTEMP`, `#LIBRARY`, `My$lib`, `qsysinc`]; - const sanitizedLibraryNames = Tools.sanitizeLibraryNames(rawLibraryNames); - - assert.deepStrictEqual(sanitizedLibraryNames, [`QTEMP`, `"#LIBRARY"`, `My\\$lib`, `qsysinc`]); - }}, + { + name: `unqualifyPath (In a named library)`, test: async () => { + const qualifiedPath = `/QSYS.LIB/MYLIB.LIB/DEVSRC.FILE/THINGY.MBR`; + const simplePath = Tools.unqualifyPath(qualifiedPath); + + assert.strictEqual(simplePath, `/MYLIB/DEVSRC/THINGY.MBR`); + } + }, + + { + name: `unqualifyPath (In QSYS)`, test: async () => { + const qualifiedPath = `/QSYS.LIB/DEVSRC.FILE/THINGY.MBR`; + const simplePath = Tools.unqualifyPath(qualifiedPath); + + assert.strictEqual(simplePath, `/QSYS/DEVSRC/THINGY.MBR`); + } + }, + + { + name: `unqualifyPath (In an ASP)`, test: async () => { + const qualifiedPath = `/myasp/QSYS.LIB/MYLIB.LIB/DEVSRC.FILE/THINGY.MBR`; + const simplePath = Tools.unqualifyPath(qualifiedPath); + + assert.strictEqual(simplePath, `/myasp/MYLIB/DEVSRC/THINGY.MBR`); + } + }, + + { + name: `sanitizeLibraryNames ($ and #)`, test: async () => { + const rawLibraryNames = [`QTEMP`, `#LIBRARY`, `My$lib`, `qsysinc`]; + const sanitizedLibraryNames = Tools.sanitizeLibraryNames(rawLibraryNames); + + assert.deepStrictEqual(sanitizedLibraryNames, [`QTEMP`, `"#LIBRARY"`, `My\\$lib`, `qsysinc`]); + }, + }, + { + name: `fixQZDFMDB2Statement`, test: async () => { + let statement = Tools.fixSQL('Select * From MYTABLE -- This is a comment') + assert.deepStrictEqual(statement, 'Select * From MYTABLE \n-- This is a comment'); + + statement = Tools.fixSQL("@COMMAND LIB(QTEMP/*ALL) TEXT('Hello!');\nSelect * From QTEMP.MYTABLE -- This is mytable"); + assert.deepStrictEqual(statement, "Call QSYS2.QCMDEXC('COMMAND LIB(QTEMP/*ALL) TEXT(''Hello!'')');\nSelect * From QTEMP.MYTABLE \n-- This is mytable"); + } + } ] }; diff --git a/src/views/helpView.ts b/src/views/helpView.ts index 8ecaae2ac..bebead702 100644 --- a/src/views/helpView.ts +++ b/src/views/helpView.ts @@ -152,6 +152,7 @@ async function getRemoteSection() { `|IBM i OS|${osVersion?.OS || '?'}|`, `|Tech Refresh|${osVersion?.TR || '?'}|`, `|CCSID|${connection.qccsid || '?'}|`, + `|Default CCSID|${connection.defaultCCSID || '?'}|`, `|SQL|${config.enableSQL ? 'Enabled' : 'Disabled'}`, `|Source dates|${config.enableSourceDates ? 'Enabled' : 'Disabled'}`, '', diff --git a/src/webviews/settings/index.ts b/src/webviews/settings/index.ts index 072d2ced0..3f00e8024 100644 --- a/src/webviews/settings/index.ts +++ b/src/webviews/settings/index.ts @@ -57,6 +57,7 @@ export class SettingsUI { .addCheckbox(`enableSQL`, `Enable SQL`, `Must be enabled to make the use of SQL and is enabled by default. If you find SQL isn't working for some reason, disable this. If your QCCSID system value is set to 65535, it is recommended that SQL is disabled. When disabled, will use import files where possible.`, config.enableSQL) .addCheckbox(`showDescInLibList`, `Show description of libraries in User Library List view`, `When enabled, library text and attribute will be shown in User Library List. It is recommended to also enable SQL for this.`, config.showDescInLibList) .addCheckbox(`showHiddenFiles`, `Show hidden files and directories in IFS browser.`, `When diabled, hidden files and directories (i.e. names starting with '.') will not be shown in the IFS browser, except for special config files.`, config.showHiddenFiles) + .addCheckbox(`autoSortIFSShortcuts`, `Sort IFS shortcuts automatically`, `Automatically sort the shortcuts in IFS browser when shortcut is added or removed.`, config.autoSortIFSShortcuts) .addCheckbox(`autoConvertIFSccsid`, `Support EBCDIC streamfiles`, `Enable converting EBCDIC to UTF-8 when opening streamfiles. When disabled, assumes all streamfiles are in UTF8. When enabled, will open streamfiles regardless of encoding. May slow down open and save operations.

You can find supported CCSIDs with /usr/bin/iconv -l`, config.autoConvertIFSccsid) .addHorizontalRule() .addCheckbox(`autoSaveBeforeAction`, `Auto Save for Actions`, `When current editor has unsaved changes, automatically save it before running an action.`, config.autoSaveBeforeAction) @@ -66,8 +67,7 @@ export class SettingsUI { tempDataTab .addInput(`tempLibrary`, `Temporary library`, `Temporary library. Cannot be QTEMP.`, { default: config.tempLibrary, minlength: 1, maxlength: 10 }) .addInput(`tempDir`, `Temporary IFS directory`, `Directory that will be used to write temporary files to. User must be authorized to create new files in this directory.`, { default: config.tempDir, minlength: 1 }) - .addCheckbox(`autoClearTempData`, `Clear temporary data automatically`, `Automatically clear temporary data in the chosen temporary library when it's done with and on startup. Deletes all *FILE objects that start with O_ in the chosen temporary library.`, config.autoClearTempData) - .addCheckbox(`autoSortIFSShortcuts`, `Sort IFS shortcuts automatically`, `Automatically sort the shortcuts in IFS browser when shortcut is added or removed.`, config.autoSortIFSShortcuts); + .addCheckbox(`autoClearTempData`, `Clear temporary data automatically`, `Automatically clear temporary data in the chosen temporary library when it's done with and on startup. Deletes all *FILE objects that start with O_ in the chosen temporary library.`, config.autoClearTempData); const sourceTab = new Section(); sourceTab diff --git a/tsconfig.json b/tsconfig.json index 22bb98aef..4e25136e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "ES2021", + "target": "ES2022", "outDir": "out", "lib": [ - "ES2021" + "ES2022" ], "forceConsistentCasingInFileNames": true, "allowJs": true, diff --git a/types/package-lock.json b/types/package-lock.json index 8254b7d1d..1cbb0a6db 100644 --- a/types/package-lock.json +++ b/types/package-lock.json @@ -1,12 +1,12 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "license": "ISC" } } diff --git a/types/package.json b/types/package.json index 08792b59a..df8a3cb34 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "@halcyontech/vscode-ibmi-types", - "version": "2.6.6-dev.0", + "version": "2.6.7-dev.0", "description": "Types for vscode-ibmi", "typings": "./typings.d.ts", "scripts": {