Skip to content

Commit d9582c6

Browse files
committed
Add variables for xml.fileAssociations
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 <davthomp@redhat.com>
1 parent 89579ca commit d9582c6

File tree

3 files changed

+196
-28
lines changed

3 files changed

+196
-28
lines changed

docs/Validation.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,18 @@ Please note that you can use wildcards in the pattern (ex: `foo*.xml`):
202202

203203
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)
204204

205+
You can also use the following three variables in either the `pattern` or `systemId`:
206+
207+
| Variable | Meaning |
208+
| --------------------------- | ------------------------------------------------------------------------ |
209+
| ${workspaceFolder} | The absolute path to root folder of the workspace that is currently open |
210+
| ${fileDirname} | The absolute path to the folder of the file that is currently opened |
211+
| ${fileBasenameNoExtension} | The current opened file's basename with no file extension |
212+
213+
If one of the variables for an association can't be expanded (eg. because vscode is opened in rootless mode),
214+
the association is ignored.
215+
This feature is specific to the VSCode client.
216+
205217
## Validation with DTD grammar
206218

207219
To associate your XML with a DTD grammar you can use several strategies:
@@ -325,7 +337,17 @@ Please note that you can use wildcards in the pattern (ex: `foo*.xml`):
325337

326338
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)
327339

340+
You can also use the following three variables in either the `pattern` or `systemId`:
341+
342+
| Variable | Meaning |
343+
| --------------------------- | ------------------------------------------------------------------------ |
344+
| ${workspaceFolder} | The absolute path to root folder of the workspace that is currently open |
345+
| ${fileDirname} | The absolute path to the folder of the file that is currently opened |
346+
| ${fileBasenameNoExtension} | The current opened file's basename with no file extension |
328347

348+
If one of the variables for an association can't be expanded (eg. because vscode is opened in rootless mode),
349+
the association is ignored.
350+
This feature is specific to the VSCode client.
329351

330352
# Other Validation Settings
331353

src/extension.ts

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,18 @@
1010
* Microsoft Corporation - Auto Closing Tags
1111
*/
1212

13-
import { prepareExecutable } from './javaServerStarter';
14-
import {
15-
LanguageClientOptions,
16-
RevealOutputChannelOn,
17-
LanguageClient,
18-
DidChangeConfigurationNotification,
19-
RequestType,
20-
TextDocumentPositionParams,
21-
ReferencesRequest,
22-
NotificationType,
23-
MessageType,
24-
ConfigurationRequest,
25-
ConfigurationParams,
26-
ExecuteCommandParams,
27-
CancellationToken,
28-
ExecuteCommandRequest, TextDocumentIdentifier
29-
} from 'vscode-languageclient';
30-
import * as requirements from './requirements';
31-
import { languages, IndentAction, workspace, window, commands, ExtensionContext, TextDocument, Position, LanguageConfiguration, Uri, extensions, Command, TextEditor } from "vscode";
32-
import * as path from 'path';
3313
import * as os from 'os';
34-
import { activateTagClosing, AutoCloseResult } from './tagClosing';
14+
import * as path from 'path';
15+
import { Command, commands, ExtensionContext, extensions, IndentAction, LanguageConfiguration, languages, Position, TextDocument, TextEditor, Uri, window, workspace } from "vscode";
16+
import { CancellationToken, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, ExecuteCommandRequest, LanguageClient, LanguageClientOptions, MessageType, NotificationType, ReferencesRequest, RequestType, RevealOutputChannelOn, TextDocumentIdentifier, TextDocumentPositionParams } from 'vscode-languageclient';
3517
import { Commands } from './commands';
36-
import { getXMLConfiguration, onConfigurationChange, subscribeJDKChangeConfiguration } from './settings';
37-
import { collectXmlJavaExtensions, onExtensionChange } from './plugin';
18+
import { prepareExecutable } from './javaServerStarter';
3819
import { markdownPreviewProvider } from "./markdownPreviewProvider";
20+
import { collectXmlJavaExtensions, onExtensionChange } from './plugin';
21+
import * as requirements from './requirements';
22+
import { getXMLConfiguration, onConfigurationChange, subscribeJDKChangeConfiguration } from './settings';
23+
import { activateTagClosing, AutoCloseResult } from './tagClosing';
24+
import { containsVariableReferenceToCurrentFile, getVariableSubstitutedAssociations } from './variableSubstitution';
3925

4026
export interface ScopeInfo {
4127
scope: "default" | "global" | "workspace" | "folder";
@@ -340,6 +326,14 @@ export function activate(context: ExtensionContext) {
340326
}
341327
return result;
342328
});
329+
// When the current document changes, update variable values that refer to the current file if these variables are referenced,
330+
// and send the updated settings to the server
331+
context.subscriptions.push(window.onDidChangeActiveTextEditor(() => {
332+
if (containsVariableReferenceToCurrentFile(getXMLConfiguration().get('fileAssociations') as XMLFileAssociation[])) {
333+
languageClient.sendNotification(DidChangeConfigurationNotification.type, { settings: getXMLSettings(requirements.java_home) });
334+
onConfigurationChange();
335+
}
336+
}));
343337

344338
const api: XMLExtensionApi = {
345339
// add API set catalogs to internal memory
@@ -441,11 +435,8 @@ export function activate(context: ExtensionContext) {
441435
xml['xml']['catalogs'].push(catalog);
442436
}
443437
})
444-
externalXmlSettings.xmlFileAssociations.forEach(element => {
445-
if (!xml['xml']['fileAssociations'].some(fileAssociation => fileAssociation.systemId === element.systemId)) {
446-
xml['xml']['fileAssociations'].push(element);
447-
}
448-
});
438+
// Apply variable substitutions for file associations
439+
xml['xml']['fileAssociations'] = [...getVariableSubstitutedAssociations(xml['xml']['fileAssociations'])];
449440

450441
return xml;
451442
}

src/variableSubstitution.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as path from "path";
2+
import { TextDocument, window, workspace, WorkspaceFolder } from "vscode";
3+
import { XMLFileAssociation } from "./extension";
4+
5+
/**
6+
* Represents a variable that refers to a value that can be resolved
7+
*/
8+
class VariableSubstitution {
9+
10+
private varName: string;
11+
private varKind: VariableSubstitutionKind;
12+
private getValue: (currentFileUri: string, currentWorkspaceUri: string) => string;
13+
private replaceRegExp: RegExp | undefined;
14+
15+
/**
16+
*
17+
* @param name the name of this variable
18+
* @param kind the kind of this variable
19+
* @param getValue a function that resolves the value of the variable
20+
*/
21+
constructor(name: string, kind: VariableSubstitutionKind, getValue: (currentFileUri: string, currentWorkspaceUri: string) => string) {
22+
this.varName = name;
23+
this.varKind = kind;
24+
this.getValue = getValue;
25+
}
26+
27+
public get name(): string {
28+
return this.varName;
29+
}
30+
31+
public get kind(): VariableSubstitutionKind {
32+
return this.varKind;
33+
}
34+
35+
/**
36+
* Returns the string with the references to this variable replaced with the value, or the original string if the value cannot be resolved
37+
*
38+
* @param original the string to substitute variable value
39+
* @param currentFileUri the uri of the currently focused file, as a string
40+
* @param currentWorkspaceUri the uri of the root of the currently open workspace, as a string
41+
* @returns the string with the references to this variable replaced with the value, or the original string if the value cannot be resolved
42+
*/
43+
public substituteString(original: string, currentFileUri: string, currentWorkspaceUri: string): string {
44+
const value: string = this.getValue(currentFileUri, currentWorkspaceUri);
45+
return value ? original.replace(this.getReplaceRegExp(), value) : original;
46+
}
47+
48+
/**
49+
* Returns a regex that matches all references to the variable
50+
*
51+
* Lazily initialized
52+
*
53+
* @returns a regex that matches all references to the variable
54+
*/
55+
public getReplaceRegExp(): RegExp {
56+
if (!this.replaceRegExp) {
57+
this.replaceRegExp = new RegExp('\\$\\{' + `${this.name}` + '\\}', 'g');
58+
}
59+
return this.replaceRegExp;
60+
}
61+
}
62+
63+
enum VariableSubstitutionKind {
64+
Workspace,
65+
File
66+
}
67+
68+
// A list of all variable substitutions. To add a new variable substitution, add an entry to this list
69+
const VARIABLE_SUBSTITUTIONS: VariableSubstitution[] = [
70+
new VariableSubstitution(
71+
"workspaceFolder",
72+
VariableSubstitutionKind.Workspace,
73+
(currentFileUri: string, currentWorkspaceUri: string): string => {
74+
return currentWorkspaceUri;
75+
}
76+
),
77+
new VariableSubstitution(
78+
"fileDirname",
79+
VariableSubstitutionKind.File,
80+
(currentFileUri: string, currentWorkspaceUri: string): string => {
81+
return path.dirname(currentFileUri);
82+
}
83+
),
84+
new VariableSubstitution(
85+
"fileBasenameNoExtension",
86+
VariableSubstitutionKind.File,
87+
(currentFileUri: string, currentWorkspaceUri: string): string => {
88+
return path.basename(currentFileUri, path.extname(currentFileUri));
89+
}
90+
)
91+
];
92+
93+
/**
94+
* Returns the file associations with as many variable references resolved as possible
95+
*
96+
* @param associations the file associations to resolve the variable references in
97+
* @returns the file associations with as many variable references resolved as possible
98+
*/
99+
export function getVariableSubstitutedAssociations(associations: XMLFileAssociation[]): XMLFileAssociation[] {
100+
101+
// Collect properties needed to resolve variables
102+
const currentFile: TextDocument = window.activeTextEditor.document;
103+
const currentFileUri: string = currentFile && currentFile.uri.fsPath;
104+
const currentWorkspace: WorkspaceFolder = workspace.getWorkspaceFolder(currentFile && currentFile.uri);
105+
const currentWorkspaceUri: string = (currentWorkspace && currentWorkspace.uri.fsPath)
106+
|| (workspace.workspaceFolders && workspace.workspaceFolders[0].uri.fsPath);
107+
108+
// Remove variables that can't be resolved
109+
let variablesToSubstitute = VARIABLE_SUBSTITUTIONS;
110+
if (!currentWorkspaceUri) {
111+
variablesToSubstitute = variablesToSubstitute.filter(variable => { variable.kind !== VariableSubstitutionKind.Workspace });
112+
}
113+
if (!currentFileUri) {
114+
variablesToSubstitute = variablesToSubstitute.filter(variable => { variable.kind !== VariableSubstitutionKind.File });
115+
}
116+
117+
/**
118+
* Returns the string with the values for all the variables that can be resolved substituted in the string
119+
*
120+
* @param val the value to substitute the variables into
121+
* @return the string with the values for all the variables that can be resolved substituted in the string
122+
*/
123+
const subVars = (val: string): string => {
124+
let newVal = val;
125+
for (const settingVariable of variablesToSubstitute) {
126+
newVal = settingVariable.substituteString(newVal, currentFileUri, currentWorkspaceUri);
127+
}
128+
return newVal;
129+
}
130+
131+
return associations.map((association: XMLFileAssociation) => {
132+
return {
133+
pattern: subVars(association.pattern),
134+
systemId: subVars(association.systemId)
135+
};
136+
});
137+
}
138+
139+
/**
140+
* Returns true if any of the file associations contain references to variables that refer to current file, and false otherwise
141+
*
142+
* @param associations A list of file associations to check
143+
* @returns true if any of the file associations contain references to variables that refer to current file, and false otherwise
144+
*/
145+
export function containsVariableReferenceToCurrentFile(associations: XMLFileAssociation[]): boolean {
146+
const fileVariables: VariableSubstitution[] = VARIABLE_SUBSTITUTIONS.filter(variable => variable.kind === VariableSubstitutionKind.File);
147+
for (const association of associations) {
148+
for (const variable of fileVariables) {
149+
if (variable.getReplaceRegExp().test(association.pattern) || variable.getReplaceRegExp().test(association.systemId)) {
150+
return true;
151+
}
152+
}
153+
}
154+
return false;
155+
}

0 commit comments

Comments
 (0)