├── .npmrc ├── package.nls.json ├── .artifactignore ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── codeql-analysis.yml │ ├── prerelease.yml │ └── main.yml ├── _config.yml ├── .jshintrc ├── images ├── logo.png └── fileIcon.svg ├── .prettierrc ├── src ├── debug │ ├── debugAdapter.ts │ ├── utils.ts │ ├── debugConfProvider.ts │ ├── debugAdapterFactory.ts │ └── dbgp.ts ├── providers │ ├── completion │ │ ├── model.ts │ │ └── structuredSystemVariables.json │ ├── ObjectScriptFoldingRangeProvider.ts │ ├── ObjectScriptRoutineSymbolProvider.ts │ ├── XmlContentProvider.ts │ ├── ObjectScriptClassFoldingRangeProvider.ts │ ├── CodeActionProvider.ts │ ├── Formatter.ts │ ├── FileDecorationProvider.ts │ ├── DocumentLinkProvider.ts │ ├── FileSystemProvider │ │ └── FileSearchProvider.ts │ ├── ObjectScriptHoverProvider.ts │ ├── ObjectScriptClassSymbolProvider.ts │ ├── WorkspaceSymbolProvider.ts │ └── DocumentFormattingEditProvider.ts ├── test │ ├── suite │ │ ├── index.ts │ │ └── extension.test.ts │ └── runTest.ts ├── commands │ ├── superclass.ts │ ├── subclass.ts │ ├── delete.ts │ ├── jumpToTagAndOffset.ts │ ├── viewOthers.ts │ ├── connectFolderToServerNamespace.ts │ ├── xmlToUdl.ts │ └── showAllClassMembers.ts ├── utils │ ├── getCSPToken.ts │ └── classDefinition.ts ├── web-extension.ts ├── languageConfiguration.ts ├── api │ └── atelier.d.ts └── explorer │ └── projectsExplorer.ts ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.base.json ├── test-fixtures └── test.code-workspace ├── .editorconfig ├── tsconfig.json ├── .vscodeignore ├── syntaxes ├── objectscript-csp.tmLanguage.json ├── objectscript_codeblock.json ├── objectscript-class_codeblock.json ├── vscode-objectscript-output.tmLanguage.json ├── objectscript-macros.tmLanguage.json └── objectscript.tmLanguage.json ├── language-configuration.json ├── language-configuration-class.jsonc ├── LICENSE ├── eslint.config.mjs ├── snippets ├── objectscript-int.json ├── objectscript.json └── objectscript-class.json ├── webpack.config.js ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md ├── GOVERNANCE.md └── test.cls /.npmrc: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.artifactignore: -------------------------------------------------------------------------------- 1 | **/* 2 | !*.vsix 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @isc-bsaviano 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This PR fixes # -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "asi": true 4 | } -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intersystems-community/vscode-objectscript/HEAD/images/logo.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto", 3 | "singleQuote": false, 4 | "trailingComma": "es5", 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /src/debug/debugAdapter.ts: -------------------------------------------------------------------------------- 1 | import { ObjectScriptDebugSession } from "./debugSession"; 2 | 3 | ObjectScriptDebugSession.run(ObjectScriptDebugSession); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | *.iml 4 | .vscode-test/ 5 | out/ 6 | dist/ 7 | *.vsix 8 | vscode*.d.ts 9 | test-fixtures 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "EditorConfig.EditorConfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.tabSize": 2, 5 | "editor.formatOnSave": true 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/providers/completion/model.ts: -------------------------------------------------------------------------------- 1 | interface CompletionModel { 2 | label: string; 3 | alias: string[]; 4 | deprecated?: boolean; 5 | documentation: string[]; 6 | link?: string; 7 | code?: string; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noImplicitReturns": false, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": false, 7 | "experimentalDecorators": true, 8 | "resolveJsonModule": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test-fixtures/test.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | ], 7 | "settings": { 8 | "objectscript.conn": { 9 | "active": false 10 | }, 11 | "objectscript.ignoreInstallServerManager": true, 12 | "intersystems.servers": { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "outDir": "out", 7 | "moduleResolution": "node", 8 | "lib": ["esnext", "dom"], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | }, 12 | "exclude": ["node_modules", ".vscode-test"] 13 | } 14 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # Run 'vsce ls' to verify that the following lines exclude everything not needed in the VSIX. 2 | 3 | ** 4 | !dist/*.js 5 | !dist/*.txt 6 | !snippets/*.json 7 | !images/*.svg 8 | !images/*.png 9 | !syntaxes/*.json 10 | !webview/*.js 11 | !CHANGELOG.md 12 | !LICENSE 13 | !README.md 14 | !package.json 15 | !package.nls.json 16 | !language-configuration.json 17 | !*.jsonc 18 | -------------------------------------------------------------------------------- /syntaxes/objectscript-csp.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "objectscript-csp", 4 | "scopeName": "source.objectscript_csp", 5 | "patterns": [ 6 | { 7 | "include": "#inline-html" 8 | } 9 | ], 10 | "repository": { 11 | "inline-html": { 12 | "name": "meta.embedded.inline.html", 13 | "patterns": [ 14 | { 15 | "include": "text.html.basic" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#;", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | // symbols used as brackets 7 | "brackets": [["{", "}"], ["(", ")"]], 8 | // symbols that are auto closed when typing 9 | "autoClosingPairs": [["{", "}"], ["(", ")"], ["\"", "\""]], 10 | // symbols that that can be used to surround a selection 11 | "surroundingPairs": [["{", "}"], ["(", ")"], ["\"", "\""]], 12 | "indentationRules": { 13 | "increaseIndentPattern": "{", 14 | "decreaseIndentPattern": "}" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return glob("**/**.test.js", { cwd: testsRoot }).then((files) => { 15 | // Add files to the test suite 16 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 17 | 18 | // Run the mocha test 19 | mocha.run((failures) => { 20 | if (failures > 0) { 21 | throw new Error(`${failures} tests failed.`); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /language-configuration-class.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "brackets": [["{", "}"], ["(", ")"], ["[", "]"]], 7 | // symbols that are auto closed when typing 8 | "autoClosingPairs": [["{", "}"], ["(", ")"], ["[", "]"], ["\"", "\""]], 9 | // symbols that that can be used to surround a selection 10 | "surroundingPairs": [["{", "}"], ["(", ")"], ["\"", "\""], ["[", "]"]], 11 | "indentationRules": { 12 | "increaseIndentPattern": "^((Class|Client)?Method|Query|XData|Storage|Trigger)", 13 | "decreaseIndentPattern": "^}" 14 | }, 15 | "folding": { 16 | "markers": { 17 | "start": "^((Class|Client)?Method|Query|XData|Storage|Trigger)", 18 | "end": "^}" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { before } from "mocha"; 3 | 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import { window, extensions } from "vscode"; 7 | import { extensionId, smExtensionId } from "../../extension"; 8 | 9 | suite("Extension Test Suite", () => { 10 | suiteSetup(async function () { 11 | // make sure extension is activated 12 | const serverManager = extensions.getExtension(smExtensionId); 13 | await serverManager?.activate(); 14 | const ext = extensions.getExtension(extensionId); 15 | await ext?.activate(); 16 | }); 17 | 18 | before(() => { 19 | window.showInformationMessage("Start all tests."); 20 | }); 21 | 22 | test("Sample test", () => { 23 | assert.ok("All good"); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | "tasks": [ 13 | { 14 | "type": "npm", 15 | "script": "webpack-dev", 16 | "presentation": { 17 | "reveal": "never" 18 | }, 19 | "group": { 20 | "isDefault": true, 21 | "kind": "build" 22 | }, 23 | "isBackground": true, 24 | "problemMatcher": "$eslint-stylish" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/providers/ObjectScriptFoldingRangeProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class ObjectScriptFoldingRangeProvider implements vscode.FoldingRangeProvider { 4 | public provideFoldingRanges( 5 | document: vscode.TextDocument, 6 | context: vscode.FoldingContext, 7 | token: vscode.CancellationToken 8 | ): vscode.ProviderResult { 9 | const ranges: vscode.FoldingRange[] = []; 10 | 11 | for (let i = 0; i < document.lineCount; i++) { 12 | const line = document.lineAt(i); 13 | 14 | if (line.text.match(/^\b\w+\b/) && !line.text.match(/^\bROUTINE\b/)) { 15 | const start = i; 16 | while (i++ && i < document.lineCount) { 17 | const text = document.lineAt(i).text; 18 | if (text.match(/^\b\w+\b/)) { 19 | break; 20 | } 21 | } 22 | i--; 23 | const end = i; 24 | ranges.push({ 25 | end, 26 | kind: vscode.FoldingRangeKind.Region, 27 | start, 28 | }); 29 | continue; 30 | } 31 | } 32 | 33 | return ranges; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/superclass.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { config } from "../extension"; 3 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 4 | import { currentFile, notIsfs } from "../utils"; 5 | import { ClassDefinition } from "../utils/classDefinition"; 6 | 7 | export async function superclass(): Promise { 8 | const file = currentFile(); 9 | if (!file || !file.name.toLowerCase().endsWith(".cls") || (notIsfs(file.uri) && !config("conn").active)) { 10 | return; 11 | } 12 | 13 | const open = (item) => { 14 | const uri = DocumentContentProvider.getUri(ClassDefinition.normalizeClassName(item, true)); 15 | vscode.window.showTextDocument(uri); 16 | }; 17 | 18 | const classDefinition = new ClassDefinition(file.name); 19 | return classDefinition 20 | .super() 21 | .then((data) => { 22 | const list = data || []; 23 | if (!list.length) { 24 | return; 25 | } 26 | vscode.window.showQuickPick(list, { title: "Pick a superclass" }).then((item) => { 27 | open(item); 28 | }); 29 | }) 30 | .catch((err) => console.error(err)); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work Copyright (c) 2018 doublefint 4 | Modified work Copyright 2018 Dmitry Maslennikov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | Are you using client-side or server-side editing?: 16 | 17 | - VS Code Version: 18 | - Extension Version: 19 | - OS: 20 | - Server `$ZVERSION` (if applicable): 21 | 22 | Steps to Reproduce: 23 | 24 | 1. 25 | 2. 26 | -------------------------------------------------------------------------------- /src/debug/utils.ts: -------------------------------------------------------------------------------- 1 | import { BaseProperty } from "./xdebugConnection"; 2 | 3 | export function formatPropertyValue(property: BaseProperty): string { 4 | let displayValue: string; 5 | if (property.hasChildren || property.type === "array" || property.type === "object") { 6 | if (property.type === "array") { 7 | // for arrays, show the length, like a var_dump would do 8 | displayValue = "array(" + (property.hasChildren ? property.numberOfChildren : 0) + ")"; 9 | } else if (property.type === "object" && property.class) { 10 | // for objects, show the class name as type (if specified) 11 | displayValue = property.class; 12 | } else { 13 | // edge case: show the type of the property as the value 14 | displayValue = property.type; 15 | } 16 | } else { 17 | // for null, uninitialized, resource, etc. show the type 18 | displayValue = property.value || property.type === "string" ? property.value : property.type; 19 | if (property.type === "string") { 20 | displayValue = '"' + displayValue + '"'; 21 | } else if (property.type === "bool") { 22 | displayValue = !!parseInt(displayValue) + ""; 23 | } 24 | } 25 | return displayValue; 26 | } 27 | -------------------------------------------------------------------------------- /syntaxes/objectscript_codeblock.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#objectscript-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "objectscript-code-block": { 11 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(objectscript)(\\s+[^`~]*)?$)", 12 | "name": "markup.fenced_code.block.markdown", 13 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 14 | "beginCaptures": { 15 | "3": { 16 | "name": "punctuation.definition.markdown" 17 | }, 18 | "4": { 19 | "name": "fenced_code.block.language.markdown" 20 | }, 21 | "5": { 22 | "name": "fenced_code.block.language.attributes.markdown" 23 | } 24 | }, 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.objectscript", 35 | "patterns": [ 36 | { 37 | "include": "source.objectscript" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.objectscript.codeblock" 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/getCSPToken.ts: -------------------------------------------------------------------------------- 1 | import { AtelierAPI } from "../api"; 2 | 3 | const allTokens = new Map>(); 4 | 5 | // Get or extend CSP token that will give current connected user access to webapp at path 6 | export async function getCSPToken(api: AtelierAPI, path: string): Promise { 7 | // Ignore any queryparams, and null out any page name 8 | const parts = path.split("?")[0].split("/"); 9 | parts.pop(); 10 | parts.push(""); 11 | path = parts.join("/"); 12 | 13 | // The first key in map-of-maps where we record tokens represents the connection target 14 | const { https, host, port, pathPrefix, username } = api.config; 15 | const connKey = JSON.stringify({ https, host, port, pathPrefix, username }); 16 | 17 | const myTokens = allTokens.get(connKey) || new Map(); 18 | const previousToken = myTokens.get(path) || ""; 19 | let token = ""; 20 | return api 21 | .actionQuery("select %Atelier_v1_Utils.General_GetCSPToken(?, ?) token", [path, previousToken]) 22 | .then((tokenObj) => { 23 | token = tokenObj.result.content[0].token; 24 | myTokens.set(path, token); 25 | allTokens.set(connKey, myTokens); 26 | return token; 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /syntaxes/objectscript-class_codeblock.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#objectscript-class-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "objectscript-class-code-block": { 11 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(objectscript-class)(\\s+[^`~]*)?$)", 12 | "name": "markup.fenced_code.block.markdown", 13 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 14 | "beginCaptures": { 15 | "3": { 16 | "name": "punctuation.definition.markdown" 17 | }, 18 | "4": { 19 | "name": "fenced_code.block.language.markdown" 20 | }, 21 | "5": { 22 | "name": "fenced_code.block.language.attributes.markdown" 23 | } 24 | }, 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.objectscript_class", 35 | "patterns": [ 36 | { 37 | "include": "source.objectscript_class" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.objectscript_class.codeblock" 45 | } 46 | -------------------------------------------------------------------------------- /src/providers/ObjectScriptRoutineSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class ObjectScriptRoutineSymbolProvider implements vscode.DocumentSymbolProvider { 4 | public provideDocumentSymbols( 5 | document: vscode.TextDocument, 6 | token: vscode.CancellationToken 7 | ): Thenable { 8 | return new Promise((resolve) => { 9 | const symbols: vscode.SymbolInformation[] = []; 10 | 11 | for (let i = 1; i < document.lineCount; i++) { 12 | let line = document.lineAt(i); 13 | 14 | const label = line.text.match(/^(%?\b\w+\b)/); 15 | if (label) { 16 | const start = line.range.start; 17 | while (++i && i < document.lineCount) { 18 | line = document.lineAt(i); 19 | if (line.text.match(/^(%?\b\w+\b)/)) { 20 | break; 21 | } 22 | } 23 | line = document.lineAt(--i); 24 | const end = line.range.start; 25 | symbols.push({ 26 | containerName: "Label", 27 | kind: vscode.SymbolKind.Method, 28 | location: new vscode.Location(document.uri, new vscode.Range(start, end)), 29 | name: label[1], 30 | }); 31 | } 32 | } 33 | 34 | resolve(symbols); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/subclass.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 4 | import { currentFile } from "../utils"; 5 | import { ClassDefinition } from "../utils/classDefinition"; 6 | 7 | export async function subclass(): Promise { 8 | const file = currentFile(); 9 | if (!file || !file.name.toLowerCase().endsWith(".cls")) { 10 | return; 11 | } 12 | const className = file.name.split(".").slice(0, -1).join("."); 13 | const api = new AtelierAPI(file.uri); 14 | if (!api.active) { 15 | return; 16 | } 17 | 18 | const open = (item) => { 19 | const uri = DocumentContentProvider.getUri(ClassDefinition.normalizeClassName(item, true)); 20 | vscode.window.showTextDocument(uri); 21 | }; 22 | 23 | return api 24 | .actionQuery("CALL %Dictionary.ClassDefinitionQuery_SubclassOf(?)", [className]) 25 | .then((data) => { 26 | const list = data.result.content.slice(0, 100) || []; 27 | if (!list.length) { 28 | return; 29 | } 30 | vscode.window 31 | .showQuickPick( 32 | list.map((el) => el.Name), 33 | { title: "Pick a subclass" } 34 | ) 35 | .then((item) => { 36 | open(item); 37 | }); 38 | }) 39 | .catch((err) => console.error(err)); 40 | } 41 | -------------------------------------------------------------------------------- /src/providers/XmlContentProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** Provides the contents of UDL documents extracted from XML files. */ 4 | export class XmlContentProvider implements vscode.TextDocumentContentProvider { 5 | /** A cache of UDL documents extracted from an XML file. */ 6 | private _udlDocsPerXmlFile: Map = new Map(); 7 | private onDidChangeEvent: vscode.EventEmitter = new vscode.EventEmitter(); 8 | 9 | public provideTextDocumentContent(uri: vscode.Uri): string | undefined { 10 | return this._udlDocsPerXmlFile 11 | .get(uri.fragment) 12 | ?.find((d) => d.name == uri.path) 13 | ?.content.join("\n"); 14 | } 15 | 16 | public get onDidChange(): vscode.Event { 17 | return this.onDidChangeEvent.event; 18 | } 19 | 20 | /** 21 | * Add `udlDocs` extracted from XML file `uri` to the cache. 22 | * Called by `previewXMLAsUDL()`. 23 | */ 24 | public addUdlDocsForFile(uri: string, udlDocs: { name: string; content: string[] }[]): void { 25 | this._udlDocsPerXmlFile.set(uri, udlDocs); 26 | } 27 | 28 | /** 29 | * Remove UDL documents extracted from XML file `uri` from the cache. 30 | * Called by `previewXMLAsUDL()`. 31 | */ 32 | public removeUdlDocsForFile(uri: string): void { 33 | this._udlDocsPerXmlFile.delete(uri); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/debug/debugConfProvider.ts: -------------------------------------------------------------------------------- 1 | import vscode = require("vscode"); 2 | 3 | import { 4 | CancellationToken, 5 | DebugConfiguration, 6 | DebugConfigurationProvider, 7 | ProviderResult, 8 | WorkspaceFolder, 9 | } from "vscode"; 10 | 11 | export class ObjectScriptConfigurationProvider implements DebugConfigurationProvider { 12 | /** 13 | * Massage a debug configuration just before a debug session is being launched, 14 | * e.g. add all missing attributes to the debug configuration. 15 | */ 16 | public resolveDebugConfiguration( 17 | folder: WorkspaceFolder | undefined, 18 | config: DebugConfiguration, 19 | token?: CancellationToken 20 | ): ProviderResult { 21 | // if launch.json is missing or empty 22 | if (!config.type && !config.request && !config.name) { 23 | const editor = vscode.window.activeTextEditor; 24 | if (editor && editor.document.languageId === "markdown") { 25 | config.type = "objectscript"; 26 | config.name = "Launch"; 27 | config.request = "launch"; 28 | config.program = "${file}"; 29 | // config.stopOnEntry = true; 30 | } 31 | } 32 | 33 | if (config.request === "launch" && !config.program) { 34 | return vscode.window.showInformationMessage("Cannot find a program to debug").then((_) => { 35 | return undefined; // abort launch 36 | }); 37 | } 38 | 39 | if (config.request === "attach" && !config.processId && !config.cspDebugId) { 40 | config.processId = "${command:PickProcess}"; 41 | } 42 | 43 | return config; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import globals from "globals"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import path from "node:path"; 5 | import { fileURLToPath } from "node:url"; 6 | import js from "@eslint/js"; 7 | import { FlatCompat } from "@eslint/eslintrc"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all 15 | }); 16 | 17 | export default [{ 18 | ignores: ["**/vscode.d.ts", "**/vscode.proposed.d.ts"], 19 | }, ...compat.extends( 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:@typescript-eslint/eslint-recommended", 23 | "prettier", 24 | "plugin:prettier/recommended", 25 | ), { 26 | plugins: { 27 | "@typescript-eslint": typescriptEslint, 28 | }, 29 | 30 | languageOptions: { 31 | globals: { 32 | ...globals.node, 33 | }, 34 | 35 | parser: tsParser, 36 | ecmaVersion: 2018, 37 | sourceType: "module", 38 | 39 | parserOptions: { 40 | project: "./tsconfig.json", 41 | }, 42 | }, 43 | 44 | rules: { 45 | "@typescript-eslint/no-explicit-any": 0, 46 | "@typescript-eslint/explicit-function-return-type": 0, 47 | "@typescript-eslint/no-unused-vars": 0, 48 | "@typescript-eslint/no-require-imports": 0, 49 | "@typescript-eslint/no-unused-expressions": 0, 50 | }, 51 | }]; -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import * as path from "path"; 3 | 4 | import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, runTests } from "@vscode/test-electron"; 5 | 6 | async function main() { 7 | try { 8 | // The folder containing the Extension Manifest package.json 9 | // Passed to `--extensionDevelopmentPath` 10 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 11 | 12 | // The path to the extension test script 13 | // Passed to --extensionTestsPath 14 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 15 | 16 | // The path to the workspace file 17 | const workspace = path.resolve("test-fixtures", "test.code-workspace"); 18 | 19 | const vscodeExecutablePath = await downloadAndUnzipVSCode("stable"); 20 | const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); 21 | 22 | const installExtension = (extId) => 23 | cp.spawnSync(cli, [...args, "--install-extension", extId], { 24 | encoding: "utf-8", 25 | stdio: "inherit", 26 | }); 27 | 28 | // Install dependent extensions 29 | installExtension("intersystems-community.servermanager"); 30 | installExtension("intersystems.language-server"); 31 | 32 | const launchArgs = ["-n", workspace, "--enable-proposed-api", "intersystems-community.vscode-objectscript"]; 33 | 34 | // Download VS Code, unzip it and run the integration test 35 | await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs }); 36 | } catch (err) { 37 | console.error("Failed to run tests", err); 38 | process.exit(1); 39 | } 40 | } 41 | 42 | main(); 43 | -------------------------------------------------------------------------------- /images/fileIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Product icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/providers/ObjectScriptClassFoldingRangeProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class ObjectScriptClassFoldingRangeProvider implements vscode.FoldingRangeProvider { 4 | public provideFoldingRanges( 5 | document: vscode.TextDocument, 6 | context: vscode.FoldingContext, 7 | token: vscode.CancellationToken 8 | ): vscode.ProviderResult { 9 | const ranges: vscode.FoldingRange[] = []; 10 | 11 | for (let i = 0; i < document.lineCount; i++) { 12 | const line = document.lineAt(i); 13 | const prevLine = i > 0 ? document.lineAt(i - 1) : { text: "" }; 14 | 15 | // Documentation block 16 | const docPattern = /\/{3}/; 17 | if (line.text.match(docPattern)) { 18 | const start = i; 19 | while (i++ && i < document.lineCount) { 20 | const text = document.lineAt(i).text; 21 | if (!text.match(docPattern)) { 22 | i--; 23 | break; 24 | } 25 | } 26 | const end = i; 27 | if (end - start > 3) { 28 | ranges.push({ 29 | end, 30 | kind: vscode.FoldingRangeKind.Comment, 31 | start, 32 | }); 33 | } 34 | continue; 35 | } 36 | if (line.text.match("^{") && !prevLine.text.match(/^\bClass\b/i)) { 37 | const start = i - 1; 38 | while (i++ && i < document.lineCount) { 39 | const text = document.lineAt(i).text; 40 | if (text.match(/^}/)) { 41 | break; 42 | } 43 | } 44 | const end = i; 45 | ranges.push({ 46 | end, 47 | kind: vscode.FoldingRangeKind.Region, 48 | start, 49 | }); 50 | continue; 51 | } 52 | } 53 | 54 | return ranges; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/web-extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | 5 | import { getLanguageConfiguration } from "./languageConfiguration"; 6 | 7 | import { ObjectScriptClassSymbolProvider } from "./providers/ObjectScriptClassSymbolProvider"; 8 | import { ObjectScriptRoutineSymbolProvider } from "./providers/ObjectScriptRoutineSymbolProvider"; 9 | 10 | const documentSelector = (...list: string[]) => list.map((language) => ({ scheme: "file", language })); 11 | 12 | // this method is called when your extension is activated 13 | // your extension is activated the very first time the command is executed 14 | export async function activate(context: vscode.ExtensionContext): Promise { 15 | vscode.commands.executeCommand("setContext", "vscode-objectscript.connectActive", false); 16 | 17 | context.subscriptions.push( 18 | vscode.languages.registerDocumentSymbolProvider( 19 | documentSelector("objectscript-class"), 20 | new ObjectScriptClassSymbolProvider() 21 | ), 22 | vscode.languages.registerDocumentSymbolProvider( 23 | documentSelector("objectscript"), 24 | new ObjectScriptRoutineSymbolProvider() 25 | ), 26 | vscode.languages.setLanguageConfiguration("objectscript-class", getLanguageConfiguration("objectscript-class")), 27 | vscode.languages.setLanguageConfiguration("objectscript", getLanguageConfiguration("objectscript")), 28 | vscode.languages.setLanguageConfiguration("objectscript-int", getLanguageConfiguration("objectscript-int")), 29 | vscode.languages.setLanguageConfiguration("objectscript-macros", getLanguageConfiguration("objectscript-macros")) 30 | ); 31 | } 32 | 33 | export function deactivate(): void { 34 | // nothing here 35 | } 36 | -------------------------------------------------------------------------------- /src/debug/debugAdapterFactory.ts: -------------------------------------------------------------------------------- 1 | import net = require("net"); 2 | import vscode = require("vscode"); 3 | import { ObjectScriptDebugSession } from "./debugSession"; 4 | 5 | export class ObjectScriptDebugAdapterDescriptorFactory 6 | implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable 7 | { 8 | private serverMap = new Map(); 9 | 10 | public createDebugAdapterDescriptor( 11 | session: vscode.DebugSession, 12 | executable: vscode.DebugAdapterExecutable | undefined 13 | ): vscode.ProviderResult { 14 | const debugSession = new ObjectScriptDebugSession(); 15 | 16 | // pickProcess may have added a suffix to inform us which folder's connection it used 17 | const workspaceFolderIndex = (session.configuration.processId as string)?.split("@")[1]; 18 | const workspaceFolderUri = workspaceFolderIndex 19 | ? vscode.workspace.workspaceFolders[parseInt(workspaceFolderIndex)]?.uri 20 | : undefined; 21 | debugSession.setupAPI(workspaceFolderUri); 22 | 23 | const serverId = debugSession.serverId; 24 | let server = this.serverMap.get(serverId); 25 | if (!server) { 26 | // start listening on a random port 27 | server = net 28 | .createServer((socket) => { 29 | debugSession.setRunAsServer(true); 30 | debugSession.start(socket as NodeJS.ReadableStream, socket); 31 | }) 32 | .listen(0); 33 | this.serverMap.set(serverId, server); 34 | } 35 | 36 | // make VS Code connect to this debug server 37 | const address = server.address(); 38 | const port = typeof address !== "string" ? address.port : 9000; 39 | return new vscode.DebugAdapterServer(port); 40 | } 41 | 42 | public dispose(): void { 43 | this.serverMap.forEach((server) => server.close()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /snippets/objectscript-int.json: -------------------------------------------------------------------------------- 1 | { 2 | "ForOrder": { 3 | "prefix": ["For"], 4 | "body": [ 5 | "Set ${1:key} = \"\"", 6 | "For {", 7 | "\tSet $1 = \\$ORDER(${2:array}($1))", 8 | "\tQuit:$1=\"\"", 9 | "\t${3:// process $2($1)}", 10 | "}" 11 | ], 12 | "description": "Iterate array with $Order" 13 | }, 14 | "SQL Statement": { 15 | "prefix": ["sql"], 16 | "body": [ 17 | "Set rs = ##class(%SQL.Statement).%ExecDirect(,\"SELECT ${1:*} FROM ${2:table}\")", 18 | "While rs.%Next() {", 19 | "\t${0:Write rs.ID, !}", 20 | "}" 21 | ], 22 | "description": "Prepare and execute SQL Query, then iterate result set 'rs'" 23 | }, 24 | "For": { 25 | "prefix": ["For"], 26 | "body": [ 27 | "For ${1:i} = ${2:1}:${3:1}:${4:9} {", 28 | "\t${0:Write $1, !}", 29 | "}" 30 | ], 31 | "description": "Typical For loop" 32 | }, 33 | "For Each": { 34 | "prefix": ["For"], 35 | "body": [ 36 | "For ${1:value} = \"${2:Red}\",\"${3:Green}\",\"${4:Blue}\" {", 37 | "\t${0:Write $1, !}", 38 | "}" 39 | ], 40 | "description": "Loop through series of values" 41 | }, 42 | "Do While": { 43 | "prefix": ["Do", "While"], 44 | "body": [ 45 | "Do {", 46 | "\t$0", 47 | "} While (${1:1 /* condition */})" 48 | ], 49 | "description": "Do While loop" 50 | }, 51 | "While": { 52 | "prefix": ["While"], 53 | "body": [ 54 | "While (${1:1 /* condition */}) {", 55 | "\t$0", 56 | "}" 57 | ], 58 | "description": "While loop" 59 | }, 60 | "Try Catch": { 61 | "prefix": ["Try"], 62 | "body": [ 63 | "Try {", 64 | "\t$0", 65 | "}", 66 | "Catch ex {", 67 | "\tSet tSC=ex.AsStatus()", 68 | "}" 69 | ], 70 | "description": "Try Catch" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/languageConfiguration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export function getLanguageConfiguration(lang: string): vscode.LanguageConfiguration { 4 | const conf = vscode.workspace.getConfiguration("objectscript"); 5 | return { 6 | wordPattern: 7 | /((?<=(class|extends|as|of) )(%?\b[a-z0-9]+(\.[a-z0-9]+)*\b))|(\^[a-z0-9]+(\.[a-z0-9]+)*)|((\${1,3}|[irm]?%|\^|#)?[a-z0-9]+)/i, 8 | brackets: [ 9 | ["{", "}"], 10 | ["(", ")"], 11 | ], 12 | comments: { 13 | lineComment: 14 | lang == "objectscript-class" 15 | ? "//" 16 | : ["objectscript", "objectscript-macros"].includes(lang) 17 | ? conf.get("commentToken") 18 | : conf.get("intCommentToken"), 19 | blockComment: ["/*", "*/"], 20 | }, 21 | autoClosingPairs: [ 22 | { 23 | open: "/*", 24 | close: "*/", 25 | notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], 26 | }, 27 | { 28 | open: "{", 29 | close: "}", 30 | notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], 31 | }, 32 | { 33 | open: "(", 34 | close: ")", 35 | notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], 36 | }, 37 | { 38 | open: '"', 39 | close: '"', 40 | notIn: [vscode.SyntaxTokenType.Comment, vscode.SyntaxTokenType.String, vscode.SyntaxTokenType.RegEx], 41 | }, 42 | ], 43 | onEnterRules: 44 | lang == "objectscript-class" 45 | ? [ 46 | { 47 | beforeText: /^\/\/\//, 48 | action: { indentAction: vscode.IndentAction.None, appendText: "/// " }, 49 | }, 50 | ] 51 | : undefined, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/providers/CodeActionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Formatter } from "./Formatter"; 3 | 4 | export class CodeActionProvider implements vscode.CodeActionProvider { 5 | private _formatter: Formatter; 6 | public constructor() { 7 | this._formatter = new Formatter(); 8 | } 9 | 10 | public provideCodeActions( 11 | document: vscode.TextDocument, 12 | range: vscode.Range | vscode.Selection, 13 | context: vscode.CodeActionContext, 14 | token: vscode.CancellationToken 15 | ): vscode.ProviderResult<(vscode.Command | vscode.CodeAction)[]> { 16 | const codeActions: vscode.CodeAction[] = []; 17 | context.diagnostics.forEach((diagnostic) => { 18 | if (diagnostic.code === "$zobjxxx") { 19 | const text = document.getText(diagnostic.range).toLowerCase(); 20 | let replacement = ""; 21 | switch (text) { 22 | case "$zobjclassmethod": 23 | replacement = "$classmethod"; 24 | break; 25 | case "$zobjmethod": 26 | replacement = "$method"; 27 | break; 28 | case "$zobjproperty": 29 | replacement = "$property"; 30 | break; 31 | case "$zobjclass": 32 | replacement = "$classname"; 33 | break; 34 | default: 35 | } 36 | if (replacement.length) { 37 | replacement = this._formatter.function(replacement); 38 | codeActions.push(this.createFix(document, diagnostic.range, replacement)); 39 | } 40 | } 41 | }); 42 | return codeActions; 43 | } 44 | 45 | private createFix(document: vscode.TextDocument, range: vscode.Range, replacement: string): vscode.CodeAction { 46 | const fix = new vscode.CodeAction(`Replace with ${replacement}`, vscode.CodeActionKind.QuickFix); 47 | fix.edit = new vscode.WorkspaceEdit(); 48 | fix.edit.replace(document.uri, range, replacement); 49 | return fix; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /snippets/objectscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "ForOrder": { 3 | "prefix": ["For"], 4 | "body": [ 5 | "Set ${1:key} = \"\"", 6 | "For {", 7 | "\tSet $1 = \\$ORDER(${2:array}($1))", 8 | "\tQuit:$1=\"\"", 9 | "\t${3:// process $2($1)}", 10 | "}" 11 | ], 12 | "description": "Iterate array with $Order" 13 | }, 14 | "SQL Statement": { 15 | "prefix": ["sql"], 16 | "body": [ 17 | "Set rs = ##class(%SQL.Statement).%ExecDirect(,\"SELECT ${1:*} FROM ${2:table}\")", 18 | "While rs.%Next() {", 19 | "\t${0:Write rs.ID, !}", 20 | "}" 21 | ], 22 | "description": "Prepare and execute SQL Query, then iterate result set 'rs'" 23 | }, 24 | "For": { 25 | "prefix": ["For"], 26 | "body": [ 27 | "For ${1:i} = ${2:1}:${3:1}:${4:9} {", 28 | "\t${0:Write $1, !}", 29 | "}" 30 | ], 31 | "description": "Typical For loop" 32 | }, 33 | "For Each": { 34 | "prefix": ["For"], 35 | "body": [ 36 | "For ${1:value} = \"${2:Red}\",\"${3:Green}\",\"${4:Blue}\" {", 37 | "\t${0:Write $1, !}", 38 | "}" 39 | ], 40 | "description": "Loop through series of values" 41 | }, 42 | "Do While": { 43 | "prefix": ["Do", "While"], 44 | "body": [ 45 | "Do {", 46 | "\t$0", 47 | "} While (${1:1 /* condition */})" 48 | ], 49 | "description": "Do While loop" 50 | }, 51 | "While": { 52 | "prefix": ["While"], 53 | "body": [ 54 | "While (${1:1 /* condition */}) {", 55 | "\t$0", 56 | "}" 57 | ], 58 | "description": "While loop" 59 | }, 60 | "ThrowOnError": { 61 | "prefix": ["$$$ThrowOnError"], 62 | "body": ["\\$\\$\\$ThrowOnError(##class(${1:class}).${2:method}())"], 63 | "description": "Invoke classmethod, store result in variable 'sc', and Throw if an error" 64 | }, 65 | "Try Catch": { 66 | "prefix": ["Try"], 67 | "body": [ 68 | "Try {", 69 | "\t$0", 70 | "}", 71 | "Catch ex {", 72 | "\tSet tSC=ex.AsStatus()", 73 | "}" 74 | ], 75 | "description": "Try Catch" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.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": "Launch Extension Alone", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--disable-extensions", 12 | "--extensionDevelopmentPath=${workspaceRoot}" 13 | ], 14 | "stopOnEntry": false, 15 | "sourceMaps": true, 16 | "outFiles": [ 17 | "${workspaceRoot}/dist/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: webpack" 20 | }, 21 | { 22 | "name": "Launch Extension", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceRoot}" 28 | ], 29 | "stopOnEntry": false, 30 | "sourceMaps": true, 31 | "outFiles": [ 32 | "${workspaceRoot}/dist/**/*.js" 33 | ], 34 | "preLaunchTask": "npm: webpack" 35 | }, 36 | { 37 | "name": "Extension Tests", 38 | "type": "extensionHost", 39 | "request": "launch", 40 | "runtimeExecutable": "${execPath}", 41 | "args": [ 42 | "${workspaceFolder}/test-fixtures/test.code-workspace", 43 | "--disable-extensions", 44 | "--extensionDevelopmentPath=${workspaceFolder}", 45 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 46 | ], 47 | "outFiles": [ 48 | "${workspaceFolder}/out/test/**/*.js" 49 | ], 50 | "preLaunchTask": "npm: test-compile" 51 | }, 52 | { 53 | "name": "Run Web Extension in VS Code", 54 | "type": "pwa-extensionHost", 55 | "debugWebWorkerHost": true, 56 | "request": "launch", 57 | "args": [ 58 | "--extensionDevelopmentPath=${workspaceFolder}", 59 | "--extensionDevelopmentKind=web" 60 | ], 61 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 62 | "preLaunchTask": "npm: webpack" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/providers/Formatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { config } from "../extension"; 3 | 4 | export type WordCase = "word" | "upper" | "lower"; 5 | 6 | export class Formatter { 7 | private _commandCase: WordCase; 8 | private _functionCase: WordCase; 9 | 10 | public constructor() { 11 | this.loadConfig(); 12 | vscode.workspace.onDidChangeConfiguration((event) => { 13 | this.loadConfig(); 14 | }); 15 | } 16 | 17 | private loadConfig() { 18 | const { commandCase, functionCase } = config("format"); 19 | this._commandCase = commandCase; 20 | this._functionCase = functionCase; 21 | } 22 | 23 | public setCase(wordCase: WordCase, value: T): T { 24 | let inputValue: string, resultValue: string; 25 | if (value instanceof vscode.SnippetString) { 26 | inputValue = value.value; 27 | } else { 28 | inputValue = value as string; 29 | } 30 | switch (wordCase.toLowerCase()) { 31 | case "lower": { 32 | resultValue = inputValue.toLowerCase(); 33 | break; 34 | } 35 | case "upper": { 36 | resultValue = inputValue.toUpperCase(); 37 | break; 38 | } 39 | case "word": 40 | default: { 41 | resultValue = inputValue.toLowerCase(); 42 | /** commands */ 43 | resultValue = resultValue.replace(/^(Z+\w|TS|TC|TRO|\w)/i, (v) => v.toUpperCase()); 44 | resultValue = resultValue.replace(/^elseif$/i, "ElseIf"); 45 | /** functions */ 46 | resultValue = resultValue.replace(/\^?\$(Z+\w|\w)/i, (v) => v.toUpperCase()); 47 | resultValue = resultValue.replace(/\$isobject/i, "$IsObject"); 48 | resultValue = resultValue.replace(/\$classmethod/i, "$ClassMethod"); 49 | resultValue = resultValue.replace(/\$classname/i, "$ClassName"); 50 | break; 51 | } 52 | } 53 | if (value instanceof vscode.SnippetString) { 54 | return new vscode.SnippetString(resultValue) as T; 55 | } else { 56 | return resultValue as T; 57 | } 58 | } 59 | 60 | public command(value: string): string { 61 | return value.replace(/\b(\w+)\b/g, (v) => this.setCase(this._commandCase, v)); 62 | } 63 | 64 | public function(value: T): T { 65 | return this.setCase(this._functionCase, value); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/api/atelier.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Atelier API 3 | */ 4 | 5 | interface ResponseStatus { 6 | errors: string[]; 7 | summary: string; 8 | } 9 | 10 | interface Content { 11 | content: T; 12 | } 13 | 14 | export interface Response { 15 | status: ResponseStatus; 16 | console: string[]; 17 | result: T; 18 | /** Value of the `Retry-After` response header, if present */ 19 | retryafter?: string; 20 | } 21 | 22 | interface ServerInfoFeature { 23 | name: string; 24 | enabled: string; 25 | } 26 | 27 | export interface UserAction { 28 | action: number; 29 | target: string; 30 | message: string; 31 | reload: boolean; 32 | doc: any; 33 | errorText: string; 34 | } 35 | 36 | export interface Document { 37 | name: string; 38 | db: string; 39 | ts: string; 40 | upd: boolean; 41 | cat: "RTN" | "CLS" | "CSP" | "OTH"; 42 | status: string; 43 | enc: boolean; 44 | flags: number; 45 | content: string[] | Buffer; 46 | ext?: UserAction | UserAction[]; 47 | } 48 | 49 | export interface ServerInfo { 50 | version: string; 51 | id: string; 52 | api: number; 53 | features: ServerInfoFeature[]; 54 | namespaces: string[]; 55 | } 56 | 57 | export interface SearchMatch { 58 | text: string; 59 | line?: string | number; 60 | member?: string; 61 | attr?: string; 62 | attrline?: number; 63 | } 64 | 65 | export interface SearchResult { 66 | doc: string; 67 | matches: SearchMatch[]; 68 | } 69 | 70 | export interface AtelierJob { 71 | pid: number; 72 | namespace: string; 73 | routine: string; 74 | state: string; 75 | device: string; 76 | } 77 | 78 | interface AsyncCompileRequest { 79 | request: "compile"; 80 | documents: string[]; 81 | source?: boolean; 82 | flags?: string; 83 | } 84 | 85 | interface AsyncSearchRequest { 86 | request: "search"; 87 | query: string; 88 | regex?: boolean; 89 | project?: string; 90 | word?: boolean; 91 | case?: boolean; 92 | wild?: boolean; 93 | documents?: string; 94 | system?: boolean; 95 | generated?: boolean; 96 | mapped?: boolean; 97 | max?: number; 98 | include?: string; 99 | exclude?: string; 100 | console: false; 101 | } 102 | 103 | interface AsyncUnitTestRequest { 104 | request: "unittest"; 105 | tests: { class: string; methods?: string[] }[]; 106 | load?: { file: string; content: string[] }[]; 107 | console?: boolean; 108 | debug?: boolean; 109 | } 110 | 111 | export type AsyncRequest = AsyncCompileRequest | AsyncSearchRequest | AsyncUnitTestRequest; 112 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '41 18 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v3 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v3 68 | -------------------------------------------------------------------------------- /src/commands/delete.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { AtelierAPI } from "../api"; 4 | import { FILESYSTEM_SCHEMA, explorerProvider } from "../extension"; 5 | import { outputChannel, uriOfWorkspaceFolder } from "../utils"; 6 | import { OtherStudioAction, fireOtherStudioAction } from "./studio"; 7 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 8 | import { UserAction } from "../api/atelier"; 9 | import { NodeBase, PackageNode, RootNode } from "../explorer/nodes"; 10 | 11 | function deleteList(items: string[], workspaceFolder: string, namespace: string): Promise { 12 | if (!items || !items.length) { 13 | vscode.window.showWarningMessage("Nothing to delete"); 14 | } 15 | 16 | const wsFolderUri = uriOfWorkspaceFolder(workspaceFolder); 17 | const api = new AtelierAPI(workspaceFolder); 18 | api.setNamespace(namespace); 19 | return Promise.all(items.map((item) => api.deleteDoc(item))).then((files) => { 20 | files.forEach((file) => { 21 | if (file.result.ext && wsFolderUri?.scheme == FILESYSTEM_SCHEMA) { 22 | // Only process source control output if we're in an isfs folder 23 | const uri = DocumentContentProvider.getUri(file.result.name); 24 | fireOtherStudioAction(OtherStudioAction.DeletedDocument, uri, file.result.ext); 25 | } 26 | }); 27 | outputChannel.appendLine(`Deleted items: ${files.filter((el) => el.result).length}`); 28 | }); 29 | } 30 | 31 | export async function deleteExplorerItems(nodes: NodeBase[]): Promise { 32 | const { workspaceFolder, namespace } = nodes[0]; 33 | const nodesPromiseList: Promise[] = []; 34 | for (const node of nodes) { 35 | nodesPromiseList.push(node instanceof RootNode ? node.getChildren(node) : Promise.resolve([node])); 36 | } 37 | return Promise.all(nodesPromiseList) 38 | .then((nodesList) => nodesList.flat()) 39 | .then((allNodes) => 40 | allNodes.reduce( 41 | (list, subNode) => list.concat(subNode instanceof PackageNode ? subNode.getClasses() : [subNode.fullName]), 42 | [] 43 | ) 44 | ) 45 | .then(async (items) => { 46 | if (items.length) { 47 | // Ask the user to confirm 48 | const confirm = await vscode.window.showWarningMessage( 49 | `About to delete ${ 50 | items.length > 1 ? `${items.length} documents` : `'${items[0]}'` 51 | }. Are you sure you want to proceed?`, 52 | "Cancel", 53 | "Confirm" 54 | ); 55 | if (confirm !== "Confirm") { 56 | // Don't delete without confirmation 57 | return; 58 | } 59 | deleteList(items, workspaceFolder, namespace); 60 | explorerProvider.refresh(); 61 | } 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /src/providers/FileDecorationProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { currentFile } from "../utils"; 3 | import { clsLangId, cspLangId, intLangId, macLangId } from "../extension"; 4 | 5 | export class FileDecorationProvider implements vscode.FileDecorationProvider { 6 | private _genBadge = String.fromCharCode(9965); // Gear 7 | private _genTooltip = "Generated"; 8 | onDidChangeFileDecorations: vscode.Event; 9 | 10 | emitter = new vscode.EventEmitter(); 11 | 12 | public constructor() { 13 | this.onDidChangeFileDecorations = this.emitter.event; 14 | } 15 | 16 | provideFileDecoration(uri: vscode.Uri): vscode.FileDecoration | undefined { 17 | let result: vscode.FileDecoration | undefined = undefined; 18 | 19 | // Only provide decorations for files that are open and not untitled 20 | const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() == uri.toString()); 21 | if ( 22 | doc != undefined && 23 | !doc.isUntitled && 24 | [clsLangId, macLangId, intLangId, cspLangId].includes(doc.languageId) && 25 | vscode.workspace 26 | .getConfiguration("objectscript", vscode.workspace.getWorkspaceFolder(uri)) 27 | .get("showGeneratedFileDecorations") 28 | ) { 29 | // Use the file's contents to check if it's generated 30 | if (doc.languageId == clsLangId) { 31 | for (let line = 0; line < doc.lineCount; line++) { 32 | const lineText = doc.lineAt(line).text; 33 | if (lineText.startsWith("Class ")) { 34 | const clsMatch = lineText.match(/[[, ]{1}GeneratedBy *= *([^,\] ]+)/i); 35 | if (clsMatch) { 36 | // This class is generated 37 | result = new vscode.FileDecoration(this._genBadge, `${this._genTooltip} by ${clsMatch[1]}`); 38 | } 39 | break; 40 | } 41 | } 42 | } else { 43 | const firstLine = doc.lineAt(0).text; 44 | if (firstLine.startsWith("ROUTINE ") && firstLine.includes("Generated]")) { 45 | // This routine is generated 46 | let tooltip = this._genTooltip; 47 | const file = currentFile(doc)?.name; 48 | if (file) { 49 | const macMatch = file.match(/^(.*)\.G[0-9]+\.mac$/); 50 | const intMatch = file.match(/^(.*)\.[0-9]+\.int$/); 51 | if (macMatch) { 52 | tooltip += ` by ${macMatch[1]}.cls`; 53 | } else if (intMatch) { 54 | tooltip += ` by ${intMatch[1]}.cls`; 55 | } else if (doc.languageId == intLangId) { 56 | tooltip += ` by ${file.slice(0, -3)}mac`; 57 | } 58 | } 59 | result = new vscode.FileDecoration(this._genBadge, tooltip); 60 | } 61 | } 62 | } 63 | 64 | return result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | /**@type {import('webpack').Configuration}*/ 4 | const config = { 5 | target: "node", // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 6 | 7 | entry: ["core-js/features/array/flat-map", "./src/extension.ts"], // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 8 | output: { 9 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 10 | path: path.resolve(__dirname, "dist"), 11 | filename: "extension.js", 12 | libraryTarget: "commonjs2", 13 | devtoolModuleFilenameTemplate: "../[resource-path]", 14 | }, 15 | devtool: "source-map", 16 | externals: { 17 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 18 | "@opentelemetry/tracing": "commonjs @opentelemetry/tracing", 19 | "applicationinsights-native-metrics": "commonjs applicationinsights-native-metrics", 20 | bufferutil: "commonjs bufferutil", 21 | "utf-8-validate": "commonjs utf-8-validate", 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: [".ts", ".js"], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: "ts-loader", 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | }; 41 | 42 | const browserConfig = /** @type WebpackConfig */ { 43 | mode: "none", 44 | target: "webworker", // web extensions run in a webworker context 45 | entry: { 46 | "web-extension": "./src/web-extension.ts", 47 | }, 48 | output: { 49 | filename: "[name].js", 50 | // eslint-disable-next-line no-undef 51 | path: path.join(__dirname, "./dist"), 52 | libraryTarget: "commonjs", 53 | }, 54 | resolve: { 55 | mainFields: ["browser", "module", "main"], 56 | extensions: [".ts", ".js"], 57 | alias: { 58 | // replace the node based resolver with the browser version 59 | "./ModuleResolver": "./BrowserModuleResolver", 60 | }, 61 | fallback: { 62 | // eslint-disable-next-line no-undef 63 | path: require.resolve("path-browserify"), 64 | os: false, 65 | }, 66 | }, 67 | module: { 68 | rules: [ 69 | { 70 | test: /\.ts$/, 71 | exclude: /node_modules/, 72 | use: [ 73 | { 74 | loader: "ts-loader", 75 | }, 76 | ], 77 | }, 78 | ], 79 | }, 80 | externals: { 81 | vscode: "commonjs vscode", // ignored because it doesn't exist 82 | }, 83 | performance: { 84 | hints: false, 85 | }, 86 | devtool: "source-map", 87 | }; 88 | 89 | module.exports = [config, browserConfig]; 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ObjectScript extension for Visual Studio Code 2 | 3 | ## Contributing a pull request 4 | 5 | ### Prerequisites 6 | 7 | 1. [Node.js](https://nodejs.org/) 18.x 8 | 1. Windows, macOS, or Linux 9 | 1. [Visual Studio Code](https://code.visualstudio.com/) 10 | 1. The following VS Code extensions: 11 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 12 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 13 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) 14 | 15 | ### Setup 16 | 17 | ```shell 18 | git clone https://github.com/intersystems-community/vscode-objectscript 19 | cd vscode-objectscript 20 | npm install 21 | ``` 22 | 23 | ### Errors and Warnings 24 | 25 | TypeScript errors and warnings will be displayed in the `PROBLEMS` panel of Visual Studio Code. 26 | 27 | ### Editing code snippets 28 | 29 | Code snippets are defined in files in the /snippets/ folder: 30 | 31 | * objectscript-class.json - snippets for class definition context 32 | * objectscript.json - snippets for objectscript context 33 | 34 | Snippets syntax is described [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets). 35 | 36 | ### Run dev build and validate your changes 37 | 38 | To test changes, open the `vscode-objectscript` folder in VSCode. 39 | Then, open the debug panel by clicking the `Run and Debug` icon on the Activity Bar, select the `Launch Extension` 40 | option from the top menu, and click start. A new window will launch with the title 41 | `[Extension Development Host]`. Do your testing here. 42 | 43 | If you want to disable all other extensions when testing in the Extension Development Host, choose the `Launch Extension Alone` option instead. 44 | 45 | ### Pull requests 46 | 47 | Work should be done on a unique branch -- not the master branch. Pull requests require the approval of two PMC members, as described in the [Governance document](GOVERNANCE.md). PMC review is often high level, so in addition to that, you should request a review by someone familiar with the technical details of your particular pull request. 48 | 49 | We do expect CI to be passing for a pull request before we will consider merging it. CI executed by pull requests will produce a `vsix` file, which can be downloaded and installed manually to test proposed functionality. 50 | 51 | ## Beta versions 52 | 53 | Any change to `master` branch will call CI, which will produce [beta release](https://github.com/intersystems-community/vscode-objectscript/releases), which can be manually installed. 54 | 55 | ## Local Build 56 | 57 | Steps to build the extension on your machine once you've cloned the repo: 58 | 59 | ```bash 60 | > npm install -g vsce 61 | # Perform the next steps in the vscode-objectscript folder. 62 | > npm install 63 | > npm run package 64 | ``` 65 | 66 | Resulting in a `vscode-objectscript-$VERSION.vsix` file in your `vscode-objectscript` folder. 67 | -------------------------------------------------------------------------------- /src/providers/DocumentLinkProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DocumentContentProvider } from "./DocumentContentProvider"; 3 | import { handleError } from "../utils"; 4 | 5 | interface StudioLink { 6 | uri: vscode.Uri; 7 | range: vscode.Range; 8 | filename: string; 9 | methodname?: string; 10 | offset: number; 11 | } 12 | 13 | export class DocumentLinkProvider implements vscode.DocumentLinkProvider { 14 | public provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult { 15 | // Possible link formats: 16 | // SomePackage.SomeClass.cls(SomeMethod+offset) 17 | // SomePackage.SomeClass.cls(offset) 18 | // SomeRoutine.int(offset) 19 | const regexs = [/((?:\w|\.)+)\((\w+)\+(\d+)\)/, /((?:\w|\.)+)\((\d+)\)/]; 20 | const documentLinks: StudioLink[] = []; 21 | 22 | for (let i = 0; i < document.lineCount; i++) { 23 | const text = document.lineAt(i).text; 24 | 25 | regexs.forEach((regex) => { 26 | const match = regex.exec(text); 27 | if (match != null) { 28 | const filename = match[1]; 29 | let methodname; 30 | let offset; 31 | 32 | if (match.length >= 4) { 33 | methodname = match[2]; 34 | offset = parseInt(match[3]); 35 | } else { 36 | offset = parseInt(match[2]); 37 | } 38 | 39 | const uri = DocumentContentProvider.getUri(filename); 40 | if (!uri) return; 41 | documentLinks.push({ 42 | range: new vscode.Range( 43 | new vscode.Position(i, match.index), 44 | new vscode.Position(i, match.index + match[0].length) 45 | ), 46 | uri, 47 | filename, 48 | methodname, 49 | offset, 50 | }); 51 | } 52 | }); 53 | } 54 | return documentLinks; 55 | } 56 | 57 | public async resolveDocumentLink(link: StudioLink, token: vscode.CancellationToken): Promise { 58 | const editor = await vscode.window 59 | .showTextDocument(link.uri) 60 | .then(undefined, (error) => handleError(error, "Failed to resolve DocumentLink to a specific location.")); 61 | if (!editor) return; 62 | let offset = link.offset; 63 | 64 | // add the offset of the method if it is a class 65 | if (link.methodname) { 66 | const symbols = await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", link.uri); 67 | const method = symbols[0].children.find( 68 | (info) => (info.detail === "ClassMethod" || info.detail === "Method") && info.name === link.methodname 69 | ); 70 | offset += method.location.range.start.line + 1; 71 | } 72 | 73 | const line = editor.document.lineAt(offset); 74 | editor.selection = new vscode.Selection(line.range.start, line.range.start); 75 | editor.revealRange(line.range, vscode.TextEditorRevealType.InCenter); 76 | 77 | return new vscode.DocumentLink(link.range, link.uri); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/providers/FileSystemProvider/FileSearchProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { isfsConfig, projectContentsFromUri, studioOpenDialogFromURI } from "../../utils/FileProviderUtil"; 3 | import { notNull, queryToFuzzyLike } from "../../utils"; 4 | import { DocumentContentProvider } from "../DocumentContentProvider"; 5 | import { ProjectItem } from "../../commands/project"; 6 | 7 | export class FileSearchProvider implements vscode.FileSearchProvider { 8 | public async provideFileSearchResults( 9 | query: vscode.FileSearchQuery, 10 | options: vscode.FileSearchOptions, 11 | token: vscode.CancellationToken 12 | ): Promise { 13 | let counter = 0; 14 | // Replace all back slashes with forward slashes 15 | let pattern = query.pattern.replace(/\\/g, "/"); 16 | if (pattern.startsWith("/")) { 17 | // Remove all leading slashes 18 | pattern = pattern.replace(/^\/+/, ""); 19 | } else if (pattern.startsWith("**/")) { 20 | // Remove a leading globstar from the pattern. 21 | // The leading globstar gets added by Find widget of Explorer tree (non-fuzzy mode), which since 1.94 uses FileSearchProvider 22 | pattern = pattern.slice(3); 23 | } 24 | const { csp, project } = isfsConfig(options.folder); 25 | if (project) { 26 | // Create a fuzzy match regex to do the filtering here 27 | let regexStr = ".*"; 28 | for (const c of pattern) regexStr += `${[".", "/"].includes(c) ? "[./]" : c}.*`; 29 | const patternRegex = new RegExp(regexStr, "i"); 30 | if (token.isCancellationRequested) return; 31 | return projectContentsFromUri(options.folder, true).then((docs) => 32 | docs 33 | .map((doc: ProjectItem) => 34 | !token.isCancellationRequested && 35 | // The document matches the query 36 | (!pattern.length || patternRegex.test(doc.Name)) && 37 | // We haven't hit the max number of results 38 | (!options.maxResults || ++counter <= options.maxResults) 39 | ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) 40 | : null 41 | ) 42 | .filter(notNull) 43 | ); 44 | } 45 | // When this is called without a query.pattern every file is supposed to be returned, so do not provide a filter 46 | const likePattern = queryToFuzzyLike(pattern); 47 | const filter = pattern.length 48 | ? `Name LIKE '${!csp ? likePattern.replace(/\//g, ".") : likePattern}' ESCAPE '\\'` 49 | : ""; 50 | if (token.isCancellationRequested) return; 51 | return studioOpenDialogFromURI(options.folder, { flat: true, filter }).then((data) => 52 | data.result.content 53 | .map((doc: { Name: string; Type: number }) => 54 | !token.isCancellationRequested && 55 | // We haven't hit the max number of results 56 | (!options.maxResults || ++counter <= options.maxResults) 57 | ? DocumentContentProvider.getUri(doc.Name, "", "", true, options.folder) 58 | : null 59 | ) 60 | .filter(notNull) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /syntaxes/vscode-objectscript-output.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "vscode-objectscript-output", 4 | "scopeName": "source.vscode_objectscript_output", 5 | "patterns": [ 6 | { 7 | "match": "^\\s*(?:\\> )?(?:ERROR).*\\#\\d+\\:", 8 | "name": "invalid" 9 | }, 10 | { 11 | "match": "^\\s*(?:ERROR)\\s*\\:", 12 | "name": "invalid" 13 | }, 14 | { 15 | "match": "^\\s*(?:Authorization error\\:)", 16 | "name": "invalid" 17 | }, 18 | { 19 | "match": "^Detected \\d+ errors during compilation in \\S+", 20 | "name": "invalid" 21 | }, 22 | { 23 | "match": "^Compilation finished successfully in \\S+", 24 | "name": "markup.bold" 25 | }, 26 | { 27 | "match": "(?<=^Compiling (?:class|routine|file|table) \\:? ?)[^\\:\\s]+", 28 | "name": "entity.name.class" 29 | }, 30 | { 31 | "match": "(?<=^Deleting (?:class|routine|file|table) )\\S+", 32 | "name": "entity.name.class" 33 | }, 34 | { 35 | "match": "(?<=^Dropping orphaned procedure\\: )\\S+", 36 | "name": "entity.name.class" 37 | }, 38 | { 39 | "match": "(?<=^(?:Class|Routine|File|Table) )\\S+(?= is up-to-date\\.)", 40 | "name": "entity.name.class" 41 | }, 42 | { 43 | "match": "\\S+(?= is up to date\\. Compile of this item skipped\\.)", 44 | "name": "entity.name.class" 45 | }, 46 | { 47 | "match": "(?<=^\\s*ERROR\\:\\s*)\\S+", 48 | "name": "entity.name.class" 49 | }, 50 | { 51 | "match": "\"(.*?)\"", 52 | "name": "string.quoted" 53 | }, 54 | { 55 | "match": "'(.*?)'", 56 | "name": "string.quoted" 57 | }, 58 | { 59 | "match": "\\b(((0|1)?[0-9][1-2]?)|(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sept(ember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?))[/|\\-|\\.| ]([0-2]?[0-9]|[3][0-1])[/|\\-|\\.| ]((19|20)?[0-9]{2})\\b", 60 | "name": "constant.numeric" 61 | }, 62 | { 63 | "match": "\\b((19|20)?[0-9]{2}[/|\\-|\\.| ](((0|1)?[0-9][1-2]?)|(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sept(ember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?))[/|\\-|\\.| ]([0-2]?[0-9]|[3][0-1]))\\b", 64 | "name": "constant.numeric" 65 | }, 66 | { 67 | "match": "\\b([0-2]?[0-9]|[3][0-1])[/|\\-|\\.| ](((0|1)?[0-9][1-2]?)|(Jan(uary)?|Feb(ruary)?|Mar(ch)?|Apr(il)?|May|Jun(e)?|Jul(y)?|Aug(ust)?|Sept(ember)?|Oct(ober)?|Nov(ember)?|Dec(ember)?))[/|\\-|\\.| ]((19|20)?[0-9]{2})\\b", 68 | "name": "constant.numeric" 69 | }, 70 | { 71 | "match": "\\b([0|1]?[0-9]|2[0-3])\\:[0-5][0-9](\\:[0-5][0-9])?( ?(?i:(a|p)m?))?( ?[+-]?[0-9]*)?\\b", 72 | "name": "constant.numeric" 73 | }, 74 | { 75 | "match": "\\b\\d+\\.?\\d*?\\b", 76 | "name": "constant.numeric" 77 | }, 78 | { 79 | "match": "\\b(?i:(0?x)?[0-9a-f][0-9a-f]+)\\b", 80 | "name": "constant.numeric" 81 | }, 82 | { 83 | "match": "^WARNING:", 84 | "name": "invalid" 85 | }, 86 | { 87 | "match": "^NOTE:", 88 | "name": "string.quoted" 89 | } 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /src/explorer/projectsExplorer.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { notIsfs } from "../utils"; 4 | import { NodeBase, ProjectsServerNsNode } from "./nodes"; 5 | 6 | export class ProjectsExplorerProvider implements vscode.TreeDataProvider { 7 | public onDidChangeTreeData: vscode.Event; 8 | private _onDidChangeTreeData: vscode.EventEmitter; 9 | 10 | /** The labels of all current root nodes */ 11 | private _roots: string[] = []; 12 | 13 | /** The server:ns string for all extra root nodes */ 14 | private readonly _extraRoots: string[] = []; 15 | 16 | public constructor() { 17 | this._onDidChangeTreeData = new vscode.EventEmitter(); 18 | this.onDidChangeTreeData = this._onDidChangeTreeData.event; 19 | } 20 | 21 | public refresh(): void { 22 | this._onDidChangeTreeData.fire(null); 23 | } 24 | public getTreeItem(element: NodeBase): vscode.TreeItem { 25 | return element.getTreeItem(); 26 | } 27 | 28 | public async getChildren(element?: NodeBase): Promise { 29 | if (!element) { 30 | return this.getRootNodes(); 31 | } 32 | return element.getChildren(element); 33 | } 34 | 35 | public openExtraServerNs(serverNs: { serverName: string; namespace: string }): void { 36 | // Check if this server namespace is already open 37 | if (this._roots.includes(`${serverNs.serverName}[${serverNs.namespace}]`)) { 38 | vscode.window.showWarningMessage( 39 | `Namespace '${serverNs.namespace}' on server '${serverNs.serverName}' is already open in the Projects Explorer`, 40 | "Dismiss" 41 | ); 42 | return; 43 | } 44 | // Add the extra root node 45 | this._extraRoots.push(`${serverNs.serverName}:${serverNs.namespace}`); 46 | // Refresh the explorer 47 | this.refresh(); 48 | } 49 | 50 | public closeExtraServerNs(node: ProjectsServerNsNode): void { 51 | const label = node.getTreeItem().label; 52 | const serverName = label.slice(0, label.lastIndexOf("[")); 53 | const namespace = label.slice(label.lastIndexOf("[") + 1, -1); 54 | const idx = this._extraRoots.findIndex((authority) => authority == `${serverName}:${namespace}`); 55 | if (idx != -1) { 56 | // Remove the extra root node 57 | this._extraRoots.splice(idx, 1); 58 | // Refresh the explorer 59 | this.refresh(); 60 | } 61 | } 62 | 63 | private async getRootNodes(): Promise { 64 | const rootNodes: NodeBase[] = []; 65 | let node: NodeBase; 66 | 67 | const workspaceFolders = vscode.workspace.workspaceFolders || []; 68 | const alreadyAdded: string[] = []; 69 | // Add the workspace root nodes 70 | workspaceFolders 71 | .filter((workspaceFolder) => workspaceFolder.uri && !notIsfs(workspaceFolder.uri)) 72 | .forEach((workspaceFolder) => { 73 | const conn = new AtelierAPI(workspaceFolder.uri).config; 74 | if (conn.active && conn.ns) { 75 | node = new ProjectsServerNsNode(workspaceFolder.name, this._onDidChangeTreeData, workspaceFolder.uri); 76 | const label = node.getTreeItem().label; 77 | if (!alreadyAdded.includes(label)) { 78 | alreadyAdded.push(label); 79 | rootNodes.push(node); 80 | } 81 | } 82 | }); 83 | // Add the extra root nodes 84 | this._extraRoots.forEach((authority) => { 85 | node = new ProjectsServerNsNode( 86 | "", 87 | this._onDidChangeTreeData, 88 | vscode.Uri.parse(`isfs-readonly://${authority}/`), 89 | true 90 | ); 91 | const label = node.getTreeItem().label; 92 | if (!alreadyAdded.includes(label)) { 93 | alreadyAdded.push(label); 94 | rootNodes.push(node); 95 | } 96 | }); 97 | this._roots = alreadyAdded; 98 | await vscode.commands.executeCommand( 99 | "setContext", 100 | "vscode-objectscript.projectsExplorerRootCount", 101 | rootNodes.length 102 | ); 103 | return rootNodes; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/providers/completion/structuredSystemVariables.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label":"^$GLOBAL", 4 | "alias":[ 5 | "^$GLOBAL", 6 | "^$G" 7 | ], 8 | "documentation":[ 9 | "Provides information about globals.\n", 10 | "```objectscript\n", 11 | "^$|nspace|GLOBAL(global_name)\n", 12 | "^$|nspace|G(global_name)\n", 13 | "```\n", 14 | "Parameters:\n\n", 15 | " |`nspace`| or[`nspace`] _Optional_ — An _extended SSVN reference_, either an explicit namespace name or an implied namespace. Must evaluate to a quoted string, which is enclosed in either square brackets (["nspace"]) or vertical bars (|"nspace"|). Namespace names are not case-sensitive; they are stored and displayed in uppercase letters. You may also specify ^$GLOBAL as a _process-private global_ as either ^||$GLOBAL or ^|"^"|$GLOBAL. `global_name`An expression that evaluates to a string containing an unsubscripted global name.\n" 16 | ], 17 | "link":"/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_sglobal" 18 | }, 19 | { 20 | "label":"^$JOB", 21 | "alias":[ 22 | "^$JOB", 23 | "^$J" 24 | ], 25 | "documentation":[ 26 | "Provides InterSystems IRIS process (job) information.\n", 27 | "```objectscript\n", 28 | "^$JOB(job_number)\n", 29 | "^$J(job_number)\n", 30 | "```\n", 31 | "Parameters:\n\n", 32 | "`job_number`The system-specific job number created when you enter the ObjectScript command. Every active InterSystems IRIS process has a unique job number. Logging in to the system initiates a job. On UNIX® systems, the job number is the pid of the child process started when InterSystems IRIS was invoked. `job_number` must be specified as an integer; hexadecimal values are not supported.\n" 33 | ], 34 | "link":"/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_sjob" 35 | }, 36 | { 37 | "label":"^$LOCK", 38 | "alias":[ 39 | "^$LOCK", 40 | "^$L" 41 | ], 42 | "documentation":[ 43 | "Provides lock name information.\n", 44 | "```objectscript\n", 45 | "^$|nspace|LOCK(lock_name,info_type,pid)\n", 46 | " ^$|nspace|L(lock_name,info_type,pid)\n", 47 | "```\n", 48 | "Parameters:\n\n", 49 | " |`nspace`| or[`nspace`] _Optional_ — An _extended SSVN reference_, either an explicit namespace name or an implied namespace. Must evaluate to a quoted string, which is enclosed in either square brackets (["nspace"]) or vertical bars (|"nspace"|). Namespace names are not case-sensitive; they are stored and displayed in uppercase letters.`lock_name`An expression that evaluates to a string containing a lock variable name, either subscripted or unsubscripted. If a literal, must be specified as a quoted string.`info_type`_Optional_ — A keyword specifying what type of information about `lock_name` to return. Must be specified as a quoted string. The available options are "OWNER", "FLAGS", "MODE", and "COUNTS".`pid`_Optional_ — For use with the "COUNTS” keyword. An integer that specifies the process ID of the owner of the lock. If specified, at most one list element is returned for "COUNTS”. If omitted (or specified as 0), a list element is returned for each owner holding the specified lock. `pid` has no effect on the other `info_type` keywords.\n" 50 | ], 51 | "link":"/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_slock" 52 | }, 53 | { 54 | "label":"^$ROUTINE", 55 | "alias":[ 56 | "^$ROUTINE", 57 | "^$R" 58 | ], 59 | "documentation":[ 60 | "Provides routine information.\n", 61 | "```objectscript\n", 62 | "^$|nspace|ROUTINE(routine_name)\n", 63 | "^$|nspace|R(routine_name)\n", 64 | "```\n", 65 | "Parameters:\n\n", 66 | " |`nspace`| or[`nspace`] _Optional_ — An _extended SSVN reference_, either an explicit namespace name or an implied namespace. Must evaluate to a quoted string, which is enclosed in either square brackets (["nspace"]) or vertical bars (|"nspace"|). Namespace names are not case-sensitive; they are stored and displayed in uppercase letters.`routine_name`An expression that evaluates to a string containing the name of a routine.\n" 67 | ], 68 | "link":"/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_sroutine" 69 | } 70 | ] -------------------------------------------------------------------------------- /src/providers/ObjectScriptHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { AtelierAPI } from "../api/index"; 4 | import { ClassDefinition } from "../utils/classDefinition"; 5 | import { currentFile } from "../utils"; 6 | import commands = require("./completion/commands.json"); 7 | import structuredSystemVariables = require("./completion/structuredSystemVariables.json"); 8 | import systemFunctions = require("./completion/systemFunctions.json"); 9 | import systemVariables = require("./completion/systemVariables.json"); 10 | 11 | export class ObjectScriptHoverProvider implements vscode.HoverProvider { 12 | public provideHover( 13 | document: vscode.TextDocument, 14 | position: vscode.Position, 15 | token: vscode.CancellationToken 16 | ): vscode.ProviderResult { 17 | if (!document.getWordRangeAtPosition(position)) { 18 | return; 19 | } 20 | return this.dollars(document, position) || this.commands(document, position); 21 | } 22 | 23 | public dollars(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { 24 | const word = document.getWordRangeAtPosition(position); 25 | const text = document.getText( 26 | new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word.end.character)) 27 | ); 28 | const file = currentFile(); 29 | 30 | const dollarsMatch = text.match(/(\^?\$+)(\b\w+\b)$/); 31 | if (dollarsMatch) { 32 | const range = document.getWordRangeAtPosition(position, /\^?\$+\b\w+\b$/); 33 | let search = dollarsMatch.shift(); 34 | const [dollars, value] = dollarsMatch; 35 | search = search.toUpperCase(); 36 | if (dollars === "$$$") { 37 | return this.macro(file.name, value).then((contents) => ({ 38 | contents: [contents.join("")], 39 | range, 40 | })); 41 | } else if (dollars === "$" || dollars === "^$") { 42 | let found = systemFunctions.find((el) => el.label === search || el.alias.includes(search)); 43 | found = found || systemVariables.find((el) => el.label === search || el.alias.includes(search)); 44 | found = found || structuredSystemVariables.find((el) => el.label === search || el.alias.includes(search)); 45 | if (found) { 46 | return { 47 | contents: [found.documentation.join(""), this.documentationLink(found.link)], 48 | range, 49 | }; 50 | } 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | public async macro(fileName: string, macro: string): Promise { 58 | const api = new AtelierAPI(); 59 | let includes = []; 60 | if (fileName.toLowerCase().endsWith(".cls")) { 61 | const classDefinition = new ClassDefinition(fileName); 62 | includes = await classDefinition.includeCode(); 63 | } else if (fileName.toLowerCase().endsWith(".inc")) { 64 | includes.push(fileName.replace(/\.inc$/i, "")); 65 | } 66 | return api 67 | .getmacrodefinition(fileName, macro, includes) 68 | .then((data) => 69 | data.result.content.definition.map((line: string) => (line.match(/^\s*#def/) ? line : `#define ${line}`)) 70 | ) 71 | .then((data) => ["```objectscript\n", ...data, "\n```"]); 72 | } 73 | 74 | public commands(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { 75 | const word = document.getWordRangeAtPosition(position); 76 | const text = document.getText( 77 | new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, word.end.character)) 78 | ); 79 | const commandMatch = text.match(/^\s+\b[a-z]+\b$/i); 80 | if (commandMatch) { 81 | const search = text.trim().toUpperCase(); 82 | const command = commands.find((el) => el.label === search || el.alias.includes(search)); 83 | if (search) { 84 | return { 85 | contents: [command.documentation.join(""), this.documentationLink(command.link)], 86 | range: word, 87 | }; 88 | } 89 | } 90 | } 91 | 92 | public documentationLink(link: string): string | null { 93 | if (link) { 94 | return `[Online documentation](${ 95 | link.startsWith("http") ? "" : "https://docs.intersystems.com/irislatest" 96 | }${link})`; 97 | } 98 | return; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/commands/jumpToTagAndOffset.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 3 | import { handleError } from "../utils"; 4 | 5 | export async function jumpToTagAndOffset(): Promise { 6 | const editor = vscode.window.activeTextEditor; 7 | if (!editor) return; 8 | const document = editor.document; 9 | if (!["objectscript", "objectscript-int"].includes(document.languageId)) { 10 | vscode.window.showWarningMessage("Jump to Tag and Offset only supports .int and .mac routines.", "Dismiss"); 11 | return; 12 | } 13 | 14 | // Get the labels from the document symbol provider 15 | const map = new Map(); 16 | const symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( 17 | "vscode.executeDocumentSymbolProvider", 18 | document.uri 19 | ); 20 | if (!Array.isArray(symbols) || !symbols.length) return; 21 | const items: vscode.QuickPickItem[] = symbols 22 | .filter((symbol) => symbol.kind === vscode.SymbolKind.Method) 23 | .map((symbol) => { 24 | map.set(symbol.name, symbol.range.start.line); 25 | return { 26 | label: symbol.name, 27 | }; 28 | }); 29 | const quickPick = vscode.window.createQuickPick(); 30 | quickPick.title = "Jump to Tag + Offset"; 31 | quickPick.items = items; 32 | quickPick.canSelectMany = false; 33 | quickPick.onDidAccept(() => { 34 | if ( 35 | quickPick.selectedItems.length && 36 | !new RegExp(`^${quickPick.selectedItems[0].label}(\\+\\d+)?$`).test(quickPick.value) 37 | ) { 38 | // Update the value to correct case and allow users to add/update the offset 39 | quickPick.value = quickPick.value.includes("+") 40 | ? `${quickPick.selectedItems[0].label}+${quickPick.value.split("+")[1]}` 41 | : quickPick.selectedItems[0].label; 42 | return; 43 | } 44 | const parts = quickPick.value.trim().split("+"); 45 | let offset = 0; 46 | if (parts[0].length) { 47 | const labelLine = map.get(parts[0]); 48 | if (labelLine == undefined) return; // Not a valid label 49 | offset = labelLine; 50 | } 51 | if (parts.length > 1) { 52 | offset += parseInt(parts[1], 10); 53 | } 54 | const line = document.lineAt(offset); 55 | const range = new vscode.Range(line.range.start, line.range.start); 56 | editor.selection = new vscode.Selection(range.start, range.start); 57 | editor.revealRange(range, vscode.TextEditorRevealType.AtTop); 58 | quickPick.hide(); 59 | }); 60 | quickPick.show(); 61 | } 62 | 63 | /** Prompt the user for an error location of the form `label+offset^routine`, then open it. */ 64 | export async function openErrorLocation(): Promise { 65 | // Prompt the user for a location 66 | const regex = /^(%?[\p{L}\d]+)?(?:\+(\d+))?\^(%?[\p{L}\d.]+)$/u; 67 | const location = await vscode.window.showInputBox({ 68 | title: "Enter the location to open", 69 | placeHolder: "label+offset^routine", 70 | validateInput: (v) => (regex.test(v.trim()) ? undefined : "Input is not in the format 'label+offset^routine'"), 71 | }); 72 | if (!location) { 73 | return; 74 | } 75 | const [, label, offset, routine] = location.trim().match(regex); 76 | // Get the uri for the routine 77 | const uri = DocumentContentProvider.getUri(`${routine}.int`); 78 | if (!uri) { 79 | return; 80 | } 81 | let selection = new vscode.Range(0, 0, 0, 0); 82 | try { 83 | if (label) { 84 | // Find the location of the tag within the document 85 | const symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( 86 | "vscode.executeDocumentSymbolProvider", 87 | uri 88 | ); 89 | for (const symbol of symbols) { 90 | if (symbol.name == label) { 91 | selection = new vscode.Range(symbol.selectionRange.start.line, 0, symbol.selectionRange.start.line, 0); 92 | break; 93 | } 94 | } 95 | } 96 | if (offset) { 97 | // Add the offset 98 | selection = new vscode.Range(selection.start.line + Number(offset), 0, selection.start.line + Number(offset), 0); 99 | } 100 | // Show the document 101 | await vscode.window.showTextDocument(uri, { preview: false, selection }); 102 | } catch (error) { 103 | handleError(error, `Failed to open routine '${routine}.int'.`); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/providers/ObjectScriptClassSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export class ObjectScriptClassSymbolProvider implements vscode.DocumentSymbolProvider { 4 | public provideDocumentSymbols( 5 | document: vscode.TextDocument, 6 | token: vscode.CancellationToken 7 | ): Thenable { 8 | return new Promise((resolve) => { 9 | let classItSelf = null; 10 | let symbols: vscode.DocumentSymbol[] = []; 11 | 12 | let inComment = false; 13 | for (let i = 0; i < document.lineCount; i++) { 14 | const line = document.lineAt(i); 15 | 16 | if (line.text.match(/\/\*/)) { 17 | inComment = true; 18 | } 19 | 20 | if (inComment) { 21 | if (line.text.match(/\*\//)) { 22 | inComment = false; 23 | } 24 | continue; 25 | } 26 | 27 | const classPat = line.text.match(/^(Class) (%?\b\w+\b(?:\.\b\w+\b)+)/i); 28 | if (classPat) { 29 | let end = new vscode.Position(document.lineCount - 1, 0); 30 | for (let j = document.lineCount - 1; j > i; j--) { 31 | if (document.lineAt(j).text.startsWith("}")) { 32 | end = new vscode.Position(j + 1, 0); 33 | break; 34 | } 35 | } 36 | classItSelf = new vscode.DocumentSymbol( 37 | classPat[2], 38 | "Class", 39 | vscode.SymbolKind.Class, 40 | new vscode.Range(new vscode.Position(0, 0), end), 41 | line.range 42 | ); 43 | symbols.push(classItSelf); 44 | symbols = classItSelf.children; 45 | } 46 | 47 | let start = line.range.start; 48 | for (let j = i; j > 1; j--) { 49 | if (!document.lineAt(j - 1).text.startsWith("/// ")) { 50 | start = document.lineAt(j).range.start; 51 | break; 52 | } 53 | } 54 | 55 | const method = line.text.match(/^((?:Client)?(?:Class)?Method|Trigger|Query) (%?\b\w+\b|"[^"]+")/i); 56 | if (method) { 57 | let startCode = line.range.start; 58 | let end = line.range.end; 59 | while (i++ && i < document.lineCount) { 60 | if (document.lineAt(i).text.match("^{")) { 61 | startCode = new vscode.Position(i + 1, 0); 62 | } 63 | if (document.lineAt(i).text.match("^}")) { 64 | end = document.lineAt(i).range.end; 65 | break; 66 | } 67 | } 68 | symbols.push({ 69 | children: undefined, 70 | detail: method[1], 71 | kind: vscode.SymbolKind.Method, 72 | name: method[2].replace(/"/g, ""), 73 | range: new vscode.Range(start, end), 74 | selectionRange: new vscode.Range(startCode, end), 75 | }); 76 | } 77 | 78 | const index = line.text.match(/^(Index|ForegnKey) (%?\b\w+\b)/i); 79 | if (index) { 80 | symbols.push({ 81 | children: undefined, 82 | detail: index[1], 83 | kind: vscode.SymbolKind.Key, 84 | name: index[2], 85 | range: new vscode.Range(start, line.range.end), 86 | selectionRange: line.range, 87 | }); 88 | } 89 | 90 | const property = line.text.match(/^(Property|Relationship) (%?\b\w+\b|"[^"]+")/i); 91 | if (property) { 92 | let end = line.range.end; 93 | if (!line.text.endsWith(";")) { 94 | while (i++ && i < document.lineCount) { 95 | if (document.lineAt(i).text.endsWith(";")) { 96 | end = document.lineAt(i).range.end; 97 | break; 98 | } 99 | } 100 | } 101 | symbols.push({ 102 | children: undefined, 103 | detail: property[1], 104 | kind: vscode.SymbolKind.Property, 105 | name: property[2], 106 | range: new vscode.Range(start, end), 107 | selectionRange: line.range, 108 | }); 109 | } 110 | 111 | const parameter = line.text.match(/^(Parameter) (%?\b\w+\b)/i); 112 | if (parameter) { 113 | symbols.push({ 114 | children: undefined, 115 | detail: parameter[1], 116 | kind: vscode.SymbolKind.Constant, 117 | name: parameter[2], 118 | range: new vscode.Range(start, line.range.end), 119 | selectionRange: line.range, 120 | }); 121 | } 122 | 123 | const other = line.text.match(/^(XData|Storage) (%?\b\w+\b)/i); 124 | if (other) { 125 | let startCode = line.range.start; 126 | let end = line.range.end; 127 | while (i++ && i < document.lineCount) { 128 | if (document.lineAt(i).text.match("^{")) { 129 | startCode = new vscode.Position(i + 1, 0); 130 | } 131 | if (document.lineAt(i).text.match("^}")) { 132 | end = document.lineAt(i).range.end; 133 | break; 134 | } 135 | } 136 | symbols.push({ 137 | children: undefined, 138 | detail: other[1], 139 | kind: vscode.SymbolKind.Struct, 140 | name: other[2], 141 | range: new vscode.Range(start, end), 142 | selectionRange: new vscode.Range(startCode, end), 143 | }); 144 | } 145 | } 146 | 147 | resolve([classItSelf]); 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /syntaxes/objectscript-macros.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "objectscript_macros", 4 | "scopeName": "source.objectscript_macros", 5 | "patterns": [ 6 | { 7 | "match": "^(ROUTINE)\\s(\\b[\\p{Alnum}.]+\\b)", 8 | "captures": { 9 | "1": { "name": "keyword.control" }, 10 | "2": { "name": "entity.name.class" } 11 | } 12 | }, 13 | { "include": "#include" }, 14 | { "include": "#dim" }, 15 | { "include": "#define" }, 16 | { "include": "#def1arg" }, 17 | { "include": "#ifdef" }, 18 | { "include": "#comment-line" }, 19 | { "include": "#sql" } 20 | ], 21 | 22 | "repository": { 23 | "include": { 24 | "patterns": [ 25 | { 26 | "begin": "^\\s*(\\#\\s*(?:(?i)include))\\s+([\\p{Alpha}%][\\p{Alnum}]*)", 27 | "end": "(?=$)", 28 | "beginCaptures": { 29 | "1": { "name": "keyword.other.objectscript" }, 30 | "2": { "name": "entity.name.objectscript" } 31 | } 32 | } 33 | ] 34 | }, 35 | "dim": { 36 | "patterns": [ 37 | { 38 | "name": "meta.preprocessor.objectscript", 39 | "match": "^\\s*(\\#\\s*(?:(?i)dim))\\s+((?[\\p{Alpha}%][\\p{Alnum}]*))(?:\\s*(,)\\s*((\\g)*))*(?:\\s+((?i)As)(?:\\s(\\g(?:\\.\\g)*)))?", 40 | "captures": { 41 | "1": { "name": "keyword.control.objectscript" }, 42 | "2": { "name": "variable.name" }, 43 | "4": { "name": "punctuation.definition.objectscript" }, 44 | "5": { "name": "variable.name" }, 45 | "7": { "name": "keyword.control.objectscript" }, 46 | "8": { "name": "entity.name.class.objectscript" } 47 | } 48 | } 49 | ] 50 | }, 51 | "define": { 52 | "patterns": [ 53 | { 54 | "name": "meta.preprocessor.objectscript", 55 | "begin": "^\\s*(\\#\\s*(?:(?i)define))\\s+((?[%\\p{Alnum}]*))(?:(\\()(\\s*\\g\\s*((,)\\s*\\g\\s*)*)(\\)))?", 56 | "beginCaptures": { 57 | "1": { "name": "keyword.control.objectscript" }, 58 | "2": { "name": "entity.name.objectscript" }, 59 | "4": { "name": "punctuation.definition.objectscript" }, 60 | "5": { "name": "variable.parameter.objectscript" }, 61 | "7": { "name": "punctuation.definition.objectscript" } 62 | }, 63 | "end": "(?[\\p{Alpha}%][\\p{Alnum}]*))(?:(\\()(\\s*\\g\\s*)(\\)))", 80 | "beginCaptures": { 81 | "1": { "name": "keyword.control.objectscript" }, 82 | "2": { "name": "entity.name.objectscript" }, 83 | "4": { "name": "punctuation.definition.objectscript" }, 84 | "5": { "name": "variable.parameter.objectscript" }, 85 | "6": { "name": "punctuation.definition.objectscript" } 86 | }, 87 | "end": "(?. 127 | 128 | For answers to common questions about this code of conduct, see the FAQ at 129 | . Translations are available at 130 | . 131 | 132 | -------------------------------------------------------------------------------- /src/debug/dbgp.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import * as WebSocket from "ws"; 3 | import * as iconv from "iconv-lite"; 4 | import { DOMParser } from "@xmldom/xmldom"; 5 | 6 | /** The encoding all XDebug messages are encoded with */ 7 | export const ENCODING = "iso-8859-1"; 8 | 9 | /** The two states the connection switches between */ 10 | enum ParsingState { 11 | DataLength, 12 | Response, 13 | } 14 | 15 | /** Wraps the NodeJS Socket and calls handleResponse() whenever a full response arrives */ 16 | export class DbgpConnection extends EventEmitter { 17 | private _socket: WebSocket; 18 | private _parsingState: ParsingState; 19 | private _chunksDataLength: number; 20 | private _chunks: Buffer[]; 21 | private _dataLength: number; 22 | private _parser: DOMParser; 23 | private _messages: Buffer[] = []; 24 | private _processingMessages = false; 25 | 26 | public constructor(socket: WebSocket) { 27 | super(); 28 | this._socket = socket; 29 | this._parsingState = ParsingState.DataLength; 30 | this._chunksDataLength = 0; 31 | this._chunks = []; 32 | socket.on("message", (data: string): void => { 33 | this._messages.push(Buffer.from(data)); 34 | if (!this._processingMessages) { 35 | this._processingMessages = true; 36 | this._handleDataChunk(); 37 | this._processingMessages = false; 38 | } 39 | }); 40 | socket.on("error", (error: Error): boolean => this.emit("error", error)); 41 | socket.on("close", (): boolean => this.emit("close")); 42 | this._parser = new DOMParser({ 43 | onError: (level, msg) => { 44 | if (level == "warning") { 45 | this.emit("warning", msg); 46 | } else { 47 | this.emit("error", new Error(msg)); 48 | } 49 | }, 50 | }); 51 | } 52 | 53 | public write(command: Buffer): Promise { 54 | return new Promise((resolve): void => { 55 | this._socket.send(command, (): void => { 56 | resolve(); 57 | }); 58 | }); 59 | } 60 | 61 | /** closes the underlying socket */ 62 | public close(): Promise { 63 | return new Promise((resolve, reject) => { 64 | this._socket.once("close", resolve); 65 | this._socket.close(); 66 | }); 67 | } 68 | 69 | private _handleDataChunk() { 70 | if (!this._messages.length) return; // Shouldn't ever happen 71 | const data: Buffer = this._messages.shift(); 72 | if (this._parsingState === ParsingState.DataLength) { 73 | // does data contain a NULL byte? 74 | const separatorIndex = data.indexOf("|"); 75 | if (separatorIndex !== -1) { 76 | // YES -> we received the data length and are ready to receive the response 77 | const lastPiece = data.slice(0, separatorIndex); 78 | this._chunks.push(lastPiece); 79 | this._chunksDataLength += lastPiece.length; 80 | this._dataLength = parseInt(iconv.decode(Buffer.concat(this._chunks, this._chunksDataLength), ENCODING)); 81 | // reset buffered chunks 82 | this._chunks = []; 83 | this._chunksDataLength = 0; 84 | // switch to response parsing state 85 | this._parsingState = ParsingState.Response; 86 | // if data contains more info (except the NULL byte) 87 | if (data.length > separatorIndex + 1) { 88 | // handle the rest of the packet as part of the response 89 | const rest = data.slice(separatorIndex + 1, this._dataLength + separatorIndex + 1); 90 | this._messages.unshift(rest); 91 | this._handleDataChunk(); 92 | // more then one data chunk in one message 93 | const restData = data.slice(this._dataLength + separatorIndex + 1); 94 | if (restData.length) { 95 | this._messages.unshift(restData); 96 | this._handleDataChunk(); 97 | } 98 | } 99 | } else { 100 | // NO -> this is only part of the data length. We wait for the next data event 101 | this._chunks.push(data); 102 | this._chunksDataLength += data.length; 103 | } 104 | } else if (this._parsingState === ParsingState.Response) { 105 | // does the new data together with the buffered data add up to the data length? 106 | if (this._chunksDataLength + data.length >= this._dataLength) { 107 | // YES -> we received the whole response 108 | // append the last piece of the response 109 | const lastResponsePiece = data.slice(0, this._dataLength - this._chunksDataLength); 110 | this._chunks.push(lastResponsePiece); 111 | this._chunksDataLength += lastResponsePiece.length; 112 | const response = Buffer.concat(this._chunks, this._chunksDataLength).toString("ascii"); 113 | // call response handler 114 | const xml = iconv.decode(Buffer.from(response, "base64"), ENCODING); 115 | const document = this._parser.parseFromString(xml, "application/xml"); 116 | this.emit("message", document); 117 | // reset buffer 118 | this._chunks = []; 119 | this._chunksDataLength = 0; 120 | // switch to data length parsing state 121 | this._parsingState = ParsingState.DataLength; 122 | // if data contains more info 123 | if (data.length > lastResponsePiece.length) { 124 | // handle the rest of the packet as data length 125 | const rest = data.slice(lastResponsePiece.length); 126 | this._messages.unshift(rest); 127 | this._handleDataChunk(); 128 | } 129 | } else { 130 | // NO -> this is not the whole response yet. We buffer it and wait for the next data event. 131 | this._chunks.push(data); 132 | this._chunksDataLength += data.length; 133 | } 134 | } 135 | while (this._messages.length) this._handleDataChunk(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: CI-prerelease 2 | 3 | on: 4 | push: 5 | branches: 6 | - prerelease 7 | paths-ignore: 8 | - ".vscode/**" 9 | - ".github/**" 10 | - "**/*.md" 11 | pull_request: 12 | branches: 13 | - prerelease 14 | release: 15 | types: 16 | - released 17 | jobs: 18 | build: 19 | timeout-minutes: 10 20 | runs-on: ubuntu-latest 21 | outputs: 22 | taggedbranch: ${{ steps.find-branch.outputs.taggedbranch }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 26 | - name: Find which branch the release tag points at 27 | id: find-branch 28 | if: github.event_name == 'release' 29 | shell: bash 30 | run: | 31 | git fetch --depth=1 origin +refs/heads/*:refs/heads/* 32 | set -x 33 | TAGGEDBRANCH=$(git for-each-ref --points-at=${{github.sha}} --format='%(refname:lstrip=2)' refs/heads/) 34 | echo "taggedbranch=$TAGGEDBRANCH" >> $GITHUB_OUTPUT 35 | - name: Set an output 36 | id: set-version 37 | run: | 38 | set -x 39 | VERSION=$(jq -r '.version' package.json | cut -d- -f1) 40 | [ $GITHUB_EVENT_NAME == 'release' ] && VERSION=${{ github.event.release.tag_name }} && VERSION=${VERSION/v/} 41 | CHANGELOG=$(cat CHANGELOG.md | sed -n "/## \[${VERSION}\]/,/## /p" | sed '/^$/d;1d;$d') 42 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 43 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 44 | git tag -l | cat 45 | [ $GITHUB_EVENT_NAME == 'push' ] && VERSION+=-beta && VERSION+=.$(($(git tag -l "v$VERSION.*" | sort -nt. -k4 2>/dev/null | tail -1 | cut -d. -f4)+1)) 46 | [ $GITHUB_EVENT_NAME == 'pull_request' ] && VERSION+=-dev.${{ github.event.pull_request.number }} 47 | echo "version=$VERSION" >> $GITHUB_OUTPUT 48 | NAME=$(jq -r '.name' package.json)-$VERSION 49 | echo "name=$NAME" >> $GITHUB_OUTPUT 50 | tmp=$(mktemp) 51 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 52 | mkdir dist 53 | echo $VERSION > meta.version 54 | echo $NAME > meta.name 55 | - name: Use Node.js 56 | uses: actions/setup-node@v4 57 | with: 58 | node-version: 20 59 | - run: npm install 60 | - name: lint 61 | run: npm run lint 62 | - run: npm run compile 63 | - name: npm test 64 | run: xvfb-run npm test 65 | - name: Build pre-release package 66 | run: | 67 | npx vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix 68 | - uses: actions/upload-artifact@v4 69 | if: github.event_name != 'release' 70 | with: 71 | name: ${{ steps.set-version.outputs.name }}.vsix 72 | path: ${{ steps.set-version.outputs.name }}.vsix 73 | - uses: actions/upload-artifact@v4 74 | with: 75 | name: meta 76 | path: | 77 | meta.name 78 | meta.version 79 | beta: 80 | if: (github.event_name == 'push') 81 | runs-on: ubuntu-latest 82 | needs: build 83 | steps: 84 | - uses: actions/download-artifact@v4 85 | with: 86 | name: meta 87 | path: . 88 | - name: Set an output 89 | id: set-version 90 | run: | 91 | set -x 92 | echo "version=`cat meta.version`" >> $GITHUB_OUTPUT 93 | echo "name=`cat meta.name`" >> $GITHUB_OUTPUT 94 | - uses: actions/download-artifact@v4 95 | with: 96 | name: ${{ steps.set-version.outputs.name }}.vsix 97 | - name: Create Pre-Release 98 | id: create-release 99 | uses: softprops/action-gh-release@v1 100 | with: 101 | tag_name: v${{ steps.set-version.outputs.version }} 102 | prerelease: ${{ github.event_name != 'release' }} 103 | files: ${{ steps.set-version.outputs.name }}.vsix 104 | token: ${{ secrets.GITHUB_TOKEN }} 105 | publish: 106 | needs: build 107 | if: github.event_name == 'release' && needs.build.outputs.taggedbranch == 'prerelease' 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | with: 112 | ref: prerelease 113 | token: ${{ secrets.TOKEN }} 114 | - uses: actions/download-artifact@v4 115 | with: 116 | name: meta 117 | path: . 118 | - name: Use Node.js 119 | uses: actions/setup-node@v4 120 | with: 121 | node-version: 20 122 | - name: Prepare pre-release build 123 | id: set-version 124 | run: | 125 | VERSION=`cat meta.version` 126 | NEXT_VERSION=`cat meta.version | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.` 127 | echo "name=`cat meta.name`" >> $GITHUB_OUTPUT 128 | tmp=$(mktemp) 129 | git config --global user.name 'ProjectBot' 130 | git config --global user.email 'bot@users.noreply.github.com' 131 | jq --arg version "${NEXT_VERSION}-SNAPSHOT" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 132 | git add package.json 133 | git commit -m 'auto bump version after pre-release' 134 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 135 | npm install 136 | jq 'del(.enableProposedApi,.enabledApiProposals)' package.json > "$tmp" && mv "$tmp" package.json 137 | git push 138 | - name: Build pre-release package 139 | run: | 140 | npx vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix 141 | - name: Upload Release Asset 142 | id: upload-release-asset 143 | uses: softprops/action-gh-release@v1 144 | with: 145 | tag_name: ${{ github.event.release.tag_name }} 146 | files: ${{ steps.set-version.outputs.name }}.vsix 147 | token: ${{ secrets.GITHUB_TOKEN }} 148 | - name: Publish to VSCode Marketplace 149 | run: | 150 | [ -n "${{ secrets.VSCE_TOKEN }}" ] && \ 151 | npx vsce publish --pre-release --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true 152 | -------------------------------------------------------------------------------- /snippets/objectscript-class.json: -------------------------------------------------------------------------------- 1 | { 2 | "ClassMethod definition": { 3 | "prefix": "ClassMethod", 4 | "body": [ 5 | "/// ${1:Description}", 6 | "ClassMethod ${2:MethodName}($3) As ${4:%Status}", 7 | "{", 8 | "\tSet ${5:sc} = \\$\\$\\$OK", 9 | "\t${0:// do something}", 10 | "\tReturn $5", 11 | "}" 12 | ] 13 | }, 14 | "Method definition": { 15 | "prefix": "Method", 16 | "body": [ 17 | "/// ${1:Description}", 18 | "Method ${2:MethodName}($3) As ${4:%Status}", 19 | "{", 20 | "\tSet ${5:sc} = \\$\\$\\$OK", 21 | "\t${0:// do something}", 22 | "\tReturn $5", 23 | "}" 24 | ] 25 | }, 26 | "Property": { 27 | "prefix": "Property", 28 | "body": [ 29 | "/// ${1:Description}", 30 | "Property ${2:PropertyName} As ${3:%String};" 31 | ] 32 | }, 33 | "Projection": { 34 | "prefix": "Projection", 35 | "body": [ 36 | "/// ${1:Description}", 37 | "Projection ${2:ProjectionName} As ${3:PackageName.ProjectionClassName};" 38 | ] 39 | }, 40 | "Unique Property": { 41 | "prefix": ["Unique", "Property"], 42 | "body": [ 43 | "/// ${1:Description}", 44 | "Property ${2:PropertyName} As ${3:%String};", 45 | "", 46 | "Index $2Index On $2 [Unique];" 47 | ] 48 | }, 49 | "Always-Computed Property": { 50 | "prefix": ["Computed", "Property"], 51 | "body" : [ 52 | "/// ${1:Description}", 53 | "Property ${2:PropertyName} As ${3:%String} [Calculated, SqlComputed, SqlComputeCode =", 54 | "\t{Set {$2} = {${4:expression}}}", 55 | "];" 56 | ] 57 | }, 58 | "Date/Time Property": { 59 | "prefix": ["Date", "Time", "Property"], 60 | "body" : [ 61 | "/// ${1:Description}", 62 | "Property ${2:PropertyName} As ${3|%Date,%Time|}(MINVAL = $4, MAXVAL = $5);" 63 | ] 64 | }, 65 | "Parameter": { 66 | "prefix": "Parameter", 67 | "body": [ 68 | "/// ${1:Description}", 69 | "Parameter ${2:PARAMETERNAME} = \"$0\";" 70 | ] 71 | }, 72 | "Index": { 73 | "prefix": "Index", 74 | "body": [ 75 | "/// ${1:Description}", 76 | "Index ${2:IndexName} On ${3:property};" 77 | ] 78 | }, 79 | "Unique Index": { 80 | "prefix": "Index", 81 | "body": [ 82 | "/// ${1:Description}", 83 | "Index ${2:IndexName} On ${3:property} [Unique];" 84 | ], 85 | "description": "Unique Index" 86 | }, 87 | "Basic Class Query": { 88 | "prefix":["Query"], 89 | "body":[ 90 | "/// ${1:Description}", 91 | "Query ${2:QueryName}($3) As %SQLQuery [ SqlProc ]", 92 | "{", 93 | "\tSELECT ${4:select-items}", 94 | "\tFROM ${5:table-refs}", 95 | "\tWHERE ${6:condition-expression}", 96 | "\tORDER BY ${7:ordering-items}", 97 | "}" 98 | ], 99 | "description": "Basic class query (%SQLQuery)" 100 | }, 101 | "Custom Class Query": { 102 | "prefix":["Query"], 103 | "body":[ 104 | "/// ${1:Description}", 105 | "Query ${2:QueryName}($3) As %Query(ROWSPEC = \"$4\") [ SqlProc ]", 106 | "{", 107 | "}", 108 | "", 109 | "ClassMethod ${2:QueryName}Execute(ByRef qHandle As %Binary${3/(\\s)|(.*)/${2:+, }$2/}) As %Status", 110 | "{", 111 | "\tQuit \\$\\$\\$OK", 112 | "}", 113 | "", 114 | "ClassMethod ${2:QueryName}Close(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ${2:QueryName}Execute ]", 115 | "{", 116 | "\tQuit \\$\\$\\$OK", 117 | "}", 118 | "", 119 | "ClassMethod ${2:QueryName}Fetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ${2:QueryName}Execute ]", 120 | "{", 121 | "\tQuit \\$\\$\\$OK", 122 | "}" 123 | ], 124 | "description": "Custom class query (%Query)" 125 | }, 126 | "Trigger": { 127 | "prefix": "Trigger", 128 | "body": [ 129 | "/// ${1:Description}", 130 | "Trigger ${2:TriggerName} [Event=${3|INSERT,UPDATE,DELETE|}, Time=${4|BEFORE,AFTER|}, Foreach=${5|row/object,row,statement|}]", 131 | "{", 132 | "\t${0:// do something}", 133 | "}" 134 | ], 135 | "description": "Trigger" 136 | }, 137 | "ForeignKey": { 138 | "prefix": "ForeignKey", 139 | "body": [ 140 | "/// ${1:Description}", 141 | "ForeignKey ${2:ForeignKeyName}(${3:property}) References ${4:referencedClass}(${5:refIndex});" 142 | ], 143 | "description": "ForeignKey" 144 | }, 145 | "Relationship": { 146 | "prefix": ["Relationship"], 147 | "body": [ 148 | "/// ${1:Description}", 149 | "Relationship ${2:RelationshipName} As ${3:classname} [ Cardinality = ${4|one,many,parent,children|}, Inverse = ${5:correspondingProperty} ];" 150 | ], 151 | "description": "Relationship" 152 | }, 153 | "XData": { 154 | "prefix": "XData", 155 | "body": [ 156 | "/// ${1:Description}", 157 | "XData ${2:XDataName}", 158 | "{", 159 | "$0", 160 | "}" 161 | ], 162 | "description": "XData" 163 | }, 164 | "Production": { 165 | "prefix": ["Production","Interoperability","ClassProduction"], 166 | "body": [ 167 | "/// ${1:Description}", 168 | "Class ${2:${TM_DIRECTORY/^.+[\\/\\\\](.*)$/$1/}.$TM_FILENAME_BASE} Extends Ens.Production", 169 | "{", 170 | "", 171 | "XData ProductionDefinition", 172 | "{", 173 | "\t", 174 | "\t\t2", 175 | "\t\t", 176 | "\t", 177 | "}", 178 | "}" 179 | ], 180 | "description": "Production Definition" 181 | }, 182 | "Request": { 183 | "prefix": ["Request","Interoperability","ClassRequest"], 184 | "body": [ 185 | "/// ${1:Description}", 186 | "Class ${2:${TM_DIRECTORY/^.+[\\/\\\\](.*)$/$1/}.$TM_FILENAME_BASE} Extends Ens.Request", 187 | "{", 188 | "$0", 189 | "}" 190 | ], 191 | "description": "Request Message Definition" 192 | }, 193 | "Response": { 194 | "prefix": ["Response","Interoperability","ClassResponse"], 195 | "body": [ 196 | "/// ${1:Description}", 197 | "Class ${2:${TM_DIRECTORY/^.+[\\/\\\\](.*)$/$1/}.$TM_FILENAME_BASE} Extends Ens.Response", 198 | "{", 199 | "$0", 200 | "}" 201 | ], 202 | "description": "Response Message Definition" 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - ".vscode/**" 9 | - ".github/**" 10 | - "**/*.md" 11 | pull_request: 12 | branches: 13 | - master 14 | release: 15 | types: 16 | - released 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.base_ref || github.run_id }} 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build: 24 | timeout-minutes: 10 25 | runs-on: ubuntu-latest 26 | outputs: 27 | taggedbranch: ${{ steps.find-branch.outputs.taggedbranch }} 28 | steps: 29 | - uses: actions/checkout@v4 30 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 31 | - name: Find which branch the release tag points at 32 | id: find-branch 33 | if: github.event_name == 'release' 34 | shell: bash 35 | run: | 36 | git fetch --depth=1 origin +refs/heads/*:refs/heads/* 37 | set -x 38 | TAGGEDBRANCH=$(git for-each-ref --points-at=${{github.sha}} --format='%(refname:lstrip=2)' refs/heads/) 39 | echo "taggedbranch=$TAGGEDBRANCH" >> $GITHUB_OUTPUT 40 | - name: Set an output 41 | id: set-version 42 | run: | 43 | set -x 44 | VERSION=$(jq -r '.version' package.json | cut -d- -f1) 45 | [ $GITHUB_EVENT_NAME == 'release' ] && VERSION=${{ github.event.release.tag_name }} && VERSION=${VERSION/v/} 46 | CHANGELOG=$(cat CHANGELOG.md | sed -n "/## \[${VERSION}\]/,/## /p" | sed '/^$/d;1d;$d') 47 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 48 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 49 | git tag -l | cat 50 | [ $GITHUB_EVENT_NAME == 'push' ] && VERSION+=-beta && VERSION+=.$(($(git tag -l "v$VERSION.*" | sort -nt. -k4 2>/dev/null | tail -1 | cut -d. -f4)+1)) 51 | [ $GITHUB_EVENT_NAME == 'pull_request' ] && VERSION+=-dev.${{ github.event.pull_request.number }} 52 | echo "version=$VERSION" >> $GITHUB_OUTPUT 53 | NAME=$(jq -r '.name' package.json)-$VERSION 54 | echo "name=$NAME" >> $GITHUB_OUTPUT 55 | tmp=$(mktemp) 56 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 57 | mkdir dist 58 | echo $VERSION > meta.version 59 | echo $NAME > meta.name 60 | - name: Use Node.js 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version: 20 64 | - run: npm install 65 | - name: lint 66 | run: npm run lint 67 | - run: npm run compile 68 | - name: npm test 69 | run: xvfb-run npm test 70 | - name: Build package 71 | run: | 72 | npx vsce package -o ${{ steps.set-version.outputs.name }}.vsix 73 | - uses: actions/upload-artifact@v4 74 | if: github.event_name != 'release' 75 | with: 76 | name: ${{ steps.set-version.outputs.name }}.vsix 77 | path: ${{ steps.set-version.outputs.name }}.vsix 78 | - uses: actions/upload-artifact@v4 79 | with: 80 | name: meta 81 | path: | 82 | meta.name 83 | meta.version 84 | beta: 85 | if: (github.event_name == 'push') 86 | runs-on: ubuntu-latest 87 | needs: build 88 | steps: 89 | - uses: actions/download-artifact@v4 90 | with: 91 | name: meta 92 | path: . 93 | - name: Set an output 94 | id: set-version 95 | run: | 96 | set -x 97 | echo "version=`cat meta.version`" >> $GITHUB_OUTPUT 98 | echo "name=`cat meta.name`" >> $GITHUB_OUTPUT 99 | - uses: actions/download-artifact@v4 100 | with: 101 | name: ${{ steps.set-version.outputs.name }}.vsix 102 | - name: Create Release 103 | id: create-release 104 | uses: softprops/action-gh-release@v1 105 | with: 106 | tag_name: v${{ steps.set-version.outputs.version }} 107 | prerelease: ${{ github.event_name != 'release' }} 108 | files: ${{ steps.set-version.outputs.name }}.vsix 109 | token: ${{ secrets.GITHUB_TOKEN }} 110 | publish: 111 | needs: build 112 | if: github.event_name == 'release' && needs.build.outputs.taggedbranch == 'master' 113 | runs-on: ubuntu-latest 114 | steps: 115 | - uses: actions/checkout@v4 116 | with: 117 | ref: master 118 | token: ${{ secrets.TOKEN }} 119 | - uses: actions/download-artifact@v4 120 | with: 121 | name: meta 122 | path: . 123 | - name: Use Node.js 124 | uses: actions/setup-node@v4 125 | with: 126 | node-version: 20 127 | - name: Prepare build 128 | id: set-version 129 | run: | 130 | VERSION=`cat meta.version` 131 | NEXT_VERSION=`cat meta.version | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.` 132 | echo "name=`cat meta.name`" >> $GITHUB_OUTPUT 133 | tmp=$(mktemp) 134 | git config --global user.name 'ProjectBot' 135 | git config --global user.email 'bot@users.noreply.github.com' 136 | jq --arg version "${NEXT_VERSION}-SNAPSHOT" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 137 | git add package.json 138 | git commit -m 'auto bump version with release' 139 | jq --arg version "$VERSION" '.version = $version' package.json > "$tmp" && mv "$tmp" package.json 140 | npm install 141 | jq 'del(.enableProposedApi,.enabledApiProposals)' package.json > "$tmp" && mv "$tmp" package.json 142 | git push 143 | - name: Build package 144 | run: | 145 | npx vsce package -o ${{ steps.set-version.outputs.name }}.vsix 146 | - name: Upload Release Asset 147 | id: upload-release-asset 148 | uses: softprops/action-gh-release@v1 149 | with: 150 | tag_name: ${{ github.event.release.tag_name }} 151 | files: ${{ steps.set-version.outputs.name }}.vsix 152 | token: ${{ secrets.GITHUB_TOKEN }} 153 | - name: Publish to VSCode Marketplace 154 | run: | 155 | [ -n "${{ secrets.VSCE_TOKEN }}" ] && \ 156 | npx vsce publish --packagePath ${{ steps.set-version.outputs.name }}.vsix -p ${{ secrets.VSCE_TOKEN }} || true 157 | - name: Publish to Open VSX Registry 158 | timeout-minutes: 5 159 | run: | 160 | [ -n "${{ secrets.OVSX_TOKEN }}" ] && \ 161 | npx ovsx publish ${{ steps.set-version.outputs.name }}.vsix --pat ${{ secrets.OVSX_TOKEN }} || true 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Known Vulnerabilities](https://snyk.io/test/github/intersystems-community/vscode-objectscript/badge.svg)](https://snyk.io/test/github/intersystems-community/vscode-objectscript) 2 | ![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/intersystems-community.vscode-objectscript.svg) 3 | [![](https://img.shields.io/visual-studio-marketplace/i/intersystems-community.vscode-objectscript.svg)](https://marketplace.visualstudio.com/items?itemName=intersystems-community.vscode-objectscript) 4 | 5 | [![](https://img.shields.io/badge/InterSystems-IRIS-blue.svg)](https://www.intersystems.com/products/intersystems-iris/) 6 | [![](https://img.shields.io/badge/InterSystems-Caché-blue.svg)](https://www.intersystems.com/products/cache/) 7 | [![](https://img.shields.io/badge/InterSystems-Ensemble-blue.svg)](https://www.intersystems.com/products/ensemble/) 8 | 9 | # InterSystems ObjectScript extension for VS Code 10 | 11 | > **Note:** The best way to install and use this extension is by installing the [InterSystems ObjectScript Extension Pack](https://marketplace.visualstudio.com/items?itemName=intersystems-community.objectscript-pack) and following the [documentation here](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO). 12 | 13 | [InterSystems®](http://www.intersystems.com) ObjectScript language support for Visual Studio Code, from the [InterSystems Developer Community](https://community.intersystems.com/). 14 | 15 | - Documentation on the [InterSystems Documentation site](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO). 16 | 17 | - Guidance on [reporting issues](https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=GVSCO_reporting). 18 | 19 | ## Features 20 | 21 | - InterSystems ObjectScript code highlighting support. 22 | - Debugging ObjectScript code. 23 | - Intellisense support for commands, system functions, and class members. 24 | - Export of existing server sources into a working folder: 25 | - open Command Palette (F1 or /Ctrl+Shift+P) 26 | - start typing 'ObjectScript' 27 | - choose `ObjectScript: Export Code from Server` 28 | - press Enter 29 | - Save and compile a class: 30 | - press /Ctrl+F7 31 | - or, select `ObjectScript: Import and Compile Current File` from Command Palette 32 | - Direct access to edit or view server code in the VS Code Explorer via `isfs` and `isfs-readonly` FileSystemProviders (e.g. using a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces)). Server-side source control is respected. 33 | - Server Explorer view (InterSystems: Explorer) with ability to export items to your working folder. 34 | - Integration with with [InterSystems Server Manager](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) for secure storage of connection passwords. 35 | 36 | ## Installation 37 | 38 | Install [Visual Studio Code](https://code.visualstudio.com/) first. 39 | 40 | Then to get a set of extensions that collaborate to bring you a great ObjectScript development experience, install the [InterSystems ObjectScript Extension Pack](https://marketplace.visualstudio.com/items?itemName=intersystems-community.objectscript-pack). 41 | 42 | When you install an extension pack VS Code installs any of its members that you don't already have. Then if you ever need to switch off all of those extensions (for example, in a VS Code workspace on a non-ObjectScript project) simply disable the extension pack at the desired level. Member extensions can still be managed individually. 43 | 44 | Open VS Code. Go to Extensions view (/Ctrl+Shift+X), use the search string **@id:intersystems-community.objectscript-pack** and install it. 45 | 46 | ## Enable Proposed APIs 47 | 48 | This extension is able to to take advantage of some VS Code APIs that have not yet been finalized. 49 | 50 | The additional features (and the APIs used) are: 51 | - Server-side [searching across files](https://code.visualstudio.com/docs/editor/codebasics#_search-across-files) being accessed using isfs (_TextSearchProvider_) 52 | - [Quick Open](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_quick-open) of isfs files (_FileSearchProvider_). 53 | 54 | To unlock these features (optional): 55 | 56 | 1. Download and install a beta version from GitHub. This is necessary because Marketplace does not allow publication of extensions that use proposed APIs. 57 | - Go to https://github.com/intersystems-community/vscode-objectscript/releases 58 | - Locate the beta immediately above the release you installed from Marketplace. For instance, if you installed `3.2.0`, look for `3.2.1-beta.1`. This will be functionally identical to the Marketplace version apart from being able to use proposed APIs. 59 | - Download the VSIX file (for example `vscode-objectscript-3.2.1-beta.1.vsix`) and install it. One way to install a VSIX is to drag it from your download folder and drop it onto the list of extensions in the Extensions view of VS Code. 60 | 61 | 2. From [Command Palette](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) choose `Preferences: Configure Runtime Arguments`. 62 | 3. In the argv.json file that opens, add this line (required for both Stable and Insiders versions of VS Code): 63 | ```json 64 | "enable-proposed-api": ["intersystems-community.vscode-objectscript"] 65 | ``` 66 | 4. Exit VS Code and relaunch it. 67 | 5. Verify that the ObjectScript channel of the Output panel reports this: 68 | ``` 69 | intersystems-community.vscode-objectscript version X.Y.Z-beta.1 activating with proposed APIs available. 70 | ``` 71 | 72 | After a subsequent update of the extension from Marketplace you will only have to download and install the new `vscode-objectscript-X.Y.Z-beta.1` VSIX. None of the other steps above are needed again. 73 | 74 | ## Notes 75 | 76 | - Connection-related output appears in the 'Output' view while switched to the 'ObjectScript' channel using the drop-down menu on the view titlebar. 77 | 78 | - The `/api/atelier/` web application used by this extension usually requires the authenticated user to have Use permission on the %Development resource ([read more](https://community.intersystems.com/post/using-atelier-rest-api)). One way is to assign the %Developer role to the user. 79 | 80 | - If you are getting `ERROR # 5540: SQLCODE: -99 Message: User xxx is not privileged for the operation` when you try to get or refresh lists of classes, routines or includes, then grant user xxx (or a SQL role they hold) Execute permission for the following SQL Procedure in the target namespace. 81 | 82 | ```SQL 83 | GRANT EXECUTE ON %Library.RoutineMgr_StudioOpenDialog TO xxx 84 | ``` 85 | -------------------------------------------------------------------------------- /src/commands/viewOthers.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 4 | import { currentFile, handleError, parseClassMemberDefinition } from "../utils"; 5 | 6 | export async function viewOthers(forceEditable = false): Promise { 7 | const file = currentFile(); 8 | if (!file) return; 9 | 10 | const open = async (item: string, forceEditable: boolean) => { 11 | const colonidx: number = item.indexOf(":"); 12 | if (colonidx !== -1) { 13 | // A location is appened to the name of the other document 14 | const options: vscode.TextDocumentShowOptions = {}; 15 | 16 | // Split the document name form the location 17 | let loc = item.slice(colonidx + 1); 18 | item = item.slice(0, colonidx); 19 | let uri: vscode.Uri; 20 | if (forceEditable) { 21 | uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable); 22 | } else { 23 | uri = DocumentContentProvider.getUri(item); 24 | } 25 | 26 | if (item.endsWith(".cls")) { 27 | // Locations in classes are of the format method+offset+namespace 28 | loc = loc.slice(0, loc.lastIndexOf("+")); 29 | let method = loc.slice(0, loc.lastIndexOf("+")); 30 | 31 | // Properly delimit method name if it contains invalid characters 32 | if (method.match(/(^([A-Za-z]|%)$)|(^([A-Za-z]|%)([A-Za-z]|\d|[^\x20-\x7F])+$)/g) === null) { 33 | method = '"' + method.replace(/"/g, '""') + '"'; 34 | } 35 | 36 | // Find the location of the given method in the class 37 | const symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( 38 | "vscode.executeDocumentSymbolProvider", 39 | uri 40 | ); 41 | if (symbols !== undefined) { 42 | for (const symbol of symbols[0].children) { 43 | if (symbol.name === method) { 44 | // This is symbol that the location is in 45 | const doc = await vscode.workspace.openTextDocument(uri); 46 | 47 | // Need to find the actual start of the method 48 | for ( 49 | let methodlinenum = symbol.selectionRange.start.line; 50 | methodlinenum <= symbol.range.end.line; 51 | methodlinenum++ 52 | ) { 53 | const methodlinetext: string = doc.lineAt(methodlinenum).text.trim(); 54 | if (methodlinetext.endsWith("{")) { 55 | // This is the last line of the method definition, so count from here 56 | const selectionline: number = methodlinenum + (+loc.slice(loc.lastIndexOf("+") + 1) || 0); 57 | options.selection = new vscode.Range(selectionline, 0, selectionline, 0); 58 | break; 59 | } 60 | } 61 | break; 62 | } 63 | } 64 | } 65 | } else { 66 | if (item.endsWith(".mac")) { 67 | // Locations in MAC routines are of the format +offset+namespace 68 | loc = loc.slice(0, loc.lastIndexOf("+")); 69 | } 70 | // Locations in INT routines are of the format +offset 71 | const linenum: number = +loc.slice(1) || 0; 72 | options.selection = new vscode.Range(linenum, 0, linenum, 0); 73 | } 74 | vscode.window.showTextDocument(uri, options); 75 | } else { 76 | let uri: vscode.Uri; 77 | if (forceEditable) { 78 | uri = DocumentContentProvider.getUri(item, undefined, undefined, forceEditable); 79 | } else { 80 | uri = DocumentContentProvider.getUri(item); 81 | } 82 | vscode.window.showTextDocument(uri); 83 | } 84 | }; 85 | 86 | const getOthers = (info) => { 87 | return info.result.content[0].others; 88 | }; 89 | 90 | const api = new AtelierAPI(file.uri); 91 | if (!api.active) return; 92 | let indexarg: string = file.name; 93 | const cursorpos: vscode.Position = vscode.window.activeTextEditor.selection.active; 94 | const fileExt: string = file.name.split(".").pop().toLowerCase(); 95 | 96 | if ( 97 | api.config.apiVersion >= 4 && 98 | (fileExt === "cls" || fileExt === "mac" || fileExt === "int") && 99 | !/^%sqlcq/i.test(indexarg) 100 | ) { 101 | // Send the server the current position in the document appended to the name if it supports it 102 | let symbols: vscode.DocumentSymbol[] = await vscode.commands.executeCommand( 103 | "vscode.executeDocumentSymbolProvider", 104 | file.uri 105 | ); 106 | if (symbols !== undefined) { 107 | if (fileExt === "cls") { 108 | symbols = symbols[0].children; 109 | } 110 | 111 | let currentSymbol: vscode.DocumentSymbol; 112 | for (const symbol of symbols) { 113 | if (symbol.range.contains(cursorpos)) { 114 | currentSymbol = symbol; 115 | break; 116 | } 117 | } 118 | 119 | if ( 120 | currentSymbol !== undefined && 121 | currentSymbol.kind === vscode.SymbolKind.Method && 122 | currentSymbol.detail.toLowerCase() !== "query" && 123 | currentSymbol.name.charAt(0) !== '"' && 124 | currentSymbol.name.charAt(currentSymbol.name.length - 1) !== '"' 125 | ) { 126 | // The current position is in a symbol that we can convert into a label+offset that the server understands 127 | let offset: number = cursorpos.line - currentSymbol.selectionRange.start.line; 128 | 129 | let isObjectScript = true; 130 | if (fileExt === "cls") { 131 | const memberInfo = parseClassMemberDefinition(vscode.window.activeTextEditor.document, currentSymbol); 132 | if (memberInfo) { 133 | const { defEndLine, language } = memberInfo; 134 | offset = cursorpos.line - defEndLine; 135 | isObjectScript = ["cache", "objectscript"].includes(language); 136 | } 137 | } 138 | 139 | offset = offset < 0 ? 0 : offset; 140 | if (isObjectScript) { 141 | // Only provide label+offset if method language is ObjectScript 142 | indexarg = indexarg + ":" + currentSymbol.name + "+" + offset; 143 | } 144 | } 145 | } 146 | } 147 | 148 | return api 149 | .actionIndex([indexarg]) 150 | .then((info) => { 151 | const listOthers = getOthers(info) || []; 152 | if (!listOthers.length) { 153 | vscode.window.showInformationMessage("There are no other documents to open.", "Dismiss"); 154 | return; 155 | } 156 | if (listOthers.length === 1) { 157 | open(listOthers[0], forceEditable); 158 | } else { 159 | vscode.window.showQuickPick(listOthers).then((item) => { 160 | open(item, forceEditable); 161 | }); 162 | } 163 | }) 164 | .catch((error) => handleError(error, "Failed to get other documents.")); 165 | } 166 | -------------------------------------------------------------------------------- /src/commands/connectFolderToServerNamespace.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { 4 | panel, 5 | resolveConnectionSpec, 6 | getResolvedConnectionSpec, 7 | serverManagerApi, 8 | resolveUsernameAndPassword, 9 | } from "../extension"; 10 | import { handleError, isUnauthenticated, notIsfs, displayableUri } from "../utils"; 11 | 12 | interface ConnSettings { 13 | server: string; 14 | ns: string; 15 | active: boolean; 16 | } 17 | 18 | export async function connectFolderToServerNamespace(): Promise { 19 | if (!vscode.workspace.workspaceFolders?.length) { 20 | vscode.window.showErrorMessage("No folders in the workspace.", "Dismiss"); 21 | return; 22 | } 23 | if (!serverManagerApi) { 24 | vscode.window.showErrorMessage( 25 | "Connecting a folder to a server namespace requires the [InterSystems Server Manager extension](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) to be installed and enabled.", 26 | "Dismiss" 27 | ); 28 | return; 29 | } 30 | // Which folder? 31 | const items: vscode.QuickPickItem[] = vscode.workspace.workspaceFolders 32 | .filter((folder) => notIsfs(folder.uri)) 33 | .map((folder) => { 34 | const config = vscode.workspace.getConfiguration("objectscript", folder); 35 | const conn: ConnSettings = config.get("conn"); 36 | return { 37 | label: folder.name, 38 | description: folder.uri.fsPath, 39 | detail: 40 | !conn.server || !conn.active 41 | ? "No active server connection" 42 | : `Currently connected to ${conn.ns} on ${conn.server}`, 43 | }; 44 | }); 45 | if (!items.length) { 46 | vscode.window.showErrorMessage("No local folders in the workspace.", "Dismiss"); 47 | return; 48 | } 49 | const pick = 50 | items.length == 1 && !items[0].detail.startsWith("Currently") 51 | ? items[0] 52 | : await vscode.window.showQuickPick(items, { title: "Pick a folder" }); 53 | if (!pick) return; 54 | const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label); 55 | // Get user's choice of server 56 | const options: vscode.QuickPickOptions = {}; 57 | const serverName: string = await serverManagerApi.pickServer(folder, options); 58 | if (!serverName) { 59 | return; 60 | } 61 | await resolveConnectionSpec(serverName, undefined, folder); 62 | // Prepare a displayable form of its connection spec as a hint to the user 63 | // This will never return the default value (second parameter) because we only just resolved the connection spec. 64 | const connSpec = getResolvedConnectionSpec(serverName, undefined); 65 | const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`; 66 | // Connect and fetch namespaces 67 | const api = new AtelierAPI(vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`)); 68 | const serverConf = vscode.workspace 69 | .getConfiguration("intersystems", folder) 70 | .inspect<{ [key: string]: any }>("servers"); 71 | if ( 72 | serverConf.workspaceFolderValue && 73 | typeof serverConf.workspaceFolderValue[serverName] == "object" && 74 | !(serverConf.workspaceValue && typeof serverConf.workspaceValue[serverName] == "object") 75 | ) { 76 | // Need to manually set connection info if the server is defined at the workspace folder level 77 | api.setConnSpec(serverName, connSpec); 78 | } 79 | const allNamespaces: string[] = await api 80 | .serverInfo(false) 81 | .then((data) => data.result.content.namespaces) 82 | .catch(async (error) => { 83 | if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) { 84 | // Attempt to resolve username and password and try again 85 | const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec); 86 | if (newSpec) { 87 | // We were able to resolve credentials, so try again 88 | api.setConnSpec(api.config.serverName, newSpec); 89 | return api 90 | .serverInfo(false) 91 | .then((data) => data.result.content.namespaces) 92 | .catch(async (err) => { 93 | handleError(err, `Failed to fetch namespace list from server at ${connDisplayString}.`); 94 | return undefined; 95 | }); 96 | } else { 97 | handleError( 98 | `Unauthenticated access rejected by '${api.serverId}'.`, 99 | `Failed to fetch namespace list from server at ${connDisplayString}.` 100 | ); 101 | return undefined; 102 | } 103 | } 104 | handleError(error, `Failed to fetch namespace list from server at ${connDisplayString}.`); 105 | return undefined; 106 | }); 107 | // Clear the panel entry created by the connection 108 | panel.text = ""; 109 | panel.tooltip = ""; 110 | // Handle serverInfo failure 111 | if (!allNamespaces) { 112 | return; 113 | } 114 | // Handle serverInfo having returned no namespaces 115 | if (!allNamespaces.length) { 116 | vscode.window.showErrorMessage(`No namespace list returned by server at ${connDisplayString}`, "Dismiss"); 117 | return; 118 | } 119 | // Get user's choice of namespace 120 | const namespace = await vscode.window.showQuickPick(allNamespaces, { 121 | title: `Pick a namespace on server '${serverName}' (${connDisplayString})`, 122 | }); 123 | if (!namespace) { 124 | return; 125 | } 126 | // Update folder's config object 127 | const config = vscode.workspace.getConfiguration("objectscript", folder); 128 | if (vscode.workspace.workspaceFile && items.length == 1) { 129 | // Ask the user if they want to enable the connection at the workspace or folder level. 130 | // Only allow this when there is a single client-side folder in the workspace because 131 | // the server may be configured at the workspace folder level. 132 | const answer = await vscode.window.showQuickPick( 133 | [ 134 | { label: `Workspace Folder ${folder.name}`, detail: displayableUri(folder.uri) }, 135 | { label: "Workspace File", detail: displayableUri(vscode.workspace.workspaceFile) }, 136 | ], 137 | { title: "Store the server connection at the workspace or folder level?" } 138 | ); 139 | if (!answer) return; 140 | if (answer.label == "Workspace File") { 141 | // Enable the connection at the workspace level 142 | const conn: any = config.inspect("conn").workspaceValue; 143 | await config.update( 144 | "conn", 145 | { ...conn, server: serverName, ns: namespace, active: true }, 146 | vscode.ConfigurationTarget.Workspace 147 | ); 148 | return; 149 | } 150 | } 151 | // Enable the connection at the workspace folder level 152 | const conn: any = config.inspect("conn").workspaceFolderValue; 153 | await config.update( 154 | "conn", 155 | { ...conn, server: serverName, ns: namespace, active: true }, 156 | vscode.ConfigurationTarget.WorkspaceFolder 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/classDefinition.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as Cache from "vscode-cache"; 3 | import { onlyUnique } from "."; 4 | import { AtelierAPI } from "../api"; 5 | import { extensionContext } from "../extension"; 6 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 7 | 8 | export class ClassDefinition { 9 | public get uri(): vscode.Uri { 10 | return DocumentContentProvider.getUri(this._classFileName, this._workspaceFolder, this._namespace); 11 | } 12 | 13 | public static normalizeClassName(className: string, withExtension = false): string { 14 | return className.replace(/^%(\b\w+\b)$/, "%Library.$1") + (withExtension ? ".cls" : ""); 15 | } 16 | private _className: string; 17 | private _classFileName: string; 18 | private _cache; 19 | private _workspaceFolder: string; 20 | private _namespace: string; 21 | 22 | public constructor(className: string, workspaceFolder?: string, namespace?: string) { 23 | this._workspaceFolder = workspaceFolder; 24 | this._namespace = namespace; 25 | if (className.endsWith(".cls")) { 26 | className = className.replace(/\.cls$/i, ""); 27 | } 28 | this._className = ClassDefinition.normalizeClassName(className, false); 29 | this._classFileName = ClassDefinition.normalizeClassName(className, true); 30 | this._cache = new Cache(extensionContext, this._classFileName); 31 | } 32 | 33 | public async getDocument(): Promise { 34 | return vscode.workspace.openTextDocument(this.uri); 35 | } 36 | 37 | public store(kind: string, data: any): any { 38 | return this._cache.put(kind, data, 36000).then(() => data); 39 | } 40 | 41 | public load(kind: string): any { 42 | return this._cache.get(kind); 43 | } 44 | 45 | public async methods(scope: "any" | "class" | "instance" = "any"): Promise { 46 | const methods = this.load("methods-" + scope) || []; 47 | if (methods.length) { 48 | return Promise.resolve(methods); 49 | } 50 | const filterScope = (method) => scope === "any" || method.scope === scope; 51 | const api = new AtelierAPI(this.uri); 52 | const getMethods = (content) => { 53 | const extend = []; 54 | content.forEach((el) => { 55 | methods.push(...el.content.methods); 56 | extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); 57 | }); 58 | if (extend.length) { 59 | return api.actionIndex(extend).then((data) => getMethods(data.result.content)); 60 | } 61 | return this.store("methods-" + scope, methods.filter(filterScope).filter(onlyUnique).sort()); 62 | }; 63 | return api.actionIndex([this._classFileName]).then((data) => getMethods(data.result.content)); 64 | } 65 | 66 | public async properties(): Promise { 67 | const properties = this.load("properties") || []; 68 | if (properties.length) { 69 | return Promise.resolve(properties); 70 | } 71 | const api = new AtelierAPI(this.uri); 72 | const getProperties = (content) => { 73 | const extend = []; 74 | content.forEach((el) => { 75 | properties.push(...el.content.properties); 76 | extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); 77 | }); 78 | if (extend.length) { 79 | return api.actionIndex(extend).then((data) => getProperties(data.result.content)); 80 | } 81 | return this.store("properties", properties.filter(onlyUnique).sort()); 82 | }; 83 | return api.actionIndex([this._classFileName]).then((data) => getProperties(data.result.content)); 84 | } 85 | 86 | public async parameters(): Promise { 87 | const parameters = this.load("parameters") || []; 88 | if (parameters.length) { 89 | return Promise.resolve(parameters); 90 | } 91 | const api = new AtelierAPI(this.uri); 92 | const getParameters = (content) => { 93 | const extend = []; 94 | content.forEach((el) => { 95 | parameters.push(...el.content.parameters); 96 | extend.push(...el.content.super.map((extendName) => ClassDefinition.normalizeClassName(extendName, true))); 97 | }); 98 | if (extend.length) { 99 | return api.actionIndex(extend).then((data) => getParameters(data.result.content)); 100 | } 101 | return this.store("parameters", parameters.filter(onlyUnique).sort()); 102 | }; 103 | return api.actionIndex([this._classFileName]).then((data) => getParameters(data.result.content)); 104 | } 105 | 106 | public async super(): Promise { 107 | const superList = this.load("super"); 108 | if (superList) { 109 | return Promise.resolve(superList); 110 | } 111 | const api = new AtelierAPI(this.uri); 112 | const sql = `SELECT PrimarySuper FROM %Dictionary.CompiledClass 113 | WHERE Name %inlist (SELECT $LISTFROMSTRING(Super, ',') FROM %Dictionary.CompiledClass WHERE Name = ?)`; 114 | return api 115 | .actionQuery(sql, [this._className]) 116 | .then( 117 | (data) => 118 | data.result.content 119 | .reduce( 120 | (list: string[], el: { PrimarySuper: string }) => 121 | list.concat(el.PrimarySuper.split("~").filter((item) => item.length)), 122 | [] 123 | ) 124 | .filter((name: string) => name !== this._className) 125 | // .filter(name => !['%Library.Base', '%Library.SystemBase'].includes(name)) 126 | ) 127 | .then((data) => this.store("super", data)); 128 | } 129 | 130 | public async includeCode(): Promise { 131 | const includeCode = this.load("includeCode"); 132 | if (includeCode) { 133 | return Promise.resolve(includeCode); 134 | } 135 | const api = new AtelierAPI(this.uri); 136 | const sql = `SELECT LIST(IncludeCode) List FROM %Dictionary.CompiledClass WHERE Name %INLIST ( 137 | SELECT $LISTFROMSTRING(PrimarySuper, '~') FROM %Dictionary.CompiledClass WHERE Name = ?)`; 138 | const defaultIncludes = ["%occInclude", "%occErrors"]; 139 | return api 140 | .actionQuery(sql, [this._className]) 141 | .then((data) => 142 | data.result.content.reduce( 143 | (list: string[], el: { List: string }) => list.concat(el.List.split(",")), 144 | defaultIncludes 145 | ) 146 | ) 147 | .then((data) => this.store("includeCode", data)); 148 | } 149 | 150 | public async getMemberLocation(name: string): Promise { 151 | let pattern; 152 | if (name.startsWith("#")) { 153 | pattern = `(Parameter) ${name.substr(1)}(?=[( ;])`; 154 | } else { 155 | pattern = `((Class)?Method|Property|RelationShip) (${name}|"${name}")(?=[( ])`; 156 | } 157 | return this.getDocument().then((document) => { 158 | for (let i = 0; i < document.lineCount; i++) { 159 | const line = document.lineAt(i); 160 | if (line.text.match(pattern)) { 161 | return new vscode.Location(this.uri, new vscode.Position(i, 0)); 162 | } 163 | } 164 | return; 165 | }); 166 | } 167 | 168 | public async getMemberLocations(name: string): Promise { 169 | const extendList = await this.super(); 170 | return Promise.all([ 171 | await this.getMemberLocation(name), 172 | ...extendList.map(async (docName) => { 173 | const classDef = new ClassDefinition(docName); 174 | return classDef.getMemberLocation(name); 175 | }), 176 | ]).then((data) => data.filter((el) => el != null)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # vscode-objectscript Governance 2 | 3 | ## Overview 4 | 5 | This is a consensus-based community project with strong leadership direction provided by the project management committee (PMC). Anyone with an interest in the project can join the community, contribute code, and participate in the decision making process. This document describes how that participation takes place and how to set about earning merit within the project community. 6 | 7 | ## Roles and Responsibilities 8 | 9 | ### Users 10 | 11 | Users are community members who have a need for the project. They are the most important members of the community and without them the project would have no purpose. Anyone can be a user; there are no special requirements. 12 | 13 | The project asks its users to participate in the project and community as much as possible. User contributions enable the project team to ensure that they are satisfying the needs of those users. Common user contributions include (but are not limited to): 14 | 15 | - evangelising about the project (e.g. a link on a website and word-of-mouth awareness raising) 16 | - informing developers of strengths and weaknesses from a new user perspective 17 | - providing moral support (a ‘thank you’ goes a long way) 18 | - providing financial support (the software is open source, but its developers need to eat) 19 | 20 | Users who continue to engage with the project and its community will often become more and more involved. Such users may find themselves becoming contributors, as described in the next section. 21 | 22 | ### Contributors 23 | 24 | Contributors are community members who contribute in concrete ways to the project. Anyone can become a contributor, and contributions can take many forms, as detailed in a separate document. There is no expectation of commitment to the project, no specific skill requirements and no selection process. 25 | 26 | In addition to their actions as users, contributors may also find themselves doing one or more of the following: 27 | 28 | - supporting new users (existing users are often the best people to support new users) 29 | - reporting bugs 30 | - identifying requirements 31 | - providing graphics and web design 32 | - programming 33 | - assisting with project infrastructure 34 | - writing documentation 35 | - fixing bugs 36 | - adding features 37 | 38 | Contributors engage with the project through the issue tracker, or by writing or editing documentation. They submit changes to the project itself via pull requests, which will be considered for inclusion in the project by existing committers (see next section). Pull requests with full test coverage will receive greater consideration than those without. 39 | 40 | As contributors gain experience and familiarity with the project, their profile within, and commitment to, the community will increase. At some stage, they may find themselves being nominated for committership. 41 | 42 | ### Committers 43 | 44 | Committers are contributors who have made several valuable contributions to the project and are now relied upon to both write code directly to the repository and screen the contributions of others. In many cases they are programmers but it is also possible that they contribute in a different role. Typically, a committer will focus on a specific aspect of the project, and will bring a level of expertise and understanding that earns them the respect of the community and the PMC. The role of committer is not an official one, it is simply a position that influential members of the community will find themselves in as the PMC looks to them for guidance and support. 45 | 46 | Committers have no authority over the overall direction of the project. However, they do have the ear of the PMC. It is a committer’s job to ensure that the PMC is aware of the community’s needs and collective objectives, and to help develop or elicit appropriate contributions to the project. Often, committers are given informal control over their specific areas of responsibility, and are assigned rights to directly modify certain areas of the source code. That is, although committers do not have explicit decision-making authority, they will often find that their actions are synonymous with the decisions made by the PMC. 47 | 48 | ### Project management committee 49 | 50 | The project management committee (PMC) sets the strategic objectives of the project and communicates these clearly to the community. It also has to understand the community as a whole and strive to satisfy as many conflicting needs as possible, while ensuring that the project's long term success. 51 | 52 | It consists of those individuals identified as ‘project owners’ on the development site. The PMC has additional responsibilities over and above those of a committer. These responsibilities ensure the smooth running of the project. PMC members are expected to review code contributions, participate in strategic planning, approve changes to the governance model and manage the copyrights within the project outputs. 53 | 54 | The PMC votes on new committers. It also makes decisions when community consensus cannot be reached. In addition, the PMC has access to the project’s private mailing list and its archives. This list is used for sensitive issues, such as votes for new committers and legal matters that cannot be discussed in public. It may also be used for project management or planning. 55 | 56 | Membership of the PMC is by invitation from the existing PMC members. A nomination will result in discussion and then a vote by the existing PMC members. PMC membership votes are subject to consensus approval of the current PMC members. 57 | 58 | ### PMC Chair 59 | 60 | The PMC Chair is a single individual, voted for by the PMC members. Once someone has been appointed Chair, they remain in that role until they choose to retire, or the PMC casts a two-thirds majority vote to remove them. 61 | 62 | The PMC Chair has no additional authority over other members of the PMC: the role is one of coordinator and facilitator. The Chair is also expected to ensure that all governance processes are adhered to, and has the casting vote when the project fails to reach consensus. 63 | 64 | ## Contribution process 65 | 66 | ## Decision making process 67 | 68 | In order to ensure that the project is not bogged down by endless discussion and continual voting, the project operates a policy of lazy consensus. This allows the majority of decisions to be made without resorting to a formal vote. 69 | 70 | ### Lazy consensus 71 | 72 | Decision making typically involves the following steps: 73 | 74 | - Proposal 75 | - Discussion 76 | - Vote (if consensus is not reached through discussion) 77 | - Decision 78 | 79 | Any community member can make a proposal for consideration by the community. In order to initiate a discussion about a new idea, they should file an issue and optionally submit a patch implementing the proposal. This will prompt a review and, if necessary, a discussion of the idea. The goal of this review and discussion is to gain approval for the contribution. Since most people in the project community have a shared vision, there is often little need for discussion in order to reach consensus. 80 | 81 | In general, as long as nobody explicitly opposes a proposal or patch, it is recognised as having the support of the community. This is called lazy consensus - that is, those who have not stated their opinion explicitly have implicitly agreed to the implementation of the proposal. 82 | 83 | At least 72 hours notice will be provided before assuming that there are no objections to the proposal. This requirement ensures that everyone is given enough time to read, digest and respond to the proposal. This time period is chosen so as to be as inclusive as possible of all participants, regardless of their location and time commitments. 84 | 85 | ### Voting 86 | 87 | Not all decisions can be made using lazy consensus. Issues such as those affecting the strategic direction or legal standing of the project must gain explicit approval in the form of a vote. Every member of the community is encouraged to express their opinions in all discussion and all votes. However, only project committers and/or PMC members (as defined above) have binding votes for the purposes of decision making. 88 | -------------------------------------------------------------------------------- /src/providers/WorkspaceSymbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { DocumentContentProvider } from "./DocumentContentProvider"; 4 | import { filesystemSchemas } from "../extension"; 5 | import { fileSpecFromURI, isfsConfig } from "../utils/FileProviderUtil"; 6 | import { allDocumentsInWorkspace } from "../utils/documentIndex"; 7 | import { handleError, queryToFuzzyLike } from "../utils"; 8 | 9 | export class WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { 10 | private readonly _sqlPrefix: string = 11 | "SELECT mem.Name, mem.Parent, mem.Type FROM (" + 12 | " SELECT Name, Name AS Parent, 'Class' AS Type FROM %Dictionary.ClassDefinition" + 13 | " UNION SELECT Name, Parent, 'Method' AS Type FROM %Dictionary.MethodDefinition" + 14 | " UNION SELECT Name, Parent, 'Property' AS Type FROM %Dictionary.PropertyDefinition" + 15 | " UNION SELECT Name, Parent, 'Parameter' AS Type FROM %Dictionary.ParameterDefinition" + 16 | " UNION SELECT Name, Parent, 'Index' AS Type FROM %Dictionary.IndexDefinition" + 17 | " UNION SELECT Name, Parent, 'ForeignKey' AS Type FROM %Dictionary.ForeignKeyDefinition" + 18 | " UNION SELECT Name, Parent, 'XData' AS Type FROM %Dictionary.XDataDefinition" + 19 | " UNION SELECT Name, Parent, 'Query' AS Type FROM %Dictionary.QueryDefinition" + 20 | " UNION SELECT Name, Parent, 'Trigger' AS Type FROM %Dictionary.TriggerDefinition" + 21 | " UNION SELECT Name, Parent, 'Storage' AS Type FROM %Dictionary.StorageDefinition" + 22 | " UNION SELECT Name, Parent, 'Projection' AS Type FROM %Dictionary.ProjectionDefinition" + 23 | ") AS mem "; 24 | 25 | private readonly _sqlPrj: string = 26 | "JOIN %Studio.Project_ProjectItemsList(?) AS pil ON mem.Parent = pil.Name AND pil.Type = 'CLS'"; 27 | 28 | private readonly _sqlDocs: string = 29 | "JOIN %Library.RoutineMgr_StudioOpenDialog(?,1,1,?,1,0,?,'Type = 4',0,?) AS sod ON mem.Parent = $EXTRACT(sod.Name,1,$LENGTH(sod.Name)-4)"; 30 | 31 | private readonly _sqlSuffix: string = " WHERE LOWER(mem.Name) LIKE ? ESCAPE '\\'"; 32 | 33 | /** 34 | * Convert the query results to VS Code symbols. Needs to be typed as `any[]` 35 | * because we aren't including ranges. They will be resolved later. 36 | */ 37 | private _queryResultToSymbols(data: any, wsFolder: vscode.WorkspaceFolder): any[] { 38 | const result = []; 39 | const uris: Map = new Map(); 40 | for (const element of data.result.content) { 41 | const kind: vscode.SymbolKind = (() => { 42 | switch (element.Type) { 43 | case "Method": 44 | return vscode.SymbolKind.Method; 45 | case "Query": 46 | return vscode.SymbolKind.Function; 47 | case "Trigger": 48 | return vscode.SymbolKind.Event; 49 | case "Parameter": 50 | return vscode.SymbolKind.Constant; 51 | case "Index": 52 | return vscode.SymbolKind.Array; 53 | case "ForeignKey": 54 | return vscode.SymbolKind.Key; 55 | case "XData": 56 | return vscode.SymbolKind.Struct; 57 | case "Storage": 58 | return vscode.SymbolKind.Object; 59 | case "Projection": 60 | return vscode.SymbolKind.Interface; 61 | case "Class": 62 | return vscode.SymbolKind.Class; 63 | default: 64 | // Property and Relationship 65 | return vscode.SymbolKind.Property; 66 | } 67 | })(); 68 | 69 | let uri: vscode.Uri; 70 | if (uris.has(element.Parent)) { 71 | uri = uris.get(element.Parent); 72 | } else { 73 | uri = DocumentContentProvider.getUri( 74 | `${element.Parent}.cls`, 75 | wsFolder.name, 76 | undefined, 77 | undefined, 78 | wsFolder.uri, 79 | // Only "file" scheme is fully supported for client-side editing 80 | wsFolder.uri.scheme != "file" 81 | ); 82 | uris.set(element.Parent, uri); 83 | } 84 | 85 | result.push({ 86 | name: element.Name, 87 | containerName: element.Type, 88 | kind, 89 | location: { 90 | uri, 91 | }, 92 | }); 93 | } 94 | return result; 95 | } 96 | 97 | public async provideWorkspaceSymbols( 98 | query: string, 99 | token: vscode.CancellationToken 100 | ): Promise { 101 | if (!vscode.workspace.workspaceFolders?.length) return; 102 | // Convert query to a LIKE compatible pattern 103 | const pattern = queryToFuzzyLike(query); 104 | if (token.isCancellationRequested) return; 105 | // Get results for all workspace folders 106 | return Promise.allSettled( 107 | vscode.workspace.workspaceFolders.map((wsFolder) => { 108 | if (filesystemSchemas.includes(wsFolder.uri.scheme)) { 109 | const { csp, system, generated, mapped, project } = isfsConfig(wsFolder.uri); 110 | if (csp) { 111 | // No classes or class members in web application folders 112 | return Promise.resolve([]); 113 | } else { 114 | const api = new AtelierAPI(wsFolder.uri); 115 | if (!api.active || token.isCancellationRequested) return Promise.resolve([]); 116 | return api 117 | .actionQuery(`${this._sqlPrefix}${project.length ? this._sqlPrj : this._sqlDocs}${this._sqlSuffix}`, [ 118 | project.length ? project : fileSpecFromURI(wsFolder.uri), 119 | system || api.ns == "%SYS" ? "1" : "0", 120 | generated ? "1" : "0", 121 | mapped ? "1" : "0", 122 | pattern, 123 | ]) 124 | .then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder))); 125 | } 126 | } else { 127 | // Use the document index to determine the classes to search 128 | const api = new AtelierAPI(wsFolder.uri); 129 | if (!api.active) return Promise.resolve([]); 130 | const docs = allDocumentsInWorkspace(wsFolder).filter((d) => d.endsWith(".cls")); 131 | if (!docs.length || token.isCancellationRequested) return Promise.resolve([]); 132 | return api 133 | .actionQuery(`${this._sqlPrefix}${this._sqlSuffix} AND mem.Parent %INLIST $LISTFROMSTRING(?)`, [ 134 | pattern, 135 | docs.map((d) => d.slice(0, -4)).join(","), 136 | ]) 137 | .then((data) => (token.isCancellationRequested ? [] : this._queryResultToSymbols(data, wsFolder))); 138 | } 139 | }) 140 | ).then((results) => 141 | results.flatMap((result) => { 142 | if (result.status == "fulfilled") { 143 | return result.value; 144 | } else { 145 | handleError(result.reason); 146 | return []; 147 | } 148 | }) 149 | ); 150 | } 151 | 152 | resolveWorkspaceSymbol(symbol: vscode.SymbolInformation): vscode.ProviderResult { 153 | return vscode.commands 154 | .executeCommand("vscode.executeDocumentSymbolProvider", symbol.location.uri) 155 | .then((docSymbols) => { 156 | if (!Array.isArray(docSymbols) || !docSymbols.length) return; 157 | if (symbol.kind == vscode.SymbolKind.Class) { 158 | symbol.location.range = docSymbols[0].selectionRange; 159 | } else { 160 | const memberType = symbol.containerName.toUpperCase(); 161 | const unquote = (n: string): string => { 162 | return n[0] == '"' ? n.slice(1, -1).replace(/""/g, '"') : n; 163 | }; 164 | for (const docSymbol of docSymbols[0].children) { 165 | if (unquote(docSymbol.name) == symbol.name && docSymbol.detail.toUpperCase().includes(memberType)) { 166 | symbol.location.range = docSymbol.selectionRange; 167 | break; 168 | } 169 | } 170 | } 171 | return symbol; 172 | }); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /test.cls: -------------------------------------------------------------------------------- 1 | /// Export different types of modules in different subfolders in UDL (plain) format 2 | /// test.dfi -> /dfi/test.dfi 3 | /// testpkg.test.cls -> /cls/testpkg/test.cls 4 | Class sc.code [ Abstract ] { 5 | 6 | /// export all available code 7 | ClassMethod export(generated = 0, system = 0, percent = 0, mapped = 0) { 8 | 9 | #define export(%code, %file) ##continue 10 | s sc = $system.OBJ.ExportUDL(%code, %file,"/diffexport") ##continue 11 | w +sc ##continue 12 | if 'sc d $system.OBJ.DisplayError(sc) 13 | 14 | #define isGenerated(%code) ##class(%RoutineMgr).IsGenerated( %code ) 15 | #define isPercented(%code) ("%" = $e(%code)) 16 | #define isMapped(%code) ##class(%RoutineMgr).IsMapped( %code ) 17 | #define log w !, code, " -> ", filename, " " 18 | 19 | #define mkdir(%filename) ##continue 20 | s path = ##class(%File).GetDirectory( %filename ) ##continue 21 | if '##class(%File).DirectoryExists( path ) { ##continue 22 | s sc = ##class(%File).CreateDirectoryChain( path ) ##continue 23 | w !, "mkdir ", path, " ", sc ##continue 24 | } 25 | 26 | 27 | 28 | #; classes 29 | s rs = ##class(%ResultSet).%New("%Dictionary.ClassDefinition:Summary") 30 | if rs.Execute() { 31 | 32 | while rs.%Next(){ 33 | 34 | s code = rs.Name _ ".cls", isSystem = rs.System 35 | if ( 'system && isSystem ) continue 36 | if ( 'generated && $$$isGenerated( code ) ) continue 37 | if ( 'percent && $$$isPercented( code ) ) continue 38 | if ( 'mapped && $$$isMapped( code ) ) continue 39 | s filename = ..filename( code ) 40 | $$$mkdir( filename ) 41 | $$$log 42 | $$$export( code, filename ) 43 | 44 | } s rs="" 45 | } 46 | 47 | #; routines 48 | s rs = ##class(%ResultSet).%New("%Routine:RoutineList") 49 | if rs.Execute() { 50 | 51 | while rs.%Next() { 52 | 53 | s code = rs.Name 54 | if ( 'generated && $$$isGenerated( code ) ) continue 55 | if ( 'percent && $$$isPercented( code ) ) continue 56 | if ( 'mapped && $$$isMapped( code ) ) continue 57 | 58 | s filename = ..filename( code ) 59 | $$$mkdir( filename ) 60 | $$$log 61 | $$$export( code, filename ) 62 | 63 | } s rs="" 64 | } 65 | 66 | 67 | #; dfi 68 | #define export(%code,%file) w ##class(%DeepSee.UserLibrary.Utils).%Export( %code, %file, 0 ) 69 | s sql = "Select fullName as Name From %DeepSee_UserLibrary.FolderItem" 70 | s rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) 71 | while rs.%Next() { 72 | s code = rs.Name, filename = ..filename( code_".dfi" ) 73 | $$$mkdir( filename ) 74 | $$$log 75 | $$$export(code,filename) 76 | 77 | } s rs="" 78 | 79 | Q 1 80 | } 81 | 82 | /// import all from workdir 83 | ClassMethod import(filemask = "*.*", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status { 84 | #define push(%dir) s dirs( $i( dirs ) ) = %dir 85 | #define next(%i,%dir) s %i=$o( dirs( "" ), 1, %dir ) k:%i'="" dirs(%i) 86 | #define isDirectory(%type) ( %type = "D" ) 87 | #define log w !, filename, " ", +sc, $S(sc=1:"",1: " "_$system.Status.GetOneErrorText(sc)) 88 | 89 | s sc = 1, dirs = "", dir = ..workdir() $$$push(dir) 90 | 91 | for { $$$next(i,dir) Q:i="" Q:dir="" 92 | 93 | s rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) 94 | 95 | while rs.%Next() { 96 | s filename = rs.Name 97 | 98 | if $$$isDirectory(rs.Type) { 99 | if recurse $$$push(filename) 100 | continue 101 | } 102 | 103 | s ext = $p( filename, ".", * ) 104 | 105 | if $zcvt( ext, "l" ) = "dfi" { 106 | s sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .loaded ) 107 | } else { 108 | s sc = $system.OBJ.Load( filename, qspec, .err, .loaded) 109 | } 110 | 111 | if verbose $$$log 112 | 113 | } 114 | } 115 | 116 | Q sc 117 | } 118 | 119 | /// get or set working directory for export/import source 120 | ClassMethod workdir(workdir) { 121 | s gln = ..gln() s:$d(workdir) @gln = workdir 122 | ///zu(12) namespace directory by default 123 | #define nsdir $zu(12,"") 124 | Q $g(@gln, $$$nsdir) 125 | } 126 | 127 | /// gl[obal] n[ame] - storage for settings 128 | ClassMethod gln() [ CodeMode = expression, Private ] { 129 | "^"_$classname() 130 | } 131 | 132 | /// test.dfi -> /dfi/test.dfi 133 | /// test.cls -> /cls/test.cls 134 | /// testpkg.test.cls -> /cls/testpkg/test.cls 135 | /// etc 136 | ClassMethod filename(code) { 137 | #define log(%dir,%sc) w !, "mkdir ", %dir, " ", sc 138 | 139 | s wd = ..workdir() 140 | 141 | if '##class(%File).DirectoryExists( wd ) { 142 | s sc = ##class(%File).CreateDirectoryChain( wd ) 143 | $$$log(wd,sc) 144 | } 145 | 146 | s ext = $p( code, ".", * ), ext = $zcvt( ext, "l" ) 147 | #; for each type - different directory 148 | 149 | s:ext'="" wd = ##class(%File).NormalizeDirectory( ext, wd ) 150 | 151 | #; directory must exist before any call (%File).NormalizeFilename( , wd) 152 | if '##class(%File).DirectoryExists( wd ) { 153 | s sc = ##class(%File).CreateDirectoryChain( wd ) 154 | $$$log(wd,sc) 155 | } 156 | 157 | s filename = ##class(%File).NormalizeFilename( code, wd ) 158 | //B:code="DPRep.Rest.JSON.cls" "L" 159 | #; for *.cls Package.Subpackage.ClassName.cls -> Folder/Subfolder/ClassName.cls 160 | if ext ="cls" { 161 | s dirs = $piece( code, ".",1, *-2 ), dirs = $translate( dirs, ".", "/" ) 162 | s relpath = dirs _ "/" _ $piece( code, ".", *-1, * ) ; 163 | s filename = ##class(%File).NormalizeFilename( relpath, wd ) 164 | } 165 | Q filename 166 | } 167 | 168 | /// import from workdir all files with ts newer than code ts in db 169 | ClassMethod importUpdated(filemask = "*.*", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status { 170 | #define push(%dir) s dirs( $i( dirs ) ) = %dir 171 | #define next(%i,%dir) s %i=$o( dirs( "" ), 1, %dir ) k:%i'="" dirs(%i) 172 | #define isDirectory(%type) ( %type = "D" ) 173 | #define log w !, filename, " -> ", codename, " ", +sc 174 | s sc = 1, dirs = "", dir = ..workdir() $$$push(dir) 175 | for { $$$next(i,dir) Q:i="" Q:dir="" 176 | 177 | s rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) 178 | 179 | while rs.%Next() { 180 | s filename = rs.Name 181 | 182 | if $$$isDirectory( rs.Type ) { 183 | if ( recurse ) $$$push(filename) ;push directory 184 | continue 185 | } 186 | 187 | s filets = rs.DateModified 188 | s codename = ..codename( filename, .ext ) 189 | s codets = ..codets( codename, ext ) 190 | if ( filets '] codets ) continue 191 | //w codename,! B "L" 192 | 193 | /* 194 | w !, " ************* import ************** " 195 | w !, "file: ", filets 196 | w !, "code: ", codets 197 | */ 198 | 199 | if ext = "dfi" { 200 | 201 | s sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .loaded ) 202 | 203 | } else { 204 | 205 | #; drop existing code before import ( purge DateModified ) 206 | s:codets'="" sc = ##class(%RoutineMgr).Delete( codename ) 207 | s sc = $system.OBJ.Load( filename, qspec, .err, .loaded) 208 | 209 | } 210 | 211 | if verbose $$$log 212 | } 213 | } 214 | Q sc 215 | } 216 | 217 | /// presumable codename 218 | ClassMethod codename(filename, ByRef ext = "") { 219 | s ext = $p( filename, ".", * ), ext = $zcvt( ext, "l" ) 220 | s path = ##class(%File).NormalizeDirectory( ext, ..workdir() ) 221 | s codename = $p( filename, path, 2 ) 222 | if ext = "dfi" { 223 | s fullname = $tr( codename, "\", "/" ) ; return fullname for dfi in $$$IsWINDOWS 224 | Q $p( fullname, ".", 1, *-1 ) ;remove extension 225 | } 226 | if (ext ="cls")!(ext="int")!(ext="inc")!(ext="mac") s codename=$tr(codename,"/",".") 227 | Q codename 228 | } 229 | 230 | ClassMethod codets(codename, ext) { 231 | s ts = "" 232 | if ext'="dfi" { 233 | s ts = ##class(%RoutineMgr).TS( codename ) 234 | } else { 235 | s sql="Select timeModified From %DeepSee_UserLibrary.FolderItem Where fullname = ?" 236 | s rs = ##class(%SQL.Statement).%ExecDirect( , sql, codename ) 237 | if rs.%Next() { 238 | s utcts = rs.timeModified 239 | s utch = $zdth( utcts, 3, , 3 ) ;utc internal format 240 | s loch = $zdth( utch, -3 ) ; utc to local timezone 241 | s ts = $zdt( loch, 3, ,0 ) ; local timestamp*/ 242 | } 243 | } 244 | Q $p( ts, "." ) ;remove ms 245 | } 246 | 247 | } -------------------------------------------------------------------------------- /syntaxes/objectscript.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "objectscript", 4 | "scopeName": "source.objectscript", 5 | "patterns": [ 6 | { 7 | "match": "^(ROUTINE)\\s(\\b[\\p{Alnum}.]+\\b)", 8 | "captures": { 9 | "1": { "name": "keyword.control" }, 10 | "2": { "name": "entity.name.class" } 11 | } 12 | }, 13 | { 14 | "include": "#comments" 15 | }, 16 | { 17 | "include": "#embedded" 18 | }, 19 | { 20 | "include": "#constants" 21 | }, 22 | { 23 | "include": "#keywords" 24 | }, 25 | { 26 | "include": "#macros" 27 | }, 28 | { 29 | "include": "#elements" 30 | } 31 | ], 32 | "repository": { 33 | "comments": { 34 | "patterns": [ 35 | { 36 | "begin": "(/\\*)", 37 | "beginCaptures": { "1": { "name": "comment.multiline.objectscript" } }, 38 | "contentName": "comment.multiline.objectscript", 39 | "end": "(.*?\\*/)", 40 | "endCaptures": { "1": { "name": "comment.multiline.objectscript" } } 41 | }, 42 | { "begin": "^\\s*#;", "end": "$", "name": "comment.line.macro.objectscript" }, 43 | { "begin": "//|;", "end": "$", "name": "comment.endline.objectscript" }, 44 | { "begin": "##;", "end": "$", "name": "comment.endline.macro.objectscript" } 45 | ] 46 | }, 47 | "statements": { 48 | "patterns": [{ "include": "#variables" }] 49 | }, 50 | "embedded": { 51 | "patterns": [ 52 | { 53 | "include": "#embeddedSQL" 54 | }, 55 | { 56 | "include": "#embeddedJS" 57 | } 58 | ] 59 | }, 60 | "embeddedSQL": { 61 | "patterns": [ 62 | { 63 | "begin": "(?i)((?:&|##)sql)(\\()", 64 | "end": "\\)", 65 | "beginCaptures": { 66 | "1": { "name": "keyword.special.sql.objectscript" }, 67 | "2": { "name": "punctuation.objectscript" } 68 | }, 69 | "endCaptures": { "0": { "name": "punctuation.objectscript" } }, 70 | "contentName": "meta.embedded.block.sql", 71 | "applyEndPatternLast": 1, 72 | "patterns": [ 73 | { "include": "#embeddedSQL-brackets" }, 74 | { "include": "source.sql" } 75 | ] 76 | } 77 | ], 78 | "repository": { 79 | "embeddedSQL-brackets": { 80 | "begin": "(?<=\\()(?!\\G)", 81 | "end": "\\)", 82 | "patterns": [ 83 | { "include": "#embeddedSQL-brackets" }, 84 | { "include": "source.sql" } 85 | ] 86 | } 87 | } 88 | }, 89 | "embeddedJS": { 90 | "patterns": [ 91 | { 92 | "begin": "(?i)(&js(cript)?)(<)", 93 | "beginCaptures": { 94 | "1": { "name": "keyword.special.js.objectscript" }, 95 | "2": { "name": "punctuation.objectscript" } 96 | }, 97 | "patterns": [{ "include": "source.js" }], 98 | "contentName": "text.js", 99 | "end": ">" 100 | } 101 | ] 102 | }, 103 | "keywords": { 104 | "patterns": [ 105 | { 106 | "include": "#commands" 107 | }, 108 | { 109 | "include": "#control-commands" 110 | } 111 | ] 112 | }, 113 | "commands": { 114 | "patterns": [ 115 | { 116 | "match": "(?i)(?<=\\s|\\.)\\b(BREAK|B|SET|S|DO|D|KILL|K|GOTO|G|READ|R|WRITE|W|OPEN|O|USE|U|CLOSE|C|CONTINUE|FOR|F|HALT|H|HANG|JOB|J|MERGE|M|NEW|N|QUIT|Q|RETURN|RET|TSTART|TS|TCOMMIT|TC|TROLLBACK|TRO|THROW|VIEW|V|XECUTE|X|ZKILL|ZL|ZNSPACE|ZN|ZTRAP|ZWRITE|ZW|ZZDUMP|ZZWRITE)\\b(?=( (?![=+-]|\\&|\\|)|:|$))", 117 | "captures": { "1": { "name": "keyword.control.objectscript" } } 118 | }, 119 | { 120 | "match": "(?i)(?<=\\s|\\.)\\b(LOCK|L)\\b(?=( (?![=]|\\&|\\|)|:|$))", 121 | "captures": { "1": { "name": "keyword.control.objectscript" } } 122 | } 123 | ] 124 | }, 125 | "control-commands": { 126 | "patterns": [ 127 | { 128 | "match": "(?i)(?<=\\s|\\.)\\b(IF|I|WHILE|FOR|F|TRY|CATCH|ELSE|E|ELSEIF)\\b(?=( (?![=+-]|\\&|\\|)|:|$))", 129 | "captures": { "1": { "name": "keyword.control.objectscript" } } 130 | } 131 | ] 132 | }, 133 | "constants": { 134 | "patterns": [ 135 | { 136 | "begin": "(\")", 137 | "beginCaptures": { 138 | "1": { 139 | "name": "punctuation.definition.string.begin.objectscript" 140 | } 141 | }, 142 | "end": "(\")", 143 | "endCaptures": { 144 | "1": { 145 | "name": "punctuation.definition.string.end.objectscript" 146 | } 147 | }, 148 | "name": "string.quoted.double.objectscript" 149 | }, 150 | { 151 | "match": "\\d+", 152 | "name": "constant.numeric.objectscript" 153 | } 154 | ] 155 | }, 156 | "macros": { 157 | "patterns": [ 158 | { 159 | "match": "(?i)(#dim)(\\s)(%?[\\p{Alnum}]+)(\\s)(?:(As)(\\s)(%?[\\p{Alnum}.]+))?", 160 | "captures": { 161 | "1": { "name": "meta.preprocessor.dim.objectscript" }, 162 | "2": { "name": "whitespace.objectscript" }, 163 | "3": { "name": "variable.name" }, 164 | "4": { "name": "whitespace.objectscript" }, 165 | "5": { "name": "keyword.as.objectscript" }, 166 | "6": { "name": "whitespace.objectscript" }, 167 | "7": { "name": "entity.name.class" } 168 | } 169 | }, 170 | { 171 | "include": "source.objectscript_macros" 172 | } 173 | ] 174 | }, 175 | "elements": { 176 | "patterns": [ 177 | { 178 | "match": "^([\\p{Alnum}]+)(\\([^)]*\\))(?:\\s+(public|private))?", 179 | "captures": { 180 | "1": { "name": "entity.name.function" }, 181 | "2": { "name": "other" }, 182 | "3": { "name": "keyword.other" } 183 | } 184 | }, 185 | { 186 | "match": "(?i)(##class)(\\()([^)]+)(\\))", 187 | "captures": { 188 | "1": { "name": "keyword.other" }, 189 | "2": { "name": "punctuation.objectscript" }, 190 | "3": { "name": "entity.name.class" }, 191 | "4": { "name": "punctuation.objectscript" } 192 | } 193 | }, 194 | { 195 | "match": "%[\\p{Alnum}]+", 196 | "name": "entity.other.attribute-name" 197 | }, 198 | { 199 | "match": "[i|r]%[\\p{Alnum}]+", 200 | "name": "entity.other.attribute-name" 201 | }, 202 | { 203 | "match": "[i|r]%\"[^\".]\"", 204 | "name": "entity.other.attribute-name" 205 | }, 206 | { 207 | "match": "(\\.{1,2})(%?[\\p{Alnum}]+)(?=\\()", 208 | "captures": { 209 | "1": { "name": "punctuation.objectscript" }, 210 | "2": { "name": "entity.other.attribute-name" } 211 | } 212 | }, 213 | { 214 | "match": "(\\.{1,2})(%?[\\p{Alnum}]+)(?!\\()", 215 | "captures": { 216 | "1": { "name": "punctuation.objectscript" }, 217 | "2": { "name": "entity.other.attribute-name" } 218 | } 219 | }, 220 | { 221 | "match": "(\\.{1,2}#)([\\p{Alnum}]+)", 222 | "captures": { 223 | "1": { "name": "punctuation.objectscript" }, 224 | "2": { "name": "meta.parameter.type.variable" } 225 | } 226 | }, 227 | { 228 | "match": "%?[\\p{Alnum}]+", 229 | "name": "variable.name.objectscrip" 230 | }, 231 | { 232 | "match": "\\^%?[\\p{Alnum}]+(\\.[\\p{Alnum}]+)*", 233 | "name": "variable.name.global.objectscrip" 234 | }, 235 | { 236 | "match": "(?i)\\$system(.[\\p{Alnum}]+)*", 237 | "name": "entity.name.function.system.objectscript" 238 | }, 239 | { 240 | "match": "\\$[\\p{Alnum}]+", 241 | "name": "entity.name.function.system.objectscript" 242 | }, 243 | { 244 | "match": "\\${2}[\\p{Alnum}]+", 245 | "name": "entity.name.function.local.objectscript" 246 | }, 247 | { 248 | "match": "\\${3}[\\p{Alnum}]+", 249 | "name": "meta.preprocessor.objectscript" 250 | } 251 | ] 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/commands/xmlToUdl.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import path = require("path"); 3 | import { config, OBJECTSCRIPTXML_FILE_SCHEMA, xmlContentProvider } from "../extension"; 4 | import { AtelierAPI } from "../api"; 5 | import { replaceFile, fileExists, getWsFolder, handleError, notIsfs, outputChannel, displayableUri } from "../utils"; 6 | import { getFileName } from "./export"; 7 | 8 | const exportHeader = /^\s* { 11 | const uri = textEditor.document.uri; 12 | const content = textEditor.document.getText(); 13 | const uriString = displayableUri(uri); 14 | if (notIsfs(uri) && uri.path.toLowerCase().endsWith("xml") && textEditor.document.lineCount > 2) { 15 | if (exportHeader.test(textEditor.document.lineAt(1).text)) { 16 | const api = new AtelierAPI(uri); 17 | if (!api.active) { 18 | vscode.window.showErrorMessage("'Preview XML as UDL' command requires an active server connection.", "Dismiss"); 19 | return; 20 | } 21 | try { 22 | // Convert the file 23 | const udlDocs: { name: string; content: string[] }[] = await api 24 | .cvtXmlUdl(content) 25 | .then((data) => data.result.content); 26 | if (udlDocs.length == 0) { 27 | vscode.window.showErrorMessage(`File '${uriString}' contains no documents that can be previewed.`, "Dismiss"); 28 | return; 29 | } 30 | // Prompt the user for documents to preview 31 | const docsToPreview = await vscode.window.showQuickPick( 32 | udlDocs.map((d) => { 33 | return { label: d.name, picked: true }; 34 | }), 35 | { 36 | canPickMany: true, 37 | title: "Select the documents to preview", 38 | } 39 | ); 40 | if (docsToPreview == undefined || docsToPreview.length == 0) { 41 | return; 42 | } 43 | const docWhitelist = docsToPreview.map((d) => d.label); 44 | // Send the UDL text to the content provider 45 | xmlContentProvider.addUdlDocsForFile(uri.toString(), udlDocs); 46 | // Open the files 47 | for (const udlDoc of udlDocs) { 48 | if (!docWhitelist.includes(udlDoc.name)) continue; // This file wasn't selected 49 | // await for response so we know when it's safe to clear the provider's cache 50 | await vscode.window 51 | .showTextDocument( 52 | vscode.Uri.from({ 53 | path: udlDoc.name, 54 | fragment: uri.toString(), 55 | scheme: OBJECTSCRIPTXML_FILE_SCHEMA, 56 | }), 57 | { 58 | preserveFocus: true, 59 | preview: false, 60 | viewColumn: vscode.ViewColumn.Beside, 61 | } 62 | ) 63 | .then( 64 | () => { 65 | // Don't need return value 66 | }, 67 | () => { 68 | // Swallow errors 69 | } 70 | ); 71 | } 72 | // Remove the UDL text from the content provider's cache 73 | xmlContentProvider.removeUdlDocsForFile(uri.toString()); 74 | } catch (error) { 75 | handleError(error, "Error executing 'Preview XML as UDL' command."); 76 | } 77 | } else if (!auto) { 78 | vscode.window.showErrorMessage(`XML file '${uriString}' is not an InterSystems export.`, "Dismiss"); 79 | } 80 | } 81 | } 82 | 83 | /** Extract the source documents in an XML file as UDL and create the UDL files using the export settings. */ 84 | export async function extractXMLFileContents(xmlUri?: vscode.Uri): Promise { 85 | if (!xmlUri && vscode.window.activeTextEditor) { 86 | // Check if the active text editor contains an XML file 87 | const activeDoc = vscode.window.activeTextEditor.document; 88 | if (notIsfs(activeDoc.uri) && activeDoc.uri.path.toLowerCase().endsWith("xml") && activeDoc.lineCount > 2) { 89 | // The active text editor contains an XML file, so process it 90 | xmlUri = activeDoc.uri; 91 | } 92 | } 93 | try { 94 | // Determine the workspace folder 95 | let wsFolder: vscode.WorkspaceFolder; 96 | if (xmlUri) { 97 | wsFolder = vscode.workspace.getWorkspaceFolder(xmlUri); 98 | } else { 99 | // Use the server connection from a workspace folder 100 | wsFolder = await getWsFolder("Pick the workspace folder to run the command in", false, false, true, true); 101 | if (!wsFolder) { 102 | if (wsFolder === undefined) { 103 | // Strict equality needed because undefined == null 104 | vscode.window.showErrorMessage( 105 | "'Extract Documents from XML File...' command requires a non-isfs workspace folder with an active server connection.", 106 | "Dismiss" 107 | ); 108 | } 109 | return; 110 | } 111 | } 112 | if (!wsFolder) return; 113 | const api = new AtelierAPI(wsFolder.uri); 114 | if (!xmlUri) { 115 | // Prompt the user the file to extract 116 | const uris = await vscode.window.showOpenDialog({ 117 | canSelectFiles: true, 118 | canSelectFolders: false, 119 | canSelectMany: false, 120 | openLabel: "Extract", 121 | filters: { 122 | "XML Files": ["xml"], 123 | }, 124 | defaultUri: wsFolder.uri, 125 | }); 126 | if (!Array.isArray(uris) || uris.length == 0) { 127 | // No file to extract 128 | return; 129 | } 130 | xmlUri = uris[0]; 131 | if (xmlUri.path.split(".").pop().toLowerCase() != "xml") { 132 | vscode.window.showErrorMessage("The selected file was not XML.", "Dismiss"); 133 | return; 134 | } 135 | } 136 | // Read the XML file 137 | const xmlContent = new TextDecoder().decode(await vscode.workspace.fs.readFile(xmlUri)).split(/\r?\n/); 138 | const xmlUriString = displayableUri(xmlUri); 139 | if (xmlContent.length < 3 || !exportHeader.test(xmlContent[1])) { 140 | vscode.window.showErrorMessage(`XML file '${xmlUriString}' is not an InterSystems export.`, "Dismiss"); 141 | return; 142 | } 143 | // Convert the file 144 | const udlDocs: { name: string; content: string[] }[] = await api 145 | .cvtXmlUdl(xmlContent.join("\n")) 146 | .then((data) => data.result.content); 147 | if (udlDocs.length == 0) { 148 | vscode.window.showErrorMessage(`File '${xmlUriString}' contains no documents that can be extracted.`, "Dismiss"); 149 | return; 150 | } 151 | // Prompt the user for documents to extract 152 | const docsToExtract = await vscode.window.showQuickPick( 153 | udlDocs.map((d) => { 154 | return { label: d.name, picked: true }; 155 | }), 156 | { 157 | canPickMany: true, 158 | ignoreFocusOut: true, 159 | title: "Pick the documents to extract", 160 | placeHolder: "Files are created using your 'objectscript.export' settings", 161 | } 162 | ); 163 | if (docsToExtract == undefined || docsToExtract.length == 0) { 164 | return; 165 | } 166 | const docWhitelist = docsToExtract.map((d) => d.label); 167 | // Write the UDL files 168 | const { atelier, folder, addCategory, map } = config("export", wsFolder.name); 169 | const rootFolder = 170 | wsFolder.uri.path + (typeof folder == "string" && folder.length ? `/${folder.replaceAll(path.sep, "/")}` : ""); 171 | let errs = 0; 172 | for (const udlDoc of udlDocs) { 173 | if (!docWhitelist.includes(udlDoc.name)) continue; // This file wasn't selected 174 | const fileUri = wsFolder.uri.with({ path: getFileName(rootFolder, udlDoc.name, atelier, addCategory, map, "/") }); 175 | if (await fileExists(fileUri)) { 176 | outputChannel.appendLine(`File '${displayableUri(fileUri)}' already exists.`); 177 | errs++; 178 | continue; 179 | } 180 | try { 181 | await replaceFile(fileUri, udlDoc.content); 182 | } catch (error) { 183 | outputChannel.appendLine( 184 | typeof error == "string" ? error : error instanceof Error ? error.toString() : JSON.stringify(error) 185 | ); 186 | errs++; 187 | } 188 | } 189 | if (errs) { 190 | vscode.window.showErrorMessage( 191 | `Failed to write ${errs} file${errs > 1 ? "s" : ""}. Check the 'ObjectScript' Output channel for details.`, 192 | "Dismiss" 193 | ); 194 | } 195 | } catch (error) { 196 | handleError(error, "Error executing 'Extract Documents from XML File...' command."); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/commands/showAllClassMembers.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtelierAPI } from "../api"; 3 | import { clsLangId, lsExtensionId } from "../extension"; 4 | import { currentFile, handleError, stripClassMemberNameQuotes } from "../utils"; 5 | import { DocumentContentProvider } from "../providers/DocumentContentProvider"; 6 | 7 | export async function showAllClassMembers(uri: vscode.Uri): Promise { 8 | try { 9 | // Determine the name of the class 10 | const uriString = uri.toString(); 11 | const textDocument = vscode.workspace.textDocuments.find((td) => td.uri.toString() == uriString); 12 | if (textDocument?.languageId != clsLangId) { 13 | vscode.window.showErrorMessage("The document in the active text editor is not a class definition.", "Dismiss"); 14 | return; 15 | } 16 | const file = currentFile(textDocument); 17 | if (!file) { 18 | vscode.window.showErrorMessage("The class definition in the active text editor is malformed.", "Dismiss"); 19 | return; 20 | } 21 | const cls = file.name.slice(0, -4); 22 | const api = new AtelierAPI(file.uri); 23 | if (!api.active) { 24 | vscode.window.showErrorMessage("Showing all members of a class requires an active server connection.", "Dismiss"); 25 | return; 26 | } 27 | // Get an array of all members 28 | const members: { 29 | Name: string; 30 | Origin: string; 31 | MemberType: "f" | "i" | "m" | "p" | "j" | "a" | "q" | "s" | "t" | "x"; 32 | Info: string; 33 | }[] = await api 34 | .actionQuery( 35 | `SELECT Name, Origin, MemberType, Info FROM ( 36 | SELECT Name, Origin, 'f' AS MemberType, Parent, Internal, NotInheritable, '('||REPLACE(Properties,',',', ')||') References '||ReferencedClass||(CASE WHEN ReferencedKey IS NOT NULL THEN '('||ReferencedKey||')' ELSE '' END) AS Info FROM %Dictionary.CompiledForeignKey UNION 37 | SELECT Name, Origin, 'i' AS MemberType, Parent, Internal, NotInheritable, (CASE WHEN Properties LIKE '%,%' THEN 'On ('||REPLACE(Properties,',',', ')||') ' WHEN Properties IS NOT NULL THEN 'On '||Properties||' ' ELSE '' END)||(CASE WHEN Type IS NOT NULL THEN '[ Type = '||Type||' ]' ELSE '' END) AS Info FROM %Dictionary.CompiledIndex WHERE NOT (Name %STARTSWITH '$') UNION 38 | SELECT Name, Origin, 'm' AS MemberType, Parent, Internal, NotInheritable, '('||(CASE WHEN FormalSpec IS NULL THEN '' ELSE REPLACE(REPLACE(FormalSpec,',',', '),'=',' = ') END)||')'||(CASE WHEN ReturnType IS NOT NULL THEN ' As '||ReturnType||(CASE WHEN ReturnTypeParams IS NOT NULL THEN '('||REPLACE(ReturnTypeParams,'=',' = ')||')' ELSE '' END) ELSE '' END) AS Info FROM %Dictionary.CompiledMethod WHERE Stub IS NULL UNION 39 | SELECT Name, Origin, 'p' AS MemberType, Parent, Internal, NotInheritable, CASE WHEN Expression IS NOT NULL THEN Expression WHEN _Default IS NOT NULL THEN _Default ELSE Type END AS Info FROM %Dictionary.CompiledParameter UNION 40 | SELECT Name, Origin, 'j' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledProjection UNION 41 | SELECT Name, Origin, 'a' AS MemberType, Parent, Internal, NotInheritable, CASE WHEN Collection IS NOT NULL THEN Collection||' Of '||Type ELSE Type END AS Info FROM %Dictionary.CompiledProperty UNION 42 | SELECT Name, Origin, 'q' AS MemberType, Parent, Internal, NotInheritable, '('||(CASE WHEN FormalSpec IS NULL THEN '' ELSE REPLACE(REPLACE(FormalSpec,',',', '),'=',' = ') END)||') As '||Type AS Info FROM %Dictionary.CompiledQuery UNION 43 | SELECT Name, Origin, 's' AS MemberType, Parent, Internal, NotInheritable, Type AS Info FROM %Dictionary.CompiledStorage UNION 44 | SELECT Name, Origin, 't' AS MemberType, Parent, Internal, NotInheritable, Event||' '||_Time||' '||Foreach AS Info FROM %Dictionary.CompiledTrigger UNION 45 | SELECT Name, Origin, 'x' AS MemberType, Parent, Internal, 0 AS NotInheritable, MimeType||(CASE WHEN SUBSTR(MimeType,-4) = '/xml' AND XMLNamespace IS NOT NULL THEN ' ('||XMLNamespace||')' ELSE '' END) AS Info FROM %Dictionary.CompiledXData 46 | ) WHERE Parent = ? AND ((NotInheritable = 0 AND Internal = 0) OR (Origin = Parent)) ORDER BY Name`.replaceAll( 47 | "\n", 48 | " " 49 | ), 50 | [cls] 51 | ) 52 | .then((data) => data?.result?.content ?? []); 53 | if (!members.length) { 54 | vscode.window.showWarningMessage( 55 | "The server returned no members for this class. If members are expected, re-compile the class then try again.", 56 | "Dismiss" 57 | ); 58 | return; 59 | } 60 | // Prompt the user to pick one 61 | const member = await vscode.window.showQuickPick( 62 | // Convert the query rows into QuickPickItems 63 | members.map((m) => { 64 | const [iconId, memberType] = (() => { 65 | switch (m.MemberType) { 66 | case "m": 67 | return ["method", "Method"]; 68 | case "q": 69 | return ["function", "Query"]; 70 | case "t": 71 | return ["event", "Trigger"]; 72 | case "p": 73 | return ["constant", "Parameter"]; 74 | case "i": 75 | return ["array", "Index"]; 76 | case "f": 77 | return ["key", "ForeignKey"]; 78 | case "x": 79 | return ["struct", "XData"]; 80 | case "s": 81 | return ["object", "Storage"]; 82 | case "j": 83 | return ["interface", "Projection"]; 84 | default: 85 | return ["property", "Property"]; 86 | } 87 | })(); 88 | let detail = m.Info; 89 | if ("mq".includes(m.MemberType)) { 90 | // Need to beautify the argument list 91 | detail = ""; 92 | let inQuotes = false; 93 | let braceDepth = 0; 94 | for (const c of m.Info) { 95 | if (c == '"') { 96 | inQuotes = !inQuotes; 97 | detail += c; 98 | continue; 99 | } 100 | if (!inQuotes) { 101 | if (c == "{") { 102 | braceDepth++; 103 | detail += c; 104 | continue; 105 | } else if (c == "}") { 106 | braceDepth = Math.max(0, braceDepth - 1); 107 | detail += c; 108 | continue; 109 | } 110 | } 111 | if (!inQuotes && braceDepth == 0 && ":&*=".includes(c)) { 112 | detail += c == ":" ? " As " : c == "&" ? "ByRef " : c == "*" ? "Output " : " = "; 113 | } else { 114 | detail += c; 115 | } 116 | } 117 | } 118 | return { 119 | label: m.Name, 120 | description: m.Origin, 121 | detail, 122 | iconPath: new vscode.ThemeIcon(`symbol-${iconId}`), 123 | memberType, 124 | }; 125 | }), 126 | { 127 | title: `All members of ${cls}`, 128 | placeHolder: "Pick a member to show it in the editor", 129 | } 130 | ); 131 | if (!member) return; 132 | // Show the picked member 133 | const targetUri = 134 | member.description == cls 135 | ? uri 136 | : DocumentContentProvider.getUri( 137 | `${member.description}.cls`, 138 | undefined, 139 | undefined, 140 | undefined, 141 | vscode.workspace.getWorkspaceFolder(uri)?.uri 142 | ); 143 | const symbols = ( 144 | await vscode.commands.executeCommand("vscode.executeDocumentSymbolProvider", targetUri) 145 | )[0]?.children; 146 | // Find the symbol for this member 147 | const memberType = member.memberType.toLowerCase(); 148 | const symbol = symbols?.find( 149 | (s) => 150 | stripClassMemberNameQuotes(s.name) == member.label && 151 | (memberType == "method" 152 | ? s.detail.toLowerCase().includes(memberType) 153 | : memberType == "property" 154 | ? ["property", "relationship"].includes(s.detail.toLowerCase()) 155 | : s.detail.toLowerCase() == memberType) 156 | ); 157 | if (!symbol) { 158 | vscode.window.showErrorMessage( 159 | `Did not find ${member.memberType} '${member.label}' in class '${member.description}'.`, 160 | "Dismiss" 161 | ); 162 | return; 163 | } 164 | // If Language Server is active, selectionRange is the member name. 165 | // Else, range is the first line of the member definition excluding description. 166 | const position = vscode.extensions.getExtension(lsExtensionId)?.isActive 167 | ? symbol.selectionRange.start 168 | : symbol.range.start; 169 | await vscode.window.showTextDocument(targetUri, { 170 | selection: new vscode.Range(position, position), 171 | preview: false, 172 | }); 173 | } catch (error) { 174 | handleError(error, "Failed to show all class members."); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/providers/DocumentFormattingEditProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Formatter } from "./Formatter"; 3 | import commands = require("./completion/commands.json"); 4 | import systemFunctions = require("./completion/systemFunctions.json"); 5 | import systemVariables = require("./completion/systemVariables.json"); 6 | 7 | export class DocumentFormattingEditProvider implements vscode.DocumentFormattingEditProvider { 8 | private _formatter: Formatter; 9 | public constructor() { 10 | this._formatter = new Formatter(); 11 | } 12 | 13 | public provideDocumentFormattingEdits( 14 | document: vscode.TextDocument, 15 | options: vscode.FormattingOptions, 16 | token: vscode.CancellationToken 17 | ): vscode.ProviderResult { 18 | return [...this.commands(document, options), ...this.functions(document, options)]; 19 | } 20 | 21 | private commands(document: vscode.TextDocument, options: vscode.FormattingOptions): vscode.TextEdit[] { 22 | const edits = []; 23 | let indent = 1; 24 | const isClass = document.fileName.toLowerCase().endsWith(".cls"); 25 | 26 | let inComment = false; 27 | let isCode = !isClass; 28 | let jsScript = false; 29 | let sql = false; 30 | let sqlParens = 0; 31 | const lineFrom = isClass ? 0 : 1; // just skip ROUTINE header 32 | for (let i = lineFrom; i < document.lineCount; i++) { 33 | const line = document.lineAt(i); 34 | const text = this.stripLineComments(line.text); 35 | 36 | if (!text.replace(/[\s\t]+/g, "").length) { 37 | continue; 38 | } 39 | 40 | if (text.match(/