snippetVariables.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { normalizeDriveLetter } from '../../../base/common/labels.js';
  6. import * as path from '../../../base/common/path.js';
  7. import { dirname } from '../../../base/common/resources.js';
  8. import { commonPrefixLength, getLeadingWhitespace, isFalsyOrWhitespace, splitLines } from '../../../base/common/strings.js';
  9. import { generateUuid } from '../../../base/common/uuid.js';
  10. import { LanguageConfigurationRegistry } from '../../common/modes/languageConfigurationRegistry.js';
  11. import { Text } from './snippetParser.js';
  12. import * as nls from '../../../nls.js';
  13. import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXTENSION } from '../../../platform/workspaces/common/workspaces.js';
  14. export const KnownSnippetVariableNames = Object.freeze({
  15. 'CURRENT_YEAR': true,
  16. 'CURRENT_YEAR_SHORT': true,
  17. 'CURRENT_MONTH': true,
  18. 'CURRENT_DATE': true,
  19. 'CURRENT_HOUR': true,
  20. 'CURRENT_MINUTE': true,
  21. 'CURRENT_SECOND': true,
  22. 'CURRENT_DAY_NAME': true,
  23. 'CURRENT_DAY_NAME_SHORT': true,
  24. 'CURRENT_MONTH_NAME': true,
  25. 'CURRENT_MONTH_NAME_SHORT': true,
  26. 'CURRENT_SECONDS_UNIX': true,
  27. 'SELECTION': true,
  28. 'CLIPBOARD': true,
  29. 'TM_SELECTED_TEXT': true,
  30. 'TM_CURRENT_LINE': true,
  31. 'TM_CURRENT_WORD': true,
  32. 'TM_LINE_INDEX': true,
  33. 'TM_LINE_NUMBER': true,
  34. 'TM_FILENAME': true,
  35. 'TM_FILENAME_BASE': true,
  36. 'TM_DIRECTORY': true,
  37. 'TM_FILEPATH': true,
  38. 'RELATIVE_FILEPATH': true,
  39. 'BLOCK_COMMENT_START': true,
  40. 'BLOCK_COMMENT_END': true,
  41. 'LINE_COMMENT': true,
  42. 'WORKSPACE_NAME': true,
  43. 'WORKSPACE_FOLDER': true,
  44. 'RANDOM': true,
  45. 'RANDOM_HEX': true,
  46. 'UUID': true
  47. });
  48. export class CompositeSnippetVariableResolver {
  49. constructor(_delegates) {
  50. this._delegates = _delegates;
  51. //
  52. }
  53. resolve(variable) {
  54. for (const delegate of this._delegates) {
  55. let value = delegate.resolve(variable);
  56. if (value !== undefined) {
  57. return value;
  58. }
  59. }
  60. return undefined;
  61. }
  62. }
  63. export class SelectionBasedVariableResolver {
  64. constructor(_model, _selection, _selectionIdx, _overtypingCapturer) {
  65. this._model = _model;
  66. this._selection = _selection;
  67. this._selectionIdx = _selectionIdx;
  68. this._overtypingCapturer = _overtypingCapturer;
  69. //
  70. }
  71. resolve(variable) {
  72. const { name } = variable;
  73. if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') {
  74. let value = this._model.getValueInRange(this._selection) || undefined;
  75. let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber;
  76. // If there was no selected text, try to get last overtyped text
  77. if (!value && this._overtypingCapturer) {
  78. const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx);
  79. if (info) {
  80. value = info.value;
  81. isMultiline = info.multiline;
  82. }
  83. }
  84. if (value && isMultiline && variable.snippet) {
  85. // Selection is a multiline string which we indentation we now
  86. // need to adjust. We compare the indentation of this variable
  87. // with the indentation at the editor position and add potential
  88. // extra indentation to the value
  89. const line = this._model.getLineContent(this._selection.startLineNumber);
  90. const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1);
  91. let varLeadingWhitespace = lineLeadingWhitespace;
  92. variable.snippet.walk(marker => {
  93. if (marker === variable) {
  94. return false;
  95. }
  96. if (marker instanceof Text) {
  97. varLeadingWhitespace = getLeadingWhitespace(splitLines(marker.value).pop());
  98. }
  99. return true;
  100. });
  101. const whitespaceCommonLength = commonPrefixLength(varLeadingWhitespace, lineLeadingWhitespace);
  102. value = value.replace(/(\r\n|\r|\n)(.*)/g, (m, newline, rest) => `${newline}${varLeadingWhitespace.substr(whitespaceCommonLength)}${rest}`);
  103. }
  104. return value;
  105. }
  106. else if (name === 'TM_CURRENT_LINE') {
  107. return this._model.getLineContent(this._selection.positionLineNumber);
  108. }
  109. else if (name === 'TM_CURRENT_WORD') {
  110. const info = this._model.getWordAtPosition({
  111. lineNumber: this._selection.positionLineNumber,
  112. column: this._selection.positionColumn
  113. });
  114. return info && info.word || undefined;
  115. }
  116. else if (name === 'TM_LINE_INDEX') {
  117. return String(this._selection.positionLineNumber - 1);
  118. }
  119. else if (name === 'TM_LINE_NUMBER') {
  120. return String(this._selection.positionLineNumber);
  121. }
  122. return undefined;
  123. }
  124. }
  125. export class ModelBasedVariableResolver {
  126. constructor(_labelService, _model) {
  127. this._labelService = _labelService;
  128. this._model = _model;
  129. //
  130. }
  131. resolve(variable) {
  132. const { name } = variable;
  133. if (name === 'TM_FILENAME') {
  134. return path.basename(this._model.uri.fsPath);
  135. }
  136. else if (name === 'TM_FILENAME_BASE') {
  137. const name = path.basename(this._model.uri.fsPath);
  138. const idx = name.lastIndexOf('.');
  139. if (idx <= 0) {
  140. return name;
  141. }
  142. else {
  143. return name.slice(0, idx);
  144. }
  145. }
  146. else if (name === 'TM_DIRECTORY') {
  147. if (path.dirname(this._model.uri.fsPath) === '.') {
  148. return '';
  149. }
  150. return this._labelService.getUriLabel(dirname(this._model.uri));
  151. }
  152. else if (name === 'TM_FILEPATH') {
  153. return this._labelService.getUriLabel(this._model.uri);
  154. }
  155. else if (name === 'RELATIVE_FILEPATH') {
  156. return this._labelService.getUriLabel(this._model.uri, { relative: true, noPrefix: true });
  157. }
  158. return undefined;
  159. }
  160. }
  161. export class ClipboardBasedVariableResolver {
  162. constructor(_readClipboardText, _selectionIdx, _selectionCount, _spread) {
  163. this._readClipboardText = _readClipboardText;
  164. this._selectionIdx = _selectionIdx;
  165. this._selectionCount = _selectionCount;
  166. this._spread = _spread;
  167. //
  168. }
  169. resolve(variable) {
  170. if (variable.name !== 'CLIPBOARD') {
  171. return undefined;
  172. }
  173. const clipboardText = this._readClipboardText();
  174. if (!clipboardText) {
  175. return undefined;
  176. }
  177. // `spread` is assigning each cursor a line of the clipboard
  178. // text whenever there the line count equals the cursor count
  179. // and when enabled
  180. if (this._spread) {
  181. const lines = clipboardText.split(/\r\n|\n|\r/).filter(s => !isFalsyOrWhitespace(s));
  182. if (lines.length === this._selectionCount) {
  183. return lines[this._selectionIdx];
  184. }
  185. }
  186. return clipboardText;
  187. }
  188. }
  189. export class CommentBasedVariableResolver {
  190. constructor(_model, _selection) {
  191. this._model = _model;
  192. this._selection = _selection;
  193. //
  194. }
  195. resolve(variable) {
  196. const { name } = variable;
  197. const langId = this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber, this._selection.selectionStartColumn);
  198. const config = LanguageConfigurationRegistry.getComments(langId);
  199. if (!config) {
  200. return undefined;
  201. }
  202. if (name === 'LINE_COMMENT') {
  203. return config.lineCommentToken || undefined;
  204. }
  205. else if (name === 'BLOCK_COMMENT_START') {
  206. return config.blockCommentStartToken || undefined;
  207. }
  208. else if (name === 'BLOCK_COMMENT_END') {
  209. return config.blockCommentEndToken || undefined;
  210. }
  211. return undefined;
  212. }
  213. }
  214. export class TimeBasedVariableResolver {
  215. constructor() {
  216. this._date = new Date();
  217. }
  218. resolve(variable) {
  219. const { name } = variable;
  220. if (name === 'CURRENT_YEAR') {
  221. return String(this._date.getFullYear());
  222. }
  223. else if (name === 'CURRENT_YEAR_SHORT') {
  224. return String(this._date.getFullYear()).slice(-2);
  225. }
  226. else if (name === 'CURRENT_MONTH') {
  227. return String(this._date.getMonth().valueOf() + 1).padStart(2, '0');
  228. }
  229. else if (name === 'CURRENT_DATE') {
  230. return String(this._date.getDate().valueOf()).padStart(2, '0');
  231. }
  232. else if (name === 'CURRENT_HOUR') {
  233. return String(this._date.getHours().valueOf()).padStart(2, '0');
  234. }
  235. else if (name === 'CURRENT_MINUTE') {
  236. return String(this._date.getMinutes().valueOf()).padStart(2, '0');
  237. }
  238. else if (name === 'CURRENT_SECOND') {
  239. return String(this._date.getSeconds().valueOf()).padStart(2, '0');
  240. }
  241. else if (name === 'CURRENT_DAY_NAME') {
  242. return TimeBasedVariableResolver.dayNames[this._date.getDay()];
  243. }
  244. else if (name === 'CURRENT_DAY_NAME_SHORT') {
  245. return TimeBasedVariableResolver.dayNamesShort[this._date.getDay()];
  246. }
  247. else if (name === 'CURRENT_MONTH_NAME') {
  248. return TimeBasedVariableResolver.monthNames[this._date.getMonth()];
  249. }
  250. else if (name === 'CURRENT_MONTH_NAME_SHORT') {
  251. return TimeBasedVariableResolver.monthNamesShort[this._date.getMonth()];
  252. }
  253. else if (name === 'CURRENT_SECONDS_UNIX') {
  254. return String(Math.floor(this._date.getTime() / 1000));
  255. }
  256. return undefined;
  257. }
  258. }
  259. TimeBasedVariableResolver.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")];
  260. TimeBasedVariableResolver.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")];
  261. TimeBasedVariableResolver.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")];
  262. TimeBasedVariableResolver.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")];
  263. export class WorkspaceBasedVariableResolver {
  264. constructor(_workspaceService) {
  265. this._workspaceService = _workspaceService;
  266. //
  267. }
  268. resolve(variable) {
  269. if (!this._workspaceService) {
  270. return undefined;
  271. }
  272. const workspaceIdentifier = toWorkspaceIdentifier(this._workspaceService.getWorkspace());
  273. if (!workspaceIdentifier) {
  274. return undefined;
  275. }
  276. if (variable.name === 'WORKSPACE_NAME') {
  277. return this._resolveWorkspaceName(workspaceIdentifier);
  278. }
  279. else if (variable.name === 'WORKSPACE_FOLDER') {
  280. return this._resoveWorkspacePath(workspaceIdentifier);
  281. }
  282. return undefined;
  283. }
  284. _resolveWorkspaceName(workspaceIdentifier) {
  285. if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
  286. return path.basename(workspaceIdentifier.uri.path);
  287. }
  288. let filename = path.basename(workspaceIdentifier.configPath.path);
  289. if (filename.endsWith(WORKSPACE_EXTENSION)) {
  290. filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
  291. }
  292. return filename;
  293. }
  294. _resoveWorkspacePath(workspaceIdentifier) {
  295. if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
  296. return normalizeDriveLetter(workspaceIdentifier.uri.fsPath);
  297. }
  298. let filename = path.basename(workspaceIdentifier.configPath.path);
  299. let folderpath = workspaceIdentifier.configPath.fsPath;
  300. if (folderpath.endsWith(filename)) {
  301. folderpath = folderpath.substr(0, folderpath.length - filename.length - 1);
  302. }
  303. return (folderpath ? normalizeDriveLetter(folderpath) : '/');
  304. }
  305. }
  306. export class RandomBasedVariableResolver {
  307. resolve(variable) {
  308. const { name } = variable;
  309. if (name === 'RANDOM') {
  310. return Math.random().toString().slice(-6);
  311. }
  312. else if (name === 'RANDOM_HEX') {
  313. return Math.random().toString(16).slice(-6);
  314. }
  315. else if (name === 'UUID') {
  316. return generateUuid();
  317. }
  318. return undefined;
  319. }
  320. }