diff --git a/package-lock.json b/package-lock.json index 38c7657b..05ef483f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -171,7 +171,7 @@ }, "@types/yauzl": { "version": "2.9.1", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", "dev": true, "requires": { @@ -403,7 +403,7 @@ }, "ajv-keywords": { "version": "3.5.2", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, @@ -869,7 +869,7 @@ }, "buffer-crc32": { "version": "0.2.13", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, "buffer-equal": { @@ -1720,13 +1720,13 @@ }, "fast-deep-equal": { "version": "3.1.3", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, @@ -1744,7 +1744,7 @@ }, "fd-slicer": { "version": "1.1.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "requires": { "pend": "~1.2.0" @@ -2975,7 +2975,7 @@ }, "mkdirp": { "version": "0.5.5", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { @@ -3042,7 +3042,7 @@ }, "neo-async": { "version": "2.6.2", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, @@ -3377,7 +3377,7 @@ }, "pend": { "version": "1.2.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" }, "picomatch": { @@ -3956,7 +3956,7 @@ }, "source-map-support": { "version": "0.5.19", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { @@ -4980,7 +4980,7 @@ }, "yauzl": { "version": "2.10.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "requires": { "buffer-crc32": "~0.2.3", diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 4653ff50..525a4a1b 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -1,9 +1,10 @@ import * as path from 'path'; -import { commands, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, Uri, window, workspace, WorkspaceEdit, Disposable } from "vscode"; +import { commands, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, Uri, window, workspace, WorkspaceEdit, Disposable, ConfigurationTarget, TextDocument, WorkspaceFolder } from "vscode"; import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentIdentifier, TextDocumentEdit } from "vscode-languageclient"; import { LanguageClient } from 'vscode-languageclient/node'; import { markdownPreviewProvider } from "../markdownPreviewProvider"; import { ClientCommandConstants, ServerCommandConstants } from "./commandConstants"; +import { getRelativePath, getFileName, getDirectoryPath, getWorkspaceUri } from '../utils/fileUtils'; /** * Register the commands for vscode-xml that don't require communication with the language server @@ -143,7 +144,8 @@ function registerValidationCommands(context: ExtensionContext) { export const bindingTypes = new Map([ ["Standard (xsi, DOCTYPE)", "standard"], - ["XML Model association", "xml-model"] + ["XML Model association", "xml-model"], + ["File association", "fileAssociation"] ]); const bindingTypeOptions: QuickPickItem[] = []; @@ -154,13 +156,13 @@ for (const label of bindingTypes.keys()) { /** * The function passed to context subscriptions for grammar association * - * @param uri the uri of the XML file path + * @param documentURI the uri of the XML file path * @param languageClient the language server client */ async function grammarAssociationCommand(documentURI: Uri, languageClient: LanguageClient) { // A click on Bind to grammar/schema... has been processed in the XML document which is not bound to a grammar - // Step 1 : open a combo to select the binding type ("standard", "xml-model") + // Step 1 : open a combo to select the binding type ("standard", "xml-model", "fileAssociation") const pickedBindingTypeOption = await window.showQuickPick(bindingTypeOptions, { placeHolder: "Binding type" }); if (!pickedBindingTypeOption) { return; @@ -179,24 +181,90 @@ async function grammarAssociationCommand(documentURI: Uri, languageClient: Langu const fileUri = await window.showOpenDialog(options); if (fileUri && fileUri[0]) { // The XSD, DTD has been selected, get the proper syntax for binding this grammar file in the XML document. - const identifier = TextDocumentIdentifier.create(documentURI.toString()); const grammarURI = fileUri[0]; try { - const result = await commands.executeCommand(ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT, identifier, grammarURI.toString(), bindingType); - // Insert the proper syntax for binding - const lspTextDocumentEdit = result; - const workEdits = new WorkspaceEdit(); - for (const edit of lspTextDocumentEdit.edits) { - workEdits.replace(documentURI, languageClient.protocol2CodeConverter.asRange(edit.range), edit.newText); + const currentFile = (window.activeTextEditor && window.activeTextEditor.document && window.activeTextEditor.document.languageId === 'xml') ? window.activeTextEditor.document : undefined; + if (bindingType == 'fileAssociation') { + // Bind grammar using file association + await bindWithFileAssociation(documentURI, grammarURI, currentFile); + } else { + // Bind grammar using standard binding + await bindWithStandard(documentURI, grammarURI, bindingType, languageClient); } - workspace.applyEdit(workEdits); // apply the edits - } catch (error) { window.showErrorMessage('Error during grammar binding: ' + error.message); }; } } +/** + * Perform grammar binding using file association through settings.json + * + * @param documentURI the URI of the current XML document + * @param grammarURI the URI of the user selected grammar file + * @param document the opened TextDocument + */ +async function bindWithFileAssociation(documentURI: Uri, grammarURI: Uri, document: TextDocument) { + const absoluteGrammarFilePath = grammarURI.toString(); + const currentFilename = getFileName(documentURI.toString()); + const currentWorkspaceUri = getWorkspaceUri(document); + // If the grammar file is in the same workspace, use the relative path, otherwise use the absolute path + const grammarFilePath = getDirectoryPath(absoluteGrammarFilePath).includes(currentWorkspaceUri.toString()) ? getRelativePath(currentWorkspaceUri.toString(), absoluteGrammarFilePath) : absoluteGrammarFilePath; + + const defaultPattern = `**/${currentFilename}`; + const inputBoxOptions = { + title: "File Association Pattern", + value: defaultPattern, + placeHolder: defaultPattern, + prompt: "Enter the pattern of the XML document(s) to be bound." + } + const inputPattern = (await window.showInputBox(inputBoxOptions)); + if (!inputPattern) { + // User closed the input box with Esc + return; + } + const fileAssociation = { + "pattern": inputPattern, + "systemId": grammarFilePath + } + addToValueToSettingArray("xml.fileAssociations", fileAssociation); +} + +/** + * Bind grammar file using standard XML document grammar + * + * @param documentURI the URI of the XML file path + * @param grammarURI the URI of the user selected grammar file + * @param bindingType the selected grammar binding type + * @param languageClient the language server client + */ +async function bindWithStandard(documentURI: Uri, grammarURI: Uri, bindingType: string, languageClient: LanguageClient) { + const identifier = TextDocumentIdentifier.create(documentURI.toString()); + const result = await commands.executeCommand(ServerCommandConstants.ASSOCIATE_GRAMMAR_INSERT, identifier, grammarURI.toString(), bindingType); + // Insert the proper syntax for binding + const lspTextDocumentEdit = result; + const workEdits = new WorkspaceEdit(); + for (const edit of lspTextDocumentEdit.edits) { + workEdits.replace(documentURI, languageClient.protocol2CodeConverter.asRange(edit.range), edit.newText); + } + workspace.applyEdit(workEdits); // apply the edits +} + +/** + * Add an entry, value, to the setting.json field, key + * + * @param key the filename/path of the xml document + * @param value the object to add to the config + */ +function addToValueToSettingArray(key: string, value: T): void { + const settingArray: T[] = workspace.getConfiguration().get(key, []); + if (settingArray.includes(value)) { + return; + } + settingArray.push(value); + workspace.getConfiguration().update(key, settingArray, ConfigurationTarget.Workspace); +} + /** * Register commands used for associating grammar file (XSD,DTD) to a given XML file for command menu and CodeLens * diff --git a/src/settings/variableSubstitution.ts b/src/settings/variableSubstitution.ts index 9c1015e6..3668cf55 100644 --- a/src/settings/variableSubstitution.ts +++ b/src/settings/variableSubstitution.ts @@ -1,5 +1,6 @@ import * as path from "path"; -import { TextDocument, window, workspace, WorkspaceFolder } from "vscode"; +import { window } from "vscode"; +import { getFilePath, getWorkspaceUri } from "../utils/fileUtils"; import { XMLFileAssociation } from "../api/xmlExtensionApi"; /** @@ -99,11 +100,9 @@ const VARIABLE_SUBSTITUTIONS: VariableSubstitution[] = [ export function getVariableSubstitutedAssociations(associations: XMLFileAssociation[]): XMLFileAssociation[] { // Collect properties needed to resolve variables - const currentFile: TextDocument = (window.activeTextEditor && window.activeTextEditor.document && window.activeTextEditor.document.languageId === 'xml') ? window.activeTextEditor.document : undefined; - const currentFileUri: string = (currentFile && currentFile.uri) ? currentFile.uri.fsPath : undefined; - const currentWorkspace: WorkspaceFolder = (currentFile && currentFile.uri) ? workspace.getWorkspaceFolder(currentFile.uri) : undefined; - const currentWorkspaceUri: string = (currentWorkspace && currentWorkspace.uri.fsPath) - || (workspace.workspaceFolders && workspace.workspaceFolders[0].uri.fsPath); + const currentFile = (window.activeTextEditor && window.activeTextEditor.document && window.activeTextEditor.document.languageId === 'xml') ? window.activeTextEditor.document : undefined; + const currentFileUri = getFilePath(currentFile); + const currentWorkspaceUri = getWorkspaceUri(currentFile); // Remove variables that can't be resolved let variablesToSubstitute = VARIABLE_SUBSTITUTIONS; @@ -123,7 +122,7 @@ export function getVariableSubstitutedAssociations(associations: XMLFileAssociat const subVars = (val: string): string => { let newVal = val; for (const settingVariable of variablesToSubstitute) { - newVal = settingVariable.substituteString(newVal, currentFileUri, currentWorkspaceUri); + newVal = settingVariable.substituteString(newVal, currentFileUri, currentWorkspaceUri.toString()); } return newVal; } diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts new file mode 100644 index 00000000..c7132cdc --- /dev/null +++ b/src/utils/fileUtils.ts @@ -0,0 +1,55 @@ +import * as path from "path"; +import { TextDocument, Uri, workspace, WorkspaceFolder } from "vscode"; + +/** + * Return the current workspace uri from root + * + * @param document the opened TextDocument + * @returns the path from root of the current workspace + */ + export function getWorkspaceUri(document: TextDocument): Uri | undefined { + const currentWorkspace: WorkspaceFolder = (document && document.uri) ? workspace.getWorkspaceFolder(document.uri) : undefined; + return ((currentWorkspace && currentWorkspace.uri) || (workspace.workspaceFolders && workspace.workspaceFolders[0].uri)); +} + +/** + * Return the uri of the current file + * + * @param document the opened TextDocument + * @returns the uri of the current file + */ +export function getFilePath(document: TextDocument): string { + return (document && document.uri) ? document.uri.fsPath : undefined; +} + +/** + * Uses path to return a basename from a uri + * + * @param filePath the uri of the file + * @return the filename + */ +export function getFileName(filePath: string): string { + return path.basename(filePath); +} + +/** + * Return the relative file path between a start and destination uri + * + * @param startPath the absolute path of the beginning directory + * @param destinationPath the absolute path of destination file + * @returns the path to the destination relative to the start + */ +export function getRelativePath(startPath: string, destinationPath: string): string { + return path.relative(path.normalize(startPath), path.normalize(destinationPath)).replace(/\\/g, '/'); +} + +/** + * Uses path to return the directory name from a uri + * + * @param filePath the uri of the file + * @return the directory path + */ +export function getDirectoryPath(filePath: string): string { + return path.dirname(filePath); + +} \ No newline at end of file