├── abl-src ├── post-launch.p ├── pre-launch-task.p ├── run.p ├── read-env-var.p ├── dict-dump.p ├── run-debug.p ├── pre-launch.p ├── check-syntax.p ├── test.p └── dict-dump-exec.p ├── .gitignore ├── .vscodeignore ├── docs └── images │ ├── debug.gif │ └── demo.gif ├── images └── progress_icon.png ├── run_env └── sample-oe-projects │ ├── project1 │ ├── src │ │ └── test01.p │ └── .openedge.json │ ├── project2 │ ├── src │ │ └── test02.p │ └── .openedge.json │ ├── project3 │ └── src │ │ └── test03.p │ └── sample.code-workspace ├── src ├── misc │ ├── OpenEdgeFormatOptions.ts │ ├── definition.ts │ └── utils.ts ├── shared │ ├── README.md │ ├── openEdgeConfigFile.ts │ └── ablPath.ts ├── ablMode.ts ├── providers │ ├── ablSymbolProvider.ts │ ├── ablDefinitionProvider.ts │ ├── ablHoverProvider.ts │ ├── ablCompletionProvider.ts │ └── ablFormattingProvider.ts ├── debugAdapter │ ├── variables.ts │ ├── ablDebugConfigurationProvider.ts │ └── messages.ts ├── checkExtensionConfig.ts ├── ablRun.ts ├── ablStatus.ts ├── ablConfig.ts ├── ablDataDictionary.ts ├── parser │ ├── sourceParser.ts │ ├── processDocument.ts │ └── documentController.ts ├── ablCheckSyntax.ts ├── OutputChannelProcess.ts ├── ablTest.ts └── main.ts ├── .prettierrc ├── .editorconfig ├── .eslintignore ├── tsconfig.json ├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── azure-pipelines.yml ├── CHANGELOG.md ├── .eslintrc ├── LICENSE ├── language-configuration.json ├── schemas └── openedge.schema.json ├── test └── abl.test.ts ├── snippets └── abl.json ├── README.md └── package.json /abl-src/post-launch.p: -------------------------------------------------------------------------------- 1 | /* */ 2 | quit. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm* 2 | node_modules 3 | *.vsix 4 | out 5 | .vscode-test 6 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | src/**/* 2 | .vscode/**/* 3 | tsconfig.json 4 | tslint.json 5 | .gitignore 6 | docs -------------------------------------------------------------------------------- /docs/images/debug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscamicas/vscode-abl/HEAD/docs/images/debug.gif -------------------------------------------------------------------------------- /docs/images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscamicas/vscode-abl/HEAD/docs/images/demo.gif -------------------------------------------------------------------------------- /images/progress_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscamicas/vscode-abl/HEAD/images/progress_icon.png -------------------------------------------------------------------------------- /run_env/sample-oe-projects/project1/src/test01.p: -------------------------------------------------------------------------------- 1 | message proversion. 2 | message "hello". 3 | message "hello2". 4 | -------------------------------------------------------------------------------- /run_env/sample-oe-projects/project2/src/test02.p: -------------------------------------------------------------------------------- 1 | message proversion. 2 | message "hello1". 3 | message "hello2". 4 | -------------------------------------------------------------------------------- /run_env/sample-oe-projects/project3/src/test03.p: -------------------------------------------------------------------------------- 1 | message proversion. 2 | message "hello1". 3 | message "hello2". 4 | -------------------------------------------------------------------------------- /src/misc/OpenEdgeFormatOptions.ts: -------------------------------------------------------------------------------- 1 | export interface OpenEdgeFormatOptions { 2 | trim?: 'none' | 'right'; 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/README.md: -------------------------------------------------------------------------------- 1 | Files in this directory are loaded by both the extension and debug adapter, so they cannot import 'vscode' -------------------------------------------------------------------------------- /abl-src/pre-launch-task.p: -------------------------------------------------------------------------------- 1 | /* TODO ajouter la possibilité de lancer un .p (ie: dog-startws.p) via option dans un fichier de config du workspace */ -------------------------------------------------------------------------------- /src/ablMode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export const ABL_MODE: vscode.DocumentFilter = { language: 'abl', scheme: 'file' }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi":true, 4 | "useTabs": false, 5 | "tabWidth": 4, 6 | "endOfLine": "lf" 7 | 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | out 6 | # don't lint nyc coverage output 7 | coverage -------------------------------------------------------------------------------- /run_env/sample-oe-projects/sample.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "project1" 5 | }, 6 | { 7 | "path": "project2" 8 | }, 9 | { 10 | "path": "project3" 11 | } 12 | ], 13 | "settings": {} 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "out", 5 | "sourceMap": true, 6 | "target": "es6", 7 | "lib": [ 8 | "es2015" 9 | ], 10 | "plugins": [ 11 | { 12 | "name": "typescript-tslint-plugin" 13 | } 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /run_env/sample-oe-projects/project2/.openedge.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenEdgeVersion": "11.7", 3 | "gui": false, 4 | "sourceDirectories": [ 5 | "src" 6 | ], 7 | "propath": ["src/procedures", "src/classes", "lib/OpenEdge.net.pl" ], 8 | "buildDirectory": "build", 9 | "dumpFiles": ["dump/sp2k.df"], 10 | "dbConnections": ["-db db/sp2k -RO"], 11 | "aliases": "sp2k,foo,bar", 12 | "numThreads": 2, 13 | "progressVersion": "12.2", 14 | "graphicalMode": false 15 | } -------------------------------------------------------------------------------- /run_env/sample-oe-projects/project1/.openedge.json: -------------------------------------------------------------------------------- 1 | { 2 | "OpenEdgeVersion": "12.2", 3 | "gui": true, 4 | "sourceDirectories": [ 5 | "src/procedures", "src/classes" 6 | ], 7 | "propath": ["src/procedures", "src/classes", "lib/OpenEdge.net.pl" ], 8 | 9 | 10 | 11 | "buildDirectory": "build", 12 | "dumpFiles": ["dump/sp2k.df"], 13 | "dbConnections": ["-db db/sp2k -RO"], 14 | "aliases": "sp2k,foo,bar", 15 | "numThreads": 2, 16 | "progressVersion": "12.2", 17 | "graphicalMode": false 18 | } -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-20.04' 11 | 12 | steps: 13 | - task: NodeTool@0 14 | inputs: 15 | versionSpec: '16.x' 16 | displayName: 'Install Node.js' 17 | 18 | - script: | 19 | npm install 20 | npm run lint 21 | displayName: 'checking lint' 22 | -------------------------------------------------------------------------------- /abl-src/run.p: -------------------------------------------------------------------------------- 1 | DEFINE VARIABLE ch_prog AS CHARACTER NO-UNDO. 2 | 3 | /* Extracts the parameters */ 4 | ASSIGN ch_prog = ENTRY( 1, SESSION:PARAMETER ). 5 | 6 | RUN VALUE( REPLACE( PROGRAM-NAME( 1 ), "run.p", "read-env-var.p") ). 7 | 8 | /* OpenEdge Startup Procedure */ 9 | DEFINE VARIABLE vsabl_oe_startup_procedure AS CHARACTER NO-UNDO. 10 | vsabl_oe_startup_procedure = OS-GETENV ( "VSABL_OE_STARTUP_PROCEDURE" ). 11 | IF LENGTH( vsabl_oe_startup_procedure ) > 0 THEN RUN VALUE( vsabl_oe_startup_procedure ). 12 | 13 | /* RUN */ 14 | RUN VALUE( ch_prog ). 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.svn": true, 6 | "**/.hg": true, 7 | "**/CVS": true, 8 | "**/.DS_Store": true, 9 | "node_modules": true, 10 | "out": true 11 | }, 12 | "search.exclude": { 13 | "**/node_modules": true, 14 | "**/bower_components": true, 15 | "out": true 16 | }, 17 | "eslint.format.enable": true, 18 | "eslint.alwaysShowStatus": true, 19 | "eslint.lintTask.enable": true, 20 | "eslint.enable": true 21 | } -------------------------------------------------------------------------------- /abl-src/read-env-var.p: -------------------------------------------------------------------------------- 1 | DEFINE VARIABLE vsabl_proPath AS CHARACTER NO-UNDO. 2 | vsabl_proPath = OS-GETENV ( "VSABL_PROPATH" ). 3 | 4 | DEFINE VARIABLE vsabl_proPathMode AS CHARACTER NO-UNDO. 5 | vsabl_proPathMode = OS-GETENV ( "VSABL_PROPATH_MODE" ). 6 | 7 | IF LENGTH( vsabl_proPath ) > 0 THEN DO: 8 | CASE vsabl_proPathMode : 9 | WHEN "append" THEN DO : 10 | ASSIGN PROPATH = PROPATH + "," + vsabl_proPath. 11 | END. 12 | WHEN "prepend" THEN DO : 13 | ASSIGN PROPATH = vsabl_proPath + "," + PROPATH. 14 | END. 15 | WHEN "overwrite" THEN DO : 16 | ASSIGN PROPATH = vsabl_proPath. 17 | END. 18 | END. 19 | END. 20 | -------------------------------------------------------------------------------- /abl-src/dict-dump.p: -------------------------------------------------------------------------------- 1 | RUN VALUE( REPLACE( PROGRAM-NAME( 1 ), "dict-dump.p", "pre-launch.p") ). 2 | 3 | /* directory */ 4 | def var nm-dir-aux as char no-undo. 5 | assign nm-dir-aux = replace(OS-GETENV("VSABL_WORKSPACE"), "~\", "/"). 6 | if r-index(nm-dir-aux, "/") < length(nm-dir-aux) 7 | then assign nm-dir-aux = nm-dir-aux + "/". 8 | 9 | def var ix as int no-undo. 10 | repeat ix = 1 to num-dbs: 11 | if (lookup(ldbname(ix), session:parameter) > 0) 12 | then do: 13 | create alias "DICTDB" for database value(ldbname(ix)). 14 | run VALUE(REPLACE(PROGRAM-NAME(1), "dict-dump.p", "dict-dump-exec.p")) (nm-dir-aux). 15 | delete alias "DICTDB". 16 | end. 17 | end. 18 | 19 | RUN VALUE( REPLACE( PROGRAM-NAME( 1 ), "dict-dump.p", "post-launch.p") ). 20 | -------------------------------------------------------------------------------- /src/providers/ablSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ABL_MODE } from '../ablMode'; 3 | import { ABLDocumentController, getDocumentController } from '../parser/documentController'; 4 | 5 | export class ABLSymbolProvider implements vscode.DocumentSymbolProvider { 6 | private _ablDocumentController: ABLDocumentController; 7 | 8 | constructor(context: vscode.ExtensionContext) { 9 | this._ablDocumentController = getDocumentController(); 10 | context.subscriptions.push(vscode.languages.registerDocumentSymbolProvider(ABL_MODE.language, this)); 11 | } 12 | 13 | public provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Thenable { 14 | return Promise.resolve(this._ablDocumentController.getDocument(document).symbols); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.3 2 | ===== 3 | - Added syntax highlight with classes support 4 | 5 | 1.1 6 | ===== 7 | - Code completion and Outline pane 8 | 9 | 1.0 10 | ===== 11 | - You can now specify a startup procedure 12 | 13 | 0.9 14 | ===== 15 | - New definition provider: the outline pane is now filled with the definitions found in the current document 16 | 17 | 0.8 18 | ===== 19 | - You can now define the dlc value from the config file (optional) 20 | 21 | 0.5 22 | ===== 23 | 24 | ## What's new 25 | - `proPath` and `proPathMode` supports in `.openedge.json` config file 26 | 27 | 0.4.2 28 | ===== 29 | 30 | ## What's new 31 | - Better syntax highlighting (define stream scope) 32 | 33 | 0.4.1 34 | ===== 35 | 36 | ## Bug fixes 37 | - full primitive type (ex: character) matches only abbrev (ex: char) 38 | 39 | 0.4.0 40 | ===== 41 | 42 | ## What's new 43 | - Better syntax highlighting (parameter vs variable) 44 | -------------------------------------------------------------------------------- /abl-src/run-debug.p: -------------------------------------------------------------------------------- 1 | DEFINE VARIABLE ch_prog AS CHARACTER NO-UNDO. 2 | 3 | /* Extracts the parameters */ 4 | 5 | ASSIGN ch_prog = OS-GETENV ( "VSABL_STARTUP_PROGRAM" ). 6 | if ch_prog = "" then 7 | do: 8 | ASSIGN ch_prog = ENTRY( 1, SESSION:PARAMETER ). 9 | end. 10 | 11 | RUN VALUE( REPLACE( PROGRAM-NAME( 1 ), "run-debug.p", "read-env-var.p") ). 12 | 13 | /* OpenEdge Startup Procedure */ 14 | DEFINE VARIABLE vsabl_oe_startup_procedure AS CHARACTER NO-UNDO. 15 | vsabl_oe_startup_procedure = OS-GETENV ( "VSABL_OE_STARTUP_PROCEDURE" ). 16 | IF LENGTH( vsabl_oe_startup_procedure ) > 0 THEN RUN VALUE( vsabl_oe_startup_procedure ). 17 | 18 | /* We have to wait for the debugger to connect to this process and set up breakpoints 19 | When ready, the host sends a key input 20 | READKEY is the easiest way I found to pause and wait for a signal from the host */ 21 | READKEY. 22 | RUN VALUE( ch_prog ). 23 | -------------------------------------------------------------------------------- /src/debugAdapter/variables.ts: -------------------------------------------------------------------------------- 1 | export enum AblDebugKind { 2 | Invalid = 0, 3 | Variable, 4 | Buffer, 5 | TempTable, 6 | DataSet, 7 | Parameter, 8 | BaseClass, 9 | Class, 10 | Array, 11 | } 12 | 13 | export interface DebugVariable { 14 | name: string; 15 | type: string; 16 | kind: AblDebugKind; 17 | value: string; 18 | children: DebugVariable[]; 19 | parentReference?: number; 20 | // unreadable: string; 21 | } 22 | 23 | const primitiveTypes = [ 24 | 'BLOB', 25 | 'CHARACTER', 26 | 'CLOB', 27 | 'COM-HANDLE', 28 | 'DATE', 29 | 'DATETIME', 30 | 'DATETIME-TZ', 31 | 'DECIMAL', 32 | 'HANDLE', 33 | 'INT64', 34 | 'INTEGER', 35 | 'LOGICAL', 36 | 'LONGCHAR', 37 | 'MEMPTR', 38 | 'RAW', 39 | 'RECID', 40 | 'ROWID', 41 | 'WIDGET-HANDLE', 42 | ]; 43 | 44 | export function isPrimitiveType(type: string) { 45 | return primitiveTypes.indexOf(type) !== -1; 46 | } 47 | -------------------------------------------------------------------------------- /src/checkExtensionConfig.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'fs'; 2 | import * as promisify from 'util.promisify'; 3 | import * as vscode from 'vscode'; 4 | import { findConfigFile, getOpenEdgeConfig } from './ablConfig'; 5 | import { getBinPath } from './shared/ablPath'; 6 | 7 | const accessAsync = promisify(access); 8 | 9 | export async function checkOpenEdgeConfigFile() { 10 | 11 | // Do we have a .openedge.json config file 12 | const oeConfig = await findConfigFile(); 13 | if (!oeConfig) { 14 | throw new Error('.openedge.json file is missing using default value'); 15 | } 16 | } 17 | 18 | export async function checkProgressBinary() { 19 | // Do we have a .openedge.json config file 20 | const oeConfig = await getOpenEdgeConfig(); 21 | // Can we find the progres binary 22 | let cmd = getBinPath('_progres.exe', oeConfig.dlc); 23 | // try to access the file (throw an Error) 24 | try { 25 | await accessAsync(cmd); 26 | } catch (e) { 27 | cmd = getBinPath('_progres', oeConfig.dlc); 28 | await accessAsync(cmd); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /abl-src/pre-launch.p: -------------------------------------------------------------------------------- 1 | def var wh_v6_display as widget-handle no-undo. 2 | 3 | assign wh_v6_display = session:first-child. 4 | repeat while valid-handle(wh_v6_display): 5 | assign wh_v6_display:hidden = yes no-error. 6 | assign wh_v6_display = wh_v6_display:next-sibling. 7 | end. 8 | 9 | assign session:v6display = yes 10 | session:Immediate-Display = yes 11 | session:data-entry-return = yes. 12 | 13 | /* set env variables */ 14 | DEFINE VARIABLE vsabl_proPath AS CHARACTER NO-UNDO. 15 | vsabl_proPath = OS-GETENV ( "VSABL_PROPATH" ). 16 | 17 | DEFINE VARIABLE vsabl_proPathMode AS CHARACTER NO-UNDO. 18 | vsabl_proPathMode = OS-GETENV ( "VSABL_PROPATH_MODE" ). 19 | 20 | IF LENGTH( vsabl_proPath ) > 0 THEN DO: 21 | CASE vsabl_proPathMode : 22 | WHEN "append" THEN DO : 23 | ASSIGN PROPATH = PROPATH + "," + vsabl_proPath. 24 | END. 25 | WHEN "prepend" THEN DO : 26 | ASSIGN PROPATH = vsabl_proPath + "," + PROPATH. 27 | END. 28 | WHEN "overwrite" THEN DO : 29 | ASSIGN PROPATH = vsabl_proPath. 30 | END. 31 | END. 32 | END. 33 | 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | "@typescript-eslint/no-unused-vars": [ 24 | "warn", 25 | { 26 | "vars": "all", 27 | "args": "none", 28 | "ignoreRestSiblings": false 29 | } 30 | ], 31 | "@typescript-eslint/no-use-before-define": [ 32 | "warn", 33 | { 34 | "functions": true, 35 | "classes": true 36 | } 37 | ], 38 | "@typescript-eslint/interface-name-prefix": 0, 39 | "@typescript-eslint/no-inferrable-types": 0 40 | } 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 chriscamicas 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 | -------------------------------------------------------------------------------- /src/ablRun.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import { getOpenEdgeConfig } from './ablConfig'; 4 | import { outputChannel } from './ablStatus'; 5 | import { create } from './OutputChannelProcess'; 6 | import { createProArgs, getProBin, setupEnvironmentVariables } from './shared/ablPath'; 7 | 8 | export function run(filename: string, ablConfig: vscode.WorkspaceConfiguration): Promise { 9 | outputChannel.clear(); 10 | let cwd = path.dirname(filename); 11 | 12 | return getOpenEdgeConfig().then((oeConfig) => { 13 | const cmd = getProBin(oeConfig.dlc); 14 | const env = setupEnvironmentVariables(process.env, oeConfig, vscode.workspace.rootPath); 15 | const args = createProArgs({ 16 | batchMode: true, 17 | param: filename, 18 | parameterFiles: oeConfig.parameterFiles, 19 | startupProcedure: path.join(__dirname, '../../abl-src/run.p'), 20 | workspaceRoot: vscode.workspace.rootPath, 21 | }); 22 | if (oeConfig.workingDirectory) { 23 | cwd = oeConfig.workingDirectory.replace('${workspaceRoot}', vscode.workspace.rootPath) 24 | .replace('${workspaceFolder}', vscode.workspace.rootPath); 25 | } 26 | return create(cmd, args, { env, cwd }, outputChannel); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": ["/*", "*/"] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["[", "]"], 12 | ["(", ")"] 13 | ], 14 | // symbols that are auto closed when typing 15 | "autoClosingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"], 21 | ["/*", "*/"] 22 | ], 23 | // symbols that that can be used to surround a selection 24 | "surroundingPairs": [ 25 | ["{", "}"], 26 | ["[", "]"], 27 | ["(", ")"], 28 | ["\"", "\""], 29 | ["'", "'"], 30 | ["/*", "*/"] 31 | ], 32 | // lines that end in a colon, followed by possible white space, denote a block 33 | // block ends with "end." or "end" + whitespace + optional word (procedure, function, case, etc) + "." 34 | "indentationRules": { 35 | "increaseIndentPattern": "^.+:\\s*$", 36 | "decreaseIndentPattern": "^\\s*end(\\.|\\s+.*\\.)$" 37 | }, 38 | "folding": { 39 | "markers": { 40 | "start": "^.+:\\s*$", 41 | "end": "^\\s*end(\\.|\\s+.*\\.)$" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/ablStatus.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 | 'use strict'; 7 | 8 | import * as vscode from 'vscode'; 9 | import { ABL_MODE } from './ablMode'; 10 | 11 | export const outputChannel = vscode.window.createOutputChannel('ABL'); 12 | 13 | let statusBarEntry: vscode.StatusBarItem; 14 | 15 | export function showHideStatus() { 16 | if (!statusBarEntry) { 17 | return; 18 | } 19 | if (!vscode.window.activeTextEditor) { 20 | statusBarEntry.hide(); 21 | return; 22 | } 23 | if (vscode.languages.match(ABL_MODE, vscode.window.activeTextEditor.document)) { 24 | statusBarEntry.show(); 25 | return; 26 | } 27 | statusBarEntry.hide(); 28 | } 29 | 30 | export function hideAblStatus() { 31 | if (statusBarEntry) { 32 | statusBarEntry.dispose(); 33 | } 34 | } 35 | 36 | export function showAblStatus(message: string, command: string, tooltip?: string) { 37 | statusBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, Number.MIN_VALUE); 38 | statusBarEntry.text = message; 39 | statusBarEntry.command = command; 40 | statusBarEntry.color = 'yellow'; 41 | statusBarEntry.tooltip = tooltip; 42 | statusBarEntry.show(); 43 | } 44 | -------------------------------------------------------------------------------- /abl-src/check-syntax.p: -------------------------------------------------------------------------------- 1 | DEFINE VARIABLE ch_prog AS CHARACTER NO-UNDO. 2 | DEFINE VARIABLE ch_mess AS CHARACTER NO-UNDO. 3 | DEFINE VARIABLE i AS INTEGER NO-UNDO. 4 | 5 | /* Extracts the parameters */ 6 | ASSIGN ch_prog = ENTRY( 1, SESSION:PARAMETER ). 7 | 8 | RUN VALUE( REPLACE( PROGRAM-NAME( 1 ), "check-syntax.p", "read-env-var.p") ). 9 | 10 | /* OpenEdge Startup Procedure */ 11 | DEFINE VARIABLE vsabl_oe_startup_procedure AS CHARACTER NO-UNDO. 12 | vsabl_oe_startup_procedure = OS-GETENV ( "VSABL_OE_STARTUP_PROCEDURE" ). 13 | IF LENGTH( vsabl_oe_startup_procedure ) > 0 THEN RUN VALUE( vsabl_oe_startup_procedure ). 14 | 15 | /* Compile without saving */ 16 | COMPILE VALUE( ch_prog ) SAVE=NO NO-ERROR. 17 | 18 | /* If there are compilation messages */ 19 | IF COMPILER:NUM-MESSAGES > 0 THEN DO: 20 | 21 | ASSIGN ch_mess = "". 22 | 23 | /* For each messages */ 24 | DO i = 1 TO COMPILER:NUM-MESSAGES: 25 | 26 | /* Generate an error line */ 27 | ASSIGN ch_mess = 28 | SUBSTITUTE( "&1 File:'&2' Row:&3 Col:&4 Error:&5 Message:&6", 29 | IF COMPILER:WARNING = TRUE THEN "WARNING" ELSE "ERROR", 30 | COMPILER:GET-FILE-NAME ( i ), 31 | COMPILER:GET-ROW ( i ), 32 | COMPILER:GET-COLUMN ( i ), 33 | COMPILER:GET-NUMBER ( i ), 34 | COMPILER:GET-MESSAGE ( i ) 35 | ) 36 | . 37 | 38 | /* display the message to the standard output */ 39 | PUT UNFORMATTED ch_mess SKIP. 40 | END. 41 | END. 42 | ELSE DO : 43 | /* display to the standard output */ 44 | PUT UNFORMATTED "SUCCESS: Syntax is Correct." SKIP. 45 | END. 46 | 47 | /* End of program */ 48 | QUIT. -------------------------------------------------------------------------------- /src/shared/openEdgeConfigFile.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs'; 2 | import * as jsonminify from 'jsonminify'; 3 | import * as promisify from 'util.promisify'; 4 | import { OpenEdgeFormatOptions } from '../misc/OpenEdgeFormatOptions'; 5 | 6 | const readFileAsync = promisify(readFile); 7 | 8 | export const OPENEDGE_CONFIG_FILENAME = '.openedge.json'; 9 | 10 | export interface TestConfig { 11 | files?: string[]; 12 | beforeAll?: Command; 13 | afterAll?: Command; 14 | beforeEach?: Command; 15 | afterEach?: Command; 16 | } 17 | 18 | export interface Command { 19 | cmd: string; 20 | args?: string[]; 21 | env?: string[]; 22 | cwd?: string; 23 | } 24 | export interface OpenEdgeConfig { 25 | dlc?: string; 26 | proPath?: string[]; 27 | proPathMode?: 'append' | 'overwrite' | 'prepend'; 28 | parameterFiles?: string[]; 29 | workingDirectory?: string; 30 | test?: TestConfig; 31 | startupProcedure?: string; 32 | dbDictionary?: string[]; 33 | format?: OpenEdgeFormatOptions; 34 | } 35 | 36 | export function loadConfigFile(filename: string): Thenable { 37 | if (!filename) { 38 | return Promise.resolve({}); 39 | } 40 | return readFileAsync(filename, { encoding: 'utf8' }).then((text) => { 41 | // We don't catch the parsing error, to send the error in the UI (via promise rejection) 42 | return JSON.parse(jsonminify(text)); 43 | }).catch((e) => { 44 | // if no .openedge.json file is found, return a default empty one, no error 45 | if (e.code === 'ENOENT') { 46 | return {}; 47 | } 48 | // other error (like parsing error) should be thrown 49 | throw e; 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/ablConfig.ts: -------------------------------------------------------------------------------- 1 | import { isNullOrUndefined } from 'util'; 2 | import { FileSystemWatcher, workspace, WorkspaceFolder } from 'vscode'; 3 | import { loadConfigFile, OPENEDGE_CONFIG_FILENAME, OpenEdgeConfig } from './shared/openEdgeConfigFile'; 4 | 5 | let openEdgeConfig: OpenEdgeConfig = null; 6 | let watcher: FileSystemWatcher = null; 7 | export let genericWorkspaceFolder: WorkspaceFolder = null; 8 | 9 | export function findConfigFile() { 10 | return workspace.findFiles(OPENEDGE_CONFIG_FILENAME).then((uris) => { 11 | if (uris.length > 0) { 12 | genericWorkspaceFolder = workspace.getWorkspaceFolder(uris[0]); 13 | return uris[0].fsPath; 14 | } 15 | return null; 16 | }); 17 | } 18 | function loadAndSetConfigFile(filename: string) { 19 | if (filename === null) { 20 | return Promise.resolve({}); 21 | } 22 | return loadConfigFile(filename).then((config) => { 23 | openEdgeConfig = config; 24 | return openEdgeConfig; 25 | }); 26 | } 27 | export function getOpenEdgeConfig() { 28 | return new Promise((resolve, reject) => { 29 | if (openEdgeConfig === null) { 30 | watcher = workspace.createFileSystemWatcher('**/' + OPENEDGE_CONFIG_FILENAME); 31 | watcher.onDidChange((uri) => loadAndSetConfigFile(uri.fsPath)); 32 | watcher.onDidCreate((uri) => loadAndSetConfigFile(uri.fsPath)); 33 | watcher.onDidDelete((uri) => loadAndSetConfigFile(uri.fsPath)); 34 | 35 | findConfigFile().then((filename) => loadAndSetConfigFile(filename)).then((config) => resolve(config)); 36 | } else { 37 | resolve(openEdgeConfig); 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /schemas/openedge.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": {}, 3 | "id": "openedge.json", 4 | "properties": { 5 | "parameterFiles": { 6 | "id": "/properties/parameterFiles", 7 | "description": "Path to .pf files", 8 | "items": { 9 | "type": "string" 10 | }, 11 | "type": "array" 12 | }, 13 | "dlc": { 14 | "id": "/properties/dlc", 15 | "description": "Path to Progress OpenEdge installation (if not set use ENVVAR $DLC)", 16 | "type": ["string", "array"] 17 | }, 18 | "proPath": { 19 | "id": "/properties/proPath", 20 | "description": "Path to include in the PROPATH variable", 21 | "items": { 22 | "type": "string" 23 | }, 24 | "type": "array" 25 | }, 26 | "proPathMode": { 27 | "default": "append", 28 | "description": "Specify how the PROPATH is modified", 29 | "enum": [ 30 | "append", 31 | "prepend", 32 | "overwrite" 33 | ], 34 | "id": "/properties/proPathMode", 35 | "title": "proPathMode", 36 | "type": "string" 37 | }, 38 | "workingDirectory": { 39 | "id": "/properties/workingDirectory", 40 | "description": "Current working directory (home)", 41 | "type": "string" 42 | }, 43 | "startupProcedure": { 44 | "id": "/properties/startupProcedure", 45 | "description": "Path to OpenEdge Startup Procedure", 46 | "type": "string" 47 | }, 48 | "dbDictionary": { 49 | "id": "/properties/dbDictionary", 50 | "description": "Logical names of database files for the auto-complete option (command: ABL Read Dictionary Structure)", 51 | "type": "array" 52 | } 53 | }, 54 | "type": "object" 55 | } 56 | -------------------------------------------------------------------------------- /test/abl.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import { convertDataToDebuggerMessage } from '../src/debugAdapter/messages'; 4 | 5 | suite('OpenEdge ABL Extension Tests', () => { 6 | test('Test MSG_CLASSINFO Processing', (done) => { 7 | const msgRaw = `MSG_CLASSINFO;1;ablRunner;Progress.Lang.Object;N;NULL;P;Next-Sibling;Progress.Lang.Object;1;R;1008 (Class OpenEdge.ABLUnit.Model.TestRootModel);P;Prev-Sibling;Progress.Lang.Object;1;R;1006 (Class OpenEdge.ABLUnit.Runner.TestConfig);`; 8 | const msg = convertDataToDebuggerMessage(msgRaw); 9 | assert.deepEqual(msg, [ 10 | { 11 | args: [], 12 | baseClass: null, 13 | code: 'MSG_CLASSINFO', 14 | properties: [{ 15 | children: [], 16 | kind: 7, 17 | name: 'Next-Sibling', 18 | type: 'Progress.Lang.Object', 19 | value: '1008 (Class OpenEdge.ABLUnit.Model.TestRootModel)', 20 | }, { 21 | children: [], 22 | kind: 7, 23 | name: 'Prev-Sibling', 24 | type: 'Progress.Lang.Object', 25 | value: '1006 (Class OpenEdge.ABLUnit.Runner.TestConfig)', 26 | }], 27 | }]); 28 | done(); 29 | }); 30 | test('Test MSG_ARRAY Processing', (done) => { 31 | // let msgRaw = `MSG_ARRAY;ext;.1;W;.3"A";.2;W;.2"";.3;W;.2"";.4;W;.2"";..`; 32 | const msgRaw = `MSG_ARRAY;ext2;1;W;2"";2;W;2"";`; 33 | const msg = convertDataToDebuggerMessage(msgRaw); 34 | assert.deepEqual(msg, [ 35 | { 36 | args: [], 37 | code: 'MSG_ARRAY', 38 | values: [ 39 | '2""', 40 | '2""', 41 | ], 42 | }]); 43 | done(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/debugAdapter/ablDebugConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable: object-literal-sort-keys 2 | import { execSync } from 'child_process'; 3 | import * as fs from 'fs'; 4 | import { dirname, isAbsolute, join } from 'path'; 5 | import * as vscode from 'vscode'; 6 | 7 | export class AblDebugConfigurationProvider implements vscode.DebugConfigurationProvider { 8 | 9 | /** 10 | * Returns an initial debug configuration based on contextual information, e.g. package.json or folder. 11 | * @param folder 12 | * @param token 13 | */ 14 | public provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): vscode.ProviderResult { 15 | 16 | return [ 17 | { 18 | name: 'Launch', 19 | type: 'abl', 20 | request: 'launch', 21 | program: '${file}', 22 | }, { 23 | name: 'Attach', 24 | type: 'abl', 25 | request: 'attach', 26 | port: 3099, 27 | address: '127.0.0.1', 28 | localRoot: '${workspaceFolder}', 29 | }, 30 | ]; 31 | } 32 | 33 | public resolveDebugConfiguration?(folder: vscode.WorkspaceFolder | undefined, debugConfiguration: vscode.DebugConfiguration, token?: vscode.CancellationToken): vscode.DebugConfiguration { 34 | if (!debugConfiguration || !debugConfiguration.request) { // if 'request' is missing interpret this as a missing launch.json 35 | const activeEditor = vscode.window.activeTextEditor; 36 | if (!activeEditor || activeEditor.document.languageId !== 'abl') { 37 | return; 38 | } 39 | 40 | return { 41 | name: 'Launch', 42 | type: 'abl', 43 | request: 'launch', 44 | program: '${file}', 45 | }; 46 | } 47 | return debugConfiguration; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--extensionDevelopmentPath=${workspaceFolder}", "run_env/sample-oe-projects/project1" 12 | ], 13 | "stopOnEntry": false, 14 | "sourceMaps": true, 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | }, 20 | { 21 | "name": "Launch as server", 22 | "type": "node", 23 | "request": "launch", 24 | "protocol": "inspector", 25 | "program": "${workspaceFolder}/out/src/debugAdapter/ablDebug.js", 26 | "args": [ 27 | "--server=4712" 28 | ], 29 | "sourceMaps": true, 30 | "outFiles": [ 31 | "${workspaceFolder}/out/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | }, 35 | { 36 | "name": "Extension Tests", 37 | "type": "extensionHost", 38 | "request": "launch", 39 | "runtimeExecutable": "${execPath}", 40 | // the workspace path should be GOPATH 41 | "args": [ 42 | "--extensionDevelopmentPath=${workspaceFolder}", 43 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 44 | ], 45 | "outFiles": [ 46 | "${workspaceFolder}/out/**/*.js" 47 | ], 48 | "preLaunchTask": "npm: watch" 49 | } 50 | ], 51 | "compounds": [ 52 | { 53 | "name": "Extension + Debug server", 54 | "configurations": [ 55 | "Run Extension", 56 | "Launch as server" 57 | ] 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /abl-src/test.p: -------------------------------------------------------------------------------- 1 | USING OpenEdge.ABLUnit.Runner.ABLRunner. 2 | USING OpenEdge.ABLUnit.Runner.TestConfig. 3 | USING OpenEdge.ABLUnit.Model.TestEntity. 4 | USING OpenEdge.ABLUnit.Results.TestTypeResult. 5 | 6 | USING Progress.Json.ObjectModel.JsonArray. 7 | USING Progress.Json.ObjectModel.JsonObject. 8 | USING Progress.Json.ObjectModel.ObjectModelParser. 9 | USING Progress.Lang.AppError. 10 | USING Progress.Lang.Error. 11 | 12 | ROUTINE-LEVEL ON ERROR UNDO, THROW. 13 | 14 | DEFINE var testFiles AS CHAR NO-UNDO. 15 | /* testFiles = "C:/Users/christophe_c/Documents/Dev/ablunit-samples/tests/iban.test.p". */ 16 | 17 | DEFINE VARIABLE ablRunner AS ABLRunner NO-UNDO. 18 | ablRunner = NEW ABLRunner(). 19 | 20 | DEFINE VARIABLE testIndex AS INTEGER NO-UNDO. 21 | DEFINE VARIABLE caseIndex AS INTEGER NO-UNDO. 22 | 23 | DEFINE VARIABLE testSummary AS TestTypeResult NO-UNDO. 24 | DEFINE VARIABLE testEntity AS TestEntity NO-UNDO. 25 | 26 | def var outputLog as char no-undo. 27 | 28 | DEFINE VARIABLE prevStackTraceProperty AS LOGICAL NO-UNDO. 29 | DEFINE VARIABLE oldWarningsList AS CHARACTER NO-UNDO. 30 | 31 | DEFINE VARIABLE testResource AS CHARACTER NO-UNDO. 32 | 33 | prevStackTraceProperty = SESSION:ERROR-STACK-TRACE. 34 | oldWarningsList = SESSION:SUPPRESS-WARNINGS-LIST. 35 | 36 | SESSION:SUPPRESS-WARNINGS-LIST = '6430,' + SESSION:SUPPRESS-WARNINGS-LIST. 37 | SESSION:ERROR-STACK-TRACE = TRUE. 38 | 39 | DO ON ERROR UNDO, LEAVE: 40 | DO testIndex = 1 TO NUM-ENTRIES(testFiles): 41 | testResource = ENTRY(testIndex, testFiles). 42 | testEntity = ablRunner:populateTestModel(testResource, 1). 43 | ablRunner:updateFile(outputLog, "TEST_TREE" + " " + ablRunner:loadSerializedTree(testEntity), FALSE). 44 | END. 45 | IF testEntity NE ? THEN DO: 46 | testSummary = ablRunner:runtests(testEntity, outputLog). 47 | /* WriteTestResults(ablResultsFile, testEntity, testSummary). */ 48 | END. 49 | 50 | FINALLY: 51 | /* COMPLETE event has to be updated anyway to complete the session. */ 52 | ablRunner:updateFile(outputLog, "COMPLETE", FALSE). 53 | SESSION:ERROR-STACK-TRACE = prevStackTraceProperty. 54 | SESSION:SUPPRESS-WARNINGS-LIST = oldWarningsList. 55 | END. 56 | END. 57 | -------------------------------------------------------------------------------- /src/ablDataDictionary.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as cp from 'child_process'; 3 | import * as path from 'path'; 4 | import { genericWorkspaceFolder, getOpenEdgeConfig } from './ablConfig'; 5 | import { outputChannel } from './ablStatus'; 6 | import { create } from './OutputChannelProcess'; 7 | import { createProArgs, getProBin, getProwinBin, setupEnvironmentVariables } from './shared/ablPath'; 8 | 9 | function genericPath(): string { 10 | if (vscode.window.activeTextEditor) { 11 | const folder = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri); 12 | if (folder) { 13 | return folder.uri.fsPath; 14 | } 15 | } 16 | if (genericWorkspaceFolder) { 17 | return genericWorkspaceFolder.uri.fsPath; 18 | } 19 | return vscode.workspace.rootPath; 20 | } 21 | 22 | export function openDataDictionary() { 23 | const cwd = genericPath(); 24 | const env = process.env; 25 | 26 | return getOpenEdgeConfig().then((oeConfig) => { 27 | const cmd = getProwinBin(oeConfig.dlc); 28 | 29 | // TODO : reuse the openedgeconfig file and pf files defined 30 | const args = createProArgs({ 31 | parameterFiles: oeConfig.parameterFiles, 32 | startupProcedure: '_dict.p', 33 | }); 34 | cp.spawn(cmd, args, { env, cwd, detached: true }); 35 | }); 36 | } 37 | 38 | export function readDataDictionary(ablConfig: vscode.WorkspaceConfiguration) { 39 | return getOpenEdgeConfig().then((oeConfig) => { 40 | const cmd = getProBin(oeConfig.dlc); 41 | const env = setupEnvironmentVariables(process.env, oeConfig, genericPath()); 42 | const dbs = (oeConfig.dbDictionary ? oeConfig.dbDictionary.join(',') : ''); 43 | const args = createProArgs({ 44 | batchMode: true, 45 | param: dbs, 46 | parameterFiles: oeConfig.parameterFiles, 47 | startupProcedure: path.join(__dirname, '../../abl-src/dict-dump.p'), 48 | workspaceRoot: genericPath(), 49 | }); 50 | let cwd = genericPath(); 51 | cwd = oeConfig.workingDirectory ? oeConfig.workingDirectory.replace('${workspaceRoot}', genericPath()).replace('${workspaceFolder}', genericPath()) : cwd; 52 | vscode.window.showInformationMessage('Updating data dictionary...'); 53 | create(cmd, args, { env: env, cwd: cwd }, outputChannel).then((res) => { 54 | vscode.window.showInformationMessage('Data dictionary ' + (res.success ? 'updated' : 'failed')); 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /abl-src/dict-dump-exec.p: -------------------------------------------------------------------------------- 1 | using Progress.Json.ObjectModel.*. 2 | 3 | def input param nm-dir-par as char no-undo. 4 | 5 | def var aJsonTable as JsonArray no-undo. 6 | def var aJsonField as JsonArray no-undo. 7 | def var aJsonIndex as JsonArray no-undo. 8 | 9 | def var oTable as JsonObject no-undo. 10 | def var oField as JsonObject no-undo. 11 | def var oIndex as JsonObject no-undo. 12 | 13 | def var isPK as logical no-undo. 14 | 15 | aJsonTable = new JsonArray(). 16 | 17 | def buffer dbFile for dictdb._file. 18 | def buffer dbField for dictdb._field. 19 | def buffer dbIndex for dictdb._index. 20 | def buffer dbIndexField for dictdb._index-field. 21 | 22 | for each dbFile: 23 | 24 | oTable = new JsonObject(). 25 | oTable:add("label", dbFile._file-name). 26 | oTable:add("kind", 5). /*Variable*/ 27 | oTable:add("detail", dbFile._Desc). 28 | aJsonTable:add(oTable). 29 | 30 | aJsonField = new JsonArray(). 31 | oTable:add("fields", aJsonField). 32 | 33 | for each dbField 34 | where dbField._file-recid = recid(dbFile): 35 | oField = new JsonObject(). 36 | oField:add("label", dbField._field-name). 37 | oField:add("kind", 4). /*Field*/ 38 | oField:add("detail", dbField._Desc). 39 | oField:add("dataType", dbField._data-type). 40 | oField:add("mandatory", dbField._mandatory). 41 | oField:add("format", dbField._format). 42 | aJsonField:add(oField). 43 | end. 44 | 45 | aJsonIndex = new JsonArray(). 46 | oTable:add("indexes", aJsonIndex). 47 | 48 | for each dbIndex 49 | where dbIndex._file-recid = recid(dbFile): 50 | 51 | assign isPK = (recid(dbIndex) = dbFile._prime-index). 52 | 53 | oIndex = new JsonObject(). 54 | oIndex:add("label", dbIndex._index-name). 55 | oIndex:add("kind", 14). /*Snippet*/ 56 | oIndex:add("detail", dbIndex._Desc). 57 | oIndex:add("unique", dbIndex._unique). 58 | oIndex:add("primary", isPK). 59 | 60 | aJsonField = new JsonArray(). 61 | for each dbIndexField 62 | where dbIndexField._index-recid = recid(dbIndex), 63 | first dbField 64 | where recid(dbField) = dbIndexField._field-recid: 65 | 66 | oField = new JsonObject(). 67 | oField:add("label", dbField._field-name). 68 | //oField:add("kind", 17). /*Reference*/ 69 | //oField:add("detail", dbField._Desc). 70 | aJsonField:add(oField). 71 | end. 72 | oIndex:add("fields", aJsonField). 73 | aJsonIndex:add(oIndex). 74 | end. 75 | 76 | end. 77 | 78 | aJsonTable:writefile(nm-dir-par + ".openedge.db." + ldbname("dictdb")). 79 | -------------------------------------------------------------------------------- /snippets/abl.json: -------------------------------------------------------------------------------- 1 | { 2 | "Http Request": { 3 | "prefix": "httprequest", 4 | "description": "ABL HttpRequest", 5 | "body": [ 6 | "using OpenEdge.Net.HTTP.RequestBuilder.", 7 | "using OpenEdge.Net.HTTP.IHttpRequest.", 8 | "using OpenEdge.Net.HTTP.IHttpResponse.", 9 | "using OpenEdge.Net.HTTP.ClientBuilder.", 10 | "", 11 | "def var baseUrl as char no-undo.", 12 | "def var apiRoute as char no-undo.", 13 | "def var oRequest as IHttpRequest no-undo.", 14 | "def var oResponse as IHttpResponse no-undo.", 15 | "", 16 | "baseUrl = \"${1:base-url}\".", 17 | "apiRoute = \"/${2:api-route}\".", 18 | "oRequest = RequestBuilder:Get(baseUrl + apiRoute):Request.", 19 | "", 20 | "oResponse = ClientBuilder:Build():Client:Execute(oRequest).", 21 | "$0" 22 | ] 23 | }, 24 | "JsonObject from IHttpResponse:Entity": { 25 | "prefix": "entity-jsonobject", 26 | "description": "Create a JsonObject from IHttpResponse:Entity", 27 | "body":[ 28 | "def var ${1:oJson} as JsonObject no-undo.", 29 | "$1 = cast(${2:oResponse}:Entity, JsonObject).", 30 | "$0" 31 | ] 32 | }, 33 | "JsonArray from IHttpResponse:Entity": { 34 | "prefix": "entity-jsonarray", 35 | "description": "Create a JsonArray from IHttpResponse:Entity", 36 | "body":[ 37 | "def var ${1:oJson} as JsonArray no-undo.", 38 | "$1 = cast(${2:oResponse}:Entity, JsonArray).", 39 | "$0" 40 | ] 41 | }, 42 | "DEF VAR NO-UNDO": { 43 | "prefix": "var", 44 | "description": "Define a variable no-undo", 45 | "body":[ 46 | "def var ${1:varName} as ${2:char} no-undo.", 47 | "$0" 48 | ] 49 | }, 50 | "DEF TEMP-TABLE NO-UNDO": { 51 | "prefix": "tt", 52 | "description": "Define a temp-table no-undo", 53 | "body": [ 54 | "def temp-table ${1:ttName} no-undo", 55 | "\tfield ${2:fieldName} as ${3:char}.", 56 | "$0" 57 | ] 58 | }, 59 | "PROCEDURE": { 60 | "prefix": "pro", 61 | "description": "Define a procedure", 62 | "body": [ 63 | "procedure ${1:procName}:", 64 | "\t$0", 65 | "end procedure." 66 | ] 67 | }, 68 | "FUNCTION": { 69 | "prefix": "func", 70 | "description": "Define a function", 71 | "body": [ 72 | "function ${1:functionName} returns ${2:char} (input ${3:parameterName} as ${4:char}):", 73 | "\t$0", 74 | "\treturn.", 75 | "end function." 76 | ] 77 | } 78 | } -------------------------------------------------------------------------------- /src/providers/ablDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import { isNullOrUndefined } from 'util'; 2 | import * as vscode from 'vscode'; 3 | import { ABL_MODE } from '../ablMode'; 4 | import { ABLParameter, SYMBOL_TYPE } from '../misc/definition'; 5 | import * as utils from '../misc/utils'; 6 | import { ABLDocumentController, getDocumentController } from '../parser/documentController'; 7 | 8 | export class ABLDefinitionProvider implements vscode.DefinitionProvider { 9 | private _ablDocumentController: ABLDocumentController; 10 | 11 | constructor(context: vscode.ExtensionContext) { 12 | this._ablDocumentController = getDocumentController(); 13 | context.subscriptions.push(vscode.languages.registerDefinitionProvider(ABL_MODE.language, this)); 14 | } 15 | 16 | public provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Thenable { 17 | // go-to definition 18 | const selection = utils.getText(document, position); 19 | const doc = this._ablDocumentController.getDocument(document); 20 | if (!doc) { 21 | return; 22 | } 23 | if (!doc.processed) { 24 | return; 25 | } 26 | const split = selection.statement.split(/[.:\s\t]/); 27 | if (split.length === 0) { 28 | return; 29 | } 30 | const words = utils.cleanArray(split); 31 | if (words.length > 0) { 32 | const symbol = doc.searchSymbol([selection.word], selection.word, position, true); 33 | if (!isNullOrUndefined(symbol) && !isNullOrUndefined(symbol.location)) { 34 | // for local temp-table parameter, go-to temp-table definition when already in then parameter line 35 | if ((symbol.location.range.start.line === position.line) && (symbol.type === SYMBOL_TYPE.LOCAL_PARAM) && (symbol.value instanceof ABLParameter)) { 36 | if (symbol.value.dataType === 'temp-table') { 37 | const ttSym = doc.searchSymbol([symbol.value.name], symbol.value.name, null, true); 38 | if (!isNullOrUndefined(ttSym) && !isNullOrUndefined(ttSym.location)) { 39 | return Promise.resolve(ttSym.location); 40 | } 41 | } 42 | } 43 | return Promise.resolve(symbol.location); 44 | } 45 | // find includes 46 | const inc = doc.includes.find((item) => item.name.toLowerCase() === selection.statement); 47 | if (!isNullOrUndefined(inc)) { 48 | const extDoc = doc.externalDocument.find((item) => item.uri.fsPath.toLowerCase() === inc.fsPath.toLowerCase()); 49 | if (!isNullOrUndefined(extDoc)) { 50 | const location = new vscode.Location(extDoc.uri, new vscode.Position(0, 0)); 51 | return Promise.resolve(location); 52 | } 53 | } 54 | } 55 | return; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/misc/definition.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export interface ICheckResult { 4 | file: string; 5 | line: number; 6 | column: number; 7 | msg: string; 8 | severity: string; 9 | } 10 | 11 | export class TextSelection { 12 | public word: string; 13 | public wordRange: vscode.Range; 14 | public statement: string; 15 | public statementRange: vscode.Range; 16 | } 17 | 18 | export enum SYMBOL_TYPE { 19 | METHOD = 'Method', 20 | INCLUDE = 'Include File', 21 | LOCAL_VAR = 'Local Variable', 22 | GLOBAL_VAR = 'Global Variable', 23 | LOCAL_PARAM = 'Local Parameter', 24 | GLOBAL_PARAM = 'Global Parameter', 25 | TEMPTABLE = 'Temp-table', 26 | TEMPTABLE_FIELD = 'Temp-table field', 27 | } 28 | 29 | export enum ABL_ASLIKE { 30 | AS = 'as', 31 | LIKE = 'like', 32 | } 33 | 34 | export enum ABL_PARAM_DIRECTION { 35 | IN = 'input', 36 | OUT = 'output', 37 | INOUT = 'input-output', 38 | } 39 | 40 | export interface ABLFieldDefinition { 41 | label: string; 42 | kind: vscode.CompletionItemKind; 43 | detail: string; 44 | dataType: string; 45 | mandatory: boolean; 46 | format: string; 47 | } 48 | export interface ABLIndexDefinition { 49 | label: string; 50 | kind: vscode.CompletionItemKind; 51 | detail: string; 52 | fields: ABLFieldDefinition[]; 53 | unique: boolean; 54 | primary: boolean; 55 | } 56 | export class ABLTableDefinition { 57 | public filename: string; 58 | public label: string; 59 | public kind: vscode.CompletionItemKind; 60 | public detail: string; 61 | public pkList: string; 62 | public fields: ABLVariable[]; 63 | public indexes: ABLIndexDefinition[]; 64 | public completionFields: vscode.CompletionList; 65 | public completionIndexes: vscode.CompletionList; 66 | public completionAdditional: vscode.CompletionList; 67 | public completion: vscode.CompletionList; 68 | 69 | get allFields(): ABLVariable[] { 70 | return this.fields; 71 | } 72 | } 73 | 74 | export class ABLVariable { 75 | public name: string; 76 | public asLike: ABL_ASLIKE; 77 | public dataType: string; 78 | public line: number; 79 | public additional?: string; 80 | } 81 | 82 | export class ABLMethod { 83 | public name: string; 84 | public lineAt: number; 85 | public lineEnd: number; 86 | public params: ABLParameter[]; 87 | public localVars: ABLVariable[]; 88 | constructor() { 89 | this.params = []; 90 | this.localVars = []; 91 | } 92 | } 93 | 94 | export class ABLParameter extends ABLVariable { 95 | public direction: ABL_PARAM_DIRECTION; 96 | } 97 | 98 | export class ABLInclude { 99 | public name: string; 100 | public fsPath: string; 101 | } 102 | 103 | export class ABLTempTable extends ABLTableDefinition { 104 | public line: number; 105 | public referenceTable: string; 106 | public referenceFields: ABLVariable[]; 107 | 108 | get allFields(): ABLVariable[] { 109 | if (this.referenceFields) { 110 | return [...this.referenceFields, ...this.fields]; 111 | } 112 | return this.fields; 113 | } 114 | } 115 | 116 | export interface ABLSymbol { 117 | type: SYMBOL_TYPE; 118 | value: ABLTempTable | ABLVariable | ABLMethod | ABLParameter | ABLInclude; 119 | origin?: ABLTempTable | ABLMethod; 120 | location?: vscode.Location; 121 | } 122 | -------------------------------------------------------------------------------- /src/parser/sourceParser.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | enum CommentType { SingleLine, MultiLine } 4 | 5 | export interface SourceCode { 6 | document: vscode.TextDocument; 7 | fullSource?: string; 8 | sourceWithoutComments?: string; 9 | sourceWithoutStrings?: string; 10 | } 11 | 12 | export class SourceParser { 13 | 14 | public getSourceCode(document: vscode.TextDocument): SourceCode { 15 | const source = document.getText(); 16 | 17 | const code: SourceCode = { document: document, fullSource: source, sourceWithoutComments: '', sourceWithoutStrings: '' }; 18 | 19 | let prevChar = ''; 20 | let nextChar = ''; 21 | let thisChar = ''; 22 | 23 | let inString = false; 24 | let inComment = false; 25 | let commentType: CommentType = null; 26 | let stringChar: string = null; 27 | let charWOComments; 28 | let charWOStrings; 29 | 30 | for (let i = 0; i < source.length; i++) { 31 | 32 | thisChar = source[i]; 33 | nextChar = source[i + 1]; 34 | prevChar = source[i - 1]; 35 | charWOComments = thisChar; 36 | charWOStrings = thisChar; 37 | 38 | switch (thisChar) { 39 | case '/': 40 | if (!inString) { 41 | // If we are not in a comment 42 | if (!inComment && nextChar === '/' || prevChar === '/') { 43 | inComment = true; 44 | commentType = CommentType.SingleLine; 45 | charWOComments = ' '; 46 | charWOStrings = ' '; 47 | } else if (!inComment && nextChar === '*') { 48 | inComment = true; 49 | commentType = CommentType.MultiLine; 50 | charWOComments = ' '; 51 | charWOStrings = ' '; 52 | } else if (inComment && commentType === CommentType.MultiLine && prevChar === '*') { 53 | inComment = false; 54 | commentType = null; 55 | charWOComments = ' '; 56 | charWOStrings = ' '; 57 | } else if (inComment) { 58 | charWOComments = ' '; 59 | charWOStrings = ' '; 60 | } 61 | } else { 62 | charWOStrings = ' '; 63 | } 64 | break; 65 | case '\n': 66 | if (inComment && commentType === CommentType.SingleLine) { 67 | inComment = false; 68 | commentType = null; 69 | } 70 | break; 71 | case '"': 72 | case '\'': 73 | if (!inComment) { 74 | charWOStrings = ' '; 75 | if (stringChar === thisChar && inString && prevChar !== '~') { 76 | inString = false; 77 | stringChar = null; 78 | } else if (stringChar === null && !inString && !inComment) { 79 | inString = true; 80 | stringChar = thisChar; 81 | } 82 | } else { 83 | charWOComments = ' '; 84 | charWOStrings = ' '; 85 | } 86 | break; 87 | default: 88 | if (inComment) { 89 | charWOComments = ' '; 90 | charWOStrings = ' '; 91 | } else if (inString) { 92 | charWOStrings = ' '; 93 | } 94 | break; 95 | } 96 | code.sourceWithoutComments += charWOComments; 97 | code.sourceWithoutStrings += charWOStrings; 98 | } 99 | 100 | return code; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/ablCheckSyntax.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { getOpenEdgeConfig } from './ablConfig'; 5 | import { outputChannel } from './ablStatus'; 6 | import { createProArgs, getProBin, setupEnvironmentVariables } from './shared/ablPath'; 7 | 8 | const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); 9 | // statusBarItem.command = 'abl.checkSyntax.showOutput'; 10 | 11 | export function removeSyntaxStatus(): void { 12 | statusBarItem.hide(); 13 | statusBarItem.text = ''; 14 | } 15 | 16 | export interface CheckResult { 17 | file: string; 18 | line: number; 19 | column: number; 20 | msg: string; 21 | severity: string; 22 | } 23 | 24 | export function checkSyntax(filename: string, ablConfig: vscode.WorkspaceConfiguration): Promise { 25 | outputChannel.clear(); 26 | statusBarItem.show(); 27 | // statusBarItem.text = '$(kebab-horizontal) Checking syntax'; 28 | statusBarItem.text = '$(sync) Checking syntax'; 29 | 30 | let cwd = path.dirname(filename); 31 | 32 | return getOpenEdgeConfig().then((oeConfig) => { 33 | const cmd = getProBin(oeConfig.dlc); 34 | const env = setupEnvironmentVariables(process.env, oeConfig, vscode.workspace.rootPath); 35 | const args = createProArgs({ 36 | batchMode: true, 37 | param: filename, 38 | parameterFiles: oeConfig.parameterFiles, 39 | startupProcedure: path.join(__dirname, '../../abl-src/check-syntax.p'), 40 | workspaceRoot: vscode.workspace.rootPath, 41 | }); 42 | if (oeConfig.workingDirectory) { 43 | cwd = oeConfig.workingDirectory.replace('${workspaceRoot}', vscode.workspace.rootPath) 44 | .replace('${workspaceFolder}', vscode.workspace.rootPath); 45 | } 46 | return new Promise((resolve, reject) => { 47 | cp.execFile(cmd, args, { env, cwd }, (err, stdout, stderr) => { 48 | try { 49 | if (err && (err as any).code === 'ENOENT') { 50 | // Since the tool is run on save which can be frequent 51 | // we avoid sending explicit notification if tool is missing 52 | // console.log(`Cannot find ${cmd}`); 53 | return resolve([]); 54 | } 55 | const useStdErr = false; // todo voir si utile 56 | if (err && stderr && !useStdErr) { 57 | outputChannel.appendLine(['Error while running tool:', cmd, ...args].join(' ')); 58 | outputChannel.appendLine(stderr); 59 | return resolve([]); 60 | } 61 | const lines = stdout.toString().split('\r\n').filter((line) => line.length > 0); 62 | if (lines.length === 1 && lines[0].startsWith('SUCCESS')) { 63 | resolve([]); 64 | return; 65 | } 66 | const results: CheckResult[] = []; 67 | 68 | // Format = &1 File:'&2' Row:&3 Col:&4 Error:&5 Message:&6 69 | const re = /(ERROR|WARNING) File:'(.*)' Row:(\d+) Col:(\d+) Error:(.*) Message:(.*)/; 70 | lines.forEach((line) => { 71 | const matches = line.match(re); 72 | 73 | if (matches) { 74 | const checkResult = { 75 | column: parseInt(matches[4], 10), 76 | file: matches[2], 77 | line: parseInt(matches[3], 10), 78 | msg: `${matches[5]}: ${matches[6]}`, 79 | severity: matches[1].toLowerCase(), 80 | }; 81 | // console.log(`${JSON.stringify(checkResult)}`); 82 | results.push(checkResult); 83 | } else { 84 | reject(stdout); 85 | } 86 | }); 87 | resolve(results); 88 | } catch (e) { 89 | reject(e); 90 | } 91 | }); 92 | }).then((results) => { 93 | if (results.length === 0) { 94 | statusBarItem.text = '$(check) Syntax OK'; 95 | } else { 96 | statusBarItem.text = '$(alert) Syntax error'; 97 | } 98 | return results; 99 | }); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenEdge ABL language support for Visual Studio Code 2 | This extension provides rich OpenEdge ABL language support for Visual Studio Code. Now you can write and run ABL procedures using the excellent IDE-like interface that Visual Studio Code provides. 3 | 4 | ## Features 5 | 6 | * Syntax highlighting 7 | * Syntax checking 8 | * Run 9 | * Debugger 10 | * Auto-complete (tables, fields, methods) 11 | 12 | ![features demo](./docs/images/demo.gif "Demo") 13 | 14 | ![debugger demo](./docs/images/debug.gif "Debugger") 15 | 16 | ## Using 17 | ### Prerequisites 18 | You need to have a local OpenEdge installation, and the `DLC` environment variable should point to the installation directory (usually `C:\Progress\OpenEdge`). 19 | ### Config file 20 | You can create a local config file for your project named `.openedge.json`, with the following structure: 21 | ```JSON 22 | { 23 | "workingDirectory": "${workspaceFolder}\\Home", 24 | "proPath": [ 25 | "c:\\temp", 26 | "${workspaceFolder}" 27 | ], 28 | "dlc": "C:/Progress/OpenEdge", //optional override 29 | "proPathMode": "append", // overwrite, prepend 30 | "parameterFiles": [ // -pf 31 | "default.pf" 32 | ], 33 | "startupProcedure" : "${workspaceFolder}/vsc-oe-startup.p", 34 | "dbDictionary": [ 35 | "myDatabaseForAutoComplete" 36 | ], 37 | "format": { 38 | "trim": "right" // none 39 | } 40 | } 41 | ``` 42 | 43 | `dlc`, `startupProcedure`, `proPath` and `workingDirectory` are optional. Default values: 44 | - `dlc`: uses environment variable $DLC 45 | - `startupProcedure`: '' 46 | - `proPath`: workspaceRoot (of VSCode) 47 | - `workingDirectory`: folder of active source code 48 | - `dbDictionary` are the logical names of database files for the auto-complete option (command: ABL Read Dictionary Structure) 49 | - `format` are formatter options 50 | 51 | #### Parameter "startupProcedure" 52 | The optional Startup Procedure for OpenEdge can be used to execute 4GL code before a check syntax/debug/run operation. Can be used to create Database aliases or instantiate Singleton Classes. The Procedure is executed everytime the IDE starts a check syntax/debug/run operation. 53 | 54 | ### Debugger 55 | You can use the debugger to connect to a remote running process (assuming it is debug-ready), or run locally with debugger. 56 | 57 | You first need to create the launch configuration in your `launch.json` file, 2 templates are available, one for launch and the other for attach). 58 | 59 | ```JSON 60 | { 61 | "version": "0.2.0", 62 | "configurations": [ 63 | { 64 | "name": "Attach to process", 65 | "type": "abl", 66 | "request": "attach", 67 | "address": "192.168.1.100", 68 | "port": 3099 69 | } 70 | ] 71 | } 72 | ``` 73 | 74 | To attach to a remote process, it needs to be [debug-ready](https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/asaps/attaching-the-debugger-to-an-appserver-session.html). 75 | The easiest way to achieve that is to add `-debugReady 3099` to the startup parameters (`.pf` file) of your application server. 76 | 77 | The debugger supports basic features 78 | - step-over, step-into, step-out, continue, suspend 79 | - breakpoints 80 | - display stack 81 | - display variables 82 | - watch/evaluate basic expressions 83 | 84 | You can map remote path to local path (1 to 1) using `localRoot` and `remoteRoot`. This is useful when debugging a remote target, even more if it only executes r-code. 85 | `localRoot` is usually your `${workspaceRoot}` (current directory opened in VSCode). `remoteRoot` may remains empty (or missing), in this particular case, the remote path is relative, and resolved via the `PROPATH` by the remote. 86 | 87 | 88 | You can also map different remote path to local path via source mapping `sourceMap`. This is useful if you don't have all the source code in a unique project (ex dependencies). 89 | 90 | ### Unit tests 91 | Based upon the ABLUnit framework (need to be installed locally), you can specify launch parameters to find and execute test files 92 | ``` 93 | { 94 | "test": { 95 | "files":[ 96 | "tests/*.test.p" 97 | ], 98 | "beforeEach": { 99 | "cmd": "%ProgramFiles%\\Git\\bin\\sh.exe", 100 | "args": [ 101 | "-c", 102 | "echo starting" 103 | ] 104 | }, 105 | "afterEach": { 106 | "cmd": "%ProgramFiles%\\Git\\bin\\sh.exe", 107 | "args": [ 108 | "-c", 109 | "echo done" 110 | ] 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | ## Greetings 117 | Largely inspired by ZaphyrVonGenevese work (https://github.com/ZaphyrVonGenevese/vscode-abl). 118 | Also inspired by vscode-go and vscode-rust extensions. 119 | 120 | Thanks to all the contributors: mscheblein 121 | 122 | ## License 123 | Licensed under the [MIT](LICENSE) License. 124 | -------------------------------------------------------------------------------- /src/OutputChannelProcess.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn, SpawnOptions } from 'child_process'; 2 | import { Readable } from 'stream'; 3 | import { OutputChannel, window } from 'vscode'; 4 | 5 | export interface Success { 6 | success: true; 7 | code: number; 8 | stdout: string; 9 | stderr: string; 10 | } 11 | 12 | export interface Error { 13 | success: false; 14 | } 15 | 16 | export interface Options { 17 | /** 18 | * The flag indicating whether data from stdout should be captured. By default, the data is 19 | * not captured. If the data is captured, then it will be given when the process ends 20 | */ 21 | captureStdout?: boolean; 22 | 23 | /** 24 | * The flag indicating whether data from stderr should be captured. By default, the data is 25 | * not captured. If the data is captured, then it will be given when the process ends 26 | */ 27 | captureStderr?: boolean; 28 | 29 | displayExit?: boolean; 30 | displayClose?: boolean; 31 | } 32 | 33 | export async function create(spawnCommand: string, spawnArgs: string[] | undefined, 34 | spawnOptions: SpawnOptions | undefined, 35 | outputChannel: OutputChannel): Promise { 36 | if (spawnOptions === undefined) { 37 | spawnOptions = {}; 38 | } 39 | spawnOptions.stdio = 'pipe'; 40 | const spawnedProcess = spawn(spawnCommand, spawnArgs, spawnOptions); 41 | outputChannel.show(); 42 | const result = await process(spawnedProcess, outputChannel, { displayClose: false, displayExit: false }); 43 | if (result.success && result.code === 0) { 44 | // outputChannel.hide(); 45 | // outputChannel.dispose(); 46 | } 47 | return result; 48 | } 49 | 50 | /** 51 | * Writes data from the process to the output channel. The function also can accept options 52 | * @param childProcess The process to write data from. The process should be creates with 53 | * options.stdio = "pipe" 54 | * @param outputChannel The output channel to write data to 55 | * @return The result of processing the process 56 | */ 57 | export function process(childProcess: ChildProcess, outputChannel: OutputChannel, options?: Options, 58 | ): Promise { 59 | const stdout = ''; 60 | const captureStdout = getOption(options, (o) => o.captureStdout, false); 61 | subscribeToDataEvent(childProcess.stdout, outputChannel, captureStdout, stdout); 62 | const stderr = ''; 63 | const captureStderr = getOption(options, (o) => o.captureStderr, false); 64 | subscribeToDataEvent(childProcess.stderr, outputChannel, captureStderr, stderr); 65 | return new Promise((resolve) => { 66 | const processProcessEnding = (code: number) => { 67 | resolve({ 68 | code, 69 | stderr, 70 | stdout, 71 | success: true, 72 | }); 73 | }; 74 | // If some error happens, then the "error" and "close" events happen. 75 | // If the process ends, then the "exit" and "close" events happen. 76 | // It is known that the order of events is not determined. 77 | let processExited = false; 78 | let processClosed = false; 79 | childProcess.on('error', (error: any) => { 80 | outputChannel.appendLine(`error: error=${error}`); 81 | resolve({ success: false }); 82 | }); 83 | childProcess.on('close', (code, signal) => { 84 | if (getOption(options, (o) => o.displayClose, false)) { 85 | outputChannel.appendLine(`\nclose: code=${code}, signal=${signal}`); 86 | } 87 | processClosed = true; 88 | if (processExited) { 89 | processProcessEnding(code); 90 | } 91 | }); 92 | childProcess.on('exit', (code, signal) => { 93 | if (getOption(options, (o) => o.displayExit, true)) { 94 | outputChannel.appendLine(`\nexit: code=${code}, signal=${signal}`); 95 | } 96 | processExited = true; 97 | if (processClosed) { 98 | processProcessEnding(code); 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | function getOption(options: Options | undefined, evaluateOption: (options: Options) => boolean | undefined, 105 | defaultValue: boolean): boolean { 106 | if (options === undefined) { 107 | return defaultValue; 108 | } 109 | const option = evaluateOption(options); 110 | if (option === undefined) { 111 | return defaultValue; 112 | } 113 | return option; 114 | } 115 | 116 | function subscribeToDataEvent(readable: Readable, outputChannel: OutputChannel, saveData: boolean, 117 | dataStorage: string): void { 118 | readable.on('data', (chunk) => { 119 | const chunkAsString = typeof chunk === 'string' ? chunk : chunk.toString(); 120 | outputChannel.append(chunkAsString); 121 | if (saveData) { 122 | dataStorage += chunkAsString; 123 | } 124 | }); 125 | } 126 | -------------------------------------------------------------------------------- /src/shared/ablPath.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { OpenEdgeConfig } from './openEdgeConfigFile'; 4 | 5 | export function getBinPath(toolName: string, dlcPath?: string | string[]) { 6 | let dlc; 7 | // Use first available folder in array of possible locations 8 | // This enables support for multiple versions 9 | if (dlcPath instanceof Array) { 10 | dlcPath.some((p) => { 11 | if (fs.existsSync(p)) { 12 | dlc = p; 13 | return true; 14 | } 15 | }); 16 | if (!dlc) { 17 | dlc = process.env.DLC; 18 | } 19 | } else { 20 | dlc = dlcPath || process.env.DLC; 21 | } 22 | if (dlc) { 23 | return path.join(dlc, 'bin', toolName); 24 | } 25 | // dlc not set, assume the binary is in the PATH 26 | return toolName; 27 | } 28 | 29 | export function getProBin(dlcPath?: string) { 30 | return getBinPath('_progres', dlcPath); 31 | } 32 | 33 | export function getProwinBin(dlcPath?: string) { 34 | let prowin = getBinPath('prowin.exe', dlcPath); 35 | if (!fs.existsSync(prowin)) { 36 | prowin = getBinPath('prowin32.exe', dlcPath); 37 | } 38 | return prowin; 39 | } 40 | export interface ProArgsOptions { 41 | startupProcedure: string; 42 | param?: string; 43 | parameterFiles?: string[]; 44 | databaseNames?: string[]; 45 | batchMode?: boolean; 46 | debugPort?: number; 47 | temporaryDirectory?: string; 48 | workspaceRoot?: string; 49 | } 50 | export function createProArgs(options: ProArgsOptions): string[] { 51 | let pfArgs = []; 52 | if (options.parameterFiles) { 53 | // pfArgs = openEdgeConfig.parameterFiles.filter(pf => pf.trim().length > 0).map(pf => { return '-pf ' + pf; }); 54 | pfArgs = options.parameterFiles 55 | .filter((pf) => pf.trim().length > 0) 56 | .reduce((r, a) => r.concat('-pf', a), []); 57 | for (let i = 0; i < pfArgs.length; i++) { 58 | pfArgs[i] = pfArgs[i].replace( 59 | '${workspaceRoot}', 60 | options.workspaceRoot 61 | ); 62 | } 63 | } 64 | let args = []; 65 | let tempDir = options.temporaryDirectory; 66 | if (!tempDir) { 67 | tempDir = process.env.TEMP; 68 | } 69 | if (tempDir) { 70 | args.push('-T'); 71 | args.push(tempDir); 72 | } 73 | args = args.concat(pfArgs); 74 | if (options.batchMode) { 75 | args.push('-b'); 76 | } 77 | if (options.startupProcedure) { 78 | args.push('-p', options.startupProcedure); 79 | } 80 | if (options.param) { 81 | args.push('-param', options.param); 82 | } 83 | if (options.debugPort) { 84 | args.push('-debugReady', options.debugPort.toString()); 85 | } 86 | 87 | return args; 88 | } 89 | 90 | export function setupEnvironmentVariables( 91 | env: any, 92 | openEdgeConfig: OpenEdgeConfig, 93 | workspaceRoot: string 94 | ): any { 95 | if (openEdgeConfig) { 96 | if ( 97 | !openEdgeConfig.proPath || 98 | !(openEdgeConfig.proPath instanceof Array) || 99 | openEdgeConfig.proPath.length === 0 100 | ) { 101 | openEdgeConfig.proPath = ['${workspaceRoot}']; 102 | } 103 | openEdgeConfig.proPath.push(path.join(__dirname, '../../../abl-src')); 104 | const paths = openEdgeConfig.proPath.map((p) => { 105 | p = p.replace('${workspaceRoot}', workspaceRoot); 106 | p = p.replace('${workspaceFolder}', workspaceRoot); 107 | p = path.posix.normalize(p); 108 | return p; 109 | }); 110 | // let paths = openEdgeConfig.proPath || []; 111 | env.VSABL_PROPATH = paths.join(','); 112 | 113 | if (openEdgeConfig.proPathMode) { 114 | env.VSABL_PROPATH_MODE = openEdgeConfig.proPathMode; 115 | } else { 116 | env.VSABL_PROPATH_MODE = 'append'; 117 | } 118 | 119 | if (openEdgeConfig.startupProcedure) { 120 | env.VSABL_OE_STARTUP_PROCEDURE = openEdgeConfig.startupProcedure 121 | .replace('${workspaceRoot}', workspaceRoot) 122 | .replace('${workspaceFolder}', workspaceRoot); 123 | } else { 124 | // unset var; required in case user changes config 125 | env.VSABL_OE_STARTUP_PROCEDURE = ''; 126 | } 127 | } 128 | env.VSABL_SRC = path.join(__dirname, '../../abl-src'); 129 | env.VSABL_WORKSPACE = workspaceRoot; 130 | // enable the debugger 131 | // cf https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/pdsoe/enabling-debugging.html 132 | env.ENABLE_OPENEDGE_DEBUGGER = 1; 133 | 134 | return env; 135 | } 136 | 137 | export function expandPathVariables( 138 | pathToExpand: string, 139 | env: any, 140 | variables: { [key: string]: string } 141 | ): string { 142 | // format VSCode ${env:VAR} 143 | // path = path.replace(/\${env:([^}]+)}/g, (_, n) => { 144 | // return env[n]; 145 | // }); 146 | 147 | // format DOS %VAR% 148 | let expandedPath = pathToExpand; 149 | expandedPath = expandedPath.replace(/%([^%]+)%/g, (_, n) => { 150 | return env[n]; 151 | }); 152 | 153 | // VSCode specific var ${workspaceFolder} 154 | expandedPath = expandedPath.replace(/\${([^}]+)}/g, (_, n) => { 155 | return variables[n]; 156 | }); 157 | return expandedPath; 158 | } 159 | -------------------------------------------------------------------------------- /src/providers/ablHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { isNullOrUndefined } from 'util'; 2 | import * as vscode from 'vscode'; 3 | import { CancellationToken, Hover, HoverProvider, Position, ProviderResult, TextDocument } from 'vscode'; 4 | import { ABL_MODE } from '../ablMode'; 5 | import { ABLFieldDefinition, ABLMethod, ABLParameter, ABLTableDefinition, ABLTempTable, ABLVariable, SYMBOL_TYPE } from '../misc/definition'; 6 | import * as utils from '../misc/utils'; 7 | import { ABLDocumentController, getDocumentController } from '../parser/documentController'; 8 | import { getTableCollection } from './ablCompletionProvider'; 9 | 10 | export class ABLHoverProvider implements HoverProvider { 11 | private _ablDocumentController: ABLDocumentController; 12 | 13 | constructor(context: vscode.ExtensionContext) { 14 | this._ablDocumentController = getDocumentController(); 15 | context.subscriptions.push(vscode.languages.registerHoverProvider(ABL_MODE.language, this)); 16 | } 17 | 18 | public provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { 19 | const doc = this._ablDocumentController.getDocument(document); 20 | const selection = utils.getText(document, position); 21 | if (!selection) { 22 | return; 23 | } 24 | const split = selection.statement.split(/[.:\s\t]/); 25 | if (split.length === 0) { 26 | return; 27 | } 28 | const words = utils.cleanArray(split); 29 | if (words.length > 0) { 30 | if ((words.length === 1) || 31 | ((words.length > 1) && (selection.word === words[0]))) { 32 | // check for table collection 33 | const tb = getTableCollection().items.find((item) => item.label.toString().toLocaleLowerCase() === selection.word); 34 | if (tb) { 35 | const tbd = tb as ABLTableDefinition; 36 | return new Hover([selection.word, '*' + tb.detail + '*', 'PK: ' + tbd.pkList], selection.wordRange); 37 | } 38 | } else { 39 | // translate buffer var/param 40 | words[0] = (doc.searchBuffer(words[0], position) || words[0]); 41 | // check for table.field collection 42 | const tb = getTableCollection().items.find((item) => item.label === words[0]); 43 | if (tb) { 44 | // tslint:disable-next-line:no-string-literal 45 | const fdLst = tb['fields'] as ABLFieldDefinition[]; 46 | const fd = fdLst.find((item) => item.label === words[1]); 47 | if (fd) { 48 | return new Hover([selection.statement, '*' + fd.detail + '*', 'Type: ' + fd.dataType, 'Format: ' + fd.format], selection.statementRange); 49 | } else { 50 | return; 51 | } 52 | } 53 | } 54 | } 55 | 56 | const symbol = doc.searchSymbol(words, selection.word, position, true); 57 | if (!isNullOrUndefined(symbol)) { 58 | if (symbol.type === SYMBOL_TYPE.TEMPTABLE) { 59 | const tt = (symbol.value) as ABLTempTable; 60 | return new Hover([selection.word, 'Temp-table *' + tt.label + '*'], selection.wordRange); 61 | } 62 | if (symbol.type === SYMBOL_TYPE.TEMPTABLE_FIELD) { 63 | const tt = (symbol.origin) as ABLTempTable; 64 | const tf = (symbol.value) as ABLVariable; 65 | return new Hover([selection.word, 'Field *' + tf.name + '*', 'from temp-table *' + tt.label + '*'], selection.wordRange); 66 | } 67 | if (symbol.type === SYMBOL_TYPE.METHOD) { 68 | const mt = (symbol.value) as ABLMethod; 69 | return new Hover([selection.word, 'Method *' + mt.name + '*'], selection.wordRange); 70 | } 71 | if (symbol.type === SYMBOL_TYPE.GLOBAL_VAR) { 72 | const gv = (symbol.value) as ABLVariable; 73 | if (gv.dataType === 'buffer') { 74 | return new Hover([selection.word, 'Global buffer *' + gv.name + '*', 'for table *' + gv.additional + '*'], selection.wordRange); 75 | } else { 76 | return new Hover([selection.word, 'Global variable *' + gv.name + '*'], selection.wordRange); 77 | } 78 | } 79 | if (symbol.type === SYMBOL_TYPE.LOCAL_PARAM) { 80 | const mt = (symbol.origin) as ABLMethod; 81 | const lp = (symbol.value) as ABLParameter; 82 | if (lp.dataType === 'temp-table') { 83 | return new Hover([selection.word, 'Local temp-table parameter *' + lp.name + '*', 'from method *' + mt.name + '*'], selection.wordRange); 84 | } else if (lp.dataType === 'buffer') { 85 | return new Hover([selection.word, 'Local buffer parameter *' + lp.name + '*', 'for table *' + lp.additional + '*', 'from method *' + mt.name + '*'], selection.wordRange); 86 | } else { 87 | return new Hover([selection.word, 'Local parameter *' + lp.name + '*', 'from method *' + mt.name + '*'], selection.wordRange); 88 | } 89 | } 90 | if (symbol.type === SYMBOL_TYPE.LOCAL_VAR) { 91 | const mt = (symbol.origin) as ABLMethod; 92 | const lv = (symbol.value) as ABLVariable; 93 | if (lv.dataType === 'buffer') { 94 | return new Hover([selection.word, 'Local buffer *' + lv.name + '*', 'for table *' + lv.additional + '*', 'from method *' + mt.name + '*'], selection.wordRange); 95 | } else { 96 | return new Hover([selection.word, 'Local variable *' + lv.name + '*', 'from method *' + mt.name + '*'], selection.wordRange); 97 | } 98 | } 99 | } 100 | 101 | return; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ablTest.ts: -------------------------------------------------------------------------------- 1 | import { mkdtemp, readFile } from 'fs'; 2 | import { tmpdir } from 'os'; 3 | import * as vscode from 'vscode'; 4 | import { getOpenEdgeConfig } from './ablConfig'; 5 | import { create } from './OutputChannelProcess'; 6 | import { createProArgs, getProBin, setupEnvironmentVariables } from './shared/ablPath'; 7 | import { OpenEdgeConfig } from './shared/openEdgeConfigFile'; 8 | 9 | import * as glob from 'glob'; 10 | import * as path from 'path'; 11 | import * as promisify from 'util.promisify'; 12 | import * as xml2js from 'xml2js'; 13 | import * as rimraf from 'rimraf'; 14 | 15 | const readFileAsync = promisify(readFile); 16 | const globAsync = promisify(glob); 17 | const mkdtempAsync = promisify(mkdtemp); 18 | const rmdirAsync = promisify(rimraf); 19 | 20 | const outputChannel = vscode.window.createOutputChannel('ABL Tests'); 21 | const failedStatusChar = '✘'; 22 | const successStatusChar = '✔'; 23 | 24 | const promiseSerial = (funcs) => 25 | funcs.reduce((promise, func) => 26 | promise.then((result) => func().then(Array.prototype.concat.bind(result))), 27 | Promise.resolve([])); 28 | 29 | export function ablTest(filename: string, ablConfig: vscode.WorkspaceConfiguration): Thenable { 30 | 31 | // let cwd = path.dirname(filename); 32 | const cwd = vscode.workspace.rootPath; 33 | 34 | return getOpenEdgeConfig().then((oeConfig) => { 35 | const cmd = getProBin(oeConfig.dlc); 36 | outputChannel.clear(); 37 | outputChannel.show(true); 38 | 39 | outputChannel.appendLine(`Starting UnitTests`); 40 | 41 | const env = setupEnvironmentVariables(process.env, oeConfig, vscode.workspace.rootPath); 42 | if (filename) { 43 | runTestFile(filename, cmd, env, cwd, oeConfig).then((summary) => { 44 | outputChannel.appendLine(`Executed ${summary.tests} tests, Errors ${summary.errors}, Failures ${summary.failures}`); 45 | }); 46 | } else { 47 | oeConfig.test.files.forEach(async (pattern) => { 48 | const files = await globAsync(pattern, { cwd }); 49 | const r = []; 50 | for (const file of files) { 51 | r.push(await runTestFile(file, cmd, env, cwd, oeConfig)); 52 | } 53 | 54 | const summary = r.reduce((s1: any, s2: any) => { 55 | return { 56 | errors: s1.errors + s2.errors, 57 | failures: s1.failures + s2.failures, 58 | tests: s1.tests + s2.tests, 59 | }; 60 | }); 61 | outputChannel.appendLine(`Executed ${summary.tests} tests, Errors ${summary.errors}, Failures ${summary.failures}`); 62 | }); 63 | } 64 | // outputChannel.appendLine(`Finished UnitTests`); 65 | }); 66 | } 67 | 68 | async function runTestFile(fileName, cmd, env, cwd, oeConfig: OpenEdgeConfig) { 69 | const outDir = await mkdtempAsync(path.join(tmpdir(), 'ablunit-')); 70 | 71 | const xmlParser = new xml2js.Parser(); 72 | const parseStringAsync = promisify(xmlParser.parseString); 73 | 74 | // TODO specif args for Tests 75 | // TODO -db ? 76 | if (oeConfig.test.beforeEach) { 77 | let beforeCmd = oeConfig.test.beforeEach.cmd; 78 | beforeCmd = beforeCmd.replace(/%([^%]+)%/g, (_, n) => { 79 | return env[n]; 80 | }); 81 | const beforeCwd = oeConfig.test.beforeEach.cwd || cwd; 82 | await create(beforeCmd, ['-c', `echo BASH before ${fileName}`], { env, cwd: beforeCwd }, outputChannel); 83 | // await create(beforeCmd, oeConfig.test.beforeEach.args, { env: env, cwd: cwd }, outputChannel); 84 | } 85 | const args = createProArgs({ 86 | batchMode: true, 87 | param: `${fileName} -outputLocation ${outDir}`, 88 | parameterFiles: oeConfig.parameterFiles, 89 | startupProcedure: 'ABLUnitCore.p', 90 | temporaryDirectory: outDir, 91 | }); 92 | const outputFile = path.join(outDir, 'results.xml'); 93 | 94 | const consoleOutput = await create(cmd, args, { env, cwd }, outputChannel); 95 | const content = await readFileAsync(outputFile); 96 | const result = await parseStringAsync(content); 97 | const testResultSummary = { 98 | errors: 0, 99 | failures: 0, 100 | tests: 0, 101 | }; 102 | if (result.testsuites) { 103 | result.testsuites.testsuite.forEach((testsuite) => { 104 | testsuite.testcase.forEach((t) => { 105 | 106 | const statusChar = t.$.status !== 'Success' ? failedStatusChar : successStatusChar; 107 | outputChannel.appendLine(`\t${statusChar} ${t.$.name}`); 108 | 109 | let stacktrace = []; 110 | if (t.$.status === 'Failure') { 111 | stacktrace = t.failure; 112 | } 113 | if (t.$.status === 'Error') { 114 | stacktrace = t.error; 115 | } 116 | stacktrace.forEach((f) => { 117 | f.split('\r\n').filter((l) => l.indexOf('ABLUnit') === -1).forEach((l) => { 118 | outputChannel.appendLine(`\t\t↱ ${l}`); 119 | }); 120 | }); 121 | 122 | }); 123 | }); 124 | testResultSummary.tests += parseInt(result.testsuites.$.tests, 10); 125 | testResultSummary.errors += parseInt(result.testsuites.$.errors, 10); 126 | testResultSummary.failures += parseInt(result.testsuites.$.failures, 10); 127 | // outputChannel.appendLine(`Executed ${result.testsuites.$.tests} tests, Errors ${result.testsuites.$.errors}, Failures ${result.testsuites.$.failures}`); 128 | } 129 | await rmdirAsync(outDir); 130 | 131 | if (oeConfig.test.afterEach) { 132 | let afterCmd = oeConfig.test.afterEach.cmd; 133 | afterCmd = afterCmd.replace(/%([^%]+)%/g, (_, n) => { 134 | return env[n]; 135 | }); 136 | const afterCwd = oeConfig.test.afterEach.cwd || cwd; 137 | await create(afterCmd, ['-c', `echo BASH after ${fileName}`], { env, cwd: afterCwd }, outputChannel); 138 | // await create(afterCmd, oeConfig.test.afterEach.args, { env: env, cwd: cwd }, outputChannel); 139 | } 140 | return testResultSummary; 141 | } 142 | -------------------------------------------------------------------------------- /src/misc/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ABLIndexDefinition, ABLTableDefinition, TextSelection } from './definition'; 3 | 4 | const regexInvalidWordEnd: RegExp = new RegExp(/[\.|\:|\-|\_|\\|\/]$/); 5 | 6 | export function getText(document: vscode.TextDocument, position: vscode.Position, escapeEndChars?: boolean): TextSelection { 7 | const res = new TextSelection(); 8 | res.wordRange = document.getWordRangeAtPosition(position, /[\w\d\-\+]+/); 9 | if (!res.wordRange) { 10 | return; 11 | } 12 | res.word = document.getText(res.wordRange).toLowerCase(); 13 | res.statementRange = document.getWordRangeAtPosition(position, /[\w\d\-\+\.\:\\\/]+/); 14 | res.statement = document.getText(res.statementRange).toLowerCase(); 15 | if (escapeEndChars !== true) { 16 | while (regexInvalidWordEnd.test(res.statement)) { 17 | res.statement = res.statement.substring(0, res.statement.length - 1); 18 | } 19 | } 20 | return res; 21 | } 22 | 23 | export function cleanArray(arr: string[]): string[] { 24 | if (!arr) { 25 | return []; 26 | } 27 | for (let i = 0; i < arr.length; i++) { 28 | if (arr[i] === '') { 29 | arr.splice(i, 1); 30 | i--; 31 | } 32 | } 33 | return arr; 34 | } 35 | 36 | export function padRight(text: string, size: number): string { 37 | while (text.length < size) { 38 | text += ' '; 39 | } 40 | return text; 41 | } 42 | export function removeInvalidRightChar(text: string): string { 43 | const regexValidWordEnd: RegExp = new RegExp(/[\w\d]$/); 44 | while (!regexValidWordEnd.test(text)) { 45 | text = text.substring(0, text.length - 1); 46 | } 47 | return text; 48 | } 49 | export function updateTableCompletionList(table: ABLTableDefinition) { 50 | table.completionFields = new vscode.CompletionList(table.allFields.map((field) => { 51 | return new vscode.CompletionItem(field.name, vscode.CompletionItemKind.Variable); 52 | })); 53 | table.completionIndexes = mapIndexCompletionList(table, table.indexes); 54 | table.completionAdditional = mapAdditionalCompletionList(table); 55 | table.completion = new vscode.CompletionList([...table.completionFields.items, ...table.completionAdditional.items, ...table.completionIndexes.items]); 56 | 57 | const pk = table.indexes.find((item) => item.primary); 58 | if ((pk) && (pk.fields)) { 59 | table.pkList = pk.fields.map((item) => item.label).join(', '); 60 | } else { 61 | table.pkList = ''; 62 | } 63 | } 64 | 65 | function mapIndexCompletionList(table: ABLTableDefinition, list: ABLIndexDefinition[]): vscode.CompletionList { 66 | const result = new vscode.CompletionList(); 67 | 68 | if (!list) { return result; } 69 | 70 | list.forEach((objItem) => { 71 | if (!objItem.fields) { return; } 72 | const item = new vscode.CompletionItem(objItem.label, vscode.CompletionItemKind.Snippet); 73 | item.insertText = getIndexSnippet(table, objItem); 74 | item.detail = objItem.fields.map((i) => i.label).join(', '); 75 | if (objItem.primary) { 76 | item.label = '>INDEX (PK) ' + item.label; 77 | item.detail = 'Primary Key, Fields: ' + item.detail; 78 | } else if (objItem.unique) { 79 | item.label = '>INDEX (U) ' + item.label; 80 | item.detail = 'Unique Index, Fields: ' + item.detail; 81 | } else { 82 | item.label = '>INDEX ' + item.label; 83 | item.detail = 'Index, Fields: ' + item.detail; 84 | } 85 | result.items.push(item); 86 | }); 87 | return result; 88 | } 89 | 90 | function mapAdditionalCompletionList(table: ABLTableDefinition): vscode.CompletionList { 91 | const result = new vscode.CompletionList(); 92 | 93 | // ALL FIELDS 94 | const item = new vscode.CompletionItem('>ALL FIELDS', vscode.CompletionItemKind.Snippet); 95 | item.insertText = getAllFieldsSnippet(table); 96 | item.detail = 'Insert all table fields'; 97 | result.items.push(item); 98 | 99 | return result; 100 | } 101 | 102 | function getIndexSnippet(table: ABLTableDefinition, index: ABLIndexDefinition): vscode.SnippetString { 103 | const snip = new vscode.SnippetString(); 104 | let first: boolean = true; 105 | let size = 0; 106 | // get max field name size 107 | index.fields.forEach((field) => { if (field.label.length > size) { size = field.label.length; } }); 108 | // fields snippet 109 | index.fields.forEach((field) => { 110 | if (first) { 111 | first = false; 112 | } else { 113 | snip.appendText('\n'); 114 | snip.appendText('\tand ' + table.label + '.'); 115 | } 116 | snip.appendText(padRight(field.label, size) + ' = '); 117 | snip.appendTabstop(); 118 | }); 119 | return snip; 120 | } 121 | 122 | function getAllFieldsSnippet(table: ABLTableDefinition): vscode.SnippetString { 123 | const snip = new vscode.SnippetString(); 124 | let first: boolean = true; 125 | let size = 0; 126 | // get max field name size 127 | table.allFields.forEach((field) => { if (field.name.length > size) { size = field.name.length; } }); 128 | // allFields snippet 129 | table.allFields.forEach((field) => { 130 | if (first) { 131 | first = false; 132 | } else { 133 | snip.appendText('\n'); 134 | snip.appendText(table.label + '.'); 135 | } 136 | snip.appendText(padRight(field.name, size) + ' = '); 137 | snip.appendTabstop(); 138 | }); 139 | return snip; 140 | } 141 | 142 | export function replaceSnippetTableName(list: vscode.CompletionItem[], tableName: string, replacement: string): vscode.CompletionItem[] { 143 | const result = [...list]; 144 | return result.map((item) => { 145 | if (item.kind === vscode.CompletionItemKind.Snippet) { 146 | item = Object.assign(new vscode.CompletionItem(item.label), item); 147 | const regex = new RegExp('^(?:[\\W]*)(' + tableName + ')(?![\\w]+)', 'gim'); 148 | let ss = ''; 149 | if (item.insertText instanceof vscode.SnippetString) { 150 | ss = item.insertText.value; 151 | } else { 152 | ss = item.insertText; 153 | } 154 | item.insertText = new vscode.SnippetString(ss.replace(regex, replacement)); 155 | } 156 | return item; 157 | }); 158 | } 159 | -------------------------------------------------------------------------------- /src/providers/ablCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as util from 'util'; 3 | import * as vscode from 'vscode'; 4 | import { ABL_MODE } from '../ablMode'; 5 | import { ABLTableDefinition } from '../misc/definition'; 6 | import { getText, replaceSnippetTableName, updateTableCompletionList } from '../misc/utils'; 7 | import { ABLDocumentController, getDocumentController } from '../parser/documentController'; 8 | 9 | let watcher: vscode.FileSystemWatcher = null; 10 | const _tableCollection: vscode.CompletionList = new vscode.CompletionList(); 11 | const readFileAsync = util.promisify(fs.readFile); 12 | 13 | export class ABLCompletionItemProvider implements vscode.CompletionItemProvider { 14 | private _ablDocumentController: ABLDocumentController; 15 | 16 | constructor(context: vscode.ExtensionContext) { 17 | this._ablDocumentController = getDocumentController(); 18 | context.subscriptions.push(vscode.languages.registerCompletionItemProvider(ABL_MODE.language, this, '.')); 19 | } 20 | 21 | public provideCompletionItems( 22 | document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): 23 | Thenable { 24 | 25 | return new Promise((resolve, reject) => { 26 | try { 27 | const completionItemResult: vscode.CompletionItem[] = []; 28 | 29 | const doc = this._ablDocumentController.getDocument(document); 30 | const p = new vscode.Position(position.line, position.character - 1); // get the previous char to compare previous statement 31 | const textSelection = getText(document, p, true); 32 | const tsParts = textSelection.statement.split('.'); 33 | 34 | if (tsParts.length === 2) { 35 | // translate buffer var/param 36 | let originalName = tsParts[0]; 37 | tsParts[0] = (doc.searchBuffer(tsParts[0], position) || tsParts[0]); 38 | if (originalName === tsParts[0]) { 39 | originalName = null; 40 | } 41 | // 42 | let result = this.getCompletionFields(tsParts[0], originalName); 43 | if ((result) && (result.length > 0)) { 44 | resolve(result); 45 | return; 46 | } 47 | result = doc.getCompletionTempTableFields(tsParts[0], originalName); 48 | if ((result) && (result.length > 0)) { 49 | resolve(result); 50 | return; 51 | } 52 | 53 | // External Temp-tables 54 | doc.externalDocument.forEach((external) => { 55 | if ((!result) || (result.length === 0)) { 56 | const extDoc = this._ablDocumentController.getDocument(external); 57 | if ((extDoc) && (extDoc.processed)) { 58 | result = extDoc.getCompletionTempTableFields(tsParts[0], originalName); 59 | } 60 | } 61 | }); 62 | if ((result) && (result.length > 0)) { 63 | resolve(result); 64 | return; 65 | } 66 | } else if (tsParts.length === 1) { 67 | // Tables 68 | const tb = _tableCollection.items; 69 | // Symbols 70 | const docSym = doc.getCompletionSymbols(position); 71 | // External Symbols 72 | let extSym: vscode.CompletionItem[] = []; 73 | doc.externalDocument.forEach((external) => { 74 | const extDoc = this._ablDocumentController.getDocument(external); 75 | if ((extDoc) && (extDoc.processed)) { 76 | extSym = [...extSym, ...extDoc.getCompletionSymbols(position)]; 77 | } 78 | }); 79 | resolve([...tb, ...docSym, ...extSym]); 80 | return; 81 | } 82 | resolve(completionItemResult); 83 | } catch { 84 | reject(); 85 | } 86 | }); 87 | } 88 | 89 | private getCompletionFields(prefix: string, replacement?: string): vscode.CompletionItem[] { 90 | // Tables 91 | const tb = _tableCollection.items.find((item) => item.label.toString().toLowerCase() === prefix); 92 | if (tb) { 93 | // tslint:disable-next-line:no-string-literal 94 | let result = tb['completion'].items; 95 | if (!util.isNullOrUndefined(replacement)) { 96 | result = replaceSnippetTableName(result, prefix, replacement); 97 | } 98 | return result; 99 | } 100 | return []; 101 | } 102 | } 103 | 104 | export function loadDumpFile(filename: string): Thenable { 105 | if (!filename) { 106 | return Promise.resolve({}); 107 | } 108 | return readFileAsync(filename, { encoding: 'utf8' }).then((text) => { 109 | return JSON.parse(text); 110 | }); 111 | } 112 | 113 | export function getTableCollection() { 114 | return _tableCollection; 115 | } 116 | 117 | function findDumpFiles() { 118 | return vscode.workspace.findFiles('**/.openedge.db.*'); 119 | } 120 | 121 | function loadAndSetDumpFile(filename: string) { 122 | unloadDumpFile(filename); 123 | return readFileAsync(filename, { encoding: 'utf8' }).then((text) => { 124 | const fileDataResult: ABLTableDefinition[] = JSON.parse(text); 125 | if (fileDataResult) { 126 | fileDataResult 127 | .map((tb) => { 128 | const obj: ABLTableDefinition = new ABLTableDefinition(); 129 | Object.assign(obj, tb); 130 | obj.filename = filename; 131 | // tslint:disable-next-line:no-string-literal 132 | obj.fields.map((fd) => fd.name = fd['label']); 133 | updateTableCompletionList(obj); 134 | return obj; 135 | }) 136 | .forEach((tb) => { 137 | _tableCollection.items.push(tb); 138 | }); 139 | } 140 | }); 141 | } 142 | 143 | function unloadDumpFile(filename: string) { 144 | // tslint:disable-next-line:no-string-literal 145 | _tableCollection.items = _tableCollection.items.filter((item) => item['filename'] !== filename); 146 | } 147 | 148 | export function watchDictDumpFiles() { 149 | return new Promise((resolve, reject) => { 150 | watcher = vscode.workspace.createFileSystemWatcher('**/.openedge.db.*'); 151 | watcher.onDidChange((uri) => loadAndSetDumpFile(uri.fsPath)); 152 | watcher.onDidDelete((uri) => unloadDumpFile(uri.fsPath)); 153 | findDumpFiles().then((filename) => { filename.forEach((f) => { loadAndSetDumpFile(f.fsPath); }); }); 154 | resolve(); 155 | }); 156 | } 157 | -------------------------------------------------------------------------------- /src/debugAdapter/messages.ts: -------------------------------------------------------------------------------- 1 | import { AblDebugKind, DebugVariable, isPrimitiveType } from './variables'; 2 | 3 | export interface DebugMessage { 4 | code: string; 5 | args: string[][]; 6 | } 7 | export interface DebugMessageListing extends DebugMessage { 8 | code: string; 9 | args: string[][]; 10 | breakpointCount: number; 11 | file: string; 12 | stoppedAtLine: number; 13 | breakpoints: DebugMessageListingBreapoint[]; 14 | } 15 | export interface DebugMessageClassInfo extends DebugMessage { 16 | baseClass: string; 17 | properties?: DebugVariable[]; 18 | } 19 | export interface DebugMessageArray extends DebugMessage { 20 | values: string[]; 21 | } 22 | export interface DebugMessageVariables extends DebugMessage { 23 | variables: DebugVariable[]; 24 | } 25 | export interface DebugMessageListingBreapoint { 26 | line: number; 27 | id: number; 28 | } 29 | 30 | export function convertDataToDebuggerMessage(data: any): DebugMessage[] { 31 | const messages: string = data.toString(); 32 | return messages.split('\0').filter((msg) => msg.length > 0).map((msg) => { 33 | 34 | const idxCode = msg.indexOf(';'); 35 | let msgCode = msg; 36 | let args = []; 37 | if (idxCode !== -1) { 38 | msgCode = msg.slice(0, idxCode); 39 | msg = msg.substr(idxCode + 1); 40 | 41 | // specific args convertion 42 | if (msgCode === 'MSG_LISTING') { 43 | args = msg.split(';').filter((p) => p.length > 0); 44 | const msgConverted: DebugMessageListing = { 45 | args: [], 46 | breakpointCount: parseInt(args[4], 10), 47 | breakpoints: [], 48 | code: msgCode, 49 | file: args[0], 50 | stoppedAtLine: parseInt(args[5], 10), 51 | }; 52 | 53 | for (let bpIdx = 0; bpIdx < msgConverted.breakpointCount; bpIdx++) { 54 | msgConverted.breakpoints.push({ 55 | id: args[6 + (bpIdx * 2) + 1], 56 | line: args[6 + bpIdx * 2], 57 | }); 58 | } 59 | return msgConverted; 60 | } else if (msgCode === 'MSG_CLASSINFO') { 61 | msg = msg.replace(/\n/g, ''); 62 | args = msg.split(';').filter((p) => p.length > 0); 63 | const msgConverted: DebugMessageClassInfo = { 64 | args: [], 65 | baseClass: args[3] === 'Y' ? args[4] : null, 66 | code: msgCode, 67 | properties: [], 68 | }; 69 | args = args.slice(5); 70 | const propCount = args.length / 6; 71 | for (let propIdx = 0; propIdx < propCount; propIdx++) { 72 | // args[propIdx * 6 + 0] : P:public, V:private 73 | // args[propIdx * 6 + 3] : ?? 74 | // args[propIdx * 6 + 4] : R, RW 75 | const variable = { 76 | children: [], 77 | kind: AblDebugKind.Variable, 78 | name: args[propIdx * 6 + 1], 79 | type: args[propIdx * 6 + 2], 80 | value: args[propIdx * 6 + 5], 81 | }; 82 | if (!isPrimitiveType(variable.type)) { 83 | variable.kind = AblDebugKind.Class; 84 | } 85 | msgConverted.properties.push(variable); 86 | } 87 | return msgConverted; 88 | } else if (msgCode === 'MSG_VARIABLES') { 89 | const parts1 = msg.split('\n').filter((p) => p.length > 0); 90 | args = parts1.map((p) => p.split(';')).filter((p) => p.length > 0); 91 | const msgConverted: DebugMessageVariables = { 92 | args: [], 93 | code: msgCode, 94 | variables: [], 95 | }; 96 | msgConverted.variables = args.map((p) => { 97 | if (p[2] !== '?') { // if not empty, it's a class 98 | return { 99 | children: [], 100 | kind: AblDebugKind.Class, 101 | name: p[0], 102 | type: p[2], 103 | value: p[6], 104 | }; 105 | } else if (p[4] !== '0') { // if > 0 this is an Extent (Array) 106 | return { 107 | children: [], 108 | kind: AblDebugKind.Array, 109 | name: p[0], 110 | type: p[1], 111 | value: p[6], 112 | }; 113 | } else { 114 | return { 115 | children: [], 116 | kind: AblDebugKind.Variable, 117 | name: p[0], 118 | type: p[1], 119 | value: p[6], 120 | }; 121 | } 122 | }); 123 | return msgConverted; 124 | } else if (msgCode === 'MSG_ARRAY') { 125 | msg = msg.replace(/\n/g, ''); 126 | args = msg.split(';').slice(1).filter((value, index) => { 127 | return (index + 1) % 3 === 0; 128 | }).map((v) => v.replace(/\u0012/g, '')); 129 | const msgConverted: DebugMessageArray = { 130 | args: [], 131 | code: msgCode, 132 | values: args as string[], 133 | }; 134 | return msgConverted; 135 | } else if (msgCode === 'MSG_PARAMETERS') { 136 | const parts1 = msg.split('\n').filter((p) => p.length > 0); 137 | args = parts1.map((p) => p.split(';')).filter((p) => p.length > 0); 138 | const msgConverted: DebugMessageVariables = { 139 | args: [], 140 | code: msgCode, 141 | variables: [], 142 | }; 143 | msgConverted.variables = args.map((p) => { 144 | let displayName = p[1]; 145 | if (p[0] === 'OUTPUT') { 146 | displayName = '\u2190' + displayName; 147 | } else if (p[0] === 'INPUT') { 148 | displayName = '\u2192' + displayName; 149 | } else if (p[0] === 'INPUT-OUTPUT') { 150 | displayName = '\u2194' + displayName; 151 | } 152 | return { 153 | children: [], 154 | kind: AblDebugKind.Parameter, 155 | name: displayName, 156 | type: p[2], 157 | value: p[5], 158 | }; 159 | }); 160 | return msgConverted; 161 | } else { 162 | const parts1 = msg.split('\n').filter((p) => p.length > 0); 163 | args = parts1.map((p) => p.split(';')).filter((p) => p.length > 0); 164 | } 165 | } 166 | return { code: msgCode, args }; 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { checkSyntax, removeSyntaxStatus } from './ablCheckSyntax'; 3 | import { openDataDictionary, readDataDictionary } from './ablDataDictionary'; 4 | import { ABL_MODE } from './ablMode'; 5 | import { run } from './ablRun'; 6 | import { ablTest } from './ablTest'; 7 | import { checkOpenEdgeConfigFile, checkProgressBinary } from './checkExtensionConfig'; 8 | import { AblDebugConfigurationProvider } from './debugAdapter/ablDebugConfigurationProvider'; 9 | import { initDocumentController } from './parser/documentController'; 10 | import { ABLCompletionItemProvider, getTableCollection, watchDictDumpFiles } from './providers/ablCompletionProvider'; 11 | import { ABLDefinitionProvider } from './providers/ablDefinitionProvider'; 12 | import { ABLFormattingProvider } from './providers/ablFormattingProvider'; 13 | import { ABLHoverProvider } from './providers/ablHoverProvider'; 14 | import { ABLSymbolProvider } from './providers/ablSymbolProvider'; 15 | 16 | let errorDiagnosticCollection: vscode.DiagnosticCollection; 17 | let warningDiagnosticCollection: vscode.DiagnosticCollection; 18 | 19 | export function activate(ctx: vscode.ExtensionContext): void { 20 | /* 21 | let useLangServer = vscode.workspace.getConfiguration('go')['useLanguageServer']; 22 | let langServerFlags: string[] = vscode.workspace.getConfiguration('go')['languageServerFlags'] || []; 23 | let toolsGopath = vscode.workspace.getConfiguration('go')['toolsGopath']; 24 | */ 25 | ctx.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('abl', new AblDebugConfigurationProvider())); 26 | 27 | startBuildOnSaveWatcher(ctx.subscriptions); 28 | startDictWatcher(); 29 | startDocumentWatcher(ctx); 30 | 31 | initProviders(ctx); 32 | registerCommands(ctx); 33 | 34 | } 35 | 36 | function registerCommands(ctx: vscode.ExtensionContext) { 37 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.propath', () => { 38 | // let gopath = process.env['GOPATH']; 39 | // let wasInfered = vscode.workspace.getConfiguration('go')['inferGopath']; 40 | vscode.window.showInformationMessage('PROPATH : ...'); 41 | })); 42 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.checkSyntax', () => { 43 | const ablConfig = vscode.workspace.getConfiguration('abl'); 44 | runBuilds(vscode.window.activeTextEditor.document, ablConfig); 45 | })); 46 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.dataDictionary', () => { 47 | openDataDictionary(); 48 | })); 49 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.dictionary.dumpDefinition', () => { 50 | const ablConfig = vscode.workspace.getConfiguration(ABL_MODE.language); 51 | readDataDictionary(ablConfig); 52 | })); 53 | 54 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.run.currentFile', () => { 55 | const ablConfig = vscode.workspace.getConfiguration('abl'); 56 | run(vscode.window.activeTextEditor.document.uri.fsPath, ablConfig); 57 | })); 58 | 59 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.test', () => { 60 | const ablConfig = vscode.workspace.getConfiguration('abl'); 61 | ablTest(null, ablConfig); 62 | })); 63 | 64 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.test.currentFile', () => { 65 | const ablConfig = vscode.workspace.getConfiguration('abl'); 66 | ablTest(vscode.window.activeTextEditor.document.uri.fsPath, ablConfig); 67 | })); 68 | 69 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.tables', () => { 70 | return getTableCollection().items.map((item) => item.label); 71 | })); 72 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.table', (tableName) => { 73 | return getTableCollection().items.find((item) => item.label === tableName); 74 | })); 75 | 76 | ctx.subscriptions.push(vscode.commands.registerCommand('abl.debug.startSession', (config) => { 77 | if (!config.request) { // if 'request' is missing interpret this as a missing launch.json 78 | const activeEditor = vscode.window.activeTextEditor; 79 | if (!activeEditor || activeEditor.document.languageId !== 'abl') { 80 | return; 81 | } 82 | 83 | // tslint:disable: object-literal-sort-keys 84 | config = Object.assign(config, { 85 | name: 'Attach', 86 | type: 'abl', 87 | request: 'attach', 88 | }); 89 | } 90 | vscode.commands.executeCommand('vscode.startDebug', config); 91 | })); 92 | 93 | errorDiagnosticCollection = vscode.languages.createDiagnosticCollection('abl-error'); 94 | ctx.subscriptions.push(errorDiagnosticCollection); 95 | warningDiagnosticCollection = vscode.languages.createDiagnosticCollection('abl-warning'); 96 | ctx.subscriptions.push(warningDiagnosticCollection); 97 | 98 | { 99 | const ablConfig = vscode.workspace.getConfiguration('abl'); 100 | const options = ['Ignore', 'Don\'t show this message again', 'Read the docs']; 101 | if (ablConfig.get('warnConfigFile')) { 102 | checkOpenEdgeConfigFile().catch((_) => { 103 | vscode.window.showInformationMessage('No .openedge.json found, using the default configuration', ...options).then((item) => { 104 | if (item === options[1]) { 105 | ablConfig.update('warnConfigFile', false); 106 | } else if (item === options[2]) { 107 | vscode.env.openExternal(vscode.Uri.parse('https://github.com/chriscamicas/vscode-abl/wiki/Config-file')); 108 | } 109 | }); 110 | }); 111 | } 112 | if (ablConfig.get('checkProgressBinary')) { 113 | checkProgressBinary().catch((_) => { 114 | vscode.window.showErrorMessage('Progress binary not found. You should check your configuration', ...options).then((item) => { 115 | if (item === options[1]) { 116 | ablConfig.update('checkProgressBinary', false); 117 | } else if (item === options[2]) { 118 | vscode.env.openExternal(vscode.Uri.parse('https://github.com/chriscamicas/vscode-abl/wiki/Progress-binary-not-found')); 119 | } 120 | }); 121 | }); 122 | } 123 | } 124 | } 125 | 126 | function deactivate() { 127 | // no need for deactivation yet 128 | } 129 | 130 | function initProviders(context: vscode.ExtensionContext) { 131 | new ABLCompletionItemProvider(context); 132 | new ABLHoverProvider(context); 133 | new ABLDefinitionProvider(context); 134 | new ABLSymbolProvider(context); 135 | new ABLFormattingProvider(context); 136 | } 137 | function startDocumentWatcher(context: vscode.ExtensionContext) { 138 | initDocumentController(context); 139 | } 140 | 141 | function runBuilds(document: vscode.TextDocument, ablConfig: vscode.WorkspaceConfiguration) { 142 | 143 | function mapSeverityToVSCodeSeverity(sev: string) { 144 | switch (sev) { 145 | case 'error': return vscode.DiagnosticSeverity.Error; 146 | case 'warning': return vscode.DiagnosticSeverity.Warning; 147 | default: return vscode.DiagnosticSeverity.Error; 148 | } 149 | } 150 | 151 | if (document.languageId !== 'abl') { 152 | return; 153 | } 154 | 155 | const uri = document.uri; 156 | checkSyntax(uri.fsPath, ablConfig).then((errors) => { 157 | errorDiagnosticCollection.clear(); 158 | warningDiagnosticCollection.clear(); 159 | 160 | const diagnosticMap: Map> = new Map(); 161 | 162 | errors.forEach((error) => { 163 | const canonicalFile = vscode.Uri.file(error.file).toString(); 164 | let startColumn = 0; 165 | let endColumn = 1; 166 | if (error.line === 0) { 167 | vscode.window.showErrorMessage(error.msg); 168 | } else { 169 | let range; 170 | if (document && document.uri.toString() === canonicalFile) { 171 | range = new vscode.Range(error.line - 1, startColumn, error.line - 1, document.lineAt(error.line - 1).range.end.character + 1); 172 | const text = document.getText(range); 173 | const [_, leading, trailing] = /^(\s*).*(\s*)$/.exec(text); 174 | startColumn = startColumn + leading.length; 175 | endColumn = text.length - trailing.length; 176 | } 177 | range = new vscode.Range(error.line - 1, startColumn, error.line - 1, endColumn); 178 | const severity = mapSeverityToVSCodeSeverity(error.severity); 179 | const diagnostic = new vscode.Diagnostic(range, error.msg, severity); 180 | let diagnostics = diagnosticMap.get(canonicalFile); 181 | if (!diagnostics) { 182 | diagnostics = new Map(); 183 | } 184 | if (!diagnostics[severity]) { 185 | diagnostics[severity] = []; 186 | } 187 | diagnostics[severity].push(diagnostic); 188 | diagnosticMap.set(canonicalFile, diagnostics); 189 | } 190 | }); 191 | diagnosticMap.forEach((diagMap, file) => { 192 | errorDiagnosticCollection.set(vscode.Uri.parse(file), diagMap[vscode.DiagnosticSeverity.Error]); 193 | warningDiagnosticCollection.set(vscode.Uri.parse(file), diagMap[vscode.DiagnosticSeverity.Warning]); 194 | }); 195 | }).catch((err) => { 196 | vscode.window.showInformationMessage('Error: ' + err); 197 | }); 198 | } 199 | 200 | function startBuildOnSaveWatcher(subscriptions: vscode.Disposable[]) { 201 | const ablConfig = vscode.workspace.getConfiguration('abl'); 202 | if (ablConfig.get('checkSyntaxOnSave') === 'file') { 203 | vscode.workspace.onDidSaveTextDocument((document) => { 204 | if (document.languageId !== 'abl') { 205 | return; 206 | } 207 | runBuilds(document, ablConfig); 208 | }, null, subscriptions); 209 | } 210 | vscode.workspace.onDidOpenTextDocument((document) => { 211 | removeSyntaxStatus(); 212 | }, null, subscriptions); 213 | vscode.window.onDidChangeActiveTextEditor((_) => { 214 | removeSyntaxStatus(); 215 | }, null, subscriptions); 216 | } 217 | 218 | function startDictWatcher() { 219 | watchDictDumpFiles(); 220 | } 221 | -------------------------------------------------------------------------------- /src/parser/processDocument.ts: -------------------------------------------------------------------------------- 1 | import { isNumber } from 'util'; 2 | import * as vscode from 'vscode'; 3 | import { ABL_ASLIKE, ABL_PARAM_DIRECTION, ABLFieldDefinition, ABLInclude, ABLIndexDefinition, ABLMethod, ABLParameter, ABLTableDefinition, ABLTempTable, ABLVariable } from '../misc/definition'; 4 | import { removeInvalidRightChar, updateTableCompletionList } from '../misc/utils'; 5 | import { SourceCode } from './sourceParser'; 6 | 7 | export function getAllIncludes(sourceCode: SourceCode): ABLInclude[] { 8 | const result: ABLInclude[] = []; 9 | // let regexInclude: RegExp = new RegExp(/\{{1}([\w\d\-\\\/\.]+)(?:.|\n)*?\}{1}/gim); 10 | // 1 = include name 11 | const regexStart: RegExp = new RegExp(/\{{1}([\w\d\-\\\/\.]+)/gim); 12 | // 1 = include name 13 | const regexEnd: RegExp = new RegExp(/\}{1}/gim); 14 | // 15 | const text = sourceCode.sourceWithoutStrings; 16 | let resStart = regexStart.exec(text); 17 | let resEnd; 18 | while (resStart) { 19 | regexEnd.lastIndex = regexStart.lastIndex; 20 | resEnd = regexEnd.exec(text); 21 | if (resEnd) { 22 | const nm = resStart[1].trim().toLowerCase(); 23 | // ignores {1} (include parameter) and {&ANYTHING} (global/scoped definition) 24 | if ((Number.isNaN(Number.parseInt(nm, 10))) && (!nm.startsWith('&')) && (!result.find((item) => item.name === nm))) { 25 | const v = new ABLInclude(); 26 | v.name = nm; 27 | result.push(v); 28 | } 29 | resStart = regexStart.exec(text); 30 | } else { 31 | break; 32 | } 33 | } 34 | return result; 35 | } 36 | 37 | export function getAllVariables(sourceCode: SourceCode): ABLVariable[] { 38 | const result: ABLVariable[] = []; 39 | // let regexDefineVar: RegExp = new RegExp(/(?:def|define){1}(?:[\s\t\n]|new|shared)+(?:var|variable){1}(?:[\s\t\n]+)([\w\d\-]+)[\s\t\n]+(as|like){1}[\s\t\n]+([\w\d\-\.]+)/gim); 40 | const regexDefineVar: RegExp = new RegExp(/(?:def|define){1}(?:[\s\t\n]|new|shared)+(?:var|variable){1}(?:[\s\t\n]+)([\w\d\-]+)[\s\t\n]+(as|like){1}[\s\t\n]+([\w\d\-\.]+)([\n\s\t\w\d\-\'\"]*)\./gim); 41 | // 1 = var name 42 | // 2 = as | like 43 | // 3 = type | field like 44 | // 4 = details (extent, no-undo, initial, etc) 45 | const text = sourceCode.sourceWithoutStrings; 46 | let res = regexDefineVar.exec(text); 47 | while (res) { 48 | const v = new ABLVariable(); 49 | try { 50 | v.name = res[1].trim(); 51 | v.asLike = res[2].trim() as ABL_ASLIKE; 52 | v.dataType = removeInvalidRightChar(res[3].trim()); // removeInvalidRightChar to remove special chars because is accepted in this capture group 53 | v.line = sourceCode.document.positionAt(res.index).line; 54 | v.additional = (res[4] || '').trim(); 55 | result.push(v); 56 | // tslint:disable-next-line:no-empty 57 | } catch { } // suppress errors 58 | res = regexDefineVar.exec(text); 59 | } 60 | return result; 61 | } 62 | 63 | export function getAllBuffers(sourceCode: SourceCode): ABLVariable[] { 64 | const result: ABLVariable[] = []; 65 | const regexDefineBuffer: RegExp = new RegExp(/(?:def|define){1}(?:[\s\t\n]|new|shared)+(?:buffer){1}[\s\t\n]+([\w\d\-]+){1}[\s\t\n]+(?:for){1}[\s\t\n]+(temp-table[\s\t\n]+)*([\w\d\-\+]*)(?:\.[^\w\d\-\+])+/gim); 66 | // 1 = buffer name 67 | // 2 = undefined | temp-table 68 | // 3 = buffer value 69 | const text = sourceCode.sourceWithoutStrings; 70 | let res = regexDefineBuffer.exec(text); 71 | while (res) { 72 | const v = new ABLVariable(); 73 | try { 74 | v.name = res[1].trim(); 75 | v.asLike = ABL_ASLIKE.AS; 76 | v.dataType = 'buffer'; 77 | v.line = sourceCode.document.positionAt(res.index).line; 78 | v.additional = res[3]; 79 | result.push(v); 80 | // tslint:disable-next-line:no-empty 81 | } catch { } // suppress errors 82 | res = regexDefineBuffer.exec(text); 83 | } 84 | return result; 85 | } 86 | 87 | export function getAllMethods(sourceCode: SourceCode): ABLMethod[] { 88 | const result: ABLMethod[] = []; 89 | // let regexMethod = new RegExp(/\b(proc|procedure|func|function){1}[\s\t\n]+([\w\d\-]+)(.*?)[\.\:]{1}(.|[\n\s])*?(?:end\s(proc|procedure|func|function)){1}\b/gim); 90 | // 1 = function | procedure 91 | // 2 = name 92 | // 3 = aditional details (returns xxx...) 93 | // 4 = code block (incomplete) 94 | 95 | const regexStart = new RegExp(/\b(proc|procedure|func|function){1}[\s\t\n]+([\w\d\-]+)(.*?)(?:[\.\:][^\w\d\-\+])/gim); 96 | // 1 = function | procedure 97 | // 2 = name 98 | // 3 = aditional details (returns xxx...) 99 | const regexEnd = new RegExp(/\b(?:end[\s\t]+(proc|procedure|func|function)){1}\b/gim); 100 | // 101 | const text = sourceCode.sourceWithoutStrings; 102 | let resStart = regexStart.exec(text); 103 | let resEnd; 104 | while (resStart) { 105 | regexEnd.lastIndex = regexStart.lastIndex; 106 | resEnd = regexEnd.exec(text); 107 | if (resEnd) { 108 | const m = new ABLMethod(); 109 | try { 110 | m.name = resStart[2]; 111 | m.lineAt = sourceCode.document.positionAt(resStart.index).line; 112 | m.lineEnd = sourceCode.document.positionAt(regexEnd.lastIndex).line; 113 | m.params = []; 114 | result.push(m); 115 | // tslint:disable-next-line:no-empty 116 | } catch { } // suppress errors 117 | resStart = regexStart.exec(text); 118 | } else { 119 | break; 120 | } 121 | } 122 | return result; 123 | } 124 | 125 | export function getAllParameters(sourceCode: SourceCode): ABLParameter[] { 126 | const result: ABLParameter[] = []; 127 | /* Primitive types */ 128 | // let regexParams: RegExp = new RegExp(/\b(?:def|define){1}[\s\t\n]+([inputo\-]*){1}[\s\t\n]+(?:param|parameter){1}[\s\t\n]+([\w\d\-\.]*){1}[\s\t\n]+(as|like){1}[\s\t\n]+([\w\d\-\.]+)/gim); 129 | let regexParams: RegExp = new RegExp(/\b(?:def|define){1}[\s\t\n]+([inputo\-]*){1}[\s\t\n]+(?:param|parameter){1}[\s\t\n]+([\w\d\-\.]*){1}[\s\t\n]+(as|like){1}[\s\t\n]+([\w\d\-\.]+)([\n\s\t\w\d\-\'\"]*)\./gim); 130 | // 1 = input | output | input-output 131 | // 2 = name 132 | // 3 = as | like 133 | // 4 = type | field like 134 | // 5 = details 135 | const text = sourceCode.sourceWithoutStrings; 136 | let res = regexParams.exec(text); 137 | while (res) { 138 | const v = new ABLParameter(); 139 | try { 140 | v.name = res[2].trim(); 141 | v.asLike = res[3].trim() as ABL_ASLIKE; 142 | v.dataType = removeInvalidRightChar(res[4].trim()); // removeInvalidRightChar to remove special chars because is accepted in this capture group 143 | v.line = sourceCode.document.positionAt(res.index).line; 144 | if (res[1].toLowerCase() === 'input') { 145 | v.direction = ABL_PARAM_DIRECTION.IN; 146 | } else if (res[1].toLowerCase() === 'output') { 147 | v.direction = ABL_PARAM_DIRECTION.OUT; 148 | } else { 149 | v.direction = ABL_PARAM_DIRECTION.INOUT; 150 | } 151 | v.additional = (res[5] || '').trim(); 152 | result.push(v); 153 | // tslint:disable-next-line:no-empty 154 | } catch { } // suppress errors 155 | res = regexParams.exec(text); 156 | } 157 | /* Temp-table */ 158 | regexParams = new RegExp(/\b(?:def|define){1}[\s\t\n]+([inputo\-]*){1}[\s\t\n]+(?:param|parameter){1}[\s\t\n]+(?:table){1}[\s\t\n]+(?:for){1}[\s\t\n]+([\w\d\-\+]*)(?:\.[^\w\d\-\+]){1}/gim); 159 | // 1 = input | output | input-output 160 | // 2 = name 161 | res = regexParams.exec(text); 162 | while (res) { 163 | const v = new ABLParameter(); 164 | try { 165 | v.name = res[2].trim(); 166 | v.asLike = ABL_ASLIKE.AS; 167 | v.dataType = 'temp-table'; 168 | v.line = sourceCode.document.positionAt(res.index).line; 169 | if (res[1].toLowerCase() === 'input') { 170 | v.direction = ABL_PARAM_DIRECTION.IN; 171 | } else if (res[1].toLowerCase() === 'output') { 172 | v.direction = ABL_PARAM_DIRECTION.OUT; 173 | } else { 174 | v.direction = ABL_PARAM_DIRECTION.INOUT; 175 | } 176 | result.push(v); 177 | } catch { } // suppress errors 178 | res = regexParams.exec(text); 179 | } 180 | /* Buffer */ 181 | regexParams = new RegExp(/\b(?:def|define){1}[\s\t\n]+(?:param|parameter){1}[\s\t\n]+(?:buffer){1}[\s\t\n]+([\w\d\-]+){1}[\s\t\n]+(?:for){1}[\s\t\n]+(temp-table[\s\t\n]+)*([\w\d\-\+]*)(?:\.[^\w\d\-\+])+/gim); 182 | // 1 = name 183 | // 2 = undefined | temp-table 184 | // 3 = buffer reference 185 | res = regexParams.exec(text); 186 | while (res) { 187 | const v = new ABLParameter(); 188 | try { 189 | v.name = res[1].trim(); 190 | v.asLike = ABL_ASLIKE.AS; 191 | v.dataType = 'buffer'; 192 | v.line = sourceCode.document.positionAt(res.index).line; 193 | v.direction = ABL_PARAM_DIRECTION.IN; 194 | v.additional = res[3]; 195 | result.push(v); 196 | } catch { } // suppress errors 197 | res = regexParams.exec(text); 198 | } 199 | // 200 | return result.sort((v1, v2) => { 201 | return v1.line - v2.line; 202 | }); 203 | } 204 | 205 | export function getAllTempTables(sourceCode: SourceCode): ABLTempTable[] { 206 | const result: ABLTempTable[] = []; 207 | // let regexTT: RegExp = new RegExp(/(?:def|define){1}(?:[\s\t\n]|new|global|shared)+(?:temp-table){1}[\s\t\n\r]+([\w\d\-]*)[\s\t\n\r]+([\w\W]*?)(?:\.(?!\w))/gim); 208 | const regexStart: RegExp = new RegExp(/\b(?:def|define){1}(?:[\s\t\n]|new|global|shared)+(?:temp-table){1}[\s\t\n\r]+([\w\d\-\+]*)[^\w\d\-\+]/gim); 209 | // 1 = name 210 | const regexEnd: RegExp = new RegExp(/\.[^\w\d\-\+]/gim); 211 | // 212 | const regexLike: RegExp = new RegExp(/\b(?:like){1}[\s\t\n]+([\w\d\-\+]+)[\s\t\n]*(?:\.[^\w\d\-\+]+|field|index|[\s\t\n\r])(?!field|index)/gim); 213 | // 1 = temp-table like 214 | const text = sourceCode.sourceWithoutStrings; 215 | let innerText; 216 | let resStart = regexStart.exec(text); 217 | let resEnd; 218 | let resLike; 219 | while (resStart) { 220 | regexEnd.lastIndex = regexStart.lastIndex; 221 | resEnd = regexEnd.exec(text); 222 | if (resEnd) { 223 | innerText = text.substring(regexStart.lastIndex, resEnd.index); 224 | const v = new ABLTempTable(); 225 | try { 226 | regexLike.lastIndex = regexStart.lastIndex; 227 | resLike = regexLike.exec(text); 228 | if ((resLike) && (resLike.index <= regexEnd.lastIndex) && (resLike.index >= regexStart.lastIndex)) { 229 | v.referenceTable = resLike[1]; 230 | } 231 | 232 | v.label = resStart[1]; 233 | v.kind = vscode.CompletionItemKind.Struct; 234 | v.detail = ''; 235 | v.fields = getTempTableFields(innerText, sourceCode); 236 | v.indexes = getTempTableIndexes(innerText); 237 | v.line = sourceCode.document.positionAt(resStart.index).line; 238 | updateTableCompletionList(v); 239 | result.push(v); 240 | } catch { } // suppress errors 241 | resStart = regexStart.exec(text); 242 | } else { 243 | break; 244 | } 245 | } 246 | return result; 247 | } 248 | 249 | function getTempTableFields(text: string, sourceCode: SourceCode): ABLVariable[] { 250 | const result: ABLVariable[] = []; 251 | const regexDefineField: RegExp = new RegExp(/(?:field){1}(?:[\s\t\n]+)([\w\d\-]+)[\s\t\n]+(as|like){1}[\s\t\n]+([\w\d\-\.]+)/gim); 252 | // 1 = var name 253 | // 2 = as | like 254 | // 3 = type | field like 255 | let res = regexDefineField.exec(text); 256 | while (res) { 257 | const v: ABLVariable = new ABLVariable(); 258 | try { 259 | v.name = res[1].trim(); 260 | v.asLike = res[2].trim() as ABL_ASLIKE; 261 | v.dataType = removeInvalidRightChar(res[3].trim()); // removeInvalidRightChar to remove special chars because is accepted in this capture group 262 | v.line = sourceCode.document.positionAt(res.index).line; 263 | result.push(v); 264 | } catch { } // suppress errors 265 | res = regexDefineField.exec(text); 266 | } 267 | return result; 268 | } 269 | 270 | function getTempTableIndexes(text: string): ABLIndexDefinition[] { 271 | return []; 272 | } 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openedge-abl", 3 | "publisher": "chriscamicas", 4 | "version": "1.3.0", 5 | "description": "OpenEdge ABL language support for VS Code.", 6 | "displayName": "OpenEdge ABL", 7 | "author": "chriscamicas", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/chriscamicas/vscode-abl.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/chriscamicas/openedge-abl-syntax/issues" 15 | }, 16 | "scripts": { 17 | "build": "vsce package", 18 | "vscode:prepublish": "npm run compile", 19 | "compile": "tsc -p ./", 20 | "watch": "tsc -watch -p ./", 21 | "pretest": "npm run compile", 22 | "test": "mocha -u tdd ./out/test", 23 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx" 24 | }, 25 | "icon": "images/progress_icon.png", 26 | "engines": { 27 | "vscode": "^1.52.0" 28 | }, 29 | "categories": [ 30 | "Programming Languages", 31 | "Snippets", 32 | "Debuggers", 33 | "Formatters" 34 | ], 35 | "activationEvents": [ 36 | "onLanguage:abl", 37 | "onDebug" 38 | ], 39 | "main": "./out/src/main", 40 | "contributes": { 41 | "languages": [ 42 | { 43 | "id": "abl", 44 | "aliases": [ 45 | "OpenEdge ABL", 46 | "abl", 47 | "Progress", 48 | "Progress 4GL" 49 | ], 50 | "extensions": [ 51 | ".w", 52 | ".p", 53 | ".i", 54 | ".cls" 55 | ], 56 | "configuration": "./language-configuration.json" 57 | } 58 | ], 59 | "grammars": [ 60 | { 61 | "language": "abl", 62 | "scopeName": "source.abl", 63 | "path": "./node_modules/abl-tmlanguage/abl.tmLanguage.json" 64 | } 65 | ], 66 | "snippets": [ 67 | { 68 | "language": "abl", 69 | "path": "./snippets/abl.json" 70 | } 71 | ], 72 | "commands": [ 73 | { 74 | "command": "abl.run.currentFile", 75 | "title": "ABL: Run", 76 | "description": "Run the current .p file" 77 | }, 78 | { 79 | "command": "abl.checkSyntax", 80 | "title": "ABL: CheckSyntax", 81 | "description": "Check the syntax for the current .p file" 82 | }, 83 | { 84 | "command": "abl.test", 85 | "title": "ABL: Test", 86 | "description": "Run all UnitTest" 87 | }, 88 | { 89 | "command": "abl.test.currentFile", 90 | "title": "ABL: Test current file", 91 | "description": "Run current UnitTest file" 92 | }, 93 | { 94 | "command": "abl.dictionary.dumpDefinition", 95 | "title": "ABL: Read Dictionary Structure", 96 | "description": "Read the DataDictionary structure for further use" 97 | }, 98 | { 99 | "command": "abl.propath", 100 | "title": "ABL: Current PROPATH", 101 | "description": "See the currently set PROPATH." 102 | }, 103 | { 104 | "command": "abl.dataDictionary", 105 | "title": "ABL: Open DataDictionary", 106 | "description": "Open the DataDictionary external tool" 107 | } 108 | ], 109 | "breakpoints": [ 110 | { 111 | "language": "abl" 112 | } 113 | ], 114 | "debuggers": [ 115 | { 116 | "type": "abl", 117 | "label": "ABL", 118 | "program": "./out/src/debugAdapter/ablDebug.js", 119 | "runtime": "node", 120 | "languages": [ 121 | "abl" 122 | ], 123 | "configurationSnippets": [ 124 | { 125 | "label": "ABL: Attach", 126 | "description": "Attach to a debug-ready process", 127 | "body": { 128 | "name": "${2:Attach to process}", 129 | "type": "abl", 130 | "request": "attach", 131 | "address": "TCP/IP address of process to be debugged", 132 | "port": 3099, 133 | "localRoot": "^\"\\${workspaceFolder}\"", 134 | "remoteRoot": "path to remote files", 135 | "sourceMap": { 136 | "Z:\\\\path\\\\to\\\\remote\\\\files\\\\**": "c:\\\\path\\\\to\\\\local\\\\src\\\\**" 137 | } 138 | } 139 | }, 140 | { 141 | "label": "ABL: Launch Program", 142 | "description": "Debug the current file", 143 | "body": { 144 | "name": "${2:Launch program}", 145 | "type": "abl", 146 | "request": "launch", 147 | "program": "^\"${1:\\${file}}\"", 148 | "cwd": "^\"\\${workspaceFolder}\"" 149 | } 150 | } 151 | ], 152 | "configurationAttributes": { 153 | "launch": { 154 | "required": [ 155 | "program" 156 | ], 157 | "properties": { 158 | "stopOnEntry": { 159 | "type": "boolean", 160 | "description": "Automatically stop the program at the first line executed.", 161 | "default": false 162 | }, 163 | "port": { 164 | "type": "number", 165 | "description": "The port that the debugger will be listening on.", 166 | "default": 3099 167 | }, 168 | "program": { 169 | "type": "string", 170 | "description": "The program to start.", 171 | "default": "${file}" 172 | }, 173 | "cwd": { 174 | "type": "string", 175 | "description": "The working directory.", 176 | "default": "${workspaceFolder}" 177 | }, 178 | "sourceMap": { 179 | "type": "object", 180 | "description": "source mapping for remote debugging", 181 | "default": { 182 | "Z:\\path\\to\\remote\\files\\**": "c:\\path\\to\\local\\src\\**" 183 | } 184 | } 185 | } 186 | }, 187 | "attach": { 188 | "required": [ 189 | "address", 190 | "port" 191 | ], 192 | "properties": { 193 | "port": { 194 | "type": "number", 195 | "description": "The port that the debugger will be listening on.", 196 | "default": 3099 197 | }, 198 | "address": { 199 | "type": "string", 200 | "description": "The remote address the debugger will connect to.", 201 | "default": "127.0.0.1" 202 | }, 203 | "localRoot": { 204 | "type": "string", 205 | "description": "The root directory for local source", 206 | "default": "${workspaceFolder}" 207 | }, 208 | "remoteRoot": { 209 | "type": "string", 210 | "description": "The root directory for remote source", 211 | "default": "" 212 | }, 213 | "sourceMap": { 214 | "type": "object", 215 | "description": "source mapping for remote debugging", 216 | "default": { 217 | "Z:\\path\\to\\remote\\files\\**": "c:\\path\\to\\local\\src\\**" 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | ], 225 | "configuration": { 226 | "type": "object", 227 | "title": "ABL configuration", 228 | "properties": { 229 | "abl.checkSyntaxOnSave": { 230 | "type": "string", 231 | "enum": [ 232 | "file", 233 | "workspace", 234 | "off" 235 | ], 236 | "default": "file", 237 | "description": "On save, check the syntax fot eh current file, the workspace, or nothing at all." 238 | }, 239 | "abl.editorContextMenuCommands": { 240 | "type": "object", 241 | "properties": { 242 | "checkSyntax": { 243 | "type": "boolean", 244 | "default": true, 245 | "description": "If true, adds command to check syntax to the editor context menu" 246 | } 247 | } 248 | }, 249 | "abl.useProcedureEditorKeyBindings": { 250 | "type": "boolean", 251 | "default": true, 252 | "description": "If true, use the same shortcuts as the Progress Procedure Editor (F2, Shift+F2, ...)" 253 | }, 254 | "abl.warnConfigFile": { 255 | "type": "boolean", 256 | "default": true, 257 | "description": "Show a warning message if config file not found" 258 | }, 259 | "abl.checkProgressBinary": { 260 | "type": "boolean", 261 | "default": true, 262 | "description": "Check if Progress binaries are accessible" 263 | } 264 | } 265 | }, 266 | "menus": { 267 | "editor/context": [ 268 | { 269 | "when": "editorTextFocus && config.abl.editorContextMenuCommands.checkSyntax && resourceLangId == abl", 270 | "command": "abl.checkSyntax", 271 | "group": "ABL group 1" 272 | } 273 | ], 274 | "commandPalette": [ 275 | { 276 | "command": "abl.run.currentFile", 277 | "when": "editorLangId == 'abl'" 278 | }, 279 | { 280 | "command": "abl.checkSyntax", 281 | "when": "editorLangId == 'abl'" 282 | }, 283 | { 284 | "command": "abl.test", 285 | "when": "editorLangId == 'abl'" 286 | }, 287 | { 288 | "command": "abl.test.currentFile", 289 | "when": "editorLangId == 'abl'" 290 | } 291 | ] 292 | }, 293 | "jsonValidation": [ 294 | { 295 | "fileMatch": ".openedge.json", 296 | "url": "./schemas/openedge.schema.json" 297 | } 298 | ], 299 | "keybindings": [ 300 | { 301 | "when": "editorTextFocus && config.abl.useProcedureEditorKeyBindings && resourceLangId == abl", 302 | "command": "abl.checkSyntax", 303 | "key": "Shift+F2" 304 | }, 305 | { 306 | "when": "editorTextFocus && config.abl.useProcedureEditorKeyBindings && resourceLangId == abl", 307 | "command": "abl.run.currentFile", 308 | "key": "F2" 309 | } 310 | ] 311 | }, 312 | "dependencies": { 313 | "abl-tmlanguage": "^1.2.0", 314 | "glob": "^7.1.6", 315 | "jsonminify": "^0.4.1", 316 | "minimatch": "^3.0.4", 317 | "rimraf": "^3.0.1", 318 | "util.promisify": "^1.0.1", 319 | "@vscode/debugadapter": "^1.51.1", 320 | "@vscode/debugprotocol": "^1.51.0", 321 | "vscode-languageclient": "^7.0.0", 322 | "xml2js": "^0.4.23" 323 | }, 324 | "devDependencies": { 325 | "@types/glob": "^7.1.1", 326 | "@types/mocha": "^9.0.0", 327 | "@types/node": "^13.7.0", 328 | "@types/vscode": "^1.52.0", 329 | "@typescript-eslint/eslint-plugin": "^5.10.0", 330 | "@typescript-eslint/parser": "^5.10.0", 331 | "eslint": "^8.7.0", 332 | "mocha": "^9.1.3", 333 | "typescript": "^3.7.5", 334 | "vscode-test": "^1.3.0" 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/parser/documentController.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { isNullOrUndefined } from 'util'; 3 | import * as vscode from 'vscode'; 4 | import { ABL_MODE } from '../ablMode'; 5 | import { ABL_PARAM_DIRECTION, ABLInclude, ABLMethod, ABLParameter, ABLSymbol, ABLTempTable, ABLVariable, SYMBOL_TYPE, TextSelection } from '../misc/definition'; 6 | import * as utils from '../misc/utils'; 7 | import { getTableCollection } from '../providers/ablCompletionProvider'; 8 | import { getAllBuffers, getAllIncludes, getAllMethods, getAllParameters, getAllTempTables, getAllVariables } from './processDocument'; 9 | import { SourceCode, SourceParser } from './sourceParser'; 10 | 11 | let thisInstance: ABLDocumentController; 12 | export function getDocumentController(): ABLDocumentController { 13 | return thisInstance; 14 | } 15 | export function initDocumentController(context: vscode.ExtensionContext): ABLDocumentController { 16 | thisInstance = new ABLDocumentController(context); 17 | return thisInstance; 18 | } 19 | 20 | export class ABLDocument { 21 | 22 | public get symbols(): vscode.SymbolInformation[] { return this._symbols; } 23 | public get methods(): ABLMethod[] { return this._methods; } 24 | public get includes(): ABLInclude[] { return this._includes; } 25 | public get tempTables(): ABLTempTable[] { return this._temps; } 26 | public get document(): vscode.TextDocument { return this._document; } 27 | public get processed(): boolean { return this._processed; } 28 | 29 | public disposables: vscode.Disposable[] = []; 30 | public debounceController; 31 | public externalDocument: vscode.TextDocument[] = []; 32 | private _document: vscode.TextDocument; 33 | private _symbols: vscode.SymbolInformation[]; 34 | private _vars: ABLVariable[]; 35 | private _methods: ABLMethod[]; 36 | private _includes: ABLInclude[]; 37 | private _temps: ABLTempTable[]; 38 | 39 | private _processed: boolean; 40 | 41 | constructor(document: vscode.TextDocument) { 42 | this._document = document; 43 | this._symbols = []; 44 | this._vars = []; 45 | this._methods = []; 46 | this._includes = []; 47 | this._temps = []; 48 | this._processed = false; 49 | } 50 | 51 | public dispose() { 52 | vscode.Disposable.from(...this.disposables).dispose(); 53 | } 54 | 55 | // tslint:disable-next-line:ban-types 56 | public getMap(): Object { 57 | if (this._processed) { 58 | // remove "completion" items from temp-table map 59 | const tt = this._temps.map((item) => { 60 | return Object.assign({}, item, { completion: undefined, completionFields: undefined, completionIndexes: undefined, completionAdditional: undefined }); 61 | }); 62 | const inc = this._includes.map((item) => { 63 | let r = Object.assign({}, item); 64 | const doc = vscode.workspace.textDocuments.find((d) => d.uri.fsPath === item.fsPath); 65 | if (doc) { 66 | const extDoc = getDocumentController().getDocument(doc); 67 | if (extDoc) { 68 | r = Object.assign(r, { map: extDoc.getMap() }); 69 | } 70 | } 71 | return r; 72 | }); 73 | 74 | return { 75 | external: this.externalDocument, 76 | includes: inc, 77 | methods: this._methods, 78 | tempTables: tt, 79 | variables: this._vars, 80 | }; 81 | } 82 | return; 83 | } 84 | 85 | public getCompletionTempTableFields(prefix: string, replacement?: string): vscode.CompletionItem[] { 86 | // Temp-tables 87 | const tt = this.tempTables.find((item) => item.label.toLowerCase() === prefix); 88 | if (tt) { 89 | let result = tt.completion.items; 90 | if (!isNullOrUndefined(replacement)) { 91 | result = utils.replaceSnippetTableName(result, prefix, replacement); 92 | } 93 | return result; 94 | } 95 | return []; 96 | } 97 | 98 | public getCompletionSymbols(position?: vscode.Position): vscode.CompletionItem[] { 99 | // Temp-tables 100 | const tt: vscode.CompletionItem[] = this._temps.map((item) => { 101 | return new vscode.CompletionItem(item.label); 102 | }); 103 | // Methods 104 | const md: vscode.CompletionItem[] = []; 105 | this._methods.forEach((m) => { 106 | const _mi = new vscode.CompletionItem(m.name, vscode.CompletionItemKind.Method); 107 | if (m.params.length > 0) { 108 | let pf = true; 109 | const snip: vscode.SnippetString = new vscode.SnippetString(); 110 | snip.appendText(m.name + '('); 111 | m.params.forEach((p) => { 112 | if (!pf) { 113 | snip.appendText(',\n\t'); 114 | } else { 115 | pf = false; 116 | } 117 | if (p.dataType === 'buffer') { 118 | snip.appendText('buffer '); 119 | } else { 120 | if (p.direction === ABL_PARAM_DIRECTION.IN) { 121 | snip.appendText('input '); 122 | } else if (p.direction === ABL_PARAM_DIRECTION.OUT) { 123 | snip.appendText('output '); 124 | } else { 125 | snip.appendText('input-output '); 126 | } 127 | if (p.dataType === 'temp-table') { 128 | snip.appendText('table '); 129 | } 130 | } 131 | snip.appendPlaceholder(p.name); 132 | }); 133 | snip.appendText(')'); 134 | _mi.insertText = snip; 135 | } 136 | md.push(_mi); 137 | }); 138 | // buffers 139 | const gb = this._vars.filter((v) => v.dataType === 'buffer').map((item) => { 140 | return new vscode.CompletionItem(item.name); 141 | }); 142 | // method buffers 143 | let lb = []; 144 | let lp = []; 145 | const mp = this.getMethodInPosition(position); 146 | if (!isNullOrUndefined(mp)) { 147 | lb = mp.localVars.filter((v) => v.dataType === 'buffer').map((item) => { 148 | return new vscode.CompletionItem(item.name); 149 | }); 150 | lp = mp.params.filter((v) => v.dataType === 'buffer').map((item) => { 151 | return new vscode.CompletionItem(item.name); 152 | }); 153 | } 154 | // 155 | return [...tt, ...md, ...gb, ...lb, ...lp]; 156 | } 157 | 158 | public pushDocumentSignal(document: ABLDocument) { 159 | if (this.processed) { 160 | const extDoc = this.externalDocument.find((item) => item === document.document); 161 | if (extDoc) { 162 | this.refreshExternalReferences(extDoc); 163 | } 164 | } 165 | } 166 | 167 | public refreshDocument(): Promise { 168 | this._processed = false; 169 | this.externalDocument = []; 170 | 171 | const refreshIncludes = this.refreshIncludes.bind(this); 172 | const refreshMethods = this.refreshMethods.bind(this); 173 | const refreshVariables = this.refreshVariables.bind(this); 174 | const refreshParameters = this.refreshParameters.bind(this); 175 | const refreshTempTables = this.refreshTempTables.bind(this); 176 | const refreshSymbols = this.refreshSymbols.bind(this); 177 | const self = this; 178 | 179 | const sourceCode = new SourceParser().getSourceCode(this._document); 180 | 181 | const result = new Promise((resolve, reject) => { 182 | refreshIncludes(sourceCode); 183 | refreshMethods(sourceCode); 184 | refreshVariables(sourceCode); 185 | refreshParameters(sourceCode); 186 | refreshTempTables(sourceCode); 187 | refreshSymbols(); 188 | resolve(self); 189 | }); 190 | 191 | // refresh temp-table "like" from other temp-tables (check if external document has been processed) 192 | // create procedure snippets with parameters 193 | 194 | // finaliza processo 195 | const finish = () => { 196 | this._processed = true; 197 | this.refreshExternalReferences(this._document); 198 | getDocumentController().broadcastDocumentChange(this); 199 | }; 200 | result.then(() => finish()); 201 | return result; 202 | } 203 | 204 | public refreshExternalReferences(document: vscode.TextDocument) { 205 | // temp-tables 206 | this.tempTables.filter((item) => item.referenceTable).forEach((item) => { 207 | const fields = this.getDeclaredTempTableFields(item.referenceTable, document); 208 | if (fields) { 209 | item.referenceFields = fields; 210 | utils.updateTableCompletionList(item); 211 | } 212 | }); 213 | } 214 | 215 | public getDeclaredTempTableFields(filename: string, changedDocument?: vscode.TextDocument): ABLVariable[] { 216 | const name = filename.toLowerCase(); 217 | const tt = this._temps.find((item) => item.label.toLowerCase() === name); 218 | if (tt) { 219 | return tt.fields; 220 | } 221 | // 222 | let items; 223 | if ((changedDocument) && (this.externalDocument.find((item) => item === changedDocument))) { 224 | const extDoc = getDocumentController().getDocument(changedDocument); 225 | if ((extDoc) && (extDoc.processed)) { 226 | items = extDoc.getDeclaredTempTableFields(filename); 227 | } 228 | } 229 | if (items) { 230 | return items; 231 | } 232 | return; 233 | } 234 | 235 | public getMethodInPosition(position?: vscode.Position): ABLMethod { 236 | if (!isNullOrUndefined(position)) { 237 | return this._methods.find((item) => { 238 | return (item.lineAt <= position.line) && (item.lineEnd >= position.line); 239 | }); 240 | } 241 | return; 242 | } 243 | 244 | public searchBuffer(name: string, position?: vscode.Position): string { 245 | // method buffers 246 | const m = this.getMethodInPosition(position); 247 | if (!isNullOrUndefined(m)) { 248 | const lb = m.localVars.filter((v) => v.dataType === 'buffer').find((v) => v.name.toLowerCase() === name.toLowerCase()); 249 | if (!isNullOrUndefined(lb)) { 250 | return lb.additional.toLowerCase(); 251 | } 252 | const lp = m.params.filter((v) => v.dataType === 'buffer').find((v) => v.name.toLowerCase() === name.toLowerCase()); 253 | if (!isNullOrUndefined(lp)) { 254 | return lp.additional.toLowerCase(); 255 | } 256 | } 257 | const res = this._vars.filter((v) => v.dataType === 'buffer').find((v) => v.name.toLowerCase() === name.toLowerCase()); 258 | if (!isNullOrUndefined(res)) { 259 | return res.additional.toLowerCase(); 260 | } 261 | return; 262 | } 263 | 264 | public searchSymbol(words: string[], selectedWord?: string, position?: vscode.Position, deepSearch?: boolean): ABLSymbol { 265 | selectedWord = ('' || selectedWord).toLowerCase(); 266 | let location: vscode.Location; 267 | if ((words.length === 1) || ((words.length > 0) && (words[0].toLowerCase() === selectedWord))) { 268 | const word = words[0].toLowerCase(); 269 | 270 | // temp-table 271 | const tt = this._temps.find((item) => item.label.toLowerCase() === word); 272 | if (!isNullOrUndefined(tt)) { 273 | location = new vscode.Location(this.document.uri, new vscode.Position(tt.line, 0)); 274 | return { type: SYMBOL_TYPE.TEMPTABLE, value: tt, location }; 275 | } 276 | 277 | // method 278 | let mt = this._methods.find((item) => item.name.toLowerCase() === word); 279 | if (!isNullOrUndefined(mt)) { 280 | location = new vscode.Location(this.document.uri, new vscode.Position(mt.lineAt, 0)); 281 | return { type: SYMBOL_TYPE.METHOD, value: mt, location }; 282 | } 283 | 284 | // local parameters / variables 285 | mt = this.getMethodInPosition(position); 286 | if (mt) { 287 | const lp = mt.params.find((item) => item.name.toLowerCase() === word); 288 | if (!isNullOrUndefined(lp)) { 289 | location = new vscode.Location(this.document.uri, new vscode.Position(lp.line, 0)); 290 | return { type: SYMBOL_TYPE.LOCAL_PARAM, value: lp, origin: mt, location }; 291 | } 292 | const lv = mt.localVars.find((item) => item.name.toLowerCase() === word); 293 | if (!isNullOrUndefined(lv)) { 294 | location = new vscode.Location(this.document.uri, new vscode.Position(lv.line, 0)); 295 | return { type: SYMBOL_TYPE.LOCAL_VAR, value: lv, origin: mt, location }; 296 | } 297 | } 298 | 299 | // variables 300 | const gv = this._vars.find((item) => item.name.toLowerCase() === word); 301 | if (!isNullOrUndefined(gv)) { 302 | location = new vscode.Location(this.document.uri, new vscode.Position(gv.line, 0)); 303 | return { type: SYMBOL_TYPE.GLOBAL_VAR, value: gv, location }; 304 | } 305 | } else if (words.length > 1) { 306 | const word0 = words[0].toLowerCase(); 307 | const word1 = words[1].toLowerCase(); 308 | // temp-table 309 | const tt = this._temps.find((item) => item.label.toLowerCase() === word0); 310 | if (!isNullOrUndefined(tt)) { 311 | const fd = tt.fields.find((item) => item.name.toLowerCase() === word1); 312 | if (fd) { 313 | location = new vscode.Location(this.document.uri, new vscode.Position(tt.line, 0)); 314 | return { type: SYMBOL_TYPE.TEMPTABLE_FIELD, value: fd, origin: tt, location }; 315 | } else { 316 | return; 317 | } 318 | } 319 | } 320 | 321 | // External documents 322 | if (deepSearch) { 323 | let extSym; 324 | this.externalDocument.forEach((external) => { 325 | if (isNullOrUndefined(extSym)) { 326 | const extDoc = getDocumentController().getDocument(external); 327 | if ((extDoc) && (extDoc.processed)) { 328 | extSym = extDoc.searchSymbol(words, selectedWord, position, deepSearch); 329 | } 330 | } 331 | }); 332 | if (!isNullOrUndefined(extSym)) { 333 | return extSym; 334 | } 335 | } 336 | 337 | return; 338 | } 339 | 340 | private insertExternalDocument(doc: vscode.TextDocument) { 341 | this.externalDocument.push(doc); 342 | this.refreshExternalReferences(doc); 343 | } 344 | 345 | private refreshIncludes(sourceCode: SourceCode) { 346 | this._includes = getAllIncludes(sourceCode); 347 | this._includes.forEach((item) => { 348 | vscode.workspace.workspaceFolders.forEach((folder) => { 349 | const uri = folder.uri.with({ path: [folder.uri.path, item.name].join('/') }); 350 | if (fs.existsSync(uri.fsPath)) { 351 | item.fsPath = uri.fsPath; 352 | if (!this.externalDocument.find((i) => i.uri.fsPath === uri.fsPath)) { 353 | vscode.workspace.openTextDocument(uri).then((doc) => this.insertExternalDocument(doc)); 354 | } 355 | } 356 | }); 357 | }); 358 | } 359 | 360 | private refreshMethods(sourceCode: SourceCode) { 361 | this._methods = getAllMethods(sourceCode); 362 | this.resolveMethodConflicts(); 363 | } 364 | 365 | private resolveMethodConflicts() { 366 | // adjust method start/end lines (missing "procedure" on "end [procedure]") 367 | let _prevMethod: ABLMethod; 368 | this._methods.forEach((method) => { 369 | if (!isNullOrUndefined(_prevMethod)) { 370 | if (method.lineAt < _prevMethod.lineEnd) { 371 | _prevMethod.lineEnd = method.lineAt - 1; 372 | } 373 | } 374 | _prevMethod = method; 375 | }); 376 | } 377 | 378 | private refreshSymbols() { 379 | this._symbols = []; 380 | // add methods 381 | this._methods.forEach((item) => { 382 | const range: vscode.Range = new vscode.Range(new vscode.Position(item.lineAt, 0), new vscode.Position(item.lineEnd, 0)); 383 | const sym = new vscode.SymbolInformation(item.name, vscode.SymbolKind.Function, range, this._document.uri, SYMBOL_TYPE.METHOD); 384 | this._symbols.push(sym); 385 | }); 386 | } 387 | 388 | private refreshVariables(sourceCode: SourceCode) { 389 | this._vars = []; 390 | const _vars = [].concat(getAllVariables(sourceCode)).concat(getAllBuffers(sourceCode)); 391 | 392 | if (!isNullOrUndefined(_vars) && !isNullOrUndefined(this._methods)) { 393 | _vars.forEach((item) => { 394 | const method = this._methods.find((m) => (m.lineAt <= item.line && m.lineEnd >= item.line)); 395 | if (method) { 396 | method.localVars.push(item); 397 | } else { 398 | this._vars.push(item); 399 | } 400 | }); 401 | } 402 | } 403 | 404 | private refreshParameters(sourceCode: SourceCode) { 405 | const _params = getAllParameters(sourceCode); 406 | _params.forEach((item) => { 407 | const method = this._methods.find((m) => (m.lineAt <= item.line && m.lineEnd >= item.line)); 408 | if (method) { 409 | method.params.push(item); 410 | } 411 | }); 412 | } 413 | 414 | private refreshTempTables(sourceCode: SourceCode) { 415 | this._temps = getAllTempTables(sourceCode); 416 | // reference to db tables 417 | this._temps.filter((item) => !isNullOrUndefined(item.referenceTable)).forEach((item) => { 418 | const tb = getTableCollection().items.find((tn) => tn.label.toString().toLowerCase() === item.referenceTable.toLowerCase()); 419 | // tslint:disable-next-line:no-string-literal 420 | if ((!isNullOrUndefined(tb)) && (!isNullOrUndefined(tb['fields']))) { 421 | // tslint:disable-next-line:no-string-literal 422 | item.referenceFields = [...tb['fields']]; 423 | utils.updateTableCompletionList(item); 424 | } 425 | }); 426 | } 427 | 428 | } 429 | 430 | export class ABLDocumentController { 431 | 432 | private _documents: ABLDocument[] = []; 433 | 434 | constructor(context: vscode.ExtensionContext) { 435 | this.initialize(context); 436 | } 437 | 438 | public dispose() { 439 | this._documents.forEach((d) => d.dispose()); 440 | } 441 | 442 | public insertDocument(document: vscode.TextDocument) { 443 | if (document.languageId === ABL_MODE.language) { 444 | if (!this._documents[document.uri.fsPath]) { 445 | const ablDoc = new ABLDocument(document); 446 | this._documents[document.uri.fsPath] = ablDoc; 447 | 448 | vscode.workspace.onDidChangeTextDocument((event) => { 449 | if (event.document.uri.fsPath === document.uri.fsPath) { 450 | this.updateDocument(document, 5000); 451 | } 452 | }, this, ablDoc.disposables); 453 | } 454 | return this.updateDocument(document); 455 | } 456 | 457 | } 458 | 459 | public removeDocument(document: vscode.TextDocument) { 460 | const d: ABLDocument = this._documents[document.uri.fsPath]; 461 | if (d) { 462 | if (d.debounceController) { 463 | clearTimeout(d.debounceController); 464 | d.debounceController = null; 465 | } 466 | vscode.Disposable.from(...d.disposables).dispose(); 467 | } 468 | delete this._documents[document.uri.fsPath]; 469 | } 470 | 471 | public updateDocument(document: vscode.TextDocument, debounceTime?: number): Thenable { 472 | if (document.languageId === ABL_MODE.language) { 473 | const ablDoc: ABLDocument = this._documents[document.uri.fsPath]; 474 | const invoke = this.invokeUpdateDocument; 475 | return new Promise((resolve, reject) => { 476 | if (ablDoc) { 477 | // cancel any pending update request 478 | if (ablDoc.debounceController) { 479 | clearTimeout(ablDoc.debounceController); 480 | } 481 | // if debouce time is set, creates a timer 482 | if (debounceTime) { 483 | ablDoc.debounceController = setTimeout(() => invoke(ablDoc), debounceTime); 484 | } else { 485 | invoke(ablDoc); 486 | } 487 | // always resolve, even if debounce time is set... 488 | resolve(); 489 | } else { 490 | reject(); 491 | } 492 | }); 493 | } 494 | } 495 | 496 | public prepareToSaveDocument(document: vscode.TextDocument) { 497 | // 498 | } 499 | 500 | public getDocument(document: vscode.TextDocument): ABLDocument { 501 | return this._documents[document.uri.fsPath]; 502 | } 503 | 504 | public broadcastDocumentChange(ablDoc: ABLDocument) { 505 | for (const item in this._documents) { 506 | if (item !== ablDoc.document.uri.fsPath) { 507 | this._documents[item].pushDocumentSignal(ablDoc); 508 | } 509 | } 510 | } 511 | 512 | private initialize(context: vscode.ExtensionContext) { 513 | context.subscriptions.push(this); 514 | 515 | // Current documents 516 | vscode.workspace.textDocuments.forEach((document) => { 517 | this.insertDocument(document); 518 | }); 519 | 520 | // Document changes 521 | vscode.workspace.onDidSaveTextDocument((document) => { this.updateDocument(document); }, null, context.subscriptions); 522 | vscode.workspace.onDidOpenTextDocument((document) => { this.insertDocument(document); }, null, context.subscriptions); 523 | vscode.workspace.onDidCloseTextDocument((document) => { this.removeDocument(document); }, null, context.subscriptions); 524 | vscode.workspace.onWillSaveTextDocument((event) => { this.prepareToSaveDocument(event.document); }, null, context.subscriptions); 525 | } 526 | 527 | private invokeUpdateDocument(ablDoc: ABLDocument) { 528 | ablDoc.refreshDocument(); 529 | } 530 | 531 | } 532 | -------------------------------------------------------------------------------- /src/providers/ablFormattingProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { CancellationToken, DocumentFormattingEditProvider, FormattingOptions, OnTypeFormattingEditProvider, Position, Range, TextDocument, TextEdit, workspace, WorkspaceConfiguration } from 'vscode'; 3 | import { getOpenEdgeConfig } from '../ablConfig'; 4 | import { ABL_MODE } from '../ablMode'; 5 | 6 | export class ABLFormattingProvider implements DocumentFormattingEditProvider, OnTypeFormattingEditProvider { 7 | 8 | constructor(context: vscode.ExtensionContext) { 9 | context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(ABL_MODE.language, this)); 10 | } 11 | 12 | public provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): Thenable { 13 | if (document.languageId !== ABL_MODE.language) { return; } 14 | return format(document, null, options); 15 | } 16 | 17 | public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): Thenable { 18 | // if (!onType) { return; } 19 | if (document.languageId !== ABL_MODE.language) { return; } 20 | return format(document, null, options); 21 | } 22 | } 23 | 24 | function format(document: TextDocument, range: Range, options: FormattingOptions): Thenable { 25 | return new Promise((resolve) => { 26 | // Create an empty list of changes 27 | const result: TextEdit[] = []; 28 | // Create a full document range 29 | if (range === null) { 30 | const start = new Position(0, 0); 31 | const end = new Position(document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length); 32 | range = new Range(start, end); 33 | } 34 | // Format the document with the user specified settings 35 | // var newText: string = PatternFormat.document(document.getText(), options, document.languageId); 36 | SpacingFormat.document(document.getText(), options, document.languageId).then((newText) => { 37 | // Push the edit into the result array 38 | result.push(new TextEdit(range, newText)); 39 | // Return the result of the change 40 | return resolve(result); 41 | }); 42 | }); 43 | } 44 | 45 | class PatternFormat { 46 | 47 | public static document(source: string, formattingOptions: FormattingOptions, languageId: string): string { 48 | const config: WorkspaceConfiguration = workspace.getConfiguration('ablFormat'); 49 | this.options = formattingOptions; 50 | this.source = source; 51 | this.langId = languageId; 52 | 53 | // Config base 54 | const space = config.get('space'); 55 | const newLine = config.get('newLine'); 56 | 57 | this.space = space; 58 | this.newLine = newLine; 59 | 60 | const spaceOther = space.language[languageId]; 61 | 62 | const braceSpaceOpenBefore = space.brace.open.before; 63 | const braceNewLine = newLine.brace; 64 | 65 | const parenSpaceOpenBefore = space.parenthesis.open.before; 66 | const parenSpaceOpenAfter = space.parenthesis.open.after; 67 | const parenSpaceCloseBefore = space.parenthesis.close.before; 68 | 69 | let s: string = ''; 70 | 71 | let ignoreSpace = false; 72 | let lastKeyword = ''; 73 | 74 | let inString: boolean = false; 75 | let inComment: boolean = false; 76 | let commentType: CommentType = null; 77 | 78 | let stringChar = null; 79 | // var textWords = ''; 80 | 81 | let line = ''; 82 | let depthLineDiff = 0; 83 | 84 | for (let i = 0; i < source.length; i++) { 85 | 86 | this.offset = i; 87 | this.char = source[i]; 88 | this.next = source[i + 1]; 89 | this.prev = source[i - 1]; 90 | this.words = this.cleanArray(line.split(/[\s()[\];|'"{}.\t\n]/)); 91 | // this.words = this.cleanArray(s.split(/[\s\(\)\[\];|'"\{\}\.\t\n]/)); 92 | // this.words = this.cleanArray(textWords.split(/[\s\(\)\[\];|'"\{\}\.\t\n]/)); 93 | this.last = this.words[this.words.length - 1]; 94 | 95 | const _char = this.char; 96 | // considera blocos do progress 97 | 98 | const spaces = this.getSpaces(_char); 99 | 100 | switch (this.char) { 101 | case '/': 102 | // If we are not in a comment 103 | if (!inComment && this.next === '/' || this.prev === '/') { 104 | inComment = true; 105 | commentType = CommentType.SingleLine; 106 | } else if (!inComment && this.next === '*') { 107 | inComment = true; 108 | commentType = CommentType.MultiLine; 109 | } else if (inComment && commentType === CommentType.MultiLine) { 110 | inComment = false; 111 | commentType = null; 112 | } 113 | // s += this.char; 114 | line += this.char; 115 | break; 116 | case '\n': 117 | if (inComment && commentType === CommentType.SingleLine) { 118 | inComment = false; 119 | commentType = null; 120 | } 121 | // s += this.char; 122 | s += this.indent(this.depth + depthLineDiff) + line.trim() + this.char; 123 | line = ''; 124 | depthLineDiff = 0; 125 | break; 126 | case '"': 127 | case '\'': 128 | if (stringChar === this.char && inString) { 129 | inString = false; 130 | stringChar = null; 131 | } else if (stringChar === null && !inString) { 132 | inString = true; 133 | stringChar = this.char; 134 | } 135 | // s += this.char; 136 | line += this.char; 137 | break; 138 | case ':': 139 | case '{': 140 | if (inString || inComment) { 141 | // s += this.char; 142 | line += this.char; 143 | break; 144 | } 145 | ignoreSpace = true; 146 | if (!braceNewLine) { 147 | let c = 0; 148 | for (const j in braceSpaceOpenBefore) { 149 | if (lastKeyword === j) { 150 | // s = s.trim(); 151 | // s += this.spacePlaceholder(braceSpaceOpenBefore[j]); 152 | // s = s.trim(); 153 | line = line.trim(); 154 | line += this.spacePlaceholder(braceSpaceOpenBefore[j]); 155 | line = line.trim(); 156 | c++; 157 | break; 158 | } 159 | } 160 | if (c === 0) { 161 | // s = s.trim(); 162 | // s += this.spacePlaceholder(braceSpaceOpenBefore.other); 163 | // s = s.trim(); 164 | line = line.trim(); 165 | line += this.spacePlaceholder(braceSpaceOpenBefore.other); 166 | line = line.trim(); 167 | } 168 | } else { 169 | // var lineStr: string = this.lineAtIndex(s, s.length).trim(); 170 | // if (lineStr != '') { 171 | // s += '\n' + this.indent(this.depth - 1); 172 | // } 173 | if (line.trim() !== '') { 174 | // s += '\n' + this.indent(this.depth - 1); 175 | // line += '\n' + this.indent(this.depth - 1); 176 | s += this.indent(this.depth + depthLineDiff) + line.trim() + '\n'; 177 | line = ''; 178 | } 179 | 180 | } 181 | this.depth++; 182 | depthLineDiff = -1; 183 | // s += this.char; 184 | line += this.char; 185 | break; 186 | case '}': 187 | if (inString || inComment) { 188 | // s += this.char; 189 | line += this.char; 190 | break; 191 | } 192 | ignoreSpace = true; 193 | this.depth--; 194 | // s += this.char; 195 | line += this.char; 196 | break; 197 | case '(': 198 | if (inString || inComment) { 199 | // s += this.char; 200 | line += this.char; 201 | break; 202 | } 203 | ignoreSpace = true; 204 | for (const j in parenSpaceOpenBefore) { 205 | if (this.last === j) { 206 | // s = s.trim(); 207 | // s += this.spacePlaceholder(parenSpaceOpenBefore[j]); 208 | // s = s.trim(); 209 | line = line.trim(); 210 | line += this.spacePlaceholder(parenSpaceOpenBefore[j]); 211 | line = line.trim(); 212 | lastKeyword = this.last; 213 | break; 214 | } 215 | } 216 | // s += this.char; 217 | line += this.char; 218 | for (const j in parenSpaceOpenAfter) { 219 | if (this.last === j) { 220 | // s = s.trim(); 221 | // s += this.spacePlaceholder(parenSpaceOpenAfter[j]); 222 | // s = s.trim(); 223 | line = line.trim(); 224 | line += this.spacePlaceholder(parenSpaceOpenAfter[j]); 225 | line = line.trim(); 226 | break; 227 | } 228 | } 229 | break; 230 | case ')': 231 | if (inString || inComment) { 232 | // s += this.char; 233 | line += this.char; 234 | break; 235 | } 236 | ignoreSpace = true; 237 | for (const j in parenSpaceCloseBefore) { 238 | if (lastKeyword === j) { 239 | // s = s.trim(); 240 | // s += this.spacePlaceholder(parenSpaceCloseBefore[j]); 241 | // s = s.trim(); 242 | line = line.trim(); 243 | line += this.spacePlaceholder(parenSpaceCloseBefore[j]); 244 | line = line.trim(); 245 | break; 246 | } 247 | } 248 | // s += this.char; 249 | line += this.char; 250 | break; 251 | case ',': 252 | // case ':': 253 | if (inString || inComment) { 254 | // s += this.char; 255 | line += this.char; 256 | break; 257 | } 258 | ignoreSpace = true; 259 | // s = this.formatItem(this.char, s, spaces); 260 | line = this.formatItem(this.char, line, spaces); 261 | break; 262 | case ';': 263 | if (inString || inComment) { 264 | // s += this.char; 265 | line += this.char; 266 | break; 267 | } 268 | ignoreSpace = true; 269 | // s = this.formatItem(this.char, s, spaces); 270 | line = this.formatItem(this.char, line, spaces); 271 | break; 272 | case '?': 273 | case '>': 274 | case '<': 275 | case '=': 276 | case '!': 277 | case '&': 278 | case '|': 279 | case '+': 280 | case '-': 281 | case '*': 282 | // case '/': 283 | // case '%': 284 | if (inString || inComment) { 285 | // s += this.char; 286 | line += this.char; 287 | break; 288 | } 289 | ignoreSpace = true; 290 | // s = this.formatOperator(this.char, s, spaces); 291 | line = this.formatOperator(this.char, line, spaces); 292 | break; 293 | default: 294 | if (spaceOther && this.char in spaceOther) { 295 | if (inString || inComment) { 296 | // s += this.char; 297 | line += this.char; 298 | break; 299 | } 300 | ignoreSpace = true; 301 | // s = this.formatItem(this.char, s, new Spaces((spaceOther[this.char].before || 0), (spaceOther[this.char].after || 0))); 302 | line = this.formatItem(this.char, line, new Spaces((spaceOther[this.char].before || 0), (spaceOther[this.char].after || 0))); 303 | } else { 304 | if (inString || inComment) { 305 | // s += this.char; 306 | line += this.char; 307 | break; 308 | } 309 | if (ignoreSpace && this.char === ' ') { 310 | // Skip 311 | } else { 312 | // s += this.char; 313 | line += this.char; 314 | ignoreSpace = false; 315 | } 316 | } 317 | break; 318 | } 319 | 320 | // ver se funciona... 321 | /*if (this.words.length > 1) { 322 | textWords = this.words[this.words.length-1] + s; 323 | }*/ 324 | } 325 | 326 | s += this.indent(this.depth) + line.trim(); 327 | s = s.replace(new RegExp(PatternFormat.spacePlaceholderStr, 'g'), ' '); 328 | 329 | return s; 330 | } 331 | protected static spacePlaceholderStr = '__VSCODE__SPACE__PLACEHOLDER__'; 332 | protected static depth: number = 0; 333 | protected static options: FormattingOptions; 334 | protected static source: string; 335 | protected static langId: string; 336 | protected static offset: number = 0; 337 | protected static prev: string = ''; 338 | protected static next: string = ''; 339 | protected static space; 340 | protected static newLine; 341 | protected static char; 342 | protected static last; 343 | protected static words: string[]; 344 | 345 | protected static languageOverride(char: string): Spaces { 346 | if (this.space.language[this.langId] && this.space.language[this.langId][char]) { 347 | return this.space.language[this.langId][char]; 348 | } 349 | return null; 350 | } 351 | 352 | protected static getSpaces(char: string): Spaces { 353 | const spaces: Spaces = new Spaces(); 354 | const config: WorkspaceConfiguration = workspace.getConfiguration('format'); 355 | switch (char) { 356 | case '&': 357 | spaces.before = config.get('space.and.before', 1); 358 | spaces.after = config.get('space.and.after', 1); 359 | break; 360 | case '|': 361 | spaces.before = config.get('space.or.before', 1); 362 | spaces.after = config.get('space.or.after', 1); 363 | break; 364 | case ',': 365 | spaces.before = config.get('space.comma.before', 1); 366 | spaces.after = config.get('space.comma.after', 1); 367 | break; 368 | case '>': 369 | spaces.before = config.get('space.greaterThan.before', 1); 370 | spaces.after = config.get('space.greaterThan.after', 1); 371 | break; 372 | case '<': 373 | spaces.before = config.get('space.lessThan.before', 1); 374 | spaces.after = config.get('space.lessThan.after', 1); 375 | break; 376 | case '=': 377 | spaces.before = config.get('space.equal.before', 1); 378 | spaces.after = config.get('space.equal.after', 1); 379 | break; 380 | case '!': 381 | spaces.before = config.get('space.not.before', 1); 382 | spaces.after = config.get('space.not.after', 1); 383 | break; 384 | case '?': 385 | spaces.before = config.get('space.question.before', 1); 386 | spaces.after = config.get('space.question.after', 1); 387 | break; 388 | case ':': 389 | spaces.before = config.get('space.colon.before', 1); 390 | spaces.after = config.get('space.colon.after', 1); 391 | break; 392 | case '-': 393 | if (this.next === '-' || this.prev === '-' || this.next.match(/\d/)) { 394 | spaces.before = config.get('space.decrement.before', 0); 395 | spaces.after = config.get('space.decrement.after', 0); 396 | } else { 397 | spaces.before = config.get('space.subtract.before', 1); 398 | spaces.after = config.get('space.subtract.after', 1); 399 | } 400 | break; 401 | case '+': 402 | if (this.next === '+' || this.prev === '+') { 403 | spaces.before = config.get('space.increment.before', 0); 404 | spaces.after = config.get('space.increment.after', 0); 405 | } else { 406 | spaces.before = config.get('space.add.before', 1); 407 | spaces.after = config.get('space.add.after', 1); 408 | } 409 | break; 410 | case ';': 411 | spaces.before = config.get('space.semicolon.before', 1); 412 | spaces.after = config.get('space.semicolon.after', 1); 413 | break; 414 | case '*': 415 | spaces.before = config.get('space.multiply.before', 1); 416 | spaces.after = config.get('space.multiply.after', 1); 417 | break; 418 | case '/': 419 | spaces.before = config.get('space.divide.before', 1); 420 | spaces.after = config.get('space.divide.after', 1); 421 | break; 422 | case '%': 423 | spaces.before = config.get('space.modulo.before', 1); 424 | spaces.after = config.get('space.modulo.after', 1); 425 | break; 426 | } 427 | return spaces; 428 | } 429 | 430 | protected static formatItem(char: string, s: string, spaces: Spaces): string { 431 | const override = this.languageOverride(char); 432 | if (override) { 433 | spaces = override; 434 | } 435 | s = s.trim(); 436 | s += PatternFormat.spacePlaceholderStr.repeat(spaces.before); 437 | s += char; 438 | s += PatternFormat.spacePlaceholderStr.repeat(spaces.after); 439 | return s.trim(); 440 | } 441 | 442 | protected static formatOperator(char: string, s: string, spaces: Spaces): string { 443 | const override = this.languageOverride(char); 444 | if (override) { 445 | spaces = override; 446 | } 447 | s = s.trim(); 448 | if (this.prev && this.notBefore(this.prev, '=', '!', '>', '<', '?', '%', '&', '|', '/')) { 449 | s += PatternFormat.spacePlaceholderStr.repeat(spaces.before); 450 | } 451 | s = s.trim(); 452 | s += char; 453 | s = s.trim(); 454 | if (this.next && this.notAfter(this.next, '=', '>', '<', '?', '%', '&', '|', '/')) { 455 | if (char !== '?' || this.source.substr(this.offset, 4) !== '?php') { 456 | s += PatternFormat.spacePlaceholderStr.repeat(spaces.after); 457 | } 458 | } 459 | return s.trim(); 460 | } 461 | 462 | protected static notBefore(prev: string, ...char: string[]): boolean { 463 | for (const c in char) { 464 | if (char[c] === prev) { 465 | return false; 466 | } 467 | } 468 | return true; 469 | } 470 | 471 | protected static notAfter(next: string, ...char: string[]): boolean { 472 | for (const c in char) { 473 | if (char[c] === next) { 474 | return false; 475 | } 476 | } 477 | return true; 478 | } 479 | 480 | protected static cleanArray(arr: string[]): string[] { 481 | for (let i = 0; i < arr.length; i++) { 482 | if (arr[i] === '') { 483 | arr.splice(i, 1); 484 | i--; 485 | } 486 | } 487 | return arr; 488 | } 489 | 490 | protected static spacePlaceholder(length: number): string { 491 | return PatternFormat.spacePlaceholderStr.repeat(length); 492 | } 493 | 494 | protected static lineAtIndex(str: string, idx: number): string { 495 | const first = str.substring(0, idx); 496 | const last = str.substring(idx); 497 | 498 | const firstNewLine = first.lastIndexOf('\n'); 499 | let secondNewLine = last.indexOf('\n'); 500 | 501 | if (secondNewLine === -1) { 502 | secondNewLine = last.length; 503 | } 504 | 505 | return str.substring(firstNewLine + 1, idx + secondNewLine); 506 | } 507 | 508 | protected static indent(amount: number) { 509 | amount = amount < 0 ? 0 : amount; 510 | return PatternFormat.spacePlaceholderStr.repeat(amount * 4); 511 | } 512 | 513 | // identifica os then/else que quebram a linha (comando nao está ao lado) 514 | // a linha de baixo deve ser agregada junto a linha atual 515 | private static regexThenWithBreak: RegExp = new RegExp(/(?:then|else){1}[\s\t]+(?![\w])/gi); 516 | } 517 | 518 | class SpacingFormat { 519 | public static document(source: string, formattingOptions: FormattingOptions, languageId: string): Promise { 520 | return getOpenEdgeConfig().then((oeConfig) => { 521 | // trim right 522 | if (oeConfig.format && oeConfig.format.trim === 'right') { 523 | const lines = source.split('\n'); 524 | for (let i = 0; i < lines.length; i++) { 525 | lines[i] = lines[i].trimRight(); 526 | } 527 | source = lines.join('\n'); 528 | } 529 | 530 | return source; 531 | }); 532 | } 533 | } 534 | 535 | enum CommentType { SingleLine, MultiLine } 536 | 537 | class Spaces { 538 | public before: number = 0; 539 | public after: number = 0; 540 | 541 | public constructor(before: number = 0, after: number = 0) { 542 | this.before = before; 543 | this.after = after; 544 | } 545 | } 546 | --------------------------------------------------------------------------------