Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import * as sinon from 'sinon';6import { DisposableStore } from '../../../../../base/common/lifecycle.js';7import { sep } from '../../../../../base/common/path.js';8import { isWindows } from '../../../../../base/common/platform.js';9import { extUriBiasedIgnorePathCase } from '../../../../../base/common/resources.js';10import { URI } from '../../../../../base/common/uri.js';11import { mock } from '../../../../../base/test/common/mock.js';12import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';13import { Selection } from '../../../../common/core/selection.js';14import { TextModel } from '../../../../common/model/textModel.js';15import { SnippetParser, Variable, VariableResolver } from '../../browser/snippetParser.js';16import { ClipboardBasedVariableResolver, CompositeSnippetVariableResolver, ModelBasedVariableResolver, SelectionBasedVariableResolver, TimeBasedVariableResolver, WorkspaceBasedVariableResolver } from '../../browser/snippetVariables.js';17import { createTextModel } from '../../../../test/common/testTextModel.js';18import { ILabelService } from '../../../../../platform/label/common/label.js';19import { IWorkspace, IWorkspaceContextService, toWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';20import { Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js';21import { toWorkspaceFolders } from '../../../../../platform/workspaces/common/workspaces.js';2223suite('Snippet Variables Resolver', function () {242526const labelService = new class extends mock<ILabelService>() {27override getUriLabel(uri: URI) {28return uri.fsPath;29}30};3132let model: TextModel;33let resolver: VariableResolver;3435setup(function () {36model = createTextModel([37'this is line one',38'this is line two',39' this is line three'40].join('\n'), undefined, undefined, URI.parse('file:///foo/files/text.txt'));4142resolver = new CompositeSnippetVariableResolver([43new ModelBasedVariableResolver(labelService, model),44new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0, undefined),45]);46});4748teardown(function () {49model.dispose();50});5152ensureNoDisposablesAreLeakedInTestSuite();535455function assertVariableResolve(resolver: VariableResolver, varName: string, expected?: string) {56const snippet = new SnippetParser().parse(`$${varName}`);57const variable = <Variable>snippet.children[0];58variable.resolve(resolver);59if (variable.children.length === 0) {60assert.strictEqual(undefined, expected);61} else {62assert.strictEqual(variable.toString(), expected);63}64}6566test('editor variables, basics', function () {67assertVariableResolve(resolver, 'TM_FILENAME', 'text.txt');68assertVariableResolve(resolver, 'something', undefined);69});7071test('editor variables, file/dir', function () {7273const disposables = new DisposableStore();7475assertVariableResolve(resolver, 'TM_FILENAME', 'text.txt');76if (!isWindows) {77assertVariableResolve(resolver, 'TM_DIRECTORY', '/foo/files');78assertVariableResolve(resolver, 'TM_FILEPATH', '/foo/files/text.txt');79}8081resolver = new ModelBasedVariableResolver(82labelService,83disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))84);85assertVariableResolve(resolver, 'TM_FILENAME', 'ghi');86if (!isWindows) {87assertVariableResolve(resolver, 'TM_DIRECTORY', '/abc/def');88assertVariableResolve(resolver, 'TM_FILEPATH', '/abc/def/ghi');89}9091resolver = new ModelBasedVariableResolver(92labelService,93disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:fff.ts')))94);95assertVariableResolve(resolver, 'TM_DIRECTORY', '');96assertVariableResolve(resolver, 'TM_FILEPATH', 'fff.ts');9798disposables.dispose();99});100101test('Path delimiters in code snippet variables aren\'t specific to remote OS #76840', function () {102103const labelService = new class extends mock<ILabelService>() {104override getUriLabel(uri: URI) {105return uri.fsPath.replace(/\/|\\/g, '|');106}107};108109const model = createTextModel([].join('\n'), undefined, undefined, URI.parse('foo:///foo/files/text.txt'));110111const resolver = new CompositeSnippetVariableResolver([new ModelBasedVariableResolver(labelService, model)]);112113assertVariableResolve(resolver, 'TM_FILEPATH', '|foo|files|text.txt');114115model.dispose();116});117118test('editor variables, selection', function () {119120resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0, undefined);121assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');122assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two');123assertVariableResolve(resolver, 'TM_LINE_INDEX', '1');124assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2');125assertVariableResolve(resolver, 'CURSOR_INDEX', '0');126assertVariableResolve(resolver, 'CURSOR_NUMBER', '1');127128resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 4, undefined);129assertVariableResolve(resolver, 'CURSOR_INDEX', '4');130assertVariableResolve(resolver, 'CURSOR_NUMBER', '5');131132resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0, undefined);133assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');134assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one');135assertVariableResolve(resolver, 'TM_LINE_INDEX', '0');136assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1');137138resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0, undefined);139assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined);140141assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this');142143resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0, undefined);144assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined);145146});147148test('TextmateSnippet, resolve variable', function () {149const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true);150assert.strictEqual(snippet.toString(), '""');151snippet.resolveVariables(resolver);152assert.strictEqual(snippet.toString(), '"this"');153154});155156test('TextmateSnippet, resolve variable with default', function () {157const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true);158assert.strictEqual(snippet.toString(), '"foo"');159snippet.resolveVariables(resolver);160assert.strictEqual(snippet.toString(), '"this"');161});162163test('More useful environment variables for snippets, #32737', function () {164165const disposables = new DisposableStore();166167assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'text');168169resolver = new ModelBasedVariableResolver(170labelService,171disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))172);173assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'ghi');174175resolver = new ModelBasedVariableResolver(176labelService,177disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:.git')))178);179assertVariableResolve(resolver, 'TM_FILENAME_BASE', '.git');180181resolver = new ModelBasedVariableResolver(182labelService,183disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:foo.')))184);185assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'foo');186187disposables.dispose();188});189190191function assertVariableResolve2(input: string, expected: string, varValue?: string) {192const snippet = new SnippetParser().parse(input)193.resolveVariables({ resolve(variable) { return varValue || variable.name; } });194195const actual = snippet.toString();196assert.strictEqual(actual, expected);197}198199test('Variable Snippet Transform', function () {200201const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true);202snippet.resolveVariables(resolver);203assert.strictEqual(snippet.toString(), 'name=text');204205assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var');206assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t');207assertVariableResolve2('${Foo/(.*)/${1:+Bar}/img}', 'Bar');208209//https://github.com/microsoft/vscode/issues/33162210assertVariableResolve2('export default class ${TM_FILENAME/(\\w+)\\.js/$1/g}', 'export default class FooFile', 'FooFile.js');211212assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/g}', 'FARbarFARbar'); // global213assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/}', 'FARbarfoobar'); // first match214assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', 'foobarfoobar'); // no match, no else215// assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', ''); // no match216217assertVariableResolve2('${foobarfoobar/(foo)/${2:+FAR}/g}', 'barbar'); // bad group reference218});219220test('Snippet transforms do not handle regex with alternatives or optional matches, #36089', function () {221222assertVariableResolve2(223'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',224'MyClass',225'my-class.js'226);227228// no hyphens229assertVariableResolve2(230'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',231'Myclass',232'myclass.js'233);234235// none matching suffix236assertVariableResolve2(237'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',238'Myclass.foo',239'myclass.foo'240);241242// more than one hyphen243assertVariableResolve2(244'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',245'ThisIsAFile',246'this-is-a-file.js'247);248249// KEBAB CASE250assertVariableResolve2(251'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',252'capital-case',253'CapitalCase'254);255256assertVariableResolve2(257'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',258'capital-case-more',259'CapitalCaseMore'260);261});262263test('Add variable to insert value from clipboard to a snippet #40153', function () {264265assertVariableResolve(new ClipboardBasedVariableResolver(() => undefined, 1, 0, true), 'CLIPBOARD', undefined);266267assertVariableResolve(new ClipboardBasedVariableResolver(() => null!, 1, 0, true), 'CLIPBOARD', undefined);268269assertVariableResolve(new ClipboardBasedVariableResolver(() => '', 1, 0, true), 'CLIPBOARD', undefined);270271assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'CLIPBOARD', 'foo');272273assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'foo', undefined);274assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'cLIPBOARD', undefined);275});276277test('Add variable to insert value from clipboard to a snippet #40153, 2', function () {278279assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1', 1, 2, true), 'CLIPBOARD', 'line1');280assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2\nline3', 1, 2, true), 'CLIPBOARD', 'line1\nline2\nline3');281282assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 1, 2, true), 'CLIPBOARD', 'line2');283resolver = new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true);284assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true), 'CLIPBOARD', 'line1');285286assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, false), 'CLIPBOARD', 'line1\nline2');287});288289290function assertVariableResolve3(resolver: VariableResolver, varName: string) {291const snippet = new SnippetParser().parse(`$${varName}`);292const variable = <Variable>snippet.children[0];293294assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`);295}296297test('Add time variables for snippets #41631, #43140', function () {298299const resolver = new TimeBasedVariableResolver;300301assertVariableResolve3(resolver, 'CURRENT_YEAR');302assertVariableResolve3(resolver, 'CURRENT_YEAR_SHORT');303assertVariableResolve3(resolver, 'CURRENT_MONTH');304assertVariableResolve3(resolver, 'CURRENT_DATE');305assertVariableResolve3(resolver, 'CURRENT_HOUR');306assertVariableResolve3(resolver, 'CURRENT_MINUTE');307assertVariableResolve3(resolver, 'CURRENT_SECOND');308assertVariableResolve3(resolver, 'CURRENT_DAY_NAME');309assertVariableResolve3(resolver, 'CURRENT_DAY_NAME_SHORT');310assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME');311assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME_SHORT');312assertVariableResolve3(resolver, 'CURRENT_SECONDS_UNIX');313assertVariableResolve3(resolver, 'CURRENT_TIMEZONE_OFFSET');314});315316test('Time-based snippet variables resolve to the same values even as time progresses', async function () {317const snippetText = `318$CURRENT_YEAR319$CURRENT_YEAR_SHORT320$CURRENT_MONTH321$CURRENT_DATE322$CURRENT_HOUR323$CURRENT_MINUTE324$CURRENT_SECOND325$CURRENT_DAY_NAME326$CURRENT_DAY_NAME_SHORT327$CURRENT_MONTH_NAME328$CURRENT_MONTH_NAME_SHORT329$CURRENT_SECONDS_UNIX330$CURRENT_TIMEZONE_OFFSET331`;332333const clock = sinon.useFakeTimers();334try {335const resolver = new TimeBasedVariableResolver;336337const firstResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);338clock.tick((365 * 24 * 3600 * 1000) + (24 * 3600 * 1000) + (3661 * 1000)); // 1 year + 1 day + 1 hour + 1 minute + 1 second339const secondResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);340341assert.strictEqual(firstResolve.toString(), secondResolve.toString(), `Time-based snippet variables resolved differently`);342} finally {343clock.restore();344}345});346347test('creating snippet - format-condition doesn\'t work #53617', function () {348349const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true);350snippet.resolveVariables({ resolve() { return '10'; } });351assert.strictEqual(snippet.toString(), 'It is line 10');352353snippet.resolveVariables({ resolve() { return '11'; } });354assert.strictEqual(snippet.toString(), 'It is not line 10');355});356357test('Add workspace name and folder variables for snippets #68261', function () {358359let workspace: IWorkspace;360const workspaceService = new class implements IWorkspaceContextService {361declare readonly _serviceBrand: undefined;362_throw = () => { throw new Error(); };363onDidChangeWorkbenchState = this._throw;364onDidChangeWorkspaceName = this._throw;365onWillChangeWorkspaceFolders = this._throw;366onDidChangeWorkspaceFolders = this._throw;367getCompleteWorkspace = this._throw;368getWorkspace(): IWorkspace { return workspace; }369getWorkbenchState = this._throw;370getWorkspaceFolder = this._throw;371isCurrentWorkspace = this._throw;372isInsideWorkspace = this._throw;373};374375const resolver = new WorkspaceBasedVariableResolver(workspaceService);376377// empty workspace378workspace = new Workspace('');379assertVariableResolve(resolver, 'WORKSPACE_NAME', undefined);380assertVariableResolve(resolver, 'WORKSPACE_FOLDER', undefined);381382// single folder workspace without config383workspace = new Workspace('', [toWorkspaceFolder(URI.file('/folderName'))]);384assertVariableResolve(resolver, 'WORKSPACE_NAME', 'folderName');385if (!isWindows) {386assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/folderName');387}388389// workspace with config390const workspaceConfigPath = URI.file('testWorkspace.code-workspace');391workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath);392assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace');393if (!isWindows) {394assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/');395}396});397398test('Add RELATIVE_FILEPATH snippet variable #114208', function () {399400let resolver: VariableResolver;401402// Mock a label service (only coded for file uris)403const workspaceLabelService = ((rootPath: string): ILabelService => {404const labelService = new class extends mock<ILabelService>() {405override getUriLabel(uri: URI, options: { relative?: boolean } = {}) {406const rootFsPath = URI.file(rootPath).fsPath + sep;407const fsPath = uri.fsPath;408if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) {409return fsPath.substring(rootFsPath.length);410}411return fsPath;412}413};414return labelService;415});416417const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt'));418419// empty workspace420resolver = new ModelBasedVariableResolver(421workspaceLabelService(''),422model423);424425if (!isWindows) {426assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt');427} else {428assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt');429}430431// single folder workspace432resolver = new ModelBasedVariableResolver(433workspaceLabelService('/foo'),434model435);436if (!isWindows) {437assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt');438} else {439assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt');440}441442model.dispose();443});444});445446447