Skip to content

Commit 64a4ec1

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 c79e830 commit 64a4ec1

File tree

3 files changed

+161
-46
lines changed

3 files changed

+161
-46
lines changed

docs/Validation.md

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

200200
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)
201201

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

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

323335
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)
324336

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

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

327349
# Other Validation Settings
328350

src/extension.ts

Lines changed: 105 additions & 46 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
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, WorkspaceFolder } from "vscode";
16+
import { CancellationToken, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, ExecuteCommandRequest, LanguageClient, LanguageClientOptions, MessageType, NotificationType, ReferencesRequest, RequestType, RevealOutputChannelOn, 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 { FILE_BASENAME_NO_EXTENSION, FILE_DIRNAME, SETTINGS_VARIABLES, VariableReferenceKind, WORKSPACE_FOLDER } from './variableReference';
3925

4026
export interface ScopeInfo {
4127
scope: "default" | "global" | "workspace" | "folder";
@@ -163,9 +149,9 @@ export function activate(context: ExtensionContext) {
163149
const sectionId = '';
164150
markdownPreviewProvider.show(context.asAbsolutePath(path.join('docs', uri)), title, sectionId, context);
165151
}));
166-
context.subscriptions.push(commands.registerCommand(Commands.OPEN_DOCS, async (params: {page: string, section: string}) => {
152+
context.subscriptions.push(commands.registerCommand(Commands.OPEN_DOCS, async (params: { page: string, section: string }) => {
167153
const page = params.page.endsWith('.md') ? params.page.substr(0, params.page.length - 3) : params.page;
168-
const uri = page + '.md';
154+
const uri = page + '.md';
169155
const sectionId = params.section || '';
170156
const title = 'XML ' + page;
171157
markdownPreviewProvider.show(context.asAbsolutePath(path.join('docs', uri)), title, sectionId, context);
@@ -262,25 +248,25 @@ export function activate(context: ExtensionContext) {
262248
// Handler for 'xml/executeClientCommand` request message that executes a command on the client
263249
languageClient.onRequest(ExecuteClientCommandRequest.type, async (params: ExecuteCommandParams) => {
264250
return await commands.executeCommand(params.command, ...params.arguments);
265-
});
251+
});
266252

267253
// Register client command to execute custom XML Language Server command
268254
context.subscriptions.push(commands.registerCommand(Commands.EXECUTE_WORKSPACE_COMMAND, (command, ...rest) => {
269-
let token: CancellationToken;
270-
let commandArgs: any[] = rest;
271-
if (rest && rest.length && CancellationToken.is(rest[rest.length - 1])) {
272-
token = rest[rest.length - 1];
273-
commandArgs = rest.slice(0, rest.length - 1);
274-
}
275-
const params: ExecuteCommandParams = {
276-
command,
277-
arguments: commandArgs
278-
};
279-
if (token) {
280-
return languageClient.sendRequest(ExecuteCommandRequest.type, params, token);
281-
} else {
282-
return languageClient.sendRequest(ExecuteCommandRequest.type, params);
283-
}
255+
let token: CancellationToken;
256+
let commandArgs: any[] = rest;
257+
if (rest && rest.length && CancellationToken.is(rest[rest.length - 1])) {
258+
token = rest[rest.length - 1];
259+
commandArgs = rest.slice(0, rest.length - 1);
260+
}
261+
const params: ExecuteCommandParams = {
262+
command,
263+
arguments: commandArgs
264+
};
265+
if (token) {
266+
return languageClient.sendRequest(ExecuteCommandRequest.type, params, token);
267+
} else {
268+
return languageClient.sendRequest(ExecuteCommandRequest.type, params);
269+
}
284270
}));
285271

286272
context.subscriptions.push(commands.registerCommand(Commands.OPEN_SETTINGS, async (settingId?: string) => {
@@ -319,6 +305,15 @@ export function activate(context: ExtensionContext) {
319305
}
320306
return result;
321307
});
308+
// When the current document changes, update ${fileDirname} and ${fileBasenameNoExtension}
309+
// for the file associations, (but only if these variables are referenced),
310+
// and send the updated settings to the server
311+
context.subscriptions.push(window.onDidChangeActiveTextEditor(() => {
312+
if (fileAssocReferencesCurrentFile()) {
313+
languageClient.sendNotification(DidChangeConfigurationNotification.type, { settings: getXMLSettings(requirements.java_home) });
314+
onConfigurationChange();
315+
}
316+
}));
322317

323318
const api: XMLExtensionApi = {
324319
// add API set catalogs to internal memory
@@ -420,14 +415,78 @@ export function activate(context: ExtensionContext) {
420415
xml['xml']['catalogs'].push(catalog);
421416
}
422417
})
423-
externalXmlSettings.xmlFileAssociations.forEach(element => {
424-
if (!xml['xml']['fileAssociations'].some(fileAssociation => fileAssociation.systemId === element.systemId)) {
425-
xml['xml']['fileAssociations'].push(element);
426-
}
427-
});
418+
const variableSubstitutedAssociations: XMLFileAssociation[] =
419+
xml['xml']['fileAssociations'].map((association: XMLFileAssociation): XMLFileAssociation => {
420+
421+
const currentFile: TextDocument = window.activeTextEditor.document;
422+
const currentFileUri: string = currentFile && currentFile.uri.fsPath;
423+
const currentWorkspace: WorkspaceFolder = workspace.getWorkspaceFolder(currentFile && currentFile.uri);
424+
const currentWorkspaceUri: string = (currentWorkspace && currentWorkspace.uri.fsPath)
425+
|| (workspace.workspaceFolders && workspace.workspaceFolders[0].uri.fsPath);
426+
427+
if (!currentWorkspaceUri
428+
&& SETTINGS_VARIABLES
429+
.filter(variable => { variable.kind === VariableReferenceKind.Workspace })
430+
.map(variable => {
431+
return association.pattern.indexOf(`&{${variable.name}}`) >= 0
432+
|| association.systemId.indexOf(`&{${variable.name}}`) >= 0;
433+
})
434+
.reduce((a, b) => { return a || b; }, false)) {
435+
return;
436+
}
437+
438+
if (!currentFileUri
439+
&& SETTINGS_VARIABLES
440+
.filter(variable => { variable.kind === VariableReferenceKind.File })
441+
.map(variable => {
442+
return association.pattern.indexOf(`&{${variable.name}}`) >= 0
443+
|| association.systemId.indexOf(`&{${variable.name}}`) >= 0;
444+
})
445+
.reduce((a, b) => { return a || b; }, false)) {
446+
return;
447+
}
448+
449+
/**
450+
* Returns the string with the values for:
451+
* * ${workspaceFolder}
452+
* * ${fileDirname}
453+
* * ${fileBasenameNoExtension}
454+
* substituted into the string
455+
*
456+
* @param val the value to substitute the variables into
457+
* @return the string with values for the variables substituted into the string
458+
*/
459+
const subVars = (val: string): string => {
460+
let newVal: string = val.replace(WORKSPACE_FOLDER.getReplaceRegExp(), currentWorkspaceUri);
461+
newVal = newVal.replace(FILE_DIRNAME.getReplaceRegExp(), path.dirname(currentFileUri));
462+
newVal = newVal.replace(FILE_BASENAME_NO_EXTENSION.getReplaceRegExp(), path.basename(currentFileUri, path.extname(currentFileUri)));
463+
return newVal;
464+
}
465+
466+
return {
467+
pattern: subVars(association.pattern),
468+
systemId: subVars(association.systemId)
469+
};
470+
});
471+
xml['xml']['fileAssociations'] = [...variableSubstitutedAssociations];
428472

429473
return xml;
430474
}
475+
476+
/**
477+
* Returns true if the the XML settings contain a file association with a variable reference to either ${fileDirname} or ${fileBasenameNoExtension} and false otherwise
478+
*
479+
* @return true if the the XML settings contain a file association with a variable reference to either ${fileDirname} or ${fileBasenameNoExtension} and false otherwise
480+
*/
481+
function fileAssocReferencesCurrentFile(): boolean {
482+
for (const assoc of getXMLConfiguration().get('fileAssociations') as XMLFileAssociation[]) {
483+
if (assoc.pattern.indexOf('${fileDirname}') >= 0 || assoc.pattern.indexOf('${fileBasenameNoExtension}') >= 0
484+
|| assoc.systemId.indexOf('${fileDirname}') >= 0 || assoc.systemId.indexOf('${fileBasenameNoExtension}') >= 0) {
485+
return true;
486+
}
487+
}
488+
return false;
489+
}
431490
}
432491

433492
export function deactivate(): void {

src/variableReference.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export class VariableReference {
2+
public name: string;
3+
public kind: VariableReferenceKind;
4+
private replaceRegExp: RegExp | undefined;
5+
6+
constructor(name: string, kind: VariableReferenceKind) {
7+
this.name = name;
8+
this.kind = kind;
9+
}
10+
11+
public getReplaceRegExp(): RegExp {
12+
if (!this.replaceRegExp) {
13+
this.replaceRegExp = new RegExp('\\$\\{' + `${this.name}` + '\\}', 'g');
14+
}
15+
return this.replaceRegExp;
16+
}
17+
}
18+
19+
export enum VariableReferenceKind {
20+
Workspace,
21+
File
22+
}
23+
24+
export const WORKSPACE_FOLDER: VariableReference = new VariableReference("workspaceFolder", VariableReferenceKind.Workspace);
25+
26+
export const FILE_DIRNAME: VariableReference = new VariableReference("fileDirname", VariableReferenceKind.File);
27+
28+
export const FILE_BASENAME_NO_EXTENSION: VariableReference = new VariableReference("fileBasenameNoExtension", VariableReferenceKind.File);
29+
30+
export const SETTINGS_VARIABLES: VariableReference[] = [
31+
WORKSPACE_FOLDER,
32+
FILE_DIRNAME,
33+
FILE_BASENAME_NO_EXTENSION
34+
];

0 commit comments

Comments
 (0)