Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetVariables.test.ts
5310 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_DIRECTORY_BASE', 'files');79assertVariableResolve(resolver, 'TM_FILEPATH', '/foo/files/text.txt');80}8182resolver = new ModelBasedVariableResolver(83labelService,84disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))85);86assertVariableResolve(resolver, 'TM_FILENAME', 'ghi');87if (!isWindows) {88assertVariableResolve(resolver, 'TM_DIRECTORY', '/abc/def');89assertVariableResolve(resolver, 'TM_DIRECTORY_BASE', 'def');90assertVariableResolve(resolver, 'TM_FILEPATH', '/abc/def/ghi');91}9293resolver = new ModelBasedVariableResolver(94labelService,95disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:fff.ts')))96);97assertVariableResolve(resolver, 'TM_DIRECTORY', '');98assertVariableResolve(resolver, 'TM_DIRECTORY_BASE', '');99assertVariableResolve(resolver, 'TM_FILEPATH', 'fff.ts');100101disposables.dispose();102});103104test('Path delimiters in code snippet variables aren\'t specific to remote OS #76840', function () {105106const labelService = new class extends mock<ILabelService>() {107override getUriLabel(uri: URI) {108return uri.fsPath.replace(/\/|\\/g, '|');109}110};111112const model = createTextModel([].join('\n'), undefined, undefined, URI.parse('foo:///foo/files/text.txt'));113114const resolver = new CompositeSnippetVariableResolver([new ModelBasedVariableResolver(labelService, model)]);115116assertVariableResolve(resolver, 'TM_FILEPATH', '|foo|files|text.txt');117118model.dispose();119});120121test('editor variables, selection', function () {122123resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0, undefined);124assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');125assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two');126assertVariableResolve(resolver, 'TM_LINE_INDEX', '1');127assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2');128assertVariableResolve(resolver, 'CURSOR_INDEX', '0');129assertVariableResolve(resolver, 'CURSOR_NUMBER', '1');130131resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 4, undefined);132assertVariableResolve(resolver, 'CURSOR_INDEX', '4');133assertVariableResolve(resolver, 'CURSOR_NUMBER', '5');134135resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0, undefined);136assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');137assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one');138assertVariableResolve(resolver, 'TM_LINE_INDEX', '0');139assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1');140141resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0, undefined);142assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined);143144assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this');145146resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0, undefined);147assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined);148149});150151test('TextmateSnippet, resolve variable', function () {152const snippet = new SnippetParser().parse('"$TM_CURRENT_WORD"', true);153assert.strictEqual(snippet.toString(), '""');154snippet.resolveVariables(resolver);155assert.strictEqual(snippet.toString(), '"this"');156157});158159test('TextmateSnippet, resolve variable with default', function () {160const snippet = new SnippetParser().parse('"${TM_CURRENT_WORD:foo}"', true);161assert.strictEqual(snippet.toString(), '"foo"');162snippet.resolveVariables(resolver);163assert.strictEqual(snippet.toString(), '"this"');164});165166test('More useful environment variables for snippets, #32737', function () {167168const disposables = new DisposableStore();169170assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'text');171172resolver = new ModelBasedVariableResolver(173labelService,174disposables.add(createTextModel('', undefined, undefined, URI.parse('http://www.pb.o/abc/def/ghi')))175);176assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'ghi');177178resolver = new ModelBasedVariableResolver(179labelService,180disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:.git')))181);182assertVariableResolve(resolver, 'TM_FILENAME_BASE', '.git');183184resolver = new ModelBasedVariableResolver(185labelService,186disposables.add(createTextModel('', undefined, undefined, URI.parse('mem:foo.')))187);188assertVariableResolve(resolver, 'TM_FILENAME_BASE', 'foo');189190disposables.dispose();191});192193194function assertVariableResolve2(input: string, expected: string, varValue?: string) {195const snippet = new SnippetParser().parse(input)196.resolveVariables({ resolve(variable) { return varValue || variable.name; } });197198const actual = snippet.toString();199assert.strictEqual(actual, expected);200}201202test('Variable Snippet Transform', function () {203204const snippet = new SnippetParser().parse('name=${TM_FILENAME/(.*)\\..+$/$1/}', true);205snippet.resolveVariables(resolver);206assert.strictEqual(snippet.toString(), 'name=text');207208assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2/}', 'Var');209assertVariableResolve2('${ThisIsAVar/([A-Z]).*(Var)/$2-${1:/downcase}/}', 'Var-t');210assertVariableResolve2('${Foo/(.*)/${1:+Bar}/img}', 'Bar');211212//https://github.com/microsoft/vscode/issues/33162213assertVariableResolve2('export default class ${TM_FILENAME/(\\w+)\\.js/$1/g}', 'export default class FooFile', 'FooFile.js');214215assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/g}', 'FARbarFARbar'); // global216assertVariableResolve2('${foobarfoobar/(foo)/${1:+FAR}/}', 'FARbarfoobar'); // first match217assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', 'foobarfoobar'); // no match, no else218// assertVariableResolve2('${foobarfoobar/(bazz)/${1:+FAR}/g}', ''); // no match219220assertVariableResolve2('${foobarfoobar/(foo)/${2:+FAR}/g}', 'barbar'); // bad group reference221});222223test('Snippet transforms do not handle regex with alternatives or optional matches, #36089', function () {224225assertVariableResolve2(226'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',227'MyClass',228'my-class.js'229);230231// no hyphens232assertVariableResolve2(233'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',234'Myclass',235'myclass.js'236);237238// none matching suffix239assertVariableResolve2(240'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',241'Myclass.foo',242'myclass.foo'243);244245// more than one hyphen246assertVariableResolve2(247'${TM_FILENAME/^(.)|(?:-(.))|(\\.js)/${1:/upcase}${2:/upcase}/g}',248'ThisIsAFile',249'this-is-a-file.js'250);251252// KEBAB CASE253assertVariableResolve2(254'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',255'capital-case',256'CapitalCase'257);258259assertVariableResolve2(260'${TM_FILENAME_BASE/([A-Z][a-z]+)([A-Z][a-z]+$)?/${1:/downcase}-${2:/downcase}/g}',261'capital-case-more',262'CapitalCaseMore'263);264});265266test('Add variable to insert value from clipboard to a snippet #40153', function () {267268assertVariableResolve(new ClipboardBasedVariableResolver(() => undefined, 1, 0, true), 'CLIPBOARD', undefined);269270assertVariableResolve(new ClipboardBasedVariableResolver(() => null!, 1, 0, true), 'CLIPBOARD', undefined);271272assertVariableResolve(new ClipboardBasedVariableResolver(() => '', 1, 0, true), 'CLIPBOARD', undefined);273274assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'CLIPBOARD', 'foo');275276assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'foo', undefined);277assertVariableResolve(new ClipboardBasedVariableResolver(() => 'foo', 1, 0, true), 'cLIPBOARD', undefined);278});279280test('Add variable to insert value from clipboard to a snippet #40153, 2', function () {281282assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1', 1, 2, true), 'CLIPBOARD', 'line1');283assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2\nline3', 1, 2, true), 'CLIPBOARD', 'line1\nline2\nline3');284285assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 1, 2, true), 'CLIPBOARD', 'line2');286resolver = new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true);287assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, true), 'CLIPBOARD', 'line1');288289assertVariableResolve(new ClipboardBasedVariableResolver(() => 'line1\nline2', 0, 2, false), 'CLIPBOARD', 'line1\nline2');290});291292293function assertVariableResolve3(resolver: VariableResolver, varName: string) {294const snippet = new SnippetParser().parse(`$${varName}`);295const variable = <Variable>snippet.children[0];296297assert.strictEqual(variable.resolve(resolver), true, `${varName} failed to resolve`);298}299300test('Add time variables for snippets #41631, #43140', function () {301302const resolver = new TimeBasedVariableResolver;303304assertVariableResolve3(resolver, 'CURRENT_YEAR');305assertVariableResolve3(resolver, 'CURRENT_YEAR_SHORT');306assertVariableResolve3(resolver, 'CURRENT_MONTH');307assertVariableResolve3(resolver, 'CURRENT_DATE');308assertVariableResolve3(resolver, 'CURRENT_HOUR');309assertVariableResolve3(resolver, 'CURRENT_MINUTE');310assertVariableResolve3(resolver, 'CURRENT_SECOND');311assertVariableResolve3(resolver, 'CURRENT_DAY_NAME');312assertVariableResolve3(resolver, 'CURRENT_DAY_NAME_SHORT');313assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME');314assertVariableResolve3(resolver, 'CURRENT_MONTH_NAME_SHORT');315assertVariableResolve3(resolver, 'CURRENT_SECONDS_UNIX');316assertVariableResolve3(resolver, 'CURRENT_TIMEZONE_OFFSET');317});318319test('Time-based snippet variables resolve to the same values even as time progresses', async function () {320const snippetText = `321$CURRENT_YEAR322$CURRENT_YEAR_SHORT323$CURRENT_MONTH324$CURRENT_DATE325$CURRENT_HOUR326$CURRENT_MINUTE327$CURRENT_SECOND328$CURRENT_DAY_NAME329$CURRENT_DAY_NAME_SHORT330$CURRENT_MONTH_NAME331$CURRENT_MONTH_NAME_SHORT332$CURRENT_SECONDS_UNIX333$CURRENT_TIMEZONE_OFFSET334`;335336const clock = sinon.useFakeTimers();337try {338const resolver = new TimeBasedVariableResolver;339340const firstResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);341clock.tick((365 * 24 * 3600 * 1000) + (24 * 3600 * 1000) + (3661 * 1000)); // 1 year + 1 day + 1 hour + 1 minute + 1 second342const secondResolve = new SnippetParser().parse(snippetText).resolveVariables(resolver);343344assert.strictEqual(firstResolve.toString(), secondResolve.toString(), `Time-based snippet variables resolved differently`);345} finally {346clock.restore();347}348});349350test('creating snippet - format-condition doesn\'t work #53617', function () {351352const snippet = new SnippetParser().parse('${TM_LINE_NUMBER/(10)/${1:?It is:It is not}/} line 10', true);353snippet.resolveVariables({ resolve() { return '10'; } });354assert.strictEqual(snippet.toString(), 'It is line 10');355356snippet.resolveVariables({ resolve() { return '11'; } });357assert.strictEqual(snippet.toString(), 'It is not line 10');358});359360test('Add workspace name and folder variables for snippets #68261', function () {361362let workspace: IWorkspace;363const workspaceService = new class implements IWorkspaceContextService {364declare readonly _serviceBrand: undefined;365_throw = () => { throw new Error(); };366onDidChangeWorkbenchState = this._throw;367onDidChangeWorkspaceName = this._throw;368onWillChangeWorkspaceFolders = this._throw;369onDidChangeWorkspaceFolders = this._throw;370getCompleteWorkspace = this._throw;371getWorkspace(): IWorkspace { return workspace; }372getWorkbenchState = this._throw;373getWorkspaceFolder = this._throw;374isCurrentWorkspace = this._throw;375isInsideWorkspace = this._throw;376};377378const resolver = new WorkspaceBasedVariableResolver(workspaceService);379380// empty workspace381workspace = new Workspace('');382assertVariableResolve(resolver, 'WORKSPACE_NAME', undefined);383assertVariableResolve(resolver, 'WORKSPACE_FOLDER', undefined);384385// single folder workspace without config386workspace = new Workspace('', [toWorkspaceFolder(URI.file('/folderName'))]);387assertVariableResolve(resolver, 'WORKSPACE_NAME', 'folderName');388if (!isWindows) {389assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/folderName');390}391392// workspace with config393const workspaceConfigPath = URI.file('testWorkspace.code-workspace');394workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath);395assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace');396if (!isWindows) {397assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/');398}399});400401test('Add RELATIVE_FILEPATH snippet variable #114208', function () {402403let resolver: VariableResolver;404405// Mock a label service (only coded for file uris)406const workspaceLabelService = ((rootPath: string): ILabelService => {407const labelService = new class extends mock<ILabelService>() {408override getUriLabel(uri: URI, options: { relative?: boolean } = {}) {409const rootFsPath = URI.file(rootPath).fsPath + sep;410const fsPath = uri.fsPath;411if (options.relative && rootPath && fsPath.startsWith(rootFsPath)) {412return fsPath.substring(rootFsPath.length);413}414return fsPath;415}416};417return labelService;418});419420const model = createTextModel('', undefined, undefined, URI.parse('file:///foo/files/text.txt'));421422// empty workspace423resolver = new ModelBasedVariableResolver(424workspaceLabelService(''),425model426);427428if (!isWindows) {429assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '/foo/files/text.txt');430} else {431assertVariableResolve(resolver, 'RELATIVE_FILEPATH', '\\foo\\files\\text.txt');432}433434// single folder workspace435resolver = new ModelBasedVariableResolver(436workspaceLabelService('/foo'),437model438);439if (!isWindows) {440assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files/text.txt');441} else {442assertVariableResolve(resolver, 'RELATIVE_FILEPATH', 'files\\text.txt');443}444445model.dispose();446});447});448449450