From 9fa8bc866aa0e3863dc54ab0195ff3ea6233299a Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 14:51:30 +0100 Subject: [PATCH 01/34] Recognize @use --- src/parser/cssNodes.ts | 8 ++++++++ src/parser/scssParser.ts | 18 +++++++++++++++++- src/test/scss/parser.test.ts | 8 +++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index d67eac83..48ccb48e 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -63,6 +63,7 @@ export enum NodeType { Keyframe, FontFace, Import, + Use, Namespace, Invocation, FunctionDeclaration, @@ -1008,6 +1009,13 @@ export class Import extends Node { } } +export class Use extends Node { + + public get type(): NodeType { + return NodeType.Use; + } +} + export class Namespace extends Node { constructor(offset: number, length: number) { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 7e10e8e1..25561506 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -29,6 +29,7 @@ export class SCSSParser extends cssParser.Parser { || this._parseMixinContent() || this._parseMixinReference() // @include || this._parseFunctionDeclaration() + || this._parseUse() || super._parseStylesheetAtStatement(); } return this._parseRuleset(true) || this._parseVariableDeclaration(); @@ -77,7 +78,7 @@ export class SCSSParser extends cssParser.Parser { if (this.prevToken) { node.colonPosition = this.prevToken.offset; } - + if (!node.setValue(this._parseExpr())) { return this.finish(node, ParseError.VariableValueExpected, [], panic); } @@ -667,4 +668,19 @@ export class SCSSParser extends cssParser.Parser { } return this.finish(node); } + + public _parseUse(): nodes.Node | null { + if (!this.peekKeyword('@use')) { + return null; + } + + const node = this.create(nodes.Use); + this.consumeToken(); + + if (!node.addChild(this._parseStringLiteral())) { + return this.finish(node, ParseError.StringLiteralExpected); + } + + return this.finish(node); + } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 4a0b3719..2a27fcf6 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -177,6 +177,12 @@ suite('SCSS - Parser', () => { assertError('@import', parser, parser._parseImport.bind(parser), ParseError.URIOrStringExpected); }); + test('@use', function () { + let parser = new SCSSParser(); + + assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); + }) + test('@media', function () { let parser = new SCSSParser(); assertNode('@media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } }', parser, parser._parseStylesheet.bind(parser)); @@ -405,4 +411,4 @@ suite('SCSS - Parser', () => { assertError('url("http://msft.com"', parser, parser._parseURILiteral.bind(parser), ParseError.RightParenthesisExpected); assertError('url(http://msft.com\')', parser, parser._parseURILiteral.bind(parser), ParseError.RightParenthesisExpected); }); -}); \ No newline at end of file +}); From 0036d50cb345e7ab64bec96a67ad7ac56a7f1506 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 15:41:02 +0100 Subject: [PATCH 02/34] Add support for @use explicit namespacing --- src/parser/scssParser.ts | 4 ++++ src/test/scss/parser.test.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 25561506..658d1337 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -681,6 +681,10 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } + if (this.acceptIdent('as') && !node.addChild(this._parseIdent())) { + return this.finish(node, ParseError.IdentifierExpected); + } + return this.finish(node); } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 2a27fcf6..14b153f2 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -179,8 +179,11 @@ suite('SCSS - Parser', () => { test('@use', function () { let parser = new SCSSParser(); + assertNode('@use "test"', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as foo', parser, parser._parseUse.bind(parser)); assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); + assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierExpected); }) test('@media', function () { From a205cb7623a85acecb1a68b7243830f28c4fd71d Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 15:51:50 +0100 Subject: [PATCH 03/34] Add wildcard expansion for @use --- src/parser/cssErrors.ts | 3 ++- src/parser/scssParser.ts | 4 ++-- src/test/scss/parser.test.ts | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index 986b11ab..5b17ab88 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -49,5 +49,6 @@ export const ParseError = { SelectorExpected: new CSSIssueType('css-selectorexpected', localize('expected.selector', "selector expected")), StringLiteralExpected: new CSSIssueType('css-stringliteralexpected', localize('expected.stringliteral', "string literal expected")), WhitespaceExpected: new CSSIssueType('css-whitespaceexpected', localize('expected.whitespace', "whitespace expected")), - MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")) + MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")), + IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', localize('expected.idorwildcard', "identifier or wildcard expected")), }; diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 658d1337..c34bc867 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -681,8 +681,8 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } - if (this.acceptIdent('as') && !node.addChild(this._parseIdent())) { - return this.finish(node, ParseError.IdentifierExpected); + if (this.acceptIdent('as') && (!node.addChild(this._parseIdent()) && !this.acceptDelim('*'))) { + return this.finish(node, ParseError.IdentifierOrWildcardExpected); } return this.finish(node); diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 14b153f2..6405be9f 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -181,6 +181,7 @@ suite('SCSS - Parser', () => { let parser = new SCSSParser(); assertNode('@use "test"', parser, parser._parseUse.bind(parser)); assertNode('@use "test" as foo', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as *', parser, parser._parseUse.bind(parser)); assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierExpected); From 27755ced1b0f1502622f853821bbae92cc921aa4 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:16:02 +0100 Subject: [PATCH 04/34] Set @use identifier from 'as' statement --- src/parser/cssNodes.ts | 13 ++++++++++++- src/parser/scssParser.ts | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 48ccb48e..923da430 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -92,7 +92,8 @@ export enum ReferenceType { Variable, Function, Keyframe, - Unknown + Unknown, + Module, } @@ -1011,9 +1012,19 @@ export class Import extends Node { export class Use extends Node { + public identifier?: Identifier; + public get type(): NodeType { return NodeType.Use; } + + public setIdentifier(node: Identifier | null): node is Identifier { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Identifier | undefined { + return this.identifier; + } } export class Namespace extends Node { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index c34bc867..8788d57f 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -681,7 +681,10 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } - if (this.acceptIdent('as') && (!node.addChild(this._parseIdent()) && !this.acceptDelim('*'))) { + if ( + this.acceptIdent('as') && + (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module])) && !this.acceptDelim('*')) + ) { return this.finish(node, ParseError.IdentifierOrWildcardExpected); } From 698ead953f36659097244a4675959f7ae3b8212d Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:43:46 +0100 Subject: [PATCH 05/34] Parse @use with configuration --- src/parser/cssNodes.ts | 39 +++++++++++++++++++++++++++++++++++ src/parser/scssParser.ts | 40 ++++++++++++++++++++++++++++++++++++ src/test/scss/parser.test.ts | 9 +++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 923da430..ab62d09d 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -84,6 +84,7 @@ export enum NodeType { GridLine, Plugin, UnknownAtRule, + ModuleConfiguration, } export enum ReferenceType { @@ -1013,11 +1014,19 @@ export class Import extends Node { export class Use extends Node { public identifier?: Identifier; + public parameters?: Nodelist; public get type(): NodeType { return NodeType.Use; } + public getParameters(): Nodelist { + if (!this.parameters) { + this.parameters = new Nodelist(this); + } + return this.parameters; + } + public setIdentifier(node: Identifier | null): node is Identifier { return this.setNode('identifier', node, 0); } @@ -1027,6 +1036,36 @@ export class Use extends Node { } } +export class ModuleConfiguration extends Node { + + public identifier?: Node; + public value?: Node; + + public get type(): NodeType { + return NodeType.ModuleConfiguration; + } + + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Node | undefined { + return this.identifier; + } + + public getName(): string { + return this.identifier ? this.identifier.getText() : ''; + } + + public setValue(node: Node | null): node is Node { + return this.setNode('value', node, 0); + } + + public getValue(): Node | undefined { + return this.value; + } +} + export class Namespace extends Node { constructor(offset: number, length: number) { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 8788d57f..bc2fc3f1 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -688,6 +688,46 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.IdentifierOrWildcardExpected); } + if (this.acceptIdent('with')) { + if (!this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.ParenthesisR]); + } + + // First variable statement, no comma. + if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + + while (this.accept(TokenType.Comma)) { + if (this.peek(TokenType.ParenthesisR)) { + break; + } + if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + } + + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected); + } + + } + + return this.finish(node); + } + + public _parseModuleConfigDeclaration(): nodes.Node | null { + + const node = this.create(nodes.ModuleConfiguration); + + if (!node.setIdentifier(this._parseVariable())) { + return null; + } + + if (!this.accept(TokenType.Colon) || !node.setValue(this._parseExpr(true))) { + return this.finish(node, ParseError.VariableValueExpected, [], [TokenType.Comma, TokenType.ParenthesisR]); + } + return this.finish(node); } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 6405be9f..9ee8321b 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -182,9 +182,16 @@ suite('SCSS - Parser', () => { assertNode('@use "test"', parser, parser._parseUse.bind(parser)); assertNode('@use "test" as foo', parser, parser._parseUse.bind(parser)); assertNode('@use "test" as *', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" with ($foo: "test", $bar: 1)', parser, parser._parseUse.bind(parser)); + assertNode('@use "test" as foo with ($foo: "test", $bar: 1)', parser, parser._parseUse.bind(parser)); assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); - assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierExpected); + assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierOrWildcardExpected); + assertError('@use "test" with', parser, parser._parseUse.bind(parser), ParseError.LeftParenthesisExpected); + assertError('@use "test" with ($foo)', parser, parser._parseUse.bind(parser), ParseError.VariableValueExpected); + assertError('@use "test" with ("bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); + assertError('@use "test" with ($foo: 1, "bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); + assertError('@use "test" with ($foo: "bar"', parser, parser._parseUse.bind(parser), ParseError.RightParenthesisExpected); }) test('@media', function () { From 9c516d3e1b266664e8cead3fb86236001f474ece Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:44:41 +0100 Subject: [PATCH 06/34] Fix missing semi colon --- src/test/scss/parser.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 9ee8321b..eed0af5e 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -192,7 +192,7 @@ suite('SCSS - Parser', () => { assertError('@use "test" with ("bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); assertError('@use "test" with ($foo: 1, "bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); assertError('@use "test" with ($foo: "bar"', parser, parser._parseUse.bind(parser), ParseError.RightParenthesisExpected); - }) + }); test('@media', function () { let parser = new SCSSParser(); From 8ae7985859ec4aeb786c2e50093c8832fb192f63 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:45:34 +0100 Subject: [PATCH 07/34] Change use NodeType enum value --- src/parser/cssNodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index ab62d09d..c2d6abe3 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -63,7 +63,6 @@ export enum NodeType { Keyframe, FontFace, Import, - Use, Namespace, Invocation, FunctionDeclaration, @@ -84,6 +83,7 @@ export enum NodeType { GridLine, Plugin, UnknownAtRule, + Use, ModuleConfiguration, } From 90f4633026bd87254333723b08d8a302c86a4d42 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 16:57:54 +0100 Subject: [PATCH 08/34] Add basic @forward parsing --- src/parser/cssNodes.ts | 9 +++++++++ src/parser/scssParser.ts | 16 ++++++++++++++++ src/test/scss/parser.test.ts | 7 +++++++ 3 files changed, 32 insertions(+) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index c2d6abe3..f29f8f24 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -85,6 +85,7 @@ export enum NodeType { UnknownAtRule, Use, ModuleConfiguration, + Forward, } export enum ReferenceType { @@ -1066,6 +1067,14 @@ export class ModuleConfiguration extends Node { } } +export class Forward extends Node { + + public get type(): NodeType { + return NodeType.Forward; + } + +} + export class Namespace extends Node { constructor(offset: number, length: number) { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index bc2fc3f1..8e5211af 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -29,6 +29,7 @@ export class SCSSParser extends cssParser.Parser { || this._parseMixinContent() || this._parseMixinReference() // @include || this._parseFunctionDeclaration() + || this._parseForward() || this._parseUse() || super._parseStylesheetAtStatement(); } @@ -730,4 +731,19 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node); } + + public _parseForward(): nodes.Node | null { + if (!this.peekKeyword('@forward')) { + return null; + } + + const node = this.create(nodes.Forward); + this.consumeToken(); + + if (!node.addChild(this._parseStringLiteral())) { + return this.finish(node, ParseError.StringLiteralExpected); + } + + return this.finish(node); + } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index eed0af5e..400ed50d 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -194,6 +194,13 @@ suite('SCSS - Parser', () => { assertError('@use "test" with ($foo: "bar"', parser, parser._parseUse.bind(parser), ParseError.RightParenthesisExpected); }); + test('@forward', function () { + let parser = new SCSSParser(); + assertNode('@forward "test"', parser, parser._parseForward.bind(parser)); + + assertError('@forward', parser, parser._parseForward.bind(parser), ParseError.StringLiteralExpected); + }); + test('@media', function () { let parser = new SCSSParser(); assertNode('@media screen { .sidebar { @media (orientation: landscape) { width: 500px; } } }', parser, parser._parseStylesheet.bind(parser)); From 88cb645212a16aecb7add2ba5fb25d7150501fe9 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 17:17:18 +0100 Subject: [PATCH 09/34] Parse @forward 'as' syntax --- src/parser/cssErrors.ts | 1 + src/parser/cssNodes.ts | 11 +++++++++++ src/parser/scssParser.ts | 12 ++++++++++++ src/test/scss/parser.test.ts | 4 ++++ 4 files changed, 28 insertions(+) diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index 5b17ab88..9e2a624f 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -51,4 +51,5 @@ export const ParseError = { WhitespaceExpected: new CSSIssueType('css-whitespaceexpected', localize('expected.whitespace', "whitespace expected")), MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")), IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', localize('expected.idorwildcard', "identifier or wildcard expected")), + WildcardExpected: new CSSIssueType('css-wildcardexpected', localize('expected.wildcard', "wildcard expected")), }; diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index f29f8f24..a98af46a 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -96,6 +96,7 @@ export enum ReferenceType { Keyframe, Unknown, Module, + Forward, } @@ -1069,10 +1070,20 @@ export class ModuleConfiguration extends Node { export class Forward extends Node { + public identifier?: Node; + public get type(): NodeType { return NodeType.Forward; } + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Node | undefined { + return this.identifier; + } + } export class Namespace extends Node { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 8e5211af..6fcda848 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -744,6 +744,18 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } + if (this.acceptIdent('as')) { + const identifier = this._parseIdent([nodes.ReferenceType.Forward]); + if (!node.setIdentifier(identifier)) { + return this.finish(node, ParseError.IdentifierExpected); + } + + // Wildcard must be the next character after the identifier string. + if (!this.acceptDelim('*') || this.prevToken!.offset != identifier.end) { + return this.finish(node, ParseError.WildcardExpected); + } + } + return this.finish(node); } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 400ed50d..bdea8dc5 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -197,8 +197,12 @@ suite('SCSS - Parser', () => { test('@forward', function () { let parser = new SCSSParser(); assertNode('@forward "test"', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" as foo-*', parser, parser._parseForward.bind(parser)); assertError('@forward', parser, parser._parseForward.bind(parser), ParseError.StringLiteralExpected); + assertError('@forward "test" as', parser, parser._parseForward.bind(parser), ParseError.IdentifierExpected); + assertError('@forward "test" as foo-', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); + assertError('@forward "test" as foo- *', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); }); test('@media', function () { From 65e37134c6f39532f81dcf9da125bbd6efddb3a3 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 20:30:33 +0100 Subject: [PATCH 10/34] Parse @forward visibility modifiers --- src/parser/cssErrors.ts | 1 + src/parser/cssNodes.ts | 21 +++++++++++++++++++++ src/parser/scssParser.ts | 27 +++++++++++++++++++++++++++ src/test/scss/parser.test.ts | 5 +++++ 4 files changed, 54 insertions(+) diff --git a/src/parser/cssErrors.ts b/src/parser/cssErrors.ts index 9e2a624f..4afed13d 100644 --- a/src/parser/cssErrors.ts +++ b/src/parser/cssErrors.ts @@ -52,4 +52,5 @@ export const ParseError = { MediaQueryExpected: new CSSIssueType('css-mediaqueryexpected', localize('expected.mediaquery', "media query expected")), IdentifierOrWildcardExpected: new CSSIssueType('css-idorwildcardexpected', localize('expected.idorwildcard', "identifier or wildcard expected")), WildcardExpected: new CSSIssueType('css-wildcardexpected', localize('expected.wildcard', "wildcard expected")), + IdentifierOrVariableExpected: new CSSIssueType('css-idorvarexpected', localize('expected.idorvar', "identifier or variable expected")), }; diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index a98af46a..0027939e 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -86,6 +86,7 @@ export enum NodeType { Use, ModuleConfiguration, Forward, + ForwardVisibility, } export enum ReferenceType { @@ -97,6 +98,7 @@ export enum ReferenceType { Unknown, Module, Forward, + ForwardVisibility, } @@ -1086,6 +1088,25 @@ export class Forward extends Node { } +export class ForwardVisibility extends Node { + + public visibility?: string; + + public get type(): NodeType { + return NodeType.ForwardVisibility; + } + + public setVisibility(visibility: string): boolean { + this.visibility = visibility; + return true; + } + + public getVisibility(): string | undefined { + return this.visibility; + } + +} + export class Namespace extends Node { constructor(offset: number, length: number) { diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 6fcda848..aa6d818a 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -756,6 +756,33 @@ export class SCSSParser extends cssParser.Parser { } } + for (const visibility of ['hide', 'show']) { + if (this.acceptIdent(visibility)) { + if (!node.addChild(this._parseForwardVisibility(visibility))) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); + } + // Only can have one of 'hide' or 'show'. + break; + } + } + return this.finish(node); } + + public _parseForwardVisibility(visibility: string): nodes.Node | null { + const node = this.create(nodes.ForwardVisibility); + node.setVisibility(visibility); + + this.consumeToken(); + + const children: nodes.Node[] = []; + let child: nodes.Node | null; + while (child = this._parseVariable() || this._parseIdent()) { + children.push(child); + } + node.addChildren(children); + + return children.length ? node : null; + } + } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index bdea8dc5..22d4ffc2 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -198,11 +198,16 @@ suite('SCSS - Parser', () => { let parser = new SCSSParser(); assertNode('@forward "test"', parser, parser._parseForward.bind(parser)); assertNode('@forward "test" as foo-*', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide this $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show this $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" as foo-* show this $that', parser, parser._parseForward.bind(parser)); assertError('@forward', parser, parser._parseForward.bind(parser), ParseError.StringLiteralExpected); assertError('@forward "test" as', parser, parser._parseForward.bind(parser), ParseError.IdentifierExpected); assertError('@forward "test" as foo-', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); assertError('@forward "test" as foo- *', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); + assertError('@forward "test" show', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); + assertError('@forward "test" hide', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); }); test('@media', function () { From dd00de11da5f1333aeb6ec83b1acbe18d4351690 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 20:40:43 +0100 Subject: [PATCH 11/34] Validate keyword on @use and @forward --- src/parser/scssParser.ts | 80 +++++++++++++++++++++--------------- src/test/scss/parser.test.ts | 2 + 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index aa6d818a..3f9607d0 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -682,36 +682,42 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } - if ( - this.acceptIdent('as') && - (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module])) && !this.acceptDelim('*')) - ) { - return this.finish(node, ParseError.IdentifierOrWildcardExpected); - } - - if (this.acceptIdent('with')) { - if (!this.accept(TokenType.ParenthesisL)) { - return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.ParenthesisR]); + if (!this.peek(TokenType.SemiColon) && !this.peek(TokenType.EOF)) { + if (!this.peekRegExp(TokenType.Ident, /as|with/)) { + return this.finish(node, ParseError.UnknownKeyword); } - // First variable statement, no comma. - if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { - return this.finish(node, ParseError.VariableNameExpected); + if ( + this.acceptIdent('as') && + (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module])) && !this.acceptDelim('*')) + ) { + return this.finish(node, ParseError.IdentifierOrWildcardExpected); } - while (this.accept(TokenType.Comma)) { - if (this.peek(TokenType.ParenthesisR)) { - break; + if (this.acceptIdent('with')) { + if (!this.accept(TokenType.ParenthesisL)) { + return this.finish(node, ParseError.LeftParenthesisExpected, [TokenType.ParenthesisR]); } + + // First variable statement, no comma. if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { return this.finish(node, ParseError.VariableNameExpected); } - } - if (!this.accept(TokenType.ParenthesisR)) { - return this.finish(node, ParseError.RightParenthesisExpected); - } + while (this.accept(TokenType.Comma)) { + if (this.peek(TokenType.ParenthesisR)) { + break; + } + if (!node.getParameters().addChild(this._parseModuleConfigDeclaration())) { + return this.finish(node, ParseError.VariableNameExpected); + } + } + if (!this.accept(TokenType.ParenthesisR)) { + return this.finish(node, ParseError.RightParenthesisExpected); + } + + } } return this.finish(node); @@ -744,25 +750,31 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node, ParseError.StringLiteralExpected); } - if (this.acceptIdent('as')) { - const identifier = this._parseIdent([nodes.ReferenceType.Forward]); - if (!node.setIdentifier(identifier)) { - return this.finish(node, ParseError.IdentifierExpected); + if (!this.peek(TokenType.SemiColon) && !this.peek(TokenType.EOF)) { + if (!this.peekRegExp(TokenType.Ident, /as|hide|show/)) { + return this.finish(node, ParseError.UnknownKeyword); } - // Wildcard must be the next character after the identifier string. - if (!this.acceptDelim('*') || this.prevToken!.offset != identifier.end) { - return this.finish(node, ParseError.WildcardExpected); + if (this.acceptIdent('as')) { + const identifier = this._parseIdent([nodes.ReferenceType.Forward]); + if (!node.setIdentifier(identifier)) { + return this.finish(node, ParseError.IdentifierExpected); + } + + // Wildcard must be the next character after the identifier string. + if (!this.acceptDelim('*') || this.prevToken!.offset != identifier.end) { + return this.finish(node, ParseError.WildcardExpected); + } } - } - for (const visibility of ['hide', 'show']) { - if (this.acceptIdent(visibility)) { - if (!node.addChild(this._parseForwardVisibility(visibility))) { - return this.finish(node, ParseError.IdentifierOrVariableExpected); + for (const visibility of ['hide', 'show']) { + if (this.acceptIdent(visibility)) { + if (!node.addChild(this._parseForwardVisibility(visibility))) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); + } + // Only can have one of 'hide' or 'show'. + break; } - // Only can have one of 'hide' or 'show'. - break; } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 22d4ffc2..d44d180c 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -186,6 +186,7 @@ suite('SCSS - Parser', () => { assertNode('@use "test" as foo with ($foo: "test", $bar: 1)', parser, parser._parseUse.bind(parser)); assertError('@use', parser, parser._parseUse.bind(parser), ParseError.StringLiteralExpected); + assertError('@use "test" foo', parser, parser._parseUse.bind(parser), ParseError.UnknownKeyword); assertError('@use "test" as', parser, parser._parseUse.bind(parser), ParseError.IdentifierOrWildcardExpected); assertError('@use "test" with', parser, parser._parseUse.bind(parser), ParseError.LeftParenthesisExpected); assertError('@use "test" with ($foo)', parser, parser._parseUse.bind(parser), ParseError.VariableValueExpected); @@ -203,6 +204,7 @@ suite('SCSS - Parser', () => { assertNode('@forward "test" as foo-* show this $that', parser, parser._parseForward.bind(parser)); assertError('@forward', parser, parser._parseForward.bind(parser), ParseError.StringLiteralExpected); + assertError('@forward "test" foo', parser, parser._parseForward.bind(parser), ParseError.UnknownKeyword); assertError('@forward "test" as', parser, parser._parseForward.bind(parser), ParseError.IdentifierExpected); assertError('@forward "test" as foo-', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); assertError('@forward "test" as foo- *', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); From 849658924af3903e8809cc92c472ffd01ebaca91 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 12 Oct 2019 21:47:22 +0100 Subject: [PATCH 12/34] Add basic module variable parsing --- src/parser/cssNodes.ts | 16 ++++++++++++++++ src/parser/scssParser.ts | 12 ++++++++++++ src/parser/scssScanner.ts | 10 ++++++++++ src/test/scss/parser.test.ts | 7 +++++++ 4 files changed, 45 insertions(+) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 0027939e..ee589120 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -87,6 +87,7 @@ export enum NodeType { ModuleConfiguration, Forward, ForwardVisibility, + ModuleName, } export enum ReferenceType { @@ -1465,6 +1466,8 @@ export class Interpolation extends Node { export class Variable extends Node { + public module?: Module; + constructor(offset: number, length: number) { super(offset, length); } @@ -1654,6 +1657,19 @@ export class GuardCondition extends Node { } } +export class Module extends Node { + + public get type(): NodeType { + return NodeType.ModuleName; + } + + public getName(): string { + const text = this.getText(); + return text.substr(0, text.length - 1); + } + +} + export interface IRule { id: string; message: string; diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 3f9607d0..9d019854 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -118,6 +118,18 @@ export class SCSSParser extends cssParser.Parser { return node; } + public _parseModuleVariable(): nodes.Module | null { + if (!this.peek(scssScanner.Module)) { + return null; + } + const moduleNode = this.create(nodes.Module); + this.consumeToken(); + + moduleNode.addChild(this._parseVariable()); + + return moduleNode; + } + public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier | null { if (!this.peek(TokenType.Ident) && !this.peek(scssScanner.InterpolationFunction) && !this.peekDelim('-')) { return null; diff --git a/src/parser/scssScanner.ts b/src/parser/scssScanner.ts index ddc9dbaf..0cdc025b 100644 --- a/src/parser/scssScanner.ts +++ b/src/parser/scssScanner.ts @@ -30,6 +30,7 @@ export const NotEqualsOperator: TokenType = customTokenValue++; export const GreaterEqualsOperator: TokenType = customTokenValue++; export const SmallerEqualsOperator: TokenType = customTokenValue++; export const Ellipsis: TokenType = customTokenValue++; +export const Module: TokenType = customTokenValue++; export class SCSSScanner extends Scanner { @@ -45,6 +46,15 @@ export class SCSSScanner extends Scanner { } } + // scss module variable access + const moduleVariable: string[] = []; + if (this.ident(moduleVariable) && this.stream.advanceIfChars([_DOT, _DLR])) { + this.stream.goBack(1); + return this.finishToken(offset, Module, moduleVariable.concat('.').join('')); + } else { + this.stream.goBackTo(offset); + } + // scss: interpolation function #{..}) if (this.stream.advanceIfChars([_HSH, _CUL])) { return this.finishToken(offset, InterpolationFunction); diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index d44d180c..c6f0f9ed 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -27,6 +27,13 @@ suite('SCSS - Parser', () => { assertNode('$-co42lor', parser, parser._parseVariable.bind(parser)); }); + test('Module variable', function () { + let parser = new SCSSParser(); + assertNode('module.$color', parser, parser._parseModuleVariable.bind(parser)); + assertNode('module.$co42lor', parser, parser._parseModuleVariable.bind(parser)); + assertNode('module.$-co42lor', parser, parser._parseModuleVariable.bind(parser)); + }); + test('VariableDeclaration', function () { let parser = new SCSSParser(); assertNode('$color: #F5F5F5', parser, parser._parseVariableDeclaration.bind(parser)); From 4007c33c78100e8a00f656b548cdb0d8f84d1027 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 08:31:59 +0100 Subject: [PATCH 13/34] Parse module variables in expressions --- src/parser/scssParser.ts | 1 + src/test/scss/parser.test.ts | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 9d019854..05dee49f 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -169,6 +169,7 @@ export class SCSSParser extends cssParser.Parser { term = this.create(nodes.Term); if (term.setExpression(this._parseVariable()) + || term.setExpression(this._parseModuleVariable()) || term.setExpression(this._parseSelectorCombinator()) || term.setExpression(this._tryParsePrio())) { return this.finish(term); diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index c6f0f9ed..2361e668 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -83,6 +83,48 @@ suite('SCSS - Parser', () => { assertNode('100% / 2 + $filler', parser, parser._parseExpr.bind(parser)); assertNode('not ($v and $b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let + 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let - 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let * 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let / 20)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 - module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 * module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 / module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('(20 + 20 + module.$let + 20 + 20 + module.$let)', parser, parser._parseExpr.bind(parser)); + assertNode('($var1 + module.$var2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$var1 + $var2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$var1 + module.$var2)', parser, parser._parseExpr.bind(parser)); + assertNode('((module.$let + 5) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('((module.$let + (5 + 2)) * 2)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$let + ((5 + 2) * 2))', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%, $color', parser, parser._parseExpr.bind(parser)); + assertNode('$color, 42%, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color, 42%, module.$color', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color - ($color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('$color - (module.$color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('module.$color - (module.$color + 10%)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$base + $filler)', parser, parser._parseExpr.bind(parser)); + assertNode('($base + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('(module.$base + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('(100% / 2 + module.$filler)', parser, parser._parseExpr.bind(parser)); + assertNode('100% / 2 + module.$filler', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and $b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and module.$b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and $b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and module.$b) or $c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and $b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not ($v and module.$b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertNode('not (module.$v and module.$b) or module.$c', parser, parser._parseExpr.bind(parser)); + assertError('(20 + 20', parser, parser._parseExpr.bind(parser), ParseError.RightParenthesisExpected); }); From 760167ec725c247443e8a093430023c10bf0b2ab Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 08:35:22 +0100 Subject: [PATCH 14/34] Add tests for interpolated module variables --- src/test/scss/parser.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 2361e668..c83f5f94 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -161,6 +161,23 @@ suite('SCSS - Parser', () => { assertNode('some-property: var(--#{$propname})', parser, parser._parseDeclaration.bind(parser)); assertNode('#{}', parser, parser._parseIdent.bind(parser)); assertError('#{1 + 2', parser, parser._parseIdent.bind(parser), ParseError.RightCurlyExpected); + + assertNode('#{module.$color}', parser, parser._parseIdent.bind(parser)); + assertNode('#{module.$d}-style: 0', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{module.$d}: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-bar-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-bar-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-bar-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo-#{module.$d}-bar: 1', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-#{$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{$d}-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('#{module.$d}-#{module.$d}: 2', parser, parser._parseDeclaration.bind(parser)); + assertNode('&:nth-child(#{module.$query}+1) { clear: $opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('&:nth-child(#{$query}+1) { clear: module.$opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('&:nth-child(#{module.$query}+1) { clear: module.$opposite-direction; }', parser, parser._parseRuleset.bind(parser)); + assertNode('--#{module.$propname}: some-value', parser, parser._parseDeclaration.bind(parser)); + assertNode('some-property: var(--#{module.$propname})', parser, parser._parseDeclaration.bind(parser)); }); test('Declaration', function () { From 3861c1126a2e2883674740b3603d6085d6eb8ae2 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 09:47:20 +0100 Subject: [PATCH 15/34] Refactor parser for function module members --- src/parser/cssNodes.ts | 15 ++++++++++----- src/parser/scssParser.ts | 31 ++++++++++++++++++++---------- src/parser/scssScanner.ts | 9 --------- src/test/scss/parser.test.ts | 37 +++++++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index ee589120..5338f05f 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -87,7 +87,7 @@ export enum NodeType { ModuleConfiguration, Forward, ForwardVisibility, - ModuleName, + Module, } export enum ReferenceType { @@ -1659,13 +1659,18 @@ export class GuardCondition extends Node { export class Module extends Node { + public identifier?: Identifier; + public get type(): NodeType { - return NodeType.ModuleName; + return NodeType.Module; } - public getName(): string { - const text = this.getText(); - return text.substr(0, text.length - 1); + public setIdentifier(node: Identifier | null): node is Identifier { + return this.setNode('identifier', node, 0); + } + + public getIdentifier(): Identifier | undefined { + return this.identifier; } } diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 05dee49f..324158af 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -118,16 +118,24 @@ export class SCSSParser extends cssParser.Parser { return node; } - public _parseModuleVariable(): nodes.Module | null { - if (!this.peek(scssScanner.Module)) { + public _parseModuleMember(): nodes.Module | null { + + const pos = this.mark(); + const node = this.create(nodes.Module); + + if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Module]))) { return null; } - const moduleNode = this.create(nodes.Module); - this.consumeToken(); - moduleNode.addChild(this._parseVariable()); + if (this.hasWhitespace() + || !this.acceptDelim('.') + || this.hasWhitespace() + || !node.addChild(this._parseVariable() || this._parseFunction())) { + this.restoreAtMark(pos); + return null; + } - return moduleNode; + return node; } public _parseIdent(referenceTypes?: nodes.ReferenceType[]): nodes.Identifier | null { @@ -164,12 +172,15 @@ export class SCSSParser extends cssParser.Parser { } public _parseTerm(): nodes.Term | null { - let term = super._parseTerm(); - if (term) { return term; } + let term = this.create(nodes.Term); + if (term.setExpression(this._parseModuleMember())) { + return this.finish(term); + } + + const superTerm = super._parseTerm(); + if (superTerm) { return superTerm; } - term = this.create(nodes.Term); if (term.setExpression(this._parseVariable()) - || term.setExpression(this._parseModuleVariable()) || term.setExpression(this._parseSelectorCombinator()) || term.setExpression(this._tryParsePrio())) { return this.finish(term); diff --git a/src/parser/scssScanner.ts b/src/parser/scssScanner.ts index 0cdc025b..8fe50278 100644 --- a/src/parser/scssScanner.ts +++ b/src/parser/scssScanner.ts @@ -46,15 +46,6 @@ export class SCSSScanner extends Scanner { } } - // scss module variable access - const moduleVariable: string[] = []; - if (this.ident(moduleVariable) && this.stream.advanceIfChars([_DOT, _DLR])) { - this.stream.goBack(1); - return this.finishToken(offset, Module, moduleVariable.concat('.').join('')); - } else { - this.stream.goBackTo(offset); - } - // scss: interpolation function #{..}) if (this.stream.advanceIfChars([_HSH, _CUL])) { return this.finishToken(offset, InterpolationFunction); diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index c83f5f94..3a550069 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -29,9 +29,10 @@ suite('SCSS - Parser', () => { test('Module variable', function () { let parser = new SCSSParser(); - assertNode('module.$color', parser, parser._parseModuleVariable.bind(parser)); - assertNode('module.$co42lor', parser, parser._parseModuleVariable.bind(parser)); - assertNode('module.$-co42lor', parser, parser._parseModuleVariable.bind(parser)); + assertNode('module.$color', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.$co42lor', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.$-co42lor', parser, parser._parseModuleMember.bind(parser)); + assertNode('module.function()', parser, parser._parseModuleMember.bind(parser)); }); test('VariableDeclaration', function () { @@ -206,6 +207,36 @@ suite('SCSS - Parser', () => { assertNode('foo: if(true, !important, null)', parser, parser._parseDeclaration.bind(parser)); assertNode('color: selector-replace(&, 1)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.$color', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / module.$let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: (20 / 20 + module.$let)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.func($red)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.func($red) !important', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate($red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: desaturate(module.$red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate(module.$red, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('dummy: module.desaturate(16, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: module.$base-color + #111', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: 100% / 2 + module.$ref', parser, parser._parseDeclaration.bind(parser)); + assertNode('border: (module.$width * 2) solid black', parser, parser._parseDeclaration.bind(parser)); + assertNode('property: module.$class', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: module.fnc($t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: fnc(module.$t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('prop-erty: module.fnc(module.$t, 10%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('width: (1em + 2em) * 3', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: #010203 + #040506', parser, parser._parseDeclaration.bind(parser)); + assertNode('font-family: sans- + "serif"', parser, parser._parseDeclaration.bind(parser)); + assertNode('margin: 3px + 4px auto', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: color.hsl(0, 100%, 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: color.hsl($hue: 0, $saturation: 100%, $lightness: 50%)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', module.flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', module.flex-gutter(), $value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if($value == \'default\', module.flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('foo: if(module.$value == \'default\', module.flex-gutter(), module.$value)', parser, parser._parseDeclaration.bind(parser)); + assertNode('color: selector.replace(&, 1)', parser, parser._parseDeclaration.bind(parser)); + assertError('fo = 8', parser, parser._parseDeclaration.bind(parser), ParseError.ColonExpected); assertError('fo:', parser, parser._parseDeclaration.bind(parser), ParseError.PropertyValueExpected); assertError('color: hsl($hue: 0,', parser, parser._parseDeclaration.bind(parser), ParseError.ExpressionExpected); From f1a6caa5cb7fcc2ba59d7c6cb6dd77cfae24e8d2 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 10:12:34 +0100 Subject: [PATCH 16/34] Parse module members in media queries --- src/parser/scssParser.ts | 5 ++++- src/test/scss/parser.test.ts | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 324158af..d4717adf 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -99,7 +99,10 @@ export class SCSSParser extends cssParser.Parser { } public _parseMediaFeatureName(): nodes.Node | null { - return this._parseFunction() || this._parseIdent() || this._parseVariable(); // first function, the indent + return this._parseModuleMember() + || this._parseFunction() // function before ident + || this._parseIdent() + || this._parseVariable(); } public _parseKeyframeSelector(): nodes.Node | null { diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 3a550069..97550306 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -319,6 +319,26 @@ suite('SCSS - Parser', () => { assertNode('.something { @media (max-width: 760px) { ~ div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.something { @media (max-width: 760px) { + div { display: block; } } }', parser, parser._parseStylesheet.bind(parser)); assertNode('@media (max-width: 760px) { + div { display: block; } }', parser, parser._parseStylesheet.bind(parser)); + + assertNode('@media #{layout.$media} and ($feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and (layout.$feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and ($feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{layout.$media} and (layout.$feature: $value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{$media} and (layout.$feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media #{layout.$media} and (layout.$feature: layout.$value) {}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth($query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth($query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): list.nth($query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (nth(list.$query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); + assertNode('@media screen and (list.nth(list.$query, 1): list.nth(list.$query, 2)) { }', parser, parser._parseMedia.bind(parser)); }); test('@keyframe', function () { From f14895f0e3fb46c0e05cc1c24b5213d7c5319aa5 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 10:33:46 +0100 Subject: [PATCH 17/34] Add tests for module syntax in @keyframe --- src/test/scss/parser.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 97550306..d04d362d 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -346,6 +346,8 @@ suite('SCSS - Parser', () => { assertNode('@keyframes name { @content; }', parser, parser._parseKeyframe.bind(parser)); assertNode('@keyframes name { @for $i from 0 through $steps { #{$i * (100%/$steps)} { transform: $rotate $translate; } } }', parser, parser._parseKeyframe.bind(parser)); // issue 42086 assertNode('@keyframes test-keyframe { @for $i from 1 through 60 { $s: ($i * 100) / 60 + "%"; } }', parser, parser._parseKeyframe.bind(parser)); + + assertNode('@keyframes name { @for $i from 0 through m.$steps { #{$i * (100%/$steps)} { transform: $rotate $translate; } } }', parser, parser._parseKeyframe.bind(parser)); }); test('@extend', function () { From 5c2baaf5efc644c87e3cd478c40e055da5a97d55 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 10:36:20 +0100 Subject: [PATCH 18/34] Add tests for module syntax in @if --- src/test/scss/parser.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index d04d362d..a1b93988 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -259,6 +259,9 @@ suite('SCSS - Parser', () => { assertNode('@include keyframe { 10% { top: 3px; } }', parser, parser._parseStylesheet.bind(parser)); assertNode('.class{&--sub-class-with-ampersand{color: red;}}', parser, parser._parseStylesheet.bind(parser)); assertError('fo { font: 2px/3px { family } }', parser, parser._parseStylesheet.bind(parser), ParseError.ColonExpected); + + assertNode('legend {foo{a:s}margin-top:0;margin-bottom:#123;margin-top:m.s(1)}', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include module.keyframe { 10% { top: 3px; } }', parser, parser._parseStylesheet.bind(parser)); }); test('@import', function () { @@ -386,6 +389,15 @@ suite('SCSS - Parser', () => { assertNode('@if $i == 1 { p { x: 3px; } }', parser, parser._parseStylesheet.bind(parser)); assertError('@if { border: 1px solid; }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertError('@if 1 }', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.LeftCurlyExpected); + + assertNode('@if 1 <= m.$let { border: 3px; } @else { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@if 1 >= (1 + m.$foo) { border: 3px; } @else if 1 + 1 == 2 { border: 4px; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('p { @if m.$i == 1 { x: 3px; } @else if $i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @if $i == 1 { x: 3px; } @else if m.$i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @if m.$i == 1 { x: 3px; } @else if m.$i == 1 { x: 4px; } @else { x: 4px; } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (list.index($_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (index(m.$_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@if (list.index(m.$_RESOURCES, "clean") != null) { @error "sdssd"; }', parser, parser._parseStylesheet.bind(parser)); }); test('@for', function () { From 9b1d731cf4fd3587356890258f988ae8f55dca1f Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 10:37:27 +0100 Subject: [PATCH 19/34] Add tests for module syntax in @for --- src/test/scss/parser.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index a1b93988..7882ec4c 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -410,6 +410,10 @@ suite('SCSS - Parser', () => { assertError('@for $i from {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertError('@for $i from 0 to {}', parser, parser._parseRuleSetDeclaration.bind(parser), ParseError.ExpressionExpected); assertNode('@for $i from 1 through 60 { $s: $i + "%"; }', parser, parser._parseRuleSetDeclaration.bind(parser)); + + assertNode('@for $k from 1 + m.$x through 5 + $x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@for $k from 1 + $x through 5 + m.$x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); + assertNode('@for $k from 1 + m.$x through 5 + m.$x { }', parser, parser._parseRuleSetDeclaration.bind(parser)); }); test('@each', function () { From 57921a755778c73c044ef37756a0f1295eac5bdc Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:02:38 +0100 Subject: [PATCH 20/34] Support module syntax in @include --- src/parser/scssParser.ts | 22 +++++++++++++++++++++- src/test/scss/parser.test.ts | 15 +++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index d4717adf..cb52289e 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -590,10 +590,30 @@ export class SCSSParser extends cssParser.Parser { const node = this.create(nodes.MixinReference); this.consumeToken(); - if (!node.setIdentifier(this._parseIdent([nodes.ReferenceType.Mixin]))) { + // Could be module or mixin identifier, set as mixin as default. + const firstIdent = this._parseIdent([nodes.ReferenceType.Mixin]); + if (!node.setIdentifier(firstIdent)) { + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } + + // Is a module accessor. + if (!this.hasWhitespace() && this.acceptDelim('.') && !this.hasWhitespace()) { + const secondIdent = this._parseIdent([nodes.ReferenceType.Mixin]); + + if (!secondIdent) { return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); } + const moduleToken = this.create(nodes.Module); + // Re-purpose first matched ident as identifier for module token. + firstIdent.referenceTypes = [nodes.ReferenceType.Module]; + moduleToken.setIdentifier(firstIdent); + + // Override identifier with second ident. + node.setIdentifier(secondIdent); + node.addChild(moduleToken); + } + if (this.accept(TokenType.ParenthesisL)) { if (node.getArguments().addChild(this._parseFunctionArgument())) { while (this.accept(TokenType.Comma)) { diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 7882ec4c..ca3f1a8b 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -471,6 +471,21 @@ suite('SCSS - Parser', () => { assertError('p { @include foo($values }', parser, parser._parseStylesheet.bind(parser), ParseError.RightParenthesisExpected); assertError('p { @include foo($values, }', parser, parser._parseStylesheet.bind(parser), ParseError.ExpressionExpected); + assertNode('p { @include lib.sexy-border(blue); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.shadows { @include lib.box-shadow(0px 4px 5px #666, 2px 6px 10px #999); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('$values: #ff0000, #00ff00, #0000ff; .primary { @include lib.colors($values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.primary { @include colors(lib.$values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('.primary { @include lib.colors(lib.$values...); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include lib.colors(this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include colors(lib.this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('@include lib.colors(lib.this("styles")...);', parser, parser._parseStylesheet.bind(parser)); + assertNode('.test { @include lib.fontsize(16px, 21px !important); }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.apply-to-ie6-only { #logo { background-image: url(/logo.gif); } } }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.foo($values,) }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include foo(lib.$values,) }', parser, parser._parseStylesheet.bind(parser)); + assertNode('p { @include lib.foo(m.$values,); }', parser, parser._parseStylesheet.bind(parser)); + + assertError('p { @include foo.($values) }', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected); }); test('@function', function () { From 8b1e6a73c36cef5ddf51701c292004bf9f79e8b4 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:04:13 +0100 Subject: [PATCH 21/34] Display error for missing module member access --- src/parser/scssParser.ts | 11 +++++++---- src/test/scss/parser.test.ts | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index cb52289e..99ec3930 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -132,12 +132,15 @@ export class SCSSParser extends cssParser.Parser { if (this.hasWhitespace() || !this.acceptDelim('.') - || this.hasWhitespace() - || !node.addChild(this._parseVariable() || this._parseFunction())) { + || this.hasWhitespace()) { this.restoreAtMark(pos); return null; } + if (!node.addChild(this._parseVariable() || this._parseFunction())) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); + } + return node; } @@ -601,8 +604,8 @@ export class SCSSParser extends cssParser.Parser { const secondIdent = this._parseIdent([nodes.ReferenceType.Mixin]); if (!secondIdent) { - return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); - } + return this.finish(node, ParseError.IdentifierExpected, [TokenType.CurlyR]); + } const moduleToken = this.create(nodes.Module); // Re-purpose first matched ident as identifier for module token. diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index ca3f1a8b..63022b0f 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -33,6 +33,8 @@ suite('SCSS - Parser', () => { assertNode('module.$co42lor', parser, parser._parseModuleMember.bind(parser)); assertNode('module.$-co42lor', parser, parser._parseModuleMember.bind(parser)); assertNode('module.function()', parser, parser._parseModuleMember.bind(parser)); + + assertError('module.', parser, parser._parseModuleMember.bind(parser), ParseError.IdentifierOrVariableExpected); }); test('VariableDeclaration', function () { From c125abdb673588f928d499703ea78eada31e3816 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:07:57 +0100 Subject: [PATCH 22/34] Modify ruleset tests with module variables --- src/test/scss/parser.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 63022b0f..7ac14de1 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -511,7 +511,7 @@ suite('SCSS - Parser', () => { test('Ruleset', function () { let parser = new SCSSParser(); - assertNode('.selector { prop: erty $let 1px; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.selector { prop: erty $let 1px m.$foo; }', parser, parser._parseRuleset.bind(parser)); assertNode('selector:active { property:value; nested:hover {}}', parser, parser._parseRuleset.bind(parser)); assertNode('selector {}', parser, parser._parseRuleset.bind(parser)); assertNode('selector { property: declaration }', parser, parser._parseRuleset.bind(parser)); @@ -525,7 +525,7 @@ suite('SCSS - Parser', () => { test('Nested Ruleset', function () { let parser = new SCSSParser(); - assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; } one: $let; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; four: m.$foo } one: $let; }', parser, parser._parseRuleset.bind(parser)); assertNode('.class1 { > .class2 { & > .class4 { rule1: v1; } } }', parser, parser._parseRuleset.bind(parser)); assertNode('foo { @at-root { display: none; } }', parser, parser._parseRuleset.bind(parser)); assertNode('th, tr { @at-root #{selector-replace(&, "tr")} { border-bottom: 0; } }', parser, parser._parseRuleset.bind(parser)); From e873d6947b04eca943428da445a5e2818fcc38ce Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:09:40 +0100 Subject: [PATCH 23/34] Test module syntax to selector interpolation --- src/test/scss/parser.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 7ac14de1..6894c0d5 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -546,6 +546,17 @@ suite('SCSS - Parser', () => { assertNode('.foo-#{&} .foo-#{&-sub} { }', parser, parser._parseRuleset.bind(parser)); assertNode('.-#{$variable} { }', parser, parser._parseRuleset.bind(parser)); assertNode('#{&}([foo=bar][bar=foo]) { }', parser, parser._parseRuleset.bind(parser)); // #49589 + + assertNode('.#{module.$name} { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-foo { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-foo-3 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.#{module.$name}-1 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('.sc-col#{module.$postfix}-2-1 { }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{module.$name} { #{$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{$name} { #{module.$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('p.#{module.$name} { #{module.$attr}-color: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('sans-#{serif} { a-#{1 + 2}-color-#{module.$attr}: blue; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.-#{module.$variable} { }', parser, parser._parseRuleset.bind(parser)); }); test('Parent Selector', function () { From f0d32abe38e2e91601cb1d9bdfa3ef1b8db35e94 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:20:39 +0100 Subject: [PATCH 24/34] Rewrite wildcard check in '@forward as' --- src/parser/scssParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 99ec3930..71b2ef1c 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -812,7 +812,7 @@ export class SCSSParser extends cssParser.Parser { } // Wildcard must be the next character after the identifier string. - if (!this.acceptDelim('*') || this.prevToken!.offset != identifier.end) { + if (this.hasWhitespace() || !this.acceptDelim('*')) { return this.finish(node, ParseError.WildcardExpected); } } From b7f21912fc400e108edcb166af26333e280093e8 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Tue, 15 Oct 2019 22:25:33 +0100 Subject: [PATCH 25/34] Rework @forward and @use as special token types --- src/parser/cssParser.ts | 9 ++++++++- src/parser/scssParser.ts | 20 ++++++++++++++++---- src/parser/scssScanner.ts | 19 +++++++++++++++++++ src/test/scss/parser.test.ts | 10 ++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index 336fc5e6..f11889cf 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -244,7 +244,10 @@ export class Parser { public _parseStylesheet(): nodes.Stylesheet { const node = this.create(nodes.Stylesheet); - node.addChild(this._parseCharset()); + + while (node.addChild(this._parseStylesheetStart())) { + // Parse statements only valid at the beginning of stylesheets. + } let inRecovery = false; do { @@ -285,6 +288,10 @@ export class Parser { return this.finish(node); } + public _parseStylesheetStart(): nodes.Node | null { + return this._parseCharset(); + } + public _parseStylesheetStatement(isNested: boolean = false): nodes.Node | null { if (this.peek(TokenType.AtKeyword)) { return this._parseStylesheetAtStatement(isNested); diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index 71b2ef1c..f8666540 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -21,6 +21,12 @@ export class SCSSParser extends cssParser.Parser { super(new scssScanner.SCSSScanner()); } + public _parseStylesheetStart(): nodes.Node | null { + return this._parseForward() + || this._parseUse() + || super._parseStylesheetStart(); + } + public _parseStylesheetStatement(): nodes.Node | null { if (this.peek(TokenType.AtKeyword)) { return this._parseWarnAndDebug() @@ -29,8 +35,6 @@ export class SCSSParser extends cssParser.Parser { || this._parseMixinContent() || this._parseMixinReference() // @include || this._parseFunctionDeclaration() - || this._parseForward() - || this._parseUse() || super._parseStylesheetAtStatement(); } return this._parseRuleset(true) || this._parseVariableDeclaration(); @@ -721,7 +725,7 @@ export class SCSSParser extends cssParser.Parser { } public _parseUse(): nodes.Node | null { - if (!this.peekKeyword('@use')) { + if (!this.peek(scssScanner.Use)) { return null; } @@ -770,6 +774,10 @@ export class SCSSParser extends cssParser.Parser { } } + if (!this.accept(TokenType.SemiColon) && !this.accept(TokenType.EOF)) { + return this.finish(node, ParseError.SemiColonExpected); + } + return this.finish(node); } @@ -789,7 +797,7 @@ export class SCSSParser extends cssParser.Parser { } public _parseForward(): nodes.Node | null { - if (!this.peekKeyword('@forward')) { + if (!this.peek(scssScanner.Forward)) { return null; } @@ -828,6 +836,10 @@ export class SCSSParser extends cssParser.Parser { } } + if (!this.accept(TokenType.SemiColon) && !this.accept(TokenType.EOF)) { + return this.finish(node, ParseError.SemiColonExpected); + } + return this.finish(node); } diff --git a/src/parser/scssScanner.ts b/src/parser/scssScanner.ts index 8fe50278..fd624230 100644 --- a/src/parser/scssScanner.ts +++ b/src/parser/scssScanner.ts @@ -19,6 +19,7 @@ const _BNG = '!'.charCodeAt(0); const _LAN = '<'.charCodeAt(0); const _RAN = '>'.charCodeAt(0); const _DOT = '.'.charCodeAt(0); +const _ATS = '@'.charCodeAt(0); let customTokenValue = TokenType.CustomToken; @@ -31,6 +32,8 @@ export const GreaterEqualsOperator: TokenType = customTokenValue++; export const SmallerEqualsOperator: TokenType = customTokenValue++; export const Ellipsis: TokenType = customTokenValue++; export const Module: TokenType = customTokenValue++; +export const Forward: TokenType = customTokenValue++; +export const Use: TokenType = customTokenValue++; export class SCSSScanner extends Scanner { @@ -82,6 +85,22 @@ export class SCSSScanner extends Scanner { return this.finishToken(offset, Ellipsis); } + // module loaders, @forward and @use + if (this.stream.advanceIfChar(_ATS)) { + const content = ['@']; + if (this.ident(content)) { + const keywordText = content.join(''); + if (keywordText === '@forward') { + return this.finishToken(offset, Forward, keywordText); + } + else if (keywordText === '@use') { + return this.finishToken(offset, Use, keywordText); + } + } + + this.stream.goBackTo(offset); + } + return super.scanNext(offset); } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 6894c0d5..52c2ec7a 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -295,6 +295,11 @@ suite('SCSS - Parser', () => { assertError('@use "test" with ("bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); assertError('@use "test" with ($foo: 1, "bar")', parser, parser._parseUse.bind(parser), ParseError.VariableNameExpected); assertError('@use "test" with ($foo: "bar"', parser, parser._parseUse.bind(parser), ParseError.RightParenthesisExpected); + + assertNode('@forward "test"; @use "lib"', parser, parser._parseStylesheet.bind(parser)); + assertNode('@use "test"; @use "lib"', parser, parser._parseStylesheet.bind(parser)); + assertError('body { @use "test" }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('body { color: red; } @use "test"', parser, parser._parseStylesheet.bind(parser), ParseError.RuleOrSelectorExpected); }); test('@forward', function () { @@ -312,6 +317,11 @@ suite('SCSS - Parser', () => { assertError('@forward "test" as foo- *', parser, parser._parseForward.bind(parser), ParseError.WildcardExpected); assertError('@forward "test" show', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); assertError('@forward "test" hide', parser, parser._parseForward.bind(parser), ParseError.IdentifierOrVariableExpected); + + assertNode('@use "lib"; @forward "test"', parser, parser._parseStylesheet.bind(parser)); + assertNode('@forward "test"; @forward "lib"', parser, parser._parseStylesheet.bind(parser)); + assertError('body { @forward "test" }', parser, parser._parseStylesheet.bind(parser), ParseError.RightCurlyExpected); + assertError('body { color: red; } @forward "test"', parser, parser._parseStylesheet.bind(parser), ParseError.RuleOrSelectorExpected); }); test('@media', function () { From cc6e84cb8508f051d649f750389c2fa1ee665747 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Wed, 16 Oct 2019 21:41:13 +0100 Subject: [PATCH 26/34] Fix @forward visibility parsing --- src/parser/cssNodes.ts | 11 +++++------ src/parser/scssParser.ts | 26 ++++++++++---------------- src/test/scss/parser.test.ts | 4 ++++ 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/parser/cssNodes.ts b/src/parser/cssNodes.ts index 5338f05f..49b6357e 100644 --- a/src/parser/cssNodes.ts +++ b/src/parser/cssNodes.ts @@ -1091,19 +1091,18 @@ export class Forward extends Node { export class ForwardVisibility extends Node { - public visibility?: string; + public identifier?: Node; public get type(): NodeType { return NodeType.ForwardVisibility; } - public setVisibility(visibility: string): boolean { - this.visibility = visibility; - return true; + public setIdentifier(node: Node | null): node is Node { + return this.setNode('identifier', node, 0); } - public getVisibility(): string | undefined { - return this.visibility; + public getIdentifier(): Node | undefined { + return this.identifier; } } diff --git a/src/parser/scssParser.ts b/src/parser/scssParser.ts index f8666540..f11568ce 100644 --- a/src/parser/scssParser.ts +++ b/src/parser/scssParser.ts @@ -825,13 +825,9 @@ export class SCSSParser extends cssParser.Parser { } } - for (const visibility of ['hide', 'show']) { - if (this.acceptIdent(visibility)) { - if (!node.addChild(this._parseForwardVisibility(visibility))) { - return this.finish(node, ParseError.IdentifierOrVariableExpected); - } - // Only can have one of 'hide' or 'show'. - break; + if (this.peekIdent('hide') || this.peekIdent('show')) { + if (!node.addChild(this._parseForwardVisibility())) { + return this.finish(node, ParseError.IdentifierOrVariableExpected); } } } @@ -843,20 +839,18 @@ export class SCSSParser extends cssParser.Parser { return this.finish(node); } - public _parseForwardVisibility(visibility: string): nodes.Node | null { + public _parseForwardVisibility(): nodes.Node | null { const node = this.create(nodes.ForwardVisibility); - node.setVisibility(visibility); - this.consumeToken(); + // Assume to be "hide" or "show". + node.setIdentifier(this._parseIdent()); - const children: nodes.Node[] = []; - let child: nodes.Node | null; - while (child = this._parseVariable() || this._parseIdent()) { - children.push(child); + while (node.addChild(this._parseVariable() || this._parseIdent())) { + // Consume all variables and idents ahead. } - node.addChildren(children); - return children.length ? node : null; + // More than just identifier + return node.getChildren().length > 1 ? node : null; } } diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 52c2ec7a..95ea04f2 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -306,7 +306,11 @@ suite('SCSS - Parser', () => { let parser = new SCSSParser(); assertNode('@forward "test"', parser, parser._parseForward.bind(parser)); assertNode('@forward "test" as foo-*', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide this', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" hide $that', parser, parser._parseForward.bind(parser)); assertNode('@forward "test" hide this $that', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show this', parser, parser._parseForward.bind(parser)); + assertNode('@forward "test" show $that', parser, parser._parseForward.bind(parser)); assertNode('@forward "test" show this $that', parser, parser._parseForward.bind(parser)); assertNode('@forward "test" as foo-* show this $that', parser, parser._parseForward.bind(parser)); From d1128bd1d746db9c2a7fdfa8e2fc8aa132e1edb1 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Mon, 14 Oct 2019 21:54:05 +0100 Subject: [PATCH 27/34] Add completion definitions for @use and @forward --- src/services/scssCompletion.ts | 10 ++++++++++ src/test/scss/scssCompletion.test.ts | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index 0160aace..33af1f67 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -189,6 +189,16 @@ export class SCSSCompletion extends CSSCompletion { documentation: localize("scss.builtin.@include", "Includes the styles defined by another mixin into the current rule."), kind: CompletionItemKind.Keyword }, + { + label: "@use", + documentation: localize("scss.builtin.@use", "Loads mixins, functions, and variables from other Sass stylesheets as 'modules', and combines CSS from multiple stylesheets together."), + kind: CompletionItemKind.Keyword + }, + { + label: "@forward", + documentation: localize("scss.builtin.@forward", "Loads a Sass stylesheet and makes its mixins, functions, and variables available when this stylesheet is loaded with the @use rule."), + kind: CompletionItemKind.Keyword + }, ]; diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index 57bb6d66..e8ed0196 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -163,7 +163,9 @@ suite('SCSS - Completions', () => { { label: '@each', insertTextFormat: InsertTextFormat.Snippet }, { label: '@while', insertTextFormat: InsertTextFormat.Snippet }, { label: '@mixin', insertTextFormat: InsertTextFormat.Snippet }, - { label: '@include' } + { label: '@include' }, + { label: '@use' }, + { label: '@forward' } ] }); From 004c86ec9f678dd8cf482be8fd61032eac381aa0 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Tue, 15 Oct 2019 17:46:16 +0100 Subject: [PATCH 28/34] Refactor module-related tests to new suite --- src/test/scss/scssCompletion.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index e8ed0196..9bb74f50 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -164,8 +164,6 @@ suite('SCSS - Completions', () => { { label: '@while', insertTextFormat: InsertTextFormat.Snippet }, { label: '@mixin', insertTextFormat: InsertTextFormat.Snippet }, { label: '@include' }, - { label: '@use' }, - { label: '@forward' } ] }); @@ -181,4 +179,15 @@ suite('SCSS - Completions', () => { ] }); }); + + suite('Modules', function (): any { + test('module-loading at-rules', function (): any { + testCompletionFor('@', { + items: [ + { label: '@use' }, + { label: '@forward' }, + ], + }); + }); + }); }); From a810af7d26b38e79ff092da7939d245655a6d394 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Tue, 15 Oct 2019 17:48:46 +0100 Subject: [PATCH 29/34] Limit module-loading at-rules to top-level scope --- src/services/scssCompletion.ts | 9 +++++++++ src/test/scss/scssCompletion.test.ts | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index 33af1f67..94e62032 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -189,6 +189,9 @@ export class SCSSCompletion extends CSSCompletion { documentation: localize("scss.builtin.@include", "Includes the styles defined by another mixin into the current rule."), kind: CompletionItemKind.Keyword }, + ]; + + private static scssModuleLoaders = [ { label: "@use", documentation: localize("scss.builtin.@use", "Loads mixins, functions, and variables from other Sass stylesheets as 'modules', and combines CSS from multiple stylesheets together."), @@ -278,7 +281,13 @@ export class SCSSCompletion extends CSSCompletion { public getCompletionForTopLevel(result: CompletionList): CompletionList { this.getCompletionForAtDirectives(result); + this.getCompletionForModuleLoaders(result); super.getCompletionForTopLevel(result); return result; } + + public getCompletionForModuleLoaders(result: CompletionList): CompletionList { + result.items.push(...SCSSCompletion.scssModuleLoaders); + return result; + } } diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index 9bb74f50..037dde4d 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -188,6 +188,14 @@ suite('SCSS - Completions', () => { { label: '@forward' }, ], }); + + // Limit to top-level scope. + testCompletionFor('.foo { @| }', { + items: [ + { label: '@use', notAvailable: true }, + { label: '@forward', notAvailable: true }, + ], + }); }); }); }); From 0bd11b9d9c21c86c357c83b6495c2ff89ec4fa88 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Wed, 16 Oct 2019 22:02:41 +0100 Subject: [PATCH 30/34] Add snippets for module loaders --- src/services/scssCompletion.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index 94e62032..2f9c5061 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -195,11 +195,15 @@ export class SCSSCompletion extends CSSCompletion { { label: "@use", documentation: localize("scss.builtin.@use", "Loads mixins, functions, and variables from other Sass stylesheets as 'modules', and combines CSS from multiple stylesheets together."), + insertText: "@use '$0';", + insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Keyword }, { label: "@forward", documentation: localize("scss.builtin.@forward", "Loads a Sass stylesheet and makes its mixins, functions, and variables available when this stylesheet is loaded with the @use rule."), + insertText: "@forward '$0';", + insertTextFormat: InsertTextFormat.Snippet, kind: CompletionItemKind.Keyword }, ]; From 73054940bcdb51f43df8bd1f769c8e10a814ab75 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Wed, 16 Oct 2019 22:10:39 +0100 Subject: [PATCH 31/34] Add path completion for @forward & @use --- src/services/cssCompletion.ts | 16 +++++++---- src/services/scssCompletion.ts | 6 ++++ src/test/scss/scssCompletion.test.ts | 42 +++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/services/cssCompletion.ts b/src/services/cssCompletion.ts index 9312c5f6..77d64fa4 100644 --- a/src/services/cssCompletion.ts +++ b/src/services/cssCompletion.ts @@ -107,7 +107,7 @@ export class CSSCompletion { this.getCompletionForUriLiteralValue(node, result); } else if (node.parent === null) { this.getCompletionForTopLevel(result); - } else if (node.type === nodes.NodeType.StringLiteral && node.parent.type === nodes.NodeType.Import) { + } else if (node.type === nodes.NodeType.StringLiteral && this.isImportPathParent(node.parent.type)) { this.getCompletionForImportPath(node, result); // } else if (node instanceof nodes.Variable) { // this.getCompletionsForVariableDeclaration() @@ -138,6 +138,10 @@ export class CSSCompletion { } } + protected isImportPathParent(type: nodes.NodeType): boolean { + return type === nodes.NodeType.Import; + } + private finalize(result: CompletionList): CompletionList { const needsSortText = result.items.some(i => !!i.sortText || i.label[0] === '-'); if (needsSortText) { @@ -192,7 +196,7 @@ export class CSSCompletion { if (completePropertyWithSemicolon && this.offset >= this.textDocument.offsetAt(range.end)) { insertText += '$0;'; } - + const item = { label: entry.name, documentation: languageFacts.getEntryDescription(entry, this.doesSupportMarkdown()), @@ -237,7 +241,7 @@ export class CSSCompletion { } return this.settings.completion.triggerPropertyValueCompletion; } - + private get isCompletePropertyWithSemicolonEnabled(): boolean { if ( !this.settings || @@ -385,7 +389,7 @@ export class CSSCompletion { kind: CompletionItemKind.Variable, sortText: 'z' }; - + if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) { completionItem.kind = CompletionItemKind.Color; } @@ -414,7 +418,7 @@ export class CSSCompletion { textEdit: TextEdit.replace(this.getCompletionRange(null), symbol.name), kind: CompletionItemKind.Variable }; - + if (typeof completionItem.documentation === 'string' && isColorString(completionItem.documentation)) { completionItem.kind = CompletionItemKind.Color; } @@ -972,7 +976,7 @@ function isDeprecated(entry: languageFacts.IEntry2): boolean { if (entry.status && (entry.status === 'nonstandard' || entry.status === 'obsolete')) { return true; } - + return false; } diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index 2f9c5061..95c61a3f 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -213,6 +213,12 @@ export class SCSSCompletion extends CSSCompletion { super('$', clientCapabilities); } + protected isImportPathParent(type: nodes.NodeType): boolean { + return type === nodes.NodeType.Forward + || type === nodes.NodeType.Use + || super.isImportPathParent(type); + } + private createReplaceFunction() { let tabStopCounter = 1; return (_match: string, p1: string) => { diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index 037dde4d..2c9ebe12 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -9,15 +9,36 @@ import * as cssLanguageService from '../../cssLanguageService'; import { TextDocument, Position, InsertTextFormat } from 'vscode-languageserver-types'; import { assertCompletion, ItemDescription } from '../css/completion.test'; +import { ImportPathCompletionContext } from '../../cssLanguageTypes'; +import { newRange } from '../css/navigation.test'; suite('SCSS - Completions', () => { - let testCompletionFor = function (value: string, expected: { count?: number, items?: ItemDescription[] }) { + let testCompletionFor = function ( + value: string, + expected: { + count?: number, + items?: ItemDescription[], + participant?: { + onImportPath?: ImportPathCompletionContext[], + }, + }, + ) { let offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); + let actualImportPathContexts: ImportPathCompletionContext[] = []; + let ls = cssLanguageService.getSCSSLanguageService(); + if (expected.participant) { + ls.setCompletionParticipants([ + { + onCssImportPath: context => actualImportPathContexts.push(context) + } + ]); + } + let document = TextDocument.create('test://test/test.scss', 'scss', 0, value); let position = Position.create(0, offset); let jsonDoc = ls.parseStylesheet(document); @@ -31,6 +52,11 @@ suite('SCSS - Completions', () => { assertCompletion(list, item, document, offset); } } + if (expected.participant) { + if (expected.participant.onImportPath) { + assert.deepEqual(actualImportPathContexts, expected.participant.onImportPath); + } + } }; test('stylesheet', function (): any { @@ -196,6 +222,20 @@ suite('SCSS - Completions', () => { { label: '@forward', notAvailable: true }, ], }); + + testCompletionFor(`@use './|'`, { + count: 0, + participant: { + onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 8), range: newRange(5, 9) }] + } + }); + + testCompletionFor(`@forward './|'`, { + count: 0, + participant: { + onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 12), range: newRange(9, 13) }] + } + }); }); }); }); From a9859012287cc1be8ca220d63d4a8f2c2c70f380 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:17:38 +0100 Subject: [PATCH 32/34] Add completion for sass module built ins --- src/services/scssCompletion.ts | 48 ++++++++++++++++++++++++++++ src/test/scss/scssCompletion.test.ts | 16 ++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/services/scssCompletion.ts b/src/services/scssCompletion.ts index 95c61a3f..bf7cef0f 100644 --- a/src/services/scssCompletion.ts +++ b/src/services/scssCompletion.ts @@ -208,6 +208,44 @@ export class SCSSCompletion extends CSSCompletion { }, ]; + private static scssModuleBuiltIns = [ + { + label: 'sass:math', + documentation: localize('scss.builtin.sass:math', 'Provides functions that operate on numbers.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:string', + documentation: localize('scss.builtin.sass:string', 'Makes it easy to combine, search, or split apart strings.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:color', + documentation: localize('scss.builtin.sass:color', 'Generates new colors based on existing ones, making it easy to build color themes.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:list', + documentation: localize('scss.builtin.sass:list', 'Lets you access and modify values in lists.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:map', + documentation: localize('scss.builtin.sass:map', 'Makes it possible to look up the value associated with a key in a map, and much more.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:selector', + documentation: localize('scss.builtin.sass:selector', 'Provides access to Sass’s powerful selector engine.'), + kind: CompletionItemKind.Text, + }, + { + label: 'sass:meta', + documentation: localize('scss.builtin.sass:meta', 'Exposes the details of Sass’s inner workings.'), + kind: CompletionItemKind.Text, + }, + ]; + constructor(clientCapabilities: ClientCapabilities | undefined) { super('$', clientCapabilities); @@ -219,6 +257,16 @@ export class SCSSCompletion extends CSSCompletion { || super.isImportPathParent(type); } + public getCompletionForImportPath(importPathNode: nodes.Node, result: CompletionList): CompletionList { + const parentType = importPathNode.getParent()!.type; + + if (parentType === nodes.NodeType.Forward || parentType === nodes.NodeType.Use) { + result.items.push(...SCSSCompletion.scssModuleBuiltIns); + } + + return super.getCompletionForImportPath(importPathNode, result); + } + private createReplaceFunction() { let tabStopCounter = 1; return (_match: string, p1: string) => { diff --git a/src/test/scss/scssCompletion.test.ts b/src/test/scss/scssCompletion.test.ts index 2c9ebe12..25b55ee5 100644 --- a/src/test/scss/scssCompletion.test.ts +++ b/src/test/scss/scssCompletion.test.ts @@ -223,15 +223,27 @@ suite('SCSS - Completions', () => { ], }); + const builtIns = { + items: [ + { label: 'sass:math' }, + { label: 'sass:string' }, + { label: 'sass:color' }, + { label: 'sass:list' }, + { label: 'sass:map' }, + { label: 'sass:selector' }, + { label: 'sass:meta' }, + ], + }; + testCompletionFor(`@use '|'`, builtIns); + testCompletionFor(`@forward '|'`, builtIns); + testCompletionFor(`@use './|'`, { - count: 0, participant: { onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 8), range: newRange(5, 9) }] } }); testCompletionFor(`@forward './|'`, { - count: 0, participant: { onImportPath: [{ pathValue: `'./'`, position: Position.create(0, 12), range: newRange(9, 13) }] } From 599fa99b9e0937ef952cf2b5380c6cc3b3195acf Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Tue, 26 Nov 2019 20:36:45 +0000 Subject: [PATCH 33/34] Keep module syntax tests orthagonal --- src/test/scss/parser.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/scss/parser.test.ts b/src/test/scss/parser.test.ts index 95ea04f2..a2b76c38 100644 --- a/src/test/scss/parser.test.ts +++ b/src/test/scss/parser.test.ts @@ -525,6 +525,7 @@ suite('SCSS - Parser', () => { test('Ruleset', function () { let parser = new SCSSParser(); + assertNode('.selector { prop: erty $let 1px; }', parser, parser._parseRuleset.bind(parser)); assertNode('.selector { prop: erty $let 1px m.$foo; }', parser, parser._parseRuleset.bind(parser)); assertNode('selector:active { property:value; nested:hover {}}', parser, parser._parseRuleset.bind(parser)); assertNode('selector {}', parser, parser._parseRuleset.bind(parser)); @@ -539,7 +540,8 @@ suite('SCSS - Parser', () => { test('Nested Ruleset', function () { let parser = new SCSSParser(); - assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; four: m.$foo } one: $let; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.class1 { $let: 1; .class { $let: 2; three: $let; let: 3; } one: $let; }', parser, parser._parseRuleset.bind(parser)); + assertNode('.class1 { $let: 1; .class { $let: m.$foo; } one: $let; }', parser, parser._parseRuleset.bind(parser)); assertNode('.class1 { > .class2 { & > .class4 { rule1: v1; } } }', parser, parser._parseRuleset.bind(parser)); assertNode('foo { @at-root { display: none; } }', parser, parser._parseRuleset.bind(parser)); assertNode('th, tr { @at-root #{selector-replace(&, "tr")} { border-bottom: 0; } }', parser, parser._parseRuleset.bind(parser)); From 54c9ef6351727c7c71077b51cb011275abba91f8 Mon Sep 17 00:00:00 2001 From: wongjn <11310624+wongjn@users.noreply.github.com> Date: Tue, 26 Nov 2019 22:01:11 +0000 Subject: [PATCH 34/34] Add document links for @forward and @use --- src/services/cssNavigation.ts | 6 +++++- src/services/scssNavigation.ts | 6 ++++++ src/test/scss/scssNavigation.test.ts | 17 ++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/services/cssNavigation.ts b/src/services/cssNavigation.ts index de341643..6e06b44d 100644 --- a/src/services/cssNavigation.ts +++ b/src/services/cssNavigation.ts @@ -86,6 +86,10 @@ export class CSSNavigation { return result; } + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return node.type === nodes.NodeType.Import; + } + public findDocumentLinks(document: TextDocument, stylesheet: nodes.Stylesheet, documentContext: DocumentContext): DocumentLink[] { const result: DocumentLink[] = []; @@ -102,7 +106,7 @@ export class CSSNavigation { * In @import, it is possible to include links that do not use `url()` * For example, `@import 'foo.css';` */ - if (candidate.parent && candidate.parent.type === nodes.NodeType.Import) { + if (candidate.parent && this.isRawStringDocumentLinkNode(candidate.parent)) { const rawText = candidate.getText(); if (startsWith(rawText, `'`) || startsWith(rawText, `"`)) { const link = uriStringNodeToDocumentLink(document, candidate, documentContext); diff --git a/src/services/scssNavigation.ts b/src/services/scssNavigation.ts index 1f7b2870..b474b59e 100644 --- a/src/services/scssNavigation.ts +++ b/src/services/scssNavigation.ts @@ -15,6 +15,12 @@ export class SCSSNavigation extends CSSNavigation { super(); } + protected isRawStringDocumentLinkNode(node: nodes.Node): boolean { + return super.isRawStringDocumentLinkNode(node) + || node.type === nodes.NodeType.Use + || node.type === nodes.NodeType.Forward; + } + public findDocumentLinks( document: TextDocument, stylesheet: nodes.Stylesheet, diff --git a/src/test/scss/scssNavigation.test.ts b/src/test/scss/scssNavigation.test.ts index f69923b4..d1688299 100644 --- a/src/test/scss/scssNavigation.test.ts +++ b/src/test/scss/scssNavigation.test.ts @@ -210,7 +210,22 @@ suite('SCSS - Navigation', () => { ]); }); - + + test('SCSS module file links', async () => { + const fixtureRoot = path.resolve(__dirname, '../../../../src/test/scss/linkFixture/non-existent'); + const getDocumentUri = (relativePath: string) => { + return URI.file(path.resolve(fixtureRoot, relativePath)).toString(); + }; + + await assertDynamicLinks(getDocumentUri('./index.scss'), `@use './foo' as f`, [ + { range: newRange(5, 12), target: getDocumentUri('./foo') } + ]); + + await assertDynamicLinks(getDocumentUri('./index.scss'), `@forward './foo' hide $private`, [ + { range: newRange(9, 16), target: getDocumentUri('./foo') } + ]); + }); + test('SCSS empty path', async () => { const p = new SCSSParser();