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:
Shoham Ben Shitrit
2024-06-20 18:38:44 +03:00
committed by GitHub
parent 62fbb97723
commit 4d858a68c9
3 changed files with 296 additions and 23 deletions
@@ -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);
});
});