├── .eslintignore
├── .eslintrc.json
├── .github
├── pr_testing_template.md
├── pull_request_template.md
└── workflows
│ ├── ci.yaml
│ ├── pr.yaml
│ └── test.yaml
├── .gitignore
├── .gitmodules
├── .nojekyll
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── ColAssist_01.png
├── ColAssist_02.png
├── DirectiveAfter.png
├── DirectiveBefore.png
├── LennonFigure1.png
├── LintFix_01.png
├── LintFix_02.png
├── OpenLintConfig.png
├── OpenLintConfig_02.png
├── Outline_01.png
├── Settings_01.png
├── lintopt_01.png
└── lintopt_02.png
├── cli
└── rpglint
│ ├── .npmignore
│ ├── index.ts
│ ├── package-lock.json
│ ├── package.json
│ ├── readme.md
│ ├── tsconfig.json
│ └── webpack.config.js
├── extension
├── client
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── base.ts
│ │ ├── commands.ts
│ │ ├── configuration.ts
│ │ ├── extension.ts
│ │ ├── language
│ │ │ ├── columnAssist.ts
│ │ │ ├── config.ts
│ │ │ └── serverReferences.ts
│ │ ├── linter.ts
│ │ ├── requests.ts
│ │ └── schemas
│ │ │ ├── linter.ts
│ │ │ └── specs.ts
│ ├── testFixture
│ │ ├── completion.txt
│ │ └── diagnostics.txt
│ ├── tsconfig.json
│ └── webpack.config.js
├── server
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── connection.ts
│ │ ├── data.ts
│ │ ├── providers
│ │ │ ├── apis
│ │ │ │ └── index.ts
│ │ │ ├── codeActions.ts
│ │ │ ├── completionItem.ts
│ │ │ ├── definition.ts
│ │ │ ├── documentSymbols.ts
│ │ │ ├── hover.ts
│ │ │ ├── implementation.ts
│ │ │ ├── index.ts
│ │ │ ├── language.ts
│ │ │ ├── linter
│ │ │ │ ├── codeActions.ts
│ │ │ │ ├── documentFormatting.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── skipRules.ts
│ │ │ ├── project
│ │ │ │ ├── exportInterfaces.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── references.ts
│ │ │ │ └── workspaceSymbol.ts
│ │ │ ├── reference.ts
│ │ │ └── rename.ts
│ │ └── server.ts
│ ├── tsconfig.json
│ └── webpack.config.js
└── tsconfig.base.json
├── index.html
├── jsconfig.json
├── language
├── document.ts
├── linter.ts
├── models
│ ├── DataPoints.ts
│ ├── cache.ts
│ ├── declaration.ts
│ ├── fixed.js
│ ├── oneLineTriggers.js
│ ├── opcodes.ts
│ └── tags.js
├── parser.ts
├── parserTypes.ts
├── statement.ts
├── tokens.ts
└── types.ts
├── media
└── logo.png
├── package-lock.json
├── package.json
├── schemas
├── rpgle.code-snippets
└── rpglint.json
├── shared.webpack.config.js
└── tests
├── eof4.rpgle
├── parserSetup.ts
├── rpgle
├── CBKDTAARA.rpgle
├── CBKHEADER.rpgle
├── CBKOPTIMIZ.rpgle
├── CBKPCFGDCL.rpgle
├── CBKPCFGREA.rpgle
├── apival01s.rpgleinc
├── copy1.rpgle
├── copy2.rpgle
├── copy3.rpgle
├── copy4.rpgleinc
├── copy5.rpgleinc
├── db00030s_h.rpgleinc
├── db00040s_h.rpgleinc
├── depth1.rpgleinc
├── eof4.rpgle
├── file1.rpgleinc
├── fixed1.rpgleinc
└── stat.rpgleinc
├── sources
└── random
│ └── hello.test.rpgle
├── suite
├── basics.test.ts
├── directives.test.ts
├── docs.test.ts
├── editing.test.ts
├── files.test.ts
├── fixed.test.ts
├── keywords.test.ts
├── linter.test.ts
├── partial.test.ts
├── references.test.ts
└── sources.test.ts
└── tables
├── department.ts
├── display.ts
├── employee.ts
├── emps.ts
└── index.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": false,
4 | "commonjs": true,
5 | "es6": true,
6 | "node": true,
7 | "mocha": true
8 | },
9 | "parserOptions": {
10 | "ecmaVersion": 2018,
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-const-assign": "warn",
18 | "no-this-before-super": "warn",
19 | "no-undef": "warn",
20 | "no-unreachable": "warn",
21 | "no-var": "error",
22 | "quotes": ["warn", "backtick"],
23 | "constructor-super": "warn",
24 | "valid-typeof": "warn",
25 | "indent": ["error", 2]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/pr_testing_template.md:
--------------------------------------------------------------------------------
1 | # Want to help test a PR?
2 |
3 | If you got a zip file from a build, extract it. Inside of it is a 'vsix'.
4 |
5 | Install it, test out the feature, and post some feedback.
6 |
7 | ### How to install
8 |
9 | 1. Uninstall your current version of the extension from VS Code.
10 | * you can do this from the extension panel
11 | * your configuration will not be lost
12 | 2. Download the `.vsix` to your machine
13 | 3. Press F1 / Command + Shift + P and select 'Install from VSIX...'
14 | 4. Select the `.vsix` from this PR.
15 | 5. Test out the feature!
16 |
17 | ### When you're done
18 |
19 | * post the feedback on the PR
20 | * uninstall the extension from the extension panel
21 | * re-install the extension from the Marketplace
22 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
2 | ### Changes
3 |
4 | Description of change here.
5 |
6 | ### Checklist
7 |
8 | * [ ] have tested my change
9 | * [ ] updated relevant documentation
10 | * [ ] Remove any/all `console.log`s I added
11 | * [ ] eslint is not complaining
12 | * [ ] have added myself to the contributors' list in the README
13 | * [ ] **for feature PRs**: PR only includes one feature enhancement.
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | release:
3 | types: [created]
4 |
5 | jobs:
6 | release:
7 | name: Release and publish
8 | runs-on: ubuntu-latest
9 | steps:
10 |
11 | # Setup prereqs
12 | - uses: actions/setup-node@v3
13 | with:
14 | node-version: '20'
15 | - uses: actions/checkout@v3
16 | - run: npm install
17 | - run: npm install -g vsce
18 |
19 | # Create and publish build to Marketplace
20 | - name: Publish to Marketplace
21 | run: vsce publish -p $PUBLISHER_TOKEN
22 | env:
23 | PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
24 |
25 | # Create and publish build to OpenVSX
26 | - name: Publish to Open VSX
27 | run: npx ovsx publish -p ${{ secrets.OPENVSX_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yaml:
--------------------------------------------------------------------------------
1 | on: pull_request
2 |
3 | jobs:
4 | release:
5 | name: Create PR build
6 | runs-on: ubuntu-latest
7 | if: contains(github.event.pull_request.labels.*.name, 'build')
8 | steps:
9 | - uses: actions/checkout@v2
10 | with:
11 | fetch-depth: 1
12 | - run: npm install
13 | - run: node ./node_modules/eslint/bin/eslint src/** --no-error-on-unmatched-pattern
14 | - run: npm run test
15 |
16 | - run: npm install -g vsce
17 | - name: Create build
18 | run: npm run package
19 |
20 | - name: Upload build
21 | uses: actions/upload-artifact@v2
22 | with:
23 | name: code-for-ibmi-pr-build
24 | path: ./*.vsix
25 |
26 | - name: Post comment
27 | uses: actions/github-script@v5
28 | with:
29 | script: |
30 | github.rest.issues.createComment({
31 | issue_number: context.issue.number,
32 | owner: context.repo.owner,
33 | repo: context.repo.repo,
34 | body: '👋 A new build is available for this PR based on ${{ github.event.pull_request.head.sha }}.\n * [Download here.](https://github.com/halcyon-tech/vscode-rpgle/actions/runs/${{ github.run_id }})\n* [Read more about how to test](https://github.com/halcyon-tech/vscode-rpgle/blob/master/.github/pr_testing_template.md)'
35 | })
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | pull_request:
3 | paths:
4 | - 'language/**'
5 | - 'tests/**'
6 |
7 | jobs:
8 | release:
9 | name: Test runner
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | with:
14 | fetch-depth: 1
15 | submodules: true
16 | - run: npm install
17 | - run: node ./node_modules/eslint/bin/eslint src/** --no-error-on-unmatched-pattern
18 | - run: npm run test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode-test/
3 | *.vsix
4 | dist
5 | .DS_Store
6 | .vscode/settings.json
7 | out
8 | tsconfig.tsbuildinfo
9 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "tests/sources/bob-recursive-example"]
2 | path = tests/sources/bob-recursive-example
3 | url = git@github.com:IBM/bob-recursive-example.git
4 | [submodule "tests/sources/BBS400"]
5 | path = tests/sources/BBS400
6 | url = git@github.com:worksofliam/BBS400.git
7 | [submodule "tests/sources/noxDB"]
8 | path = tests/sources/noxDB
9 | url = git@github.com:sitemule/noxDB.git
10 | [submodule "tests/sources/xmlservice"]
11 | path = tests/sources/xmlservice
12 | url = git@github.com:IBM/xmlservice.git
13 | [submodule "tests/sources/I_builder"]
14 | path = tests/sources/I_builder
15 | url = git@github.com:ibmiiste/I_builder.git
16 | [submodule "tests/sources/ibmi-company_system"]
17 | path = tests/sources/ibmi-company_system
18 | url = git@github.com:IBM/ibmi-company_system.git
19 | [submodule "tests/sources/httpapi"]
20 | path = tests/sources/httpapi
21 | url = git@github.com:ScottKlement/httpapi.git
22 | [submodule "tests/sources/rpgle-repl"]
23 | path = tests/sources/rpgle-repl
24 | url = git@github.com:tom-writes-code/rpgle-repl.git
25 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/.nojekyll
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 | "dbaeumer.vscode-eslint"
8 | ]
9 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | {
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "type": "extensionHost",
7 | "request": "launch",
8 | "name": "Launch Client",
9 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"],
10 | "outFiles": ["${workspaceRoot}/out/**/*.js"],
11 | "sourceMaps": true,
12 | "sourceMapPathOverrides": {
13 | "webpack://client/./*": "${workspaceFolder}/extension/client/*"
14 | },
15 | "preLaunchTask": {
16 | "type": "npm",
17 | "script": "webpack:dev"
18 | }
19 | },
20 | {
21 | "type": "node",
22 | "request": "attach",
23 | "name": "Attach to Server",
24 | "port": 8789,
25 | "restart": true,
26 | "sourceMaps": true,
27 | "outFiles": ["${workspaceRoot}/out/**/*.js"],
28 | "sourceMapPathOverrides": {
29 | "webpack://server/./*": "${workspaceFolder}/extension/server/*"
30 | },
31 | },
32 | {
33 | "name": "Debug Tests",
34 | "type": "node",
35 | "request": "launch",
36 | "program": "${workspaceRoot}/out/tests/tests/index.js",
37 | "sourceMaps": true,
38 | "preLaunchTask": {
39 | "type": "npm",
40 | "script": "compile:tests"
41 | },
42 | "args": ["sqlRunner1"],
43 | "env": {
44 | "INCLUDE_DIR": "${workspaceFolder}"
45 | }
46 | }
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "webpack:dev",
7 | "group": "build",
8 | "presentation": {
9 | "panel": "dedicated",
10 | "reveal": "never"
11 | },
12 | "problemMatcher": "$ts-webpack-watch"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .github/**
2 | assets/**
3 | **/node_modules
4 | node_modules
5 | scripts
6 | .vscode/**
7 | .vscode-test/**
8 | .gitignore
9 | .yarnrc
10 | vsc-extension-quickstart.md
11 | **/jsconfig.json
12 | **/*.map
13 | **/.eslintrc.json
14 | tests
15 | cli
16 | language
17 | **/cli
18 | cli/**
19 | cli
20 | extension
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "vscode-rpgle" extension will be documented in this file.
4 |
5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
6 |
7 | ## [Unreleased]
8 |
9 | - Initial release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Halcyon Tech Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vscode-rpgle README
2 |
3 |
4 |
5 | Adds functionality to assist in writing accurate, readable and consistent RPGLE, including:
6 |
7 | - Content assist
8 | - Outline view
9 | - Linter, including indentation checking and reformatting (`**FREE` only)
10 | - Column assist for fixed-format RPGLE.
11 |
12 | Depends on the Code for IBM i extension due to source code living on the remote system when developing with source members.
13 |
14 | ## Documentation
15 |
16 | Check out the [official documentation](https://codefori.github.io/docs/#/pages/extensions/rpgle/linter) for a usage guide.
17 |
18 | # Developing
19 |
20 | 1. Fork & clone
21 | 2. `npm i`
22 | 3. Run
23 |
24 | ## Debugging
25 |
26 | To run the tests, you have two options:
27 |
28 | 1. `npm run test` from the command line
29 | 2. 'Debug Tests' from the VS Code debugger
30 |
31 | To run debug the extension and server, from the VS Code debugger:
32 |
33 | 1. Debug 'Launch Client'
34 | 2. Debug 'Attach to Server'
35 |
36 | # Previous contributors
37 |
38 | Thanks so much to everyone [who has contributed](https://github.com/codefori/vscode-rpgle/graphs/contributors).
39 |
40 | - [@worksofliam](https://github.com/worksofliam)
41 | - [@SJLennon](https://github.com/SJLennon)
42 | - [@sebCIL](https://github.com/sebCIL)
43 | - [@p-behr](https://github.com/p-behr)
44 | - [@chrjorgensen](https://github.com/chrjorgensen)
45 | - [@sebjulliand](https://github.com/sebjulliand)
46 | - [@richardm90](https://github.com/richardm90)
47 | - [@wright4i](https://github.com/wright4i)
48 | - [@SanjulaGanepola](https://github.com/SanjulaGanepola)
--------------------------------------------------------------------------------
/assets/ColAssist_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/ColAssist_01.png
--------------------------------------------------------------------------------
/assets/ColAssist_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/ColAssist_02.png
--------------------------------------------------------------------------------
/assets/DirectiveAfter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/DirectiveAfter.png
--------------------------------------------------------------------------------
/assets/DirectiveBefore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/DirectiveBefore.png
--------------------------------------------------------------------------------
/assets/LennonFigure1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LennonFigure1.png
--------------------------------------------------------------------------------
/assets/LintFix_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LintFix_01.png
--------------------------------------------------------------------------------
/assets/LintFix_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/LintFix_02.png
--------------------------------------------------------------------------------
/assets/OpenLintConfig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/OpenLintConfig.png
--------------------------------------------------------------------------------
/assets/OpenLintConfig_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/OpenLintConfig_02.png
--------------------------------------------------------------------------------
/assets/Outline_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/Outline_01.png
--------------------------------------------------------------------------------
/assets/Settings_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/Settings_01.png
--------------------------------------------------------------------------------
/assets/lintopt_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/lintopt_01.png
--------------------------------------------------------------------------------
/assets/lintopt_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/assets/lintopt_02.png
--------------------------------------------------------------------------------
/cli/rpglint/.npmignore:
--------------------------------------------------------------------------------
1 | index.ts
2 | tsconfig.json
3 | webpack.config.js
--------------------------------------------------------------------------------
/cli/rpglint/index.ts:
--------------------------------------------------------------------------------
1 | // rpglint CLI
2 | // --files [glob]
3 | // --cwd "[path]"
4 |
5 | import glob from "glob";
6 | import { readFileSync } from 'fs';
7 |
8 | import Parser from '../../language/parser';
9 | import Linter from '../../language/linter';
10 | import { Rules } from '../../language/parserTypes';
11 | import path from 'path';
12 |
13 | type FormatTypes = "standard" | "flc";
14 |
15 | main();
16 |
17 | async function main() {
18 | const parms = process.argv.slice(2);
19 |
20 | let cwd = process.cwd();
21 | let scanGlob = `**/*.{SQLRPGLE,sqlrpgle,RPGLE,rpgle}`;
22 | let maxErrors: number|undefined;
23 | let outputType: FormatTypes;
24 |
25 | for (let i = 0; i < parms.length; i++) {
26 | switch (parms[0]) {
27 | case `-f`:
28 | case `--files`:
29 | scanGlob = parms[i + 1];
30 | i++;
31 | break;
32 |
33 | case `-d`:
34 | case `--cwd`:
35 | cwd = parms[i + 1];
36 | i++;
37 | break;
38 |
39 | case `-m`:
40 | case `--max`:
41 | maxErrors = Number(parms[i + 1]);
42 | i++;
43 | break;
44 |
45 | case `-o`:
46 | case `--output`:
47 | outputType = (parms[i+1] as FormatTypes);
48 | i++;
49 | break;
50 |
51 | case `-h`:
52 | case `--help`:
53 | console.log(`rpglint (derived from vscode-rpgle).`);
54 | console.log(``);
55 | console.log(`See who's contributed: https://github.com/halcyon-tech/vscode-rpgle/graphs/contributors`);
56 | console.log();
57 | console.log(`Rules are inherited from the 'rpglint.json' found in the working directory.`);
58 | console.log(`This configuration file usually lives in '.vscode/rpglint.json'.`);
59 | console.log();
60 | console.log(`\t-d`)
61 | console.log(`\t--cwd\t\tTo see the directory of where source code lives.`);
62 | console.log(`\t\t\tThe default is the current working directory.`);
63 | console.log();
64 | console.log(`\t-f`);
65 | console.log(`\t--files\t\tGlob used to search for sources in the working directory.`);
66 | console.log(`\t\t\tDefaults to '${scanGlob}'`);
67 | console.log();
68 | console.log(`\t-m`);
69 | console.log(`\t--max\t\tThe max limit of errored files before the process ends itself.`);
70 | console.log();
71 | console.log(`\t-o`);
72 | console.log(`\t--output\tFormat of the lint errors in standard out.`);
73 | console.log(`\t\t\tDefaults to standard. Available: standard, flc`);
74 | console.log();
75 | process.exit(0);
76 | }
77 | }
78 |
79 | let rules: Rules;
80 | let parser: Parser;
81 | let files: string[];
82 |
83 | try {
84 | rules = getLintConfig(cwd);
85 | parser = setupParser(cwd, scanGlob);
86 | files = getFiles(cwd, scanGlob);
87 | } catch (e) {
88 | error(e.message || e);
89 | process.exit(1);
90 | }
91 |
92 | const ruleCount = Object.keys(rules).length;
93 |
94 | if (ruleCount === 0) {
95 | error(`rpglint.json does not have any rules. Exiting.`);
96 | process.exit();
97 | }
98 |
99 | let totalFailures = 0;
100 |
101 | console.log(`Linting ${files.length} file${files.length !== 1 ? `s` : ``}.`);
102 |
103 | if (files.length > 500) {
104 | console.log(`Looks like you are linting a lot of files! It might be worth checking out the '--max' parameter to limit the amount of errors that will be produced. This is useful to end the lint process after so many issues.`);
105 | }
106 |
107 | console.log();
108 |
109 | for (const filePath of files) {
110 | if (maxErrors && totalFailures >= maxErrors) {
111 | console.log();
112 | console.log(`Max errors of ${maxErrors} has been reached. Ending.`);
113 | break;
114 | }
115 |
116 | try {
117 | const content = readFileSync(filePath, { encoding: `utf-8` });
118 | const eol = content.includes(`\r\n`) ? `\r\n` : `\n`;
119 | const eolIndexes: number[] = [];
120 |
121 | for (let i = 0; i < content.length; i++) {
122 | if (content.substring(i, i + eol.length) === eol) {
123 | eolIndexes.push(i);
124 | }
125 | }
126 |
127 | if (content.length > 6 && content.substring(0, 6).toLowerCase() === `**free`) {
128 | const docs = await parser.getDocs(
129 | filePath,
130 | content,
131 | {
132 | withIncludes: true
133 | }
134 | );
135 |
136 | const lintResult = Linter.getErrors({
137 | uri: filePath,
138 | content
139 | }, rules, docs);
140 |
141 | const totalErrors = lintResult.errors.length + lintResult.indentErrors.length;
142 |
143 | if (totalErrors) {
144 | totalFailures += 1;
145 |
146 | switch (outputType) {
147 | case `flc`:
148 | if (lintResult.indentErrors.length) {
149 | lintResult.indentErrors.forEach(indentError => {
150 | console.log(`${filePath}:${indentError.line + 1}:${indentError.currentIndent}:Expected indent of ${indentError.expectedIndent}`);
151 | });
152 | }
153 |
154 | if (lintResult.errors.length) {
155 | lintResult.errors.forEach(error => {
156 | const line = eolIndexes.findIndex(index => index > error.offset.position);
157 | const offset = error.offset.position - (eolIndexes[line-1] || 0);
158 | console.log(`${filePath}:${line+1}:${offset}:${Linter.getErrorText(error.type)}`);
159 | });
160 | }
161 | break;
162 |
163 | case `standard`:
164 | default:
165 | const relative = path.relative(cwd, filePath);
166 | console.log(`${relative}: ${totalErrors} error${totalErrors !== 1 ? `s` : ``}.`);
167 | if (lintResult.indentErrors.length) {
168 | lintResult.indentErrors.forEach(indentError => {
169 | console.log(`\tLine ${indentError.line + 1}: expected indent of ${indentError.expectedIndent}, got ${indentError.currentIndent}`);
170 | });
171 | }
172 |
173 | if (lintResult.errors.length) {
174 | lintResult.errors.forEach(error => {
175 | const line = eolIndexes.findIndex(index => index > error.offset.position);
176 | const offset = error.offset.position - (eolIndexes[line-1] || 0);
177 | console.log(`\tLine ${line+1}, column ${offset}: ${Linter.getErrorText(error.type)}`);
178 | });
179 | }
180 | break;
181 | }
182 | }
183 | }
184 |
185 | } catch (e) {
186 | error(`Failed to lint ${filePath}: ${e.message || e}`);
187 | error(`Report this issue to us with an example: github.com/halcyon-tech/vscode-rpgle/issues`);
188 | }
189 | }
190 |
191 | process.exit(totalFailures > 0 ? 1 : 0);
192 | }
193 |
194 | function getLintConfig(cwd: string): Rules {
195 | const files = glob.sync(`**/rpglint.json`, {
196 | cwd,
197 | absolute: true,
198 | nocase: true,
199 | dot: true
200 | });
201 |
202 | if (files.length >= 1) {
203 | const file = files[0];
204 |
205 | const content = readFileSync(file, { encoding: `utf-8` });
206 | const result = JSON.parse(content);
207 | return result;
208 | }
209 |
210 | throw new Error(`Unable to locate rpglint.json`);
211 | }
212 |
213 | function getFiles(cwd: string, globPath: string): string[] {
214 | return glob.sync(globPath, {
215 | cwd,
216 | absolute: true,
217 | nocase: true,
218 | });
219 | }
220 |
221 | function error(line: string) {
222 | process.stdout.write(line + `\n`);
223 | }
224 |
225 | function setupParser(cwd: string, globPath: string): Parser {
226 | const parser = new Parser();
227 |
228 | parser.setIncludeFileFetch(async (baseFile: string, includeFile: string) => {
229 | if (includeFile.startsWith(`'`) && includeFile.endsWith(`'`)) {
230 | includeFile = includeFile.substring(1, includeFile.length - 1);
231 | }
232 |
233 | if (includeFile.includes(`,`)) {
234 | includeFile = includeFile.split(`,`).join(`/`) + `.*`;
235 | }
236 |
237 | includeFile = path.join(`**`, includeFile);
238 | const files = glob.sync(includeFile, {
239 | cwd,
240 | absolute: true,
241 | nocase: true,
242 | });
243 |
244 | if (files.length >= 1) {
245 | const file = files[0];
246 |
247 | const content = readFileSync(file, { encoding: `utf-8` });
248 | return {
249 | found: true,
250 | uri: file,
251 | lines: content.split(`\n`)
252 | }
253 | }
254 |
255 | return {
256 | found: false
257 | };
258 | });
259 |
260 | parser.setTableFetch(async (table: string, aliases = false) => {
261 | // Can't support tables in CLI mode I suppose?
262 | return [];
263 | });
264 |
265 | return parser;
266 | }
--------------------------------------------------------------------------------
/cli/rpglint/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@halcyontech/rpglint",
3 | "version": "0.27.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@halcyontech/rpglint",
9 | "version": "0.27.0",
10 | "license": "MIT",
11 | "bin": {
12 | "rpglint": "dist/index.js"
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cli/rpglint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@halcyontech/rpglint",
3 | "version": "0.27.0",
4 | "description": "rpglint CLI tool",
5 | "bin": {
6 | "rpglint": "./dist/index.js"
7 | },
8 | "scripts": {
9 | "webpack:dev": "webpack --mode none --config ./webpack.config.js",
10 | "webpack": "webpack --mode production --config ./webpack.config.js",
11 | "local": "npm run webpack:dev && npm i -g",
12 | "deploy": "npm run webpack && npm i && npm publish --access public"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/halcyon-tech/vscode-rpgle.git"
17 | },
18 | "keywords": [
19 | "rpgle",
20 | "ibmi"
21 | ],
22 | "author": "Halcyon Tech",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/halcyon-tech/vscode-rpgle/issues"
26 | },
27 | "homepage": "https://github.com/halcyon-tech/vscode-rpgle#readme"
28 | }
29 |
--------------------------------------------------------------------------------
/cli/rpglint/readme.md:
--------------------------------------------------------------------------------
1 | # RPG Linter CLI
2 |
3 | This is a command-line interface (CLI) for the RPG Linter, derived from the vscode-rpgle extension. It allows you to lint your RPG code from the command line, using the same rules and configuration as the vscode-rpgle extension.
4 |
5 | ## Installation
6 |
7 | `rpglint` can be installed through npm. You can see the package on npmjs.com! `rpglint` is intended to be installed globally and not at a project level. To do that, you can simply run:
8 |
9 | ```bash
10 | npm i @halcyontech/rpglint -g
11 | ```
12 |
13 | ## Usage
14 |
15 | You can use the following command to see the available parameters:
16 |
17 | ```bash
18 | rpglint -h
19 | ```
20 |
21 | The CLI accepts several command-line arguments to customize its behavior:
22 |
23 | - `-f` or `--files`: A glob pattern used to search for source files in the working directory. Defaults to `**/*.{SQLRPGLE,sqlrpgle,RPGLE,rpgle}`.
24 | - `-d` or `--cwd`: The directory where the source code lives. Defaults to the current working directory.
25 | - `-m` or `--max`: The maximum number of errored files before the process ends itself.
26 | - `-o` or `--output`: The format of the lint errors in standard out. Defaults to `standard`. Available options are `standard` and `flc`.
27 | - `-h` or `--help`: Displays help information.
28 |
29 | The CLI uses a configuration file named `rpglint.json` located in the working directory (usually in `.vscode/rpglint.json`) to determine the linting rules.
30 |
31 | ## Example
32 |
33 | Let's test `rpglint` with the IBM sample repository, Company System! `ibmi-company_system` is available on GitHub and anybody can clone it. For this example, we will clone it to our local device and then run `rpglint` against it.
34 |
35 | ```bash
36 | # cd to where the repo will be created
37 | cd ~/Downloads/
38 |
39 | # clone the repository. HTTPS clone should work for everyone
40 | git clone git@github.com:IBM/ibmi-company_system.git
41 |
42 | # cd into the repository
43 | cd ibmi-company_system
44 | ```
45 |
46 | By default, there are a bunch of errors in this repository. We do this to show off that `rpglint` works as expected! You can just use `rpglint` inside of this directory for the linter to run!
47 |
48 | ```bash
49 | barry$ pwd
50 | ~/Downloads/ibmi-company_system
51 |
52 | barry$ rpglint
53 | Linting 3 files.
54 |
55 | /Users/barry/Downloads/ibmi-company_system/qrpglesrc/employees.pgm.sqlrpgle: 22 errors.
56 | Line 8: expected indent of 0, got 6
57 | Line 12: expected indent of 0, got 6
58 | Line 38: expected indent of 0, got 6
59 | Line 39: expected indent of 0, got 6
60 | Line 45: expected indent of 0, got 8
61 | Line 64: expected indent of 0, got 8
62 | Line 4, column 0: Variable name casing does not match definition.
63 | Line 8, column 6: Comments must be correctly formatted.
64 | Line 10, column 0: Directives must be in uppercase.
65 | Line 12, column 6: Comments must be correctly formatted.
66 | Line 14, column 0: Variable name casing does not match definition.
67 | Line 38, column 6: Comments must be correctly formatted.
68 | Line 39, column 6: Comments must be correctly formatted.
69 | Line 45, column 8: Comments must be correctly formatted.
70 | Line 64, column 8: Comments must be correctly formatted.
71 | Line 74, column 2: Variable name casing does not match definition.
72 | Line 107, column 8: Variable name casing does not match definition.
73 | Line 116, column 2: Variable name casing does not match definition.
74 | Line 141, column 6: Variable name casing does not match definition.
75 | Line 91, column 2: Same string literal used more than once. Consider using a constant instead.
76 | Line 93, column 4: Same string literal used more than once. Consider using a constant instead.
77 | Line 101, column 6: Same string literal used more than once. Consider using a constant instead.
78 | rpglint simply sp
79 | ```
80 |
81 | `rpglint` simply spits out all the issues it finds with the RPGLE code in the repository folder. The intention of a linter is to enforce code standards in a code base. When the linter finds errors, it will have an exit code of `1` and `0` if there are none.
82 |
83 | ## rpglint in your CI process
84 |
85 | Because we are able to use `rpglint` from the command line, this means we've opened up a totally new world to ourselves. We can now run `rpglint` as part of a CI (Continuous Integration) process. For example, here's a GitHub Action .yml which configures `rpglint` to run when a PR request is created or updated against the main branch:
86 |
87 | ```yaml
88 | name: rpglint CI
89 |
90 | on:
91 | pull_request:
92 | # Set your workflow to run on pull_request events that target the main branch
93 | branches: [ "main" ]
94 |
95 | jobs:
96 | build:
97 | runs-on: ubuntu-latest
98 |
99 | strategy:
100 | matrix:
101 | node-version: [18.x]
102 |
103 | steps:
104 | - uses: actions/checkout@v3
105 | - name: Use Node.js ${{ matrix.node-version }}
106 | uses: actions/setup-node@v3
107 | with:
108 | node-version: ${{ matrix.node-version }}
109 | cache: 'npm'
110 | - run: npm i -g rpglint
111 | - run: rpglint
112 | ```
113 |
114 | The best part about the `rpglint` command is that it returns exit code `1` if there are errors. CI tools (like GitHub Actions, Jenkins, GitLab Runner, etc) can use this status code to determine if the CI was successful. It makes sense that the CI fails if there are lint errors, because it doesn't match the code standard defined in the `rpglint.json`.
115 |
--------------------------------------------------------------------------------
/cli/rpglint/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "ES2019",
5 | "noImplicitAny": false,
6 | "noUnusedParameters": false,
7 | "strict": false,
8 | "allowJs": true,
9 | "outDir": "./dist",
10 | "esModuleInterop": true,
11 | "sourceMap": true
12 | }
13 | }
--------------------------------------------------------------------------------
/cli/rpglint/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | // @ts-nocheck
7 |
8 | 'use strict';
9 |
10 | const withDefaults = require(`../../shared.webpack.config`);
11 | const path = require(`path`);
12 | const webpack = require(`webpack`);
13 |
14 | module.exports = withDefaults({
15 | context: path.join(__dirname),
16 | entry: {
17 | extension: `./index.ts`,
18 | },
19 | output: {
20 | filename: `index.js`,
21 | path: path.join(__dirname, `dist`)
22 | },
23 | // Other stuff
24 | plugins: [
25 | new webpack.BannerPlugin({banner: `#! /usr/bin/env node`, raw: true})
26 | ]
27 | });
--------------------------------------------------------------------------------
/extension/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "description": "VSCode client for RPGLE LSP",
4 | "author": "Liam Allan",
5 | "license": "MIT",
6 | "version": "0.0.1",
7 | "publisher": "vscode",
8 | "engines": {
9 | "vscode": "^1.63.0"
10 | },
11 | "dependencies": {
12 | "vscode-languageclient": "^7.0.0"
13 | },
14 | "devDependencies": {
15 | "@halcyontech/vscode-ibmi-types": "^2.15.3",
16 | "@types/vscode": "^1.63.0",
17 | "@vscode/test-electron": "^2.1.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/extension/client/src/base.ts:
--------------------------------------------------------------------------------
1 | import { CodeForIBMi } from "@halcyontech/vscode-ibmi-types";
2 | import Instance from "@halcyontech/vscode-ibmi-types/Instance";
3 | import { ConfigurationChangeEvent, Extension, extensions, workspace } from "vscode";
4 |
5 | let baseExtension: Extension|undefined;
6 |
7 | export async function checkAndWait() {
8 | baseExtension = extensions.getExtension(`halcyontechltd.code-for-ibmi`);
9 |
10 | if (baseExtension) {
11 | if (!baseExtension.isActive) {
12 | await baseExtension.activate();
13 | }
14 | }
15 |
16 | return getInstance();
17 | }
18 |
19 | /**
20 | * This should be used on your extension activation.
21 | */
22 | export function loadBase(): CodeForIBMi|undefined {
23 | if (!baseExtension) {
24 | baseExtension = (extensions ? extensions.getExtension(`halcyontechltd.code-for-ibmi`) : undefined);
25 | }
26 |
27 | return (baseExtension && baseExtension.isActive && baseExtension.exports ? baseExtension.exports : undefined);
28 | }
29 |
30 | /**
31 | * Used when you want to fetch the extension 'instance' (the connection)
32 | */
33 | export function getInstance(): Instance|undefined {
34 | const base = loadBase();
35 | return (base ? base.instance : undefined);
36 | }
37 |
38 | // Stolen directly from vscode-ibmi
39 | export function onCodeForIBMiConfigurationChange(props: string | string[], todo: (value: ConfigurationChangeEvent) => void) {
40 | const keys = (Array.isArray(props) ? props : Array.of(props)).map(key => `code-for-ibmi.${key}`);
41 | return workspace.onDidChangeConfiguration(async event => {
42 | if (keys.some(key => event.affectsConfiguration(key))) {
43 | todo(event);
44 | }
45 | })
46 | }
--------------------------------------------------------------------------------
/extension/client/src/commands.ts:
--------------------------------------------------------------------------------
1 | import { commands, ExtensionContext, window } from "vscode";
2 | import { clearTableCache } from "./requests";
3 | import { LanguageClient } from "vscode-languageclient/node";
4 |
5 | export function registerCommands(context: ExtensionContext, client: LanguageClient) {
6 | context.subscriptions.push(
7 | commands.registerCommand(`vscode-rpgle.server.reloadCache`, () => {
8 | clearTableCache(client);
9 | })
10 | )
11 | }
--------------------------------------------------------------------------------
/extension/client/src/configuration.ts:
--------------------------------------------------------------------------------
1 | import { ConfigurationTarget, workspace } from 'vscode';
2 |
3 | export function get(prop: string) {
4 | const globalData = workspace.getConfiguration(`vscode-rpgle`);
5 | return globalData.get(prop);
6 | }
7 |
8 | export const RULER_ENABLED_BY_DEFAULT = `rulerEnabledByDefault`;
9 | export const projectFilesGlob = `**/*.{rpgle,RPGLE,sqlrpgle,SQLRPGLE,rpgleinc,RPGLEINC}`;
--------------------------------------------------------------------------------
/extension/client/src/extension.ts:
--------------------------------------------------------------------------------
1 | /* --------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | * ------------------------------------------------------------------------------------------ */
5 |
6 | import * as path from 'path';
7 | import { workspace, ExtensionContext } from 'vscode';
8 |
9 | import * as Linter from "./linter";
10 | import * as columnAssist from "./language/columnAssist";
11 |
12 |
13 | import {
14 | LanguageClient,
15 | LanguageClientOptions,
16 | ServerOptions,
17 | TransportKind
18 | } from 'vscode-languageclient/node';
19 |
20 | import { projectFilesGlob } from './configuration';
21 | import { clearTableCache, buildRequestHandlers } from './requests';
22 | import { getServerImplementationProvider, getServerSymbolProvider } from './language/serverReferences';
23 | import { checkAndWait, loadBase, onCodeForIBMiConfigurationChange } from './base';
24 | import { registerCommands } from './commands';
25 | import { setLanguageSettings } from './language/config';
26 |
27 | let client: LanguageClient;
28 |
29 | export function activate(context: ExtensionContext) {
30 | // The server is implemented in node
31 | const serverModule = context.asAbsolutePath(
32 | path.join('out', 'server.js')
33 | );
34 | // The debug options for the server
35 | // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
36 | const debugOptions = { execArgv: ['--nolazy', '--inspect=8789'] };
37 |
38 | // If the extension is launched in debug mode then the debug server options are used
39 | // Otherwise the run options are used
40 | const serverOptions: ServerOptions = {
41 | run: { module: serverModule, transport: TransportKind.ipc },
42 | debug: {
43 | module: serverModule,
44 | transport: TransportKind.ipc,
45 | options: debugOptions
46 | }
47 | };
48 |
49 | loadBase();
50 |
51 | // Options to control the language client
52 | const clientOptions: LanguageClientOptions = {
53 | // Register the server for plain text documents
54 | documentSelector: [
55 | { language: 'rpgle' },
56 | ],
57 | synchronize: {
58 | fileEvents: [
59 | workspace.createFileSystemWatcher('**/iproj.json'),
60 | workspace.createFileSystemWatcher('**/rpglint.json'),
61 | workspace.createFileSystemWatcher(projectFilesGlob),
62 | ]
63 | }
64 | };
65 |
66 | // Create the language client and start the client.
67 | client = new LanguageClient(
68 | 'lsp-rpgle-client',
69 | 'RPGLE language client',
70 | serverOptions,
71 | clientOptions
72 | );
73 |
74 | client.onReady().then(async () => {
75 | buildRequestHandlers(client);
76 |
77 | const instance = await checkAndWait();
78 |
79 | // We need to clear table caches when the connection changes
80 | if (instance) {
81 | // When the connection is established
82 | instance.subscribe(context, "connected", "vscode-rpgle", () => {
83 | clearTableCache(client);
84 | });
85 |
86 | // When the library list changes
87 | context.subscriptions.push(
88 | onCodeForIBMiConfigurationChange("connectionSettings", async () => {
89 | clearTableCache(client);
90 | }),
91 | );
92 | }
93 | });
94 |
95 | // Start the client. This will also launch the server
96 | client.start();
97 |
98 | Linter.initialise(context);
99 | columnAssist.registerColumnAssist(context);
100 |
101 | registerCommands(context, client);
102 |
103 | context.subscriptions.push(getServerSymbolProvider());
104 | context.subscriptions.push(getServerImplementationProvider());
105 | context.subscriptions.push(setLanguageSettings());
106 | // context.subscriptions.push(...initBuilder(client));
107 |
108 |
109 | console.log(`started`);
110 | }
111 |
112 | export function deactivate(): Thenable | undefined {
113 | if (!client) {
114 | return undefined;
115 | }
116 | return client.stop();
117 | }
118 |
--------------------------------------------------------------------------------
/extension/client/src/language/columnAssist.ts:
--------------------------------------------------------------------------------
1 |
2 | import { commands, DecorationOptions, ExtensionContext, Range, Selection, TextDocument, ThemeColor, window } from 'vscode';
3 | import * as Configuration from "../configuration";
4 | import { loadBase } from '../base';
5 |
6 | const currentArea = window.createTextEditorDecorationType({
7 | backgroundColor: `rgba(242, 242, 109, 0.3)`,
8 | border: `1px solid grey`,
9 | });
10 |
11 | const notCurrentArea = window.createTextEditorDecorationType({
12 | backgroundColor: `rgba(242, 242, 109, 0.1)`,
13 | border: `1px solid grey`,
14 | });
15 |
16 | const outlineBar = window.createTextEditorDecorationType({
17 | backgroundColor: new ThemeColor(`editor.background`),
18 | isWholeLine: true,
19 | opacity: `0`,
20 | });
21 |
22 | let rulerEnabled = Configuration.get(Configuration.RULER_ENABLED_BY_DEFAULT) || false
23 | let currentEditorLine = -1;
24 |
25 | import { SpecFieldDef, SpecFieldValue, SpecRulers, specs } from '../schemas/specs';
26 |
27 | const getAreasForLine = (line: string, index: number) => {
28 | if (line.length < 6) return undefined;
29 | if (line[6] === `*`) return undefined;
30 |
31 | const specLetter = line[5].toUpperCase();
32 | if (specs[specLetter]) {
33 | const specification = specs[specLetter];
34 |
35 | const active = specification.findIndex((box: any) => index >= box.start && index <= box.end);
36 |
37 | return {
38 | specification,
39 | active,
40 | outline: SpecRulers[specLetter]
41 | };
42 | }
43 | }
44 |
45 | function documentIsFree(document: TextDocument) {
46 | if (document.languageId === `rpgle`) {
47 | const line = document.getText(new Range(0, 0, 0, 6)).toUpperCase();
48 | return line === `**FREE`;
49 | }
50 |
51 | return false;
52 | }
53 |
54 | export function registerColumnAssist(context: ExtensionContext) {
55 | context.subscriptions.push(
56 | commands.registerCommand(`vscode-rpgle.assist.launchUI`, async () => {
57 | const editor = window.activeTextEditor;
58 | if (editor) {
59 | const document = editor.document;
60 |
61 | if (document.languageId === `rpgle`) {
62 | if (!documentIsFree(document)) {
63 | const lineNumber = editor.selection.start.line;
64 | const positionIndex = editor.selection.start.character;
65 |
66 | const positionsData = await promptLine(
67 | document.getText(new Range(lineNumber, 0, lineNumber, 100)),
68 | positionIndex
69 | );
70 |
71 | if (positionsData) {
72 | window.showTextDocument(document).then(newEditor => {
73 | newEditor.edit(editBuilder => {
74 | editBuilder.replace(new Range(lineNumber, 0, lineNumber, 80), positionsData);
75 | });
76 | })
77 | }
78 | }
79 | }
80 | }
81 | }),
82 |
83 | commands.registerCommand(`vscode-rpgle.assist.toggleFixedRuler`, async () => {
84 | rulerEnabled = !rulerEnabled;
85 |
86 | if (rulerEnabled) {
87 | updateRuler();
88 | } else {
89 | clearRulers();
90 | }
91 | }),
92 |
93 | commands.registerCommand(`vscode-rpgle.assist.moveLeft`, () => {
94 | moveFromPosition(`left`);
95 | }),
96 | commands.registerCommand(`vscode-rpgle.assist.moveRight`, () => {
97 | moveFromPosition(`right`);
98 | }),
99 |
100 | window.onDidChangeTextEditorSelection(e => {
101 | const editor = e.textEditor;
102 | if (rulerEnabled) {
103 | updateRuler(editor);
104 | } else {
105 | clearRulers(editor);
106 | }
107 | }),
108 | )
109 | }
110 |
111 | function moveFromPosition(direction: "left"|"right", editor = window.activeTextEditor) {
112 | if (editor && editor.document.languageId === `rpgle` && !documentIsFree(editor.document)) {
113 | const document = editor.document;
114 | const lineNumber = editor.selection.start.line;
115 | const positionIndex = editor.selection.start.character;
116 |
117 | const positionsData = getAreasForLine(
118 | document.getText(new Range(lineNumber, 0, lineNumber, 100)),
119 | positionIndex
120 | );
121 |
122 | if (positionsData) {
123 | let newIndex: number|undefined;
124 | if (direction === `left`) {
125 | newIndex = positionsData.active - 1;
126 | } else
127 | if (direction === `right`) {
128 | newIndex = positionsData.active + 1;
129 | }
130 |
131 | if (newIndex !== undefined && newIndex >= 0 && newIndex < positionsData.specification.length) {
132 | const box = positionsData.specification[newIndex];
133 | if (box) {
134 | editor.selection = new Selection(lineNumber, box.start, lineNumber, box.start);
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
141 | function updateRuler(editor = window.activeTextEditor) {
142 | let clear = true;
143 |
144 | if (editor) {
145 | const document = editor.document;
146 | if (document.languageId === `rpgle`) {
147 | if (!documentIsFree(document)) {
148 | const lineNumber = editor.selection.start.line;
149 | const positionIndex = editor.selection.start.character;
150 |
151 | const positionsData = getAreasForLine(
152 | document.getText(new Range(lineNumber, 0, lineNumber, 100)),
153 | positionIndex
154 | );
155 |
156 | if (positionsData) {
157 | let decorations: DecorationOptions[] = [];
158 |
159 | positionsData.specification.forEach((box: any, index: number) => {
160 | if (index === positionsData.active) {
161 | //There should only be one current.
162 | editor.setDecorations(currentArea, [{
163 | hoverMessage: box.name,
164 | range: new Range(lineNumber, box.start, lineNumber, box.end+1)
165 | }]);
166 |
167 | } else {
168 | decorations.push({
169 | hoverMessage: box.name,
170 | range: new Range(lineNumber, box.start, lineNumber, box.end+1)
171 | })
172 | }
173 | });
174 |
175 | editor.setDecorations(notCurrentArea, decorations);
176 |
177 | if (currentEditorLine !== lineNumber && lineNumber >= 1) {
178 | editor.setDecorations(outlineBar, [
179 | {
180 | range: new Range(lineNumber-1, 0, lineNumber-1, 80),
181 | renderOptions: {
182 | before: {
183 | contentText: positionsData.outline,
184 | color: new ThemeColor(`editorLineNumber.foreground`),
185 | }
186 | }
187 | },
188 | ]);
189 | }
190 |
191 | clear = false;
192 | }
193 |
194 | currentEditorLine = lineNumber;
195 | }
196 | }
197 | }
198 |
199 | if (clear) {
200 | clearRulers(editor);
201 | }
202 | }
203 |
204 | function clearRulers(editor = window.activeTextEditor) {
205 | if (editor) {
206 | editor.setDecorations(currentArea, []);
207 | editor.setDecorations(notCurrentArea, []);
208 | editor.setDecorations(outlineBar, []);
209 | }
210 | }
211 |
212 | interface FieldBox {
213 | id: string,
214 | text: string,
215 | content: string
216 | values?: SpecFieldValue[],
217 | maxLength?: number
218 | }
219 |
220 | async function promptLine (line: string, _index: number): Promise {
221 | const base = loadBase();
222 |
223 | if (!base) {
224 | window.showErrorMessage(`Code for IBM i is not installed. It is required due to required UI tools.`);
225 | return undefined;
226 | };
227 |
228 |
229 | if (line.length < 6) return undefined;
230 | if (line[6] === `*`) return undefined;
231 | line = line.padEnd(80);
232 |
233 | const specLetter = line[5].toUpperCase();
234 | if (specs[specLetter]) {
235 | const specification = specs[specLetter];
236 |
237 | let parts: FieldBox[] = [];
238 |
239 | specification.forEach(box => {
240 | parts.push({
241 | id: box.id,
242 | text: box.name,
243 | content: line.substring(box.start, box.end+1).trimEnd(),
244 | values: box.values,
245 | maxLength: box.values ? undefined : (box.end+1)-box.start
246 | });
247 | });
248 |
249 | const ui = base.customUI();
250 |
251 | parts.forEach((box, index) => {
252 | if (box.values) {
253 | //Select box
254 | ui.addSelect(box.id, box.text, box.values.map(item => ({
255 | selected: item.value.toUpperCase() === box.content.toUpperCase(),
256 | value: item.value,
257 | description: item.value,
258 | text: item.text
259 | })))
260 |
261 | } else {
262 | //Input field
263 | ui.addInput(box.id, box.text);
264 | ui.fields[index].default = box.content;
265 | ui.fields[index].maxlength = box.maxLength;
266 | }
267 | });
268 |
269 | ui.addButtons(
270 | { id: `apply`, label: `Apply changes` },
271 | { id: `cancel`, label: `Cancel` }
272 | );
273 |
274 | const result = await ui.loadPage<{[key: string]: string}>(`Column Assistant`);
275 |
276 | if (result && result.data) {
277 | result.panel.dispose();
278 | const data = result.data;
279 |
280 | if (data.buttons !== `cancel`) {
281 | let spot: SpecFieldDef|undefined, length: number;
282 | for (const key in data) {
283 | spot = specification.find(box => box.id === key);
284 | if (spot) {
285 | length = (spot.end+1)-spot.start;
286 |
287 | if (data[key].length > length) data[key] = data[key].substr(0, length);
288 |
289 | line = line.substring(0, spot.start) + (spot.padStart ? data[key].padStart(length) : data[key].padEnd(length)) + line.substring(spot.end+1);
290 | }
291 | }
292 |
293 | return line.trimEnd();
294 | }
295 | }
296 |
297 | return undefined;
298 | } else {
299 | return undefined;
300 | }
301 | }
--------------------------------------------------------------------------------
/extension/client/src/language/config.ts:
--------------------------------------------------------------------------------
1 | import { languages } from "vscode";
2 |
3 | export function setLanguageSettings() {
4 | return languages.setLanguageConfiguration(`rpgle`, {
5 | wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g
6 | });
7 | }
--------------------------------------------------------------------------------
/extension/client/src/language/serverReferences.ts:
--------------------------------------------------------------------------------
1 | import { commands, Definition, DocumentSymbol, languages, Location, ProgressLocation, Range, SymbolInformation, SymbolKind, TextDocument, Uri, window, workspace } from "vscode";
2 | import { getInstance } from "../base";
3 | import IBMi from "@halcyontech/vscode-ibmi-types/api/IBMi";
4 | import { ConnectionConfig, IBMiMember } from "@halcyontech/vscode-ibmi-types";
5 |
6 | export function getServerSymbolProvider() {
7 | let latestFetch: ExportInfo[]|undefined;
8 |
9 | return languages.registerWorkspaceSymbolProvider({
10 | provideWorkspaceSymbols: async (query, token): Promise => {
11 | const instance = getInstance();
12 | const editor = window.activeTextEditor;
13 |
14 | // Since this is a members only resolve.
15 | const documentIsValid = (doc: TextDocument) => {
16 | return doc.uri.scheme === `member` && doc.languageId === `rpgle`;
17 | }
18 |
19 | if (instance && instance.getConnection()) {
20 | const connection = instance.getConnection();
21 | const config = connection.config! //TODO in vscode-ibmi 3.0.0 - change to getConfig()
22 |
23 | let member: IBMiMember|undefined;
24 |
25 | if (editor && documentIsValid(editor.document)) {
26 | const uriPath = editor.document.uri.path;
27 | member = connection.parserMemberPath(uriPath);
28 | }
29 |
30 | const libraryList = getLibraryList(config, member);
31 |
32 | if (query.length === 0 || !latestFetch) {
33 | latestFetch = await binderLookup(connection, libraryList, {generic: query});
34 | }
35 |
36 | return latestFetch.map(e => {
37 | return new SymbolInformation(
38 | e.symbolName,
39 | SymbolKind.Function,
40 | e.moduleName,
41 | new Location(e.assumedUri, new Range(0, 0, 0, 0))
42 | )
43 | })
44 | }
45 |
46 | return [];
47 | },
48 |
49 | resolveWorkspaceSymbol: async (symbol, token): Promise => {
50 | const matchingSymbol = await window.withProgress({location: ProgressLocation.Window, title: `Fetching symbol ${symbol.name}`}, () => getSymbolFromDocument(symbol.location.uri, symbol.name));
51 |
52 | if (matchingSymbol) {
53 | return new SymbolInformation(
54 | matchingSymbol.name,
55 | matchingSymbol.kind,
56 | symbol.containerName,
57 | new Location(symbol.location.uri, matchingSymbol.selectionRange)
58 | )
59 | }
60 |
61 | return symbol;
62 | }
63 | })
64 | }
65 |
66 | export function getServerImplementationProvider() {
67 | return languages.registerImplementationProvider({language: `rpgle`, scheme: `member`}, {
68 | async provideImplementation(document, position, token): Promise {
69 | const instance = getInstance();
70 | const connection = instance?.getConnection();
71 |
72 | if (connection) {
73 | const word = document.getText(document.getWordRangeAtPosition(position));
74 | const config = connection.getConfig() //TODO in vscode-ibmi 3.0.0 - change to getConfig()
75 |
76 | const uriPath = document.uri.path;
77 | const member = connection.parserMemberPath(uriPath);
78 |
79 | const libraryList = getLibraryList(config, member);
80 |
81 | const exports = await binderLookup(connection, libraryList, {specific: word});
82 |
83 | if (exports.length) {
84 | const exportsInLibraryListOrder = libraryList.map(lib => exports.find(e => e.sourceLibrary === lib)).filter(e => e) as ExportInfo[];
85 |
86 | const resultingLocation = await window.withProgress({location: ProgressLocation.Window, title: `Resolving ${word}`}, async (progress) => {
87 | for (const exportInfo of exportsInLibraryListOrder) {
88 | progress.report({message: `checking ${exportInfo.moduleLibrary}/${exportInfo.moduleName}`});
89 | const uri = exportInfo.assumedUri;
90 |
91 | const possibleSymbol = await getSymbolFromDocument(uri, word);
92 | if (possibleSymbol) {
93 | return new Location(uri, possibleSymbol.selectionRange);
94 | }
95 | }
96 | });
97 |
98 | return resultingLocation;
99 | }
100 |
101 | return;
102 | }
103 | },
104 | });
105 | }
106 |
107 | async function getSymbolFromDocument(docUri: Uri, name: string): Promise {
108 | try {
109 | const openedDocument = await workspace.openTextDocument(docUri);
110 | const symbols = await getDocumentSymbols(openedDocument.uri);
111 | return symbols.find(s => s.name.toUpperCase() === name.toUpperCase());
112 | } catch (e) {
113 | console.log(e);
114 | }
115 |
116 | return;
117 | }
118 |
119 | function getLibraryList(config: ConnectionConfig, member?: IBMiMember): string[] {
120 | let libraryList = [config.currentLibrary, ...config.libraryList];
121 |
122 | if (member) {
123 | const editorLibrary = member.library;
124 | if (editorLibrary) {
125 | if (!libraryList.includes(editorLibrary)) {
126 | libraryList.unshift(editorLibrary);
127 | }
128 | }
129 | }
130 |
131 | return libraryList;
132 | }
133 |
134 | function getDocumentSymbols(uri: Uri) {
135 | return commands.executeCommand(`vscode.executeDocumentSymbolProvider`, uri) || [];
136 | }
137 |
138 | interface ExportInfo {
139 | symbolName: string;
140 | programLibrary: string;
141 | programName: string;
142 | moduleLibrary: string;
143 | moduleName: string;
144 | sourceLibrary: string;
145 | sourceFile: string;
146 | sourceMember: string;
147 | attribute: string;
148 | assumedUri: Uri;
149 | }
150 |
151 | async function binderLookup(connection: IBMi, libraryList: string[], filter: {specific?: string, generic?: string} = {}) {
152 | let symbolClause = ``;
153 |
154 | if (filter.generic) {
155 | symbolClause = filter.generic ? `UPPER(b.SYMBOL_NAME) like '%${filter.generic.toUpperCase()}%' and` : ``;
156 | } else if (filter.specific) {
157 | symbolClause = filter.specific ? `UPPER(b.SYMBOL_NAME) = '${filter.specific.toUpperCase()}' and` : ``;
158 | }
159 |
160 | const libraryInList = libraryList.map(lib => `'${lib.toUpperCase()}'`).join(`, `);
161 |
162 | const statement = [
163 | `select`,
164 | ` b.SYMBOL_NAME,`,
165 | ` b.PROGRAM_LIBRARY as PGM_LIB,`,
166 | ` c.ENTRY as PGM_NAME,`,
167 | ` a.BOUND_MODULE_LIBRARY as MOD_LIB, `,
168 | ` a.BOUND_MODULE as MOD_NAME, `,
169 | // ...(streamFileSupported ? [`a.SOURCE_STREAM_FILE_PATH as PATH,`] : []),
170 | ` a.SOURCE_FILE_LIBRARY as LIB, `,
171 | ` a.SOURCE_FILE as SPF, `,
172 | ` a.SOURCE_FILE_MEMBER as MBR,`,
173 | ` a.MODULE_ATTRIBUTE as ATTR`,
174 | `from QSYS2.BOUND_MODULE_INFO as a`,
175 | `right join QSYS2.PROGRAM_EXPORT_IMPORT_INFO as b`,
176 | ` on a.PROGRAM_LIBRARY = b.PROGRAM_LIBRARY and a.PROGRAM_NAME = b.PROGRAM_NAME`,
177 | `right join qsys2.BINDING_DIRECTORY_INFO as c`,
178 | ` on c.ENTRY = b.PROGRAM_NAME`,
179 | `where ${symbolClause}`,
180 | ` (c.BINDING_DIRECTORY_LIBRARY in (${libraryInList})) and`,
181 | ` ((c.ENTRY_LIBRARY = b.PROGRAM_LIBRARY) or (c.ENTRY_LIBRARY = '*LIBL' and b.PROGRAM_LIBRARY in (${libraryInList}))) and`,
182 | ` (a.SOURCE_FILE_MEMBER is not null)`,
183 | // ` (${streamFileSupported ? `a.SOURCE_STREAM_FILE_PATH is not null or` : ``} a.SOURCE_FILE_MEMBER is not null)`
184 | ].join(` `);
185 |
186 | let exports: ExportInfo[] = [];
187 |
188 | try {
189 | const results = await connection.runSQL(statement);
190 |
191 | exports = results.map((r): ExportInfo => {
192 | return {
193 | symbolName: r.SYMBOL_NAME as string,
194 | programLibrary: r.PGM_LIB as string,
195 | programName: r.PGM_NAME as string,
196 | moduleLibrary: r.MOD_LIB as string,
197 | moduleName: r.MOD_NAME as string,
198 | sourceLibrary: r.LIB as string,
199 | sourceFile: r.SPF as string,
200 | sourceMember: r.MBR as string,
201 | attribute: r.ATTR as string,
202 | assumedUri: Uri.from({
203 | scheme: `member`,
204 | path: [``, r.LIB, r.SPF, `${r.MBR}.${r.ATTR}`].join(`/`)
205 | })
206 | }
207 | })
208 |
209 | } catch (e) {
210 | console.log(e);
211 | }
212 |
213 | return exports;
214 | }
--------------------------------------------------------------------------------
/extension/client/src/linter.ts:
--------------------------------------------------------------------------------
1 | import path = require('path');
2 | import { commands, ExtensionContext, Uri, ViewColumn, window, workspace } from 'vscode';
3 | import {getInstance} from './base';
4 |
5 | import {DEFAULT_SCHEMA} from "./schemas/linter"
6 |
7 | export function initialise(context: ExtensionContext) {
8 | context.subscriptions.push(
9 | commands.registerCommand(`vscode-rpgle.openLintConfig`, async (filter) => {
10 | const instance = getInstance();
11 | const editor = window.activeTextEditor;
12 |
13 | let exists = false;
14 |
15 | if (editor && ![`member`, `streamfile`].includes(editor.document.uri.scheme)) {
16 | const workspaces = workspace.workspaceFolders;
17 | if (workspaces && workspaces.length > 0) {
18 | const linter = await workspace.findFiles(`**/.vscode/rpglint.json`, `**/.git`, 1);
19 | let uri;
20 | if (linter && linter.length > 0) {
21 | uri = linter[0];
22 |
23 | console.log(`Uri path: ${JSON.stringify(uri)}`);
24 |
25 | } else {
26 | console.log(`String path: ${path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`)}`);
27 |
28 | uri = Uri.from({
29 | scheme: `file`,
30 | path: path.join(workspaces[0].uri.fsPath, `.vscode`, `rpglint.json`)
31 | });
32 |
33 | console.log(`Creating Uri path: ${JSON.stringify(uri)}`);
34 |
35 | await workspace.fs.writeFile(
36 | uri,
37 | Buffer.from(JSON.stringify(DEFAULT_SCHEMA, null, 2), `utf8`)
38 | );
39 | }
40 |
41 | workspace.openTextDocument(uri).then(doc => {
42 | window.showTextDocument(doc, {
43 | viewColumn: ViewColumn.One
44 | });
45 | });
46 | }
47 |
48 | } else if (instance && instance.getConnection()) {
49 | const connection = instance.getConnection();
50 | const content = instance.getContent();
51 |
52 | /** @type {"member"|"streamfile"} */
53 | let type = `member`;
54 | let configPath: string | undefined;
55 |
56 | if (filter && filter.description) {
57 | // Bad way to get the library for the filter ..
58 | const library: string = (filter.description.split(`/`)[0]).toLocaleUpperCase();
59 |
60 | if (library.includes(`*`)) {
61 | window.showErrorMessage(`Cannot show lint config for a library filter.`);
62 | return;
63 | }
64 |
65 | configPath = `${library}/VSCODE/RPGLINT.JSON`;
66 |
67 | exists = (await connection.runCommand({
68 | command: `CHKOBJ OBJ(${library}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`,
69 | noLibList: true
70 | })).code === 0;
71 |
72 | } else if (editor) {
73 | //@ts-ignore
74 | type = editor.document.uri.scheme;
75 |
76 | console.log(`Uri remote path: ${JSON.stringify(editor.document.uri)}`);
77 |
78 | switch (type) {
79 | case `member`:
80 | const memberPath = parseMemberUri(editor.document.uri.path);
81 | const cleanString = [
82 | memberPath.library,
83 | `VSCODE`,
84 | `RPGLINT.JSON`
85 | ].join(`/`);
86 |
87 | const memberUri = Uri.from({
88 | scheme: `member`,
89 | path: cleanString
90 | });
91 |
92 | configPath = memberUri.path;
93 |
94 | exists = (await connection.runCommand({
95 | command: `CHKOBJ OBJ(${memberPath.library!.toLocaleUpperCase()}/VSCODE) OBJTYPE(*FILE) MBR(RPGLINT)`,
96 | noLibList: true
97 | })).code === 0;
98 | break;
99 |
100 | case `streamfile`:
101 | const config = instance.getConfig();
102 | if (config.homeDirectory) {
103 | configPath = path.posix.join(config.homeDirectory, `.vscode`, `rpglint.json`)
104 | exists = await content.testStreamFile(configPath, `r`);
105 | }
106 | break;
107 | }
108 | } else {
109 | window.showErrorMessage(`No active editor found.`);
110 | }
111 |
112 | if (configPath) {
113 | console.log(`Current path: ${configPath}`);
114 |
115 | if (exists) {
116 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath);
117 | } else {
118 | window.showErrorMessage(`RPGLE linter config doesn't exist for this file. Would you like to create a default at ${configPath}?`, `Yes`, `No`).then
119 | (async (value) => {
120 | if (value === `Yes`) {
121 | const jsonString = JSON.stringify(DEFAULT_SCHEMA, null, 2);
122 |
123 | switch (type) {
124 | case `member`:
125 | if (configPath) {
126 | const memberPath = configPath.split(`/`);
127 |
128 | // Will not crash, even if it fails
129 | await connection.runCommand(
130 | {
131 | 'command': `CRTSRCPF FILE(${memberPath[0]}/VSCODE) RCDLEN(112)`
132 | }
133 | );
134 |
135 | // Will not crash, even if it fails
136 | await connection.runCommand(
137 | {
138 | command: `ADDPFM FILE(${memberPath[0]}/VSCODE) MBR(RPGLINT) SRCTYPE(JSON)`
139 | }
140 | );
141 |
142 | try {
143 | console.log(`Member path: ${[memberPath[0], `VSCODE`, `RPGLINT`].join(`/`)}`);
144 |
145 | await content.uploadMemberContent(undefined, memberPath[0], `VSCODE`, `RPGLINT`, jsonString);
146 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath);
147 | } catch (e) {
148 | console.log(e);
149 | window.showErrorMessage(`Failed to create and open new lint configuration file: ${configPath}`);
150 | }
151 | }
152 | break;
153 |
154 | case `streamfile`:
155 | console.log(`IFS path: ${configPath}`);
156 |
157 | try {
158 | await content.writeStreamfile(configPath, jsonString);
159 | await commands.executeCommand(`code-for-ibmi.openEditable`, configPath);
160 | } catch (e) {
161 | console.log(e);
162 | window.showErrorMessage(`Failed to create and open new lint configuration file: ${configPath}`);
163 | }
164 | break;
165 | }
166 | }
167 | });
168 | }
169 | } else {
170 | window.showErrorMessage(`No lint config path for this file. File must either be a member or a streamfile on the host IBM i.`);
171 | }
172 | } else {
173 | window.showErrorMessage(`Not connected to a system.`);
174 | }
175 | }),
176 | )
177 | }
178 |
179 | function parseMemberUri(fullPath: string): {asp?: string, library?: string, file?: string, name: string} {
180 | const parts = fullPath.split(`/`).map(s => s.split(`,`)).flat().filter(s => s.length >= 1);
181 | return {
182 | name: path.parse(parts[parts.length - 1]).name,
183 | file: parts[parts.length - 2],
184 | library: parts[parts.length - 3],
185 | asp: parts[parts.length - 4]
186 | }
187 | };
--------------------------------------------------------------------------------
/extension/client/src/requests.ts:
--------------------------------------------------------------------------------
1 | import path = require('path');
2 | import { Uri, workspace, RelativePattern, commands } from 'vscode';
3 | import { LanguageClient } from 'vscode-languageclient/node';
4 | import { getInstance } from './base';
5 | import { IBMiMember } from '@halcyontech/vscode-ibmi-types';
6 |
7 | export function buildRequestHandlers(client: LanguageClient) {
8 | /**
9 | * Validates a URI.
10 | * 1. Attemps to open a valid full path
11 | * 2. If running in a workspace, will search for the file by basename
12 | */
13 | client.onRequest("getUri", async (stringUri: string): Promise => {
14 | const uri = Uri.parse(stringUri);
15 | let doc;
16 | try {
17 | doc = await workspace.openTextDocument(uri);
18 | } catch (e: any) {
19 | doc = undefined;
20 | }
21 |
22 | if (doc) {
23 | return doc.uri.toString();
24 | } else
25 | if (uri.scheme === `file`) {
26 | const basename = path.basename(uri.path);
27 | const [possibleFile] = await workspace.findFiles(`**/${basename}`, `**/.git`, 1);
28 | if (possibleFile) {
29 | return possibleFile.toString();
30 | }
31 | }
32 |
33 | return;
34 | });
35 |
36 | /**
37 | * Returns the working directory from Code for IBM i.
38 | */
39 | client.onRequest("getWorkingDirectory", async (): Promise => {
40 | const instance = getInstance();
41 | if (instance) {
42 | const connection = instance.getConnection();
43 | const config = connection?.getConfig();
44 | if (config) {
45 | return config.homeDirectory;
46 | }
47 | }
48 | })
49 |
50 | /**
51 | * Gets the text content for a provided Uri
52 | */
53 | client.onRequest("getFile", async (stringUri: string): Promise => {
54 | // Always assumes URI is valid. Use getUri first
55 | const uri = Uri.parse(stringUri);
56 | try {
57 | const doc = await workspace.openTextDocument(uri);
58 |
59 | if (doc) {
60 | return doc.getText();
61 | }
62 | } catch (e) { }
63 |
64 | return;
65 | });
66 |
67 | /**
68 | * Resolves member paths
69 | */
70 | client.onRequest("memberResolve", async (parms: string[]): Promise => {
71 | let memberName = parms[0], sourceFile = parms[1];
72 |
73 | const instance = getInstance();
74 | const connection = instance?.getConnection();
75 |
76 | if (connection) {
77 | const config = connection.getConfig();
78 | const content = connection.getContent();
79 |
80 | if (config && content) {
81 | const files = [config?.currentLibrary, ...config?.libraryList!]
82 | .filter(l => l !== undefined)
83 | .map(l => ({ name: sourceFile, library: l! }));
84 |
85 | try {
86 | const member = await content?.memberResolve(memberName, files);
87 |
88 | return member;
89 | } catch (e) {
90 | console.log(e);
91 | return undefined;
92 | }
93 | }
94 | }
95 | });
96 |
97 | client.onRequest("streamfileResolve", async (parms: any[]): Promise => {
98 | const bases: string[] = parms[0];
99 | const includePaths: string[] = parms[1];
100 |
101 | const instance = getInstance();
102 | const connection = instance?.getConnection();
103 |
104 | if (connection) {
105 | const content = connection.getContent();
106 | const config = connection.getConfig()!;
107 |
108 | if (instance && content && config) {
109 | if (includePaths.length === 0) {
110 | includePaths.push(config.homeDirectory);
111 | }
112 |
113 | const resolvedPath = await content?.streamfileResolve(bases, includePaths);
114 |
115 | return resolvedPath;
116 | }
117 | }
118 | });
119 |
120 | /**
121 | * Gets the column information for a provided file
122 | */
123 | client.onRequest(`getObject`, async (table: string) => {
124 | const instance = getInstance();
125 |
126 | if (instance) {
127 | console.log(`Fetching table: ${table}`);
128 |
129 | const connection = instance.getConnection();
130 | if (connection) {
131 | const content = connection.getContent();
132 | const config = connection.getConfig();
133 |
134 | const dateStr = Date.now().toString().substr(-6);
135 | const randomFile = `R${table.substring(0, 3)}${dateStr}`.substring(0, 10);
136 | const fullPath = `${config.tempLibrary}/${randomFile}`;
137 |
138 | console.log(`Temp OUTFILE: ${fullPath}`);
139 |
140 | const parts = {
141 | schema: `*LIBL`,
142 | table: ``,
143 | };
144 |
145 | if (table.includes(`/`)) {
146 | const splitName = table.split(`/`);
147 | if (splitName.length >= 2) parts.schema = splitName[splitName.length - 2];
148 | if (splitName.length >= 1) parts.table = splitName[splitName.length - 1];
149 | } else {
150 | parts.table = table;
151 | }
152 |
153 | // TODO: handle .env file here?
154 |
155 | const outfileRes: any = await connection.runCommand({
156 | environment: `ile`,
157 | command: `DSPFFD FILE(${parts.schema}/${parts.table}) OUTPUT(*OUTFILE) OUTFILE(${fullPath})`
158 | });
159 |
160 | console.log(outfileRes);
161 | const resultCode = outfileRes.code || 0;
162 |
163 | if (resultCode === 0) {
164 | const data: any[] = await content.getTable(config.tempLibrary, randomFile, randomFile, true);
165 |
166 | console.log(`Temp OUTFILE read. ${data.length} rows.`);
167 |
168 | connection.runCommand({
169 | environment: `ile`,
170 | command: `DLTOBJ OBJ(${fullPath}) OBJTYPE(*FILE)`
171 | });
172 |
173 | return data;
174 | }
175 | }
176 | }
177 |
178 | return [];
179 | });
180 | }
181 |
182 | export function clearTableCache(client: LanguageClient) {
183 | client.sendRequest(`clearTableCache`);
184 | }
--------------------------------------------------------------------------------
/extension/client/src/schemas/linter.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_SCHEMA = {
2 | indent: 2,
3 | PrototypeCheck: true,
4 | NoOCCURS: true,
5 | NoSELECTAll: true,
6 | UppercaseConstants: true,
7 | IncorrectVariableCase: true,
8 | NoSQLJoins: true,
9 | PrettyComments: true,
10 | NoGlobalSubroutines: true,
11 | NoExternalTo: [
12 | `QCMD`,
13 | `QP2TERM`,
14 | `QSH`,
15 | `SYSTEM`,
16 | `QCMDEXC`,
17 | ]
18 | }
--------------------------------------------------------------------------------
/extension/client/src/schemas/specs.ts:
--------------------------------------------------------------------------------
1 | export type SpecFieldValue = {value: string, text: string};
2 | export type SpecFieldDef = {id: string, name: string, start: number, end: number, values?: SpecFieldValue[], padStart?: boolean}
3 |
4 | export const SpecRulers: {[spec: string]: string} = {
5 | C: `.....CL0N01Factor1+++++++Opcode&ExtFactor2+++++++Result++++++++Len++D+HiLoEq`,
6 | D: `.....DName+++++++++++ETDsFrom+++To/L+++IDc.Keywords++++++++++++++++++++`,
7 | F: `.....FFilename++IPEASFRlen+LKlen+AIDevice+.Keywords++++++++++++++++++++`,
8 | P: `.....PName+++++++++++..T...................Keywords++++++++++++++++++++`
9 | }
10 |
11 | export const specs: {[spec: string]: SpecFieldDef[]} = {
12 | C: [
13 | {
14 | id: `controlLevel`,
15 | name: `Control Level`,
16 | start: 6,
17 | end: 7,
18 | },
19 | {
20 | id: `indicators`,
21 | name: `Indicators`,
22 | start: 8,
23 | end: 10,
24 | },
25 | {
26 | id: `factor1`,
27 | name: `Factor 1`,
28 | start: 11,
29 | end: 24
30 | },
31 | {
32 | id: `operation`,
33 | name: `Operation and Extender`,
34 | start: 25,
35 | end: 34
36 | },
37 | {
38 | id: `factor2`,
39 | name: `Factor 2`,
40 | start: 35,
41 | end: 48
42 | },
43 | {
44 | id: `result`,
45 | name: `Result Field`,
46 | start: 49,
47 | end: 62
48 | },
49 | {
50 | id: `fieldLength`,
51 | name: `Field Length`,
52 | start: 63,
53 | end: 67
54 | },
55 | {
56 | id: `decimalPositions`,
57 | name: `Decimal Positions`,
58 | start: 68,
59 | end: 69
60 | },
61 | {
62 | id: `resultingIndicatorsA`,
63 | name: `Resulting Indicator`,
64 | start: 70,
65 | end: 71
66 | },
67 | {
68 | id: `resultingIndicatorsB`,
69 | name: `Resulting Indicator`,
70 | start: 72,
71 | end: 73
72 | },
73 | {
74 | id: `resultingIndicatorsC`,
75 | name: `Resulting Indicator`,
76 | start: 74,
77 | end: 75
78 | }
79 | ],
80 |
81 | D: [
82 | {start: 6, end: 20, name: `Name`, id: `name`},
83 | {start: 21, end: 21, name: `External Description`, id: `externalDescription`},
84 | {start: 22, end: 22, name: `Type of Data Structure`, id: `typeOfDs`},
85 | {start: 23, end: 24, name: `Definition Type`, id: `definitionType`, values: [
86 | { value: ``,
87 | text: `The specification defines either a data structure subfield or a parameter within a prototype or procedure interface definition.`},
88 | { value: `C`,
89 | text: `The specification defines a constant. Position 25 must be blank.`},
90 | { value: `DS`,
91 | text: `The specification defines a data structure.`},
92 | { value: `PR`,
93 | text: `The specification defines a prototype and the return value, if any.`},
94 | { value: `PI`,
95 | text: `The specification defines a procedure interface, and the return value if any.`},
96 | { value: `S`,
97 | text: `The specification defines a standalone field, array or table. Position 25 must be blank.`},
98 | ]},
99 | {start: 25, end: 31, name: `From Position`, id: `fromPosition`, padStart: true},
100 | {start: 32, end: 38, name: `To Position / Length`, id: `toPosition`, padStart: true},
101 | {start: 39, end: 39, name: `Internal Data Type`, id: `internalDataType`, values: [
102 | { value: `A`,
103 | text: `Character (Fixed or Variable-length format)`},
104 | { value: `B`,
105 | text: `Numeric (Binary format)`},
106 | { value: `C`,
107 | text: `UCS-2 (Fixed or Variable-length format)`},
108 | { value: `D`,
109 | text: `Date`},
110 | { value: `F`,
111 | text: `Numeric (Float format)`},
112 | { value: `G`,
113 | text: `Graphic (Fixed or Variable-length format)`},
114 | { value: `I`,
115 | text: `Numeric (Integer format)`},
116 | { value: `N`,
117 | text: `Character (Indicator format)`},
118 | { value: `O`,
119 | text: `Object`},
120 | { value: `P`,
121 | text: `Numeric (Packed decimal format)`},
122 | { value: `S`,
123 | text: `Numeric (Zoned format)`},
124 | { value: `T`,
125 | text: `Time`},
126 | { value: `U`,
127 | text: `Numeric (Unsigned format)`},
128 | { value: `Z`,
129 | text: `Timestamp`},
130 | { value: `*`,
131 | text: `Basing pointer or procedure pointer`},
132 | { value: ``, text: `Blank (Character, Packed or Zoned)`}
133 | ]},
134 | {start: 40, end: 41, name: `Decimal Positions`, id: `decimalPositions`, padStart: true},
135 | {start: 43, end: 79, name: `Keywords`, id: `keywords`}
136 | ],
137 | F: [
138 | {start: 6, end: 15, name: `File Name`, id: `fileName`},
139 | {start: 16, end: 16, name: `File Type`, id: `fileType`},
140 | {start: 17, end: 17, name: `File Designation`, id: `fileDesignation`},
141 | {start: 18, end: 18, name: `End of File`, id: `endOfFile`},
142 | {start: 19, end: 19, name: `File Addition`, id: `fileAddition`},
143 | {start: 20, end: 20, name: `Sequence`, id: `sequence`},
144 | {start: 21, end: 21, name: `File Format`, id: `fileFormat`},
145 | {start: 22, end: 26, name: `Record Length`, id: `recordLength`},
146 | {start: 27, end: 27, name: `Limits Processing`, id: `limitsProcessing`},
147 | {start: 28, end: 32, name: `Length of Key or Record Address`, id: `keyLength`},
148 | {start: 33, end: 33, name: `Record Address Type`, id: `addressType`},
149 | {start: 34, end: 34, name: `File Organization`, id: `fileOrg`},
150 | {start: 35, end: 41, name: `Device`, id: `device`},
151 | {start: 43, end: 79, name: `Keywords`, id: `keywords`}
152 | ],
153 | P: [
154 | {start: 6, end: 20, name: `Name`, id: `name`},
155 | {start: 23, end: 23, name: `Begin/End Procedure`, id: `proc`},
156 | {start: 43, end: 79, name: `Keywords`, id: `keywords`}
157 | ]
158 | };
--------------------------------------------------------------------------------
/extension/client/testFixture/completion.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefori/vscode-rpgle/09e6cbca24e9c04fd2c8cece4cbf40f8c1808468/extension/client/testFixture/completion.txt
--------------------------------------------------------------------------------
/extension/client/testFixture/diagnostics.txt:
--------------------------------------------------------------------------------
1 | ANY browsers, ANY OS.
--------------------------------------------------------------------------------
/extension/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "target": "es2020",
6 | "outDir": "out",
7 | "rootDir": "src",
8 | "lib": [ "es2020" ],
9 | "sourceMap": true,
10 | "composite": true
11 | },
12 | "include": [
13 | "src"
14 | ],
15 | "exclude": [
16 | "node_modules", ".vscode-test", "src/test"
17 | ]
18 | }
--------------------------------------------------------------------------------
/extension/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | /*---------------------------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * Licensed under the MIT License. See License.txt in the project root for license information.
4 | *--------------------------------------------------------------------------------------------*/
5 |
6 | //@ts-check
7 |
8 | 'use strict';
9 |
10 | const withDefaults = require(`../../shared.webpack.config`);
11 | const path = require(`path`);
12 |
13 | module.exports = withDefaults({
14 | context: path.join(__dirname),
15 | entry: {
16 | extension: `./src/extension.ts`,
17 | },
18 | output: {
19 | filename: `extension.js`,
20 | path: path.join(__dirname, `..`, `..`, `out`)
21 | }
22 | });
--------------------------------------------------------------------------------
/extension/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "description": "RPGLE language server",
4 | "version": "1.0.0",
5 | "author": "Liam Allan",
6 | "license": "MIT",
7 | "engines": {
8 | "node": "*"
9 | },
10 | "dependencies": {
11 | "glob": "^7.2.0",
12 | "minimatch": "^5.1.0",
13 | "p-queue": "^7.3.4",
14 | "vscode-languageserver": "^8.0.2",
15 | "vscode-languageserver-textdocument": "^1.0.7",
16 | "vscode-uri": "^3.0.6"
17 | },
18 | "devDependencies": {
19 | "@halcyontech/vscode-ibmi-types": "^2.14.0",
20 | "@types/glob": "^8.0.0",
21 | "webpack": "^5.24.3"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/extension/server/src/connection.ts:
--------------------------------------------------------------------------------
1 | import { IBMiMember } from '@halcyontech/vscode-ibmi-types';
2 |
3 | import {
4 | createConnection,
5 | DidChangeWatchedFilesParams,
6 | ProposedFeatures,
7 | _Connection,
8 | WorkspaceFolder
9 | } from 'vscode-languageserver/node';
10 |
11 | import PQueue from 'p-queue';
12 |
13 | import { documents, findFile, parser } from './providers';
14 | import { includePath } from './providers/project';
15 |
16 | // Create a connection for the server, using Node's IPC as a transport.
17 | // Also include all preview / proposed LSP features.
18 | export const connection: _Connection = createConnection(ProposedFeatures.all);
19 |
20 | const queue = new PQueue();
21 |
22 | export let watchedFilesChangeEvent: ((params: DidChangeWatchedFilesParams) => void)[] = [];
23 | connection.onDidChangeWatchedFiles((params: DidChangeWatchedFilesParams) => {
24 | watchedFilesChangeEvent.forEach(editEvent => editEvent(params));
25 | })
26 |
27 | export async function validateUri(stringUri: string, scheme = ``) {
28 | // First, check local cache
29 | const possibleCachedFile = findFile(stringUri, scheme);
30 | if (possibleCachedFile) return possibleCachedFile;
31 |
32 | console.log(`Validating file from server: ${stringUri}`);
33 |
34 | // Then reach out to the extension to find it
35 | const uri: string|undefined = await connection.sendRequest("getUri", stringUri);
36 | if (uri) return uri;
37 |
38 | return;
39 | }
40 |
41 | export async function getFileRequest(uri: string) {
42 | // First, check if it's local
43 | const localCacheDoc = documents.get(uri);
44 | if (localCacheDoc) return localCacheDoc.getText();
45 |
46 | console.log(`Fetching file from server: ${uri}`);
47 |
48 | // If not, then grab it from remote
49 | const body: string|undefined = await connection.sendRequest("getFile", uri);
50 | if (body) {
51 | // TODO.. cache it?
52 | return body;
53 | }
54 |
55 | return;
56 | }
57 |
58 | export let resolvedMembers: {[baseUri: string]: {[fileKey: string]: IBMiMember}} = {};
59 | export let resolvedStreamfiles: {[baseUri: string]: {[fileKey: string]: string}} = {};
60 |
61 | export async function memberResolve(baseUri: string, member: string, file: string): Promise {
62 | const fileKey = file+member;
63 |
64 | if (resolvedMembers[baseUri] && resolvedMembers[baseUri][fileKey]) return resolvedMembers[baseUri][fileKey];
65 |
66 | try {
67 | const resolvedMember = await queue.add(() => {return connection.sendRequest("memberResolve", [member, file])}) as IBMiMember|undefined;
68 | // const resolvedMember = await connection.sendRequest("memberResolve", [member, file]) as IBMiMember|undefined;
69 |
70 | if (resolvedMember) {
71 | if (!resolvedMembers[baseUri]) resolvedMembers[baseUri] = {};
72 | resolvedMembers[baseUri][fileKey] = resolvedMember;
73 | }
74 |
75 | return resolvedMember;
76 | } catch (e) {
77 | console.log(`Member resolve failed.`);
78 | console.log(JSON.stringify({baseUri, member, file}));
79 | console.log(e);
80 | }
81 |
82 | return undefined;
83 | }
84 |
85 | export async function streamfileResolve(baseUri: string, base: string[]): Promise {
86 | const baseString = base.join(`-`);
87 | if (resolvedStreamfiles[baseUri] && resolvedStreamfiles[baseUri][baseString]) return resolvedStreamfiles[baseUri][baseString];
88 |
89 | const workspace = await getWorkspaceFolder(baseUri);
90 |
91 | const paths = (workspace ? includePath[workspace.uri] : []) || [];
92 |
93 | try {
94 | const resolvedPath = await queue.add(() => {return connection.sendRequest("streamfileResolve", [base, paths])}) as string|undefined;
95 | // const resolvedPath = await connection.sendRequest("streamfileResolve", [base, paths]) as string|undefined;
96 |
97 | if (resolvedPath) {
98 | if (!resolvedStreamfiles[baseUri]) resolvedStreamfiles[baseUri] = {};
99 | resolvedStreamfiles[baseUri][baseString] = resolvedPath;
100 | }
101 |
102 | return resolvedPath;
103 | } catch (e) {
104 | console.log(`Streamfile resolve failed.`);
105 | console.log(JSON.stringify({baseUri, base, paths}));
106 | console.log(e);
107 | }
108 |
109 | return undefined;
110 | }
111 |
112 | export function getWorkingDirectory(): Promise {
113 | return connection.sendRequest("getWorkingDirectory");
114 | }
115 |
116 | export function getObject(objectPath: string): Promise