diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index 986b11ab..4afed13d 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -49,5 +49,8 @@ export const ParseError = { SelectorExpected: new CSSIssueType('css-selectorexpected', localize('expected.selector', "selector expected")), StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected")), WhitespaceExpected: new CSSIssueType('css-whitespaceexpected', localize('expected.whitespace', "whitespace expected")), - MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")) + MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")), + IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', localize('expected.idorwildcard', "identifier or wildcard expected")), + WildcardExpected: new CSSIssueType('css-wildcardexpected', localize('expected.wildcard', "wildcard expected")), + IdentifierOrVariableExpected: new CSSIssueType('css-idorvarexpected', localize('expected.idorvar', "identifier or variable expected")), }; diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index d67eac83..49b6357e 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -83,6 +83,11 @@ export enum NodeType { GridLine, Plugin, UnknownAtRule, + Use, + ModuleConfiguration, + Forward, + ForwardVisibility, + Module, } export enum ReferenceType { @@ -91,7 +96,10 @@ export enum ReferenceType { Variable, Function, Keyframe, - Unknown + Unknown, + Module, + Forward, + ForwardVisibility, } @@ -1008,6 +1016,97 @@ export class Import extends Node { } } +export class Use extends Node { + + public identifier?: Identifier; + public parameters?: Nodelist; + + public get type(): NodeType { + return NodeType.Use; + } + + public getParameters(): Nodelist { + if (!this.parameters) { + this.parameters = new Nodelist(this); + } + return this.parameters; + } + + public setIdentifier(node: Identifier | null): node is Identifier { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Identifier | undefined { + return this.identifier; + } +} + +export class ModuleConfiguration extends Node { + + public identifier?: Node; + public value?: Node; + + public get type(): NodeType { + return NodeType.ModuleConfiguration; + } + + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Node | undefined { + return this.identifier; + } + + public getName(): string { + return this.identifier ? this.identifier.getText() : ''; + } + + public setValue(node: Node | null): node is Node { + return this.setNode('value', node, 0); + } + + public getValue(): Node | undefined { + return this.value; + } +} + +export class Forward extends Node { + + public identifier?: Node; + + public get type(): NodeType { + return NodeType.Forward; + } + + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Node | undefined { + return this.identifier; + } + +} + +export class ForwardVisibility extends Node { + + public identifier?: Node; + + public get type(): NodeType { + return NodeType.ForwardVisibility; + } + + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Node | undefined { + return this.identifier; + } + +} + export class Namespace extends Node { constructor(offset: number, length: number) { @@ -1366,6 +1465,8 @@ export class Interpolation extends Node { export class Variable extends Node { + public module?: Module; + constructor(offset: number, length: number) { super(offset, length); } @@ -1555,6 +1656,24 @@ export class GuardCondition extends Node { } } +export class Module extends Node { + + public identifier?: Identifier; + + public get type(): NodeType { + return NodeType.Module; + } + + public setIdentifier(node: Identifier | null): node is Identifier { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Identifier | undefined { + return this.identifier; + } + +} + export interface IRule { id: string; message: string; diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index 9b995325..8e33f9b7 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -244,7 +244,10 @@ export class Parser { public _parseStylesheet(): nodes.Stylesheet { const node = this.create(nodes.Stylesheet); - node.addChild(this._parseCharset()); + + while (node.addChild(this._parseStylesheetStart())) { + // Parse statements only valid at the beginning of stylesheets. + } let inRecovery = false; do { @@ -285,6 +288,10 @@ export class Parser { return this.finish(node); } + public _parseStylesheetStart(): nodes.Node | null { + return this._parseCharset(); + } + public _parseStylesheetStatement(isNested: boolean = false): nodes.Node | null { if (this.peek(TokenType.AtKeyword)) { return this._parseStylesheetAtStatement(isNested); diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 7e10e8e1..f11568ce 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -21,6 +21,12 @@ export class SCSSParser extends cssParser.Parser { super(new scssScanner.SCSSScanner()); } + public _parseStylesheetStart(): nodes.Node | null { + return this._parseForward() + || this._parseUse() + || super._parseStylesheetStart(); + } + public _parseStylesheetStatement(): nodes.Node | null { if (this.peek(TokenType.AtKeyword)) { return this._parseWarnAndDebug() @@ -77,7 +83,7 @@ export class SCSSParser extends cssParser.Parser { if (this.prevToken) { node.colonPosition = this.prevToken.offset; } - + if (!node.setValue(this._parseExpr())) { return this.finish(node, ParseError.VariableValueExpected, [], panic); } @@ -97,7 +103,10 @@ export class SCSSParser extends cssParser.Parser { } public _parseMediaFeatureName(): nodes.Node | null { - return this._parseFunction() || this._parseIdent() || this._parseVariable(); // first function, the indent + return this._parseModuleMember() + || this._parseFunction() // function before ident + || this._parseIdent() + || this._parseVariable(); } public _parseKeyframeSelector(): nodes.Node | null { @@ -116,6 +125,29 @@ export class SCSSParser extends cssParser.Parser { return node; } + public _parseModuleMember(): nodes.Module | null { + + const pos = this.mark(); + const node = this.create(nodes.Module); + + if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module]))) { + return null; + } + + if (this.hasWhitespace() + || !this.acceptDelim('.') + || this.hasWhitespace()) { + this.restoreAtMark(pos); + return null; + } + + if (!node.addChild(this._parseVariable() || this._parseFunction())) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); + } + + return node; + } + public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier | null { if (!this.peek(TokenType.Ident) && !this.peek(scssScanner.InterpolationFunction) && !this.peekDelim('-')) { return null; @@ -150,10 +182,14 @@ export class SCSSParser extends cssParser.Parser { } public _parseTerm(): nodes.Term | null { - let term = super._parseTerm(); - if (term) { return term; } + let term = this.create(nodes.Term); + if (term.setExpression(this._parseModuleMember())) { + return this.finish(term); + } + + const superTerm = super._parseTerm(); + if (superTerm) { return superTerm; } - term = this.create(nodes.Term); if (term.setExpression(this._parseVariable()) || term.setExpression(this._parseSelectorCombinator()) || term.setExpression(this._tryParsePrio())) { @@ -561,10 +597,30 @@ export class SCSSParser extends cssParser.Parser { const node = this.create(nodes.MixinReference); this.consumeToken(); - if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { + // Could be module or mixin identifier, set as mixin as default. + const firstIdent = this._parseIdent([nodes.ReferenceType.Mixin]); + if (!node.setIdentifier(firstIdent)) { return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); } + // Is a module accessor. + if (!this.hasWhitespace() && this.acceptDelim('.') && !this.hasWhitespace()) { + const secondIdent = this._parseIdent([nodes.ReferenceType.Mixin]); + + if (!secondIdent) { + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } + + const moduleToken = this.create(nodes.Module); + // Re-purpose first matched ident as identifier for module token. + firstIdent.referenceTypes = [nodes.ReferenceType.Module]; + moduleToken.setIdentifier(firstIdent); + + // Override identifier with second ident. + node.setIdentifier(secondIdent); + node.addChild(moduleToken); + } + if (this.accept(TokenType.ParenthesisL)) { if (node.getArguments().addChild(this._parseFunctionArgument())) { while (this.accept(TokenType.Comma)) { @@ -667,4 +723,134 @@ export class SCSSParser extends cssParser.Parser { } return this.finish(node); } + + public _parseUse(): nodes.Node | null { + if (!this.peek(scssScanner.Use)) { + return null; + } + + const node = this.create(nodes.Use); + this.consumeToken(); + + if (!node.addChild(this._parseStringLiteral())) { + return this.finish(node, ParseError.StringLiteralExpected); + } + + if (!this.peek(TokenType.SemiColon) && !this.peek(TokenType.EOF)) { + if (!this.peekRegExp(TokenType.Ident, /as|with/)) { + return this.finish(node, ParseError.UnknownKeyword); + } + + if ( + this.acceptIdent('as') && + (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module])) && !this.acceptDelim('*')) + ) { + return this.finish(node, ParseError.IdentifierOrWildcardExpected); + } + + if (this.acceptIdent('with')) { + if (!this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.ParenthesisR]); + } + + // First variable statement, no comma. + if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + + while (this.accept(TokenType.Comma)) { + if (this.peek(TokenType.ParenthesisR)) { + break; + } + if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected); + } + + } + } + + if (!this.accept(TokenType.SemiColon) && !this.accept(TokenType.EOF)) { + return this.finish(node, ParseError.SemiColonExpected); + } + + return this.finish(node); + } + + public _parseModuleConfigDeclaration(): nodes.Node | null { + + const node = this.create(nodes.ModuleConfiguration); + + if (!node.setIdentifier(this._parseVariable())) { + return null; + } + + if (!this.accept(TokenType.Colon) || !node.setValue(this._parseExpr(true))) { + return this.finish(node, ParseError.VariableValueExpected, [], [TokenType.Comma, TokenType.ParenthesisR]); + } + + return this.finish(node); + } + + public _parseForward(): nodes.Node | null { + if (!this.peek(scssScanner.Forward)) { + return null; + } + + const node = this.create(nodes.Forward); + this.consumeToken(); + + if (!node.addChild(this._parseStringLiteral())) { + return this.finish(node, ParseError.StringLiteralExpected); + } + + if (!this.peek(TokenType.SemiColon) && !this.peek(TokenType.EOF)) { + if (!this.peekRegExp(TokenType.Ident, /as|hide|show/)) { + return this.finish(node, ParseError.UnknownKeyword); + } + + if (this.acceptIdent('as')) { + const identifier = this._parseIdent([nodes.ReferenceType.Forward]); + if (!node.setIdentifier(identifier)) { + return this.finish(node, ParseError.IdentifierExpected); + } + + // Wildcard must be the next character after the identifier string. + if (this.hasWhitespace() || !this.acceptDelim('*')) { + return this.finish(node, ParseError.WildcardExpected); + } + } + + if (this.peekIdent('hide') || this.peekIdent('show')) { + if (!node.addChild(this._parseForwardVisibility())) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); + } + } + } + + if (!this.accept(TokenType.SemiColon) && !this.accept(TokenType.EOF)) { + return this.finish(node, ParseError.SemiColonExpected); + } + + return this.finish(node); + } + + public _parseForwardVisibility(): nodes.Node | null { + const node = this.create(nodes.ForwardVisibility); + + // Assume to be "hide" or "show". + node.setIdentifier(this._parseIdent()); + + while (node.addChild(this._parseVariable() || this._parseIdent())) { + // Consume all variables and idents ahead. + } + + // More than just identifier + return node.getChildren().length > 1 ? node : null; + } + } diff --git a/src/parser/scssScanner.ts b/src/parser/scssScanner.ts index ddc9dbaf..fd624230 100644 --- a/src/parser/scssScanner.ts +++ b/src/parser/scssScanner.ts @@ -19,6 +19,7 @@ const _BNG = '!'.charCodeAt(0); const _LAN = '<'.charCodeAt(0); const _RAN = '>'.charCodeAt(0); const _DOT = '.'.charCodeAt(0); +const _ATS = '@'.charCodeAt(0); let customTokenValue = TokenType.CustomToken; @@ -30,6 +31,9 @@ export const NotEqualsOperator: TokenType = customTokenValue++; export const GreaterEqualsOperator: TokenType = customTokenValue++; export const SmallerEqualsOperator: TokenType = customTokenValue++; export const Ellipsis: TokenType = customTokenValue++; +export const Module: TokenType = customTokenValue++; +export const Forward: TokenType = customTokenValue++; +export const Use: TokenType = customTokenValue++; export class SCSSScanner extends Scanner { @@ -81,6 +85,22 @@ export class SCSSScanner extends Scanner { return this.finishToken(offset, Ellipsis); } + // module loaders, @forward and @use + if (this.stream.advanceIfChar(_ATS)) { + const content = ['@']; + if (this.ident(content)) { + const keywordText = content.join(''); + if (keywordText === '@forward') { + return this.finishToken(offset, Forward, keywordText); + } + else if (keywordText === '@use') { + return this.finishToken(offset, Use, keywordText); + } + } + + this.stream.goBackTo(offset); + } + return super.scanNext(offset); } diff --git a/src/services/cssCompletion.ts b/src/services/cssCompletion.ts index fc5bfc27..106feb09 100644 --- a/src/services/cssCompletion.ts +++ b/src/services/cssCompletion.ts @@ -119,7 +119,7 @@ export class CSSCompletion { this.getCompletionForUriLiteralValue(node, result); } else if (node.parent === null) { this.getCompletionForTopLevel(result); - } else if (node.type === nodes.NodeType.StringLiteral && node.parent.type === nodes.NodeType.Import) { + } else if (node.type === nodes.NodeType.StringLiteral && this.isImportPathParent(node.parent.type)) { this.getCompletionForImportPath(node, result); // } else if (node instanceof nodes.Variable) { // this.getCompletionsForVariableDeclaration() @@ -150,6 +150,10 @@ export class CSSCompletion { } } + protected isImportPathParent(type: nodes.NodeType): boolean { + return type === nodes.NodeType.Import; + } + private finalize(result: CompletionList): CompletionList { const needsSortText = result.items.some(i => !!i.sortText || i.label[0] === '-'); if (needsSortText) { @@ -211,6 +215,7 @@ export class CSSCompletion { if (!declaration && completePropertyWithSemicolon) { insertText += '$0;'; } + // Cases such as .selector { p; } or .selector { p:; } if (declaration && !declaration.semicolonPosition) { if (completePropertyWithSemicolon && this.offset >= this.textDocument.offsetAt(range.end)) { @@ -262,7 +267,7 @@ export class CSSCompletion { } return this.settings.completion.triggerPropertyValueCompletion; } - + private get isCompletePropertyWithSemicolonEnabled(): boolean { if ( !this.settings || @@ -411,7 +416,7 @@ export class CSSCompletion { kind: CompletionItemKind.Variable, sortText: SortTexts.Variable }; - + if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) { completionItem.kind = CompletionItemKind.Color; } @@ -440,7 +445,7 @@ export class CSSCompletion { textEdit: TextEdit.replace(this.getCompletionRange(null), symbol.name), kind: CompletionItemKind.Variable }; - + if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) { completionItem.kind = CompletionItemKind.Color; } @@ -998,7 +1003,7 @@ function isDeprecated(entry: languageFacts.IEntry2): boolean { if (entry.status && (entry.status === 'nonstandard' || entry.status === 'obsolete')) { return true; } - + return false; } diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index de341643..6e06b44d 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -86,6 +86,10 @@ export class CSSNavigation { return result; } + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return node.type === nodes.NodeType.Import; + } + public findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): DocumentLink[] { const result: DocumentLink[] = []; @@ -102,7 +106,7 @@ export class CSSNavigation { * In @import, it is possible to include links that do not use `url()` * For example, `@import 'foo.css';` */ - if (candidate.parent && candidate.parent.type === nodes.NodeType.Import) { + if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { const rawText = candidate.getText(); if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { const link = uriStringNodeToDocumentLink(document, candidate, documentContext); diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index f7876a76..2447b4d3 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -196,11 +196,82 @@ export class SCSSCompletion extends CSSCompletion { } ]; + private static scssModuleLoaders = [ + { + label: "@use", + documentation: localize("scss.builtin.@use", "Loads mixins, functions, and variables from other Sass stylesheets as 'modules', and combines CSS from multiple stylesheets together."), + insertText: "@use '$0';", + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Keyword + }, + { + label: "@forward", + documentation: localize("scss.builtin.@forward", "Loads a Sass stylesheet and makes its mixins, functions, and variables available when this stylesheet is loaded with the @use rule."), + insertText: "@forward '$0';", + insertTextFormat: InsertTextFormat.Snippet, + kind: CompletionItemKind.Keyword + }, + ]; + + private static scssModuleBuiltIns = [ + { + label: 'sass:math', + documentation: localize('scss.builtin.sass:math', 'Provides functions that operate on numbers.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:string', + documentation: localize('scss.builtin.sass:string', 'Makes it easy to combine, search, or split apart strings.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:color', + documentation: localize('scss.builtin.sass:color', 'Generates new colors based on existing ones, making it easy to build color themes.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:list', + documentation: localize('scss.builtin.sass:list', 'Lets you access and modify values in lists.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:map', + documentation: localize('scss.builtin.sass:map', 'Makes it possible to look up the value associated with a key in a map, and much more.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:selector', + documentation: localize('scss.builtin.sass:selector', 'Provides access to Sass’s powerful selector engine.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:meta', + documentation: localize('scss.builtin.sass:meta', 'Exposes the details of Sass’s inner workings.'), + kind: CompletionItemKind.Text, + }, + ]; + constructor(clientCapabilities: ClientCapabilities | undefined) { super('$', clientCapabilities); } + protected isImportPathParent(type: nodes.NodeType): boolean { + return type === nodes.NodeType.Forward + || type === nodes.NodeType.Use + || super.isImportPathParent(type); + } + + public getCompletionForImportPath(importPathNode: nodes.Node, result: CompletionList): CompletionList { + const parentType = importPathNode.getParent()!.type; + + if (parentType === nodes.NodeType.Forward || parentType === nodes.NodeType.Use) { + result.items.push(...SCSSCompletion.scssModuleBuiltIns); + } + + return super.getCompletionForImportPath(importPathNode, result); + } + private createReplaceFunction() { let tabStopCounter = 1; return (_match: string, p1: string) => { @@ -273,7 +344,13 @@ export class SCSSCompletion extends CSSCompletion { public getCompletionForTopLevel(result: CompletionList): CompletionList { this.getCompletionForAtDirectives(result); + this.getCompletionForModuleLoaders(result); super.getCompletionForTopLevel(result); return result; } + + public getCompletionForModuleLoaders(result: CompletionList): CompletionList { + result.items.push(...SCSSCompletion.scssModuleLoaders); + return result; + } } diff --git a/src/services/scssNavigation.ts b/src/services/scssNavigation.ts index 1f7b2870..b474b59e 100644 --- a/src/services/scssNavigation.ts +++ b/src/services/scssNavigation.ts @@ -15,6 +15,12 @@ export class SCSSNavigation extends CSSNavigation { super(); } + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return super.isRawStringDocumentLinkNode(node) + || node.type === nodes.NodeType.Use + || node.type === nodes.NodeType.Forward; + } + public findDocumentLinks( document: TextDocument, stylesheet: nodes.Stylesheet, diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 4a0b3719..a2b76c38 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -27,6 +27,16 @@ suite('SCSS - Parser', () => { assertNode('$-co42lor', parser, parser._parseVariable.bind(parser)); }); + test('Module variable', function () { + let parser = new SCSSParser(); + assertNode('module.$color', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.$co42lor', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.$-co42lor', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.function()', parser, parser._parseModuleMember.bind(parser)); + + assertError('module.', parser, parser._parseModuleMember.bind(parser), ParseError.IdentifierOrVariableExpected); + }); + test('VariableDeclaration', function () { let parser = new SCSSParser(); assertNode('$color: #F5F5F5', parser, parser._parseVariableDeclaration.bind(parser)); @@ -76,6 +86,48 @@ suite('SCSS - Parser', () => { assertNode('100% / 2 + $filler', parser, parser._parseExpr.bind(parser)); assertNode('not ($v and $b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let + 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let - 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let * 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let / 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 - module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 * module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 / module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + module.$let + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('($var1 + module.$var2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$var1 + $var2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$var1 + module.$var2)', parser, parser._parseExpr.bind(parser)); + assertNode('((module.$let + 5) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('((module.$let + (5 + 2)) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let + ((5 + 2) * 2))', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, 42%, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color - ($color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('$color - (module.$color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color - (module.$color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$base + $filler)', parser, parser._parseExpr.bind(parser)); + assertNode('($base + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$base + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('(100% / 2 + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('100% / 2 + module.$filler', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and $b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and module.$b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and $b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and module.$b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and $b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and module.$b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and module.$b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertError('(20 + 20', parser, parser._parseExpr.bind(parser), ParseError.RightParenthesisExpected); }); @@ -112,6 +164,23 @@ suite('SCSS - Parser', () => { assertNode('some-property: var(--#{$propname})', parser, parser._parseDeclaration.bind(parser)); assertNode('#{}', parser, parser._parseIdent.bind(parser)); assertError('#{1 + 2', parser, parser._parseIdent.bind(parser), ParseError.RightCurlyExpected); + + assertNode('#{module.$color}', parser, parser._parseIdent.bind(parser)); + assertNode('#{module.$d}-style: 0', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{module.$d}: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-bar-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-bar-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-bar-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{module.$d}-bar: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('&:nth-child(#{module.$query}+1) { clear: $opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('&:nth-child(#{$query}+1) { clear: module.$opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('&:nth-child(#{module.$query}+1) { clear: module.$opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('--#{module.$propname}: some-value', parser, parser._parseDeclaration.bind(parser)); + assertNode('some-property: var(--#{module.$propname})', parser, parser._parseDeclaration.bind(parser)); }); test('Declaration', function () { @@ -140,6 +209,36 @@ suite('SCSS - Parser', () => { assertNode('foo: if(true, !important, null)', parser, parser._parseDeclaration.bind(parser)); assertNode('color: selector-replace(&, 1)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.$color', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / module.$let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / 20 + module.$let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.func($red)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.func($red) !important', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate($red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: desaturate(module.$red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate(module.$red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate(16, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: module.$base-color + #111', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: 100% / 2 + module.$ref', parser, parser._parseDeclaration.bind(parser)); + assertNode('border: (module.$width * 2) solid black', parser, parser._parseDeclaration.bind(parser)); + assertNode('property: module.$class', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: module.fnc($t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: fnc(module.$t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: module.fnc(module.$t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('width: (1em + 2em) * 3', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: #010203 + #040506', parser, parser._parseDeclaration.bind(parser)); + assertNode('font-family: sans- + "serif"', parser, parser._parseDeclaration.bind(parser)); + assertNode('margin: 3px + 4px auto', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: color.hsl(0, 100%, 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: color.hsl($hue: 0, $saturation: 100%, $lightness: 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', module.flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', module.flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', module.flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', module.flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: selector.replace(&, 1)', parser, parser._parseDeclaration.bind(parser)); + assertError('fo = 8', parser, parser._parseDeclaration.bind(parser), ParseError.ColonExpected); assertError('fo:', parser, parser._parseDeclaration.bind(parser), ParseError.PropertyValueExpected); assertError('color: hsl($hue: 0,', parser, parser._parseDeclaration.bind(parser), ParseError.ExpressionExpected); @@ -162,6 +261,9 @@ suite('SCSS - Parser', () => { assertNode('@include keyframe { 10% { top: 3px; } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.class{&--sub-class-with-ampersand{color: red;}}', parser, parser._parseStylesheet.bind(parser)); assertError('fo { font: 2px/3px { family } }', parser, parser._parseStylesheet.bind(parser), ParseError.ColonExpected); + + assertNode('legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:m.s(1)}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include module.keyframe { 10% { top: 3px; } }', parser, parser._parseStylesheet.bind(parser)); }); test('@import', function () { @@ -177,6 +279,55 @@ suite('SCSS - Parser', () => { assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected); }); + test('@use', function () { + let parser = new SCSSParser(); + assertNode('@use "test"', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as foo', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as *', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" with ($foo: "test", $bar: 1)', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as foo with ($foo: "test", $bar: 1)', parser, parser._parseUse.bind(parser)); + + assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); + assertError('@use "test" foo', parser, parser._parseUse.bind(parser), ParseError.UnknownKeyword); + assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierOrWildcardExpected); + assertError('@use "test" with', parser, parser._parseUse.bind(parser), ParseError.LeftParenthesisExpected); + assertError('@use "test" with ($foo)', parser, parser._parseUse.bind(parser), ParseError.VariableValueExpected); + assertError('@use "test" with ("bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); + assertError('@use "test" with ($foo: 1, "bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); + assertError('@use "test" with ($foo: "bar"', parser, parser._parseUse.bind(parser), ParseError.RightParenthesisExpected); + + assertNode('@forward "test"; @use "lib"', parser, parser._parseStylesheet.bind(parser)); + assertNode('@use "test"; @use "lib"', parser, parser._parseStylesheet.bind(parser)); + assertError('body { @use "test" }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('body { color: red; } @use "test"', parser, parser._parseStylesheet.bind(parser), ParseError.RuleOrSelectorExpected); + }); + + test('@forward', function () { + let parser = new SCSSParser(); + assertNode('@forward "test"', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" as foo-*', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide this', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide this $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show this', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show this $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" as foo-* show this $that', parser, parser._parseForward.bind(parser)); + + assertError('@forward', parser, parser._parseForward.bind(parser), ParseError.StringLiteralExpected); + assertError('@forward "test" foo', parser, parser._parseForward.bind(parser), ParseError.UnknownKeyword); + assertError('@forward "test" as', parser, parser._parseForward.bind(parser), ParseError.IdentifierExpected); + assertError('@forward "test" as foo-', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); + assertError('@forward "test" as foo- *', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); + assertError('@forward "test" show', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); + assertError('@forward "test" hide', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); + + assertNode('@use "lib"; @forward "test"', parser, parser._parseStylesheet.bind(parser)); + assertNode('@forward "test"; @forward "lib"', parser, parser._parseStylesheet.bind(parser)); + assertError('body { @forward "test" }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('body { color: red; } @forward "test"', parser, parser._parseStylesheet.bind(parser), ParseError.RuleOrSelectorExpected); + }); + test('@media', function () { let parser = new SCSSParser(); assertNode('@media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } }', parser, parser._parseStylesheet.bind(parser)); @@ -187,6 +338,26 @@ suite('SCSS - Parser', () => { assertNode('.something { @media (max-width: 760px) { ~ div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.something { @media (max-width: 760px) { + div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser)); + + assertNode('@media #{layout.$media} and ($feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and (layout.$feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and ($feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{layout.$media} and (layout.$feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and (layout.$feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{layout.$media} and (layout.$feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); }); test('@keyframe', function () { @@ -194,6 +365,8 @@ suite('SCSS - Parser', () => { assertNode('@keyframes name { @content; }', parser, parser._parseKeyframe.bind(parser)); assertNode('@keyframes name { @for $i from 0 through $steps { #{$i * (100%/$steps)} { transform: $rotate $translate; } } }', parser, parser._parseKeyframe.bind(parser)); // issue 42086 assertNode('@keyframes test-keyframe { @for $i from 1 through 60 { $s: ($i * 100) / 60 + "%"; } }', parser, parser._parseKeyframe.bind(parser)); + + assertNode('@keyframes name { @for $i from 0 through m.$steps { #{$i * (100%/$steps)} { transform: $rotate $translate; } } }', parser, parser._parseKeyframe.bind(parser)); }); test('@extend', function () { @@ -232,6 +405,15 @@ suite('SCSS - Parser', () => { assertNode('@if $i == 1 { p { x: 3px; } }', parser, parser._parseStylesheet.bind(parser)); assertError('@if { border: 1px solid; }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertError('@if 1 }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.LeftCurlyExpected); + + assertNode('@if 1 <= m.$let { border: 3px; } @else { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if 1 >= (1 + m.$foo) { border: 3px; } @else if 1 + 1 == 2 { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('p { @if m.$i == 1 { x: 3px; } @else if $i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @if $i == 1 { x: 3px; } @else if m.$i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @if m.$i == 1 { x: 3px; } @else if m.$i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (list.index($_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (index(m.$_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (list.index(m.$_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); }); test('@for', function () { @@ -244,6 +426,10 @@ suite('SCSS - Parser', () => { assertError('@for $i from {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertError('@for $i from 0 to {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertNode('@for $i from 1 through 60 { $s: $i + "%"; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + + assertNode('@for $k from 1 + m.$x through 5 + $x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@for $k from 1 + $x through 5 + m.$x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@for $k from 1 + m.$x through 5 + m.$x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); }); test('@each', function () { @@ -301,6 +487,21 @@ suite('SCSS - Parser', () => { assertError('p { @include foo($values }', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); assertError('p { @include foo($values, }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected); + assertNode('p { @include lib.sexy-border(blue); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.shadows { @include lib.box-shadow(0px 4px 5px #666, 2px 6px 10px #999); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('$values: #ff0000, #00ff00, #0000ff; .primary { @include lib.colors($values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.primary { @include colors(lib.$values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.primary { @include lib.colors(lib.$values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include lib.colors(this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include colors(lib.this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include lib.colors(lib.this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('.test { @include lib.fontsize(16px, 21px !important); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.apply-to-ie6-only { #logo { background-image: url(/logo.gif); } } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.foo($values,) }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include foo(lib.$values,) }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.foo(m.$values,); }', parser, parser._parseStylesheet.bind(parser)); + + assertError('p { @include foo.($values) }', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); }); test('@function', function () { @@ -325,6 +526,7 @@ suite('SCSS - Parser', () => { test('Ruleset', function () { let parser = new SCSSParser(); assertNode('.selector { prop: erty $let 1px; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.selector { prop: erty $let 1px m.$foo; }', parser, parser._parseRuleset.bind(parser)); assertNode('selector:active { property:value; nested:hover {}}', parser, parser._parseRuleset.bind(parser)); assertNode('selector {}', parser, parser._parseRuleset.bind(parser)); assertNode('selector { property: declaration }', parser, parser._parseRuleset.bind(parser)); @@ -339,6 +541,7 @@ suite('SCSS - Parser', () => { test('Nested Ruleset', function () { let parser = new SCSSParser(); assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; } one: $let; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.class1 { $let: 1; .class { $let: m.$foo; } one: $let; }', parser, parser._parseRuleset.bind(parser)); assertNode('.class1 { > .class2 { & > .class4 { rule1: v1; } } }', parser, parser._parseRuleset.bind(parser)); assertNode('foo { @at-root { display: none; } }', parser, parser._parseRuleset.bind(parser)); assertNode('th, tr { @at-root #{selector-replace(&, "tr")} { border-bottom: 0; } }', parser, parser._parseRuleset.bind(parser)); @@ -359,6 +562,17 @@ suite('SCSS - Parser', () => { assertNode('.foo-#{&} .foo-#{&-sub} { }', parser, parser._parseRuleset.bind(parser)); assertNode('.-#{$variable} { }', parser, parser._parseRuleset.bind(parser)); assertNode('#{&}([foo=bar][bar=foo]) { }', parser, parser._parseRuleset.bind(parser)); // #49589 + + assertNode('.#{module.$name} { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-foo { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-foo-3 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-1 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.sc-col#{module.$postfix}-2-1 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{module.$name} { #{$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{$name} { #{module.$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{module.$name} { #{module.$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('sans-#{serif} { a-#{1 + 2}-color-#{module.$attr}: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.-#{module.$variable} { }', parser, parser._parseRuleset.bind(parser)); }); test('Parent Selector', function () { @@ -405,4 +619,4 @@ suite('SCSS - Parser', () => { assertError('url("http://msft.com"', parser, parser._parseURILiteral.bind(parser), ParseError.RightParenthesisExpected); assertError('url(http://msft.com\')', parser, parser._parseURILiteral.bind(parser), ParseError.RightParenthesisExpected); }); -}); \ No newline at end of file +}); diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index ad8cdcdc..3672caee 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -10,15 +10,36 @@ import * as cssLanguageService from '../../cssLanguageService'; import { Position, InsertTextFormat } from 'vscode-languageserver-types'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { assertCompletion, ItemDescription } from '../css/completion.test'; +import { ImportPathCompletionContext } from '../../cssLanguageTypes'; +import { newRange } from '../css/navigation.test'; suite('SCSS - Completions', () => { - let testCompletionFor = function (value: string, expected: { count?: number, items?: ItemDescription[] }) { + let testCompletionFor = function ( + value: string, + expected: { + count?: number, + items?: ItemDescription[], + participant?: { + onImportPath?: ImportPathCompletionContext[], + }, + }, + ) { let offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); + let actualImportPathContexts: ImportPathCompletionContext[] = []; + let ls = cssLanguageService.getSCSSLanguageService(); + if (expected.participant) { + ls.setCompletionParticipants([ + { + onCssImportPath: context => actualImportPathContexts.push(context) + } + ]); + } + let document = TextDocument.create('test://test/test.scss', 'scss', 0, value); let position = Position.create(0, offset); let jsonDoc = ls.parseStylesheet(document); @@ -32,6 +53,11 @@ suite('SCSS - Completions', () => { assertCompletion(list, item, document, offset); } } + if (expected.participant) { + if (expected.participant.onImportPath) { + assert.deepEqual(actualImportPathContexts, expected.participant.onImportPath); + } + } }; test('stylesheet', function (): any { @@ -182,6 +208,51 @@ suite('SCSS - Completions', () => { ] }); }); + + suite('Modules', function (): any { + test('module-loading at-rules', function (): any { + testCompletionFor('@', { + items: [ + { label: '@use' }, + { label: '@forward' }, + ], + }); + + // Limit to top-level scope. + testCompletionFor('.foo { @| }', { + items: [ + { label: '@use', notAvailable: true }, + { label: '@forward', notAvailable: true }, + ], + }); + + const builtIns = { + items: [ + { label: 'sass:math' }, + { label: 'sass:string' }, + { label: 'sass:color' }, + { label: 'sass:list' }, + { label: 'sass:map' }, + { label: 'sass:selector' }, + { label: 'sass:meta' }, + ], + }; + testCompletionFor(`@use '|'`, builtIns); + testCompletionFor(`@forward '|'`, builtIns); + + testCompletionFor(`@use './|'`, { + participant: { + onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 8), range: newRange(5, 9) }] + } + }); + + testCompletionFor(`@forward './|'`, { + participant: { + onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 12), range: newRange(9, 13) }] + } + }); + }); + }); test('Enum + color restrictions are sorted properly', () => { testCompletionFor('.foo { text-decoration: | }', { diff --git a/src/test/scss/scssNavigation.test.ts b/src/test/scss/scssNavigation.test.ts index f69923b4..d1688299 100644 --- a/src/test/scss/scssNavigation.test.ts +++ b/src/test/scss/scssNavigation.test.ts @@ -210,7 +210,22 @@ suite('SCSS - Navigation', () => { ]); }); - + + test('SCSS module file links', async () => { + const fixtureRoot = path.resolve(__dirname, '../../../../src/test/scss/linkFixture/non-existent'); + const getDocumentUri = (relativePath: string) => { + return URI.file(path.resolve(fixtureRoot, relativePath)).toString(); + }; + + await assertDynamicLinks(getDocumentUri('./index.scss'), `@use './foo' as f`, [ + { range: newRange(5, 12), target: getDocumentUri('./foo') } + ]); + + await assertDynamicLinks(getDocumentUri('./index.scss'), `@forward './foo' hide $private`, [ + { range: newRange(9, 16), target: getDocumentUri('./foo') } + ]); + }); + test('SCSS empty path', async () => { const p = new SCSSParser();