diff --git a/README.md b/README.md index 5ea29ca24..2f908ae2f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Follow the section [External Macro Libraries and COPY Members](#External-Macro-L The `pgm_conf.json` file can be provided implicitly by another product that supports integration with HLASM Language Support (e.g. Endevor Bridge for Git). +You can also specify your processor group configuration in the Visual Studio Code extension settings in the `hlasm.proc_grps` and `hlasm.pgm_conf` keys. When `proc_grps.json` or `pgm_conf.json` files are present in the workspace, they take precedence over any configuration that is specified in the extension settings. + ## Language Features The HLASM Language Support extension parses and analyzes all parts of a HLASM program including the listing. It resolves all ordinary symbols, variable symbols and checks the validity of most instructions. The extension supports conditional and unconditional branching and can define global and local variable symbols. It can also expand macros and COPY instructions. diff --git a/clients/vscode-hlasmplugin/CHANGELOG.md b/clients/vscode-hlasmplugin/CHANGELOG.md index e056e5e54..7523dce40 100644 --- a/clients/vscode-hlasmplugin/CHANGELOG.md +++ b/clients/vscode-hlasmplugin/CHANGELOG.md @@ -4,6 +4,7 @@ #### Added - Validate even-odd register requirements of machine instructions +- Support configuration in user or workspace settings #### Fixed - Accept additional URI schemes based on currently opened workspaces diff --git a/clients/vscode-hlasmplugin/package.json b/clients/vscode-hlasmplugin/package.json index 0c5b21363..155470dfc 100644 --- a/clients/vscode-hlasmplugin/package.json +++ b/clients/vscode-hlasmplugin/package.json @@ -25,7 +25,8 @@ "multi-root ready" ], "activationEvents": [ - "onStartupFinished" + "onStartupFinished", + "onFileSystem:broadcommfd.hlasm-language-support-configuration" ], "icon": "resources/logo.png", "main": "./dist/extension.js", @@ -440,6 +441,22 @@ "type": "object", "title": "HLASM Language Support Configuration", "properties": { + "hlasm.proc_grps": { + "scope": "resource", + "description": "Processor group configuration", + "$ref": "broadcommfd.hlasm-language-support-configuration:///schema/proc_grps.schema.json", + "default": { + "pgroups": [] + } + }, + "hlasm.pgm_conf": { + "scope": "resource", + "description": "Program configuration", + "$ref": "broadcommfd.hlasm-language-support-configuration:///schema/pgm_conf.schema.json", + "default": { + "pgms": [] + } + }, "hlasm.arguments": { "type": "array", "default": [], @@ -452,7 +469,7 @@ "type": "boolean", "default": true, "description": "Whether or not to send file events to HLASM LS extension (file created, changed or deleted). This can be disabled for performance consideration.", - "deprecationMessage": "Deprected: Language server follows dynamic registration options" + "deprecationMessage": "Deprecated: Language server follows dynamic registration options" }, "hlasm.continuationHandling": { "type": "boolean", diff --git a/clients/vscode-hlasmplugin/src/code_actions/configurationFilesActions.ts b/clients/vscode-hlasmplugin/src/code_actions/configurationFilesActions.ts index 2a5ec7768..b742f4ba8 100644 --- a/clients/vscode-hlasmplugin/src/code_actions/configurationFilesActions.ts +++ b/clients/vscode-hlasmplugin/src/code_actions/configurationFilesActions.ts @@ -13,52 +13,121 @@ */ import * as vscode from 'vscode'; -import { ConfigurationNodeDetails, ConfigurationNodes, anyConfigurationNodeExists } from '../configurationNodes'; -import { proc_grps_file, pgm_conf_file } from '../constants'; +import { ConfigurationNodeDetails, ConfigurationNodes, ConfigurationNodesWithoutWorkspace, SettingsConfigurationNodeDetails, SettingsConfigurationNodeDetailsWithoutWorkspace, anyConfigurationNodeExists } from '../configurationNodes'; -function codeActionFactory(create: boolean, filename: string | undefined, args: any[] | undefined): vscode.CodeAction { - const filesDescription: string = filename ? filename + ' configuration file' : 'configuration files'; +function filenameFromUri(uri: vscode.Uri): string { + return uri.path.substring(uri.path.lastIndexOf('/') + 1); +} + +function createAllConfigurationFiles(ws: vscode.Uri, relativeUri: string, pgroup: string): vscode.CodeAction { + return { + title: 'Create configuration files', + command: { + title: 'Create configuration files', + command: 'extension.hlasm-plugin.createCompleteConfig', + arguments: [ws, relativeUri, pgroup], + }, + kind: vscode.CodeActionKind.QuickFix + }; +} + +function createConfigurationFile(filename: string, ws: vscode.Uri, relativeUri: string | null, pgroup: string | null): vscode.CodeAction { + const filesDescription: string = filename + ' configuration file'; + return { + title: 'Create ' + filesDescription, + command: { + title: 'Create ' + filesDescription, + command: 'extension.hlasm-plugin.createCompleteConfig', + arguments: [ws, relativeUri, pgroup], + }, + kind: vscode.CodeActionKind.QuickFix + }; +} + +function modifyConfigurationFile(uri: vscode.Uri): vscode.CodeAction { + const filename = filenameFromUri(uri); + const filesDescription: string = filename + ' configuration file'; return { - title: (create ? 'Create ' : 'Modify ') + filesDescription, + title: 'Modify ' + filesDescription, command: { - title: (create ? 'Create ' : 'Open ') + filesDescription, - command: create ? 'extension.hlasm-plugin.createCompleteConfig' : 'vscode.open', - arguments: args + title: 'Open ' + filesDescription, + command: 'vscode.open', + arguments: [uri] }, kind: vscode.CodeActionKind.QuickFix }; } -function generateProcGrpsCodeAction(procGrps: ConfigurationNodeDetails, wsUri: vscode.Uri): vscode.CodeAction { - return procGrps.exists ? codeActionFactory(false, proc_grps_file, [procGrps.uri]) : codeActionFactory(true, proc_grps_file, [wsUri, null, '']); +function modifySettings(s: SettingsConfigurationNodeDetails | SettingsConfigurationNodeDetailsWithoutWorkspace): vscode.CodeAction { + const title = `Modify settings key '${s.key}'`; + if (s.scope === vscode.ConfigurationTarget.WorkspaceFolder) + return { + title, + command: { + title, + command: 'vscode.open', + arguments: [vscode.Uri.joinPath(s.ws, '.vscode/settings.json')], + }, + }; + else + return { + title, + command: { + title, + command: s.scope === vscode.ConfigurationTarget.Workspace ? 'workbench.action.openWorkspaceSettingsFile' : 'workbench.action.openSettingsJson', + arguments: [{ + revealSetting: { + key: s.key, + edit: true, + } + }], + }, + }; +} + +function generateProcGrpsCodeAction(procGrps: ConfigurationNodeDetails | SettingsConfigurationNodeDetails, wsUri: vscode.Uri): vscode.CodeAction { + if (procGrps.exists) + if ('uri' in procGrps) + return modifyConfigurationFile(procGrps.uri); + else + return modifySettings(procGrps); + else + return createConfigurationFile(filenameFromUri(procGrps.uri), wsUri, null, ''); } function generatePgmConfCodeAction(configNodes: ConfigurationNodes, wsUri: vscode.Uri, documentRelativeUri: string): vscode.CodeAction { - if (configNodes.pgmConf.exists) - return codeActionFactory(false, pgm_conf_file, [configNodes.pgmConf.uri]); + if (configNodes.pgmConf.exists) { + if ('uri' in configNodes.pgmConf) + return modifyConfigurationFile(configNodes.pgmConf.uri); + else + return modifySettings(configNodes.pgmConf); + } else { if (configNodes.bridgeJson.exists || configNodes.ebgFolder.exists) { // TODO: could we trigger B4G sync? } - return codeActionFactory(true, pgm_conf_file, [wsUri, documentRelativeUri, null]); + return createConfigurationFile(filenameFromUri(configNodes.pgmConf.uri), wsUri, documentRelativeUri, null); } } -export function generateConfigurationFilesCodeActions(suggestProcGrpsChange: boolean, suggestPgmConfChange: boolean, configNodes: ConfigurationNodes, wsUri: vscode.Uri, documentRelativeUri: string): vscode.CodeAction[] { +export function generateConfigurationFilesCodeActions(suggestProcGrpsChange: boolean, suggestPgmConfChange: boolean, config: { nodes: ConfigurationNodes, ws: vscode.Uri, documentRelativeUri: string } | { nodes: ConfigurationNodesWithoutWorkspace }): vscode.CodeAction[] { if (!suggestProcGrpsChange && !suggestPgmConfChange) return []; - if (!anyConfigurationNodeExists(configNodes)) - return [codeActionFactory(true, undefined, [wsUri, documentRelativeUri, 'GRP1'])]; + if (!('ws' in config)) + return [modifySettings(config.nodes.procGrps), modifySettings(config.nodes.pgmConf)]; + + if (!anyConfigurationNodeExists(config.nodes)) + return [createAllConfigurationFiles(config.ws, config.documentRelativeUri, 'GRP1')]; const result: vscode.CodeAction[] = []; if (suggestProcGrpsChange) - result.push(generateProcGrpsCodeAction(configNodes.procGrps, wsUri)); + result.push(generateProcGrpsCodeAction(config.nodes.procGrps, config.ws)); if (suggestPgmConfChange) - result.push(generatePgmConfCodeAction(configNodes, wsUri, documentRelativeUri)); + result.push(generatePgmConfCodeAction(config.nodes, config.ws, config.documentRelativeUri)); return result; } diff --git a/clients/vscode-hlasmplugin/src/configurationNodes.ts b/clients/vscode-hlasmplugin/src/configurationNodes.ts index 8c02833c7..953b47520 100644 --- a/clients/vscode-hlasmplugin/src/configurationNodes.ts +++ b/clients/vscode-hlasmplugin/src/configurationNodes.ts @@ -16,29 +16,103 @@ import * as vscode from 'vscode'; import { uriExists } from './helpers' import { hlasmplugin_folder, proc_grps_file, pgm_conf_file, bridge_json_file, ebg_folder as ebg_folder } from './constants'; -export interface ConfigurationNodeDetails { - uri: vscode.Uri | typeof vscode.Uri; +export type ConfigurationNodeDetails = { + uri: vscode.Uri; exists: boolean; -} +}; + +export type SettingsConfigurationNodeDetails = { + ws: vscode.Uri; + scope: vscode.ConfigurationTarget; + key: string; + exists: true; +}; + +export type SettingsConfigurationNodeDetailsWithoutWorkspace = { + scope: vscode.ConfigurationTarget.Global | vscode.ConfigurationTarget.Workspace; + key: string; + exists: boolean; +}; + +export type BridgeConfigurationNodeDetails = ConfigurationNodeDetails | { + uri: null; + exists: false; +}; -export interface ConfigurationNodes { - procGrps: ConfigurationNodeDetails; - pgmConf: ConfigurationNodeDetails; - bridgeJson: ConfigurationNodeDetails; +export type ConfigurationNodes = { + procGrps: ConfigurationNodeDetails | SettingsConfigurationNodeDetails; + pgmConf: ConfigurationNodeDetails | SettingsConfigurationNodeDetails; + bridgeJson: BridgeConfigurationNodeDetails; ebgFolder: ConfigurationNodeDetails; +}; + +export type ConfigurationNodesWithoutWorkspace = { + procGrps: SettingsConfigurationNodeDetailsWithoutWorkspace; + pgmConf: SettingsConfigurationNodeDetailsWithoutWorkspace; +}; + +function addAlternative(node: ConfigurationNodeDetails, ws: vscode.Uri, key: string): ConfigurationNodeDetails | SettingsConfigurationNodeDetails { + if (node.exists) + return node; + + const config = vscode.workspace.getConfiguration('hlasm', ws); + const data = config.get(key); + + if (!data || typeof data !== 'object') + return node; + + const details = config.inspect(key); + return { + ws, + scope: details?.workspaceFolderValue + ? vscode.ConfigurationTarget.WorkspaceFolder + : details?.workspaceValue + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global, + key: `hlasm.${key}`, + exists: true, + }; +} + +function generateConfigurationNodesWithoutWorkspace(): ConfigurationNodesWithoutWorkspace { + const config = vscode.workspace.getConfiguration('hlasm'); + const procDetails = config.inspect('hlasm.proc_grps'); + const pgmDetails = config.inspect('hlasm.pgm_conf'); + const scope = vscode.workspace.workspaceFile ? vscode.ConfigurationTarget.Workspace : vscode.ConfigurationTarget.Global; + + return { + procGrps: { scope, key: 'hlasm.proc_grps', exists: !!(procDetails?.globalValue || procDetails?.workspaceValue) }, + pgmConf: { scope, key: 'hlasm.pgm_conf', exists: !!(pgmDetails?.globalValue || pgmDetails?.workspaceValue) }, + }; } -export async function retrieveConfigurationNodes(workspace: vscode.Uri, documentUri: vscode.Uri | undefined, fs: vscode.FileSystem = vscode.workspace.fs): Promise { +export async function retrieveConfigurationNodes(workspace: vscode.Uri, documentUri: vscode.Uri | undefined): Promise; +export async function retrieveConfigurationNodes(workspace: vscode.Uri, documentUri: vscode.Uri | undefined, fs: vscode.FileSystem): Promise; +export async function retrieveConfigurationNodes(workspace: undefined, documentUri: vscode.Uri | undefined): Promise; +export async function retrieveConfigurationNodes(workspace: undefined, documentUri: vscode.Uri | undefined, fs: vscode.FileSystem): Promise; +export async function retrieveConfigurationNodes(workspace: vscode.Uri | undefined, documentUri: vscode.Uri | undefined, fs: vscode.FileSystem = vscode.workspace.fs): Promise { + if (!workspace) + return generateConfigurationNodesWithoutWorkspace(); + const procGrps = vscode.Uri.joinPath(workspace, hlasmplugin_folder, proc_grps_file); const pgmConf = vscode.Uri.joinPath(workspace, hlasmplugin_folder, pgm_conf_file); const bridgeJson = documentUri ? vscode.Uri.joinPath(documentUri, "..", bridge_json_file) : undefined; const ebgFolder = vscode.Uri.joinPath(workspace, ebg_folder); - return Promise.all([ + + + const results = await Promise.all([ uriExists(procGrps, fs).then(b => { return { uri: procGrps, exists: b }; }), uriExists(pgmConf, fs).then(b => { return { uri: pgmConf, exists: b }; }), - bridgeJson ? uriExists(bridgeJson, fs).then(b => { return { uri: bridgeJson, exists: b }; }) : { uri: vscode.Uri, exists: false }, + bridgeJson ? uriExists(bridgeJson, fs).then(b => { return { uri: bridgeJson, exists: b }; }) : { uri: null, exists: false } as BridgeConfigurationNodeDetails, uriExists(ebgFolder, fs).then(b => { return { uri: ebgFolder, exists: b }; }), - ]).then(arr => { return { procGrps: arr[0], pgmConf: arr[1], bridgeJson: arr[2], ebgFolder: arr[3] }; }); + ]); + + return { + procGrps: addAlternative(results[0], workspace, 'proc_grps'), + pgmConf: addAlternative(results[1], workspace, 'pgm_conf'), + bridgeJson: results[2], + ebgFolder: results[3], + }; } export function anyConfigurationNodeExists(configNodes: ConfigurationNodes) { diff --git a/clients/vscode-hlasmplugin/src/configurationSchemaFileSystem.ts b/clients/vscode-hlasmplugin/src/configurationSchemaFileSystem.ts new file mode 100644 index 000000000..ff13ced47 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/configurationSchemaFileSystem.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +import * as vscode from 'vscode'; +import { schemaConfiguration } from './constants'; + +export function registerConfigurationFileSystemProvider(extensionUri: vscode.Uri) { + const generatePath = (uri: vscode.Uri) => vscode.Uri.joinPath(extensionUri, uri.path); + + const emmiter = new vscode.EventEmitter(); + vscode.workspace.registerFileSystemProvider(schemaConfiguration, { + onDidChangeFile: emmiter.event, + watch() { return { dispose: () => { } }; }, + stat(uri) { return vscode.workspace.fs.stat(generatePath(uri)); }, + readDirectory() { throw Error("not implemented"); }, + createDirectory() { throw Error("not implemented"); }, + readFile(uri) { return vscode.workspace.fs.readFile(generatePath(uri)); }, + writeFile() { throw Error("not implemented"); }, + delete() { throw Error("not implemented"); }, + rename() { throw Error("not implemented"); }, + }, { isReadonly: true, isCaseSensitive: true }); + +} diff --git a/clients/vscode-hlasmplugin/src/configurationsHandler.ts b/clients/vscode-hlasmplugin/src/configurationsHandler.ts index 699a43a9a..32c88c72f 100644 --- a/clients/vscode-hlasmplugin/src/configurationsHandler.ts +++ b/clients/vscode-hlasmplugin/src/configurationsHandler.ts @@ -166,9 +166,11 @@ export class ConfigurationsHandler { return generateConfigurationFilesCodeActions( !configNodes.procGrps.exists, !(configNodes.pgmConf.exists || configNodes.bridgeJson.exists || configNodes.ebgFolder.exists), - configNodes, - workspace.uri, - documentRelativeUri) + { + nodes: configNodes, + ws: workspace.uri, + documentRelativeUri + }) .map(x => new vscode.CodeLens(document.lineAt(0).range, x.command)); } } diff --git a/clients/vscode-hlasmplugin/src/constants.ts b/clients/vscode-hlasmplugin/src/constants.ts index 5de6a6305..68681d3ef 100644 --- a/clients/vscode-hlasmplugin/src/constants.ts +++ b/clients/vscode-hlasmplugin/src/constants.ts @@ -36,3 +36,4 @@ export const debugTypeHlasm = 'hlasm'; export const schemeExternalFiles = 'hlasm-external'; export const schemeVirtualFiles = 'hlasm'; export const schemeOutput = 'hlasm-output'; +export const schemaConfiguration = 'broadcommfd.hlasm-language-support-configuration'; diff --git a/clients/vscode-hlasmplugin/src/extension.ts b/clients/vscode-hlasmplugin/src/extension.ts index 259a3749e..fa7245886 100644 --- a/clients/vscode-hlasmplugin/src/extension.ts +++ b/clients/vscode-hlasmplugin/src/extension.ts @@ -45,6 +45,7 @@ import { registerListingServices } from './hlasmListingServices'; import { MementoKey } from './mementoKeys'; import { registerOutputDocumentContentProvider, showOutputCommand } from './hlasmOutputContentProvider'; import { setServerLogLevel } from './hlasmLogLevel'; +import { registerConfigurationFileSystemProvider } from './configurationSchemaFileSystem'; const sleep = (ms: number) => { return new Promise((resolve) => { setTimeout(resolve, ms) }); @@ -84,6 +85,8 @@ function whenString(x: any): string | undefined { * activates the extension */ export async function activate(context: vscode.ExtensionContext): Promise { + registerConfigurationFileSystemProvider(context.extensionUri); + const serverVariant = getConfig('serverVariant', 'native'); const version = whenString(context.extension.packageJSON?.version); diff --git a/clients/vscode-hlasmplugin/src/hlasmCodeActionsProvider.ts b/clients/vscode-hlasmplugin/src/hlasmCodeActionsProvider.ts index cff8b8281..a72629d4d 100644 --- a/clients/vscode-hlasmplugin/src/hlasmCodeActionsProvider.ts +++ b/clients/vscode-hlasmplugin/src/hlasmCodeActionsProvider.ts @@ -36,19 +36,19 @@ export class HLASMCodeActionsProvider implements vscode.CodeActionProvider { if (E049.length > 0) result.push(...await generateOpcodeSuggestionsCodeActions(E049, this.client, document)); - const wsUri = vscode.workspace.getWorkspaceFolder(document.uri)?.uri; - if (!wsUri) - return result; - const documentRelativeUri = vscode.workspace.asRelativePath(document.uri); + const ws = vscode.workspace.getWorkspaceFolder(document.uri)?.uri; - const configNodes = await retrieveConfigurationNodes(wsUri, document.uri); + const config = ws + ? { nodes: await retrieveConfigurationNodes(ws, document.uri), ws, documentRelativeUri: vscode.workspace.asRelativePath(document.uri) } + : { nodes: await retrieveConfigurationNodes(undefined, document.uri) }; - if (E049.length > 0 && configNodes.procGrps.exists) + if (E049.length > 0 && config.nodes.procGrps.exists) result.push(...generateDownloadDependenciesCodeActions()); const suggestProcGrpsChange = E049.length > 0; const suggestPgmConfChange = E049.length > 0 || context.diagnostics.some(x => x.code === 'SUP'); - result.push(...generateConfigurationFilesCodeActions(suggestProcGrpsChange, suggestPgmConfChange, configNodes, wsUri, documentRelativeUri)); + + result.push(...generateConfigurationFilesCodeActions(suggestProcGrpsChange, suggestPgmConfChange, config)); return result; } diff --git a/clients/vscode-hlasmplugin/src/test/suite/codeActions.test.ts b/clients/vscode-hlasmplugin/src/test/suite/codeActions.test.ts index 0f5dd15cd..b1a5616c0 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/codeActions.test.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/codeActions.test.ts @@ -123,4 +123,14 @@ suite('Code actions', () => { await helper.closeAllEditors(); }).timeout(15000).slow(10000); + + test('Without workspace', async () => { + const { document } = await helper.showUntitledDocument(' MAC\n END', 'hlasm'); + + const codeActionsList = await queryCodeActions(document.uri, new vscode.Range(0, 1, 0, 3), 500); + + assert.strictEqual(codeActionsList.map(x => x.title).filter(x => x.indexOf('settings key') != -1).length, 2); + + await helper.closeAllEditors(); + }).timeout(15000).slow(10000); }); diff --git a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts index 9c09f2562..3a63b9038 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts @@ -69,6 +69,16 @@ export async function showDocument(workspace_file: string, language_id: string | return result; } +export async function showUntitledDocument(content: string, language: string | undefined = undefined) { + // open and show the file + let document = await vscode.workspace.openTextDocument({ content, language }); + + const visible = activeEditorChanged(); + const result = { editor: await vscode.window.showTextDocument(document, { preview: false }), document }; + assert.strictEqual(await visible, result.editor); + return result; +} + export async function closeAllEditors() { await vscode.commands.executeCommand('workbench.action.files.revert'); // workbench.action.closeAllEditors et al. saves content diff --git a/language_server/src/lsp/feature_workspace_folders.cpp b/language_server/src/lsp/feature_workspace_folders.cpp index 32dc84fe9..62031e655 100644 --- a/language_server/src/lsp/feature_workspace_folders.cpp +++ b/language_server/src/lsp/feature_workspace_folders.cpp @@ -21,25 +21,6 @@ #include "utils/path_conversions.h" #include "utils/platform.h" -NLOHMANN_JSON_NAMESPACE_BEGIN -template<> -struct adl_serializer -{ - static void from_json(const nlohmann::json& config, hlasm_plugin::parser_library::lib_config& cfg) - { - cfg = {}; - - auto found = config.find("diagnosticsSuppressLimit"); - if (found != config.end() && found->is_number()) - { - cfg.diag_supress_limit = found->get(); - if (cfg.diag_supress_limit < 0) - cfg.diag_supress_limit = 0; - } - } -}; -NLOHMANN_JSON_NAMESPACE_END - namespace hlasm_plugin::language_server::lsp { feature_workspace_folders::feature_workspace_folders( @@ -176,7 +157,8 @@ void feature_workspace_folders::send_configuration_request() static const nlohmann::json config_request_args { { "items", nlohmann::json::array_t { - { { "section", "hlasm" } }, + { { "section", "hlasm.diagnosticsSuppressLimit" } }, + nlohmann::json::object(), }, } }; response_->request( @@ -225,13 +207,23 @@ void feature_workspace_folders::register_file_change_notifictions() void feature_workspace_folders::configuration(const nlohmann::json& params) const { - if (params.size() != 1) + if (params.size() != 2) { LOG_WARNING("Unexpected configuration response received."); return; } - ws_mngr_.configuration_changed(parser_library::lib_config(params[0])); + const auto& suppressLimit = params[0]; + parser_library::lib_config cfg; + if (suppressLimit.is_number()) + { + cfg.diag_supress_limit = suppressLimit.get(); + if (cfg.diag_supress_limit < 0) + cfg.diag_supress_limit = 0; + } + const auto& full_cfg = params[1]; + + ws_mngr_.configuration_changed(cfg, full_cfg.dump()); } void feature_workspace_folders::did_change_configuration(const nlohmann::json&) { send_configuration_request(); } diff --git a/language_server/test/lsp/workspace_folders_test.cpp b/language_server/test/lsp/workspace_folders_test.cpp index 21010c2cb..1efa714d5 100644 --- a/language_server/test/lsp/workspace_folders_test.cpp +++ b/language_server/test/lsp/workspace_folders_test.cpp @@ -23,6 +23,7 @@ #include "nlohmann/json.hpp" #include "utils/platform.h" +using namespace ::testing; using namespace hlasm_plugin; using namespace hlasm_plugin::language_server; using hlasm_plugin::utils::platform::is_windows; @@ -44,23 +45,23 @@ TEST(workspace_folders, did_change_workspace_folders) std::map notifs; f.register_methods(notifs); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("OneDrive"), ::testing::StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("OneDrive"), StrEq(ws1_uri))); auto params1 = nlohmann::json::parse(R"({"event":{"added":[{"uri":")" + ws1_uri + R"(","name":"OneDrive"}],"removed":[]}})"); notifs["workspace/didChangeWorkspaceFolders"].as_notification_handler()(params1); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("TwoDrive"), ::testing::StrEq(ws2_uri))); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("ThreeDrive"), ::testing::StrEq(ws3_uri))); - EXPECT_CALL(ws_mngr, remove_workspace(::testing::StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("TwoDrive"), StrEq(ws2_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("ThreeDrive"), StrEq(ws3_uri))); + EXPECT_CALL(ws_mngr, remove_workspace(StrEq(ws1_uri))); auto params2 = nlohmann::json::parse(R"({"event":{"added":[{"uri":")" + ws2_uri + R"(","name":"TwoDrive"},{"uri":")" + ws3_uri + R"(","name":"ThreeDrive"}],"removed":[{"uri":")" + ws1_uri + R"(","name":"OneDrive"}]}})"); notifs["workspace/didChangeWorkspaceFolders"].as_notification_handler()(params2); - EXPECT_CALL(ws_mngr, remove_workspace(::testing::StrEq(ws2_uri))); - EXPECT_CALL(ws_mngr, remove_workspace(::testing::StrEq(ws3_uri))); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("FourDrive"), ::testing::StrEq(ws4_uri))); + EXPECT_CALL(ws_mngr, remove_workspace(StrEq(ws2_uri))); + EXPECT_CALL(ws_mngr, remove_workspace(StrEq(ws3_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("FourDrive"), StrEq(ws4_uri))); auto params3 = nlohmann::json::parse(R"({"event":{"added":[{"uri":")" + ws4_uri + R"(","name":"FourDrive"}],"removed":[{"uri":")" + ws2_uri + R"(","name":"TwoDrive"},{"uri":")" + ws3_uri + R"(","name":"ThreeDrive"}]}})"); @@ -113,8 +114,8 @@ TEST(workspace_folders, initialize_folders) "workspaceFolders":[{"uri":")" + ws1_uri + R"(","name":"one"},{"uri":")" + ws2_uri + R"(","name":"two"}]})"); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("one"), ::testing::StrEq(ws1_uri))); - EXPECT_CALL(ws_mngr, add_workspace(::testing::StrEq("two"), ::testing::StrEq(ws2_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("one"), StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(StrEq("two"), StrEq(ws2_uri))); f.initialize_feature(init2); f.initialized(); @@ -125,7 +126,7 @@ TEST(workspace_folders, initialize_folders) + ws1_uri + R"(","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"executeCommand":{"dynamicRegistration":true},"configuration":true, "workspaceFolders":false},"textDocument":{"publishDiagnostics":{"relatedInformation":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"]}},"definition":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true},"codeAction":{"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"typeDefinition":{"dynamicRegistration":true},"implementation":{"dynamicRegistration":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true}}},"trace":"off"})"); - EXPECT_CALL(ws_mngr, add_workspace(_, ::testing::StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(_, StrEq(ws1_uri))); f.initialize_feature(init3); f.initialized(); @@ -135,7 +136,7 @@ TEST(workspace_folders, initialize_folders) + ws1_path_json_string + R"(", "rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"executeCommand":{"dynamicRegistration":true},"configuration":true, "workspaceFolders":false},"textDocument":{"publishDiagnostics":{"relatedInformation":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"]}},"definition":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true},"codeAction":{"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"typeDefinition":{"dynamicRegistration":true},"implementation":{"dynamicRegistration":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true}}},"trace":"off"})"); - EXPECT_CALL(ws_mngr, add_workspace(_, ::testing::StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(_, StrEq(ws1_uri))); f.initialize_feature(init4); f.initialized(); @@ -144,7 +145,7 @@ TEST(workspace_folders, initialize_folders) "rootPath":")" + ws1_path_json_string + R"(","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"executeCommand":{"dynamicRegistration":true},"configuration":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"]}},"definition":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true},"codeAction":{"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"typeDefinition":{"dynamicRegistration":true},"implementation":{"dynamicRegistration":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true}}},"trace":"off"})"); - EXPECT_CALL(ws_mngr, add_workspace(_, ::testing::StrEq(ws1_uri))); + EXPECT_CALL(ws_mngr, add_workspace(_, StrEq(ws1_uri))); f.initialize_feature(init5); f.initialized(); } @@ -196,24 +197,24 @@ TEST(workspace_folders, did_change_configuration) { "items", nlohmann::json::array_t { - { - { "section", "hlasm" }, - }, + { { "section", "hlasm.diagnosticsSuppressLimit" } }, + nlohmann::json::object(), }, }, }; - EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_, ::testing::_)) - .WillOnce(::testing::SaveArg<2>(&handler)); + EXPECT_CALL(provider, request("workspace/configuration", config_request_args, _, _)).WillOnce(SaveArg<2>(&handler)); methods["workspace/didChangeConfiguration"].as_notification_handler()("{}"_json); parser_library::lib_config expected_config; expected_config.diag_supress_limit = 42; - EXPECT_CALL(ws_mngr, configuration_changed(::testing::Eq(expected_config))); + EXPECT_CALL(ws_mngr, + configuration_changed(Eq(expected_config), + Eq(R"({"hlasm":{"diagnosticsSuppressLimit":42,"pgm_conf":{"pgms":[]},"proc_grps":{"pgroups":[]}}})"))); - handler(R"([{"diagnosticsSuppressLimit":42}])"_json); + handler(R"([42,{"hlasm":{"diagnosticsSuppressLimit":42,"pgm_conf":{"pgms":[]},"proc_grps":{"pgroups":[]}}}])"_json); } TEST(workspace_folders, did_change_configuration_with_requests) @@ -229,7 +230,7 @@ TEST(workspace_folders, did_change_configuration_with_requests) EXPECT_CALL(req_mock, request_workspace_configuration(StrEq("testurl"), _)); - ws_mngr->configuration_changed({}); + ws_mngr->configuration_changed({}, {}); } TEST(workspace_folders, did_change_configuration_empty_configuration_params) @@ -251,17 +252,17 @@ TEST(workspace_folders, did_change_configuration_empty_configuration_params) { "items", nlohmann::json::array_t { - { { "section", "hlasm" } }, + { { "section", "hlasm.diagnosticsSuppressLimit" } }, + nlohmann::json::object(), }, }, }; - EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_, ::testing::_)) - .WillOnce(::testing::SaveArg<2>(&handler)); + EXPECT_CALL(provider, request("workspace/configuration", config_request_args, _, _)).WillOnce(SaveArg<2>(&handler)); methods["workspace/didChangeConfiguration"].as_notification_handler()("{}"_json); - EXPECT_CALL(ws_mngr, configuration_changed(::testing::_)).Times(0); + EXPECT_CALL(ws_mngr, configuration_changed(_, _)).Times(0); handler(R"([])"_json); } diff --git a/language_server/test/ws_mngr_mock.h b/language_server/test/ws_mngr_mock.h index cf0e7cead..8608fd766 100644 --- a/language_server/test/ws_mngr_mock.h +++ b/language_server/test/ws_mngr_mock.h @@ -68,7 +68,7 @@ class ws_mngr_mock : public workspace_manager (std::string_view, workspace_manager_response>), (override)); - MOCK_METHOD(void, configuration_changed, (const lib_config& new_config), (override)); + MOCK_METHOD(void, configuration_changed, (const lib_config& new_config, std::string_view full_cfg), (override)); MOCK_METHOD(void, register_diagnostics_consumer, (diagnostics_consumer * consumer), (override)); MOCK_METHOD(void, unregister_diagnostics_consumer, (diagnostics_consumer * consumer), (override)); diff --git a/parser_library/include/workspace_manager.h b/parser_library/include/workspace_manager.h index 42a685abd..9823d7199 100644 --- a/parser_library/include/workspace_manager.h +++ b/parser_library/include/workspace_manager.h @@ -133,7 +133,7 @@ class workspace_manager virtual void document_symbol( std::string_view document_uri, workspace_manager_response> resp) = 0; - virtual void configuration_changed(const lib_config& new_config) = 0; + virtual void configuration_changed(const lib_config& new_config, std::string_view full_cfg) = 0; // implementation of observer pattern - register consumer. virtual void register_diagnostics_consumer(diagnostics_consumer* consumer) = 0; diff --git a/parser_library/src/workspace_manager.cpp b/parser_library/src/workspace_manager.cpp index 446458406..29b92a034 100644 --- a/parser_library/src/workspace_manager.cpp +++ b/parser_library/src/workspace_manager.cpp @@ -692,16 +692,19 @@ class workspace_manager_impl final : public workspace_manager, }); } - void configuration_changed(const lib_config& new_config) override + void configuration_changed(const lib_config& new_config, std::string_view full_cfg) override { // TODO: should this action be also performed IN ORDER? m_global_config = new_config; + auto cfg = std::make_shared( + full_cfg.empty() ? nlohmann::json() : nlohmann::json::parse(full_cfg)); m_work_queue.emplace_back(work_item { next_unique_id(), &m_implicit_workspace, - std::function([this, &ws = m_implicit_workspace.ws]() -> utils::task { + std::function([this, &ws = m_implicit_workspace.ws, cfg = std::move(cfg)]() -> utils::task { + m_implicit_workspace.settings = cfg; return ws.settings_updated().then([this](bool u) { if (u) notify_diagnostics_consumers(); @@ -1137,7 +1140,17 @@ class workspace_manager_impl final : public workspace_manager, , m_file_manager(*this) , m_implicit_workspace(m_file_manager, m_global_config, this) , m_vscode_extensions(vscode_extensions) - {} + { + m_work_queue.emplace_back(work_item { + next_unique_id(), + &m_implicit_workspace, + std::function([this, &ws = m_implicit_workspace.ws]() -> utils::task { + return ws.open().then([this]() { notify_diagnostics_consumers(); }); + }), + {}, + work_item_type::workspace_open, + }); + } workspace_manager_impl(const workspace_manager_impl&) = delete; workspace_manager_impl& operator=(const workspace_manager_impl&) = delete; diff --git a/parser_library/src/workspaces/library_local.cpp b/parser_library/src/workspaces/library_local.cpp index 3cdc5d567..fa2dbab06 100644 --- a/parser_library/src/workspaces/library_local.cpp +++ b/parser_library/src/workspaces/library_local.cpp @@ -52,7 +52,7 @@ library_local::library_local(file_manager& file_manager, , m_lib_loc(std::move(lib_loc)) , m_extensions(std::move(options.extensions)) , m_optional(options.optional_library) - , m_proc_grps_loc(std::move(proc_grps_loc)) + , m_err_loc(std::move(proc_grps_loc)) { if (m_extensions.size()) adjust_extensions_vector(m_extensions); @@ -64,7 +64,7 @@ library_local::library_local(library_local&& l) noexcept , m_files_collection(l.m_files_collection.exchange(nullptr)) , m_extensions(std::move(l.m_extensions)) , m_optional(l.m_optional) - , m_proc_grps_loc(std::move(l.m_proc_grps_loc)) + , m_err_loc(std::move(l.m_err_loc)) {} utils::task library_local::refresh() @@ -136,13 +136,13 @@ library_local::files_collection_t library_local::load_files( break; case hlasm_plugin::utils::path::list_directory_rc::not_exists: if (!m_optional) - new_diags.push_back(error_L0002(m_proc_grps_loc, m_lib_loc)); + new_diags.push_back(error_L0002(m_err_loc, m_lib_loc)); break; case hlasm_plugin::utils::path::list_directory_rc::not_a_directory: - new_diags.push_back(error_L0002(m_proc_grps_loc, m_lib_loc)); + new_diags.push_back(error_L0002(m_err_loc, m_lib_loc)); break; case hlasm_plugin::utils::path::list_directory_rc::other_failure: - new_diags.push_back(error_L0001(m_proc_grps_loc, m_lib_loc)); + new_diags.push_back(error_L0001(m_err_loc, m_lib_loc)); break; } @@ -193,7 +193,7 @@ library_local::files_collection_t library_local::load_files( } if (conflict_count > 0) - new_diags.push_back(warning_L0004(m_proc_grps_loc, m_lib_loc, file_name_conflicts, !m_extensions.empty())); + new_diags.push_back(warning_L0004(m_err_loc, m_lib_loc, file_name_conflicts, !m_extensions.empty())); m_files_collection.store(new_state); diff --git a/parser_library/src/workspaces/library_local.h b/parser_library/src/workspaces/library_local.h index 6be9031cb..033819143 100644 --- a/parser_library/src/workspaces/library_local.h +++ b/parser_library/src/workspaces/library_local.h @@ -72,7 +72,7 @@ class library_local final : public library library_local(file_manager& file_manager, utils::resource::resource_location lib_loc, library_local_options options, - utils::resource::resource_location proc_grps_loc); + utils::resource::resource_location err_loc); library_local(const library_local&) = delete; library_local& operator=(const library_local&) = delete; @@ -123,7 +123,7 @@ class library_local final : public library atomic_files_collection_t m_files_collection; std::vector m_extensions; bool m_optional = false; - utils::resource::resource_location m_proc_grps_loc; + utils::resource::resource_location m_err_loc; files_collection_t load_files(std::pair>, utils::path::list_directory_rc>); diff --git a/parser_library/src/workspaces/workspace_configuration.cpp b/parser_library/src/workspaces/workspace_configuration.cpp index 53ac4e095..a9c93421c 100644 --- a/parser_library/src/workspaces/workspace_configuration.cpp +++ b/parser_library/src/workspaces/workspace_configuration.cpp @@ -131,9 +131,9 @@ const nlohmann::json* find_member(std::string_view key, const nlohmann::json& j) return nullptr; } -std::optional find_setting(std::string_view key, const nlohmann::json& m_j) +const nlohmann::json* find_subobject(std::string_view key, const nlohmann::json& input) { - const nlohmann::json* j = &m_j; + const nlohmann::json* j = &input; while (true) { @@ -142,7 +142,7 @@ std::optional find_setting(std::string_view key, const nlohman j = find_member(subkey, *j); if (!j) - return std::nullopt; + return nullptr; if (dot == std::string_view::npos) break; @@ -150,10 +150,7 @@ std::optional find_setting(std::string_view key, const nlohman key.remove_prefix(dot + 1); } - if (!j->is_string()) - return std::nullopt; - else - return j->get(); + return j; } struct json_settings_replacer @@ -165,6 +162,7 @@ struct json_settings_replacer const utils::resource::resource_location& location; std::match_results matches; + std::unordered_set> unavailable; void operator()(nlohmann::json& val) @@ -201,12 +199,12 @@ struct json_settings_replacer if (key.starts_with(config_section)) { auto reduced_key = key.substr(config_section.size()); - auto v = find_setting(reduced_key, global_settings); - if (v.has_value()) - r.append(*v); + const auto* v = find_subobject(reduced_key, global_settings); + if (v && v->is_string()) + r.append(v->get()); else - unavailable.emplace(key); - utilized_settings_values.emplace(reduced_key, v); + unavailable.emplace(reduced_key); + update_utilized_settings(reduced_key, v); } else if (key == "workspaceFolder") { @@ -223,6 +221,20 @@ struct json_settings_replacer return result; } + + void update_utilized_settings(std::string_view key, const nlohmann::json* j) + { + if (!j) + { + utilized_settings_values.emplace( + std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(std::nullopt)); + } + else + { + utilized_settings_values.emplace( + std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(*j)); + } + } }; const std::regex json_settings_replacer::config_reference(R"(\$\{([^}]+)\})"); @@ -238,16 +250,19 @@ workspace_configuration::workspace_configuration(file_manager& fm, , m_external_configuration_requests(ecr) , m_pgm_conf_store(std::make_unique(m_proc_grps)) { - auto hlasm_folder = utils::resource::resource_location::join(m_location, HLASM_PLUGIN_FOLDER); - m_proc_grps_loc = utils::resource::resource_location::join(hlasm_folder, FILENAME_PROC_GRPS); - m_pgm_conf_loc = utils::resource::resource_location::join(hlasm_folder, FILENAME_PGM_CONF); + if (!m_location.empty()) + { + auto hlasm_folder = utils::resource::resource_location::join(m_location, HLASM_PLUGIN_FOLDER); + m_proc_grps_loc = utils::resource::resource_location::join(hlasm_folder, FILENAME_PROC_GRPS); + m_pgm_conf_loc = utils::resource::resource_location::join(hlasm_folder, FILENAME_PGM_CONF); + } } workspace_configuration::~workspace_configuration() = default; bool workspace_configuration::is_configuration_file(const utils::resource::resource_location& file) const { - return !m_location.empty() && (is_config_file(file) || is_b4g_config_file(file)); + return !m_location.empty() && !file.empty() && (is_config_file(file) || is_b4g_config_file(file)); } template @@ -275,7 +290,7 @@ std::shared_ptr workspace_configuration::get_local_library( return it->second.first; } - auto result = std::make_shared(m_file_manager, url, opts, m_proc_grps_loc); + auto result = std::make_shared(m_file_manager, url, opts, m_proc_grps_current_loc); m_libraries.try_emplace(std::make_pair(url, library_options(opts)), result, true); return result; } @@ -403,7 +418,7 @@ utils::task workspace_configuration::process_processor_group_library(const confi std::optional lib_path = substitute_home_directory(lib.path); if (!lib_path.has_value()) { - diags.push_back(warning_L0006(m_proc_grps_loc, lib.path)); + diags.push_back(warning_L0006(m_proc_grps_current_loc, lib.path)); return {}; } @@ -445,7 +460,7 @@ void workspace_configuration::process_program(const config::program_mapping& pgm std::optional pgm_name = substitute_home_directory(pgm.program); if (!pgm_name.has_value()) { - diags.push_back(warning_L0006(m_pgm_conf_loc, pgm.program)); + diags.push_back(warning_L0006(m_pgm_conf_current_loc, pgm.program)); return; } @@ -466,7 +481,7 @@ void workspace_configuration::process_program(const config::program_mapping& pgm bool workspace_configuration::is_config_file(const utils::resource::resource_location& file) const { - return file == m_proc_grps_loc || file == m_pgm_conf_loc; + return !file.empty() && (file == m_proc_grps_loc || file == m_pgm_conf_loc); } bool workspace_configuration::is_b4g_config_file(const utils::resource::resource_location& file) const @@ -484,6 +499,29 @@ lib_config load_from_pgm_config(const config::pgm_conf& config) return loaded; } +[[nodiscard]] utils::value_task> load_json_from_file( + file_manager& fm, const utils::resource::resource_location& file) +{ + auto text = co_await fm.get_file_content(file); + if (!text.has_value()) + co_return parse_config_file_result::not_found; + + try + { + co_return nlohmann::json::parse(text.value()); + } + catch (const nlohmann::json::exception&) + { + co_return parse_config_file_result::error; + } +} + +template +bool equals(const std::variant& v, const U& u) +{ + return std::holds_alternative(v) && std::get(v) == u; +} + // open config files and parse them utils::value_task workspace_configuration::load_and_process_config( std::vector& diags) @@ -491,18 +529,24 @@ utils::value_task workspace_configuration::load_and_pr diags.clear(); config::proc_grps proc_groups; + config::pgm_conf pgm_config; global_settings_map utilized_settings_values; m_proc_grps.clear(); m_pgm_conf_store->clear(); m_b4g_config_cache.clear(); - if (auto l = co_await load_proc_config(proc_groups, utilized_settings_values, diags); - l != parse_config_file_result::parsed) - co_return l; + auto [proc_gprs_result, proc_src] = co_await load_proc_config(proc_groups, utilized_settings_values, diags); - config::pgm_conf pgm_config; - const auto pgm_conf_loaded = co_await load_pgm_config(pgm_config, utilized_settings_values, diags); + auto [pgm_conf_loaded, pgm_src] = co_await load_pgm_config(pgm_config, utilized_settings_values, diags); + + m_proc_grps_current_loc = std::move(proc_src); + m_pgm_conf_current_loc = std::move(pgm_src); + + m_utilized_settings_values = std::move(utilized_settings_values); + + if (proc_gprs_result != parse_config_file_result::parsed) + co_return proc_gprs_result; co_await process_processor_group_and_cleanup_libraries( proc_groups.pgroups, proc_groups.macro_extensions, empty_alternative_cfg_root, diags); @@ -520,87 +564,136 @@ utils::value_task workspace_configuration::load_and_pr process_program(pgm, diags); } - m_utilized_settings_values = std::move(utilized_settings_values); m_proc_grps_source = std::move(proc_groups); // we need to tolerate pgm_conf processing failure, because other products may provide the info co_return parse_config_file_result::parsed; } -utils::value_task workspace_configuration::load_proc_config( +utils::value_task> +workspace_configuration::load_proc_config( config::proc_grps& proc_groups, global_settings_map& utilized_settings_values, std::vector& diags) { const auto current_settings = m_global_settings.load(); json_settings_replacer json_visitor { *current_settings, utilized_settings_values, m_location }; - // proc_grps.json parse - auto proc_grps_content = co_await m_file_manager.get_file_content(m_proc_grps_loc); - if (!proc_grps_content.has_value()) - co_return parse_config_file_result::not_found; + auto config_source = m_proc_grps_loc; + + std::variant proc_json_or_err = parse_config_file_result::not_found; + if (!m_proc_grps_loc.empty()) + proc_json_or_err = co_await load_json_from_file(m_file_manager, m_proc_grps_loc); + + if (equals(proc_json_or_err, parse_config_file_result::not_found)) + { + static const std::string key = "hlasm.proc_grps"; + const auto* proc_conf = find_subobject(key, *current_settings); + if (proc_conf && proc_conf->is_object()) + { + proc_json_or_err = *proc_conf; + config_source = m_location; + } + else if (proc_conf && !proc_conf->is_null()) + { + proc_json_or_err = parse_config_file_result::error; + } + json_visitor.update_utilized_settings(key, proc_conf); + } + if (std::holds_alternative(proc_json_or_err)) + { + auto reason = std::get(proc_json_or_err); + if (reason == parse_config_file_result::error) + diags.push_back(error_W0002(config_source)); + co_return { reason, config_source }; + } try { - auto proc_json = nlohmann::json::parse(proc_grps_content.value()); + auto& proc_json = std::get(proc_json_or_err); json_visitor(proc_json); proc_json.get_to(proc_groups); } catch (const nlohmann::json::exception&) { // could not load proc_grps - diags.push_back(error_W0002(m_proc_grps_loc)); - co_return parse_config_file_result::error; + diags.push_back(error_W0002(config_source)); + co_return { parse_config_file_result::error, config_source }; } for (const auto& var : json_visitor.unavailable) - diags.push_back(warn_W0007(m_proc_grps_loc, var)); + diags.push_back(warn_W0007(config_source, var)); for (const auto& pg : proc_groups.pgroups) { if (!pg.asm_options.valid()) - diags.push_back(error_W0005(m_proc_grps_loc, pg.name, "processor group")); + diags.push_back(error_W0005(config_source, pg.name, "processor group")); for (const auto& p : pg.preprocessors) { if (!p.valid()) - diags.push_back(error_W0006(m_proc_grps_loc, pg.name, p.type())); + diags.push_back(error_W0006(config_source, pg.name, p.type())); } } - co_return parse_config_file_result::parsed; + co_return { parse_config_file_result::parsed, config_source }; } -utils::value_task workspace_configuration::load_pgm_config( +utils::value_task> +workspace_configuration::load_pgm_config( config::pgm_conf& pgm_config, global_settings_map& utilized_settings_values, std::vector& diags) { const auto current_settings = m_global_settings.load(); json_settings_replacer json_visitor { *current_settings, utilized_settings_values, m_location }; - // pgm_conf.json parse - auto pgm_conf_content = co_await m_file_manager.get_file_content(m_pgm_conf_loc); - if (!pgm_conf_content.has_value()) - co_return parse_config_file_result::not_found; + auto config_source = m_pgm_conf_loc; + + std::variant pgm_json_or_err = parse_config_file_result::not_found; + if (!m_pgm_conf_loc.empty()) + pgm_json_or_err = co_await load_json_from_file(m_file_manager, m_pgm_conf_loc); + + if (equals(pgm_json_or_err, parse_config_file_result::not_found)) + { + static const std::string key = "hlasm.pgm_conf"; + const auto* pgm_conf = find_subobject(key, *current_settings); + if (pgm_conf && pgm_conf->is_object()) + { + pgm_json_or_err = *pgm_conf; + config_source = m_location; + } + else if (pgm_conf && !pgm_conf->is_null()) + { + pgm_json_or_err = parse_config_file_result::error; + } + json_visitor.update_utilized_settings(key, pgm_conf); + } + if (std::holds_alternative(pgm_json_or_err)) + { + auto reason = std::get(pgm_json_or_err); + if (reason == parse_config_file_result::error) + diags.push_back(error_W0003(config_source)); + co_return { reason, config_source }; + } try { - auto pgm_json = nlohmann::json::parse(pgm_conf_content.value()); + auto& pgm_json = std::get(pgm_json_or_err); json_visitor(pgm_json); pgm_json.get_to(pgm_config); } catch (const nlohmann::json::exception&) { - diags.push_back(error_W0003(m_pgm_conf_loc)); - co_return parse_config_file_result::error; + diags.push_back(error_W0003(config_source)); + co_return { parse_config_file_result::error, config_source }; } for (const auto& var : json_visitor.unavailable) - diags.push_back(warn_W0007(m_pgm_conf_loc, var)); + diags.push_back(warn_W0007(config_source, var)); for (const auto& pgm : pgm_config.pgms) { if (!pgm.opts.valid()) - diags.push_back(error_W0005(m_pgm_conf_loc, pgm.program, "program")); + diags.push_back(error_W0005(config_source, pgm.program, "program")); } - co_return parse_config_file_result::parsed; + co_return { parse_config_file_result::parsed, config_source }; } bool workspace_configuration::settings_updated() const @@ -608,8 +701,14 @@ bool workspace_configuration::settings_updated() const auto global_settings = m_global_settings.load(); for (const auto& [key, value] : m_utilized_settings_values) { - if (find_setting(key, *global_settings) != value) + const auto* obj = find_subobject(key, *global_settings); + if (obj == nullptr && !value.has_value()) + continue; + if (!obj) return true; + if (*obj == value) + continue; + return true; } return false; } @@ -633,15 +732,16 @@ utils::value_task workspace_configuration::parse_b4g_c it->second = {}; } - auto b4g_config_content = co_await m_file_manager.get_file_content(cfg_file_rl); - if (!b4g_config_content.has_value()) - co_return parse_config_file_result::not_found; + auto b4g_config_or_err = co_await load_json_from_file(m_file_manager, cfg_file_rl); + if (std::holds_alternative(b4g_config_or_err)) + co_return std::get(b4g_config_or_err); const void* new_tag = std::to_address(it); auto& conf = it->second; try { - conf.config.emplace(nlohmann::json::parse(b4g_config_content.value()).get()); + auto& b4g_config_json = std::get(b4g_config_or_err); + conf.config.emplace(b4g_config_json.get()); } catch (const nlohmann::json::exception&) { @@ -697,7 +797,7 @@ utils::task workspace_configuration::find_and_add_libs(utils::resource::resource if (std::error_code ec; dirs_to_search.emplace_back(m_file_manager.canonical(root, ec), root), ec) { if (!opts.optional_library) - diags.push_back(error_L0001(m_proc_grps_loc, root)); + diags.push_back(error_L0001(m_proc_grps_current_loc, root)); co_return; } @@ -707,7 +807,7 @@ utils::task workspace_configuration::find_and_add_libs(utils::resource::resource const auto first = std::exchange(first_, false); if (processed_canonical_paths.size() > limit) { - diags.push_back(warning_L0005(m_proc_grps_loc, path_pattern.to_presentable(), limit)); + diags.push_back(warning_L0005(m_proc_grps_current_loc, path_pattern.to_presentable(), limit)); break; } @@ -724,7 +824,7 @@ utils::task workspace_configuration::find_and_add_libs(utils::resource::resource if (return_code != utils::path::list_directory_rc::done) { if (!first || !opts.optional_library || return_code != utils::path::list_directory_rc::not_exists) - diags.push_back(error_L0001(m_proc_grps_loc, dir)); + diags.push_back(error_L0001(m_proc_grps_current_loc, dir)); break; } @@ -749,7 +849,7 @@ void workspace_configuration::add_missing_diags(const diagnosable& target, }; bool empty_cfg_rl = config_file_rl.empty(); - const auto& adjusted_conf_rl = empty_cfg_rl ? m_pgm_conf_loc : config_file_rl; + const auto& adjusted_conf_rl = empty_cfg_rl ? m_pgm_conf_current_loc : config_file_rl; for (const auto& categorized_missing_pgroups = m_pgm_conf_store->get_categorized_missing_pgroups(config_file_rl, opened_files); @@ -794,14 +894,11 @@ void workspace_configuration::produce_diagnostics( utils::value_task workspace_configuration::parse_configuration_file( std::optional file) { - if (!m_location.empty()) - { - if (!file.has_value() || is_config_file(*file)) - return load_and_process_config(m_config_diags); + if (!file.has_value() || is_config_file(*file)) + return load_and_process_config(m_config_diags); - if (is_b4g_config_file(*file)) - return parse_b4g_config_file(std::move(*file)); - } + if (!m_location.empty() && is_b4g_config_file(*file)) + return parse_b4g_config_file(std::move(*file)); return utils::value_task::from_value(parse_config_file_result::not_found); } diff --git a/parser_library/src/workspaces/workspace_configuration.h b/parser_library/src/workspaces/workspace_configuration.h index ed335d432..34702f694 100644 --- a/parser_library/src/workspaces/workspace_configuration.h +++ b/parser_library/src/workspaces/workspace_configuration.h @@ -44,7 +44,7 @@ class external_configuration_requests; } // namespace hlasm_plugin::parser_library namespace hlasm_plugin::parser_library::workspaces { using global_settings_map = - std::unordered_map, utils::hashers::string_hasher, std::equal_to<>>; + std::unordered_map, utils::hashers::string_hasher, std::equal_to<>>; class file_manager; class program_configuration_storage; @@ -182,6 +182,9 @@ class workspace_configuration utils::resource::resource_location m_proc_grps_loc; utils::resource::resource_location m_pgm_conf_loc; + utils::resource::resource_location m_proc_grps_current_loc; + utils::resource::resource_location m_pgm_conf_current_loc; + config::proc_grps m_proc_grps_source; proc_groups_map m_proc_grps; @@ -253,9 +256,11 @@ class workspace_configuration [[nodiscard]] utils::value_task load_and_process_config(std::vector& diags); - [[nodiscard]] utils::value_task load_proc_config( + [[nodiscard]] utils::value_task> + load_proc_config( config::proc_grps& proc_groups, global_settings_map& utilized_settings_values, std::vector& diags); - [[nodiscard]] utils::value_task load_pgm_config( + [[nodiscard]] utils::value_task> + load_pgm_config( config::pgm_conf& pgm_config, global_settings_map& utilized_settings_values, std::vector& diags); [[nodiscard]] utils::task find_and_add_libs(utils::resource::resource_location root, diff --git a/parser_library/test/workspace_manager_test.cpp b/parser_library/test/workspace_manager_test.cpp index 7ddbec4b4..d28c853d4 100644 --- a/parser_library/test/workspace_manager_test.cpp +++ b/parser_library/test/workspace_manager_test.cpp @@ -166,3 +166,34 @@ TEST(workspace_manager, extended_scheme_allowed_list) EXPECT_FALSE(diags.diags.empty()); // not suppressed EXPECT_TRUE(called); } + +TEST(workspace_manager, implicit_configuration) +{ + const std::string_view uri = "untitled:file1"; + NiceMock ext_mock; + diag_consumer_mock diags; + + auto ws_mngr = create_workspace_manager(&ext_mock, true); + ws_mngr->register_diagnostics_consumer(&diags); + ws_mngr->add_workspace("dir", "test:/dir"); + ws_mngr->configuration_changed({}, + R"({"hlasm":{"proc_grps":{"pgroups":[{"name":"P1","libs":["test:/dir/macs/"]}]},"pgm_conf":{"pgms":[{"program":"**","pgroup":"P1"}]}}})"); + + EXPECT_CALL(ext_mock, read_external_file).WillRepeatedly(Invoke([](auto, auto r) { r.error(-1, ""); })); + EXPECT_CALL(ext_mock, read_external_directory(StrEq("test:/dir/macs/"), _, _)) + .WillOnce(Invoke([](auto, auto r, auto) { + static constexpr std::string_view resp[] = { "test:/dir/macs/MAC" }; + r.provide(workspace_manager_external_directory_result { .member_urls = resp }); + })); + + ws_mngr->did_open_file("test:/dir/macs/MAC", 1, R"( MACRO + MAC + MNOTE 'Hello' + MEND +)"); + ws_mngr->did_open_file(uri, 1, " MAC"); + + ws_mngr->idle_handler(); + + EXPECT_TRUE(matches_message_text(diags.diags, { "Hello" })); +}