Skip to content

Commit

Permalink
Add variables for xml.fileAssociations
Browse files Browse the repository at this point in the history
Adds three variables that can be used in `xml.fileAssociations`:
 * ${workspaceFolder}
 * ${fileDirname}
 * ${fileBasenameNoExtension}

These variables can be used for both the `pattern` and the `systemId`.

Closes #307

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Oct 28, 2020
1 parent 89579ca commit d9582c6
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 28 deletions.
22 changes: 22 additions & 0 deletions docs/Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ 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`:

| 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.

## Validation with DTD grammar

To associate your XML with a DTD grammar you can use several strategies:
Expand Down Expand Up @@ -325,7 +337,17 @@ 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.

# Other Validation Settings

Expand Down
47 changes: 19 additions & 28 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,18 @@
* Microsoft Corporation - Auto Closing Tags
*/

import { prepareExecutable } from './javaServerStarter';
import {
LanguageClientOptions,
RevealOutputChannelOn,
LanguageClient,
DidChangeConfigurationNotification,
RequestType,
TextDocumentPositionParams,
ReferencesRequest,
NotificationType,
MessageType,
ConfigurationRequest,
ConfigurationParams,
ExecuteCommandParams,
CancellationToken,
ExecuteCommandRequest, TextDocumentIdentifier
} from 'vscode-languageclient';
import * as requirements from './requirements';
import { languages, IndentAction, workspace, window, commands, ExtensionContext, TextDocument, Position, LanguageConfiguration, Uri, extensions, Command, TextEditor } from "vscode";
import * as path from 'path';
import * as os from 'os';
import { activateTagClosing, AutoCloseResult } from './tagClosing';
import * as path from 'path';
import { Command, commands, ExtensionContext, extensions, IndentAction, LanguageConfiguration, languages, Position, TextDocument, TextEditor, Uri, window, workspace } from "vscode";
import { CancellationToken, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, ExecuteCommandRequest, LanguageClient, LanguageClientOptions, MessageType, NotificationType, ReferencesRequest, RequestType, RevealOutputChannelOn, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient';
import { Commands } from './commands';
import { getXMLConfiguration, onConfigurationChange, subscribeJDKChangeConfiguration } from './settings';
import { collectXmlJavaExtensions, onExtensionChange } from './plugin';
import { prepareExecutable } from './javaServerStarter';
import { markdownPreviewProvider } from "./markdownPreviewProvider";
import { collectXmlJavaExtensions, onExtensionChange } from './plugin';
import * as requirements from './requirements';
import { getXMLConfiguration, onConfigurationChange, subscribeJDKChangeConfiguration } from './settings';
import { activateTagClosing, AutoCloseResult } from './tagClosing';
import { containsVariableReferenceToCurrentFile, getVariableSubstitutedAssociations } from './variableSubstitution';

export interface ScopeInfo {
scope: "default" | "global" | "workspace" | "folder";
Expand Down Expand Up @@ -340,6 +326,14 @@ export function activate(context: ExtensionContext) {
}
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(requirements.java_home) });
onConfigurationChange();
}
}));

const api: XMLExtensionApi = {
// add API set catalogs to internal memory
Expand Down Expand Up @@ -441,11 +435,8 @@ export function activate(context: ExtensionContext) {
xml['xml']['catalogs'].push(catalog);
}
})
externalXmlSettings.xmlFileAssociations.forEach(element => {
if (!xml['xml']['fileAssociations'].some(fileAssociation => fileAssociation.systemId === element.systemId)) {
xml['xml']['fileAssociations'].push(element);
}
});
// Apply variable substitutions for file associations
xml['xml']['fileAssociations'] = [...getVariableSubstitutedAssociations(xml['xml']['fileAssociations'])];

return xml;
}
Expand Down
155 changes: 155 additions & 0 deletions src/variableSubstitution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as path from "path";
import { TextDocument, window, workspace, WorkspaceFolder } from "vscode";
import { XMLFileAssociation } from "./extension";

/**
* 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: TextDocument = window.activeTextEditor.document;
const currentFileUri: string = currentFile && currentFile.uri.fsPath;
const currentWorkspace: WorkspaceFolder = workspace.getWorkspaceFolder(currentFile && currentFile.uri);
const currentWorkspaceUri: string = (currentWorkspace && currentWorkspace.uri.fsPath)
|| (workspace.workspaceFolders && workspace.workspaceFolders[0].uri.fsPath);

// Remove variables that can't be resolved
let variablesToSubstitute = VARIABLE_SUBSTITUTIONS;
if (!currentWorkspaceUri) {
variablesToSubstitute = variablesToSubstitute.filter(variable => { variable.kind !== VariableSubstitutionKind.Workspace });
}
if (!currentFileUri) {
variablesToSubstitute = variablesToSubstitute.filter(variable => { 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);
}
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;
}

0 comments on commit d9582c6

Please sign in to comment.