🎉 initiate project *astro_rewrite*

This commit is contained in:
sindrekjelsrud 2023-07-19 21:31:30 +02:00
parent ffd4d5e86c
commit 2ba37bfbe3
8658 changed files with 2268794 additions and 2538 deletions

View file

@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nodes from '../parser/cssNodes';
import { difference } from '../utils/strings';
import { Rules } from '../services/lintRules';
import { Command, TextEdit, CodeAction, CodeActionKind, TextDocumentEdit, VersionedTextDocumentIdentifier } from '../cssLanguageTypes';
import * as l10n from '@vscode/l10n';
export class CSSCodeActions {
constructor(cssDataManager) {
this.cssDataManager = cssDataManager;
}
doCodeActions(document, range, context, stylesheet) {
return this.doCodeActions2(document, range, context, stylesheet).map(ca => {
const textDocumentEdit = ca.edit && ca.edit.documentChanges && ca.edit.documentChanges[0];
return Command.create(ca.title, '_css.applyCodeAction', document.uri, document.version, textDocumentEdit && textDocumentEdit.edits);
});
}
doCodeActions2(document, range, context, stylesheet) {
const result = [];
if (context.diagnostics) {
for (const diagnostic of context.diagnostics) {
this.appendFixesForMarker(document, stylesheet, diagnostic, result);
}
}
return result;
}
getFixesForUnknownProperty(document, property, marker, result) {
const propertyName = property.getName();
const candidates = [];
this.cssDataManager.getProperties().forEach(p => {
const score = difference(propertyName, p.name);
if (score >= propertyName.length / 2 /*score_lim*/) {
candidates.push({ property: p.name, score });
}
});
// Sort in descending order.
candidates.sort((a, b) => {
return b.score - a.score || a.property.localeCompare(b.property);
});
let maxActions = 3;
for (const candidate of candidates) {
const propertyName = candidate.property;
const title = l10n.t("Rename to '{0}'", propertyName);
const edit = TextEdit.replace(marker.range, propertyName);
const documentIdentifier = VersionedTextDocumentIdentifier.create(document.uri, document.version);
const workspaceEdit = { documentChanges: [TextDocumentEdit.create(documentIdentifier, [edit])] };
const codeAction = CodeAction.create(title, workspaceEdit, CodeActionKind.QuickFix);
codeAction.diagnostics = [marker];
result.push(codeAction);
if (--maxActions <= 0) {
return;
}
}
}
appendFixesForMarker(document, stylesheet, marker, result) {
if (marker.code !== Rules.UnknownProperty.id) {
return;
}
const offset = document.offsetAt(marker.range.start);
const end = document.offsetAt(marker.range.end);
const nodepath = nodes.getNodePath(stylesheet, offset);
for (let i = nodepath.length - 1; i >= 0; i--) {
const node = nodepath[i];
if (node instanceof nodes.Declaration) {
const property = node.getProperty();
if (property && property.offset === offset && property.end === end) {
this.getFixesForUnknownProperty(document, property, marker, result);
return;
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,190 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TokenType, Scanner } from '../parser/cssScanner';
import { SCSSScanner, InterpolationFunction } from '../parser/scssScanner';
import { LESSScanner } from '../parser/lessScanner';
export function getFoldingRanges(document, context) {
const ranges = computeFoldingRanges(document);
return limitFoldingRanges(ranges, context);
}
function computeFoldingRanges(document) {
function getStartLine(t) {
return document.positionAt(t.offset).line;
}
function getEndLine(t) {
return document.positionAt(t.offset + t.len).line;
}
function getScanner() {
switch (document.languageId) {
case 'scss':
return new SCSSScanner();
case 'less':
return new LESSScanner();
default:
return new Scanner();
}
}
function tokenToRange(t, kind) {
const startLine = getStartLine(t);
const endLine = getEndLine(t);
if (startLine !== endLine) {
return {
startLine,
endLine,
kind
};
}
else {
return null;
}
}
const ranges = [];
const delimiterStack = [];
const scanner = getScanner();
scanner.ignoreComment = false;
scanner.setSource(document.getText());
let token = scanner.scan();
let prevToken = null;
while (token.type !== TokenType.EOF) {
switch (token.type) {
case TokenType.CurlyL:
case InterpolationFunction:
{
delimiterStack.push({ line: getStartLine(token), type: 'brace', isStart: true });
break;
}
case TokenType.CurlyR: {
if (delimiterStack.length !== 0) {
const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'brace');
if (!prevDelimiter) {
break;
}
let endLine = getEndLine(token);
if (prevDelimiter.type === 'brace') {
/**
* Other than the case when curly brace is not on a new line by itself, for example
* .foo {
* color: red; }
* Use endLine minus one to show ending curly brace
*/
if (prevToken && getEndLine(prevToken) !== endLine) {
endLine--;
}
if (prevDelimiter.line !== endLine) {
ranges.push({
startLine: prevDelimiter.line,
endLine,
kind: undefined
});
}
}
}
break;
}
/**
* In CSS, there is no single line comment prefixed with //
* All comments are marked as `Comment`
*/
case TokenType.Comment: {
const commentRegionMarkerToDelimiter = (marker) => {
if (marker === '#region') {
return { line: getStartLine(token), type: 'comment', isStart: true };
}
else {
return { line: getEndLine(token), type: 'comment', isStart: false };
}
};
const getCurrDelimiter = (token) => {
const matches = token.text.match(/^\s*\/\*\s*(#region|#endregion)\b\s*(.*?)\s*\*\//);
if (matches) {
return commentRegionMarkerToDelimiter(matches[1]);
}
else if (document.languageId === 'scss' || document.languageId === 'less') {
const matches = token.text.match(/^\s*\/\/\s*(#region|#endregion)\b\s*(.*?)\s*/);
if (matches) {
return commentRegionMarkerToDelimiter(matches[1]);
}
}
return null;
};
const currDelimiter = getCurrDelimiter(token);
// /* */ comment region folding
// All #region and #endregion cases
if (currDelimiter) {
if (currDelimiter.isStart) {
delimiterStack.push(currDelimiter);
}
else {
const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'comment');
if (!prevDelimiter) {
break;
}
if (prevDelimiter.type === 'comment') {
if (prevDelimiter.line !== currDelimiter.line) {
ranges.push({
startLine: prevDelimiter.line,
endLine: currDelimiter.line,
kind: 'region'
});
}
}
}
}
// Multiline comment case
else {
const range = tokenToRange(token, 'comment');
if (range) {
ranges.push(range);
}
}
break;
}
}
prevToken = token;
token = scanner.scan();
}
return ranges;
}
function popPrevStartDelimiterOfType(stack, type) {
if (stack.length === 0) {
return null;
}
for (let i = stack.length - 1; i >= 0; i--) {
if (stack[i].type === type && stack[i].isStart) {
return stack.splice(i, 1)[0];
}
}
return null;
}
/**
* - Sort regions
* - Remove invalid regions (intersections)
* - If limit exceeds, only return `rangeLimit` amount of ranges
*/
function limitFoldingRanges(ranges, context) {
const maxRanges = context && context.rangeLimit || Number.MAX_VALUE;
const sortedRanges = ranges.sort((r1, r2) => {
let diff = r1.startLine - r2.startLine;
if (diff === 0) {
diff = r1.endLine - r2.endLine;
}
return diff;
});
const validRanges = [];
let prevEndLine = -1;
sortedRanges.forEach(r => {
if (!(r.startLine < prevEndLine && prevEndLine < r.endLine)) {
validRanges.push(r);
prevEndLine = r.endLine;
}
});
if (validRanges.length < maxRanges) {
return validRanges;
}
else {
return validRanges.slice(0, maxRanges);
}
}

View file

@ -0,0 +1,136 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Range, Position } from '../cssLanguageTypes';
import { css_beautify } from '../beautify/beautify-css';
import { repeat } from '../utils/strings';
export function format(document, range, options) {
let value = document.getText();
let includesEnd = true;
let initialIndentLevel = 0;
let inRule = false;
const tabSize = options.tabSize || 4;
if (range) {
let startOffset = document.offsetAt(range.start);
// include all leading whitespace iff at the beginning of the line
let extendedStart = startOffset;
while (extendedStart > 0 && isWhitespace(value, extendedStart - 1)) {
extendedStart--;
}
if (extendedStart === 0 || isEOL(value, extendedStart - 1)) {
startOffset = extendedStart;
}
else {
// else keep at least one whitespace
if (extendedStart < startOffset) {
startOffset = extendedStart + 1;
}
}
// include all following whitespace until the end of the line
let endOffset = document.offsetAt(range.end);
let extendedEnd = endOffset;
while (extendedEnd < value.length && isWhitespace(value, extendedEnd)) {
extendedEnd++;
}
if (extendedEnd === value.length || isEOL(value, extendedEnd)) {
endOffset = extendedEnd;
}
range = Range.create(document.positionAt(startOffset), document.positionAt(endOffset));
// Test if inside a rule
inRule = isInRule(value, startOffset);
includesEnd = endOffset === value.length;
value = value.substring(startOffset, endOffset);
if (startOffset !== 0) {
const startOfLineOffset = document.offsetAt(Position.create(range.start.line, 0));
initialIndentLevel = computeIndentLevel(document.getText(), startOfLineOffset, options);
}
if (inRule) {
value = `{\n${trimLeft(value)}`;
}
}
else {
range = Range.create(Position.create(0, 0), document.positionAt(value.length));
}
const cssOptions = {
indent_size: tabSize,
indent_char: options.insertSpaces ? ' ' : '\t',
end_with_newline: includesEnd && getFormatOption(options, 'insertFinalNewline', false),
selector_separator_newline: getFormatOption(options, 'newlineBetweenSelectors', true),
newline_between_rules: getFormatOption(options, 'newlineBetweenRules', true),
space_around_selector_separator: getFormatOption(options, 'spaceAroundSelectorSeparator', false),
brace_style: getFormatOption(options, 'braceStyle', 'collapse'),
indent_empty_lines: getFormatOption(options, 'indentEmptyLines', false),
max_preserve_newlines: getFormatOption(options, 'maxPreserveNewLines', undefined),
preserve_newlines: getFormatOption(options, 'preserveNewLines', true),
wrap_line_length: getFormatOption(options, 'wrapLineLength', undefined),
eol: '\n'
};
let result = css_beautify(value, cssOptions);
if (inRule) {
result = trimLeft(result.substring(2));
}
if (initialIndentLevel > 0) {
const indent = options.insertSpaces ? repeat(' ', tabSize * initialIndentLevel) : repeat('\t', initialIndentLevel);
result = result.split('\n').join('\n' + indent);
if (range.start.character === 0) {
result = indent + result; // keep the indent
}
}
return [{
range: range,
newText: result
}];
}
function trimLeft(str) {
return str.replace(/^\s+/, '');
}
const _CUL = '{'.charCodeAt(0);
const _CUR = '}'.charCodeAt(0);
function isInRule(str, offset) {
while (offset >= 0) {
const ch = str.charCodeAt(offset);
if (ch === _CUL) {
return true;
}
else if (ch === _CUR) {
return false;
}
offset--;
}
return false;
}
function getFormatOption(options, key, dflt) {
if (options && options.hasOwnProperty(key)) {
const value = options[key];
if (value !== null) {
return value;
}
}
return dflt;
}
function computeIndentLevel(content, offset, options) {
let i = offset;
let nChars = 0;
const tabSize = options.tabSize || 4;
while (i < content.length) {
const ch = content.charAt(i);
if (ch === ' ') {
nChars++;
}
else if (ch === '\t') {
nChars += tabSize;
}
else {
break;
}
i++;
}
return Math.floor(nChars / tabSize);
}
function isEOL(text, offset) {
return '\r\n'.indexOf(text.charAt(offset)) !== -1;
}
function isWhitespace(text, offset) {
return ' \t'.indexOf(text.charAt(offset)) !== -1;
}

View file

@ -0,0 +1,148 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nodes from '../parser/cssNodes';
import * as languageFacts from '../languageFacts/facts';
import { SelectorPrinting } from './selectorPrinting';
import { startsWith } from '../utils/strings';
import { Range, MarkupKind } from '../cssLanguageTypes';
import { isDefined } from '../utils/objects';
export class CSSHover {
constructor(clientCapabilities, cssDataManager) {
this.clientCapabilities = clientCapabilities;
this.cssDataManager = cssDataManager;
this.selectorPrinting = new SelectorPrinting(cssDataManager);
}
configure(settings) {
this.defaultSettings = settings;
}
doHover(document, position, stylesheet, settings = this.defaultSettings) {
function getRange(node) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}
const offset = document.offsetAt(position);
const nodepath = nodes.getNodePath(stylesheet, offset);
/**
* nodepath is top-down
* Build up the hover by appending inner node's information
*/
let hover = null;
for (let i = 0; i < nodepath.length; i++) {
const node = nodepath[i];
if (node instanceof nodes.Selector) {
hover = {
contents: this.selectorPrinting.selectorToMarkedString(node),
range: getRange(node)
};
break;
}
if (node instanceof nodes.SimpleSelector) {
/**
* Some sass specific at rules such as `@at-root` are parsed as `SimpleSelector`
*/
if (!startsWith(node.getText(), '@')) {
hover = {
contents: this.selectorPrinting.simpleSelectorToMarkedString(node),
range: getRange(node)
};
}
break;
}
if (node instanceof nodes.Declaration) {
const propertyName = node.getFullPropertyName();
const entry = this.cssDataManager.getProperty(propertyName);
if (entry) {
const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings);
if (contents) {
hover = {
contents,
range: getRange(node)
};
}
else {
hover = null;
}
}
continue;
}
if (node instanceof nodes.UnknownAtRule) {
const atRuleName = node.getText();
const entry = this.cssDataManager.getAtDirective(atRuleName);
if (entry) {
const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings);
if (contents) {
hover = {
contents,
range: getRange(node)
};
}
else {
hover = null;
}
}
continue;
}
if (node instanceof nodes.Node && node.type === nodes.NodeType.PseudoSelector) {
const selectorName = node.getText();
const entry = selectorName.slice(0, 2) === '::'
? this.cssDataManager.getPseudoElement(selectorName)
: this.cssDataManager.getPseudoClass(selectorName);
if (entry) {
const contents = languageFacts.getEntryDescription(entry, this.doesSupportMarkdown(), settings);
if (contents) {
hover = {
contents,
range: getRange(node)
};
}
else {
hover = null;
}
}
continue;
}
}
if (hover) {
hover.contents = this.convertContents(hover.contents);
}
return hover;
}
convertContents(contents) {
if (!this.doesSupportMarkdown()) {
if (typeof contents === 'string') {
return contents;
}
// MarkupContent
else if ('kind' in contents) {
return {
kind: 'plaintext',
value: contents.value
};
}
// MarkedString[]
else if (Array.isArray(contents)) {
return contents.map(c => {
return typeof c === 'string' ? c : c.value;
});
}
// MarkedString
else {
return contents.value;
}
}
return contents;
}
doesSupportMarkdown() {
if (!isDefined(this.supportsMarkdown)) {
if (!isDefined(this.clientCapabilities)) {
this.supportsMarkdown = true;
return this.supportsMarkdown;
}
const hover = this.clientCapabilities.textDocument && this.clientCapabilities.textDocument.hover;
this.supportsMarkdown = hover && hover.contentFormat && Array.isArray(hover.contentFormat) && hover.contentFormat.indexOf(MarkupKind.Markdown) !== -1;
}
return this.supportsMarkdown;
}
}

View file

@ -0,0 +1,463 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { DocumentHighlightKind, Location, Range, SymbolKind, TextEdit, FileType } from '../cssLanguageTypes';
import * as l10n from '@vscode/l10n';
import * as nodes from '../parser/cssNodes';
import { Symbols } from '../parser/cssSymbolScope';
import { getColorValue, hslFromColor, hwbFromColor } from '../languageFacts/facts';
import { startsWith } from '../utils/strings';
import { dirname, joinPath } from '../utils/resources';
const startsWithSchemeRegex = /^\w+:\/\//;
const startsWithData = /^data:/;
export class CSSNavigation {
constructor(fileSystemProvider, resolveModuleReferences) {
this.fileSystemProvider = fileSystemProvider;
this.resolveModuleReferences = resolveModuleReferences;
}
findDefinition(document, position, stylesheet) {
const symbols = new Symbols(stylesheet);
const offset = document.offsetAt(position);
const node = nodes.getNodeAtOffset(stylesheet, offset);
if (!node) {
return null;
}
const symbol = symbols.findSymbolFromNode(node);
if (!symbol) {
return null;
}
return {
uri: document.uri,
range: getRange(symbol.node, document)
};
}
findReferences(document, position, stylesheet) {
const highlights = this.findDocumentHighlights(document, position, stylesheet);
return highlights.map(h => {
return {
uri: document.uri,
range: h.range
};
});
}
getHighlightNode(document, position, stylesheet) {
const offset = document.offsetAt(position);
let node = nodes.getNodeAtOffset(stylesheet, offset);
if (!node || node.type === nodes.NodeType.Stylesheet || node.type === nodes.NodeType.Declarations) {
return;
}
if (node.type === nodes.NodeType.Identifier && node.parent && node.parent.type === nodes.NodeType.ClassSelector) {
node = node.parent;
}
return node;
}
findDocumentHighlights(document, position, stylesheet) {
const result = [];
const node = this.getHighlightNode(document, position, stylesheet);
if (!node) {
return result;
}
const symbols = new Symbols(stylesheet);
const symbol = symbols.findSymbolFromNode(node);
const name = node.getText();
stylesheet.accept(candidate => {
if (symbol) {
if (symbols.matchesSymbol(candidate, symbol)) {
result.push({
kind: getHighlightKind(candidate),
range: getRange(candidate, document)
});
return false;
}
}
else if (node && node.type === candidate.type && candidate.matches(name)) {
// Same node type and data
result.push({
kind: getHighlightKind(candidate),
range: getRange(candidate, document)
});
}
return true;
});
return result;
}
isRawStringDocumentLinkNode(node) {
return node.type === nodes.NodeType.Import;
}
findDocumentLinks(document, stylesheet, documentContext) {
const linkData = this.findUnresolvedLinks(document, stylesheet);
const resolvedLinks = [];
for (let data of linkData) {
const link = data.link;
const target = link.target;
if (!target || startsWithData.test(target)) {
// no links for data:
}
else if (startsWithSchemeRegex.test(target)) {
resolvedLinks.push(link);
}
else {
const resolved = documentContext.resolveReference(target, document.uri);
if (resolved) {
link.target = resolved;
}
resolvedLinks.push(link);
}
}
return resolvedLinks;
}
async findDocumentLinks2(document, stylesheet, documentContext) {
const linkData = this.findUnresolvedLinks(document, stylesheet);
const resolvedLinks = [];
for (let data of linkData) {
const link = data.link;
const target = link.target;
if (!target || startsWithData.test(target)) {
// no links for data:
}
else if (startsWithSchemeRegex.test(target)) {
resolvedLinks.push(link);
}
else {
const resolvedTarget = await this.resolveReference(target, document.uri, documentContext, data.isRawLink);
if (resolvedTarget !== undefined) {
link.target = resolvedTarget;
resolvedLinks.push(link);
}
}
}
return resolvedLinks;
}
findUnresolvedLinks(document, stylesheet) {
const result = [];
const collect = (uriStringNode) => {
let rawUri = uriStringNode.getText();
const range = getRange(uriStringNode, document);
// Make sure the range is not empty
if (range.start.line === range.end.line && range.start.character === range.end.character) {
return;
}
if (startsWith(rawUri, `'`) || startsWith(rawUri, `"`)) {
rawUri = rawUri.slice(1, -1);
}
const isRawLink = uriStringNode.parent ? this.isRawStringDocumentLinkNode(uriStringNode.parent) : false;
result.push({ link: { target: rawUri, range }, isRawLink });
};
stylesheet.accept(candidate => {
if (candidate.type === nodes.NodeType.URILiteral) {
const first = candidate.getChild(0);
if (first) {
collect(first);
}
return false;
}
/**
* In @import, it is possible to include links that do not use `url()`
* For example, `@import 'foo.css';`
*/
if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) {
const rawText = candidate.getText();
if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) {
collect(candidate);
}
return false;
}
return true;
});
return result;
}
findSymbolInformations(document, stylesheet) {
const result = [];
const addSymbolInformation = (name, kind, symbolNodeOrRange) => {
const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange;
const entry = {
name: name || l10n.t('<undefined>'),
kind,
location: Location.create(document.uri, range)
};
result.push(entry);
};
this.collectDocumentSymbols(document, stylesheet, addSymbolInformation);
return result;
}
findDocumentSymbols(document, stylesheet) {
const result = [];
const parents = [];
const addDocumentSymbol = (name, kind, symbolNodeOrRange, nameNodeOrRange, bodyNode) => {
const range = symbolNodeOrRange instanceof nodes.Node ? getRange(symbolNodeOrRange, document) : symbolNodeOrRange;
let selectionRange = nameNodeOrRange instanceof nodes.Node ? getRange(nameNodeOrRange, document) : nameNodeOrRange;
if (!selectionRange || !containsRange(range, selectionRange)) {
selectionRange = Range.create(range.start, range.start);
}
const entry = {
name: name || l10n.t('<undefined>'),
kind,
range,
selectionRange
};
let top = parents.pop();
while (top && !containsRange(top[1], range)) {
top = parents.pop();
}
if (top) {
const topSymbol = top[0];
if (!topSymbol.children) {
topSymbol.children = [];
}
topSymbol.children.push(entry);
parents.push(top); // put back top
}
else {
result.push(entry);
}
if (bodyNode) {
parents.push([entry, getRange(bodyNode, document)]);
}
};
this.collectDocumentSymbols(document, stylesheet, addDocumentSymbol);
return result;
}
collectDocumentSymbols(document, stylesheet, collect) {
stylesheet.accept(node => {
if (node instanceof nodes.RuleSet) {
for (const selector of node.getSelectors().getChildren()) {
if (selector instanceof nodes.Selector) {
const range = Range.create(document.positionAt(selector.offset), document.positionAt(node.end));
collect(selector.getText(), SymbolKind.Class, range, selector, node.getDeclarations());
}
}
}
else if (node instanceof nodes.VariableDeclaration) {
collect(node.getName(), SymbolKind.Variable, node, node.getVariable(), undefined);
}
else if (node instanceof nodes.MixinDeclaration) {
collect(node.getName(), SymbolKind.Method, node, node.getIdentifier(), node.getDeclarations());
}
else if (node instanceof nodes.FunctionDeclaration) {
collect(node.getName(), SymbolKind.Function, node, node.getIdentifier(), node.getDeclarations());
}
else if (node instanceof nodes.Keyframe) {
const name = l10n.t("@keyframes {0}", node.getName());
collect(name, SymbolKind.Class, node, node.getIdentifier(), node.getDeclarations());
}
else if (node instanceof nodes.FontFace) {
const name = l10n.t("@font-face");
collect(name, SymbolKind.Class, node, undefined, node.getDeclarations());
}
else if (node instanceof nodes.Media) {
const mediaList = node.getChild(0);
if (mediaList instanceof nodes.Medialist) {
const name = '@media ' + mediaList.getText();
collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations());
}
}
return true;
});
}
findDocumentColors(document, stylesheet) {
const result = [];
stylesheet.accept((node) => {
const colorInfo = getColorInformation(node, document);
if (colorInfo) {
result.push(colorInfo);
}
return true;
});
return result;
}
getColorPresentations(document, stylesheet, color, range) {
const result = [];
const red256 = Math.round(color.red * 255), green256 = Math.round(color.green * 255), blue256 = Math.round(color.blue * 255);
let label;
if (color.alpha === 1) {
label = `rgb(${red256}, ${green256}, ${blue256})`;
}
else {
label = `rgba(${red256}, ${green256}, ${blue256}, ${color.alpha})`;
}
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
if (color.alpha === 1) {
label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}`;
}
else {
label = `#${toTwoDigitHex(red256)}${toTwoDigitHex(green256)}${toTwoDigitHex(blue256)}${toTwoDigitHex(Math.round(color.alpha * 255))}`;
}
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
const hsl = hslFromColor(color);
if (hsl.a === 1) {
label = `hsl(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%)`;
}
else {
label = `hsla(${hsl.h}, ${Math.round(hsl.s * 100)}%, ${Math.round(hsl.l * 100)}%, ${hsl.a})`;
}
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
const hwb = hwbFromColor(color);
if (hwb.a === 1) {
label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}%)`;
}
else {
label = `hwb(${hwb.h} ${Math.round(hwb.w * 100)}% ${Math.round(hwb.b * 100)}% / ${hwb.a})`;
}
result.push({ label: label, textEdit: TextEdit.replace(range, label) });
return result;
}
prepareRename(document, position, stylesheet) {
const node = this.getHighlightNode(document, position, stylesheet);
if (node) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}
}
doRename(document, position, newName, stylesheet) {
const highlights = this.findDocumentHighlights(document, position, stylesheet);
const edits = highlights.map(h => TextEdit.replace(h.range, newName));
return {
changes: { [document.uri]: edits }
};
}
async resolveModuleReference(ref, documentUri, documentContext) {
if (startsWith(documentUri, 'file://')) {
const moduleName = getModuleNameFromPath(ref);
if (moduleName && moduleName !== '.' && moduleName !== '..') {
const rootFolderUri = documentContext.resolveReference('/', documentUri);
const documentFolderUri = dirname(documentUri);
const modulePath = await this.resolvePathToModule(moduleName, documentFolderUri, rootFolderUri);
if (modulePath) {
const pathWithinModule = ref.substring(moduleName.length + 1);
return joinPath(modulePath, pathWithinModule);
}
}
}
return undefined;
}
async mapReference(target, isRawLink) {
return target;
}
async resolveReference(target, documentUri, documentContext, isRawLink = false) {
// Following [css-loader](https://github.com/webpack-contrib/css-loader#url)
// and [sass-loader's](https://github.com/webpack-contrib/sass-loader#imports)
// convention, if an import path starts with ~ then use node module resolution
// *unless* it starts with "~/" as this refers to the user's home directory.
if (target[0] === '~' && target[1] !== '/' && this.fileSystemProvider) {
target = target.substring(1);
return this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink);
}
const ref = await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink);
// Following [less-loader](https://github.com/webpack-contrib/less-loader#imports)
// and [sass-loader's](https://github.com/webpack-contrib/sass-loader#resolving-import-at-rules)
// new resolving import at-rules (~ is deprecated). The loader will first try to resolve @import as a relative path. If it cannot be resolved,
// then the loader will try to resolve @import inside node_modules.
if (this.resolveModuleReferences) {
if (ref && await this.fileExists(ref)) {
return ref;
}
const moduleReference = await this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink);
if (moduleReference) {
return moduleReference;
}
}
// fall back. it might not exists
return ref;
}
async resolvePathToModule(_moduleName, documentFolderUri, rootFolderUri) {
// resolve the module relative to the document. We can't use `require` here as the code is webpacked.
const packPath = joinPath(documentFolderUri, 'node_modules', _moduleName, 'package.json');
if (await this.fileExists(packPath)) {
return dirname(packPath);
}
else if (rootFolderUri && documentFolderUri.startsWith(rootFolderUri) && (documentFolderUri.length !== rootFolderUri.length)) {
return this.resolvePathToModule(_moduleName, dirname(documentFolderUri), rootFolderUri);
}
return undefined;
}
async fileExists(uri) {
if (!this.fileSystemProvider) {
return false;
}
try {
const stat = await this.fileSystemProvider.stat(uri);
if (stat.type === FileType.Unknown && stat.size === -1) {
return false;
}
return true;
}
catch (err) {
return false;
}
}
}
function getColorInformation(node, document) {
const color = getColorValue(node);
if (color) {
const range = getRange(node, document);
return { color, range };
}
return null;
}
function getRange(node, document) {
return Range.create(document.positionAt(node.offset), document.positionAt(node.end));
}
/**
* Test if `otherRange` is in `range`. If the ranges are equal, will return true.
*/
function containsRange(range, otherRange) {
const otherStartLine = otherRange.start.line, otherEndLine = otherRange.end.line;
const rangeStartLine = range.start.line, rangeEndLine = range.end.line;
if (otherStartLine < rangeStartLine || otherEndLine < rangeStartLine) {
return false;
}
if (otherStartLine > rangeEndLine || otherEndLine > rangeEndLine) {
return false;
}
if (otherStartLine === rangeStartLine && otherRange.start.character < range.start.character) {
return false;
}
if (otherEndLine === rangeEndLine && otherRange.end.character > range.end.character) {
return false;
}
return true;
}
function getHighlightKind(node) {
if (node.type === nodes.NodeType.Selector) {
return DocumentHighlightKind.Write;
}
if (node instanceof nodes.Identifier) {
if (node.parent && node.parent instanceof nodes.Property) {
if (node.isCustomProperty) {
return DocumentHighlightKind.Write;
}
}
}
if (node.parent) {
switch (node.parent.type) {
case nodes.NodeType.FunctionDeclaration:
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.Keyframe:
case nodes.NodeType.VariableDeclaration:
case nodes.NodeType.FunctionParameter:
return DocumentHighlightKind.Write;
}
}
return DocumentHighlightKind.Read;
}
function toTwoDigitHex(n) {
const r = n.toString(16);
return r.length !== 2 ? '0' + r : r;
}
function getModuleNameFromPath(path) {
const firstSlash = path.indexOf('/');
if (firstSlash === -1) {
return '';
}
// If a scoped module (starts with @) then get up until second instance of '/', or to the end of the string for root-level imports.
if (path[0] === '@') {
const secondSlash = path.indexOf('/', firstSlash + 1);
if (secondSlash === -1) {
return path;
}
return path.substring(0, secondSlash);
}
// Otherwise get until first instance of '/'
return path.substring(0, firstSlash);
}

View file

@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Range, SelectionRange } from '../cssLanguageTypes';
import { NodeType } from '../parser/cssNodes';
export function getSelectionRanges(document, positions, stylesheet) {
function getSelectionRange(position) {
const applicableRanges = getApplicableRanges(position);
let current = undefined;
for (let index = applicableRanges.length - 1; index >= 0; index--) {
current = SelectionRange.create(Range.create(document.positionAt(applicableRanges[index][0]), document.positionAt(applicableRanges[index][1])), current);
}
if (!current) {
current = SelectionRange.create(Range.create(position, position));
}
return current;
}
return positions.map(getSelectionRange);
function getApplicableRanges(position) {
const offset = document.offsetAt(position);
let currNode = stylesheet.findChildAtOffset(offset, true);
if (!currNode) {
return [];
}
const result = [];
while (currNode) {
if (currNode.parent &&
currNode.offset === currNode.parent.offset &&
currNode.end === currNode.parent.end) {
currNode = currNode.parent;
continue;
}
// The `{ }` part of `.a { }`
if (currNode.type === NodeType.Declarations) {
if (offset > currNode.offset && offset < currNode.end) {
// Return `{ }` and the range inside `{` and `}`
result.push([currNode.offset + 1, currNode.end - 1]);
}
}
result.push([currNode.offset, currNode.end]);
currNode = currNode.parent;
}
return result;
}
}

View file

@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nodes from '../parser/cssNodes';
import { LintConfigurationSettings, Rules } from './lintRules';
import { LintVisitor } from './lint';
import { Range, DiagnosticSeverity } from '../cssLanguageTypes';
export class CSSValidation {
constructor(cssDataManager) {
this.cssDataManager = cssDataManager;
}
configure(settings) {
this.settings = settings;
}
doValidation(document, stylesheet, settings = this.settings) {
if (settings && settings.validate === false) {
return [];
}
const entries = [];
entries.push.apply(entries, nodes.ParseErrorCollector.entries(stylesheet));
entries.push.apply(entries, LintVisitor.entries(stylesheet, document, new LintConfigurationSettings(settings && settings.lint), this.cssDataManager));
const ruleIds = [];
for (const r in Rules) {
ruleIds.push(Rules[r].id);
}
function toDiagnostic(marker) {
const range = Range.create(document.positionAt(marker.getOffset()), document.positionAt(marker.getOffset() + marker.getLength()));
const source = document.languageId;
return {
code: marker.getRule().id,
source: source,
message: marker.getMessage(),
severity: marker.getLevel() === nodes.Level.Warning ? DiagnosticSeverity.Warning : DiagnosticSeverity.Error,
range: range
};
}
return entries.filter(entry => entry.getLevel() !== nodes.Level.Ignore).map(toDiagnostic);
}
}

View file

@ -0,0 +1,378 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CSSCompletion } from './cssCompletion';
import { CompletionItemKind, InsertTextFormat, TextEdit } from '../cssLanguageTypes';
import * as l10n from '@vscode/l10n';
class LESSCompletion extends CSSCompletion {
constructor(lsOptions, cssDataManager) {
super('@', lsOptions, cssDataManager);
}
createFunctionProposals(proposals, existingNode, sortToEnd, result) {
for (const p of proposals) {
const item = {
label: p.name,
detail: p.example,
documentation: p.description,
textEdit: TextEdit.replace(this.getCompletionRange(existingNode), p.name + '($0)'),
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Function
};
if (sortToEnd) {
item.sortText = 'z';
}
result.items.push(item);
}
return result;
}
getTermProposals(entry, existingNode, result) {
let functions = LESSCompletion.builtInProposals;
if (entry) {
functions = functions.filter(f => !f.type || !entry.restrictions || entry.restrictions.indexOf(f.type) !== -1);
}
this.createFunctionProposals(functions, existingNode, true, result);
return super.getTermProposals(entry, existingNode, result);
}
getColorProposals(entry, existingNode, result) {
this.createFunctionProposals(LESSCompletion.colorProposals, existingNode, false, result);
return super.getColorProposals(entry, existingNode, result);
}
getCompletionsForDeclarationProperty(declaration, result) {
this.getCompletionsForSelector(null, true, result);
return super.getCompletionsForDeclarationProperty(declaration, result);
}
}
LESSCompletion.builtInProposals = [
// Boolean functions
{
'name': 'if',
'example': 'if(condition, trueValue [, falseValue]);',
'description': l10n.t('returns one of two values depending on a condition.')
},
{
'name': 'boolean',
'example': 'boolean(condition);',
'description': l10n.t('"store" a boolean test for later evaluation in a guard or if().')
},
// List functions
{
'name': 'length',
'example': 'length(@list);',
'description': l10n.t('returns the number of elements in a value list')
},
{
'name': 'extract',
'example': 'extract(@list, index);',
'description': l10n.t('returns a value at the specified position in the list')
},
{
'name': 'range',
'example': 'range([start, ] end [, step]);',
'description': l10n.t('generate a list spanning a range of values')
},
{
'name': 'each',
'example': 'each(@list, ruleset);',
'description': l10n.t('bind the evaluation of a ruleset to each member of a list.')
},
// Other built-ins
{
'name': 'escape',
'example': 'escape(@string);',
'description': l10n.t('URL encodes a string')
},
{
'name': 'e',
'example': 'e(@string);',
'description': l10n.t('escape string content')
},
{
'name': 'replace',
'example': 'replace(@string, @pattern, @replacement[, @flags]);',
'description': l10n.t('string replace')
},
{
'name': 'unit',
'example': 'unit(@dimension, [@unit: \'\']);',
'description': l10n.t('remove or change the unit of a dimension')
},
{
'name': 'color',
'example': 'color(@string);',
'description': l10n.t('parses a string to a color'),
'type': 'color'
},
{
'name': 'convert',
'example': 'convert(@value, unit);',
'description': l10n.t('converts numbers from one type into another')
},
{
'name': 'data-uri',
'example': 'data-uri([mimetype,] url);',
'description': l10n.t('inlines a resource and falls back to `url()`'),
'type': 'url'
},
{
'name': 'abs',
'description': l10n.t('absolute value of a number'),
'example': 'abs(number);'
},
{
'name': 'acos',
'description': l10n.t('arccosine - inverse of cosine function'),
'example': 'acos(number);'
},
{
'name': 'asin',
'description': l10n.t('arcsine - inverse of sine function'),
'example': 'asin(number);'
},
{
'name': 'ceil',
'example': 'ceil(@number);',
'description': l10n.t('rounds up to an integer')
},
{
'name': 'cos',
'description': l10n.t('cosine function'),
'example': 'cos(number);'
},
{
'name': 'floor',
'description': l10n.t('rounds down to an integer'),
'example': 'floor(@number);'
},
{
'name': 'percentage',
'description': l10n.t('converts to a %, e.g. 0.5 > 50%'),
'example': 'percentage(@number);',
'type': 'percentage'
},
{
'name': 'round',
'description': l10n.t('rounds a number to a number of places'),
'example': 'round(number, [places: 0]);'
},
{
'name': 'sqrt',
'description': l10n.t('calculates square root of a number'),
'example': 'sqrt(number);'
},
{
'name': 'sin',
'description': l10n.t('sine function'),
'example': 'sin(number);'
},
{
'name': 'tan',
'description': l10n.t('tangent function'),
'example': 'tan(number);'
},
{
'name': 'atan',
'description': l10n.t('arctangent - inverse of tangent function'),
'example': 'atan(number);'
},
{
'name': 'pi',
'description': l10n.t('returns pi'),
'example': 'pi();'
},
{
'name': 'pow',
'description': l10n.t('first argument raised to the power of the second argument'),
'example': 'pow(@base, @exponent);'
},
{
'name': 'mod',
'description': l10n.t('first argument modulus second argument'),
'example': 'mod(number, number);'
},
{
'name': 'min',
'description': l10n.t('returns the lowest of one or more values'),
'example': 'min(@x, @y);'
},
{
'name': 'max',
'description': l10n.t('returns the lowest of one or more values'),
'example': 'max(@x, @y);'
}
];
LESSCompletion.colorProposals = [
{
'name': 'argb',
'example': 'argb(@color);',
'description': l10n.t('creates a #AARRGGBB')
},
{
'name': 'hsl',
'example': 'hsl(@hue, @saturation, @lightness);',
'description': l10n.t('creates a color')
},
{
'name': 'hsla',
'example': 'hsla(@hue, @saturation, @lightness, @alpha);',
'description': l10n.t('creates a color')
},
{
'name': 'hsv',
'example': 'hsv(@hue, @saturation, @value);',
'description': l10n.t('creates a color')
},
{
'name': 'hsva',
'example': 'hsva(@hue, @saturation, @value, @alpha);',
'description': l10n.t('creates a color')
},
{
'name': 'hue',
'example': 'hue(@color);',
'description': l10n.t('returns the `hue` channel of `@color` in the HSL space')
},
{
'name': 'saturation',
'example': 'saturation(@color);',
'description': l10n.t('returns the `saturation` channel of `@color` in the HSL space')
},
{
'name': 'lightness',
'example': 'lightness(@color);',
'description': l10n.t('returns the `lightness` channel of `@color` in the HSL space')
},
{
'name': 'hsvhue',
'example': 'hsvhue(@color);',
'description': l10n.t('returns the `hue` channel of `@color` in the HSV space')
},
{
'name': 'hsvsaturation',
'example': 'hsvsaturation(@color);',
'description': l10n.t('returns the `saturation` channel of `@color` in the HSV space')
},
{
'name': 'hsvvalue',
'example': 'hsvvalue(@color);',
'description': l10n.t('returns the `value` channel of `@color` in the HSV space')
},
{
'name': 'red',
'example': 'red(@color);',
'description': l10n.t('returns the `red` channel of `@color`')
},
{
'name': 'green',
'example': 'green(@color);',
'description': l10n.t('returns the `green` channel of `@color`')
},
{
'name': 'blue',
'example': 'blue(@color);',
'description': l10n.t('returns the `blue` channel of `@color`')
},
{
'name': 'alpha',
'example': 'alpha(@color);',
'description': l10n.t('returns the `alpha` channel of `@color`')
},
{
'name': 'luma',
'example': 'luma(@color);',
'description': l10n.t('returns the `luma` value (perceptual brightness) of `@color`')
},
{
'name': 'saturate',
'example': 'saturate(@color, 10%);',
'description': l10n.t('return `@color` 10% points more saturated')
},
{
'name': 'desaturate',
'example': 'desaturate(@color, 10%);',
'description': l10n.t('return `@color` 10% points less saturated')
},
{
'name': 'lighten',
'example': 'lighten(@color, 10%);',
'description': l10n.t('return `@color` 10% points lighter')
},
{
'name': 'darken',
'example': 'darken(@color, 10%);',
'description': l10n.t('return `@color` 10% points darker')
},
{
'name': 'fadein',
'example': 'fadein(@color, 10%);',
'description': l10n.t('return `@color` 10% points less transparent')
},
{
'name': 'fadeout',
'example': 'fadeout(@color, 10%);',
'description': l10n.t('return `@color` 10% points more transparent')
},
{
'name': 'fade',
'example': 'fade(@color, 50%);',
'description': l10n.t('return `@color` with 50% transparency')
},
{
'name': 'spin',
'example': 'spin(@color, 10);',
'description': l10n.t('return `@color` with a 10 degree larger in hue')
},
{
'name': 'mix',
'example': 'mix(@color1, @color2, [@weight: 50%]);',
'description': l10n.t('return a mix of `@color1` and `@color2`')
},
{
'name': 'greyscale',
'example': 'greyscale(@color);',
'description': l10n.t('returns a grey, 100% desaturated color'),
},
{
'name': 'contrast',
'example': 'contrast(@color1, [@darkcolor: black], [@lightcolor: white], [@threshold: 43%]);',
'description': l10n.t('return `@darkcolor` if `@color1 is> 43% luma` otherwise return `@lightcolor`, see notes')
},
{
'name': 'multiply',
'example': 'multiply(@color1, @color2);'
},
{
'name': 'screen',
'example': 'screen(@color1, @color2);'
},
{
'name': 'overlay',
'example': 'overlay(@color1, @color2);'
},
{
'name': 'softlight',
'example': 'softlight(@color1, @color2);'
},
{
'name': 'hardlight',
'example': 'hardlight(@color1, @color2);'
},
{
'name': 'difference',
'example': 'difference(@color1, @color2);'
},
{
'name': 'exclusion',
'example': 'exclusion(@color1, @color2);'
},
{
'name': 'average',
'example': 'average(@color1, @color2);'
},
{
'name': 'negation',
'example': 'negation(@color1, @color2);'
}
];
export { LESSCompletion };

View file

@ -0,0 +1,565 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as l10n from '@vscode/l10n';
import * as languageFacts from '../languageFacts/facts';
import * as nodes from '../parser/cssNodes';
import { union } from '../utils/arrays';
import { Rules, Settings } from './lintRules';
import calculateBoxModel, { Element } from './lintUtil';
class NodesByRootMap {
constructor() {
this.data = {};
}
add(root, name, node) {
let entry = this.data[root];
if (!entry) {
entry = { nodes: [], names: [] };
this.data[root] = entry;
}
entry.names.push(name);
if (node) {
entry.nodes.push(node);
}
}
}
class LintVisitor {
static entries(node, document, settings, cssDataManager, entryFilter) {
const visitor = new LintVisitor(document, settings, cssDataManager);
node.acceptVisitor(visitor);
visitor.completeValidations();
return visitor.getEntries(entryFilter);
}
constructor(document, settings, cssDataManager) {
this.cssDataManager = cssDataManager;
this.warnings = [];
this.settings = settings;
this.documentText = document.getText();
this.keyframes = new NodesByRootMap();
this.validProperties = {};
const properties = settings.getSetting(Settings.ValidProperties);
if (Array.isArray(properties)) {
properties.forEach((p) => {
if (typeof p === 'string') {
const name = p.trim().toLowerCase();
if (name.length) {
this.validProperties[name] = true;
}
}
});
}
}
isValidPropertyDeclaration(element) {
const propertyName = element.fullPropertyName;
return this.validProperties[propertyName];
}
fetch(input, s) {
const elements = [];
for (const curr of input) {
if (curr.fullPropertyName === s) {
elements.push(curr);
}
}
return elements;
}
fetchWithValue(input, s, v) {
const elements = [];
for (const inputElement of input) {
if (inputElement.fullPropertyName === s) {
const expression = inputElement.node.getValue();
if (expression && this.findValueInExpression(expression, v)) {
elements.push(inputElement);
}
}
}
return elements;
}
findValueInExpression(expression, v) {
let found = false;
expression.accept(node => {
if (node.type === nodes.NodeType.Identifier && node.matches(v)) {
found = true;
}
return !found;
});
return found;
}
getEntries(filter = (nodes.Level.Warning | nodes.Level.Error)) {
return this.warnings.filter(entry => {
return (entry.getLevel() & filter) !== 0;
});
}
addEntry(node, rule, details) {
const entry = new nodes.Marker(node, rule, this.settings.getRule(rule), details);
this.warnings.push(entry);
}
getMissingNames(expected, actual) {
const expectedClone = expected.slice(0); // clone
for (let i = 0; i < actual.length; i++) {
const k = expectedClone.indexOf(actual[i]);
if (k !== -1) {
expectedClone[k] = null;
}
}
let result = null;
for (let i = 0; i < expectedClone.length; i++) {
const curr = expectedClone[i];
if (curr) {
if (result === null) {
result = l10n.t("'{0}'", curr);
}
else {
result = l10n.t("{0}, '{1}'", result, curr);
}
}
}
return result;
}
visitNode(node) {
switch (node.type) {
case nodes.NodeType.UnknownAtRule:
return this.visitUnknownAtRule(node);
case nodes.NodeType.Keyframe:
return this.visitKeyframe(node);
case nodes.NodeType.FontFace:
return this.visitFontFace(node);
case nodes.NodeType.Ruleset:
return this.visitRuleSet(node);
case nodes.NodeType.SimpleSelector:
return this.visitSimpleSelector(node);
case nodes.NodeType.Function:
return this.visitFunction(node);
case nodes.NodeType.NumericValue:
return this.visitNumericValue(node);
case nodes.NodeType.Import:
return this.visitImport(node);
case nodes.NodeType.HexColorValue:
return this.visitHexColorValue(node);
case nodes.NodeType.Prio:
return this.visitPrio(node);
case nodes.NodeType.IdentifierSelector:
return this.visitIdentifierSelector(node);
}
return true;
}
completeValidations() {
this.validateKeyframes();
}
visitUnknownAtRule(node) {
const atRuleName = node.getChild(0);
if (!atRuleName) {
return false;
}
const atDirective = this.cssDataManager.getAtDirective(atRuleName.getText());
if (atDirective) {
return false;
}
this.addEntry(atRuleName, Rules.UnknownAtRules, `Unknown at rule ${atRuleName.getText()}`);
return true;
}
visitKeyframe(node) {
const keyword = node.getKeyword();
if (!keyword) {
return false;
}
const text = keyword.getText();
this.keyframes.add(node.getName(), text, (text !== '@keyframes') ? keyword : null);
return true;
}
validateKeyframes() {
// @keyframe and it's vendor specific alternatives
// @keyframe should be included
const expected = ['@-webkit-keyframes', '@-moz-keyframes', '@-o-keyframes'];
for (const name in this.keyframes.data) {
const actual = this.keyframes.data[name].names;
const needsStandard = (actual.indexOf('@keyframes') === -1);
if (!needsStandard && actual.length === 1) {
continue; // only the non-vendor specific keyword is used, that's fine, no warning
}
const missingVendorSpecific = this.getMissingNames(expected, actual);
if (missingVendorSpecific || needsStandard) {
for (const node of this.keyframes.data[name].nodes) {
if (needsStandard) {
const message = l10n.t("Always define standard rule '@keyframes' when defining keyframes.");
this.addEntry(node, Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
}
if (missingVendorSpecific) {
const message = l10n.t("Always include all vendor specific rules: Missing: {0}", missingVendorSpecific);
this.addEntry(node, Rules.AllVendorPrefixes, message);
}
}
}
}
return true;
}
visitSimpleSelector(node) {
/////////////////////////////////////////////////////////////
// Lint - The universal selector (*) is known to be slow.
/////////////////////////////////////////////////////////////
const firstChar = this.documentText.charAt(node.offset);
if (node.length === 1 && firstChar === '*') {
this.addEntry(node, Rules.UniversalSelector);
}
return true;
}
visitIdentifierSelector(node) {
/////////////////////////////////////////////////////////////
// Lint - Avoid id selectors
/////////////////////////////////////////////////////////////
this.addEntry(node, Rules.AvoidIdSelector);
return true;
}
visitImport(node) {
/////////////////////////////////////////////////////////////
// Lint - Import statements shouldn't be used, because they aren't offering parallel downloads.
/////////////////////////////////////////////////////////////
this.addEntry(node, Rules.ImportStatemement);
return true;
}
visitRuleSet(node) {
/////////////////////////////////////////////////////////////
// Lint - Don't use empty rulesets.
/////////////////////////////////////////////////////////////
const declarations = node.getDeclarations();
if (!declarations) {
// syntax error
return false;
}
if (!declarations.hasChildren()) {
this.addEntry(node.getSelectors(), Rules.EmptyRuleSet);
}
const propertyTable = [];
for (const element of declarations.getChildren()) {
if (element instanceof nodes.Declaration) {
propertyTable.push(new Element(element));
}
}
/////////////////////////////////////////////////////////////
// the rule warns when it finds:
// width being used with border, border-left, border-right, padding, padding-left, or padding-right
// height being used with border, border-top, border-bottom, padding, padding-top, or padding-bottom
// No error when box-sizing property is specified, as it assumes the user knows what he's doing.
// see https://github.com/CSSLint/csslint/wiki/Beware-of-box-model-size
/////////////////////////////////////////////////////////////
const boxModel = calculateBoxModel(propertyTable);
if (boxModel.width) {
let properties = [];
if (boxModel.right.value) {
properties = union(properties, boxModel.right.properties);
}
if (boxModel.left.value) {
properties = union(properties, boxModel.left.properties);
}
if (properties.length !== 0) {
for (const item of properties) {
this.addEntry(item.node, Rules.BewareOfBoxModelSize);
}
this.addEntry(boxModel.width.node, Rules.BewareOfBoxModelSize);
}
}
if (boxModel.height) {
let properties = [];
if (boxModel.top.value) {
properties = union(properties, boxModel.top.properties);
}
if (boxModel.bottom.value) {
properties = union(properties, boxModel.bottom.properties);
}
if (properties.length !== 0) {
for (const item of properties) {
this.addEntry(item.node, Rules.BewareOfBoxModelSize);
}
this.addEntry(boxModel.height.node, Rules.BewareOfBoxModelSize);
}
}
/////////////////////////////////////////////////////////////
// Properties ignored due to display
/////////////////////////////////////////////////////////////
// With 'display: inline-block', 'float' has no effect
let displayElems = this.fetchWithValue(propertyTable, 'display', 'inline-block');
if (displayElems.length > 0) {
const elem = this.fetch(propertyTable, 'float');
for (let index = 0; index < elem.length; index++) {
const node = elem[index].node;
const value = node.getValue();
if (value && !value.matches('none')) {
this.addEntry(node, Rules.PropertyIgnoredDueToDisplay, l10n.t("inline-block is ignored due to the float. If 'float' has a value other than 'none', the box is floated and 'display' is treated as 'block'"));
}
}
}
// With 'display: block', 'vertical-align' has no effect
displayElems = this.fetchWithValue(propertyTable, 'display', 'block');
if (displayElems.length > 0) {
const elem = this.fetch(propertyTable, 'vertical-align');
for (let index = 0; index < elem.length; index++) {
this.addEntry(elem[index].node, Rules.PropertyIgnoredDueToDisplay, l10n.t("Property is ignored due to the display. With 'display: block', vertical-align should not be used."));
}
}
/////////////////////////////////////////////////////////////
// Avoid 'float'
/////////////////////////////////////////////////////////////
const elements = this.fetch(propertyTable, 'float');
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
if (!this.isValidPropertyDeclaration(element)) {
this.addEntry(element.node, Rules.AvoidFloat);
}
}
/////////////////////////////////////////////////////////////
// Don't use duplicate declarations.
/////////////////////////////////////////////////////////////
for (let i = 0; i < propertyTable.length; i++) {
const element = propertyTable[i];
if (element.fullPropertyName !== 'background' && !this.validProperties[element.fullPropertyName]) {
const value = element.node.getValue();
if (value && this.documentText.charAt(value.offset) !== '-') {
const elements = this.fetch(propertyTable, element.fullPropertyName);
if (elements.length > 1) {
for (let k = 0; k < elements.length; k++) {
const value = elements[k].node.getValue();
if (value && this.documentText.charAt(value.offset) !== '-' && elements[k] !== element) {
this.addEntry(element.node, Rules.DuplicateDeclarations);
}
}
}
}
}
}
/////////////////////////////////////////////////////////////
// Unknown propery & When using a vendor-prefixed gradient, make sure to use them all.
/////////////////////////////////////////////////////////////
const isExportBlock = node.getSelectors().matches(":export");
if (!isExportBlock) {
const propertiesBySuffix = new NodesByRootMap();
let containsUnknowns = false;
for (const element of propertyTable) {
const decl = element.node;
if (this.isCSSDeclaration(decl)) {
let name = element.fullPropertyName;
const firstChar = name.charAt(0);
if (firstChar === '-') {
if (name.charAt(1) !== '-') { // avoid css variables
if (!this.cssDataManager.isKnownProperty(name) && !this.validProperties[name]) {
this.addEntry(decl.getProperty(), Rules.UnknownVendorSpecificProperty);
}
const nonPrefixedName = decl.getNonPrefixedPropertyName();
propertiesBySuffix.add(nonPrefixedName, name, decl.getProperty());
}
}
else {
const fullName = name;
if (firstChar === '*' || firstChar === '_') {
this.addEntry(decl.getProperty(), Rules.IEStarHack);
name = name.substr(1);
}
// _property and *property might be contributed via custom data
if (!this.cssDataManager.isKnownProperty(fullName) && !this.cssDataManager.isKnownProperty(name)) {
if (!this.validProperties[name]) {
this.addEntry(decl.getProperty(), Rules.UnknownProperty, l10n.t("Unknown property: '{0}'", decl.getFullPropertyName()));
}
}
propertiesBySuffix.add(name, name, null); // don't pass the node as we don't show errors on the standard
}
}
else {
containsUnknowns = true;
}
}
if (!containsUnknowns) { // don't perform this test if there are
for (const suffix in propertiesBySuffix.data) {
const entry = propertiesBySuffix.data[suffix];
const actual = entry.names;
const needsStandard = this.cssDataManager.isStandardProperty(suffix) && (actual.indexOf(suffix) === -1);
if (!needsStandard && actual.length === 1) {
continue; // only the non-vendor specific rule is used, that's fine, no warning
}
/**
* We should ignore missing standard properties, if there's an explicit contextual reference to a
* vendor specific pseudo-element selector with the same vendor (prefix)
*
* (See https://github.com/microsoft/vscode/issues/164350)
*/
const entriesThatNeedStandard = new Set(needsStandard ? entry.nodes : []);
if (needsStandard) {
const pseudoElements = this.getContextualVendorSpecificPseudoElements(node);
for (const node of entry.nodes) {
const propertyName = node.getName();
const prefix = propertyName.substring(0, propertyName.length - suffix.length);
if (pseudoElements.some(x => x.startsWith(prefix))) {
entriesThatNeedStandard.delete(node);
}
}
}
const expected = [];
for (let i = 0, len = LintVisitor.prefixes.length; i < len; i++) {
const prefix = LintVisitor.prefixes[i];
if (this.cssDataManager.isStandardProperty(prefix + suffix)) {
expected.push(prefix + suffix);
}
}
const missingVendorSpecific = this.getMissingNames(expected, actual);
if (missingVendorSpecific || needsStandard) {
for (const node of entry.nodes) {
if (needsStandard && entriesThatNeedStandard.has(node)) {
const message = l10n.t("Also define the standard property '{0}' for compatibility", suffix);
this.addEntry(node, Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
}
if (missingVendorSpecific) {
const message = l10n.t("Always include all vendor specific properties: Missing: {0}", missingVendorSpecific);
this.addEntry(node, Rules.AllVendorPrefixes, message);
}
}
}
}
}
}
return true;
}
/**
* Walks up the syntax tree (starting from given `node`) and captures vendor
* specific pseudo-element selectors.
* @returns An array of vendor specific pseudo-elements; or empty if none
* was found.
*/
getContextualVendorSpecificPseudoElements(node) {
function walkDown(s, n) {
for (const child of n.getChildren()) {
if (child.type === nodes.NodeType.PseudoSelector) {
const pseudoElement = child.getChildren()[0]?.getText();
if (pseudoElement) {
s.add(pseudoElement);
}
}
walkDown(s, child);
}
}
function walkUp(s, n) {
if (n.type === nodes.NodeType.Ruleset) {
for (const selector of n.getSelectors().getChildren()) {
walkDown(s, selector);
}
}
return n.parent ? walkUp(s, n.parent) : undefined;
}
const result = new Set();
walkUp(result, node);
return Array.from(result);
}
visitPrio(node) {
/////////////////////////////////////////////////////////////
// Don't use !important
/////////////////////////////////////////////////////////////
this.addEntry(node, Rules.AvoidImportant);
return true;
}
visitNumericValue(node) {
/////////////////////////////////////////////////////////////
// 0 has no following unit
/////////////////////////////////////////////////////////////
const funcDecl = node.findParent(nodes.NodeType.Function);
if (funcDecl && funcDecl.getName() === 'calc') {
return true;
}
const decl = node.findParent(nodes.NodeType.Declaration);
if (decl) {
const declValue = decl.getValue();
if (declValue) {
const value = node.getValue();
if (!value.unit || languageFacts.units.length.indexOf(value.unit.toLowerCase()) === -1) {
return true;
}
if (parseFloat(value.value) === 0.0 && !!value.unit && !this.validProperties[decl.getFullPropertyName()]) {
this.addEntry(node, Rules.ZeroWithUnit);
}
}
}
return true;
}
visitFontFace(node) {
const declarations = node.getDeclarations();
if (!declarations) {
// syntax error
return false;
}
let definesSrc = false, definesFontFamily = false;
let containsUnknowns = false;
for (const node of declarations.getChildren()) {
if (this.isCSSDeclaration(node)) {
const name = node.getProperty().getName().toLowerCase();
if (name === 'src') {
definesSrc = true;
}
if (name === 'font-family') {
definesFontFamily = true;
}
}
else {
containsUnknowns = true;
}
}
if (!containsUnknowns && (!definesSrc || !definesFontFamily)) {
this.addEntry(node, Rules.RequiredPropertiesForFontFace);
}
return true;
}
isCSSDeclaration(node) {
if (node instanceof nodes.Declaration) {
if (!node.getValue()) {
return false;
}
const property = node.getProperty();
if (!property) {
return false;
}
const identifier = property.getIdentifier();
if (!identifier || identifier.containsInterpolation()) {
return false;
}
return true;
}
return false;
}
visitHexColorValue(node) {
// Rule: #eeff0011 or #eeff00 or #ef01 or #ef0
const length = node.length;
if (length !== 9 && length !== 7 && length !== 5 && length !== 4) {
this.addEntry(node, Rules.HexColorLength);
}
return false;
}
visitFunction(node) {
const fnName = node.getName().toLowerCase();
let expectedAttrCount = -1;
let actualAttrCount = 0;
switch (fnName) {
case 'rgb(':
case 'hsl(':
expectedAttrCount = 3;
break;
case 'rgba(':
case 'hsla(':
expectedAttrCount = 4;
break;
}
if (expectedAttrCount !== -1) {
node.getArguments().accept(n => {
if (n instanceof nodes.BinaryExpression) {
actualAttrCount += 1;
return false;
}
return true;
});
if (actualAttrCount !== expectedAttrCount) {
this.addEntry(node, Rules.ArgsInColorFunction);
}
}
return true;
}
}
LintVisitor.prefixes = [
'-ms-', '-moz-', '-o-', '-webkit-', // Quite common
// '-xv-', '-atsc-', '-wap-', '-khtml-', 'mso-', 'prince-', '-ah-', '-hp-', '-ro-', '-rim-', '-tc-' // Quite un-common
];
export { LintVisitor };

View file

@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nodes from '../parser/cssNodes';
import * as l10n from '@vscode/l10n';
const Warning = nodes.Level.Warning;
const Error = nodes.Level.Error;
const Ignore = nodes.Level.Ignore;
export class Rule {
constructor(id, message, defaultValue) {
this.id = id;
this.message = message;
this.defaultValue = defaultValue;
// nothing to do
}
}
export class Setting {
constructor(id, message, defaultValue) {
this.id = id;
this.message = message;
this.defaultValue = defaultValue;
// nothing to do
}
}
export const Rules = {
AllVendorPrefixes: new Rule('compatibleVendorPrefixes', l10n.t("When using a vendor-specific prefix make sure to also include all other vendor-specific properties"), Ignore),
IncludeStandardPropertyWhenUsingVendorPrefix: new Rule('vendorPrefix', l10n.t("When using a vendor-specific prefix also include the standard property"), Warning),
DuplicateDeclarations: new Rule('duplicateProperties', l10n.t("Do not use duplicate style definitions"), Ignore),
EmptyRuleSet: new Rule('emptyRules', l10n.t("Do not use empty rulesets"), Warning),
ImportStatemement: new Rule('importStatement', l10n.t("Import statements do not load in parallel"), Ignore),
BewareOfBoxModelSize: new Rule('boxModel', l10n.t("Do not use width or height when using padding or border"), Ignore),
UniversalSelector: new Rule('universalSelector', l10n.t("The universal selector (*) is known to be slow"), Ignore),
ZeroWithUnit: new Rule('zeroUnits', l10n.t("No unit for zero needed"), Ignore),
RequiredPropertiesForFontFace: new Rule('fontFaceProperties', l10n.t("@font-face rule must define 'src' and 'font-family' properties"), Warning),
HexColorLength: new Rule('hexColorLength', l10n.t("Hex colors must consist of three, four, six or eight hex numbers"), Error),
ArgsInColorFunction: new Rule('argumentsInColorFunction', l10n.t("Invalid number of parameters"), Error),
UnknownProperty: new Rule('unknownProperties', l10n.t("Unknown property."), Warning),
UnknownAtRules: new Rule('unknownAtRules', l10n.t("Unknown at-rule."), Warning),
IEStarHack: new Rule('ieHack', l10n.t("IE hacks are only necessary when supporting IE7 and older"), Ignore),
UnknownVendorSpecificProperty: new Rule('unknownVendorSpecificProperties', l10n.t("Unknown vendor specific property."), Ignore),
PropertyIgnoredDueToDisplay: new Rule('propertyIgnoredDueToDisplay', l10n.t("Property is ignored due to the display."), Warning),
AvoidImportant: new Rule('important', l10n.t("Avoid using !important. It is an indication that the specificity of the entire CSS has gotten out of control and needs to be refactored."), Ignore),
AvoidFloat: new Rule('float', l10n.t("Avoid using 'float'. Floats lead to fragile CSS that is easy to break if one aspect of the layout changes."), Ignore),
AvoidIdSelector: new Rule('idSelector', l10n.t("Selectors should not contain IDs because these rules are too tightly coupled with the HTML."), Ignore),
};
export const Settings = {
ValidProperties: new Setting('validProperties', l10n.t("A list of properties that are not validated against the `unknownProperties` rule."), [])
};
export class LintConfigurationSettings {
constructor(conf = {}) {
this.conf = conf;
}
getRule(rule) {
if (this.conf.hasOwnProperty(rule.id)) {
const level = toLevel(this.conf[rule.id]);
if (level) {
return level;
}
}
return rule.defaultValue;
}
getSetting(setting) {
return this.conf[setting.id];
}
}
function toLevel(level) {
switch (level) {
case 'ignore': return nodes.Level.Ignore;
case 'warning': return nodes.Level.Warning;
case 'error': return nodes.Level.Error;
}
return null;
}

View file

@ -0,0 +1,196 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { includes } from '../utils/arrays';
export class Element {
constructor(decl) {
this.fullPropertyName = decl.getFullPropertyName().toLowerCase();
this.node = decl;
}
}
function setSide(model, side, value, property) {
const state = model[side];
state.value = value;
if (value) {
if (!includes(state.properties, property)) {
state.properties.push(property);
}
}
}
function setAllSides(model, value, property) {
setSide(model, 'top', value, property);
setSide(model, 'right', value, property);
setSide(model, 'bottom', value, property);
setSide(model, 'left', value, property);
}
function updateModelWithValue(model, side, value, property) {
if (side === 'top' || side === 'right' ||
side === 'bottom' || side === 'left') {
setSide(model, side, value, property);
}
else {
setAllSides(model, value, property);
}
}
function updateModelWithList(model, values, property) {
switch (values.length) {
case 1:
updateModelWithValue(model, undefined, values[0], property);
break;
case 2:
updateModelWithValue(model, 'top', values[0], property);
updateModelWithValue(model, 'bottom', values[0], property);
updateModelWithValue(model, 'right', values[1], property);
updateModelWithValue(model, 'left', values[1], property);
break;
case 3:
updateModelWithValue(model, 'top', values[0], property);
updateModelWithValue(model, 'right', values[1], property);
updateModelWithValue(model, 'left', values[1], property);
updateModelWithValue(model, 'bottom', values[2], property);
break;
case 4:
updateModelWithValue(model, 'top', values[0], property);
updateModelWithValue(model, 'right', values[1], property);
updateModelWithValue(model, 'bottom', values[2], property);
updateModelWithValue(model, 'left', values[3], property);
break;
}
}
function matches(value, candidates) {
for (let candidate of candidates) {
if (value.matches(candidate)) {
return true;
}
}
return false;
}
/**
* @param allowsKeywords whether the initial value of property is zero, so keywords `initial` and `unset` count as zero
* @return `true` if this node represents a non-zero border; otherwise, `false`
*/
function checkLineWidth(value, allowsKeywords = true) {
if (allowsKeywords && matches(value, ['initial', 'unset'])) {
return false;
}
// a <length> is a value and a unit
// so use `parseFloat` to strip the unit
return parseFloat(value.getText()) !== 0;
}
function checkLineWidthList(nodes, allowsKeywords = true) {
return nodes.map(node => checkLineWidth(node, allowsKeywords));
}
/**
* @param allowsKeywords whether keywords `initial` and `unset` count as zero
* @return `true` if this node represents a non-zero border; otherwise, `false`
*/
function checkLineStyle(valueNode, allowsKeywords = true) {
if (matches(valueNode, ['none', 'hidden'])) {
return false;
}
if (allowsKeywords && matches(valueNode, ['initial', 'unset'])) {
return false;
}
return true;
}
function checkLineStyleList(nodes, allowsKeywords = true) {
return nodes.map(node => checkLineStyle(node, allowsKeywords));
}
function checkBorderShorthand(node) {
const children = node.getChildren();
// the only child can be a keyword, a <line-width>, or a <line-style>
// if either check returns false, the result is no border
if (children.length === 1) {
const value = children[0];
return checkLineWidth(value) && checkLineStyle(value);
}
// multiple children can't contain keywords
// if any child means no border, the result is no border
for (const child of children) {
const value = child;
if (!checkLineWidth(value, /* allowsKeywords: */ false) ||
!checkLineStyle(value, /* allowsKeywords: */ false)) {
return false;
}
}
return true;
}
export default function calculateBoxModel(propertyTable) {
const model = {
top: { value: false, properties: [] },
right: { value: false, properties: [] },
bottom: { value: false, properties: [] },
left: { value: false, properties: [] },
};
for (const property of propertyTable) {
const value = property.node.value;
if (typeof value === 'undefined') {
continue;
}
switch (property.fullPropertyName) {
case 'box-sizing':
// has `box-sizing`, bail out
return {
top: { value: false, properties: [] },
right: { value: false, properties: [] },
bottom: { value: false, properties: [] },
left: { value: false, properties: [] },
};
case 'width':
model.width = property;
break;
case 'height':
model.height = property;
break;
default:
const segments = property.fullPropertyName.split('-');
switch (segments[0]) {
case 'border':
switch (segments[1]) {
case undefined:
case 'top':
case 'right':
case 'bottom':
case 'left':
switch (segments[2]) {
case undefined:
updateModelWithValue(model, segments[1], checkBorderShorthand(value), property);
break;
case 'width':
// the initial value of `border-width` is `medium`, not zero
updateModelWithValue(model, segments[1], checkLineWidth(value, false), property);
break;
case 'style':
// the initial value of `border-style` is `none`
updateModelWithValue(model, segments[1], checkLineStyle(value, true), property);
break;
}
break;
case 'width':
// the initial value of `border-width` is `medium`, not zero
updateModelWithList(model, checkLineWidthList(value.getChildren(), false), property);
break;
case 'style':
// the initial value of `border-style` is `none`
updateModelWithList(model, checkLineStyleList(value.getChildren(), true), property);
break;
}
break;
case 'padding':
if (segments.length === 1) {
// the initial value of `padding` is zero
updateModelWithList(model, checkLineWidthList(value.getChildren(), true), property);
}
else {
// the initial value of `padding` is zero
updateModelWithValue(model, segments[1], checkLineWidth(value, true), property);
}
break;
}
break;
}
}
return model;
}

View file

@ -0,0 +1,157 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FileType, CompletionItemKind, TextEdit, Range, Position } from '../cssLanguageTypes';
import { startsWith, endsWith } from '../utils/strings';
import { joinPath } from '../utils/resources';
export class PathCompletionParticipant {
constructor(readDirectory) {
this.readDirectory = readDirectory;
this.literalCompletions = [];
this.importCompletions = [];
}
onCssURILiteralValue(context) {
this.literalCompletions.push(context);
}
onCssImportPath(context) {
this.importCompletions.push(context);
}
async computeCompletions(document, documentContext) {
const result = { items: [], isIncomplete: false };
for (const literalCompletion of this.literalCompletions) {
const uriValue = literalCompletion.uriValue;
const fullValue = stripQuotes(uriValue);
if (fullValue === '.' || fullValue === '..') {
result.isIncomplete = true;
}
else {
const items = await this.providePathSuggestions(uriValue, literalCompletion.position, literalCompletion.range, document, documentContext);
for (let item of items) {
result.items.push(item);
}
}
}
for (const importCompletion of this.importCompletions) {
const pathValue = importCompletion.pathValue;
const fullValue = stripQuotes(pathValue);
if (fullValue === '.' || fullValue === '..') {
result.isIncomplete = true;
}
else {
let suggestions = await this.providePathSuggestions(pathValue, importCompletion.position, importCompletion.range, document, documentContext);
if (document.languageId === 'scss') {
suggestions.forEach(s => {
if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) {
if (s.textEdit) {
s.textEdit.newText = s.label.slice(1, -5);
}
else {
s.label = s.label.slice(1, -5);
}
}
});
}
for (let item of suggestions) {
result.items.push(item);
}
}
}
return result;
}
async providePathSuggestions(pathValue, position, range, document, documentContext) {
const fullValue = stripQuotes(pathValue);
const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`);
const valueBeforeCursor = isValueQuoted
? fullValue.slice(0, position.character - (range.start.character + 1))
: fullValue.slice(0, position.character - range.start.character);
const currentDocUri = document.uri;
const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range;
const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange);
const valueBeforeLastSlash = valueBeforeCursor.substring(0, valueBeforeCursor.lastIndexOf('/') + 1); // keep the last slash
let parentDir = documentContext.resolveReference(valueBeforeLastSlash || '.', currentDocUri);
if (parentDir) {
try {
const result = [];
const infos = await this.readDirectory(parentDir);
for (const [name, type] of infos) {
// Exclude paths that start with `.`
if (name.charCodeAt(0) !== CharCode_dot && (type === FileType.Directory || joinPath(parentDir, name) !== currentDocUri)) {
result.push(createCompletionItem(name, type === FileType.Directory, replaceRange));
}
}
return result;
}
catch (e) {
// ignore
}
}
return [];
}
}
const CharCode_dot = '.'.charCodeAt(0);
function stripQuotes(fullValue) {
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
return fullValue.slice(1, -1);
}
else {
return fullValue;
}
}
function pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange) {
let replaceRange;
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
if (lastIndexOfSlash === -1) {
replaceRange = fullValueRange;
}
else {
// For cases where cursor is in the middle of attribute value, like <script src="./s|rc/test.js">
// Find the last slash before cursor, and calculate the start of replace range from there
const valueAfterLastSlash = fullValue.slice(lastIndexOfSlash + 1);
const startPos = shiftPosition(fullValueRange.end, -valueAfterLastSlash.length);
// If whitespace exists, replace until it
const whitespaceIndex = valueAfterLastSlash.indexOf(' ');
let endPos;
if (whitespaceIndex !== -1) {
endPos = shiftPosition(startPos, whitespaceIndex);
}
else {
endPos = fullValueRange.end;
}
replaceRange = Range.create(startPos, endPos);
}
return replaceRange;
}
function createCompletionItem(name, isDir, replaceRange) {
if (isDir) {
name = name + '/';
return {
label: escapePath(name),
kind: CompletionItemKind.Folder,
textEdit: TextEdit.replace(replaceRange, escapePath(name)),
command: {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
}
};
}
else {
return {
label: escapePath(name),
kind: CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, escapePath(name))
};
}
}
// Escape https://www.w3.org/TR/CSS1/#url
function escapePath(p) {
return p.replace(/(\s|\(|\)|,|"|')/g, '\\$1');
}
function shiftPosition(pos, offset) {
return Position.create(pos.line, pos.character + offset);
}
function shiftRange(range, startOffset, endOffset) {
const start = shiftPosition(range.start, startOffset);
const end = shiftPosition(range.end, endOffset);
return Range.create(start, end);
}

View file

@ -0,0 +1,355 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CSSCompletion } from './cssCompletion';
import * as nodes from '../parser/cssNodes';
import { CompletionItemKind, TextEdit, InsertTextFormat } from '../cssLanguageTypes';
import * as l10n from '@vscode/l10n';
const sassDocumentationName = l10n.t('Sass documentation');
class SCSSCompletion extends CSSCompletion {
constructor(lsServiceOptions, cssDataManager) {
super('$', lsServiceOptions, cssDataManager);
addReferencesToDocumentation(SCSSCompletion.scssModuleLoaders);
addReferencesToDocumentation(SCSSCompletion.scssModuleBuiltIns);
}
isImportPathParent(type) {
return type === nodes.NodeType.Forward
|| type === nodes.NodeType.Use
|| super.isImportPathParent(type);
}
getCompletionForImportPath(importPathNode, result) {
const parentType = importPathNode.getParent().type;
if (parentType === nodes.NodeType.Forward || parentType === nodes.NodeType.Use) {
for (let p of SCSSCompletion.scssModuleBuiltIns) {
const item = {
label: p.label,
documentation: p.documentation,
textEdit: TextEdit.replace(this.getCompletionRange(importPathNode), `'${p.label}'`),
kind: CompletionItemKind.Module
};
result.items.push(item);
}
}
return super.getCompletionForImportPath(importPathNode, result);
}
createReplaceFunction() {
let tabStopCounter = 1;
return (_match, p1) => {
return '\\' + p1 + ': ${' + tabStopCounter++ + ':' + (SCSSCompletion.variableDefaults[p1] || '') + '}';
};
}
createFunctionProposals(proposals, existingNode, sortToEnd, result) {
for (const p of proposals) {
const insertText = p.func.replace(/\[?(\$\w+)\]?/g, this.createReplaceFunction());
const label = p.func.substr(0, p.func.indexOf('('));
const item = {
label: label,
detail: p.func,
documentation: p.desc,
textEdit: TextEdit.replace(this.getCompletionRange(existingNode), insertText),
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Function
};
if (sortToEnd) {
item.sortText = 'z';
}
result.items.push(item);
}
return result;
}
getCompletionsForSelector(ruleSet, isNested, result) {
this.createFunctionProposals(SCSSCompletion.selectorFuncs, null, true, result);
return super.getCompletionsForSelector(ruleSet, isNested, result);
}
getTermProposals(entry, existingNode, result) {
let functions = SCSSCompletion.builtInFuncs;
if (entry) {
functions = functions.filter(f => !f.type || !entry.restrictions || entry.restrictions.indexOf(f.type) !== -1);
}
this.createFunctionProposals(functions, existingNode, true, result);
return super.getTermProposals(entry, existingNode, result);
}
getColorProposals(entry, existingNode, result) {
this.createFunctionProposals(SCSSCompletion.colorProposals, existingNode, false, result);
return super.getColorProposals(entry, existingNode, result);
}
getCompletionsForDeclarationProperty(declaration, result) {
this.getCompletionForAtDirectives(result);
this.getCompletionsForSelector(null, true, result);
return super.getCompletionsForDeclarationProperty(declaration, result);
}
getCompletionsForExtendsReference(_extendsRef, existingNode, result) {
const symbols = this.getSymbolContext().findSymbolsAtOffset(this.offset, nodes.ReferenceType.Rule);
for (const symbol of symbols) {
const suggest = {
label: symbol.name,
textEdit: TextEdit.replace(this.getCompletionRange(existingNode), symbol.name),
kind: CompletionItemKind.Function,
};
result.items.push(suggest);
}
return result;
}
getCompletionForAtDirectives(result) {
result.items.push(...SCSSCompletion.scssAtDirectives);
return result;
}
getCompletionForTopLevel(result) {
this.getCompletionForAtDirectives(result);
this.getCompletionForModuleLoaders(result);
super.getCompletionForTopLevel(result);
return result;
}
getCompletionForModuleLoaders(result) {
result.items.push(...SCSSCompletion.scssModuleLoaders);
return result;
}
}
SCSSCompletion.variableDefaults = {
'$red': '1',
'$green': '2',
'$blue': '3',
'$alpha': '1.0',
'$color': '#000000',
'$weight': '0.5',
'$hue': '0',
'$saturation': '0%',
'$lightness': '0%',
'$degrees': '0',
'$amount': '0',
'$string': '""',
'$substring': '"s"',
'$number': '0',
'$limit': '1'
};
SCSSCompletion.colorProposals = [
{ func: 'red($color)', desc: l10n.t('Gets the red component of a color.') },
{ func: 'green($color)', desc: l10n.t('Gets the green component of a color.') },
{ func: 'blue($color)', desc: l10n.t('Gets the blue component of a color.') },
{ func: 'mix($color, $color, [$weight])', desc: l10n.t('Mixes two colors together.') },
{ func: 'hue($color)', desc: l10n.t('Gets the hue component of a color.') },
{ func: 'saturation($color)', desc: l10n.t('Gets the saturation component of a color.') },
{ func: 'lightness($color)', desc: l10n.t('Gets the lightness component of a color.') },
{ func: 'adjust-hue($color, $degrees)', desc: l10n.t('Changes the hue of a color.') },
{ func: 'lighten($color, $amount)', desc: l10n.t('Makes a color lighter.') },
{ func: 'darken($color, $amount)', desc: l10n.t('Makes a color darker.') },
{ func: 'saturate($color, $amount)', desc: l10n.t('Makes a color more saturated.') },
{ func: 'desaturate($color, $amount)', desc: l10n.t('Makes a color less saturated.') },
{ func: 'grayscale($color)', desc: l10n.t('Converts a color to grayscale.') },
{ func: 'complement($color)', desc: l10n.t('Returns the complement of a color.') },
{ func: 'invert($color)', desc: l10n.t('Returns the inverse of a color.') },
{ func: 'alpha($color)', desc: l10n.t('Gets the opacity component of a color.') },
{ func: 'opacity($color)', desc: 'Gets the alpha component (opacity) of a color.' },
{ func: 'rgba($color, $alpha)', desc: l10n.t('Changes the alpha component for a color.') },
{ func: 'opacify($color, $amount)', desc: l10n.t('Makes a color more opaque.') },
{ func: 'fade-in($color, $amount)', desc: l10n.t('Makes a color more opaque.') },
{ func: 'transparentize($color, $amount)', desc: l10n.t('Makes a color more transparent.') },
{ func: 'fade-out($color, $amount)', desc: l10n.t('Makes a color more transparent.') },
{ func: 'adjust-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: l10n.t('Increases or decreases one or more components of a color.') },
{ func: 'scale-color($color, [$red], [$green], [$blue], [$saturation], [$lightness], [$alpha])', desc: l10n.t('Fluidly scales one or more properties of a color.') },
{ func: 'change-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: l10n.t('Changes one or more properties of a color.') },
{ func: 'ie-hex-str($color)', desc: l10n.t('Converts a color into the format understood by IE filters.') }
];
SCSSCompletion.selectorFuncs = [
{ func: 'selector-nest($selectors…)', desc: l10n.t('Nests selector beneath one another like they would be nested in the stylesheet.') },
{ func: 'selector-append($selectors…)', desc: l10n.t('Appends selectors to one another without spaces in between.') },
{ func: 'selector-extend($selector, $extendee, $extender)', desc: l10n.t('Extends $extendee with $extender within $selector.') },
{ func: 'selector-replace($selector, $original, $replacement)', desc: l10n.t('Replaces $original with $replacement within $selector.') },
{ func: 'selector-unify($selector1, $selector2)', desc: l10n.t('Unifies two selectors to produce a selector that matches elements matched by both.') },
{ func: 'is-superselector($super, $sub)', desc: l10n.t('Returns whether $super matches all the elements $sub does, and possibly more.') },
{ func: 'simple-selectors($selector)', desc: l10n.t('Returns the simple selectors that comprise a compound selector.') },
{ func: 'selector-parse($selector)', desc: l10n.t('Parses a selector into the format returned by &.') }
];
SCSSCompletion.builtInFuncs = [
{ func: 'unquote($string)', desc: l10n.t('Removes quotes from a string.') },
{ func: 'quote($string)', desc: l10n.t('Adds quotes to a string.') },
{ func: 'str-length($string)', desc: l10n.t('Returns the number of characters in a string.') },
{ func: 'str-insert($string, $insert, $index)', desc: l10n.t('Inserts $insert into $string at $index.') },
{ func: 'str-index($string, $substring)', desc: l10n.t('Returns the index of the first occurance of $substring in $string.') },
{ func: 'str-slice($string, $start-at, [$end-at])', desc: l10n.t('Extracts a substring from $string.') },
{ func: 'to-upper-case($string)', desc: l10n.t('Converts a string to upper case.') },
{ func: 'to-lower-case($string)', desc: l10n.t('Converts a string to lower case.') },
{ func: 'percentage($number)', desc: l10n.t('Converts a unitless number to a percentage.'), type: 'percentage' },
{ func: 'round($number)', desc: l10n.t('Rounds a number to the nearest whole number.') },
{ func: 'ceil($number)', desc: l10n.t('Rounds a number up to the next whole number.') },
{ func: 'floor($number)', desc: l10n.t('Rounds a number down to the previous whole number.') },
{ func: 'abs($number)', desc: l10n.t('Returns the absolute value of a number.') },
{ func: 'min($numbers)', desc: l10n.t('Finds the minimum of several numbers.') },
{ func: 'max($numbers)', desc: l10n.t('Finds the maximum of several numbers.') },
{ func: 'random([$limit])', desc: l10n.t('Returns a random number.') },
{ func: 'length($list)', desc: l10n.t('Returns the length of a list.') },
{ func: 'nth($list, $n)', desc: l10n.t('Returns a specific item in a list.') },
{ func: 'set-nth($list, $n, $value)', desc: l10n.t('Replaces the nth item in a list.') },
{ func: 'join($list1, $list2, [$separator])', desc: l10n.t('Joins together two lists into one.') },
{ func: 'append($list1, $val, [$separator])', desc: l10n.t('Appends a single value onto the end of a list.') },
{ func: 'zip($lists)', desc: l10n.t('Combines several lists into a single multidimensional list.') },
{ func: 'index($list, $value)', desc: l10n.t('Returns the position of a value within a list.') },
{ func: 'list-separator(#list)', desc: l10n.t('Returns the separator of a list.') },
{ func: 'map-get($map, $key)', desc: l10n.t('Returns the value in a map associated with a given key.') },
{ func: 'map-merge($map1, $map2)', desc: l10n.t('Merges two maps together into a new map.') },
{ func: 'map-remove($map, $keys)', desc: l10n.t('Returns a new map with keys removed.') },
{ func: 'map-keys($map)', desc: l10n.t('Returns a list of all keys in a map.') },
{ func: 'map-values($map)', desc: l10n.t('Returns a list of all values in a map.') },
{ func: 'map-has-key($map, $key)', desc: l10n.t('Returns whether a map has a value associated with a given key.') },
{ func: 'keywords($args)', desc: l10n.t('Returns the keywords passed to a function that takes variable arguments.') },
{ func: 'feature-exists($feature)', desc: l10n.t('Returns whether a feature exists in the current Sass runtime.') },
{ func: 'variable-exists($name)', desc: l10n.t('Returns whether a variable with the given name exists in the current scope.') },
{ func: 'global-variable-exists($name)', desc: l10n.t('Returns whether a variable with the given name exists in the global scope.') },
{ func: 'function-exists($name)', desc: l10n.t('Returns whether a function with the given name exists.') },
{ func: 'mixin-exists($name)', desc: l10n.t('Returns whether a mixin with the given name exists.') },
{ func: 'inspect($value)', desc: l10n.t('Returns the string representation of a value as it would be represented in Sass.') },
{ func: 'type-of($value)', desc: l10n.t('Returns the type of a value.') },
{ func: 'unit($number)', desc: l10n.t('Returns the unit(s) associated with a number.') },
{ func: 'unitless($number)', desc: l10n.t('Returns whether a number has units.') },
{ func: 'comparable($number1, $number2)', desc: l10n.t('Returns whether two numbers can be added, subtracted, or compared.') },
{ func: 'call($name, $args…)', desc: l10n.t('Dynamically calls a Sass function.') }
];
SCSSCompletion.scssAtDirectives = [
{
label: "@extend",
documentation: l10n.t("Inherits the styles of another selector."),
kind: CompletionItemKind.Keyword
},
{
label: "@at-root",
documentation: l10n.t("Causes one or more rules to be emitted at the root of the document."),
kind: CompletionItemKind.Keyword
},
{
label: "@debug",
documentation: l10n.t("Prints the value of an expression to the standard error output stream. Useful for debugging complicated Sass files."),
kind: CompletionItemKind.Keyword
},
{
label: "@warn",
documentation: l10n.t("Prints the value of an expression to the standard error output stream. Useful for libraries that need to warn users of deprecations or recovering from minor mixin usage mistakes. Warnings can be turned off with the `--quiet` command-line option or the `:quiet` Sass option."),
kind: CompletionItemKind.Keyword
},
{
label: "@error",
documentation: l10n.t("Throws the value of an expression as a fatal error with stack trace. Useful for validating arguments to mixins and functions."),
kind: CompletionItemKind.Keyword
},
{
label: "@if",
documentation: l10n.t("Includes the body if the expression does not evaluate to `false` or `null`."),
insertText: "@if ${1:expr} {\n\t$0\n}",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@for",
documentation: l10n.t("For loop that repeatedly outputs a set of styles for each `$var` in the `from/through` or `from/to` clause."),
insertText: "@for \\$${1:var} from ${2:start} ${3|to,through|} ${4:end} {\n\t$0\n}",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@each",
documentation: l10n.t("Each loop that sets `$var` to each item in the list or map, then outputs the styles it contains using that value of `$var`."),
insertText: "@each \\$${1:var} in ${2:list} {\n\t$0\n}",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@while",
documentation: l10n.t("While loop that takes an expression and repeatedly outputs the nested styles until the statement evaluates to `false`."),
insertText: "@while ${1:condition} {\n\t$0\n}",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@mixin",
documentation: l10n.t("Defines styles that can be re-used throughout the stylesheet with `@include`."),
insertText: "@mixin ${1:name} {\n\t$0\n}",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@include",
documentation: l10n.t("Includes the styles defined by another mixin into the current rule."),
kind: CompletionItemKind.Keyword
},
{
label: "@function",
documentation: l10n.t("Defines complex operations that can be re-used throughout stylesheets."),
kind: CompletionItemKind.Keyword
}
];
SCSSCompletion.scssModuleLoaders = [
{
label: "@use",
documentation: l10n.t("Loads mixins, functions, and variables from other Sass stylesheets as 'modules', and combines CSS from multiple stylesheets together."),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/at-rules/use' }],
insertText: "@use $0;",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
{
label: "@forward",
documentation: l10n.t("Loads a Sass stylesheet and makes its mixins, functions, and variables available when this stylesheet is loaded with the @use rule."),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/at-rules/forward' }],
insertText: "@forward $0;",
insertTextFormat: InsertTextFormat.Snippet,
kind: CompletionItemKind.Keyword
},
];
SCSSCompletion.scssModuleBuiltIns = [
{
label: 'sass:math',
documentation: l10n.t('Provides functions that operate on numbers.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/math' }]
},
{
label: 'sass:string',
documentation: l10n.t('Makes it easy to combine, search, or split apart strings.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/string' }]
},
{
label: 'sass:color',
documentation: l10n.t('Generates new colors based on existing ones, making it easy to build color themes.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/color' }]
},
{
label: 'sass:list',
documentation: l10n.t('Lets you access and modify values in lists.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/list' }]
},
{
label: 'sass:map',
documentation: l10n.t('Makes it possible to look up the value associated with a key in a map, and much more.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/map' }]
},
{
label: 'sass:selector',
documentation: l10n.t('Provides access to Sasss powerful selector engine.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/selector' }]
},
{
label: 'sass:meta',
documentation: l10n.t('Exposes the details of Sasss inner workings.'),
references: [{ name: sassDocumentationName, url: 'https://sass-lang.com/documentation/modules/meta' }]
},
];
export { SCSSCompletion };
/**
* Todo @Pine: Remove this and do it through custom data
*/
function addReferencesToDocumentation(items) {
items.forEach(i => {
if (i.documentation && i.references && i.references.length > 0) {
const markdownDoc = typeof i.documentation === 'string'
? { kind: 'markdown', value: i.documentation }
: { kind: 'markdown', value: i.documentation.value };
markdownDoc.value += '\n\n';
markdownDoc.value += i.references
.map(r => {
return `[${r.name}](${r.url})`;
})
.join(' | ');
i.documentation = markdownDoc;
}
});
}

View file

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { CSSNavigation } from './cssNavigation';
import * as nodes from '../parser/cssNodes';
import { URI, Utils } from 'vscode-uri';
import { startsWith } from '../utils/strings';
export class SCSSNavigation extends CSSNavigation {
constructor(fileSystemProvider) {
super(fileSystemProvider, true);
}
isRawStringDocumentLinkNode(node) {
return (super.isRawStringDocumentLinkNode(node) ||
node.type === nodes.NodeType.Use ||
node.type === nodes.NodeType.Forward);
}
async mapReference(target, isRawLink) {
if (this.fileSystemProvider && target && isRawLink) {
const pathVariations = toPathVariations(target);
for (const variation of pathVariations) {
if (await this.fileExists(variation)) {
return variation;
}
}
}
return target;
}
async resolveReference(target, documentUri, documentContext, isRawLink = false) {
if (startsWith(target, 'sass:')) {
return undefined; // sass library
}
return super.resolveReference(target, documentUri, documentContext, isRawLink);
}
}
function toPathVariations(target) {
// No variation for links that ends with suffix
if (target.endsWith('.scss') || target.endsWith('.css')) {
return [target];
}
// If a link is like a/, try resolving a/index.scss and a/_index.scss
if (target.endsWith('/')) {
return [target + 'index.scss', target + '_index.scss'];
}
const targetUri = URI.parse(target);
const basename = Utils.basename(targetUri);
const dirname = Utils.dirname(targetUri);
if (basename.startsWith('_')) {
// No variation for links such as _a
return [Utils.joinPath(dirname, basename + '.scss').toString(true)];
}
return [
Utils.joinPath(dirname, basename + '.scss').toString(true),
Utils.joinPath(dirname, '_' + basename + '.scss').toString(true),
target + '/index.scss',
target + '/_index.scss',
Utils.joinPath(dirname, basename + '.css').toString(true)
];
}

View file

@ -0,0 +1,490 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nodes from '../parser/cssNodes';
import { Scanner } from '../parser/cssScanner';
import * as l10n from '@vscode/l10n';
export class Element {
constructor() {
this.parent = null;
this.children = null;
this.attributes = null;
}
findAttribute(name) {
if (this.attributes) {
for (const attribute of this.attributes) {
if (attribute.name === name) {
return attribute.value;
}
}
}
return null;
}
addChild(child) {
if (child instanceof Element) {
child.parent = this;
}
if (!this.children) {
this.children = [];
}
this.children.push(child);
}
append(text) {
if (this.attributes) {
const last = this.attributes[this.attributes.length - 1];
last.value = last.value + text;
}
}
prepend(text) {
if (this.attributes) {
const first = this.attributes[0];
first.value = text + first.value;
}
}
findRoot() {
let curr = this;
while (curr.parent && !(curr.parent instanceof RootElement)) {
curr = curr.parent;
}
return curr;
}
removeChild(child) {
if (this.children) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
return true;
}
}
return false;
}
addAttr(name, value) {
if (!this.attributes) {
this.attributes = [];
}
for (const attribute of this.attributes) {
if (attribute.name === name) {
attribute.value += ' ' + value;
return;
}
}
this.attributes.push({ name, value });
}
clone(cloneChildren = true) {
const elem = new Element();
if (this.attributes) {
elem.attributes = [];
for (const attribute of this.attributes) {
elem.addAttr(attribute.name, attribute.value);
}
}
if (cloneChildren && this.children) {
elem.children = [];
for (let index = 0; index < this.children.length; index++) {
elem.addChild(this.children[index].clone());
}
}
return elem;
}
cloneWithParent() {
const clone = this.clone(false);
if (this.parent && !(this.parent instanceof RootElement)) {
const parentClone = this.parent.cloneWithParent();
parentClone.addChild(clone);
}
return clone;
}
}
export class RootElement extends Element {
}
export class LabelElement extends Element {
constructor(label) {
super();
this.addAttr('name', label);
}
}
class MarkedStringPrinter {
constructor(quote) {
this.quote = quote;
this.result = [];
// empty
}
print(element) {
this.result = [];
if (element instanceof RootElement) {
if (element.children) {
this.doPrint(element.children, 0);
}
}
else {
this.doPrint([element], 0);
}
const value = this.result.join('\n');
return [{ language: 'html', value }];
}
doPrint(elements, indent) {
for (const element of elements) {
this.doPrintElement(element, indent);
if (element.children) {
this.doPrint(element.children, indent + 1);
}
}
}
writeLine(level, content) {
const indent = new Array(level + 1).join(' ');
this.result.push(indent + content);
}
doPrintElement(element, indent) {
const name = element.findAttribute('name');
// special case: a simple label
if (element instanceof LabelElement || name === '\u2026') {
this.writeLine(indent, name);
return;
}
// the real deal
const content = ['<'];
// element name
if (name) {
content.push(name);
}
else {
content.push('element');
}
// attributes
if (element.attributes) {
for (const attr of element.attributes) {
if (attr.name !== 'name') {
content.push(' ');
content.push(attr.name);
const value = attr.value;
if (value) {
content.push('=');
content.push(quotes.ensure(value, this.quote));
}
}
}
}
content.push('>');
this.writeLine(indent, content.join(''));
}
}
var quotes;
(function (quotes) {
function ensure(value, which) {
return which + remove(value) + which;
}
quotes.ensure = ensure;
function remove(value) {
const match = value.match(/^['"](.*)["']$/);
if (match) {
return match[1];
}
return value;
}
quotes.remove = remove;
})(quotes || (quotes = {}));
class Specificity {
constructor() {
/** Count of identifiers (e.g., `#app`) */
this.id = 0;
/** Count of attributes (`[type="number"]`), classes (`.container-fluid`), and pseudo-classes (`:hover`) */
this.attr = 0;
/** Count of tag names (`div`), and pseudo-elements (`::before`) */
this.tag = 0;
}
}
export function toElement(node, parentElement) {
let result = new Element();
for (const child of node.getChildren()) {
switch (child.type) {
case nodes.NodeType.SelectorCombinator:
if (parentElement) {
const segments = child.getText().split('&');
if (segments.length === 1) {
// should not happen
result.addAttr('name', segments[0]);
break;
}
result = parentElement.cloneWithParent();
if (segments[0]) {
const root = result.findRoot();
root.prepend(segments[0]);
}
for (let i = 1; i < segments.length; i++) {
if (i > 1) {
const clone = parentElement.cloneWithParent();
result.addChild(clone.findRoot());
result = clone;
}
result.append(segments[i]);
}
}
break;
case nodes.NodeType.SelectorPlaceholder:
if (child.matches('@at-root')) {
return result;
}
// fall through
case nodes.NodeType.ElementNameSelector:
const text = child.getText();
result.addAttr('name', text === '*' ? 'element' : unescape(text));
break;
case nodes.NodeType.ClassSelector:
result.addAttr('class', unescape(child.getText().substring(1)));
break;
case nodes.NodeType.IdentifierSelector:
result.addAttr('id', unescape(child.getText().substring(1)));
break;
case nodes.NodeType.MixinDeclaration:
result.addAttr('class', child.getName());
break;
case nodes.NodeType.PseudoSelector:
result.addAttr(unescape(child.getText()), '');
break;
case nodes.NodeType.AttributeSelector:
const selector = child;
const identifier = selector.getIdentifier();
if (identifier) {
const expression = selector.getValue();
const operator = selector.getOperator();
let value;
if (expression && operator) {
switch (unescape(operator.getText())) {
case '|=':
// excatly or followed by -words
value = `${quotes.remove(unescape(expression.getText()))}-\u2026`;
break;
case '^=':
// prefix
value = `${quotes.remove(unescape(expression.getText()))}\u2026`;
break;
case '$=':
// suffix
value = `\u2026${quotes.remove(unescape(expression.getText()))}`;
break;
case '~=':
// one of a list of words
value = ` \u2026 ${quotes.remove(unescape(expression.getText()))} \u2026 `;
break;
case '*=':
// substring
value = `\u2026${quotes.remove(unescape(expression.getText()))}\u2026`;
break;
default:
value = quotes.remove(unescape(expression.getText()));
break;
}
}
result.addAttr(unescape(identifier.getText()), value);
}
break;
}
}
return result;
}
function unescape(content) {
const scanner = new Scanner();
scanner.setSource(content);
const token = scanner.scanUnquotedString();
if (token) {
return token.text;
}
return content;
}
export class SelectorPrinting {
constructor(cssDataManager) {
this.cssDataManager = cssDataManager;
}
selectorToMarkedString(node) {
const root = selectorToElement(node);
if (root) {
const markedStrings = new MarkedStringPrinter('"').print(root);
markedStrings.push(this.selectorToSpecificityMarkedString(node));
return markedStrings;
}
else {
return [];
}
}
simpleSelectorToMarkedString(node) {
const element = toElement(node);
const markedStrings = new MarkedStringPrinter('"').print(element);
markedStrings.push(this.selectorToSpecificityMarkedString(node));
return markedStrings;
}
isPseudoElementIdentifier(text) {
const match = text.match(/^::?([\w-]+)/);
if (!match) {
return false;
}
return !!this.cssDataManager.getPseudoElement("::" + match[1]);
}
selectorToSpecificityMarkedString(node) {
//https://www.w3.org/TR/selectors-3/#specificity
const calculateScore = (node) => {
const specificity = new Specificity();
elementLoop: for (const element of node.getChildren()) {
switch (element.type) {
case nodes.NodeType.IdentifierSelector:
specificity.id++;
break;
case nodes.NodeType.ClassSelector:
case nodes.NodeType.AttributeSelector:
specificity.attr++;
break;
case nodes.NodeType.ElementNameSelector:
//ignore universal selector
if (element.matches("*")) {
break;
}
specificity.tag++;
break;
case nodes.NodeType.PseudoSelector:
const text = element.getText();
if (this.isPseudoElementIdentifier(text)) {
specificity.tag++; // pseudo element
continue elementLoop;
}
// where and child selectors have zero specificity
if (text.match(/^:where/i)) {
continue elementLoop;
}
// the most specific child selector
if (text.match(/^:(not|has|is)/i) && element.getChildren().length > 0) {
let mostSpecificListItem = new Specificity();
for (const containerElement of element.getChildren()) {
let list;
if (containerElement.type === nodes.NodeType.Undefined) { // containerElement is a list of selectors
list = containerElement.getChildren();
}
else { // containerElement is a selector
list = [containerElement];
}
for (const childElement of containerElement.getChildren()) {
const itemSpecificity = calculateScore(childElement);
if (itemSpecificity.id > mostSpecificListItem.id) {
mostSpecificListItem = itemSpecificity;
continue;
}
else if (itemSpecificity.id < mostSpecificListItem.id) {
continue;
}
if (itemSpecificity.attr > mostSpecificListItem.attr) {
mostSpecificListItem = itemSpecificity;
continue;
}
else if (itemSpecificity.attr < mostSpecificListItem.attr) {
continue;
}
if (itemSpecificity.tag > mostSpecificListItem.tag) {
mostSpecificListItem = itemSpecificity;
continue;
}
}
}
specificity.id += mostSpecificListItem.id;
specificity.attr += mostSpecificListItem.attr;
specificity.tag += mostSpecificListItem.tag;
continue elementLoop;
}
specificity.attr++; //pseudo class
continue elementLoop;
}
if (element.getChildren().length > 0) {
const itemSpecificity = calculateScore(element);
specificity.id += itemSpecificity.id;
specificity.attr += itemSpecificity.attr;
specificity.tag += itemSpecificity.tag;
}
}
return specificity;
};
const specificity = calculateScore(node);
return `[${l10n.t("Selector Specificity")}](https://developer.mozilla.org/docs/Web/CSS/Specificity): (${specificity.id}, ${specificity.attr}, ${specificity.tag})`;
}
}
class SelectorElementBuilder {
constructor(element) {
this.prev = null;
this.element = element;
}
processSelector(selector) {
let parentElement = null;
if (!(this.element instanceof RootElement)) {
if (selector.getChildren().some((c) => c.hasChildren() && c.getChild(0).type === nodes.NodeType.SelectorCombinator)) {
const curr = this.element.findRoot();
if (curr.parent instanceof RootElement) {
parentElement = this.element;
this.element = curr.parent;
this.element.removeChild(curr);
this.prev = null;
}
}
}
for (const selectorChild of selector.getChildren()) {
if (selectorChild instanceof nodes.SimpleSelector) {
if (this.prev instanceof nodes.SimpleSelector) {
const labelElement = new LabelElement('\u2026');
this.element.addChild(labelElement);
this.element = labelElement;
}
else if (this.prev && (this.prev.matches('+') || this.prev.matches('~')) && this.element.parent) {
this.element = this.element.parent;
}
if (this.prev && this.prev.matches('~')) {
this.element.addChild(new LabelElement('\u22EE'));
}
const thisElement = toElement(selectorChild, parentElement);
const root = thisElement.findRoot();
this.element.addChild(root);
this.element = thisElement;
}
if (selectorChild instanceof nodes.SimpleSelector ||
selectorChild.type === nodes.NodeType.SelectorCombinatorParent ||
selectorChild.type === nodes.NodeType.SelectorCombinatorShadowPiercingDescendant ||
selectorChild.type === nodes.NodeType.SelectorCombinatorSibling ||
selectorChild.type === nodes.NodeType.SelectorCombinatorAllSiblings) {
this.prev = selectorChild;
}
}
}
}
function isNewSelectorContext(node) {
switch (node.type) {
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.Stylesheet:
return true;
}
return false;
}
export function selectorToElement(node) {
if (node.matches('@at-root')) {
return null;
}
const root = new RootElement();
const parentRuleSets = [];
const ruleSet = node.getParent();
if (ruleSet instanceof nodes.RuleSet) {
let parent = ruleSet.getParent(); // parent of the selector's ruleset
while (parent && !isNewSelectorContext(parent)) {
if (parent instanceof nodes.RuleSet) {
if (parent.getSelectors().matches('@at-root')) {
break;
}
parentRuleSets.push(parent);
}
parent = parent.getParent();
}
}
const builder = new SelectorElementBuilder(root);
for (let i = parentRuleSets.length - 1; i >= 0; i--) {
const selector = parentRuleSets[i].getSelectors().getChild(0);
if (selector) {
builder.processSelector(selector);
}
}
builder.processSelector(node);
return root;
}