diff --git a/docs/Preferences.md b/docs/Preferences.md index 971b3ba4..e08c0501 100644 --- a/docs/Preferences.md +++ b/docs/Preferences.md @@ -78,7 +78,7 @@ Can be accessed through `xml.fileAssociations`. - Provides ability to associate a schema with a file pattern. + Provides ability to associate a schema with a file pattern. Both `pattern` and `systemId` support [variable substitution](Validation.md#file-variables) (e.g. `${workspaceFolder}`, `${fileDirname}`, `${env:Name}`). Example: ```json @@ -88,8 +88,8 @@ "pattern": "file1.xml" }, { - "systemId": "http://www.w3.org/2001/XMLSchema.xsd", - "pattern": "**/*.xsd" + "systemId": "${workspaceFolder}/schemas/config.xsd", + "pattern": "${workspaceFolder}/config/**/*.xml" } ] ``` diff --git a/docs/Validation.md b/docs/Validation.md index 6ffaf8b4..88146dd7 100644 --- a/docs/Validation.md +++ b/docs/Validation.md @@ -215,17 +215,72 @@ Please note that you can use wildcards in the pattern (ex: `foo*.xml`): In this case, all XML files that start with foo and end with .xml will be associated with the XSD (foo1.xml, foo2.xml, etc) -You can also use the following three variables in either the `pattern` or `systemId`: +You can also use variables in either the `pattern` or `systemId`. Variables are resolved per document at match time. - | Variable | Meaning | - | --------------------------- | ------------------------------------------------------------------------ | - | ${workspaceFolder} | The absolute path to root folder of the workspace that is currently open | - | ${fileDirname} | The absolute path to the folder of the file that is currently opened | - | ${fileBasenameNoExtension} | The current opened file's basename with no file extension | +#### File variables -If one of the variables for an association can't be expanded (eg. because vscode is opened in rootless mode), -the association is ignored. -This feature is specific to the VSCode client. + | Variable | Description | Example (`/home/user/project/src/data/test.xml`) | + | --------------------------- | ---------------------------------------------------- | ------------------------------------------------ | + | `${file}` | Full path of the document | `/home/user/project/src/data/test.xml` | + | `${fileDirname}` | Directory of the document | `/home/user/project/src/data` | + | `${fileBasename}` | File name with extension | `test.xml` | + | `${fileBasenameNoExtension}`| File name without extension | `test` | + | `${fileExtname}` | File extension (with dot) | `.xml` | + +#### Workspace variables + + | Variable | Description | Example (workspace: `/home/user/project`) | + | --------------------------- | ---------------------------------------------------- | ------------------------------------------------ | + | `${workspaceFolder}` | Workspace folder containing the document | `/home/user/project` | + | `${workspaceFolderBasename}`| Name of the workspace folder | `project` | + | `${fileWorkspaceFolder}` | Same as `${workspaceFolder}` | `/home/user/project` | + | `${relativeFile}` | Document path relative to workspace folder | `src/data/test.xml` | + | `${relativeFileDirname}` | Directory of the relative path | `src/data` | + +In a multi-root workspace, the deepest workspace folder that contains the document is used. If no workspace folder contains the document, the first workspace folder is used as fallback. + +#### Environment variables + + | Variable | Description | + | --------------------------- | ---------------------------------------------------- | + | `${env:Name}` | Value of environment variable `Name` | + +If an environment variable is not set, the variable reference is left unchanged. + +#### Examples with variables + +Associate all XML files under a workspace `data/` folder with a schema next to each file: + +```json +"xml.fileAssociations": [ + { + "systemId": "${fileDirname}/schema.xsd", + "pattern": "${workspaceFolder}/data/**/*.xml" + } +] +``` + +Use an environment variable to point to a shared schema directory: + +```json +"xml.fileAssociations": [ + { + "systemId": "${env:SCHEMA_HOME}/myschema.xsd", + "pattern": "**/*.myxml" + } +] +``` + +Use a schema stored inside the workspace: + +```json +"xml.fileAssociations": [ + { + "systemId": "${workspaceFolder}/schemas/config.xsd", + "pattern": "${workspaceFolder}/config/**/*.xml" + } +] +``` Please note that if the XSD defines a `targetNamespace="http://foo"` like the following `foo.xsd` XSD file: @@ -387,17 +442,7 @@ Please note that you can use wildcards in the pattern (ex: `foo*.xml`): In this case, all XML files that start with foo and end with .xml will be associated with the DTD (foo1.xml, foo2.xml, etc) -You can also use the following three variables in either the `pattern` or `systemId`: - - | Variable | Meaning | - | --------------------------- | ------------------------------------------------------------------------ | - | ${workspaceFolder} | The absolute path to root folder of the workspace that is currently open | - | ${fileDirname} | The absolute path to the folder of the file that is currently opened | - | ${fileBasenameNoExtension} | The current opened file's basename with no file extension | - -If one of the variables for an association can't be expanded (eg. because vscode is opened in rootless mode), -the association is ignored. -This feature is specific to the VSCode client. +You can also use variables in either the `pattern` or `systemId`. See [variable substitution in XSD file associations](#file-variables) for the full list of supported variables and examples. ## Validation with RelaxNG grammar diff --git a/src/client/xmlClient.ts b/src/client/xmlClient.ts index 16b95bf4..88a514cd 100644 --- a/src/client/xmlClient.ts +++ b/src/client/xmlClient.ts @@ -2,14 +2,12 @@ import { TelemetryEvent } from '@redhat-developer/vscode-redhat-telemetry/lib'; import { commands, ExtensionContext, extensions, Position, TextDocument, TextEditor, Uri, window, workspace } from 'vscode'; import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, DocumentFilter, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, State } from "vscode-languageclient"; import { Executable, LanguageClient } from 'vscode-languageclient/node'; -import { XMLFileAssociation } from '../api/xmlExtensionApi'; import { registerClientServerCommands } from '../commands/registerCommands'; import * as ServerCommandConstants from '../commands/serverCommandConstants'; import { onExtensionChange } from '../plugin'; import { RequirementsData } from "../server/requirements"; import { ExternalXmlSettings } from "../settings/externalXmlSettings"; import { getXMLConfiguration, getXMLSettings, onConfigurationChange, subscribeJDKChangeConfiguration } from "../settings/settings"; -import { containsVariableReferenceToCurrentFile } from '../settings/variableSubstitution'; import * as Telemetry from '../telemetry'; import { ClientErrorHandler } from './clientErrorHandler'; import { getLanguageParticipants } from './languageParticipants'; @@ -135,15 +133,6 @@ export async function startLanguageClient(context: ExtensionContext, executable: return result; }); - // When the current document changes, update variable values that refer to the current file if these variables are referenced, - // and send the updated settings to the server - context.subscriptions.push(window.onDidChangeActiveTextEditor(() => { - if (containsVariableReferenceToCurrentFile(getXMLConfiguration().get('fileAssociations') as XMLFileAssociation[])) { - languageClient.sendNotification(DidChangeConfigurationNotification.type, { settings: getXMLSettings(requirementsData.java_home, logfile, externalXmlSettings) }); - onConfigurationChange(); - } - })); - const onDidGrantWorkspaceTrust = (workspace as any).onDidGrantWorkspaceTrust; if (onDidGrantWorkspaceTrust !== undefined) { context.subscriptions.push(onDidGrantWorkspaceTrust(() => { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 17af2600..c3405620 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -1,6 +1,5 @@ import * as path from 'path'; import { commands, Extension, extensions, window, workspace, WorkspaceConfiguration } from "vscode"; -import { getVariableSubstitutedAssociations } from "./variableSubstitution"; export interface ScopeInfo { scope: "default" | "global" | "workspace" | "folder"; @@ -248,8 +247,5 @@ export function getXMLSettings(javaHome: string | undefined, logfile: string, ex } }); - // Apply variable substitutions for file associations - xml['xml']['fileAssociations'] = [...getVariableSubstitutedAssociations(xml['xml']['fileAssociations'])]; - return xml; } \ No newline at end of file diff --git a/src/settings/variableSubstitution.ts b/src/settings/variableSubstitution.ts deleted file mode 100644 index 0e86518a..00000000 --- a/src/settings/variableSubstitution.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as path from "path"; -import { window } from "vscode"; -import { getFilePath, getWorkspaceUri } from "../utils/fileUtils"; -import { XMLFileAssociation } from "../api/xmlExtensionApi"; - -/** - * Represents a variable that refers to a value that can be resolved - */ -class VariableSubstitution { - - private varName: string; - private varKind: VariableSubstitutionKind; - private getValue: (currentFileUri: string, currentWorkspaceUri: string) => string; - private replaceRegExp: RegExp | undefined; - - /** - * - * @param name the name of this variable - * @param kind the kind of this variable - * @param getValue a function that resolves the value of the variable - */ - constructor(name: string, kind: VariableSubstitutionKind, getValue: (currentFileUri: string, currentWorkspaceUri: string) => string) { - this.varName = name; - this.varKind = kind; - this.getValue = getValue; - } - - public get name(): string { - return this.varName; - } - - public get kind(): VariableSubstitutionKind { - return this.varKind; - } - - /** - * Returns the string with the references to this variable replaced with the value, or the original string if the value cannot be resolved - * - * @param original the string to substitute variable value - * @param currentFileUri the uri of the currently focused file, as a string - * @param currentWorkspaceUri the uri of the root of the currently open workspace, as a string - * @returns the string with the references to this variable replaced with the value, or the original string if the value cannot be resolved - */ - public substituteString(original: string, currentFileUri: string, currentWorkspaceUri: string): string { - const value: string = this.getValue(currentFileUri, currentWorkspaceUri); - return value ? original.replace(this.getReplaceRegExp(), value) : original; - } - - /** - * Returns a regex that matches all references to the variable - * - * Lazily initialized - * - * @returns a regex that matches all references to the variable - */ - public getReplaceRegExp(): RegExp { - if (!this.replaceRegExp) { - this.replaceRegExp = new RegExp('\\$\\{' + `${this.name}` + '\\}', 'g'); - } - return this.replaceRegExp; - } -} - -enum VariableSubstitutionKind { - Workspace, - File -} - -// A list of all variable substitutions. To add a new variable substitution, add an entry to this list -const VARIABLE_SUBSTITUTIONS: VariableSubstitution[] = [ - new VariableSubstitution( - "workspaceFolder", - VariableSubstitutionKind.Workspace, - (currentFileUri: string, currentWorkspaceUri: string): string => { - return currentWorkspaceUri; - } - ), - new VariableSubstitution( - "fileDirname", - VariableSubstitutionKind.File, - (currentFileUri: string, _currentWorkspaceUri: string): string => { - return path.dirname(currentFileUri); - } - ), - new VariableSubstitution( - "fileBasenameNoExtension", - VariableSubstitutionKind.File, - (currentFileUri: string, _currentWorkspaceUri: string): string => { - return path.basename(currentFileUri, path.extname(currentFileUri)); - } - ) -]; - -/** - * Returns the file associations with as many variable references resolved as possible - * - * @param associations the file associations to resolve the variable references in - * @returns the file associations with as many variable references resolved as possible - */ -export function getVariableSubstitutedAssociations(associations: XMLFileAssociation[]): XMLFileAssociation[] { - - // Collect properties needed to resolve variables - 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; - if (!currentWorkspaceUri) { - variablesToSubstitute = variablesToSubstitute.filter(variable => { return variable.kind !== VariableSubstitutionKind.Workspace }); - } - if (!currentFileUri) { - variablesToSubstitute = variablesToSubstitute.filter(variable => { return variable.kind !== VariableSubstitutionKind.File }); - } - - /** - * Returns the string with the values for all the variables that can be resolved substituted in the string - * - * @param val the value to substitute the variables into - * @return the string with the values for all the variables that can be resolved substituted in the string - */ - const subVars = (val: string): string => { - let newVal = val; - for (const settingVariable of variablesToSubstitute) { - newVal = settingVariable.substituteString(newVal, currentFileUri, currentWorkspaceUri?.toString()); - } - return newVal; - } - - return associations.map((association: XMLFileAssociation) => { - return { - pattern: subVars(association.pattern), - systemId: subVars(association.systemId) - }; - }); -} - -/** - * Returns true if any of the file associations contain references to variables that refer to current file, and false otherwise - * - * @param associations A list of file associations to check - * @returns true if any of the file associations contain references to variables that refer to current file, and false otherwise - */ -export function containsVariableReferenceToCurrentFile(associations: XMLFileAssociation[]): boolean { - const fileVariables: VariableSubstitution[] = VARIABLE_SUBSTITUTIONS.filter(variable => variable.kind === VariableSubstitutionKind.File); - for (const association of associations) { - for (const variable of fileVariables) { - if (variable.getReplaceRegExp().test(association.pattern) || variable.getReplaceRegExp().test(association.systemId)) { - return true; - } - } - } - return false; -}