diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a9f0ed6..c2d1117e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 16.78.0 + - New AL Symbols Browser language model tool for GitHub Copilot integration - allows AI assistants to search and browse AL project symbols (tables, pages, codeunits, etc.) with filtering, dependency inclusion, and source code resolution + - VS Code engine minimum version bumped to 1.95.0 + - Tree view icon paths fixed to use vscode.Uri (warning directives, duplicate code, AL outline) - Issues #652, #653, #654, #655 - BC27 fixes Thank you diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 3688cc5f..1e48e5a7 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "al-code-outline", - "version": "3.0.56", + "version": "16.78.0", "license": "MIT", "dependencies": { "vscode-jsonrpc": "^8.2.0", @@ -15,7 +15,7 @@ "devDependencies": { "@types/mocha": "^10.0.6", "@types/node": "18.x", - "@types/vscode": "^1.85.0", + "@types/vscode": "^1.95.0", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "@vscode/test-cli": "^0.0.4", @@ -24,7 +24,7 @@ "typescript": "^5.3.3" }, "engines": { - "vscode": "^1.85.0" + "vscode": "^1.95.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -295,10 +295,11 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.85.0.tgz", - "integrity": "sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==", - "dev": true + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.16.0", @@ -340,6 +341,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz", "integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.16.0", "@typescript-eslint/types": "6.16.0", @@ -534,6 +536,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -956,6 +959,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2546,6 +2550,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/vscode-extension/package.json b/vscode-extension/package.json index 5d6e23ce..d0f302ec 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -5,7 +5,7 @@ "version": "16.78.0", "publisher": "andrzejzwierzchowski", "engines": { - "vscode": "^1.85.0" + "vscode": "^1.95.0" }, "author": { "name": "Andrzej Zwierzchowski", @@ -1401,17 +1401,86 @@ ] } ], + "languageModelTools": [ + { + "name": "azALDevTools_symbolsBrowser", + "displayName": "AL Symbols Browser", + "toolReferenceName": "alSymbolsBrowser", + "icon": "$(symbol-class)", + "canBeReferencedInPrompt": true, + "modelDescription": "Search and browse AL (Business Central) project symbols such as tables, pages, codeunits, reports, queries, xmlports, enums, interfaces, and extensions. Supports wildcard name filtering (e.g. *Customer*), filtering by symbol kind, and optionally including dependency symbols and child members. Returns a formatted list of matching AL objects with their kind, id, name, source, and optionally their fields, methods, triggers, and other child elements.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Wildcard name filter to match symbol names, e.g. '*Customer*', '*Item*', 'Sales*'. Use * for any characters, ? for a single character." + }, + "symbolKind": { + "type": "string", + "description": "Filter by AL object kind. Use 'all' or omit to return all kinds.", + "enum": [ + "all", + "table", + "page", + "codeunit", + "report", + "query", + "xmlport", + "enum", + "interface", + "tableExtension", + "pageExtension", + "enumExtension", + "reportExtension", + "permissionSet", + "permissionSetExtension", + "profile", + "pageCustomization", + "controlAddIn", + "dotNetPackage", + "entitlement" + ] + }, + "includeDependencies": { + "type": "boolean", + "default": true, + "description": "Whether to include symbols from dependency apps. Defaults to true." + }, + "includeBody": { + "type": "boolean", + "default": false, + "description": "Whether to include child members (fields, methods, triggers, etc.) in the output. Defaults to false." + }, + "includeSourceCode": { + "type": "boolean", + "default": false, + "description": "Whether to resolve and return the full AL source code for each matched symbol. Works for project files and dependency .app packages. Defaults to false." + }, + "maxResults": { + "type": "number", + "default": 50, + "description": "Maximum number of symbols to return. Defaults to 50." + }, + "workspaceFolderPath": { + "type": "string", + "description": "Absolute path to the AL workspace folder. If omitted, uses the current active workspace folder." + } + } + } + } + ], "configuration": [ { "title": "AZ AL Dev Tools/AL Code Outline", "properties": { - "alOutline.activeBuildConfiguration" : { + "alOutline.activeBuildConfiguration": { "type": "string", "default": "", "scope": "resource", "description": "Name of the active build configuration (active app.json file) for the workspace folder" }, - "alOutline.buildConfigurationNaming" : { + "alOutline.buildConfigurationNaming": { "type": "string", "default": "none", "scope": "resource", @@ -1429,7 +1498,7 @@ "app.json." ] }, - "alOutline.limitEnumNameLength" : { + "alOutline.limitEnumNameLength": { "type": "boolean", "default": false, "scope": "resource", @@ -2184,7 +2253,7 @@ "devDependencies": { "@types/mocha": "^10.0.6", "@types/node": "18.x", - "@types/vscode": "^1.85.0", + "@types/vscode": "^1.95.0", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "@vscode/test-cli": "^0.0.4", diff --git a/vscode-extension/src/codeanalyzers/warningDirectivesTreeProvider.ts b/vscode-extension/src/codeanalyzers/warningDirectivesTreeProvider.ts index df3dd23e..8e4c81d8 100644 --- a/vscode-extension/src/codeanalyzers/warningDirectivesTreeProvider.ts +++ b/vscode-extension/src/codeanalyzers/warningDirectivesTreeProvider.ts @@ -53,7 +53,7 @@ export class WarningDirectivesTreeProvider implements vscode.TreeDataProvider { + protected _context : DevToolsExtensionContext; + + constructor(context : DevToolsExtensionContext) { + this._context = context; + } + + async invoke(options : vscode.LanguageModelToolInvocationOptions, token : vscode.CancellationToken) : Promise { + let input = options.input; + let includeDeps = input.includeDependencies !== false; + let includeBody = input.includeBody === true; + let maxResults = (input.maxResults) ? input.maxResults : 50; + let query = (input.query) ? input.query.trim() : undefined; + let kindFilter = (input.symbolKind) ? input.symbolKind.trim() : undefined; + + let workspacePath = (input.workspaceFolderPath) ? input.workspaceFolderPath : this._context.alLangProxy.getCurrentWorkspaceFolderPath(); + if (!workspacePath) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart('No AL workspace folder is currently open.') + ]); + } + + let lib = new ALProjectSymbolsLibrary(this._context, includeDeps, workspacePath); + let loaded = await lib.loadAsync(true); + + if (!loaded || !lib.rootSymbol) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart('Failed to load project symbols. Make sure the AL Language extension is active and the project compiles.') + ]); + } + + if (token.isCancellationRequested) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart('Operation cancelled.') + ]); + } + + //collect all AL objects from the symbol tree + let allObjects : AZSymbolInformation[] = []; + lib.rootSymbol.collectObjectSymbols(allObjects); + + //filter by kind + if ((kindFilter) && (kindFilter !== 'all')) { + let allowedKinds = SYMBOL_KIND_MAP[kindFilter]; + if (allowedKinds) { + allObjects = allObjects.filter(s => allowedKinds.indexOf(s.kind) >= 0); + } + } + + //filter by name query (supports wildcards) + if (query) { + let pattern = this.wildcardToRegex(query); + allObjects = allObjects.filter(s => pattern.test(s.name) || pattern.test(s.fullName)); + } + + //limit results + let totalCount = allObjects.length; + let truncated = allObjects.length > maxResults; + if (truncated) { + allObjects = allObjects.slice(0, maxResults); + } + + if (allObjects.length === 0) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart('No symbols found matching the given criteria.') + ]); + } + + //format output + let lines : string[] = []; + lines.push('Found ' + totalCount.toString() + ' symbol(s)' + (truncated ? ' (showing first ' + maxResults.toString() + ')' : '') + ':\n'); + for (let i=0; i, _token : vscode.CancellationToken) : Promise { + let input = options.input; + let message = 'Browsing AL project symbols'; + if (input.query) { + message += ' matching "' + input.query + '"'; + } + if ((input.symbolKind) && (input.symbolKind !== 'all')) { + message += ' (kind: ' + input.symbolKind + ')'; + } + return { invocationMessage: message }; + } + + protected formatSymbol(symbol : AZSymbolInformation, includeChildren : boolean, indent : number) : string { + let prefix = ' '.repeat(indent); + let kindName = symbol.getObjectTypeName(); + let id = symbol.id ? ' ' + symbol.id.toString() : ''; + let ext = symbol.extends ? ' extends "' + symbol.extends + '"' : ''; + let src = symbol.source ? ' [' + symbol.source + ']' : ''; + let line = prefix + '- ' + kindName + id + ' "' + symbol.name + '"' + ext + src; + if ((includeChildren) && (symbol.childSymbols)) { + for (let i=0; i { + if (!workspaceFolder) + return undefined; + + let locationResponse = await this._context.toolsLangServerClient.getProjectSymbolLocation( + new ToolsGetProjectSymbolLocationRequest( + workspaceFolder.uri.fsPath, libraryPath, symbol.kind.toString(), symbol.name)); + + if ((!locationResponse) || (!locationResponse.location)) + return undefined; + + let location = locationResponse.location; + if ((!location.schema) || (!location.sourcePath)) + return undefined; + + if (location.schema === 'file') { + try { + let content = fs.readFileSync(location.sourcePath, 'utf8'); + return content; + } catch { + return undefined; + } + } else if (location.schema === 'alapp') { + let pathParts = location.sourcePath.split('::'); + if (pathParts.length >= 2) { + let appPath = pathParts[0]; + let filePath = pathParts.slice(1).join('::'); + let contentResponse = await this._context.toolsLangServerClient.getALAppContent( + new ToolsGetALAppContentRequest(appPath, filePath)); + if ((contentResponse) && (contentResponse.source)) + return contentResponse.source; + } + } else if (location.schema === 'al-preview') { + try { + let previewUri = vscode.Uri.parse( + 'al-preview://allang/' + workspaceFolder.name + '/' + encodeURIComponent(location.sourcePath)); + let doc = await vscode.workspace.openTextDocument(previewUri); + return doc.getText(); + } catch { + return undefined; + } + } + + return undefined; + } + + protected wildcardToRegex(pattern : string) : RegExp { + let escaped = pattern.replace(/([.+^${}()|[\]\\])/g, '\\$1'); + let regexStr = escaped.replace(/\*/g, '.*').replace(/\?/g, '.'); + return new RegExp('^' + regexStr + '$', 'i'); + } +}