From 65b2c5ca27bbd58f469b89fd52584dedfce8d211 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 13 Jun 2016 18:57:54 +0200 Subject: [PATCH] Move SCSS to extension --- .../css/server/src/cssLanguageService.ts | 20 +- extensions/css/server/src/cssServerMain.ts | 5 +- .../css/server/src/parser/scssErrors.ts | 26 + .../css/server/src/parser/scssParser.ts | 535 ++++++++++++++++++ .../css/server/src/parser/scssScanner.ts | 115 ++++ .../css/server/src/services/scssCompletion.ts | 158 ++++++ .../src/test/{ => css}/codeActions.test.ts | 10 +- .../src/test/{ => css}/completion.test.ts | 48 +- .../src/test/{ => css}/languageFacts.test.ts | 6 +- .../server/src/test/{ => css}/lint.test.ts | 8 +- .../src/test/{ => css}/navigation.test.ts | 16 +- .../server/src/test/{ => css}/nodes.test.ts | 4 +- .../server/src/test/{ => css}/parser.test.ts | 8 +- .../server/src/test/{ => css}/scanner.test.ts | 2 +- .../test/{ => css}/selectorPrinting.test.ts | 6 +- .../css/server/src/test/scss/example.scss | 336 +++++++++++ .../src/test/scss/languageFacts.test.ts | 24 + .../css/server/src/test/scss/lint.test.ts | 52 ++ .../css/server/src/test/scss/parser.test.ts | 335 +++++++++++ .../src/test/scss/scssCompletion.test.ts | 78 +++ .../src/test/scss/scssNavigation.test.ts | 68 +++ .../src/test/scss/selectorPrinting.test.ts | 34 ++ extensions/css/server/test/mocha.opts | 3 +- 23 files changed, 1837 insertions(+), 60 deletions(-) create mode 100644 extensions/css/server/src/parser/scssErrors.ts create mode 100644 extensions/css/server/src/parser/scssParser.ts create mode 100644 extensions/css/server/src/parser/scssScanner.ts create mode 100644 extensions/css/server/src/services/scssCompletion.ts rename extensions/css/server/src/test/{ => css}/codeActions.test.ts (91%) rename extensions/css/server/src/test/{ => css}/completion.test.ts (85%) rename extensions/css/server/src/test/{ => css}/languageFacts.test.ts (92%) rename extensions/css/server/src/test/{ => css}/lint.test.ts (96%) rename extensions/css/server/src/test/{ => css}/navigation.test.ts (94%) rename extensions/css/server/src/test/{ => css}/nodes.test.ts (96%) rename extensions/css/server/src/test/{ => css}/parser.test.ts (99%) rename extensions/css/server/src/test/{ => css}/scanner.test.ts (99%) rename extensions/css/server/src/test/{ => css}/selectorPrinting.test.ts (95%) create mode 100644 extensions/css/server/src/test/scss/example.scss create mode 100644 extensions/css/server/src/test/scss/languageFacts.test.ts create mode 100644 extensions/css/server/src/test/scss/lint.test.ts create mode 100644 extensions/css/server/src/test/scss/parser.test.ts create mode 100644 extensions/css/server/src/test/scss/scssCompletion.test.ts create mode 100644 extensions/css/server/src/test/scss/scssNavigation.test.ts create mode 100644 extensions/css/server/src/test/scss/selectorPrinting.test.ts diff --git a/extensions/css/server/src/cssLanguageService.ts b/extensions/css/server/src/cssLanguageService.ts index ede6b1183cc..9c3a1bfc35e 100644 --- a/extensions/css/server/src/cssLanguageService.ts +++ b/extensions/css/server/src/cssLanguageService.ts @@ -15,6 +15,9 @@ import {CSSNavigation} from './services/cssNavigation'; import {CSSCodeActions} from './services/cssCodeActions'; import {CSSValidation} from './services/cssValidation'; +import {SCSSParser} from './parser/scssParser'; +import {SCSSCompletion} from './services/scssCompletion'; + export interface LanguageService { configure(raw: LanguageSettings): void; doValidation(document: TextDocument, stylesheet: Stylesheet): Thenable; @@ -34,17 +37,18 @@ export interface LanguageSettings { lint?: any; } -export function getCSSLanguageService() : LanguageService { - let parser = new Parser(); + let cssParser = new Parser(); let cssCompletion = new CSSCompletion(); let cssHover = new CSSHover(); let cssValidation = new CSSValidation(); let cssNavigation = new CSSNavigation(); let cssCodeActions = new CSSCodeActions(); + +export function getCSSLanguageService() : LanguageService { return { configure: cssValidation.configure.bind(cssValidation), doValidation: cssValidation.doValidation.bind(cssValidation), - parseStylesheet: parser.parseStylesheet.bind(parser), + parseStylesheet: cssParser.parseStylesheet.bind(cssParser), doComplete: cssCompletion.doComplete.bind(cssCompletion), doHover: cssHover.doHover.bind(cssHover), findDefinition: cssNavigation.findDefinition.bind(cssNavigation), @@ -54,4 +58,14 @@ export function getCSSLanguageService() : LanguageService { doCodeActions: cssCodeActions.doCodeActions.bind(cssCodeActions), findColorSymbols: cssNavigation.findColorSymbols.bind(cssNavigation) }; +} + +let scssParser = new SCSSParser(); +let scssCompletion = new SCSSCompletion(); + +export function getSCSSLanguageService() : LanguageService { + let languageService = getCSSLanguageService(); + languageService.parseStylesheet = scssParser.parseStylesheet.bind(scssParser); + languageService.doComplete = scssCompletion.doComplete.bind(scssCompletion); + return languageService; } \ No newline at end of file diff --git a/extensions/css/server/src/cssServerMain.ts b/extensions/css/server/src/cssServerMain.ts index 89330247d73..94b72cbdbaa 100644 --- a/extensions/css/server/src/cssServerMain.ts +++ b/extensions/css/server/src/cssServerMain.ts @@ -9,7 +9,7 @@ import { TextDocuments, TextDocument, InitializeParams, InitializeResult, RequestType } from 'vscode-languageserver'; -import {getCSSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService'; +import {getCSSLanguageService, getSCSSLanguageService, LanguageSettings, LanguageService} from './cssLanguageService'; import {Stylesheet} from './parser/cssNodes'; import * as nls from 'vscode-nls'; @@ -56,7 +56,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { }); let languageServices : { [id:string]: LanguageService} = { - css: getCSSLanguageService() + css: getCSSLanguageService(), + scss: getSCSSLanguageService() }; function getLanguageService(document: TextDocument) { diff --git a/extensions/css/server/src/parser/scssErrors.ts b/extensions/css/server/src/parser/scssErrors.ts new file mode 100644 index 00000000000..2a25d3a0042 --- /dev/null +++ b/extensions/css/server/src/parser/scssErrors.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './cssNodes'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class SCSSIssueType implements nodes.IRule { + id: string; + message: string; + + public constructor(id: string, message: string) { + this.id = id; + this.message = message; + } +} + +export var SCSSParseError = { + FromExpected: new SCSSIssueType('sass-fromexpected', localize('expected.from', "'from' expected")), + ThroughOrToExpected: new SCSSIssueType('sass-throughexpected', localize('expected.through', "'through' or 'to' expected")), + InExpected: new SCSSIssueType('sass-fromexpected', localize('expected.in', "'in' expected")), +}; diff --git a/extensions/css/server/src/parser/scssParser.ts b/extensions/css/server/src/parser/scssParser.ts new file mode 100644 index 00000000000..fa9b6955347 --- /dev/null +++ b/extensions/css/server/src/parser/scssParser.ts @@ -0,0 +1,535 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sassScanner from './scssScanner'; +import {TokenType} from './cssScanner'; +import * as cssParser from './cssParser'; +import * as nodes from './cssNodes'; + +import {SCSSParseError} from './scssErrors'; +import {ParseError} from './cssErrors'; + +/// +/// A parser for Sass +/// http://sass-lang.com/documentation/file.SASS_REFERENCE.html +/// +export class SCSSParser extends cssParser.Parser { + + public constructor() { + super(new sassScanner.SCSSScanner()); + } + + public _parseStylesheetStatement(): nodes.Node { + return super._parseStylesheetStatement() + || this._parseVariableDeclaration() + || this._parseWarnAndDebug() + || this._parseControlStatement() + || this._parseMixinDeclaration() + || this._parseMixinContent() + || this._parseMixinReference() // @include + || this._parseFunctionDeclaration(); + } + + public _parseImport(): nodes.Node { + let node = this.create(nodes.Import); + if (!this.accept(TokenType.AtKeyword, '@import')) { + return null; + } + + if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) { + return this.finish(node, ParseError.URIOrStringExpected); + } + while (this.accept(TokenType.Comma)) { + if (!this.accept(TokenType.URI) && !this.accept(TokenType.String)) { + return this.finish(node, ParseError.URIOrStringExpected); + } + } + + node.setMedialist(this._parseMediaList()); + + return this.finish(node); + } + + // Sass variables: $font-size: 12px; + public _parseVariableDeclaration(panic: TokenType[] = []): nodes.VariableDeclaration { + let node = this.create(nodes.VariableDeclaration); + + if (!node.setVariable(this._parseVariable())) { + return null; + } + + if (!this.accept(TokenType.Colon, ':')) { + return this.finish(node, ParseError.ColonExpected); + } + node.colonPosition = this.prevToken.offset; + + if (!node.setValue(this._parseExpr())) { + return this.finish(node, ParseError.VariableValueExpected, [], panic); + } + + if (this.accept(TokenType.Exclamation)) { + if (!this.accept(TokenType.Ident, 'default', true)) { + return this.finish(node, ParseError.UnknownKeyword); + } + } + + if (this.peek(TokenType.SemiColon)) { + node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist + } + + return this.finish(node); + } + + public _parseMediaFeatureName(): nodes.Node { + return this._parseFunction() || this._parseIdent() || this._parseVariable(); // first function, the indent + } + + public _parseKeyframeSelector(): nodes.Node { + return super._parseKeyframeSelector() || this._parseMixinContent(); + } + + public _parseVariable(): nodes.Variable { + let node = this.create(nodes.Variable); + if (!this.accept(sassScanner.VariableName)) { + return null; + } + return node; + } + + public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier { + let node = this.create(nodes.Identifier); + node.referenceTypes = referenceTypes; + let hasContent = false; + while (this.accept(TokenType.Ident) || node.addChild(this._parseInterpolation())) { + hasContent = true; + if (!this.hasWhitespace() && this.accept(TokenType.Delim, '-')) { + // '-' is a valid char inside a ident (special treatment here to support #{foo}-#{bar}) + } + if (this.hasWhitespace()) { + break; + } + } + return hasContent ? this.finish(node) : null; + } + + public _parseTerm(): nodes.Term { + let term = super._parseTerm(); + if (term) { return term; } + + term = this.create(nodes.Term); + if (term.setExpression(this._parseVariable())) { + return this.finish(term); + } + + return null; + } + + public _parseInterpolation(): nodes.Node { + let node = this.create(nodes.Interpolation); + if (this.accept(sassScanner.InterpolationFunction)) { + if (!node.addChild(this._parseBinaryExpr())) { + return this.finish(node, ParseError.ExpressionExpected); + } + if (!this.accept(TokenType.CurlyR)) { + return this.finish(node, ParseError.RightCurlyExpected); + } + return this.finish(node); + } + return null; + } + + public _parseOperator(): nodes.Node { + if (this.peek(sassScanner.EqualsOperator) || this.peek(sassScanner.NotEqualsOperator) + || this.peek(sassScanner.GreaterEqualsOperator) || this.peek(sassScanner.SmallerEqualsOperator) + || this.peek(TokenType.Delim, '>') || this.peek(TokenType.Delim, '<') + || this.peek(TokenType.Ident, 'and') || this.peek(TokenType.Ident, 'or') + || this.peek(TokenType.Delim, '%') + ) { + let node = this.createNode(nodes.NodeType.Operator); + this.consumeToken(); + return this.finish(node); + } + return super._parseOperator(); + } + + public _parseUnaryOperator(): nodes.Node { + if (this.peek(TokenType.Ident, 'not')) { + let node = this.create(nodes.Node); + this.consumeToken(); + return this.finish(node); + } + return super._parseUnaryOperator(); + } + + public _parseRuleSetDeclaration(): nodes.Node { + if (this.peek(TokenType.AtKeyword)) { + return this._parseKeyframe() // nested @keyframe + || this._parseImport() // nested @import + || this._parseMedia() // nested @media + || this._parseFontFace() // nested @font-face + || this._parseWarnAndDebug() // @warn and @debug statements + || this._parseControlStatement() // @if, @while, @for, @each + || this._parseFunctionDeclaration() // @function + || this._parseExtends() // @extends + || this._parseMixinReference() // @include + || this._parseMixinContent() // @content + || this._parseMixinDeclaration(); // nested @mixin + } + return this._parseVariableDeclaration() // variable declaration + || this._tryParseRuleset(true) // nested ruleset + || super._parseRuleSetDeclaration(); // try css ruleset declaration as last so in the error case, the ast will contain a declaration + } + + public _parseDeclaration(resyncStopTokens?: TokenType[]): nodes.Declaration { + let node = this.create(nodes.Declaration); + if (!node.setProperty(this._parseProperty())) { + return null; + } + + if (!this.accept(TokenType.Colon, ':')) { + return this.finish(node, ParseError.ColonExpected, [TokenType.Colon], resyncStopTokens); + } + node.colonPosition = this.prevToken.offset; + + let hasContent = false; + if (node.setValue(this._parseExpr())) { + hasContent = true; + node.addChild(this._parsePrio()); + } + if (this.peek(TokenType.CurlyL)) { + node.setNestedProperties(this._parseNestedProperties()); + } else { + if (!hasContent) { + return this.finish(node, ParseError.PropertyValueExpected); + } + } + if (this.peek(TokenType.SemiColon)) { + node.semicolonPosition = this.token.offset; // not part of the declaration, but useful information for code assist + } + return this.finish(node); + } + + public _parseNestedProperties(): nodes.NestedProperties { + let node = this.create(nodes.NestedProperties); + return this._parseBody(node, this._parseDeclaration.bind(this)); + } + + public _parseExtends(): nodes.Node { + let node = this.create(nodes.ExtendsReference); + if (this.accept(TokenType.AtKeyword, '@extend')) { + if (!node.setSelector(this._parseSimpleSelector())) { + return this.finish(node, ParseError.SelectorExpected); + } + if (this.accept(TokenType.Exclamation)) { + if (!this.accept(TokenType.Ident, 'optional', true)) { + return this.finish(node, ParseError.UnknownKeyword); + } + } + return this.finish(node); + } + return null; + } + + public _parseSimpleSelectorBody(): nodes.Node { + return this._parseSelectorCombinator() || this._parseSelectorPlaceholder() || super._parseSimpleSelectorBody(); + } + + public _parseSelectorCombinator(): nodes.Node { + let node = this.createNode(nodes.NodeType.SelectorCombinator); + if (this.accept(TokenType.Delim, '&')) { + while (!this.hasWhitespace() && (this.accept(TokenType.Delim, '-') || node.addChild(this._parseIdent()) || this.accept(TokenType.Delim, '&'))) { + // support &-foo + } + return this.finish(node); + } + return null; + } + + public _parseSelectorPlaceholder(): nodes.Node { + let node = this.createNode(nodes.NodeType.SelectorPlaceholder); + if (this.accept(TokenType.Delim, '%')) { + this._parseIdent(); + return this.finish(node); + } + return null; + } + + public _parseWarnAndDebug(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@debug') && !this.peek(TokenType.AtKeyword, '@warn')) { + return null; + } + let node = this.createNode(nodes.NodeType.Debug); + this.consumeToken(); // @debug or @warn + node.addChild(this._parseExpr()); // optional + return this.finish(node); + } + + public _parseControlStatement(parseStatement: () => nodes.Node = this._parseRuleSetDeclaration.bind(this)): nodes.Node { + if (!this.peek(TokenType.AtKeyword)) { + return null; + } + return this._parseIfStatement(parseStatement) || this._parseForStatement(parseStatement) + || this._parseEachStatement(parseStatement) || this._parseWhileStatement(parseStatement); + } + + public _parseIfStatement(parseStatement: () => nodes.Node): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@if')) { + return null; + } + return this._internalParseIfStatement(parseStatement); + } + + private _internalParseIfStatement(parseStatement: () => nodes.Node): nodes.IfStatement { + let node = this.create(nodes.IfStatement); + this.consumeToken(); // @if or if + if (!node.setExpression(this._parseBinaryExpr())) { + return this.finish(node, ParseError.ExpressionExpected); + } + this._parseBody(node, parseStatement); + if (this.accept(TokenType.AtKeyword, '@else')) { + if (this.peek(TokenType.Ident, 'if')) { + node.setElseClause(this._internalParseIfStatement(parseStatement)); + } else if (this.peek(TokenType.CurlyL)) { + let elseNode = this.create(nodes.ElseStatement); + this._parseBody(elseNode, parseStatement); + node.setElseClause(elseNode); + } + } + return this.finish(node); + } + + public _parseForStatement(parseStatement: () => nodes.Node): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@for')) { + return null; + } + + let node = this.create(nodes.ForStatement); + this.consumeToken(); // @for + if (!node.setVariable(this._parseVariable())) { + return this.finish(node, ParseError.VariableNameExpected, [TokenType.CurlyR]); + } + if (!this.accept(TokenType.Ident, 'from')) { + return this.finish(node, SCSSParseError.FromExpected, [TokenType.CurlyR]); + } + if (!node.addChild(this._parseBinaryExpr())) { + return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]); + } + if (!this.accept(TokenType.Ident, 'to') && !this.accept(TokenType.Ident, 'through')) { + return this.finish(node, SCSSParseError.ThroughOrToExpected, [TokenType.CurlyR]); + } + if (!node.addChild(this._parseBinaryExpr())) { + return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]); + } + + return this._parseBody(node, parseStatement); + } + + public _parseEachStatement(parseStatement: () => nodes.Node): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@each')) { + return null; + } + + let node = this.create(nodes.EachStatement); + this.consumeToken(); // @each + if (!node.setVariable(this._parseVariable())) { + return this.finish(node, ParseError.VariableNameExpected, [TokenType.CurlyR]); + } + if (!this.accept(TokenType.Ident, 'in')) { + return this.finish(node, SCSSParseError.InExpected, [TokenType.CurlyR]); + } + if (!node.addChild(this._parseExpr())) { + return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]); + } + + return this._parseBody(node, parseStatement); + } + + public _parseWhileStatement(parseStatement: () => nodes.Node): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@while')) { + return null; + } + + let node = this.create(nodes.WhileStatement); + this.consumeToken(); // @while + if (!node.addChild(this._parseBinaryExpr())) { + return this.finish(node, ParseError.ExpressionExpected, [TokenType.CurlyR]); + } + + return this._parseBody(node, parseStatement); + } + + public _parseFunctionBodyDeclaration(): nodes.Node { + return this._parseVariableDeclaration() || this._parseReturnStatement() + || this._parseControlStatement(this._parseFunctionBodyDeclaration.bind(this)); + } + + public _parseFunctionDeclaration(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@function')) { + return null; + } + + let node = this.create(nodes.FunctionDeclaration); + this.consumeToken(); // @function + + if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Function]))) { + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } + + if (!this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.CurlyR]); + } + + if (node.getParameters().addChild(this._parseParameterDeclaration())) { + while (this.accept(TokenType.Comma)) { + if (!node.getParameters().addChild(this._parseParameterDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.CurlyR]); + } + + return this._parseBody(node, this._parseFunctionBodyDeclaration.bind(this)); + } + + public _parseReturnStatement(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@return')) { + return null; + } + + let node = this.createNode(nodes.NodeType.ReturnStatement); + this.consumeToken(); // @function + + if (!node.addChild(this._parseExpr())) { + return this.finish(node, ParseError.ExpressionExpected); + } + return this.finish(node); + } + + public _parseMixinDeclaration(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@mixin')) { + return null; + } + + let node = this.create(nodes.MixinDeclaration); + this.consumeToken(); + + if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } + + if (this.accept(TokenType.ParenthesisL)) { + if (node.getParameters().addChild(this._parseParameterDeclaration())) { + while (this.accept(TokenType.Comma)) { + if (!node.getParameters().addChild(this._parseParameterDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected, [TokenType.CurlyR]); + } + } + + return this._parseBody(node, this._parseRuleSetDeclaration.bind(this)); + } + + public _parseParameterDeclaration(): nodes.Node { + + let node = this.create(nodes.FunctionParameter); + + if (!node.setIdentifier(this._parseVariable())) { + return null; + } + + if (this.accept(sassScanner.Ellipsis)) { + // ok + } + + if (this.accept(TokenType.Colon)) { + if (!node.setDefaultValue(this._parseExpr(true))) { + return this.finish(node, ParseError.VariableValueExpected, [], [TokenType.Comma, TokenType.ParenthesisR]); + } + } + return this.finish(node); + } + + public _parseMixinContent(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@content')) { + return null; + } + let node = this.createNode(nodes.NodeType.MixinContent); + this.consumeToken(); + return this.finish(node); + } + + + public _parseMixinReference(): nodes.Node { + if (!this.peek(TokenType.AtKeyword, '@include')) { + return null; + } + + let node = this.create(nodes.MixinReference); + this.consumeToken(); + + if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } + + if (this.accept(TokenType.ParenthesisL)) { + if (node.getArguments().addChild(this._parseFunctionArgument())) { + while (this.accept(TokenType.Comma)) { + if (!node.getArguments().addChild(this._parseFunctionArgument())) { + return this.finish(node, ParseError.ExpressionExpected); + } + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected); + } + } + + if (this.peek(TokenType.CurlyL)) { + let content = this.create(nodes.BodyDeclaration); + this._parseBody(content, this._parseMixinReferenceBodyStatement.bind(this)); + node.setContent(content); + } + return this.finish(node); + } + + public _parseMixinReferenceBodyStatement(): nodes.Node { + return this._parseRuleSetDeclaration() || this._parseKeyframeSelector(); + } + + public _parseFunctionArgument(): nodes.Node { + // [variableName ':'] expression | variableName '...' + let node = this.create(nodes.FunctionArgument); + + let pos = this.mark(); + let argument = this._parseVariable(); + if (argument) { + if (!this.accept(TokenType.Colon)) { + if (this.accept(sassScanner.Ellipsis)) { // optional + node.setValue(argument); + return this.finish(node); + } else { + this.restoreAtMark(pos); + } + } else { + node.setIdentifier(argument); + } + } + + if (node.setValue(this._parseExpr(true))) { + return this.finish(node); + } + + return null; + } +} \ No newline at end of file diff --git a/extensions/css/server/src/parser/scssScanner.ts b/extensions/css/server/src/parser/scssScanner.ts new file mode 100644 index 00000000000..929cc24b91f --- /dev/null +++ b/extensions/css/server/src/parser/scssScanner.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * 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, IToken} from './cssScanner'; + +const _FSL = '/'.charCodeAt(0); +const _NWL = '\n'.charCodeAt(0); +const _CAR = '\r'.charCodeAt(0); +const _LFD = '\f'.charCodeAt(0); + +const _DLR = '$'.charCodeAt(0); +const _HSH = '#'.charCodeAt(0); +const _CUL = '{'.charCodeAt(0); +const _EQS = '='.charCodeAt(0); +const _BNG = '!'.charCodeAt(0); +const _LAN = '<'.charCodeAt(0); +const _RAN = '>'.charCodeAt(0); +const _DOT = '.'.charCodeAt(0); + +let customTokenValue = TokenType.CustomToken; + +export const VariableName = customTokenValue++; +export const InterpolationFunction: TokenType = customTokenValue++; +export const Default: TokenType = customTokenValue++; +export const EqualsOperator: TokenType = customTokenValue++; +export const NotEqualsOperator: TokenType = customTokenValue++; +export const GreaterEqualsOperator: TokenType = customTokenValue++; +export const SmallerEqualsOperator: TokenType = customTokenValue++; +export const Ellipsis: TokenType = customTokenValue++; + +export class SCSSScanner extends Scanner { + + public scan(): IToken { + + // processes all whitespaces and comments + const triviaToken = this.trivia(); + if (triviaToken !== null) { + return triviaToken; + } + + const offset = this.stream.pos(); + + // sass variable + if (this.stream.advanceIfChar(_DLR)) { + const content = ['$']; + if (this.ident(content)) { + return this.finishToken(offset, VariableName, content.join('')); + } else { + this.stream.goBackTo(offset); + } + } + + // Sass: interpolation function #{..}) + if (this.stream.advanceIfChars([_HSH, _CUL])) { + return this.finishToken(offset, InterpolationFunction); + } + + // operator == + if (this.stream.advanceIfChars([_EQS, _EQS])) { + return this.finishToken(offset, EqualsOperator); + } + + // operator != + if (this.stream.advanceIfChars([_BNG, _EQS])) { + return this.finishToken(offset, NotEqualsOperator); + } + + // operators <, <= + if (this.stream.advanceIfChar(_LAN)) { + if (this.stream.advanceIfChar(_EQS)) { + return this.finishToken(offset, SmallerEqualsOperator); + } + return this.finishToken(offset, TokenType.Delim); + } + + // ooperators >, >= + if (this.stream.advanceIfChar(_RAN)) { + if (this.stream.advanceIfChar(_EQS)) { + return this.finishToken(offset, GreaterEqualsOperator); + } + return this.finishToken(offset, TokenType.Delim); + } + + // ellipis + if (this.stream.advanceIfChars([_DOT, _DOT, _DOT])) { + return this.finishToken(offset, Ellipsis); + } + + return super.scan(); + } + + protected comment(): boolean { + if (super.comment()) { + return true; + } + if (this.stream.advanceIfChars([_FSL, _FSL])) { + this.stream.advanceWhileChar((ch: number) => { + switch (ch) { + case _NWL: + case _CAR: + case _LFD: + return false; + default: + return true; + } + }); + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/extensions/css/server/src/services/scssCompletion.ts b/extensions/css/server/src/services/scssCompletion.ts new file mode 100644 index 00000000000..1a2615bc73b --- /dev/null +++ b/extensions/css/server/src/services/scssCompletion.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * 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 languageFacts from './languageFacts'; +import {CSSCompletion} from './cssCompletion'; +import * as nodes from '../parser/cssNodes'; +import {CompletionList, CompletionItemKind} from 'vscode-languageserver'; + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class SCSSCompletion extends CSSCompletion { + + private static variableDefaults: { [key: string]: string; } = { + '$red': '1', + '$green': '2', + '$blue': '3', + '$alpha': '1.0', + '$color': '$color', + '$weight': '0.5', + '$hue': '0', + '$saturation': '0%', + '$lightness': '0%', + '$degrees': '0', + '$amount': '0', + '$string': '""', + '$substring': '"s"', + '$number': '0', + '$limit': '1' + }; + + private static colorProposals = [ + { func: 'red($color)', desc: localize('scss.builtin.red', 'Gets the red component of a color.') }, + { func: 'green($color)', desc: localize('scss.builtin.green', 'Gets the green component of a color.') }, + { func: 'blue($color)', desc: localize('scss.builtin.blue', 'Gets the blue component of a color.') }, + { func: 'mix($color, $color, [$weight])', desc: localize('scss.builtin.mix', 'Mixes two colors together.') }, + { func: 'hue($color)', desc: localize('scss.builtin.hue', 'Gets the hue component of a color.') }, + { func: 'saturation($color)', desc: localize('scss.builtin.saturation', 'Gets the saturation component of a color.') }, + { func: 'lightness($color)', desc: localize('scss.builtin.lightness', 'Gets the lightness component of a color.') }, + { func: 'adjust-hue($color, $degrees)', desc: localize('scss.builtin.adjust-hue', 'Changes the hue of a color.') }, + { func: 'lighten($color, $amount)', desc: localize('scss.builtin.lighten', 'Makes a color lighter.') }, + { func: 'darken($color, $amount)', desc: localize('scss.builtin.darken', 'Makes a color darker.') }, + { func: 'saturate($color, $amount)', desc: localize('scss.builtin.saturate', 'Makes a color more saturated.') }, + { func: 'desaturate($color, $amount)', desc: localize('scss.builtin.desaturate', 'Makes a color less saturated.') }, + { func: 'grayscale($color)', desc: localize('scss.builtin.grayscale', 'Converts a color to grayscale.') }, + { func: 'complement($color)', desc: localize('scss.builtin.complement', 'Returns the complement of a color.') }, + { func: 'invert($color)', desc: localize('scss.builtin.invert', 'Returns the inverse of a color.') }, + { func: 'alpha($color)', desc: localize('scss.builtin.alpha', 'Gets the opacity component of a color.') }, + { func: 'opacity($color)', desc: 'Gets the alpha component (opacity) of a color.' }, + { func: 'rgba($color, $alpha)', desc: localize('scss.builtin.rgba', 'Changes the alpha component for a color.') }, + { func: 'opacify($color, $amount)', desc: localize('scss.builtin.opacify', 'Makes a color more opaque.') }, + { func: 'fade-in($color, $amount)', desc: localize('scss.builtin.fade-in', 'Makes a color more opaque.') }, + { func: 'transparentize($color, $amount) / fade-out($color, $amount)', desc: localize('scss.builtin.transparentize', 'Makes a color more transparent.') }, + { func: 'adjust-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.adjust-color', 'Increases or decreases one or more components of a color.') }, + { func: 'scale-color($color, [$red], [$green], [$blue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.scale-color', 'Fluidly scales one or more properties of a color.') }, + { func: 'change-color($color, [$red], [$green], [$blue], [$hue], [$saturation], [$lightness], [$alpha])', desc: localize('scss.builtin.change-color', 'Changes one or more properties of a color.') }, + { func: 'ie-hex-str($color)', desc: localize('scss.builtin.ie-hex-str', 'Converts a color into the format understood by IE filters.') } + ]; + + private static selectorFuncs = [ + { func: 'selector-nest($selectors…)', desc: localize('scss.builtin.selector-nest', 'Nests selector beneath one another like they would be nested in the stylesheet.') }, + { func: 'selector-append($selectors…)', desc: localize('scss.builtin.selector-append', 'Appends selectors to one another without spaces in between.') }, + { func: 'selector-extend($selector, $extendee, $extender)', desc: localize('scss.builtin.selector-extend', 'Extends $extendee with $extender within $selector.') }, + { func: 'selector-replace($selector, $original, $replacement)', desc: localize('scss.builtin.selector-replace', 'Replaces $original with $replacement within $selector.') }, + { func: 'selector-unify($selector1, $selector2)', desc: localize('scss.builtin.selector-unify', 'Unifies two selectors to produce a selector that matches elements matched by both.') }, + { func: 'is-superselector($super, $sub)', desc: localize('scss.builtin.is-superselector', 'Returns whether $super matches all the elements $sub does, and possibly more.') }, + { func: 'simple-selectors($selector)', desc: localize('scss.builtin.simple-selectors', 'Returns the simple selectors that comprise a compound selector.') }, + { func: 'selector-parse($selector)', desc: localize('scss.builtin.selector-parse', 'Parses a selector into the format returned by &.') } + ]; + + private static builtInFuncs = [ + { func: 'unquote($string)', desc: localize('scss.builtin.unquote', 'Removes quotes from a string.') }, + { func: 'quote($string)', desc: localize('scss.builtin.quote', 'Adds quotes to a string.') }, + { func: 'str-length($string)', desc: localize('scss.builtin.str-length', 'Returns the number of characters in a string.') }, + { func: 'str-insert($string, $insert, $index)', desc: localize('scss.builtin.str-insert', 'Inserts $insert into $string at $index.') }, + { func: 'str-index($string, $substring)', desc: localize('scss.builtin.str-index', 'Returns the index of the first occurance of $substring in $string.') }, + { func: 'str-slice($string, $start-at, [$end-at])', desc: localize('scss.builtin.str-slice', 'Extracts a substring from $string.') }, + { func: 'to-upper-case($string)', desc: localize('scss.builtin.to-upper-case', 'Converts a string to upper case.') }, + { func: 'to-lower-case($string)', desc: localize('scss.builtin.to-lower-case', 'Converts a string to lower case.') }, + { func: 'percentage($number)', desc: localize('scss.builtin.percentage', 'Converts a unitless number to a percentage.') }, + { func: 'round($number)', desc: localize('scss.builtin.round', 'Rounds a number to the nearest whole number.') }, + { func: 'ceil($number)', desc: localize('scss.builtin.ceil', 'Rounds a number up to the next whole number.') }, + { func: 'floor($number)', desc: localize('scss.builtin.floor', 'Rounds a number down to the previous whole number.') }, + { func: 'abs($number)', desc: localize('scss.builtin.abs', 'Returns the absolute value of a number.') }, + { func: 'min($numbers)', desc: localize('scss.builtin.min', 'Finds the minimum of several numbers.') }, + { func: 'max($numbers)', desc: localize('scss.builtin.max', 'Finds the maximum of several numbers.') }, + { func: 'random([$limit])', desc: localize('scss.builtin.random', 'Returns a random number.') }, + { func: 'length($list)', desc: localize('scss.builtin.length', 'Returns the length of a list.') }, + { func: 'nth($list, $n)', desc: localize('scss.builtin.nth', 'Returns a specific item in a list.') }, + { func: 'set-nth($list, $n, $value)', desc: localize('scss.builtin.set-nth', 'Replaces the nth item in a list.') }, + { func: 'join($list1, $list2, [$separator])', desc: localize('scss.builtin.join', 'Joins together two lists into one.') }, + { func: 'append($list1, $val, [$separator])', desc: localize('scss.builtin.append', 'Appends a single value onto the end of a list.') }, + { func: 'zip($lists)', desc: localize('scss.builtin.zip', 'Combines several lists into a single multidimensional list.') }, + { func: 'index($list, $value)', desc: localize('scss.builtin.index', 'Returns the position of a value within a list.') }, + { func: 'list-separator(#list)', desc: localize('scss.builtin.list-separator', 'Returns the separator of a list.') }, + { func: 'map-get($map, $key)', desc: localize('scss.builtin.map-get', 'Returns the value in a map associated with a given key.') }, + { func: 'map-merge($map1, $map2)', desc: localize('scss.builtin.map-merge', 'Merges two maps together into a new map.') }, + { func: 'map-remove($map, $keys)', desc: localize('scss.builtin.map-remove', 'Returns a new map with keys removed.') }, + { func: 'map-keys($map)', desc: localize('scss.builtin.map-keys', 'Returns a list of all keys in a map.') }, + { func: 'map-values($map)', desc: localize('scss.builtin.map-values', 'Returns a list of all values in a map.') }, + { func: 'map-has-key($map, $key)', desc: localize('scss.builtin.map-has-key', 'Returns whether a map has a value associated with a given key.') }, + { func: 'keywords($args)', desc: localize('scss.builtin.keywords', 'Returns the keywords passed to a function that takes variable arguments.') }, + { func: 'feature-exists($feature)', desc: localize('scss.builtin.feature-exists', 'Returns whether a feature exists in the current Sass runtime.') }, + { func: 'variable-exists($name)', desc: localize('scss.builtin.variable-exists', 'Returns whether a variable with the given name exists in the current scope.') }, + { func: 'global-variable-exists($name)', desc: localize('scss.builtin.global-variable-exists', 'Returns whether a variable with the given name exists in the global scope.') }, + { func: 'function-exists($name)', desc: localize('scss.builtin.function-exists', 'Returns whether a function with the given name exists.') }, + { func: 'mixin-exists($name)', desc: localize('scss.builtin.mixin-exists', 'Returns whether a mixin with the given name exists.') }, + { func: 'inspect($value)', desc: localize('scss.builtin.inspect', 'Returns the string representation of a value as it would be represented in Sass.') }, + { func: 'type-of($value)', desc: localize('scss.builtin.type-of', 'Returns the type of a value.') }, + { func: 'unit($number)', desc: localize('scss.builtin.unit', 'Returns the unit(s) associated with a number.') }, + { func: 'unitless($number)', desc: localize('scss.builtin.unitless', 'Returns whether a number has units.') }, + { func: 'comparable($number1, $number2)', desc: localize('scss.builtin.comparable', 'Returns whether two numbers can be added, subtracted, or compared.') }, + { func: 'call($name, $args…)', desc: localize('scss.builtin.call', 'Dynamically calls a Sass function.') } + ]; + + constructor() { + super('$'); + } + + private createFunctionProposals(proposals: {func: string; desc: string; }[], result: CompletionList): CompletionList { + let replaceFunction = (match: string, p1: string) => p1 + ': {{' + (SCSSCompletion.variableDefaults[p1] || '') + '}}'; + proposals.forEach((p) => { + result.items.push({ + label: p.func.substr(0, p.func.indexOf('(')), + detail: p.func, + documentation: p.desc, + insertText: p.func.replace(/\[?(\$\w+)\]?/g, replaceFunction), + kind: CompletionItemKind.Function + }); + }); + return result; + } + + public getCompletionsForSelector(ruleSet: nodes.RuleSet, result: CompletionList): CompletionList { + this.createFunctionProposals(SCSSCompletion.selectorFuncs, result); + return super.getCompletionsForSelector(ruleSet, result); + } + + public getTermProposals(result: CompletionList): CompletionList { + this.createFunctionProposals(SCSSCompletion.builtInFuncs, result); + return super.getTermProposals(result); + } + + protected getColorProposals(entry: languageFacts.IEntry, result: CompletionList): CompletionList { + this.createFunctionProposals(SCSSCompletion.colorProposals, result); + return super.getColorProposals(entry, result); + } + + public getCompletionsForDeclarationProperty(result: CompletionList): CompletionList { + this.getCompletionsForSelector(null, result); + return super.getCompletionsForDeclarationProperty(result); + } + +} + diff --git a/extensions/css/server/src/test/codeActions.test.ts b/extensions/css/server/src/test/css/codeActions.test.ts similarity index 91% rename from extensions/css/server/src/test/codeActions.test.ts rename to extensions/css/server/src/test/css/codeActions.test.ts index 5e2b1419e06..e3ff8a0d2d4 100644 --- a/extensions/css/server/src/test/codeActions.test.ts +++ b/extensions/css/server/src/test/css/codeActions.test.ts @@ -5,13 +5,13 @@ 'use strict'; import * as assert from 'assert'; -import {Parser} from '../parser/cssParser'; -import {CSSCompletion} from '../services/cssCompletion'; -import {CSSCodeActions} from '../services/cssCodeActions'; -import {CSSValidation} from '../services/cssValidation'; +import {Parser} from '../../parser/cssParser'; +import {CSSCompletion} from '../../services/cssCompletion'; +import {CSSCodeActions} from '../../services/cssCodeActions'; +import {CSSValidation} from '../../services/cssValidation'; import {CompletionList, TextDocument, TextEdit, Position, Range, Command} from 'vscode-languageserver'; -import {applyEdits} from './textEditSupport'; +import {applyEdits} from '../textEditSupport'; suite('CSS - Code Actions', () => { let testCodeActions = function (value: string, tokenBefore: string): Thenable<{ commands: Command[]; document: TextDocument; }> { diff --git a/extensions/css/server/src/test/completion.test.ts b/extensions/css/server/src/test/css/completion.test.ts similarity index 85% rename from extensions/css/server/src/test/completion.test.ts rename to extensions/css/server/src/test/css/completion.test.ts index 6075700d715..6685226b457 100644 --- a/extensions/css/server/src/test/completion.test.ts +++ b/extensions/css/server/src/test/css/completion.test.ts @@ -5,36 +5,36 @@ 'use strict'; import * as assert from 'assert'; -import {Parser} from '../parser/cssParser'; -import {CSSCompletion} from '../services/cssCompletion'; +import {Parser} from '../../parser/cssParser'; +import {CSSCompletion} from '../../services/cssCompletion'; import {CompletionList, TextDocument, TextEdit, Position, CompletionItemKind} from 'vscode-languageserver'; -import {applyEdits} from './textEditSupport'; +import {applyEdits} from '../textEditSupport'; -suite('CSS - Completion', () => { +export interface ItemDescription { + label: string; + documentation?: string; + kind?: CompletionItemKind; + resultText?: string; +} - interface ItemDescription { - label: string; - documentation?: string; - kind?: CompletionItemKind; - resultText?: string; +export let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document?: TextDocument) { + let matches = completions.items.filter(completion => { + return completion.label === expected.label; + }); + assert.equal(matches.length, 1, expected.label + " should only existing once: Actual: " + completions.items.map(c => c.label).join(', ')); + if (expected.documentation) { + assert.equal(matches[0].documentation, expected.documentation); + } + if (expected.kind) { + assert.equal(matches[0].kind, expected.kind); + } + if (document && expected.resultText) { + assert.equal(applyEdits(document, [matches[0].textEdit]), expected.resultText); } +}; - let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document?: TextDocument) { - let matches = completions.items.filter(completion => { - return completion.label === expected.label; - }); - assert.equal(matches.length, 1, expected.label + " should only existing once: Actual: " + completions.items.map(c => c.label).join(', ')); - if (expected.documentation) { - assert.equal(matches[0].documentation, expected.documentation); - } - if (expected.kind) { - assert.equal(matches[0].kind, expected.kind); - } - if (document && expected.resultText) { - assert.equal(applyEdits(document, [matches[0].textEdit]), expected.resultText); - } - }; +suite('CSS - Completion', () => { let testCompletionFor = function (value: string, stringBefore: string, expected: { count?: number, items?: ItemDescription[] }): Thenable { let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0; diff --git a/extensions/css/server/src/test/languageFacts.test.ts b/extensions/css/server/src/test/css/languageFacts.test.ts similarity index 92% rename from extensions/css/server/src/test/languageFacts.test.ts rename to extensions/css/server/src/test/css/languageFacts.test.ts index e85096b1f4a..2222f68c97f 100644 --- a/extensions/css/server/src/test/languageFacts.test.ts +++ b/extensions/css/server/src/test/css/languageFacts.test.ts @@ -5,9 +5,9 @@ 'use strict'; import * as assert from 'assert'; -import * as languageFacts from '../services/languageFacts'; -import {Parser} from '../parser/cssParser'; -import * as nodes from '../parser/cssNodes'; +import * as languageFacts from '../../services/languageFacts'; +import {Parser} from '../../parser/cssParser'; +import * as nodes from '../../parser/cssNodes'; import {TextDocument} from 'vscode-languageserver'; export function assertColor(parser: Parser, text: string, selection: string, isColor: boolean): void { diff --git a/extensions/css/server/src/test/lint.test.ts b/extensions/css/server/src/test/css/lint.test.ts similarity index 96% rename from extensions/css/server/src/test/lint.test.ts rename to extensions/css/server/src/test/css/lint.test.ts index 371f492a843..fe39d2b8f18 100644 --- a/extensions/css/server/src/test/lint.test.ts +++ b/extensions/css/server/src/test/css/lint.test.ts @@ -5,10 +5,10 @@ 'use strict'; import * as assert from 'assert'; -import * as nodes from '../parser/cssNodes'; -import {Parser} from '../parser/cssParser'; -import {LintVisitor} from '../services/lint'; -import {Rule, Rules} from '../services/lintRules'; +import * as nodes from '../../parser/cssNodes'; +import {Parser} from '../../parser/cssParser'; +import {LintVisitor} from '../../services/lint'; +import {Rule, Rules} from '../../services/lintRules'; import {TextDocument} from 'vscode-languageserver'; export function assertEntries(node: nodes.Node, rules: nodes.IRule[]): void { diff --git a/extensions/css/server/src/test/navigation.test.ts b/extensions/css/server/src/test/css/navigation.test.ts similarity index 94% rename from extensions/css/server/src/test/navigation.test.ts rename to extensions/css/server/src/test/css/navigation.test.ts index 3068edd077a..5b57e79bdb5 100644 --- a/extensions/css/server/src/test/navigation.test.ts +++ b/extensions/css/server/src/test/css/navigation.test.ts @@ -5,10 +5,10 @@ 'use strict'; import * as assert from 'assert'; -import {Scope, GlobalScope, ScopeBuilder} from '../parser/cssSymbolScope'; -import * as nodes from '../parser/cssNodes'; -import {Parser} from '../parser/cssParser'; -import {CSSNavigation} from '../services/cssNavigation'; +import {Scope, GlobalScope, ScopeBuilder} from '../../parser/cssSymbolScope'; +import * as nodes from '../../parser/cssNodes'; +import {Parser} from '../../parser/cssParser'; +import {CSSNavigation} from '../../services/cssNavigation'; import {TextDocument, DocumentHighlightKind} from 'vscode-languageserver'; @@ -17,7 +17,7 @@ export function assertScopesAndSymbols(p: Parser, input: string, expected: strin assert.equal(scopeToString(global), expected); } -export function assertHighlights(p: Parser, input: string, marker: string, expectedMatches: number, expectedWrites: number): Thenable { +export function assertHighlights(p: Parser, input: string, marker: string, expectedMatches: number, expectedWrites: number, elementName?: string): Thenable { let document = TextDocument.create('test://test/test.css', 'css', 0, input); let stylesheet = p.parseStylesheet(document); @@ -27,7 +27,7 @@ export function assertHighlights(p: Parser, input: string, marker: string, expec let position = document.positionAt(index); return new CSSNavigation().findDocumentHighlights(document, position, stylesheet).then(highlights => { - assert.equal(highlights.length, expectedMatches); + assert.equal(highlights.length, expectedMatches, input); let nWrites = 0; for (let highlight of highlights) { @@ -36,7 +36,7 @@ export function assertHighlights(p: Parser, input: string, marker: string, expec } let range = highlight.range; let start = document.offsetAt(range.start), end = document.offsetAt(range.end); - assert.equal(document.getText().substring(start, end), marker); + assert.equal(document.getText().substring(start, end), elementName || marker); } assert.equal(nWrites, expectedWrites); }); @@ -170,7 +170,7 @@ suite('CSS - Symbols', () => { assertScopesAndSymbols(p, '@font-face { font-family: "Bitstream Vera Serif Bold"; }', '[]'); }); - test('mark occurrences', function (testDone) { + test('mark highlights', function (testDone) { let p = new Parser(); Promise.all([ assertHighlights(p, '@keyframes id {}; #main { animation: id 4s linear 0s infinite alternate; }', 'id', 2, 1), diff --git a/extensions/css/server/src/test/nodes.test.ts b/extensions/css/server/src/test/css/nodes.test.ts similarity index 96% rename from extensions/css/server/src/test/nodes.test.ts rename to extensions/css/server/src/test/css/nodes.test.ts index 5ee5801107c..cafa8ddd1e6 100644 --- a/extensions/css/server/src/test/nodes.test.ts +++ b/extensions/css/server/src/test/css/nodes.test.ts @@ -5,8 +5,8 @@ 'use strict'; import * as assert from 'assert'; -import * as nodes from '../parser/cssNodes'; -import {Parser} from '../parser/cssParser'; +import * as nodes from '../../parser/cssNodes'; +import {Parser} from '../../parser/cssParser'; export class PrintingVisitor implements nodes.IVisitor { diff --git a/extensions/css/server/src/test/parser.test.ts b/extensions/css/server/src/test/css/parser.test.ts similarity index 99% rename from extensions/css/server/src/test/parser.test.ts rename to extensions/css/server/src/test/css/parser.test.ts index c856c810a66..6de5fda42a7 100644 --- a/extensions/css/server/src/test/parser.test.ts +++ b/extensions/css/server/src/test/css/parser.test.ts @@ -5,10 +5,10 @@ 'use strict'; import * as assert from 'assert'; -import {Parser} from '../parser/cssParser'; -import {TokenType} from '../parser/cssScanner'; -import * as nodes from '../parser/cssNodes'; -import {ParseError} from '../parser/cssErrors'; +import {Parser} from '../../parser/cssParser'; +import {TokenType} from '../../parser/cssScanner'; +import * as nodes from '../../parser/cssNodes'; +import {ParseError} from '../../parser/cssErrors'; export function assertNode(text: string, parser: Parser, f: () => nodes.Node): nodes.Node { let node = parser.internalParse(text, f); diff --git a/extensions/css/server/src/test/scanner.test.ts b/extensions/css/server/src/test/css/scanner.test.ts similarity index 99% rename from extensions/css/server/src/test/scanner.test.ts rename to extensions/css/server/src/test/css/scanner.test.ts index 3ccf8396488..0c517a40e24 100644 --- a/extensions/css/server/src/test/scanner.test.ts +++ b/extensions/css/server/src/test/css/scanner.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import {Scanner, TokenType} from '../parser/cssScanner'; +import {Scanner, TokenType} from '../../parser/cssScanner'; suite('CSS - Scanner', () => { diff --git a/extensions/css/server/src/test/selectorPrinting.test.ts b/extensions/css/server/src/test/css/selectorPrinting.test.ts similarity index 95% rename from extensions/css/server/src/test/selectorPrinting.test.ts rename to extensions/css/server/src/test/css/selectorPrinting.test.ts index 5709e5b1059..3ba972184c2 100644 --- a/extensions/css/server/src/test/selectorPrinting.test.ts +++ b/extensions/css/server/src/test/css/selectorPrinting.test.ts @@ -5,9 +5,9 @@ 'use strict'; import * as assert from 'assert'; -import {Parser} from '../parser/cssParser'; -import * as nodes from '../parser/cssNodes'; -import * as selectorPrinter from '../services/selectorPrinting'; +import {Parser} from '../../parser/cssParser'; +import * as nodes from '../../parser/cssNodes'; +import * as selectorPrinter from '../../services/selectorPrinting'; import {TextDocument} from 'vscode-languageserver'; function elementToString(element: selectorPrinter.Element): string { diff --git a/extensions/css/server/src/test/scss/example.scss b/extensions/css/server/src/test/scss/example.scss new file mode 100644 index 00000000000..4fb3e5cc3ca --- /dev/null +++ b/extensions/css/server/src/test/scss/example.scss @@ -0,0 +1,336 @@ +// snippets from the Sass documentation at http://sass-lang.com/ + +/* css stuff */ +/* charset */ +@charset "UTF-8"; + +/* nested rules */ +#main { + width: 97%; + p, div { + font-size: 2em; + a { font-weight: bold; } + } + pre { font-size: 3em; } +} + +/* parent selector (&) */ +#main { + color: black; + a { + font-weight: bold; + &:hover { color: red; } + } +} + +/* nested properties */ +.funky { + font: 2px/3px { + family: fantasy; + size: 30em; + weight: bold; + } + color: black; +} + +/* nesting conflicts */ +tr.default { + foo: { // properties + foo : 1; + } + foo: 1px; // rule + foo.bar { // selector + foo : 1; + } + foo:bar { // selector + foo : 1; + } + foo: 1px; // rule +} + +/* extended comment syntax */ +/* This comment is + * several lines long. + * since it uses the CSS comment syntax, + * it will appear in the CSS output. */ +body { color: black; } + +// These comments are only one line long each. +// They won't appear in the CSS output, +// since they use the single-line comment syntax. +a { color: green; } + +/* variables */ +$width: 5em; +$width: "Second width?" !default; +#main { + $localvar: 6em; + width: $width; + + $font-size: 12px; + $line-height: 30px; + font: #{$font-size}/#{$line-height}; +} +$name: foo; +$attr: border; +p.#{$name} { + #{$attr}-color: blue; +} + +/* variable declaration with whitespaces */ +// Set the color of your columns +$grid-background-column-color : rgba(100, 100, 225, 0.25) !default; + +/* operations*/ +p { + width: (1em + 2em) * 3; + color: #010203 + #040506; + font-family: sans- + "serif"; + margin: 3px + 4px auto; + content: "I ate #{5 + 10} pies!"; + color: hsl(0, 100%, 50%); + color: hsl($hue: 0, $saturation: 100%, $lightness: 50%); +} +/* functions*/ +$grid-width: 40px; +$gutter-width: 10px; +@function grid-width($n) { + @return $n * $grid-width + ($n - 1) * $gutter-width; +} +#sidebar { width: grid-width(5); } + +/* @import */ +@import "foo.scss"; +$family: unquote("Droid+Sans"); +@import "rounded-corners", url("http://fonts.googleapis.com/css?family=#{$family}"); +#main { + @import "example"; +} + +/* @media */ +.sidebar { + width: 300px; + @media screen and (orientation: landscape) { + width: 500px; + } +} + +/* @extend */ +.error { + border: 1px #f00; + background-color: #fdd; +} +.seriousError { + @extend .error; + border-width: 3px; +} +#context a%extreme { + color: blue; + font-weight: bold; + font-size: 2em; +} +.notice { + @extend %extreme !optional; +} + +/* @debug and @warn */ +@debug 10em + 12em; +@mixin adjust-location($x, $y) { + @if unitless($x) { + @warn "Assuming #{$x} to be in pixels"; + $x: 1px * $x; + } + @if unitless($y) { + @warn "Assuming #{$y} to be in pixels"; + $y: 1px * $y; + } + position: relative; left: $x; top: $y; +} + +/* control directives */ + +/* if statement */ +p { + @if 1 + 1 == 2 { border: 1px solid; } + @if 5 < 3 { border: 2px dotted; } + @if null { border: 3px double; } +} + +/* if else statement */ +$type: monster; +p { + @if $type == ocean { + color: blue; + } @else { + color: black; + } +} + +/* for statement */ +@for $i from 1 through 3 { + .item-#{$i} { width: 2em * $i; } +} + +/* each statement */ +@each $animal in puma, sea-slug, egret, salamander { + .#{$animal}-icon { + background-image: url('/images/#{$animal}.png'); + } +} + +/* while statement */ +$i: 6; +@while $i > 0 { + .item-#{$i} { width: 2em * $i; } + $i: $i - 2; +} + +/* function with controlstatements */ +@function foo($total, $a) { + @for $i from 0 to $total { + @if (unit($a) == "%") and ($i == ($total - 1)) { + $z: 100%; + @return '1'; + } + } + @return $grid; +} + +/* @mixin simple*/ +@mixin large-text { + font: { + family: Arial; + size: 20px; + weight: bold; + } + color: #ff0000; +} +.page-title { + @include large-text; + padding: 4px; +} + +/* mixin with parameters */ +@mixin sexy-border($color, $width: 1in) { + border: { + color: $color; + width: $width; + style: dashed; + } +} +p { @include sexy-border(blue); } + +/* mixin with varargs */ +@mixin box-shadow($shadows...) { + -moz-box-shadow: $shadows; + -webkit-box-shadow: $shadows; + box-shadow: $shadows; +} +.shadows { + @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999); +} + +/* include with varargs */ +@mixin colors($text, $background, $border) { + color: $text; + background-color: $background; + border-color: $border; +} +$values: #ff0000, #00ff00, #0000ff; +.primary { + @include colors($values...); +} + +/* include with body */ +@mixin apply-to-ie6-only { + * html { + @content; + } +} +@include apply-to-ie6-only { + #logo { + background-image: url(/logo.gif); + } +} + + + +/* attributes */ +[rel="external"]::after { + content: 's'; +} +/*page */ +@page :left { + margin-left: 4cm; + margin-right: 3cm; +} + +/* missing semicolons */ +tr.default { + foo.bar { + $foo: 1px + } + foo: { + foo : white + } + foo.bar1 { + @extend tr.default + } + foo.bar2 { + @import "compass" + } + bar: black +} + +/* rules without whitespace */ +legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:s(1)} + +/* extend with interpolation variable */ +@mixin error($a: false) { + @extend .#{$a}; + @extend ##{$a}; +} +#bar {a: 1px;} +.bar {b: 1px;} +foo { + @include error('bar'); +} + +/* css3: @font face */ +@font-face { font-family: Delicious; src: url('Delicious-Roman.otf'); } + +/* rule names with variables */ +.orbit-#{$d}-prev { + #{$d}-style: 0; + foo-#{$d}: 1; + #{$d}-bar-#{$d}: 2; + foo-#{$d}-bar: 1; +} + +/* keyframes */ +@-webkit-keyframes NAME-YOUR-ANIMATION { + 0% { opacity: 0; } + 100% { opacity: 1; } +} +@-moz-keyframes NAME-YOUR-ANIMATION { + 0% { opacity: 0; } + 100% { opacity: 1; } +} +@-o-keyframes NAME-YOUR-ANIMATION { + 0% { opacity: 0; } + 100% { opacity: 1; } +} +@keyframes NAME-YOUR-ANIMATION { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +/* string escaping */ +[data-icon='test-1']:before { + content:'\\'; +} +/* a comment */ +$var1: '\''; +$var2: "\""; +/* another comment */ + diff --git a/extensions/css/server/src/test/scss/languageFacts.test.ts b/extensions/css/server/src/test/scss/languageFacts.test.ts new file mode 100644 index 00000000000..eaaebd5700b --- /dev/null +++ b/extensions/css/server/src/test/scss/languageFacts.test.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {SCSSParser} from '../../parser/scssParser'; +import {assertColor} from '../css/languageFacts.test'; + +suite('SCSS - Language facts', () => { + + test('is color', function () { + let parser = new SCSSParser(); + assertColor(parser, '#main { color: foo(red) }', 'red', true); + assertColor(parser, '#main { color: red() }', 'red', false); + assertColor(parser, '#main { red { nested: 1px } }', 'red', false); + assertColor(parser, '#main { @include red; }', 'red', false); + assertColor(parser, '#main { @include foo($f: red); }', 'red', true); + assertColor(parser, '@function red($p) { @return 1px; }', 'red', false); + assertColor(parser, '@function foo($p) { @return red; }', 'red', true); + assertColor(parser, '@function foo($r: red) { @return $r; }', 'red', true); + }); +}); + diff --git a/extensions/css/server/src/test/scss/lint.test.ts b/extensions/css/server/src/test/scss/lint.test.ts new file mode 100644 index 00000000000..4e7de2149a4 --- /dev/null +++ b/extensions/css/server/src/test/scss/lint.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {Rule, Rules} from '../../services/lintRules'; +import {assertEntries} from '../css/lint.test'; +import {SCSSParser} from '../../parser/scssParser'; + +function assertFontFace(input: string, ...rules: Rule[]): void { + let p = new SCSSParser(); + let node = p.internalParse(input, p._parseFontFace); + + assertEntries(node, rules); +} + +function assertRuleSet(input: string, ...rules: Rule[]): void { + let p = new SCSSParser(); + let node = p.internalParse(input, p._parseRuleset); + assertEntries(node, rules); +} + +suite('SCSS - Lint', () => { + + test('empty ruleset', function () { + assertRuleSet('selector { color: red; nested {} }', Rules.EmptyRuleSet); + }); + + test('font-face required properties', function () { + assertFontFace('@font-face { }', Rules.RequiredPropertiesForFontFace); + assertFontFace('@font-face { src: url(test.tff) }', Rules.RequiredPropertiesForFontFace); + assertFontFace('@font-face { font-family: \'name\' }', Rules.RequiredPropertiesForFontFace); + assertFontFace('@font-face { font-#{family}: foo }'); // no error, ignore all unknown properties + assertFontFace('@font-face { font: {family: foo } }'); // no error, ignore all nested properties + assertFontFace('@font-face { @if true { } }'); // no error, ignore all nested properties + }); + + test('unknown properties', function () { + assertRuleSet('selector { -ms-property: "rest is missing" }', Rules.UnknownProperty); + assertRuleSet('selector { -moz-box-shadow: "rest is missing" }', Rules.UnknownProperty, Rules.IncludeStandardPropertyWhenUsingVendorPrefix); + assertRuleSet('selector { box-shadow: none }'); // no error + assertRuleSet('selector { -moz-#{box}-shadow: none }'); // no error if theres an interpolation + assertRuleSet('selector { outer: { nested : blue }'); // no error for nested + }); + + test('vendor specific prefixes', function () { + assertRuleSet('selector { -moz-animation: none }', Rules.AllVendorPrefixes, Rules.IncludeStandardPropertyWhenUsingVendorPrefix); + assertRuleSet('selector { -moz-transform: none; transform: none }', Rules.AllVendorPrefixes); + assertRuleSet('selector { -moz-transform: none; transform: none; -o-transform: none; -webkit-transform: none; -ms-transform: none; }'); + }); +}); diff --git a/extensions/css/server/src/test/scss/parser.test.ts b/extensions/css/server/src/test/scss/parser.test.ts new file mode 100644 index 00000000000..cdfb58aa9bb --- /dev/null +++ b/extensions/css/server/src/test/scss/parser.test.ts @@ -0,0 +1,335 @@ +/*--------------------------------------------------------------------------------------------- + * 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 assert from 'assert'; +import {SCSSParser} from '../../parser/scssParser'; +import * as nodes from '../../parser/cssNodes'; +import {ParseError} from '../../parser/cssErrors'; +import {SCSSParseError} from '../../parser/scssErrors'; + +import {assertNode, assertError} from '../css/parser.test'; + +suite('SCSS - Parser', () => { + + test('Comments', function () { + let parser = new SCSSParser(); + assertNode(' a { b: /* comment */ c }', parser, parser._parseStylesheet.bind(parser)); + assertNode(' a { b: /* comment \n * is several\n * lines long\n */ c }', parser, parser._parseStylesheet.bind(parser)); + assertNode(' a { b: // single line comment\n c }', parser, parser._parseStylesheet.bind(parser)); + }); + + test('Variable', function () { + let parser = new SCSSParser(); + assertNode('$color', parser, parser._parseVariable.bind(parser)); + assertNode('$co42lor', parser, parser._parseVariable.bind(parser)); + assertNode('$-co42lor', parser, parser._parseVariable.bind(parser)); + }); + + test('VariableDeclaration', function () { + let parser = new SCSSParser(); + assertNode('$color: #F5F5F5', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$color: 0', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$color: 255', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$color: 25.5', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$color: 25px', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$color: 25.5px !default', parser, parser._parseVariableDeclaration.bind(parser)); + assertNode('$primary-font: "wf_SegoeUI","Segoe UI","Segoe","Segoe WP"', parser, parser._parseVariableDeclaration.bind(parser)); + assertError('$color: red !def', parser, parser._parseVariableDeclaration.bind(parser), ParseError.UnknownKeyword); + assertError('$color : !default', parser, parser._parseVariableDeclaration.bind(parser), ParseError.VariableValueExpected); + assertError('$color !default', parser, parser._parseVariableDeclaration.bind(parser), ParseError.ColonExpected); + }); + + test('Expr', function () { + let parser = new SCSSParser(); + assertNode('($let + 20)', parser, parser._parseExpr.bind(parser)); + assertNode('($let - 20)', parser, parser._parseExpr.bind(parser)); + assertNode('($let * 20)', parser, parser._parseExpr.bind(parser)); + assertNode('($let / 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 - $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 * $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 / $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 / 20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + $let + 20 + 20 + $let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20)', parser, parser._parseExpr.bind(parser)); + assertNode('($var1 + $var2)', parser, parser._parseExpr.bind(parser)); + assertNode('(($let + 5) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('(($let + (5 + 2)) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('($let + ((5 + 2) * 2))', parser, parser._parseExpr.bind(parser)); + assertNode('$color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, 42%', parser, parser._parseExpr.bind(parser)); + assertNode('$color, 42%, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color - ($color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('($base + $filler)', parser, parser._parseExpr.bind(parser)); + assertNode('(100% / 2 + $filler)', parser, parser._parseExpr.bind(parser)); + assertNode('100% / 2 + $filler', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and $b) or $c', parser, parser._parseExpr.bind(parser)); + + assertError('(20 + 20', parser, parser._parseExpr.bind(parser), ParseError.RightParenthesisExpected); + }); + + test('SassOperator', function () { + let parser = new SCSSParser(); + assertNode('>=', parser, parser._parseOperator.bind(parser)); + assertNode('>', parser, parser._parseOperator.bind(parser)); + assertNode('<', parser, parser._parseOperator.bind(parser)); + assertNode('<=', parser, parser._parseOperator.bind(parser)); + assertNode('==', parser, parser._parseOperator.bind(parser)); + assertNode('!=', parser, parser._parseOperator.bind(parser)); + assertNode('and', parser, parser._parseOperator.bind(parser)); + assertNode('+', parser, parser._parseOperator.bind(parser)); + assertNode('-', parser, parser._parseOperator.bind(parser)); + assertNode('*', parser, parser._parseOperator.bind(parser)); + assertNode('/', parser, parser._parseOperator.bind(parser)); + assertNode('%', parser, parser._parseOperator.bind(parser)); + assertNode('not', parser, parser._parseUnaryOperator.bind(parser)); + }); + + test('Interpolation', function () { + let parser = new SCSSParser(); + assertNode('#{red}', parser, parser._parseIdent.bind(parser)); + assertNode('#{$color}', parser, parser._parseIdent.bind(parser)); + assertNode('#{3 + 4}', parser, parser._parseIdent.bind(parser)); + assertNode('#{3 + #{3 + 4}}', parser, parser._parseIdent.bind(parser)); + assertNode('#{$d}-style: 0', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{$d}: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-bar-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{$d}-bar: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('&:nth-child(#{$query}+1) { clear: $opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertError('#{}', parser, parser._parseIdent.bind(parser), ParseError.ExpressionExpected); + assertError('#{1 + 2', parser, parser._parseIdent.bind(parser), ParseError.RightCurlyExpected); + }); + + test('Declaration', function () { + let parser = new SCSSParser(); + assertNode('border: thin solid 1px', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: $color', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: blue', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / $let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / 20 + $let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: func($red)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: desaturate($red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: desaturate(16, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: $base-color + #111', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: 100% / 2 + $ref', parser, parser._parseDeclaration.bind(parser)); + assertNode('border: ($width * 2) solid black', parser, parser._parseDeclaration.bind(parser)); + assertNode('property: $class', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: fnc($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: hsl(0, 100%, 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: hsl($hue: 0, $saturation: 100%, $lightness: 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', flex-gutter(), $value)', 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); + assertError('color: hsl($hue: 0', parser, parser._parseDeclaration.bind(parser), ParseError.RightParenthesisExpected); + }); + + test('Stylesheet', function () { + let parser = new SCSSParser(); + assertNode('$color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser)); + assertNode('$color: #F5F5F5; $color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser)); + assertNode('$color: #F5F5F5; $color: #F5F5F5; $color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser)); + assertNode('#main { width: 97%; p, div { a { font-weight: bold; } } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('a { &:hover { color: red; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('fo { font: 2px/3px { family: fantasy; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.foo { bar: { yoo: fantasy; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('selector { propsuffix: { nested: 1px; } rule: 1px; nested.selector { foo: 1; } nested:selector { foo: 2 }}', parser, parser._parseStylesheet.bind(parser)); + assertNode('legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:s(1)}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin keyframe { @keyframes name { @content; } }', parser, parser._parseStylesheet.bind(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); + }); + + test('@import', function () { + let parser = new SCSSParser(); + assertNode('@import "test.css"', parser, parser._parseImport.bind(parser)); + assertNode('@import url("test.css")', parser, parser._parseImport.bind(parser)); + assertNode('@import "test.css", "bar.css"', parser, parser._parseImport.bind(parser)); + assertNode('@import "test.css", "bar.css" screen, projection', parser, parser._parseImport.bind(parser)); + assertNode('foo { @import "test.css"; }', parser, parser._parseStylesheet.bind(parser)); + + assertError('@import "test.css" "bar.css"', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected); + assertError('@import "test.css", screen', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected); + assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected); + }); + + test('@media', function () { + let parser = new SCSSParser(); + assertNode('@media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and ($feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('foo { bar { @media screen and (orientation: landscape) {}} }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media screen and (nth($query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + }); + + test('@keyframe', function () { + let parser = new SCSSParser(); + assertNode('@keyframes name { @content; }', parser, parser._parseKeyframe.bind(parser)); + }); + + test('@extend', function () { + let parser = new SCSSParser(); + assertNode('foo { @extend .error; border-width: 3px; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('a.important { @extend .notice !optional; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.hoverlink { @extend a:hover; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.seriousError { @extend .error; @extend .attention; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('#context a%extreme { color: blue; } .notice { @extend %extreme }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media print { .error { } .seriousError { @extend .error; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin error($a: false) { @extend .#{$a}; @extend ##{$a}; }', parser, parser._parseStylesheet.bind(parser)); + + assertError('.hoverlink { @extend }', parser, parser._parseStylesheet.bind(parser), ParseError.SelectorExpected); + assertError('.hoverlink { @extend %extreme !default }', parser, parser._parseStylesheet.bind(parser), ParseError.UnknownKeyword); + }); + + test('@debug', function () { + let parser = new SCSSParser(); + assertNode('@debug test;', parser, parser._parseStylesheet.bind(parser)); + assertNode('foo { @debug 1 + 4; nested { @warn 1 4; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if $foo == 1 { @debug 1 + 4 }', parser, parser._parseStylesheet.bind(parser)); + }); + + test('@if', function () { + let parser = new SCSSParser(); + assertNode('@if 1 + 1 == 2 { border: 1px solid; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if 5 < 3 { border: 2px dotted; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if null { border: 3px double; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if 1 <= $let { border: 3px; } @else { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if 1 >= (1 + $foo) { border: 3px; } @else if 1 + 1 == 2 { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('p { @if $i == 1 { x: 3px; } @else if $i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(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); + }); + + test('@for', function () { + let parser = new SCSSParser(); + assertNode('@for $i from 1 to 5 { .item-#{$i} { width: 2em * $i; } }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@for $k from 1 + $x through 5 + $x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertError('@for i from 0 to 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.VariableNameExpected); + assertError('@for $i to 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.FromExpected); + assertError('@for $i from 0 by 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.ThroughOrToExpected); + assertError('@for $i from {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); + assertError('@for $i from 0 to {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); + }); + + test('@each', function () { + let parser = new SCSSParser(); + assertNode('@each $i in 1, 2, 3 { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@each $i in 1 2 3 { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertError('@each i in 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.VariableNameExpected); + assertError('@each $i from 4 {}', parser, parser._parseRuleSetDeclaration.bind(parser), SCSSParseError.InExpected); + assertError('@each $i in {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); + }); + + test('@while', function () { + let parser = new SCSSParser(); + assertNode('@while $i < 0 { .item-#{$i} { width: 2em * $i; } $i: $i - 2; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertError('@while {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); + assertError('@while $i != 4', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.LeftCurlyExpected); + assertError('@while ($i >= 4) {', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.RightCurlyExpected); + }); + + test('@mixin', function () { + let parser = new SCSSParser(); + assertNode('@mixin large-text { font: { family: Arial; size: 20px; } color: #ff0000; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin sexy-border($color, $width: 1in) { color: black; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin box-shadow($shadows...) { -moz-box-shadow: $shadows; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin apply-to-ie6-only { * html { @content; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin #{foo}($color){}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@mixin foo ($i:4) { size: $i; @include wee ($i - 1); }', parser, parser._parseStylesheet.bind(parser)); + assertError('@mixin $1 {}', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); + assertError('@mixin foo() i {}', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected); + assertError('@mixin foo(1) {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('@mixin foo($color = 9) {}', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('@mixin foo($color)', parser, parser._parseStylesheet.bind(parser), ParseError.LeftCurlyExpected); + assertError('@mixin foo($color){', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('@mixin foo($color,){', parser, parser._parseStylesheet.bind(parser), ParseError.VariableNameExpected); + }); + + test('@include', function () { + let parser = new SCSSParser(); + assertNode('p { @include sexy-border(blue); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.shadows { @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('$values: #ff0000, #00ff00, #0000ff; .primary { @include colors($values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include apply-to-ie6-only { #logo { background-image: url(/logo.gif); } } }', parser, parser._parseStylesheet.bind(parser)); + assertError('p { @include sexy-border blue', parser, parser._parseStylesheet.bind(parser), ParseError.SemiColonExpected); + assertError('p { @include sexy-border($values blue', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('p { @include }', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); + assertError('p { @include foo($values }', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('p { @include foo($values, }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected); + + }); + + test('@function', function () { + let parser = new SCSSParser(); + assertNode('@function grid-width($n) { @return $n * $grid-width + ($n - 1) * $gutter-width; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@function grid-width($n: 1, $e) { @return 0; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@function foo($total, $a) { @for $i from 0 to $total { } @return $grid; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@function foo() { @if (unit($a) == "%") and ($i == ($total - 1)) { @return 0; } @return 1; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@function is-even($int) { @if $int%2 == 0 { @return true; } @return false }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@function bar ($i) { @if $i > 0 { @return $i * bar($i - 1); } @return 1; }', parser, parser._parseStylesheet.bind(parser)); + assertError('@function foo {} ', parser, parser._parseStylesheet.bind(parser), ParseError.LeftParenthesisExpected); + assertError('@function {} ', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); + assertError('@function foo($a $b) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('@function foo($a {} ', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); + assertError('@function foo($a...) { @return; }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected); + assertError('@function foo($a,) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.VariableNameExpected); + assertError('@function foo($a:) {} ', parser, parser._parseStylesheet.bind(parser), ParseError.VariableValueExpected); + + }); + + test('Ruleset', function () { + let parser = new SCSSParser(); + assertNode('.selector { prop: erty $let 1px; }', 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)); + assertNode('selector { $variable: declaration }', parser, parser._parseRuleset.bind(parser)); + assertNode('selector { nested {}}', parser, parser._parseRuleset.bind(parser)); + assertNode('selector { nested, a, b {}}', parser, parser._parseRuleset.bind(parser)); + assertNode('selector { property: value; property: $value; }', parser, parser._parseRuleset.bind(parser)); + assertNode('selector { property: value; @keyframes foo {} @-moz-keyframes foo {}}', parser, parser._parseRuleset.bind(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 { > .class2 { & > .class4 { rule1: v1; } } }', parser, parser._parseRuleset.bind(parser)); + }); + + test('Selector Interpolation', function () { + let parser = new SCSSParser(); + assertNode('.#{$name} { }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{$name} { #{$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('sans-#{serif} { a-#{1 + 2}-color-#{$attr}: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('##{f} .#{f} #{f}:#{f} { }', parser, parser._parseRuleset.bind(parser)); + }); + + test('Parent Selector', function () { + let parser = new SCSSParser(); + assertNode('&:hover', parser, parser._parseSimpleSelector.bind(parser)); + assertNode('&.float', parser, parser._parseSimpleSelector.bind(parser)); + assertNode('&-bar', parser, parser._parseSimpleSelector.bind(parser)); + assertNode('&&', parser, parser._parseSimpleSelector.bind(parser)); + }); + + test('Selector Placeholder', function () { + let parser = new SCSSParser(); + assertNode('%hover', parser, parser._parseSimpleSelector.bind(parser)); + assertNode('a%float', parser, parser._parseSimpleSelector.bind(parser)); + }); +}); \ No newline at end of file diff --git a/extensions/css/server/src/test/scss/scssCompletion.test.ts b/extensions/css/server/src/test/scss/scssCompletion.test.ts new file mode 100644 index 00000000000..a65357da0bb --- /dev/null +++ b/extensions/css/server/src/test/scss/scssCompletion.test.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * 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 assert from 'assert'; + +import {SCSSParser} from '../../parser/scssParser'; +import {SCSSCompletion} from '../../services/scssCompletion'; +import * as nodes from '../../parser/cssNodes'; +import {TextDocument, Position} from 'vscode-languageserver'; +import {assertCompletion, ItemDescription} from '../css/completion.test'; + +suite('SCSS - Completions', () => { + + let testCompletionFor = function (value: string, stringBefore: string, expected: { count?: number, items?: ItemDescription[] }): Thenable { + let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0; + + let completionProvider = new SCSSCompletion(); + + let document = TextDocument.create('test://test/test.scss', 'scss', 0, value); + let position = Position.create(0, idx); + let jsonDoc = new SCSSParser().parseStylesheet(document); + return completionProvider.doComplete(document, position, jsonDoc).then(list => { + if (expected.count) { + assert.equal(list.items, expected.count); + } + if (expected.items) { + for (let item of expected.items) { + assertCompletion(list, item, document); + } + } + }); + }; + + test('sylesheet', function (testDone): any { + Promise.all([ + testCompletionFor('$i: 0; body { width: ', 'width: ', { + items: [ + { label: '$i' } + ] + }), + testCompletionFor('@for $i from 1 through 3 { .item-#{$i} { width: 2em * $i; } }', '.item-#{', { + items: [ + { label: '$i' } + ] + }), + testCompletionFor('.foo { background-color: d', 'background-color: d', { + items: [ + { label: 'darken' }, + { label: 'desaturate' } + ] + }), + testCompletionFor('@function foo($x, $y) { @return $x + $y; } .foo { background-color: f', 'background-color: f', { + items: [ + { label: 'foo' } + ] + }), + testCompletionFor('.foo { di span { } ', 'di', { + items: [ + { label: 'display' }, + { label: 'div' } + ] + }), + testCompletionFor('.foo { .', '{ .', { + items: [ + { label: '.foo' } + ] + }), + // issue #250 + testCompletionFor('.foo { display: block;', 'block;', { + count: 0 + }), + ]).then(() => testDone(), (error) => testDone(error)); + + }); +}); diff --git a/extensions/css/server/src/test/scss/scssNavigation.test.ts b/extensions/css/server/src/test/scss/scssNavigation.test.ts new file mode 100644 index 00000000000..d5eb3e64a84 --- /dev/null +++ b/extensions/css/server/src/test/scss/scssNavigation.test.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {SCSSParser} from '../../parser/scssParser'; +import * as nodes from '../../parser/cssNodes'; +import {assertSymbolsInScope, assertScopesAndSymbols, assertHighlights} from '../css/navigation.test'; + +suite('SCSS - Symbols', () => { + + test('symbols in scopes', function() { + var p = new SCSSParser(); + assertSymbolsInScope(p, '$var: iable;', 0, { name:'$var', type:nodes.ReferenceType.Variable }); + assertSymbolsInScope(p, '$var: iable;', 11, { name:'$var', type:nodes.ReferenceType.Variable }); + assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 11, { name:'$var', type:nodes.ReferenceType.Variable }, { name:'.class', type:nodes.ReferenceType.Rule }); + assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 22, { name:'$color', type:nodes.ReferenceType.Variable }); + assertSymbolsInScope(p, '$var: iable; .class { $color: blue; }', 36, { name:'$color', type:nodes.ReferenceType.Variable }); + + assertSymbolsInScope(p, '@namespace "x"; @mixin mix() {}', 0, { name:'mix', type:nodes.ReferenceType.Mixin }); + assertSymbolsInScope(p, '@mixin mix { @mixin nested() {} }', 12, { name:'nested', type:nodes.ReferenceType.Mixin }); + assertSymbolsInScope(p, '@mixin mix () { @mixin nested() {} }', 13); + }); + + test('scopes and symbols', function() { + var p = new SCSSParser(); + assertScopesAndSymbols(p, '$var1: 1; $var2: 2; .foo { $var3: 3; }', '$var1,$var2,.foo,[$var3]'); + assertScopesAndSymbols(p, '@mixin mixin1 { $var0: 1} @mixin mixin2($var1) { $var3: 3 }', 'mixin1,mixin2,[$var0],[$var1,$var3]'); + assertScopesAndSymbols(p, 'a b { $var0: 1; c { d { } } }', '[$var0,c,[d,[]]]'); + assertScopesAndSymbols(p, '@function a($p1: 1, $p2: 2) { $v1: 3; @return $v1; }', 'a,[$p1,$p2,$v1]'); + assertScopesAndSymbols(p, '$var1: 3; @if $var1 == 2 { $var2: 1; } @else { $var2: 2; $var3: 2;} ', '$var1,[$var2],[$var2,$var3]'); + assertScopesAndSymbols(p, '@if $var1 == 2 { $var2: 1; } @else if $var1 == 2 { $var3: 2; } @else { $var3: 2; } ', '[$var2],[$var3],[$var3]'); + assertScopesAndSymbols(p, '$var1: 3; @while $var1 < 2 { #rule { a: b; } }', '$var1,[#rule,[]]'); + assertScopesAndSymbols(p, '$i:0; @each $name in f1, f2, f3 { $i:$i+1; }', '$i,[$name,$i]'); + assertScopesAndSymbols(p, '$i:0; @for $x from $i to 5 { }', '$i,[$x]'); + }); + + test('mark highlights', function(testDone) { + var p = new SCSSParser(); + Promise.all([ + assertHighlights(p, '$var1: 1; $var2: /**/$var1;', '$var1', 2, 1), + assertHighlights(p, '$var1: 1; p { $var2: /**/$var1; }', '/**/', 2, 1, '$var1'), + assertHighlights(p, 'r1 { $var1: 1; p1: $var1;} r2,r3 { $var1: 1; p1: /**/$var1 + $var1;}', '/**/', 3, 1, '$var1'), + assertHighlights(p, '.r1 { r1: 1em; } r2 { r1: 2em; @extend /**/.r1;}', '/**/', 2, 1, '.r1'), + assertHighlights(p, '/**/%r1 { r1: 1em; } r2 { r1: 2em; @extend %r1;}', '/**/', 2, 1, '%r1'), + assertHighlights(p, '@mixin r1 { r1: $p1; } r2 { r2: 2em; @include /**/r1; }', '/**/', 2, 1, 'r1'), + assertHighlights(p, '@mixin r1($p1) { r1: $p1; } r2 { r2: 2em; @include /**/r1(2px); }', '/**/', 2, 1, 'r1'), + assertHighlights(p, '$p1: 1; @mixin r1($p1: $p1) { r1: $p1; } r2 { r2: 2em; @include /**/r1; }', '/**/', 2, 1, 'r1'), + assertHighlights(p, '/**/$p1: 1; @mixin r1($p1: $p1) { r1: $p1; }', '/**/', 2, 1, '$p1'), + assertHighlights(p, '$p1 : 1; @mixin r1($p1) { r1: /**/$p1; }', '/**/', 2, 1, '$p1'), + assertHighlights(p, '/**/$p1 : 1; @mixin r1($p1) { r1: $p1; }', '/**/', 1, 1, '$p1'), + assertHighlights(p, '$p1 : 1; @mixin r1(/**/$p1) { r1: $p1; }', '/**/', 2, 1, '$p1'), + assertHighlights(p, '$p1 : 1; @function r1($p1, $p2: /**/$p1) { @return $p1 + $p1 + $p2; }', '/**/', 2, 1, '$p1'), + assertHighlights(p, '$p1 : 1; @function r1($p1, /**/$p2: $p1) { @return $p1 + $p2 + $p2; }', '/**/', 3, 1, '$p2'), + assertHighlights(p, '@function r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return /**/r1(1, 2); }', '/**/', 2, 1, 'r1'), + assertHighlights(p, '@function /**/r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return r1(1, 2); } p { x: r2(); }', '/**/', 2, 1, 'r1'), + assertHighlights(p, '@function r1($p1, $p2) { @return $p1 + $p2; } @function r2() { @return r1(/**/$p1 : 1, $p2 : 2); } p { x: r2(); }', '/**/', 3, 1, '$p1'), + + assertHighlights(p, '@mixin /*here*/foo { display: inline } foo { @include foo; }', '/*here*/', 2, 1, 'foo'), + assertHighlights(p, '@mixin foo { display: inline } foo { @include /*here*/foo; }', '/*here*/', 2, 1, 'foo'), + assertHighlights(p, '@mixin foo { display: inline } /*here*/foo { @include foo; }', '/*here*/', 1, 1, 'foo'), + assertHighlights(p, '@function /*here*/foo($i) { @return $i*$i; } #foo { width: foo(2); }', '/*here*/', 2, 1, 'foo'), + assertHighlights(p, '@function foo($i) { @return $i*$i; } #foo { width: /*here*/foo(2); }', '/*here*/', 2, 1, 'foo') + ]).then(() => testDone(), (error) => testDone(error)); + }); + +}); \ No newline at end of file diff --git a/extensions/css/server/src/test/scss/selectorPrinting.test.ts b/extensions/css/server/src/test/scss/selectorPrinting.test.ts new file mode 100644 index 00000000000..08648a1fcd3 --- /dev/null +++ b/extensions/css/server/src/test/scss/selectorPrinting.test.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {SCSSParser} from '../../parser/scssParser'; +import {parseSelector} from '../css/selectorPrinting.test'; + +suite('SCSS - Selector Printing', () => { + + test('nested selector', function () { + let p = new SCSSParser(); + parseSelector(p, 'o1 { e1 { } }', 'e1', '{o1{…{e1}}}'); + parseSelector(p, 'o1 { e1.div { } }', 'e1', '{o1{…{e1[class=div]}}}'); + parseSelector(p, 'o1 o2 { e1 { } }', 'e1', '{o1{…{o2{…{e1}}}}}'); + parseSelector(p, 'o1, o2 { e1 { } }', 'e1', '{o1{…{e1}}}'); + parseSelector(p, 'o1 { @if $a { e1 { } } }', 'e1', '{o1{…{e1}}}'); + parseSelector(p, 'o1 { @mixin a { e1 { } } }', 'e1', '{e1}'); + parseSelector(p, 'o1 { @mixin a { e1 { } } }', 'e1', '{e1}'); + }); + + test('referencing selector', function () { + let p = new SCSSParser(); + parseSelector(p, 'o1 { &:hover { }}', '&', '{o1[:hover=]}'); + parseSelector(p, 'o1 { &:hover & { }}', '&', '{o1[:hover=]{…{o1}}}'); + }); + + test('placeholders', function () { + let p = new SCSSParser(); + parseSelector(p, '%o1 { e1 { } }', 'e1', '{%o1{…{e1}}}'); + }); +}); \ No newline at end of file diff --git a/extensions/css/server/test/mocha.opts b/extensions/css/server/test/mocha.opts index 710cf36a9f0..b3834b9652f 100644 --- a/extensions/css/server/test/mocha.opts +++ b/extensions/css/server/test/mocha.opts @@ -1,3 +1,4 @@ --ui tdd --useColors true -./out/test +./out/test/css +./out/test/scss -- GitLab