eae5d8c807
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
666 lines
21 KiB
TypeScript
666 lines
21 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as vscode from 'vscode';
|
|
import parse from '@emmetio/html-matcher';
|
|
import parseStylesheet from '@emmetio/css-parser';
|
|
import { Node as FlatNode, HtmlNode as HtmlFlatNode, Property as FlatProperty, Rule as FlatRule, CssToken as FlatCssToken, Stylesheet as FlatStylesheet } from 'EmmetFlatNode';
|
|
import { DocumentStreamReader } from './bufferStream';
|
|
import * as EmmetHelper from 'vscode-emmet-helper';
|
|
import { TextDocument as LSTextDocument } from 'vscode-languageserver-textdocument';
|
|
import { getRootNode } from './parseDocument';
|
|
|
|
let _emmetHelper: typeof EmmetHelper;
|
|
let _currentExtensionsPath: string | undefined = undefined;
|
|
|
|
let _homeDir: vscode.Uri | undefined;
|
|
|
|
|
|
export function setHomeDir(homeDir: vscode.Uri) {
|
|
_homeDir = homeDir;
|
|
}
|
|
|
|
export function getEmmetHelper() {
|
|
// Lazy load vscode-emmet-helper instead of importing it
|
|
// directly to reduce the start-up time of the extension
|
|
if (!_emmetHelper) {
|
|
_emmetHelper = require('vscode-emmet-helper');
|
|
}
|
|
updateEmmetExtensionsPath();
|
|
return _emmetHelper;
|
|
}
|
|
|
|
/**
|
|
* Update Emmet Helper to use user snippets from the extensionsPath setting
|
|
*/
|
|
export function updateEmmetExtensionsPath(forceRefresh: boolean = false) {
|
|
if (!_emmetHelper) {
|
|
return;
|
|
}
|
|
let extensionsPath = vscode.workspace.getConfiguration('emmet')['extensionsPath'];
|
|
if (forceRefresh || _currentExtensionsPath !== extensionsPath) {
|
|
_currentExtensionsPath = extensionsPath;
|
|
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
|
|
return;
|
|
} else {
|
|
const rootPath = vscode.workspace.workspaceFolders[0].uri;
|
|
const fileSystem = vscode.workspace.fs;
|
|
_emmetHelper.updateExtensionsPath(extensionsPath, fileSystem, rootPath, _homeDir).then(null, (err: string) => vscode.window.showErrorMessage(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mapping between languages that support Emmet and completion trigger characters
|
|
*/
|
|
export const LANGUAGE_MODES: { [id: string]: string[] } = {
|
|
'html': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'jade': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'slim': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'haml': ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'xml': ['.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'xsl': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'css': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'scss': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'sass': [':', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'less': [':', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'stylus': [':', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'javascriptreact': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
|
'typescriptreact': ['!', '.', '}', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
|
};
|
|
|
|
export function isStyleSheet(syntax: string): boolean {
|
|
let stylesheetSyntaxes = ['css', 'scss', 'sass', 'less', 'stylus'];
|
|
return (stylesheetSyntaxes.indexOf(syntax) > -1);
|
|
}
|
|
|
|
export function validate(allowStylesheet: boolean = true): boolean {
|
|
let editor = vscode.window.activeTextEditor;
|
|
if (!editor) {
|
|
vscode.window.showInformationMessage('No editor is active');
|
|
return false;
|
|
}
|
|
if (!allowStylesheet && isStyleSheet(editor.document.languageId)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function getMappingForIncludedLanguages(): any {
|
|
// Explicitly map languages that have built-in grammar in VS Code to their parent language
|
|
// to get emmet completion support
|
|
// For other languages, users will have to use `emmet.includeLanguages` or
|
|
// language specific extensions can provide emmet completion support
|
|
const MAPPED_MODES: Object = {
|
|
'handlebars': 'html',
|
|
'php': 'html'
|
|
};
|
|
|
|
const finalMappedModes = Object.create(null);
|
|
let includeLanguagesConfig = vscode.workspace.getConfiguration('emmet')['includeLanguages'];
|
|
let includeLanguages = Object.assign({}, MAPPED_MODES, includeLanguagesConfig ? includeLanguagesConfig : {});
|
|
Object.keys(includeLanguages).forEach(syntax => {
|
|
if (typeof includeLanguages[syntax] === 'string' && LANGUAGE_MODES[includeLanguages[syntax]]) {
|
|
finalMappedModes[syntax] = includeLanguages[syntax];
|
|
}
|
|
});
|
|
return finalMappedModes;
|
|
}
|
|
|
|
/**
|
|
* Get the corresponding emmet mode for given vscode language mode
|
|
* E.g.: jsx for typescriptreact/javascriptreact or pug for jade
|
|
* If the language is not supported by emmet or has been excluded via `excludeLanguages` setting,
|
|
* then nothing is returned
|
|
*
|
|
* @param excludedLanguages Array of language ids that user has chosen to exclude for emmet
|
|
*/
|
|
export function getEmmetMode(language: string, excludedLanguages: string[]): string | undefined {
|
|
if (!language || excludedLanguages.indexOf(language) > -1) {
|
|
return;
|
|
}
|
|
if (/\b(typescriptreact|javascriptreact|jsx-tags)\b/.test(language)) { // treat tsx like jsx
|
|
return 'jsx';
|
|
}
|
|
if (language === 'sass-indented') { // map sass-indented to sass
|
|
return 'sass';
|
|
}
|
|
if (language === 'jade') {
|
|
return 'pug';
|
|
}
|
|
const syntaxes = getSyntaxes();
|
|
if (syntaxes.markup.includes(language) || syntaxes.stylesheet.includes(language)) {
|
|
return language;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const closeBrace = 125;
|
|
const openBrace = 123;
|
|
const slash = 47;
|
|
const star = 42;
|
|
|
|
/**
|
|
* Traverse the given document backward & forward from given position
|
|
* to find a complete ruleset, then parse just that to return a Stylesheet
|
|
* @param document vscode.TextDocument
|
|
* @param position vscode.Position
|
|
*/
|
|
export function parsePartialStylesheet(document: vscode.TextDocument, position: vscode.Position): FlatStylesheet | undefined {
|
|
const isCSS = document.languageId === 'css';
|
|
const positionOffset = document.offsetAt(position);
|
|
let startOffset = 0;
|
|
let endOffset = document.getText().length;
|
|
const limitCharacter = positionOffset - 5000;
|
|
const limitOffset = limitCharacter > 0 ? limitCharacter : startOffset;
|
|
const stream = new DocumentStreamReader(document, positionOffset);
|
|
|
|
function findOpeningCommentBeforePosition(pos: number): number | undefined {
|
|
const text = document.getText().substring(0, pos);
|
|
let offset = text.lastIndexOf('/*');
|
|
if (offset === -1) {
|
|
return;
|
|
}
|
|
return offset;
|
|
}
|
|
|
|
function findClosingCommentAfterPosition(pos: number): number | undefined {
|
|
const text = document.getText().substring(pos);
|
|
let offset = text.indexOf('*/');
|
|
if (offset === -1) {
|
|
return;
|
|
}
|
|
offset += 2 + pos;
|
|
return offset;
|
|
}
|
|
|
|
function consumeLineCommentBackwards() {
|
|
const posLineNumber = document.positionAt(stream.pos).line;
|
|
if (!isCSS && currentLine !== posLineNumber) {
|
|
currentLine = posLineNumber;
|
|
const startLineComment = document.lineAt(currentLine).text.indexOf('//');
|
|
if (startLineComment > -1) {
|
|
stream.pos = document.offsetAt(new vscode.Position(currentLine, startLineComment));
|
|
}
|
|
}
|
|
}
|
|
|
|
function consumeBlockCommentBackwards() {
|
|
if (stream.peek() === slash) {
|
|
if (stream.backUp(1) === star) {
|
|
stream.pos = findOpeningCommentBeforePosition(stream.pos) ?? startOffset;
|
|
} else {
|
|
stream.next();
|
|
}
|
|
}
|
|
}
|
|
|
|
function consumeCommentForwards() {
|
|
if (stream.eat(slash)) {
|
|
if (stream.eat(slash) && !isCSS) {
|
|
const posLineNumber = document.positionAt(stream.pos).line;
|
|
stream.pos = document.offsetAt(new vscode.Position(posLineNumber + 1, 0));
|
|
} else if (stream.eat(star)) {
|
|
stream.pos = findClosingCommentAfterPosition(stream.pos) ?? endOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go forward until we find a closing brace.
|
|
while (!stream.eof() && !stream.eat(closeBrace)) {
|
|
if (stream.peek() === slash) {
|
|
consumeCommentForwards();
|
|
} else {
|
|
stream.next();
|
|
}
|
|
}
|
|
|
|
if (!stream.eof()) {
|
|
endOffset = stream.pos;
|
|
}
|
|
|
|
stream.pos = positionOffset;
|
|
let openBracesToFind = 1;
|
|
let currentLine = position.line;
|
|
let exit = false;
|
|
|
|
// Go back until we found an opening brace. If we find a closing one, consume its pair and continue.
|
|
while (!exit && openBracesToFind > 0 && !stream.sof()) {
|
|
consumeLineCommentBackwards();
|
|
|
|
switch (stream.backUp(1)) {
|
|
case openBrace:
|
|
openBracesToFind--;
|
|
break;
|
|
case closeBrace:
|
|
if (isCSS) {
|
|
stream.next();
|
|
startOffset = stream.pos;
|
|
exit = true;
|
|
} else {
|
|
openBracesToFind++;
|
|
}
|
|
break;
|
|
case slash:
|
|
consumeBlockCommentBackwards();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (position.line - document.positionAt(stream.pos).line > 100
|
|
|| stream.pos <= limitOffset) {
|
|
exit = true;
|
|
}
|
|
}
|
|
|
|
// We are at an opening brace. We need to include its selector.
|
|
currentLine = document.positionAt(stream.pos).line;
|
|
openBracesToFind = 0;
|
|
let foundSelector = false;
|
|
while (!exit && !stream.sof() && !foundSelector && openBracesToFind >= 0) {
|
|
consumeLineCommentBackwards();
|
|
|
|
const ch = stream.backUp(1);
|
|
if (/\s/.test(String.fromCharCode(ch))) {
|
|
continue;
|
|
}
|
|
|
|
switch (ch) {
|
|
case slash:
|
|
consumeBlockCommentBackwards();
|
|
break;
|
|
case closeBrace:
|
|
openBracesToFind++;
|
|
break;
|
|
case openBrace:
|
|
openBracesToFind--;
|
|
break;
|
|
default:
|
|
if (!openBracesToFind) {
|
|
foundSelector = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!stream.sof() && foundSelector) {
|
|
startOffset = stream.pos;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const buffer = ' '.repeat(startOffset) + document.getText().substring(startOffset, endOffset);
|
|
return parseStylesheet(buffer);
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns node corresponding to given position in the given root node
|
|
*/
|
|
export function getFlatNode(root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): FlatNode | undefined {
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
function getFlatNodeChild(child: FlatNode | undefined): FlatNode | undefined {
|
|
if (!child) {
|
|
return;
|
|
}
|
|
const nodeStart = child.start;
|
|
const nodeEnd = child.end;
|
|
if ((nodeStart < offset && nodeEnd > offset)
|
|
|| (includeNodeBoundary && nodeStart <= offset && nodeEnd >= offset)) {
|
|
return getFlatNodeChildren(child.children) ?? child;
|
|
}
|
|
else if ('close' in <any>child) {
|
|
// We have an HTML node in this case.
|
|
// In case this node is an invalid unpaired HTML node,
|
|
// we still want to search its children
|
|
const htmlChild = <HtmlFlatNode>child;
|
|
if (htmlChild.open && !htmlChild.close) {
|
|
return getFlatNodeChildren(htmlChild.children);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
function getFlatNodeChildren(children: FlatNode[]): FlatNode | undefined {
|
|
for (let i = 0; i < children.length; i++) {
|
|
const foundChild = getFlatNodeChild(children[i]);
|
|
if (foundChild) {
|
|
return foundChild;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
return getFlatNodeChildren(root.children);
|
|
}
|
|
|
|
export const allowedMimeTypesInScriptTag = ['text/html', 'text/plain', 'text/x-template', 'text/template', 'text/ng-template'];
|
|
|
|
/**
|
|
* Finds the HTML node within an HTML document at a given position
|
|
* If position is inside a script tag of type template, then it will be parsed to find the inner HTML node as well
|
|
*/
|
|
export function getHtmlFlatNode(documentText: string, root: FlatNode | undefined, offset: number, includeNodeBoundary: boolean): HtmlFlatNode | undefined {
|
|
const currentNode: HtmlFlatNode | undefined = <HtmlFlatNode | undefined>getFlatNode(root, offset, includeNodeBoundary);
|
|
if (!currentNode) { return; }
|
|
|
|
const isTemplateScript = currentNode.name === 'script' &&
|
|
(currentNode.attributes &&
|
|
currentNode.attributes.some(x => x.name.toString() === 'type'
|
|
&& allowedMimeTypesInScriptTag.includes(x.value.toString())));
|
|
if (isTemplateScript
|
|
&& currentNode.open
|
|
&& offset > currentNode.open.end
|
|
&& (!currentNode.close || offset < currentNode.close.start)) {
|
|
// blank out the rest of the document and search for the node within
|
|
const beforePadding = ' '.repeat(currentNode.open.end);
|
|
const endToUse = currentNode.close ? currentNode.close.start : currentNode.end;
|
|
const scriptBodyText = beforePadding + documentText.substring(currentNode.open.end, endToUse);
|
|
const innerRoot: HtmlFlatNode = parse(scriptBodyText);
|
|
const scriptBodyNode = getHtmlFlatNode(scriptBodyText, innerRoot, offset, includeNodeBoundary);
|
|
if (scriptBodyNode) {
|
|
scriptBodyNode.parent = currentNode;
|
|
currentNode.children.push(scriptBodyNode);
|
|
return scriptBodyNode;
|
|
}
|
|
}
|
|
return currentNode;
|
|
}
|
|
|
|
export function offsetRangeToSelection(document: vscode.TextDocument, start: number, end: number): vscode.Selection {
|
|
const startPos = document.positionAt(start);
|
|
const endPos = document.positionAt(end);
|
|
return new vscode.Selection(startPos, endPos);
|
|
}
|
|
|
|
export function offsetRangeToVsRange(document: vscode.TextDocument, start: number, end: number): vscode.Range {
|
|
const startPos = document.positionAt(start);
|
|
const endPos = document.positionAt(end);
|
|
return new vscode.Range(startPos, endPos);
|
|
}
|
|
|
|
/**
|
|
* Returns the deepest non comment node under given node
|
|
*/
|
|
export function getDeepestFlatNode(node: FlatNode | undefined): FlatNode | undefined {
|
|
if (!node || !node.children || node.children.length === 0 || !node.children.find(x => x.type !== 'comment')) {
|
|
return node;
|
|
}
|
|
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
if (node.children[i].type !== 'comment') {
|
|
return getDeepestFlatNode(node.children[i]);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function findNextWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
|
|
|
|
let foundSpace = pos === -1;
|
|
let foundStart = false;
|
|
let foundEnd = false;
|
|
|
|
let newSelectionStart;
|
|
let newSelectionEnd;
|
|
while (pos < propertyValue.length - 1) {
|
|
pos++;
|
|
if (!foundSpace) {
|
|
if (propertyValue[pos] === ' ') {
|
|
foundSpace = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (foundSpace && !foundStart && propertyValue[pos] === ' ') {
|
|
continue;
|
|
}
|
|
if (!foundStart) {
|
|
newSelectionStart = pos;
|
|
foundStart = true;
|
|
continue;
|
|
}
|
|
if (propertyValue[pos] === ' ') {
|
|
newSelectionEnd = pos;
|
|
foundEnd = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundStart && !foundEnd) {
|
|
newSelectionEnd = propertyValue.length;
|
|
}
|
|
|
|
return [newSelectionStart, newSelectionEnd];
|
|
}
|
|
|
|
export function findPrevWord(propertyValue: string, pos: number): [number | undefined, number | undefined] {
|
|
|
|
let foundSpace = pos === propertyValue.length;
|
|
let foundStart = false;
|
|
let foundEnd = false;
|
|
|
|
let newSelectionStart;
|
|
let newSelectionEnd;
|
|
while (pos > -1) {
|
|
pos--;
|
|
if (!foundSpace) {
|
|
if (propertyValue[pos] === ' ') {
|
|
foundSpace = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (foundSpace && !foundEnd && propertyValue[pos] === ' ') {
|
|
continue;
|
|
}
|
|
if (!foundEnd) {
|
|
newSelectionEnd = pos + 1;
|
|
foundEnd = true;
|
|
continue;
|
|
}
|
|
if (propertyValue[pos] === ' ') {
|
|
newSelectionStart = pos + 1;
|
|
foundStart = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundEnd && !foundStart) {
|
|
newSelectionStart = 0;
|
|
}
|
|
|
|
return [newSelectionStart, newSelectionEnd];
|
|
}
|
|
|
|
export function getNodesInBetween(node1: FlatNode, node2: FlatNode): FlatNode[] {
|
|
// Same node
|
|
if (sameNodes(node1, node2)) {
|
|
return [node1];
|
|
}
|
|
|
|
// Not siblings
|
|
if (!sameNodes(node1.parent, node2.parent)) {
|
|
// node2 is ancestor of node1
|
|
if (node2.start < node1.start) {
|
|
return [node2];
|
|
}
|
|
|
|
// node1 is ancestor of node2
|
|
if (node2.start < node1.end) {
|
|
return [node1];
|
|
}
|
|
|
|
// Get the highest ancestor of node1 that should be commented
|
|
while (node1.parent && node1.parent.end < node2.start) {
|
|
node1 = node1.parent;
|
|
}
|
|
|
|
// Get the highest ancestor of node2 that should be commented
|
|
while (node2.parent && node2.parent.start > node1.start) {
|
|
node2 = node2.parent;
|
|
}
|
|
}
|
|
|
|
const siblings: FlatNode[] = [];
|
|
let currentNode: FlatNode | undefined = node1;
|
|
const position = node2.end;
|
|
while (currentNode && position > currentNode.start) {
|
|
siblings.push(currentNode);
|
|
currentNode = currentNode.nextSibling;
|
|
}
|
|
return siblings;
|
|
}
|
|
|
|
export function sameNodes(node1: FlatNode | undefined, node2: FlatNode | undefined): boolean {
|
|
// return true if they're both undefined
|
|
if (!node1 && !node2) {
|
|
return true;
|
|
}
|
|
// return false if only one of them is undefined
|
|
if (!node1 || !node2) {
|
|
return false;
|
|
}
|
|
return node1.start === node2.start && node1.end === node2.end;
|
|
}
|
|
|
|
export function getEmmetConfiguration(syntax: string) {
|
|
const emmetConfig = vscode.workspace.getConfiguration('emmet');
|
|
const syntaxProfiles = Object.assign({}, emmetConfig['syntaxProfiles'] || {});
|
|
const preferences = Object.assign({}, emmetConfig['preferences'] || {});
|
|
// jsx, xml and xsl syntaxes need to have self closing tags unless otherwise configured by user
|
|
if (syntax === 'jsx' || syntax === 'xml' || syntax === 'xsl') {
|
|
syntaxProfiles[syntax] = syntaxProfiles[syntax] || {};
|
|
if (typeof syntaxProfiles[syntax] === 'object'
|
|
&& !syntaxProfiles[syntax].hasOwnProperty('self_closing_tag') // Old Emmet format
|
|
&& !syntaxProfiles[syntax].hasOwnProperty('selfClosingStyle') // Emmet 2.0 format
|
|
) {
|
|
syntaxProfiles[syntax] = {
|
|
...syntaxProfiles[syntax],
|
|
selfClosingStyle: 'xml'
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
preferences,
|
|
showExpandedAbbreviation: emmetConfig['showExpandedAbbreviation'],
|
|
showAbbreviationSuggestions: emmetConfig['showAbbreviationSuggestions'],
|
|
syntaxProfiles,
|
|
variables: emmetConfig['variables'],
|
|
excludeLanguages: emmetConfig['excludeLanguages'],
|
|
showSuggestionsAsSnippets: emmetConfig['showSuggestionsAsSnippets']
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Itereates by each child, as well as nested child's children, in their order
|
|
* and invokes `fn` for each. If `fn` function returns `false`, iteration stops
|
|
*/
|
|
export function iterateCSSToken(token: FlatCssToken, fn: (x: any) => any): boolean {
|
|
for (let i = 0, il = token.size; i < il; i++) {
|
|
if (fn(token.item(i)) === false || iterateCSSToken(token.item(i), fn) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns `name` CSS property from given `rule`
|
|
*/
|
|
export function getCssPropertyFromRule(rule: FlatRule, name: string): FlatProperty | undefined {
|
|
return rule.children.find(node => node.type === 'property' && node.name === name) as FlatProperty;
|
|
}
|
|
|
|
/**
|
|
* Returns css property under caret in given editor or `null` if such node cannot
|
|
* be found
|
|
*/
|
|
export function getCssPropertyFromDocument(editor: vscode.TextEditor, position: vscode.Position): FlatProperty | null {
|
|
const document = editor.document;
|
|
const rootNode = getRootNode(document, true);
|
|
const offset = document.offsetAt(position);
|
|
const node = getFlatNode(rootNode, offset, true);
|
|
|
|
if (isStyleSheet(editor.document.languageId)) {
|
|
return node && node.type === 'property' ? <FlatProperty>node : null;
|
|
}
|
|
|
|
const htmlNode = <HtmlFlatNode>node;
|
|
if (htmlNode
|
|
&& htmlNode.name === 'style'
|
|
&& htmlNode.open && htmlNode.close
|
|
&& htmlNode.open.end < offset
|
|
&& htmlNode.close.start > offset) {
|
|
const buffer = ' '.repeat(htmlNode.start) +
|
|
document.getText().substring(htmlNode.start, htmlNode.end);
|
|
const innerRootNode = parseStylesheet(buffer);
|
|
const innerNode = getFlatNode(innerRootNode, offset, true);
|
|
return (innerNode && innerNode.type === 'property') ? <FlatProperty>innerNode : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
export function getEmbeddedCssNodeIfAny(document: vscode.TextDocument, currentNode: FlatNode | undefined, position: vscode.Position): FlatNode | undefined {
|
|
if (!currentNode) {
|
|
return;
|
|
}
|
|
const currentHtmlNode = <HtmlFlatNode>currentNode;
|
|
if (currentHtmlNode && currentHtmlNode.open && currentHtmlNode.close) {
|
|
const offset = document.offsetAt(position);
|
|
if (currentHtmlNode.open.end <= offset && offset <= currentHtmlNode.close.start) {
|
|
if (currentHtmlNode.name === 'style'
|
|
&& currentHtmlNode.open.end < offset
|
|
&& currentHtmlNode.close.start > offset) {
|
|
const buffer = ' '.repeat(currentHtmlNode.open.end) + document.getText().substring(currentHtmlNode.open.end, currentHtmlNode.close.start);
|
|
return parseStylesheet(buffer);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
export function isStyleAttribute(currentNode: FlatNode | undefined, offset: number): boolean {
|
|
if (!currentNode) {
|
|
return false;
|
|
}
|
|
const currentHtmlNode = <HtmlFlatNode>currentNode;
|
|
const index = (currentHtmlNode.attributes || []).findIndex(x => x.name.toString() === 'style');
|
|
if (index === -1) {
|
|
return false;
|
|
}
|
|
const styleAttribute = currentHtmlNode.attributes[index];
|
|
return offset >= styleAttribute.value.start && offset <= styleAttribute.value.end;
|
|
}
|
|
|
|
export function isNumber(obj: any): obj is number {
|
|
return typeof obj === 'number';
|
|
}
|
|
|
|
export function toLSTextDocument(doc: vscode.TextDocument): LSTextDocument {
|
|
return LSTextDocument.create(doc.uri.toString(), doc.languageId, doc.version, doc.getText());
|
|
}
|
|
|
|
export function getPathBaseName(path: string): string {
|
|
const pathAfterSlashSplit = path.split('/').pop();
|
|
const pathAfterBackslashSplit = pathAfterSlashSplit ? pathAfterSlashSplit.split('\\').pop() : '';
|
|
return pathAfterBackslashSplit ?? '';
|
|
}
|
|
|
|
export function getSyntaxes() {
|
|
/**
|
|
* List of all known syntaxes, from emmetio/emmet
|
|
*/
|
|
return {
|
|
markup: ['html', 'xml', 'xsl', 'jsx', 'js', 'pug', 'slim', 'haml'],
|
|
stylesheet: ['css', 'sass', 'scss', 'less', 'sss', 'stylus']
|
|
};
|
|
}
|