Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetVariables.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*--------------------------------------------------------------------------------------------*/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_FILEPATH': true,44'CURSOR_INDEX': true, // 0-offset45'CURSOR_NUMBER': true, // 1-offset46'RELATIVE_FILEPATH': true,47'BLOCK_COMMENT_START': true,48'BLOCK_COMMENT_END': true,49'LINE_COMMENT': true,50'WORKSPACE_NAME': true,51'WORKSPACE_FOLDER': true,52'RANDOM': true,53'RANDOM_HEX': true,54'UUID': true55});5657export class CompositeSnippetVariableResolver implements VariableResolver {5859constructor(private readonly _delegates: VariableResolver[]) {60//61}6263resolve(variable: Variable): string | undefined {64for (const delegate of this._delegates) {65const value = delegate.resolve(variable);66if (value !== undefined) {67return value;68}69}70return undefined;71}72}7374export class SelectionBasedVariableResolver implements VariableResolver {7576constructor(77private readonly _model: ITextModel,78private readonly _selection: Selection,79private readonly _selectionIdx: number,80private readonly _overtypingCapturer: OvertypingCapturer | undefined81) {82//83}8485resolve(variable: Variable): string | undefined {8687const { name } = variable;8889if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') {90let value = this._model.getValueInRange(this._selection) || undefined;91let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber;9293// If there was no selected text, try to get last overtyped text94if (!value && this._overtypingCapturer) {95const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx);96if (info) {97value = info.value;98isMultiline = info.multiline;99}100}101102if (value && isMultiline && variable.snippet) {103// Selection is a multiline string which we indentation we now104// need to adjust. We compare the indentation of this variable105// with the indentation at the editor position and add potential106// extra indentation to the value107108const line = this._model.getLineContent(this._selection.startLineNumber);109const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1);110111let varLeadingWhitespace = lineLeadingWhitespace;112variable.snippet.walk(marker => {113if (marker === variable) {114return false;115}116if (marker instanceof Text) {117varLeadingWhitespace = getLeadingWhitespace(splitLines(marker.value).pop()!);118}119return true;120});121const whitespaceCommonLength = commonPrefixLength(varLeadingWhitespace, lineLeadingWhitespace);122123value = value.replace(124/(\r\n|\r|\n)(.*)/g,125(m, newline, rest) => `${newline}${varLeadingWhitespace.substr(whitespaceCommonLength)}${rest}`126);127}128return value;129130} else if (name === 'TM_CURRENT_LINE') {131return this._model.getLineContent(this._selection.positionLineNumber);132133} else if (name === 'TM_CURRENT_WORD') {134const info = this._model.getWordAtPosition({135lineNumber: this._selection.positionLineNumber,136column: this._selection.positionColumn137});138return info && info.word || undefined;139140} else if (name === 'TM_LINE_INDEX') {141return String(this._selection.positionLineNumber - 1);142143} else if (name === 'TM_LINE_NUMBER') {144return String(this._selection.positionLineNumber);145146} else if (name === 'CURSOR_INDEX') {147return String(this._selectionIdx);148149} else if (name === 'CURSOR_NUMBER') {150return String(this._selectionIdx + 1);151}152return undefined;153}154}155156export class ModelBasedVariableResolver implements VariableResolver {157158constructor(159private readonly _labelService: ILabelService,160private readonly _model: ITextModel161) {162//163}164165resolve(variable: Variable): string | undefined {166167const { name } = variable;168169if (name === 'TM_FILENAME') {170return path.basename(this._model.uri.fsPath);171172} else if (name === 'TM_FILENAME_BASE') {173const name = path.basename(this._model.uri.fsPath);174const idx = name.lastIndexOf('.');175if (idx <= 0) {176return name;177} else {178return name.slice(0, idx);179}180181} else if (name === 'TM_DIRECTORY') {182if (path.dirname(this._model.uri.fsPath) === '.') {183return '';184}185return this._labelService.getUriLabel(dirname(this._model.uri));186187} else if (name === 'TM_FILEPATH') {188return this._labelService.getUriLabel(this._model.uri);189} else if (name === 'RELATIVE_FILEPATH') {190return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true });191}192193return undefined;194}195}196197export interface IReadClipboardText {198(): string | undefined;199}200201export class ClipboardBasedVariableResolver implements VariableResolver {202203constructor(204private readonly _readClipboardText: IReadClipboardText,205private readonly _selectionIdx: number,206private readonly _selectionCount: number,207private readonly _spread: boolean208) {209//210}211212resolve(variable: Variable): string | undefined {213if (variable.name !== 'CLIPBOARD') {214return undefined;215}216217const clipboardText = this._readClipboardText();218if (!clipboardText) {219return undefined;220}221222// `spread` is assigning each cursor a line of the clipboard223// text whenever there the line count equals the cursor count224// and when enabled225if (this._spread) {226const lines = clipboardText.split(/\r\n|\n|\r/).filter(s => !isFalsyOrWhitespace(s));227if (lines.length === this._selectionCount) {228return lines[this._selectionIdx];229}230}231return clipboardText;232}233}234export class CommentBasedVariableResolver implements VariableResolver {235constructor(236private readonly _model: ITextModel,237private readonly _selection: Selection,238@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService239) {240//241}242resolve(variable: Variable): string | undefined {243const { name } = variable;244const langId = this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber, this._selection.selectionStartColumn);245const config = this._languageConfigurationService.getLanguageConfiguration(langId).comments;246if (!config) {247return undefined;248}249if (name === 'LINE_COMMENT') {250return config.lineCommentToken || undefined;251} else if (name === 'BLOCK_COMMENT_START') {252return config.blockCommentStartToken || undefined;253} else if (name === 'BLOCK_COMMENT_END') {254return config.blockCommentEndToken || undefined;255}256return undefined;257}258}259export class TimeBasedVariableResolver implements VariableResolver {260261private 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")];262private 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")];263private 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")];264private 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")];265266private readonly _date = new Date();267268resolve(variable: Variable): string | undefined {269const { name } = variable;270271if (name === 'CURRENT_YEAR') {272return String(this._date.getFullYear());273} else if (name === 'CURRENT_YEAR_SHORT') {274return String(this._date.getFullYear()).slice(-2);275} else if (name === 'CURRENT_MONTH') {276return String(this._date.getMonth().valueOf() + 1).padStart(2, '0');277} else if (name === 'CURRENT_DATE') {278return String(this._date.getDate().valueOf()).padStart(2, '0');279} else if (name === 'CURRENT_HOUR') {280return String(this._date.getHours().valueOf()).padStart(2, '0');281} else if (name === 'CURRENT_MINUTE') {282return String(this._date.getMinutes().valueOf()).padStart(2, '0');283} else if (name === 'CURRENT_SECOND') {284return String(this._date.getSeconds().valueOf()).padStart(2, '0');285} else if (name === 'CURRENT_DAY_NAME') {286return TimeBasedVariableResolver.dayNames[this._date.getDay()];287} else if (name === 'CURRENT_DAY_NAME_SHORT') {288return TimeBasedVariableResolver.dayNamesShort[this._date.getDay()];289} else if (name === 'CURRENT_MONTH_NAME') {290return TimeBasedVariableResolver.monthNames[this._date.getMonth()];291} else if (name === 'CURRENT_MONTH_NAME_SHORT') {292return TimeBasedVariableResolver.monthNamesShort[this._date.getMonth()];293} else if (name === 'CURRENT_SECONDS_UNIX') {294return String(Math.floor(this._date.getTime() / 1000));295} else if (name === 'CURRENT_TIMEZONE_OFFSET') {296const rawTimeOffset = this._date.getTimezoneOffset();297const sign = rawTimeOffset > 0 ? '-' : '+';298const hours = Math.trunc(Math.abs(rawTimeOffset / 60));299const hoursString = (hours < 10 ? '0' + hours : hours);300const minutes = Math.abs(rawTimeOffset) - hours * 60;301const minutesString = (minutes < 10 ? '0' + minutes : minutes);302return sign + hoursString + ':' + minutesString;303}304305return undefined;306}307}308309export class WorkspaceBasedVariableResolver implements VariableResolver {310constructor(311private readonly _workspaceService: IWorkspaceContextService | undefined,312) {313//314}315316resolve(variable: Variable): string | undefined {317if (!this._workspaceService) {318return undefined;319}320321const workspaceIdentifier = toWorkspaceIdentifier(this._workspaceService.getWorkspace());322if (isEmptyWorkspaceIdentifier(workspaceIdentifier)) {323return undefined;324}325326if (variable.name === 'WORKSPACE_NAME') {327return this._resolveWorkspaceName(workspaceIdentifier);328} else if (variable.name === 'WORKSPACE_FOLDER') {329return this._resoveWorkspacePath(workspaceIdentifier);330}331332return undefined;333}334private _resolveWorkspaceName(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {335if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {336return path.basename(workspaceIdentifier.uri.path);337}338339let filename = path.basename(workspaceIdentifier.configPath.path);340if (filename.endsWith(WORKSPACE_EXTENSION)) {341filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);342}343return filename;344}345private _resoveWorkspacePath(workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {346if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {347return normalizeDriveLetter(workspaceIdentifier.uri.fsPath);348}349350const filename = path.basename(workspaceIdentifier.configPath.path);351let folderpath = workspaceIdentifier.configPath.fsPath;352if (folderpath.endsWith(filename)) {353folderpath = folderpath.substr(0, folderpath.length - filename.length - 1);354}355return (folderpath ? normalizeDriveLetter(folderpath) : '/');356}357}358359export class RandomBasedVariableResolver implements VariableResolver {360resolve(variable: Variable): string | undefined {361const { name } = variable;362363if (name === 'RANDOM') {364return Math.random().toString().slice(-6);365} else if (name === 'RANDOM_HEX') {366return Math.random().toString(16).slice(-6);367} else if (name === 'UUID') {368return generateUuid();369}370371return undefined;372}373}374375376