mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-14 04:00:38 +01:00
allow usage of extname(N) template in custom labels (#213033)
* allow usage of `filenamePart` template in custom labels * use extname(n) instead of filename part * change extname to return the full extensions and not just the last file extension * Fix regex to match extname Return match when N in `extname(N)` is larger than extensions amount * Add tests * Fix `.file` label assertion * Make sure `extname` doesn't include leading dots have filename return the file name from the start until the first non-leading `.` * 💄 --------- Co-authored-by: BeniBenj <besimmonds@microsoft.com> Co-authored-by: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
62fbb97723
commit
4d858a68c9
@@ -102,9 +102,10 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
let customEditorLabelDescription = localize('workbench.editor.label.patterns', "Controls the rendering of the editor label. Each __Item__ is a pattern that matches a file path. Both relative and absolute file paths are supported. The relative path must include the WORKSPACE_FOLDER (e.g `WORKSPACE_FOLDER/src/**.tsx` or `*/src/**.tsx`). Absolute patterns must start with a `/`. In case multiple patterns match, the longest matching path will be picked. Each __Value__ is the template for the rendered editor when the __Item__ matches. Variables are substituted based on the context:");
|
||||
customEditorLabelDescription += '\n- ' + [
|
||||
localize('workbench.editor.label.dirname', "`${dirname}`: name of the folder in which the file is located (e.g. `WORKSPACE_FOLDER/folder/file.txt -> folder`)."),
|
||||
localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absoulte path, otherwise it corresponds to the workspace folder."),
|
||||
localize('workbench.editor.label.nthdirname', "`${dirname(N)}`: name of the nth parent folder in which the file is located (e.g. `N=2: WORKSPACE_FOLDER/static/folder/file.txt -> WORKSPACE_FOLDER`). Folders can be picked from the start of the path by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.txt -> WORKSPACE_FOLDER`). If the __Item__ is an absolute pattern path, the first folder (`N=-1`) refers to the first folder in the absolute path, otherwise it corresponds to the workspace folder."),
|
||||
localize('workbench.editor.label.filename', "`${filename}`: name of the file without the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> file`)."),
|
||||
localize('workbench.editor.label.extname', "`${extname}`: the file extension (e.g. `WORKSPACE_FOLDER/folder/file.txt -> txt`)."),
|
||||
localize('workbench.editor.label.nthextname', "`${extname(N)}`: the nth extension of the file separated by '.' (e.g. `N=2: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext1`). Extension can be picked from the start of the extension by using negative numbers (e.g. `N=-1: WORKSPACE_FOLDER/folder/file.ext1.ext2.ext3 -> ext2`)."),
|
||||
].join('\n- '); // intentionally concatenated to not produce a string that is too long for translations
|
||||
customEditorLabelDescription += '\n\n' + localize('customEditorLabelDescriptionExample', "Example: `\"**/static/**/*.html\": \"${filename} - ${dirname} (${extname})\"` will render a file `WORKSPACE_FOLDER/static/folder/file.html` as `file - folder (html)`.");
|
||||
|
||||
@@ -113,7 +114,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
|
||||
additionalProperties:
|
||||
{
|
||||
type: 'string',
|
||||
markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern mtches. May include the variables ${dirname}, ${filename} and ${extname}."),
|
||||
markdownDescription: localize('workbench.editor.label.template', "The template which should be rendered when the pattern matches. May include the variables ${dirname}, ${filename} and ${extname}."),
|
||||
minLength: 1,
|
||||
pattern: '.*[a-zA-Z0-9].*'
|
||||
},
|
||||
|
||||
@@ -51,10 +51,10 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito
|
||||
this.storeEnablementState();
|
||||
this.storeCustomPatterns();
|
||||
|
||||
this.registerListernes();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListernes(): void {
|
||||
private registerListeners(): void {
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
// Cache the enabled state
|
||||
if (e.affectsConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED)) {
|
||||
@@ -148,29 +148,43 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito
|
||||
}
|
||||
|
||||
if (pattern.parsedPattern(relevantPath)) {
|
||||
return this.applyTempate(pattern.template, resource, relevantPath);
|
||||
return this.applyTemplate(pattern.template, resource, relevantPath);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|dirname\(([-+]?\d+)\))\}/g;
|
||||
private applyTempate(template: string, resource: URI, relevantPath: string): string {
|
||||
private readonly _parsedTemplateExpression = /\$\{(dirname|filename|extname|extname\((?<extnameN>[-+]?\d+)\)|dirname\((?<dirnameN>[-+]?\d+)\))\}/g;
|
||||
private readonly _filenameCaptureExpression = /(?<filename>^\.*[^.]*)/;
|
||||
private applyTemplate(template: string, resource: URI, relevantPath: string): string {
|
||||
let parsedPath: undefined | ParsedPath;
|
||||
return template.replace(this._parsedTemplateExpression, (match: string, variable: string, arg: string) => {
|
||||
return template.replace(this._parsedTemplateExpression, (match: string, variable: string, ...args: any[]) => {
|
||||
parsedPath = parsedPath ?? parsePath(resource.path);
|
||||
switch (variable) {
|
||||
case 'filename':
|
||||
return parsedPath.name;
|
||||
case 'extname':
|
||||
return parsedPath.ext.slice(1);
|
||||
default: { // dirname and dirname(arg)
|
||||
const n = variable === 'dirname' ? 0 : parseInt(arg);
|
||||
const nthDir = this.getNthDirname(dirname(relevantPath), n);
|
||||
if (nthDir) {
|
||||
return nthDir;
|
||||
}
|
||||
// named group matches
|
||||
const { dirnameN = '0', extnameN = '0' }: { dirnameN?: string; extnameN?: string } = args.pop();
|
||||
|
||||
if (variable === 'filename') {
|
||||
const { filename } = this._filenameCaptureExpression.exec(parsedPath.base)?.groups ?? {};
|
||||
if (filename) {
|
||||
return filename;
|
||||
}
|
||||
} else if (variable === 'extname') {
|
||||
const extension = this.getExtnames(parsedPath.base);
|
||||
if (extension) {
|
||||
return extension;
|
||||
}
|
||||
} else if (variable.startsWith('extname')) {
|
||||
const n = parseInt(extnameN);
|
||||
const nthExtname = this.getNthExtname(parsedPath.base, n);
|
||||
if (nthExtname) {
|
||||
return nthExtname;
|
||||
}
|
||||
} else if (variable.startsWith('dirname')) {
|
||||
const n = parseInt(dirnameN);
|
||||
const nthDir = this.getNthDirname(dirname(relevantPath), n);
|
||||
if (nthDir) {
|
||||
return nthDir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +192,36 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito
|
||||
});
|
||||
}
|
||||
|
||||
private removeLeadingDot(path: string): string {
|
||||
let withoutLeadingDot = path;
|
||||
while (withoutLeadingDot.startsWith('.')) {
|
||||
withoutLeadingDot = withoutLeadingDot.slice(1);
|
||||
}
|
||||
return withoutLeadingDot;
|
||||
}
|
||||
|
||||
private getNthDirname(path: string, n: number): string | undefined {
|
||||
// grand-parent/parent/filename.ext1.ext2 -> [grand-parent, parent]
|
||||
path = path.startsWith('/') ? path.slice(1) : path;
|
||||
const pathFragments = path.split('/');
|
||||
|
||||
const length = pathFragments.length;
|
||||
return this.getNthFragment(pathFragments, n);
|
||||
}
|
||||
|
||||
private getExtnames(fullFileName: string): string {
|
||||
return this.removeLeadingDot(fullFileName).split('.').slice(1).join('.');
|
||||
}
|
||||
|
||||
private getNthExtname(fullFileName: string, n: number): string | undefined {
|
||||
// file.ext1.ext2.ext3 -> [file, ext1, ext2, ext3]
|
||||
const extensionNameFragments = this.removeLeadingDot(fullFileName).split('.');
|
||||
extensionNameFragments.shift(); // remove the first element which is the file name
|
||||
|
||||
return this.getNthFragment(extensionNameFragments, n);
|
||||
}
|
||||
|
||||
private getNthFragment(fragments: string[], n: number): string | undefined {
|
||||
const length = fragments.length;
|
||||
|
||||
let nth;
|
||||
if (n < 0) {
|
||||
@@ -192,11 +230,11 @@ export class CustomEditorLabelService extends Disposable implements ICustomEdito
|
||||
nth = length - n - 1;
|
||||
}
|
||||
|
||||
const nthDir = pathFragments[nth];
|
||||
if (nthDir === undefined || nthDir === '') {
|
||||
const nthFragment = fragments[nth];
|
||||
if (nthFragment === undefined || nthFragment === '') {
|
||||
return undefined;
|
||||
}
|
||||
return nthDir;
|
||||
return nthFragment;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { CustomEditorLabelService } from 'vs/workbench/services/editor/common/customEditorLabelService';
|
||||
import { ITestInstantiationService, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
|
||||
suite('Custom Editor Label Service', () => {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
setup(() => { });
|
||||
|
||||
teardown(async () => {
|
||||
disposables.clear();
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
async function createCustomLabelService(instantiationService: ITestInstantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[CustomEditorLabelService, TestConfigurationService, TestServiceAccessor]> {
|
||||
const configService = new TestConfigurationService();
|
||||
await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_ENABLED, true);
|
||||
instantiationService.stub(IConfigurationService, configService);
|
||||
|
||||
const customLabelService = disposables.add(instantiationService.createInstance(CustomEditorLabelService));
|
||||
return [customLabelService, configService, instantiationService.createInstance(TestServiceAccessor)];
|
||||
}
|
||||
|
||||
async function updatePattern(configService: TestConfigurationService, value: any): Promise<void> {
|
||||
await configService.setUserConfiguration(CustomEditorLabelService.SETTING_ID_PATTERNS, value);
|
||||
configService.onDidChangeConfigurationEmitter.fire({
|
||||
affectsConfiguration: (key: string) => key === CustomEditorLabelService.SETTING_ID_PATTERNS,
|
||||
source: ConfigurationTarget.USER,
|
||||
affectedKeys: new Set(CustomEditorLabelService.SETTING_ID_PATTERNS),
|
||||
change: {
|
||||
keys: [],
|
||||
overrides: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('Custom Labels: filename.extname', async () => {
|
||||
const [customLabelService, configService] = await createCustomLabelService();
|
||||
|
||||
await updatePattern(configService, {
|
||||
'**': '${filename}.${extname}'
|
||||
});
|
||||
|
||||
const filenames = [
|
||||
'file.txt',
|
||||
'file.txt1.tx2',
|
||||
'.file.txt',
|
||||
];
|
||||
|
||||
for (const filename of filenames) {
|
||||
const label = customLabelService.getName(URI.file(filename));
|
||||
assert.strictEqual(label, filename);
|
||||
}
|
||||
|
||||
let label = customLabelService.getName(URI.file('file'));
|
||||
assert.strictEqual(label, 'file.${extname}');
|
||||
|
||||
label = customLabelService.getName(URI.file('.file'));
|
||||
assert.strictEqual(label, '.file.${extname}');
|
||||
});
|
||||
|
||||
test('Custom Labels: filename', async () => {
|
||||
const [customLabelService, configService] = await createCustomLabelService();
|
||||
|
||||
await updatePattern(configService, {
|
||||
'**': '${filename}',
|
||||
});
|
||||
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file')), 'file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file.txt')), 'file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), 'file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('folder/file.txt1.txt2')), 'file');
|
||||
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file')), '.file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), '.file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), '.file');
|
||||
assert.strictEqual(customLabelService.getName(URI.file('folder/.file.txt1.txt2')), '.file');
|
||||
});
|
||||
|
||||
test('Custom Labels: extname(N)', async () => {
|
||||
const [customLabelService, configService] = await createCustomLabelService();
|
||||
|
||||
await updatePattern(configService, {
|
||||
'**/ext/**': '${extname}',
|
||||
'**/ext0/**': '${extname(0)}',
|
||||
'**/ext1/**': '${extname(1)}',
|
||||
'**/ext2/**': '${extname(2)}',
|
||||
'**/extMinus1/**': '${extname(-1)}',
|
||||
'**/extMinus2/**': '${extname(-2)}',
|
||||
});
|
||||
|
||||
interface IExt {
|
||||
extname?: string;
|
||||
ext0?: string;
|
||||
ext1?: string;
|
||||
ext2?: string;
|
||||
extMinus1?: string;
|
||||
extMinus2?: string;
|
||||
}
|
||||
|
||||
function assertExtname(filename: string, ext: IExt): void {
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/ext/${filename}`)), ext.extname ?? '${extname}', filename);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/ext0/${filename}`)), ext.ext0 ?? '${extname(0)}', filename);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/ext1/${filename}`)), ext.ext1 ?? '${extname(1)}', filename);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/ext2/${filename}`)), ext.ext2 ?? '${extname(2)}', filename);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus1/${filename}`)), ext.extMinus1 ?? '${extname(-1)}', filename);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(`test/extMinus2/${filename}`)), ext.extMinus2 ?? '${extname(-2)}', filename);
|
||||
}
|
||||
|
||||
assertExtname('file.txt', {
|
||||
extname: 'txt',
|
||||
ext0: 'txt',
|
||||
extMinus1: 'txt',
|
||||
});
|
||||
|
||||
assertExtname('file.txt1.txt2', {
|
||||
extname: 'txt1.txt2',
|
||||
ext0: 'txt2',
|
||||
ext1: 'txt1',
|
||||
extMinus1: 'txt1',
|
||||
extMinus2: 'txt2',
|
||||
});
|
||||
|
||||
assertExtname('.file.txt1.txt2', {
|
||||
extname: 'txt1.txt2',
|
||||
ext0: 'txt2',
|
||||
ext1: 'txt1',
|
||||
extMinus1: 'txt1',
|
||||
extMinus2: 'txt2',
|
||||
});
|
||||
|
||||
assertExtname('.file.txt1.txt2.txt3.txt4', {
|
||||
extname: 'txt1.txt2.txt3.txt4',
|
||||
ext0: 'txt4',
|
||||
ext1: 'txt3',
|
||||
ext2: 'txt2',
|
||||
extMinus1: 'txt1',
|
||||
extMinus2: 'txt2',
|
||||
});
|
||||
|
||||
assertExtname('file', {});
|
||||
assertExtname('.file', {});
|
||||
});
|
||||
|
||||
test('Custom Labels: dirname(N)', async () => {
|
||||
const [customLabelService, configService] = await createCustomLabelService();
|
||||
|
||||
await updatePattern(configService, {
|
||||
'**': '${dirname},${dirname(0)},${dirname(1)},${dirname(2)},${dirname(-1)},${dirname(-2)}',
|
||||
});
|
||||
|
||||
interface IDir {
|
||||
dirname?: string;
|
||||
dir0?: string;
|
||||
dir1?: string;
|
||||
dir2?: string;
|
||||
dirMinus1?: string;
|
||||
dirMinus2?: string;
|
||||
}
|
||||
|
||||
function assertDirname(path: string, dir: IDir): void {
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[0], dir.dirname ?? '${dirname}', path);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[1], dir.dir0 ?? '${dirname(0)}', path);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[2], dir.dir1 ?? '${dirname(1)}', path);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[3], dir.dir2 ?? '${dirname(2)}', path);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[4], dir.dirMinus1 ?? '${dirname(-1)}', path);
|
||||
assert.strictEqual(customLabelService.getName(URI.file(path))?.split(',')[5], dir.dirMinus2 ?? '${dirname(-2)}', path);
|
||||
}
|
||||
|
||||
assertDirname('folder/file.txt', {
|
||||
dirname: 'folder',
|
||||
dir0: 'folder',
|
||||
dirMinus1: 'folder',
|
||||
});
|
||||
|
||||
assertDirname('root/folder/file.txt', {
|
||||
dirname: 'folder',
|
||||
dir0: 'folder',
|
||||
dir1: 'root',
|
||||
dirMinus1: 'root',
|
||||
dirMinus2: 'folder',
|
||||
});
|
||||
|
||||
assertDirname('root/.folder/file.txt', {
|
||||
dirname: '.folder',
|
||||
dir0: '.folder',
|
||||
dir1: 'root',
|
||||
dirMinus1: 'root',
|
||||
dirMinus2: '.folder',
|
||||
});
|
||||
|
||||
assertDirname('root/parent/folder/file.txt', {
|
||||
dirname: 'folder',
|
||||
dir0: 'folder',
|
||||
dir1: 'parent',
|
||||
dir2: 'root',
|
||||
dirMinus1: 'root',
|
||||
dirMinus2: 'parent',
|
||||
});
|
||||
|
||||
assertDirname('file.txt', {});
|
||||
});
|
||||
|
||||
test('Custom Labels: no pattern match', async () => {
|
||||
const [customLabelService, configService] = await createCustomLabelService();
|
||||
|
||||
await updatePattern(configService, {
|
||||
'**/folder/**': 'folder',
|
||||
'file': 'file',
|
||||
});
|
||||
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file.txt')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('file.txt1.txt2')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined);
|
||||
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file.txt')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('.file.txt1.txt2')), undefined);
|
||||
assert.strictEqual(customLabelService.getName(URI.file('folder1/file.txt1.txt2')), undefined);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user