Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetVariables.ts
5311 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*--------------------------------------------------------------------------------------------*/45import { normalizeDriveLetter } from '../../../../base/common/labels.js';6import * as path from '../../../../base/common/path.js';7import { dirname } from '../../../../base/common/resources.js';8import { commonPrefixLength, getLeadingWhitespace, isFalsyOrWhitespace, splitLines } from '../../../../base/common/strings.js';9import { generateUuid } from '../../../../base/common/uuid.js';10import { Selection } from '../../../common/core/selection.js';11import { ITextModel } from '../../../common/model.js';12import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';13import { Text, Variable, VariableResolver } from './snippetParser.js';14import { OvertypingCapturer } from '../../suggest/browser/suggestOvertypingCapturer.js';15import * as nls from '../../../../nls.js';16import { ILabelService } from '../../../../platform/label/common/label.js';17import { WORKSPACE_EXTENSION, isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspaceContextService, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, isEmptyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';1819export const KnownSnippetVariableNames = Object.freeze<{ [key: string]: true }>({20'CURRENT_YEAR': true,21'CURRENT_YEAR_SHORT': true,22'CURRENT_MONTH': true,23'CURRENT_DATE': true,24'CURRENT_HOUR': true,25'CURRENT_MINUTE': true,26'CURRENT_SECOND': true,27'CURRENT_DAY_NAME': true,28'CURRENT_DAY_NAME_SHORT': true,29'CURRENT_MONTH_NAME': true,30'CURRENT_MONTH_NAME_SHORT': true,31'CURRENT_SECONDS_UNIX': true,32'CURRENT_TIMEZONE_OFFSET': true,33'SELECTION': true,34'CLIPBOARD': true,35'TM_SELECTED_TEXT': true,36'TM_CURRENT_LINE': true,37'TM_CURRENT_WORD': true,38'TM_LINE_INDEX': true,39'TM_LINE_NUMBER': true,40'TM_FILENAME': true,41'TM_FILENAME_BASE': true,42'TM_DIRECTORY': true,43'TM_DIRECTORY_BASE': true,44'TM_FILEPATH': true,45'CURSOR_INDEX': true, // 0-offset46'CURSOR_NUMBER': true, // 1-offset47'RELATIVE_FILEPATH': true,48'BLOCK_COMMENT_START': true,49'BLOCK_COMMENT_END': true,50'LINE_COMMENT': true,51'WORKSPACE_NAME': true,52'WORKSPACE_FOLDER': true,53'RANDOM': true,54'RANDOM_HEX': true,55'UUID': true56});5758export class CompositeSnippetVariableResolver implements VariableResolver {5960constructor(private readonly _delegates: VariableResolver[]) {61//62}6364resolve(variable: Variable): string | undefined {65for (const delegate of this._delegates) {66const value = delegate.resolve(variable);67if (value !== undefined) {68return value;69}70}71return undefined;72}73}7475export class SelectionBasedVariableResolver implements VariableResolver {7677constructor(78private readonly _model: ITextModel,79private readonly _selection: Selection,80private readonly _selectionIdx: number,81private readonly _overtypingCapturer: OvertypingCapturer | undefined82) {83//84}8586resolve(variable: Variable): string | undefined {8788const { name } = variable;8990if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') {91let value = this._model.getValueInRange(this._selection) || undefined;92let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber;9394// If there was no selected text, try to get last overtyped text95if (!value && this._overtypingCapturer) {96const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx);97if (info) {98value = info.value;99isMultiline = info.multiline;100}101}102103if (value && isMultiline && variable.snippet) {104// Selection is a multiline string which we indentation we now105// need to adjust. We compare the indentation of this variable106// with the indentation at the editor position and add potential107// extra indentation to the value108109const line = this._model.getLineContent(this._selection.startLineNumber);110const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1);111112let varLeadingWhitespace = lineLeadingWhitespace;113variable.snippet.walk(marker => {114if (marker === variable) {115return false;116}117if (marker instanceof Text) {118varLeadingWhitespace = getLeadingWhitespace(splitLines(marker.value).pop()!);119}120return true;121});122const whitespaceCommonLength = commonPrefixLength(varLeadingWhitespace, lineLeadingWhitespace);123124value = value.replace(125/(\r\n|\r|\n)(.*)/g,126(m, newline, rest) => `${newline}${varLeadingWhitespace.substr(whitespaceCommonLength)}${rest}`127);128}129return value;130131} else if (name === 'TM_CURRENT_LINE') {132return this._model.getLineContent(this._selection.positionLineNumber);133134} else if (name === 'TM_CURRENT_WORD') {135const info = this._model.getWordAtPosition({136lineNumber: this._selection.positionLineNumber,137column: this._selection.positionColumn138});139return info && info.word || undefined;140141} else if (name === 'TM_LINE_INDEX') {142return String(this._selection.positionLineNumber - 1);143144} else if (name === 'TM_LINE_NUMBER') {145return String(this._selection.positionLineNumber);146147} else if (name === 'CURSOR_INDEX') {148return String(this._selectionIdx);149150} else if (name === 'CURSOR_NUMBER') {151return String(this._selectionIdx + 1);152}153return undefined;154}155}156157export class ModelBasedVariableResolver implements VariableResolver {158159constructor(160private readonly _labelService: ILabelService,161private readonly _model: ITextModel162) {163//164}165166resolve(variable: Variable): string | undefined {167168const { name } = variable;169170if (name === 'TM_FILENAME') {171return path.basename(this._model.uri.fsPath);172173} else if (name === 'TM_FILENAME_BASE') {174const name = path.basename(this._model.uri.fsPath);175const idx = name.lastIndexOf('.');176if (idx <= 0) {177return name;178} else {179return name.slice(0, idx);180}181182} else if (name === 'TM_DIRECTORY') {183if (path.dirname(this._model.uri.fsPath) === '.') {184return '';185}186return this._labelService.getUriLabel(dirname(this._model.uri));187188} else if (name === 'TM_DIRECTORY_BASE') {189if (path.dirname(this._model.uri.fsPath) === '.') {190return '';191}192return path.basename(path.dirname(this._model.uri.fsPath));193194} else if (name === 'TM_FILEPATH') {195return this._labelService.getUriLabel(this._model.uri);196} else if (name === 'RELATIVE_FILEPATH') {197return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true });198}199200return undefined;201}202}203204export interface IReadClipboardText {205(): string | undefined;206}207208export class ClipboardBasedVariableResolver implements VariableResolver {209210constructor(211private readonly _readClipboardText: IReadClipboardText,212private readonly _selectionIdx: number,213private readonly _selectionCount: number,214private readonly _spread: boolean215) {216//217}218219resolve(variable: Variable): string | undefined {220if (variable.name !== 'CLIPBOARD') {221return undefined;222}223224const clipboardText = this._readClipboardText();225if (!clipboardText) {226return undefined;227}228229// `spread` is assigning each cursor a line of the clipboard230// text whenever there the line count equals the cursor count231// and when enabled232if (this._spread) {233const lines = clipboardText.split(/\r\n|\n|\r/).filter(s => !isFalsyOrWhitespace(s));234if (lines.length === this._selectionCount) {235return lines[this._selectionIdx];236}237}238return clipboardText;239}240}241export class CommentBasedVariableResolver implements VariableResolver {242constructor(243private readonly _model: ITextModel,244private readonly _selection: Selection,245@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService246) {247//248}249resolve(variable: Variable): string | undefined {250const { name } = variable;251const langId = this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber, this._selection.selectionStartColumn);252const config = this._languageConfigurationService.getLanguageConfiguration(langId).comments;253if (!config) {254return undefined;255}256if (name === 'LINE_COMMENT') {257return config.lineCommentToken || undefined;258} else if (name === 'BLOCK_COMMENT_START') {259return config.blockCommentStartToken || undefined;260} else if (name === 'BLOCK_COMMENT_END') {261return config.blockCommentEndToken || undefined;262}263return undefined;264}265}266export class TimeBasedVariableResolver implements VariableResolver {267268private static readonly dayNames = [nls.localize('Sunday', "Sunday"), nls.localize('Monday', "Monday"), nls.localize('Tuesday', "Tuesday"), nls.localize('Wednesday', "Wednesday"), nls.localize('Thursday', "Thursday"), nls.localize('Friday', "Friday"), nls.localize('Saturday', "Saturday")];269private static readonly dayNamesShort = [nls.localize('SundayShort', "Sun"), nls.localize('MondayShort', "Mon"), nls.localize('TuesdayShort', "Tue"), nls.localize('WednesdayShort', "Wed"), nls.localize('ThursdayShort', "Thu"), nls.localize('FridayShort', "Fri"), nls.localize('SaturdayShort', "Sat")];270private static readonly monthNames = [nls.localize('January', "January"), nls.localize('February', "February"), nls.localize('March', "March"), nls.localize('April', "April"), nls.localize('May', "May"), nls.localize('June', "June"), nls.localize('July', "July"), nls.localize('August', "August"), nls.localize('September', "September"), nls.localize('October', "October"), nls.localize('November', "November"), nls.localize('December', "December")];271private static readonly monthNamesShort = [nls.localize('JanuaryShort', "Jan"), nls.localize('FebruaryShort', "Feb"), nls.localize('MarchShort', "Mar"), nls.localize('AprilShort', "Apr"), nls.localize('MayShort', "May"), nls.localize('JuneShort', "Jun"), nls.localize('JulyShort', "Jul"), nls.localize('AugustShort', "Aug"), nls.localize('SeptemberShort', "Sep"), nls.localize('OctoberShort', "Oct"), nls.localize('NovemberShort', "Nov"), nls.localize('DecemberShort', "Dec")];272273private readonly _date = new Date();274275resolve(variable: Variable): string | undefined {276const { name } = variable;277278if (name === 'CURRENT_YEAR') {279return String(this._date.getFullYear());280} else if (name === 'CURRENT_YEAR_SHORT') {281return String(this._date.getFullYear()).slice(-2);282} else if (name === 'CURRENT_MONTH') {283return String(this._date.getMonth().valueOf() + 1).padStart(2, '0');284} else if (name === 'CURRENT_DATE') {285return String(this._date.getDate().valueOf()).padStart(2, '0');286} else if (name === 'CURRENT_HOUR') {287return String(this._date.getHours().valueOf()).padStart(2, '0');288} else if (name === 'CURRENT_MINUTE') {289return String(this._date.getMinutes().valueOf()).padStart(2, '0');290} else if (name === 'CURRENT_SECOND') {291return String(this._date.getSeconds().valueOf()).padStart(2, '0');292} else if (name === 'CURRENT_DAY_NAME') {293return TimeBasedVariableResolver.dayNames[this._date.getDay()];294} else if (name === 'CURRENT_DAY_NAME_SHORT') {295return TimeBasedVariableResolver.dayNamesShort[this._date.getDay()];296} else if (name === 'CURRENT_MONTH_NAME') {297return TimeBasedVariableResolver.monthNames[this._date.getMonth()];298} else if (name === 'CURRENT_MONTH_NAME_SHORT') {299return TimeBasedVariableResolver.monthNamesShort[this._date.getMonth()];300} else if (name === 'CURRENT_SECONDS_UNIX') {301return String(Math.floor(this._date.getTime() / 1000));302} else if (name === 'CURRENT_TIMEZONE_OFFSET') {303const rawTimeOffset = this._date.getTimezoneOffset();304const sign = rawTimeOffset > 0 ? '-' : '+';305const hours = Math.trunc(Math.abs(rawTimeOffset / 60));306const hoursString = (hours < 10 ? '0' + hours : hours);307const minutes = Math.abs(rawTimeOffset) - hours * 60;308const minutesString = (minutes < 10 ? '0' + minutes : minutes);309return sign + hoursString + ':' + minutesString;310}311312return undefined;313}314}315316export class WorkspaceBasedVariableResolver implements VariableResolver {317constructor(318private readonly _workspaceService: IWorkspaceContextService | undefined,319) {320//321}322323resolve(variable: Variable): string | undefined {324if (!this._workspaceService) {325return undefined;326}327328const workspaceIdentifier = toWorkspaceIdentifier(this._workspaceService.getWorkspace());329if (isEmptyWorkspaceIdentifier(workspaceIdentifier)) {330return undefined;331}332333if (variable.name === 'WORKSPACE_NAME') {334return this._resolveWorkspaceName(workspaceIdentifier);335} else if (variable.name === 'WORKSPACE_FOLDER') {336return this._resoveWorkspacePath(workspaceIdentifier);337}338339return undefined;340}341private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {342if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {343return path.basename(workspaceIdentifier.uri.path);344}345346let filename = path.basename(workspaceIdentifier.configPath.path);347if (filename.endsWith(WORKSPACE_EXTENSION)) {348filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);349}350return filename;351}352private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {353if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {354return normalizeDriveLetter(workspaceIdentifier.uri.fsPath);355}356357const filename = path.basename(workspaceIdentifier.configPath.path);358let folderpath = workspaceIdentifier.configPath.fsPath;359if (folderpath.endsWith(filename)) {360folderpath = folderpath.substr(0, folderpath.length - filename.length - 1);361}362return (folderpath ? normalizeDriveLetter(folderpath) : '/');363}364}365366export class RandomBasedVariableResolver implements VariableResolver {367resolve(variable: Variable): string | undefined {368const { name } = variable;369370if (name === 'RANDOM') {371return Math.random().toString().slice(-6);372} else if (name === 'RANDOM_HEX') {373return Math.random().toString(16).slice(-6);374} else if (name === 'UUID') {375return generateUuid();376}377378return undefined;379}380}381382383