My dotfiles
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 

952 Zeilen
46 KiB

  1. "use strict";
  2. /*---------------------------------------------------------------------------------------------
  3. * Copyright (c) Microsoft Corporation. All rights reserved.
  4. * Licensed under the MIT License. See License.txt in the project root for license information.
  5. *--------------------------------------------------------------------------------------------*/
  6. Object.defineProperty(exports, "__esModule", { value: true });
  7. const vscode_languageserver_types_1 = require("vscode-languageserver-types");
  8. const expand_full_1 = require("./expand/expand-full");
  9. const extract = require("@emmetio/extract-abbreviation");
  10. const path = require("path");
  11. const fs = require("fs");
  12. const JSONC = require("jsonc-parser");
  13. const os_1 = require("os");
  14. const data_1 = require("./data");
  15. const snippetKeyCache = new Map();
  16. let markupSnippetKeys;
  17. let markupSnippetKeysRegex;
  18. const stylesheetCustomSnippetsKeyCache = new Map();
  19. const htmlAbbreviationStartRegex = /^[a-z,A-Z,!,(,[,#,\.]/;
  20. const cssAbbreviationRegex = /^-?[a-z,A-Z,!,@,#]/;
  21. const htmlAbbreviationRegex = /[a-z,A-Z\.]/;
  22. const emmetModes = ['html', 'pug', 'slim', 'haml', 'xml', 'xsl', 'jsx', 'css', 'scss', 'sass', 'less', 'stylus'];
  23. const commonlyUsedTags = [...data_1.htmlData.tags, 'lorem'];
  24. const bemFilterSuffix = 'bem';
  25. const filterDelimitor = '|';
  26. const trimFilterSuffix = 't';
  27. const commentFilterSuffix = 'c';
  28. const maxFilters = 3;
  29. const vendorPrefixes = { 'w': "webkit", 'm': "moz", 's': "ms", 'o': "o" };
  30. const defaultVendorProperties = {
  31. 'w': "animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-clip, background-composite, background-origin, background-size, border-fit, border-horizontal-spacing, border-image, border-vertical-spacing, box-align, box-direction, box-flex, box-flex-group, box-lines, box-ordinal-group, box-orient, box-pack, box-reflect, box-shadow, color-correction, column-break-after, column-break-before, column-break-inside, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, dashboard-region, font-smoothing, highlight, hyphenate-character, hyphenate-limit-after, hyphenate-limit-before, hyphens, line-box-contain, line-break, line-clamp, locale, margin-before-collapse, margin-after-collapse, marquee-direction, marquee-increment, marquee-repetition, marquee-style, mask-attachment, mask-box-image, mask-box-image-outset, mask-box-image-repeat, mask-box-image-slice, mask-box-image-source, mask-box-image-width, mask-clip, mask-composite, mask-image, mask-origin, mask-position, mask-repeat, mask-size, nbsp-mode, perspective, perspective-origin, rtl-ordering, text-combine, text-decorations-in-effect, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-fill-color, text-orientation, text-security, text-stroke-color, text-stroke-width, transform, transition, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, user-drag, user-modify, user-select, writing-mode, svg-shadow, box-sizing, border-radius",
  32. 'm': "animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-inline-policy, binding, border-bottom-colors, border-image, border-left-colors, border-right-colors, border-top-colors, box-align, box-direction, box-flex, box-ordinal-group, box-orient, box-pack, box-shadow, box-sizing, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-width, float-edge, font-feature-settings, font-language-override, force-broken-image-icon, hyphens, image-region, orient, outline-radius-bottomleft, outline-radius-bottomright, outline-radius-topleft, outline-radius-topright, perspective, perspective-origin, stack-sizing, tab-size, text-blink, text-decoration-color, text-decoration-line, text-decoration-style, text-size-adjust, transform, transform-origin, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-focus, user-input, user-modify, user-select, window-shadow, background-clip, border-radius",
  33. 's': "accelerator, backface-visibility, background-position-x, background-position-y, behavior, block-progression, box-align, box-direction, box-flex, box-line-progression, box-lines, box-ordinal-group, box-orient, box-pack, content-zoom-boundary, content-zoom-boundary-max, content-zoom-boundary-min, content-zoom-chaining, content-zoom-snap, content-zoom-snap-points, content-zoom-snap-type, content-zooming, filter, flow-from, flow-into, font-feature-settings, grid-column, grid-column-align, grid-column-span, grid-columns, grid-layer, grid-row, grid-row-align, grid-row-span, grid-rows, high-contrast-adjust, hyphenate-limit-chars, hyphenate-limit-lines, hyphenate-limit-zone, hyphens, ime-mode, interpolation-mode, layout-flow, layout-grid, layout-grid-char, layout-grid-line, layout-grid-mode, layout-grid-type, line-break, overflow-style, perspective, perspective-origin, perspective-origin-x, perspective-origin-y, scroll-boundary, scroll-boundary-bottom, scroll-boundary-left, scroll-boundary-right, scroll-boundary-top, scroll-chaining, scroll-rails, scroll-snap-points-x, scroll-snap-points-y, scroll-snap-type, scroll-snap-x, scroll-snap-y, scrollbar-arrow-color, scrollbar-base-color, scrollbar-darkshadow-color, scrollbar-face-color, scrollbar-highlight-color, scrollbar-shadow-color, scrollbar-track-color, text-align-last, text-autospace, text-justify, text-kashida-space, text-overflow, text-size-adjust, text-underline-position, touch-action, transform, transform-origin, transform-origin-x, transform-origin-y, transform-origin-z, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-select, word-break, wrap-flow, wrap-margin, wrap-through, writing-mode",
  34. 'o': "dashboard-region, animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, border-image, link, link-source, object-fit, object-position, tab-size, table-baseline, transform, transform-origin, transition, transition-delay, transition-duration, transition-property, transition-timing-function, accesskey, input-format, input-required, marquee-dir, marquee-loop, marquee-speed, marquee-style"
  35. };
  36. /**
  37. * Returns all applicable emmet expansions for abbreviation at given position in a CompletionList
  38. * @param document TextDocument in which completions are requested
  39. * @param position Position in the document at which completions are requested
  40. * @param syntax Emmet supported language
  41. * @param emmetConfig Emmet Configurations as derived from VS Code
  42. */
  43. function doComplete(document, position, syntax, emmetConfig) {
  44. if (emmetConfig.showExpandedAbbreviation === 'never' || !getEmmetMode(syntax, emmetConfig.excludeLanguages)) {
  45. return;
  46. }
  47. // Fetch markupSnippets so that we can provide possible abbreviation completions
  48. // For example, when text at position is `a`, completions should return `a:blank`, `a:link`, `acr` etc.
  49. if (!isStyleSheet(syntax)) {
  50. if (!snippetKeyCache.has(syntax) || !markupSnippetKeysRegex || markupSnippetKeysRegex.length === 0) {
  51. let registry = customSnippetRegistry[syntax] ? customSnippetRegistry[syntax] : expand_full_1.createSnippetsRegistry(syntax);
  52. if (!snippetKeyCache.has(syntax)) {
  53. snippetKeyCache.set(syntax, registry.all({ type: 'string' }).map(snippet => {
  54. return snippet.key;
  55. }));
  56. }
  57. markupSnippetKeysRegex = registry.all({ type: 'regexp' }).map(snippet => {
  58. return snippet.key;
  59. });
  60. }
  61. markupSnippetKeys = snippetKeyCache.get(syntax);
  62. }
  63. let extractedValue = extractAbbreviation(document, position, { syntax, lookAhead: !isStyleSheet(syntax) });
  64. if (!extractedValue) {
  65. return;
  66. }
  67. let { abbreviationRange, abbreviation, filter } = extractedValue;
  68. let currentLineTillPosition = getCurrentLine(document, position).substr(0, position.character);
  69. let currentWord = getCurrentWord(currentLineTillPosition);
  70. // Dont attempt to expand open tags
  71. if (currentWord === abbreviation
  72. && currentLineTillPosition.endsWith(`<${abbreviation}`)
  73. && (syntax === 'html' || syntax === 'xml' || syntax === 'xsl' || syntax === 'jsx')) {
  74. return;
  75. }
  76. // `preferences` is supported in Emmet config to allow backward compatibility
  77. // `getExpandOptions` converts it into a format understandable by new modules
  78. // We retain a copy here to be used by the vendor prefixing feature
  79. let expandOptions = getExpandOptions(syntax, emmetConfig, filter);
  80. let preferences = expandOptions['preferences'];
  81. delete expandOptions['preferences'];
  82. let expandedText;
  83. let expandedAbbr;
  84. let completionItems = [];
  85. // Create completion item after expanding given abbreviation
  86. // if abbreviation is valid and expanded value is not noise
  87. const createExpandedAbbr = (syntax, abbr) => {
  88. if (!isAbbreviationValid(syntax, abbreviation)) {
  89. return;
  90. }
  91. try {
  92. expandedText = expand_full_1.expand(abbr, expandOptions);
  93. }
  94. catch (e) {
  95. }
  96. if (!expandedText || isExpandedTextNoise(syntax, abbr, expandedText)) {
  97. return;
  98. }
  99. expandedAbbr = vscode_languageserver_types_1.CompletionItem.create(abbr);
  100. expandedAbbr.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedText)));
  101. expandedAbbr.documentation = replaceTabStopsWithCursors(expandedText);
  102. expandedAbbr.insertTextFormat = vscode_languageserver_types_1.InsertTextFormat.Snippet;
  103. expandedAbbr.detail = 'Emmet Abbreviation';
  104. expandedAbbr.label = abbreviation;
  105. expandedAbbr.label += filter ? '|' + filter.replace(',', '|') : "";
  106. completionItems = [expandedAbbr];
  107. };
  108. if (isStyleSheet(syntax)) {
  109. let { prefixOptions, abbreviationWithoutPrefix } = splitVendorPrefix(abbreviation);
  110. createExpandedAbbr(syntax, abbreviationWithoutPrefix);
  111. // When abbr is longer than usual emmet snippets and matches better with existing css property, then no emmet
  112. if (abbreviationWithoutPrefix.length > 4
  113. && data_1.cssData.properties.find(x => x.startsWith(abbreviationWithoutPrefix))) {
  114. return vscode_languageserver_types_1.CompletionList.create([], true);
  115. }
  116. if (expandedAbbr) {
  117. let prefixedExpandedText = applyVendorPrefixes(expandedText, prefixOptions, preferences);
  118. expandedAbbr.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(prefixedExpandedText)));
  119. expandedAbbr.documentation = replaceTabStopsWithCursors(prefixedExpandedText);
  120. expandedAbbr.label = removeTabStops(expandedText);
  121. expandedAbbr.filterText = abbreviation;
  122. // Custom snippets should show up in completions if abbreviation is a prefix
  123. const stylesheetCustomSnippetsKeys = stylesheetCustomSnippetsKeyCache.has(syntax) ? stylesheetCustomSnippetsKeyCache.get(syntax) : stylesheetCustomSnippetsKeyCache.get('css');
  124. completionItems = makeSnippetSuggestion(stylesheetCustomSnippetsKeys, abbreviation, abbreviation, abbreviationRange, expandOptions, 'Emmet Custom Snippet', false);
  125. if (!completionItems.find(x => x.textEdit.newText === expandedAbbr.textEdit.newText)) {
  126. // Fix for https://github.com/Microsoft/vscode/issues/28933#issuecomment-309236902
  127. // When user types in propertyname, emmet uses it to match with snippet names, resulting in width -> widows or font-family -> font: family
  128. // Filter out those cases here.
  129. const abbrRegex = new RegExp('.*' + abbreviationWithoutPrefix.split('').map(x => (x === '$' || x === '+') ? '\\' + x : x).join('.*') + '.*', 'i');
  130. if (/\d/.test(abbreviation) || abbrRegex.test(expandedAbbr.label)) {
  131. completionItems.push(expandedAbbr);
  132. }
  133. }
  134. }
  135. // Incomplete abbreviations that use vendor prefix
  136. if (!completionItems.length && (abbreviation === '-' || /^-[wmso]{1,4}-?$/.test(abbreviation))) {
  137. return vscode_languageserver_types_1.CompletionList.create([], true);
  138. }
  139. }
  140. else {
  141. createExpandedAbbr(syntax, abbreviation);
  142. let tagToFindMoreSuggestionsFor = abbreviation;
  143. let newTagMatches = abbreviation.match(/(>|\+)([\w:-]+)$/);
  144. if (newTagMatches && newTagMatches.length === 3) {
  145. tagToFindMoreSuggestionsFor = newTagMatches[2];
  146. }
  147. let commonlyUsedTagSuggestions = makeSnippetSuggestion(commonlyUsedTags, tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation');
  148. completionItems = completionItems.concat(commonlyUsedTagSuggestions);
  149. if (emmetConfig.showAbbreviationSuggestions === true) {
  150. let abbreviationSuggestions = makeSnippetSuggestion(markupSnippetKeys.filter(x => !commonlyUsedTags.includes(x)), tagToFindMoreSuggestionsFor, abbreviation, abbreviationRange, expandOptions, 'Emmet Abbreviation');
  151. // Workaround for the main expanded abbr not appearing before the snippet suggestions
  152. if (expandedAbbr && abbreviationSuggestions.length > 0 && tagToFindMoreSuggestionsFor !== abbreviation) {
  153. expandedAbbr.sortText = '0' + expandedAbbr.label;
  154. abbreviationSuggestions.forEach(item => {
  155. // Workaround for snippet suggestions items getting filtered out as the complete abbr does not start with snippetKey
  156. item.filterText = abbreviation;
  157. // Workaround for the main expanded abbr not appearing before the snippet suggestions
  158. item.sortText = '9' + abbreviation;
  159. });
  160. }
  161. completionItems = completionItems.concat(abbreviationSuggestions);
  162. }
  163. }
  164. if (emmetConfig.showSuggestionsAsSnippets === true) {
  165. completionItems.forEach(x => x.kind = vscode_languageserver_types_1.CompletionItemKind.Snippet);
  166. }
  167. return completionItems.length ? vscode_languageserver_types_1.CompletionList.create(completionItems, true) : undefined;
  168. }
  169. exports.doComplete = doComplete;
  170. /**
  171. * Create & return snippets for snippet keys that start with given prefix
  172. */
  173. function makeSnippetSuggestion(snippetKeys, prefix, abbreviation, abbreviationRange, expandOptions, snippetDetail, skipFullMatch = true) {
  174. if (!prefix || !snippetKeys) {
  175. return [];
  176. }
  177. let snippetCompletions = [];
  178. snippetKeys.forEach(snippetKey => {
  179. if (!snippetKey.startsWith(prefix.toLowerCase()) || (skipFullMatch && snippetKey === prefix.toLowerCase())) {
  180. return;
  181. }
  182. let currentAbbr = abbreviation + snippetKey.substr(prefix.length);
  183. let expandedAbbr;
  184. try {
  185. expandedAbbr = expand_full_1.expand(currentAbbr, expandOptions);
  186. }
  187. catch (e) {
  188. }
  189. if (!expandedAbbr) {
  190. return;
  191. }
  192. let item = vscode_languageserver_types_1.CompletionItem.create(prefix + snippetKey.substr(prefix.length));
  193. item.documentation = replaceTabStopsWithCursors(expandedAbbr);
  194. item.detail = snippetDetail;
  195. item.textEdit = vscode_languageserver_types_1.TextEdit.replace(abbreviationRange, escapeNonTabStopDollar(addFinalTabStop(expandedAbbr)));
  196. item.insertTextFormat = vscode_languageserver_types_1.InsertTextFormat.Snippet;
  197. snippetCompletions.push(item);
  198. });
  199. return snippetCompletions;
  200. }
  201. function getCurrentWord(currentLineTillPosition) {
  202. if (currentLineTillPosition) {
  203. let matches = currentLineTillPosition.match(/[\w,:,-,\.]*$/);
  204. if (matches) {
  205. return matches[0];
  206. }
  207. }
  208. }
  209. function replaceTabStopsWithCursors(expandedWord) {
  210. return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1|').replace(/\$\{\d+:([^\}]+)\}/g, '$1');
  211. }
  212. function removeTabStops(expandedWord) {
  213. return expandedWord.replace(/([^\\])\$\{\d+\}/g, '$1').replace(/\$\{\d+:([^\}]+)\}/g, '$1');
  214. }
  215. function escapeNonTabStopDollar(text) {
  216. return text ? text.replace(/([^\\])(\$)([^\{])/g, '$1\\$2$3') : text;
  217. }
  218. function addFinalTabStop(text) {
  219. if (!text || !text.trim()) {
  220. return text;
  221. }
  222. let maxTabStop = -1;
  223. let maxTabStopRanges = [];
  224. let foundLastStop = false;
  225. let replaceWithLastStop = false;
  226. let i = 0;
  227. let n = text.length;
  228. try {
  229. while (i < n && !foundLastStop) {
  230. // Look for ${
  231. if (text[i++] != '$' || text[i++] != '{') {
  232. continue;
  233. }
  234. // Find tabstop
  235. let numberStart = -1;
  236. let numberEnd = -1;
  237. while (i < n && /\d/.test(text[i])) {
  238. numberStart = numberStart < 0 ? i : numberStart;
  239. numberEnd = i + 1;
  240. i++;
  241. }
  242. // If ${ was not followed by a number and either } or :, then its not a tabstop
  243. if (numberStart === -1 || numberEnd === -1 || i >= n || (text[i] != '}' && text[i] != ':')) {
  244. continue;
  245. }
  246. // If ${0} was found, then break
  247. const currentTabStop = text.substring(numberStart, numberEnd);
  248. foundLastStop = currentTabStop === '0';
  249. if (foundLastStop) {
  250. break;
  251. }
  252. let foundPlaceholder = false;
  253. if (text[i++] == ':') {
  254. // TODO: Nested placeholders may break here
  255. while (i < n) {
  256. if (text[i] == '}') {
  257. foundPlaceholder = true;
  258. break;
  259. }
  260. i++;
  261. }
  262. }
  263. // Decide to replace currentTabStop with ${0} only if its the max among all tabstops and is not a placeholder
  264. if (Number(currentTabStop) > Number(maxTabStop)) {
  265. maxTabStop = currentTabStop;
  266. maxTabStopRanges = [{ numberStart, numberEnd }];
  267. replaceWithLastStop = !foundPlaceholder;
  268. }
  269. else if (currentTabStop == maxTabStop) {
  270. maxTabStopRanges.push({ numberStart, numberEnd });
  271. }
  272. }
  273. }
  274. catch (e) {
  275. }
  276. if (replaceWithLastStop && !foundLastStop) {
  277. for (let i = 0; i < maxTabStopRanges.length; i++) {
  278. let rangeStart = maxTabStopRanges[i].numberStart;
  279. let rangeEnd = maxTabStopRanges[i].numberEnd;
  280. text = text.substr(0, rangeStart) + '0' + text.substr(rangeEnd);
  281. }
  282. }
  283. return text;
  284. }
  285. function getCurrentLine(document, position) {
  286. let offset = document.offsetAt(position);
  287. let text = document.getText();
  288. let start = 0;
  289. let end = text.length;
  290. for (let i = offset - 1; i >= 0; i--) {
  291. if (text[i] === '\n') {
  292. start = i + 1;
  293. break;
  294. }
  295. }
  296. for (let i = offset; i < text.length; i++) {
  297. if (text[i] === '\n') {
  298. end = i;
  299. break;
  300. }
  301. }
  302. return text.substring(start, end);
  303. }
  304. let customSnippetRegistry = {};
  305. let variablesFromFile = {};
  306. let profilesFromFile = {};
  307. exports.emmetSnippetField = (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`;
  308. function isStyleSheet(syntax) {
  309. let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
  310. return (stylesheetSyntaxes.indexOf(syntax) > -1);
  311. }
  312. exports.isStyleSheet = isStyleSheet;
  313. function getFilters(text, pos) {
  314. let filter;
  315. for (let i = 0; i < maxFilters; i++) {
  316. if (text.endsWith(`${filterDelimitor}${bemFilterSuffix}`, pos)) {
  317. pos -= bemFilterSuffix.length + 1;
  318. filter = filter ? bemFilterSuffix + ',' + filter : bemFilterSuffix;
  319. }
  320. else if (text.endsWith(`${filterDelimitor}${commentFilterSuffix}`, pos)) {
  321. pos -= commentFilterSuffix.length + 1;
  322. filter = filter ? commentFilterSuffix + ',' + filter : commentFilterSuffix;
  323. }
  324. else if (text.endsWith(`${filterDelimitor}${trimFilterSuffix}`, pos)) {
  325. pos -= trimFilterSuffix.length + 1;
  326. filter = filter ? trimFilterSuffix + ',' + filter : trimFilterSuffix;
  327. }
  328. else {
  329. break;
  330. }
  331. }
  332. return {
  333. pos: pos,
  334. filter: filter
  335. };
  336. }
  337. /**
  338. * * Extracts abbreviation from the given position in the given document
  339. * @param document The TextDocument from which abbreviation needs to be extracted
  340. * @param position The Position in the given document from where abbreviation needs to be extracted
  341. * @param options The options to pass to the @emmetio/extract-abbreviation module
  342. */
  343. function extractAbbreviation(document, position, options) {
  344. const currentLine = getCurrentLine(document, position);
  345. const currentLineTillPosition = currentLine.substr(0, position.character);
  346. const { pos, filter } = getFilters(currentLineTillPosition, position.character);
  347. const lengthOccupiedByFilter = filter ? filter.length + 1 : 0;
  348. try {
  349. let extractOptions = options;
  350. if (typeof extractOptions !== 'boolean') {
  351. extractOptions = extractOptions || {};
  352. extractOptions = {
  353. syntax: (isStyleSheet(extractOptions.syntax) || extractOptions.syntax === 'stylesheet') ? 'stylesheet' : 'markup',
  354. lookAhead: extractOptions.lookAhead
  355. };
  356. }
  357. const result = extract(currentLine, pos, extractOptions);
  358. const rangeToReplace = vscode_languageserver_types_1.Range.create(position.line, result.location, position.line, result.location + result.abbreviation.length + lengthOccupiedByFilter);
  359. return {
  360. abbreviationRange: rangeToReplace,
  361. abbreviation: result.abbreviation,
  362. filter
  363. };
  364. }
  365. catch (e) {
  366. }
  367. }
  368. exports.extractAbbreviation = extractAbbreviation;
  369. /**
  370. * Extracts abbreviation from the given text
  371. * @param text Text from which abbreviation needs to be extracted
  372. * @param syntax Syntax used to extract the abbreviation from the given text
  373. */
  374. function extractAbbreviationFromText(text, syntax) {
  375. if (!text) {
  376. return;
  377. }
  378. const { pos, filter } = getFilters(text, text.length);
  379. try {
  380. let extractOptions = (isStyleSheet(syntax) || syntax === 'stylesheet') ? { syntax: 'stylesheet', lookAhead: false } : true;
  381. const result = extract(text, pos, extractOptions);
  382. return {
  383. abbreviation: result.abbreviation,
  384. filter
  385. };
  386. }
  387. catch (e) {
  388. }
  389. }
  390. exports.extractAbbreviationFromText = extractAbbreviationFromText;
  391. /**
  392. * Returns a boolean denoting validity of given abbreviation in the context of given syntax
  393. * Not needed once https://github.com/emmetio/atom-plugin/issues/22 is fixed
  394. * @param syntax string
  395. * @param abbreviation string
  396. */
  397. function isAbbreviationValid(syntax, abbreviation) {
  398. if (!abbreviation) {
  399. return false;
  400. }
  401. if (isStyleSheet(syntax)) {
  402. // Fix for https://github.com/Microsoft/vscode/issues/1623 in new emmet
  403. if (abbreviation.endsWith(':')) {
  404. return false;
  405. }
  406. if (abbreviation.indexOf('#') > -1) {
  407. return hexColorRegex.test(abbreviation) || propertyHexColorRegex.test(abbreviation);
  408. }
  409. return cssAbbreviationRegex.test(abbreviation);
  410. }
  411. if (abbreviation.startsWith('!')) {
  412. return !/[^!]/.test(abbreviation);
  413. }
  414. const multipleMatch = abbreviation.match(/\*(\d+)$/);
  415. if (multipleMatch) {
  416. return parseInt(multipleMatch[1], 10) <= 100;
  417. }
  418. // Its common for users to type (sometextinsidebrackets), this should not be treated as an abbreviation
  419. // Grouping in abbreviation is valid only if it's inside a text node or preceeded/succeeded with one of the symbols for nesting, sibling, repeater or climb up
  420. if ((/\(/.test(abbreviation) || /\)/.test(abbreviation)) && !/\{[^\}\{]*[\(\)]+[^\}\{]*\}(?:[>\+\*\^]|$)/.test(abbreviation) && !/\(.*\)[>\+\*\^]/.test(abbreviation) && !/[>\+\*\^]\(.*\)/.test(abbreviation)) {
  421. return false;
  422. }
  423. return (htmlAbbreviationStartRegex.test(abbreviation) && htmlAbbreviationRegex.test(abbreviation));
  424. }
  425. exports.isAbbreviationValid = isAbbreviationValid;
  426. function isExpandedTextNoise(syntax, abbreviation, expandedText) {
  427. // Unresolved css abbreviations get expanded to a blank property value
  428. // Eg: abc -> abc: ; or abc:d -> abc: d; which is noise if it gets suggested for every word typed
  429. if (isStyleSheet(syntax)) {
  430. let after = (syntax === 'sass' || syntax === 'stylus') ? '' : ';';
  431. return expandedText === `${abbreviation}: \${1}${after}` || expandedText.replace(/\s/g, '') === abbreviation.replace(/\s/g, '') + after;
  432. }
  433. if (commonlyUsedTags.indexOf(abbreviation.toLowerCase()) > -1 || markupSnippetKeys.indexOf(abbreviation) > -1) {
  434. return false;
  435. }
  436. // Custom tags can have - or :
  437. if (/[-,:]/.test(abbreviation) && !/--|::/.test(abbreviation) && !abbreviation.endsWith(':')) {
  438. return false;
  439. }
  440. // Its common for users to type some text and end it with period, this should not be treated as an abbreviation
  441. // Else it becomes noise.
  442. // When user just types '.', return the expansion
  443. // Otherwise emmet loses change to participate later
  444. // For example in `.foo`. See https://github.com/Microsoft/vscode/issues/66013
  445. if (abbreviation === '.') {
  446. return false;
  447. }
  448. const dotMatches = abbreviation.match(/^([a-z,A-Z,\d]*)\.$/);
  449. if (dotMatches) {
  450. // Valid html tags such as `div.`
  451. if (dotMatches[1] && data_1.htmlData.tags.includes(dotMatches[1])) {
  452. return false;
  453. }
  454. return true;
  455. }
  456. // Unresolved html abbreviations get expanded as if it were a tag
  457. // Eg: abc -> <abc></abc> which is noise if it gets suggested for every word typed
  458. return (expandedText.toLowerCase() === `<${abbreviation.toLowerCase()}>\${1}</${abbreviation.toLowerCase()}>`);
  459. }
  460. /**
  461. * Returns options to be used by the @emmetio/expand-abbreviation module
  462. * @param syntax
  463. * @param textToReplace
  464. */
  465. function getExpandOptions(syntax, emmetConfig, filter) {
  466. emmetConfig = emmetConfig || {};
  467. emmetConfig['preferences'] = emmetConfig['preferences'] || {};
  468. // Fetch snippet registry
  469. let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
  470. if (!customSnippetRegistry[syntax] && customSnippetRegistry[baseSyntax]) {
  471. customSnippetRegistry[syntax] = customSnippetRegistry[baseSyntax];
  472. }
  473. // Fetch Profile
  474. let profile = getProfile(syntax, emmetConfig['syntaxProfiles']);
  475. let filtersFromProfile = (profile && profile['filters']) ? profile['filters'].split(',') : [];
  476. filtersFromProfile = filtersFromProfile.map(filterFromProfile => filterFromProfile.trim());
  477. // Update profile based on preferences
  478. if (emmetConfig['preferences']['format.noIndentTags']) {
  479. if (Array.isArray(emmetConfig['preferences']['format.noIndentTags'])) {
  480. profile['formatSkip'] = emmetConfig['preferences']['format.noIndentTags'];
  481. }
  482. else if (typeof emmetConfig['preferences']['format.noIndentTags'] === 'string') {
  483. profile['formatSkip'] = emmetConfig['preferences']['format.noIndentTags'].split(',');
  484. }
  485. }
  486. if (emmetConfig['preferences']['format.forceIndentationForTags']) {
  487. if (Array.isArray(emmetConfig['preferences']['format.forceIndentationForTags'])) {
  488. profile['formatForce'] = emmetConfig['preferences']['format.forceIndentationForTags'];
  489. }
  490. else if (typeof emmetConfig['preferences']['format.forceIndentationForTags'] === 'string') {
  491. profile['formatForce'] = emmetConfig['preferences']['format.forceIndentationForTags'].split(',');
  492. }
  493. }
  494. if (emmetConfig['preferences']['profile.allowCompactBoolean'] && typeof emmetConfig['preferences']['profile.allowCompactBoolean'] === 'boolean') {
  495. profile['compactBooleanAttributes'] = emmetConfig['preferences']['profile.allowCompactBoolean'];
  496. }
  497. // Fetch Add Ons
  498. let addons = {};
  499. if (filter && filter.split(',').find(x => x.trim() === 'bem') || filtersFromProfile.indexOf('bem') > -1) {
  500. addons['bem'] = { element: '__' };
  501. if (emmetConfig['preferences']['bem.elementSeparator']) {
  502. addons['bem']['element'] = emmetConfig['preferences']['bem.elementSeparator'];
  503. }
  504. if (emmetConfig['preferences']['bem.modifierSeparator']) {
  505. addons['bem']['modifier'] = emmetConfig['preferences']['bem.modifierSeparator'];
  506. }
  507. }
  508. if (syntax === 'jsx') {
  509. addons['jsx'] = true;
  510. }
  511. // Fetch Formatters
  512. let formatters = getFormatters(syntax, emmetConfig['preferences']);
  513. if (filter && filter.split(',').find(x => x.trim() === 'c') || filtersFromProfile.indexOf('c') > -1) {
  514. if (!formatters['comment']) {
  515. formatters['comment'] = {
  516. enabled: true
  517. };
  518. }
  519. else {
  520. formatters['comment']['enabled'] = true;
  521. }
  522. }
  523. // If the user doesn't provide specific properties for a vendor, use the default values
  524. let preferences = emmetConfig['preferences'];
  525. for (const v in vendorPrefixes) {
  526. let vendorProperties = preferences['css.' + vendorPrefixes[v] + 'Properties'];
  527. if (vendorProperties == null) {
  528. preferences['css.' + vendorPrefixes[v] + 'Properties'] = defaultVendorProperties[v];
  529. }
  530. }
  531. return {
  532. field: exports.emmetSnippetField,
  533. syntax: syntax,
  534. profile: profile,
  535. addons: addons,
  536. variables: getVariables(emmetConfig['variables']),
  537. snippets: customSnippetRegistry[syntax],
  538. format: formatters,
  539. preferences: preferences
  540. };
  541. }
  542. exports.getExpandOptions = getExpandOptions;
  543. function splitVendorPrefix(abbreviation) {
  544. abbreviation = abbreviation || "";
  545. if (abbreviation[0] != '-') {
  546. return {
  547. prefixOptions: "",
  548. abbreviationWithoutPrefix: abbreviation
  549. };
  550. }
  551. else {
  552. abbreviation = abbreviation.substr(1);
  553. let pref = "-";
  554. if (/^[wmso]*-./.test(abbreviation)) {
  555. let index = abbreviation.indexOf("-");
  556. if (index > -1) {
  557. pref += abbreviation.substr(0, index + 1);
  558. abbreviation = abbreviation.substr(index + 1);
  559. }
  560. }
  561. return {
  562. prefixOptions: pref,
  563. abbreviationWithoutPrefix: abbreviation
  564. };
  565. }
  566. }
  567. function applyVendorPrefixes(expandedProperty, vendors, preferences) {
  568. preferences = preferences || {};
  569. expandedProperty = expandedProperty || "";
  570. vendors = vendors || "";
  571. if (vendors[0] !== '-') {
  572. return expandedProperty;
  573. }
  574. if (vendors == "-") {
  575. let defaultVendors = "-";
  576. let property = expandedProperty.substr(0, expandedProperty.indexOf(':'));
  577. if (!property) {
  578. return expandedProperty;
  579. }
  580. for (const v in vendorPrefixes) {
  581. let vendorProperties = preferences['css.' + vendorPrefixes[v] + 'Properties'];
  582. if (vendorProperties && vendorProperties.split(',').find(x => x.trim() === property))
  583. defaultVendors += v;
  584. }
  585. // If no vendors specified, add all
  586. vendors = defaultVendors == "-" ? "-wmso" : defaultVendors;
  587. vendors += '-';
  588. }
  589. vendors = vendors.substr(1);
  590. let prefixedProperty = "";
  591. for (let index = 0; index < vendors.length - 1; index++) {
  592. prefixedProperty += '-' + vendorPrefixes[vendors[index]] + '-' + expandedProperty + "\n";
  593. }
  594. return prefixedProperty + expandedProperty;
  595. }
  596. /**
  597. * Parses given abbreviation using given options and returns a tree
  598. * @param abbreviation string
  599. * @param options options used by the @emmetio/expand-abbreviation module to parse given abbreviation
  600. */
  601. function parseAbbreviation(abbreviation, options) {
  602. return expand_full_1.parse(abbreviation, options);
  603. }
  604. exports.parseAbbreviation = parseAbbreviation;
  605. /**
  606. * Expands given abbreviation using given options
  607. * @param abbreviation string or parsed abbreviation
  608. * @param options options used by the @emmetio/expand-abbreviation module to expand given abbreviation
  609. */
  610. function expandAbbreviation(abbreviation, options) {
  611. let expandedText;
  612. let preferences = options['preferences'];
  613. delete options['preferences'];
  614. if (isStyleSheet(options['syntax']) && typeof abbreviation === 'string') {
  615. let { prefixOptions, abbreviationWithoutPrefix } = splitVendorPrefix(abbreviation);
  616. expandedText = expand_full_1.expand(abbreviationWithoutPrefix, options);
  617. expandedText = applyVendorPrefixes(expandedText, prefixOptions, preferences);
  618. }
  619. else {
  620. expandedText = expand_full_1.expand(abbreviation, options);
  621. }
  622. return escapeNonTabStopDollar(addFinalTabStop(expandedText));
  623. }
  624. exports.expandAbbreviation = expandAbbreviation;
  625. /**
  626. * Maps and returns syntaxProfiles of previous format to ones compatible with new emmet modules
  627. * @param syntax
  628. */
  629. function getProfile(syntax, profilesFromSettings) {
  630. if (!profilesFromSettings) {
  631. profilesFromSettings = {};
  632. }
  633. let profilesConfig = Object.assign({}, profilesFromFile, profilesFromSettings);
  634. let options = profilesConfig[syntax];
  635. if (!options || typeof options === 'string') {
  636. if (options === 'xhtml') {
  637. return {
  638. selfClosingStyle: 'xhtml'
  639. };
  640. }
  641. return {};
  642. }
  643. let newOptions = {};
  644. for (let key in options) {
  645. switch (key) {
  646. case 'tag_case':
  647. newOptions['tagCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
  648. break;
  649. case 'attr_case':
  650. newOptions['attributeCase'] = (options[key] === 'lower' || options[key] === 'upper') ? options[key] : '';
  651. break;
  652. case 'attr_quotes':
  653. newOptions['attributeQuotes'] = options[key];
  654. break;
  655. case 'tag_nl':
  656. newOptions['format'] = (options[key] === true || options[key] === false) ? options[key] : true;
  657. break;
  658. case 'inline_break':
  659. newOptions['inlineBreak'] = options[key];
  660. break;
  661. case 'self_closing_tag':
  662. if (options[key] === true) {
  663. newOptions['selfClosingStyle'] = 'xml';
  664. break;
  665. }
  666. if (options[key] === false) {
  667. newOptions['selfClosingStyle'] = 'html';
  668. break;
  669. }
  670. newOptions['selfClosingStyle'] = options[key];
  671. break;
  672. case 'compact_bool':
  673. newOptions['compactBooleanAttributes'] = options[key];
  674. break;
  675. default:
  676. newOptions[key] = options[key];
  677. break;
  678. }
  679. }
  680. return newOptions;
  681. }
  682. /**
  683. * Returns variables to be used while expanding snippets
  684. */
  685. function getVariables(variablesFromSettings) {
  686. if (!variablesFromSettings) {
  687. return variablesFromFile;
  688. }
  689. return Object.assign({}, variablesFromFile, variablesFromSettings);
  690. }
  691. function getFormatters(syntax, preferences) {
  692. if (!preferences) {
  693. return {};
  694. }
  695. if (!isStyleSheet(syntax)) {
  696. let commentFormatter = {};
  697. for (let key in preferences) {
  698. switch (key) {
  699. case 'filter.commentAfter':
  700. commentFormatter['after'] = preferences[key];
  701. break;
  702. case 'filter.commentBefore':
  703. commentFormatter['before'] = preferences[key];
  704. break;
  705. case 'filter.commentTrigger':
  706. commentFormatter['trigger'] = preferences[key];
  707. break;
  708. default:
  709. break;
  710. }
  711. }
  712. return {
  713. comment: commentFormatter
  714. };
  715. }
  716. let fuzzySearchMinScore = typeof preferences['css.fuzzySearchMinScore'] === 'number' ? preferences['css.fuzzySearchMinScore'] : 0.3;
  717. if (fuzzySearchMinScore > 1) {
  718. fuzzySearchMinScore = 1;
  719. }
  720. else if (fuzzySearchMinScore < 0) {
  721. fuzzySearchMinScore = 0;
  722. }
  723. let stylesheetFormatter = {
  724. 'fuzzySearchMinScore': fuzzySearchMinScore
  725. };
  726. for (let key in preferences) {
  727. switch (key) {
  728. case 'css.floatUnit':
  729. stylesheetFormatter['floatUnit'] = preferences[key];
  730. break;
  731. case 'css.intUnit':
  732. stylesheetFormatter['intUnit'] = preferences[key];
  733. break;
  734. case 'css.unitAliases':
  735. let unitAliases = {};
  736. preferences[key].split(',').forEach(alias => {
  737. if (!alias || !alias.trim() || alias.indexOf(':') === -1) {
  738. return;
  739. }
  740. let aliasName = alias.substr(0, alias.indexOf(':'));
  741. let aliasValue = alias.substr(aliasName.length + 1);
  742. if (!aliasName.trim() || !aliasValue) {
  743. return;
  744. }
  745. unitAliases[aliasName.trim()] = aliasValue;
  746. });
  747. stylesheetFormatter['unitAliases'] = unitAliases;
  748. break;
  749. case `${syntax}.valueSeparator`:
  750. stylesheetFormatter['between'] = preferences[key];
  751. break;
  752. case `${syntax}.propertyEnd`:
  753. stylesheetFormatter['after'] = preferences[key];
  754. break;
  755. default:
  756. break;
  757. }
  758. }
  759. return {
  760. stylesheet: stylesheetFormatter
  761. };
  762. }
  763. /**
  764. * Updates customizations from snippets.json and syntaxProfiles.json files in the directory configured in emmet.extensionsPath setting
  765. */
  766. function updateExtensionsPath(emmetExtensionsPath, workspaceFolderPath) {
  767. if (!emmetExtensionsPath || !emmetExtensionsPath.trim()) {
  768. resetSettingsFromFile();
  769. return Promise.resolve();
  770. }
  771. emmetExtensionsPath = emmetExtensionsPath.trim();
  772. workspaceFolderPath = workspaceFolderPath ? workspaceFolderPath.trim() : '';
  773. if (emmetExtensionsPath[0] === '~') {
  774. emmetExtensionsPath = path.join(os_1.homedir(), emmetExtensionsPath.substr(1));
  775. }
  776. else if (!path.isAbsolute(emmetExtensionsPath) && workspaceFolderPath) {
  777. emmetExtensionsPath = path.join(workspaceFolderPath, emmetExtensionsPath);
  778. }
  779. if (!path.isAbsolute(emmetExtensionsPath)) {
  780. resetSettingsFromFile();
  781. return Promise.reject('The path provided in emmet.extensionsPath setting should be absoulte path');
  782. }
  783. if (!dirExists(emmetExtensionsPath)) {
  784. resetSettingsFromFile();
  785. return Promise.reject(`The directory ${emmetExtensionsPath} doesnt exist. Update emmet.extensionsPath setting`);
  786. }
  787. let dirPath = emmetExtensionsPath;
  788. let snippetsPath = path.join(dirPath, 'snippets.json');
  789. let profilesPath = path.join(dirPath, 'syntaxProfiles.json');
  790. let snippetsPromise = new Promise((resolve, reject) => {
  791. fs.readFile(snippetsPath, (err, snippetsData) => {
  792. if (err) {
  793. return reject(`Error while fetching the file ${snippetsPath}`);
  794. }
  795. try {
  796. let errors = [];
  797. let snippetsJson = JSONC.parse(snippetsData.toString(), errors);
  798. if (errors.length > 0) {
  799. return reject(`Found error ${JSONC.ScanError[errors[0].error]} while parsing the file ${snippetsPath} at offset ${errors[0].offset}`);
  800. }
  801. variablesFromFile = snippetsJson['variables'];
  802. customSnippetRegistry = {};
  803. snippetKeyCache.clear();
  804. Object.keys(snippetsJson).forEach(syntax => {
  805. if (!snippetsJson[syntax]['snippets']) {
  806. return;
  807. }
  808. let baseSyntax = isStyleSheet(syntax) ? 'css' : 'html';
  809. let customSnippets = snippetsJson[syntax]['snippets'];
  810. if (snippetsJson[baseSyntax] && snippetsJson[baseSyntax]['snippets'] && baseSyntax !== syntax) {
  811. customSnippets = Object.assign({}, snippetsJson[baseSyntax]['snippets'], snippetsJson[syntax]['snippets']);
  812. }
  813. if (!isStyleSheet(syntax)) {
  814. // In Emmet 2.0 all snippets should be valid abbreviations
  815. // Convert old snippets that do not follow this format to new format
  816. for (let snippetKey in customSnippets) {
  817. if (customSnippets.hasOwnProperty(snippetKey)
  818. && customSnippets[snippetKey].startsWith('<')
  819. && customSnippets[snippetKey].endsWith('>')) {
  820. customSnippets[snippetKey] = `{${customSnippets[snippetKey]}}`;
  821. }
  822. }
  823. }
  824. else {
  825. stylesheetCustomSnippetsKeyCache.set(syntax, Object.keys(customSnippets));
  826. }
  827. customSnippetRegistry[syntax] = expand_full_1.createSnippetsRegistry(syntax, customSnippets);
  828. let snippetKeys = customSnippetRegistry[syntax].all({ type: 'string' }).map(snippet => {
  829. return snippet.key;
  830. });
  831. snippetKeyCache.set(syntax, snippetKeys);
  832. });
  833. }
  834. catch (e) {
  835. return reject(`Error while parsing the file ${snippetsPath}`);
  836. }
  837. return resolve();
  838. });
  839. });
  840. let variablesPromise = new Promise((resolve, reject) => {
  841. fs.readFile(profilesPath, (err, profilesData) => {
  842. try {
  843. if (!err) {
  844. profilesFromFile = JSON.parse(profilesData.toString());
  845. }
  846. }
  847. catch (e) {
  848. }
  849. return resolve();
  850. });
  851. });
  852. return Promise.all([snippetsPromise, variablesFromFile]).then(() => Promise.resolve());
  853. }
  854. exports.updateExtensionsPath = updateExtensionsPath;
  855. function dirExists(dirPath) {
  856. try {
  857. return fs.statSync(dirPath).isDirectory();
  858. }
  859. catch (e) {
  860. return false;
  861. }
  862. }
  863. function resetSettingsFromFile() {
  864. customSnippetRegistry = {};
  865. snippetKeyCache.clear();
  866. stylesheetCustomSnippetsKeyCache.clear();
  867. profilesFromFile = {};
  868. variablesFromFile = {};
  869. }
  870. /**
  871. * Get the corresponding emmet mode for given vscode language mode
  872. * Eg: jsx for typescriptreact/javascriptreact or pug for jade
  873. * If the language is not supported by emmet or has been exlcuded via `exlcudeLanguages` setting,
  874. * then nothing is returned
  875. *
  876. * @param language
  877. * @param exlcudedLanguages Array of language ids that user has chosen to exlcude for emmet
  878. */
  879. function getEmmetMode(language, excludedLanguages = []) {
  880. if (!language || excludedLanguages.indexOf(language) > -1) {
  881. return;
  882. }
  883. if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx
  884. return 'jsx';
  885. }
  886. if (language === 'sass-indented') { // map sass-indented to sass
  887. return 'sass';
  888. }
  889. if (language === 'jade') {
  890. return 'pug';
  891. }
  892. if (emmetModes.indexOf(language) > -1) {
  893. return language;
  894. }
  895. }
  896. exports.getEmmetMode = getEmmetMode;
  897. const propertyHexColorRegex = /^[a-zA-Z]+:?#[\d.a-fA-F]{0,6}$/;
  898. const hexColorRegex = /^#[\d,a-f,A-F]{1,6}$/;
  899. const onlyLetters = /^[a-z,A-Z]+$/;
  900. /**
  901. * Returns a completion participant for Emmet of the form {
  902. * onCssProperty: () => void
  903. * onCssPropertyValue: () => void
  904. * onHtmlContent: () => void
  905. * }
  906. * @param document The TextDocument for which completions are being provided
  907. * @param position The Position in the given document where completions are being provided
  908. * @param syntax The Emmet syntax to use when providing Emmet completions
  909. * @param emmetSettings The Emmet settings to use when providing Emmet completions
  910. * @param result The Completion List object that needs to be updated with Emmet completions
  911. */
  912. function getEmmetCompletionParticipants(document, position, syntax, emmetSettings, result) {
  913. return {
  914. getId: () => 'emmet',
  915. onCssProperty: (context) => {
  916. if (context && context.propertyName) {
  917. const currentresult = doComplete(document, position, syntax, emmetSettings);
  918. if (result && currentresult) {
  919. result.items = currentresult.items;
  920. result.isIncomplete = true;
  921. }
  922. }
  923. },
  924. onCssPropertyValue: (context) => {
  925. if (context && context.propertyValue) {
  926. const extractedResults = extractAbbreviation(document, position, { syntax: 'css', lookAhead: false });
  927. if (!extractedResults) {
  928. return;
  929. }
  930. const validAbbreviationWithColon = extractedResults.abbreviation === `${context.propertyName}:${context.propertyValue}` && onlyLetters.test(context.propertyValue);
  931. if (validAbbreviationWithColon // Allows abbreviations like pos:f
  932. || hexColorRegex.test(extractedResults.abbreviation)
  933. || extractedResults.abbreviation === '!') {
  934. const currentresult = doComplete(document, position, syntax, emmetSettings);
  935. if (result && currentresult) {
  936. result.items = currentresult.items;
  937. result.isIncomplete = true;
  938. }
  939. }
  940. }
  941. },
  942. onHtmlContent: () => {
  943. const currentresult = doComplete(document, position, syntax, emmetSettings);
  944. if (result && currentresult) {
  945. result.items = currentresult.items;
  946. result.isIncomplete = true;
  947. }
  948. }
  949. };
  950. }
  951. exports.getEmmetCompletionParticipants = getEmmetCompletionParticipants;
  952. //# sourceMappingURL=emmetHelper.js.map