Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/parser/cssNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export enum NodeType {
MixinContentReference,
MixinContentDeclaration,
Media,
Scope,
Keyframe,
FontFace,
Import,
Expand Down Expand Up @@ -1166,6 +1167,58 @@ export class Media extends BodyDeclaration {
}
}

export class Scope extends BodyDeclaration {
constructor(offset: number, length: number) {
super(offset, length);
}

public get type(): NodeType {
return NodeType.Scope;
}
}

export class ScopeLimits extends Node {
public scopeStart?: Node;
public scopeEnd?: Node;

constructor(offset: number, length: number) {
super(offset, length);
}

public get type(): NodeType {
return NodeType.Scope;
}

public getScopeStart(): Node | undefined {
return this.scopeStart;
}

public setScopeStart(right: Node | null): right is Node {
return this.setNode('scopeStart', right);
}

public getScopeEnd(): Node | undefined {
return this.scopeEnd;
}

public setScopeEnd(right: Node | null): right is Node {
return this.setNode('scopeEnd', right);
}

public getName(): string {
let name = ''

if (this.scopeStart) {
name += this.scopeStart.getText()
}
if (this.scopeEnd) {
name += `${this.scopeStart ? ' ' : ''}→ ${this.scopeEnd.getText()}`
}

return name
}
}

export class Supports extends BodyDeclaration {

constructor(offset: number, length: number) {
Expand Down
65 changes: 65 additions & 0 deletions src/parser/cssParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export class Parser {
public _parseStylesheetAtStatement(isNested: boolean = false): nodes.Node | null {
return this._parseImport()
|| this._parseMedia(isNested)
|| this._parseScope()
|| this._parsePage()
|| this._parseFontFace()
|| this._parseKeyframe()
Expand Down Expand Up @@ -364,6 +365,7 @@ export class Parser {

protected _parseRuleSetDeclarationAtStatement(): nodes.Node | null {
return this._parseMedia(true)
|| this._parseScope()
|| this._parseSupports(true)
|| this._parseLayer(true)
|| this._parseContainer(true)
Expand Down Expand Up @@ -398,6 +400,7 @@ export class Parser {
case nodes.NodeType.MixinDeclaration:
case nodes.NodeType.FunctionDeclaration:
case nodes.NodeType.MixinContentDeclaration:
case nodes.NodeType.Scope:
return false;
case nodes.NodeType.ExtendsReference:
case nodes.NodeType.MixinContentReference:
Expand Down Expand Up @@ -1246,6 +1249,68 @@ export class Parser {
return this._parseRatio() || this._parseTermExpression();
}

public _parseScope(): nodes.Node | null {
// @scope [<scope-limits>]? { <block-contents> }
if (!this.peekKeyword('@scope')) {
return null;
}

const node = this.create(nodes.Scope);
// @scope
this.consumeToken();

node.addChild(this._parseScopeLimits())

return this._parseBody(node, this._parseScopeDeclaration.bind(this));
}

public _parseScopeDeclaration(): nodes.Node | null {
// Treat as nested as regular declarations are implicity wrapped with :where(:scope)
// https://github.com/w3c/csswg-drafts/issues/10389
// pseudo-selectors implicitly target :scope
// https://drafts.csswg.org/css-cascade-6/#scoped-rules
const isNested = true
return this._tryParseRuleset(isNested)
|| this._tryToParseDeclaration()
|| this._parseStylesheetStatement(isNested);
}

public _parseScopeLimits(): nodes.Node | null {
// [(<scope-start>)]? [to (<scope-end>)]?
const node = this.create(nodes.ScopeLimits);

// [(<scope-start>)]?
if (this.accept(TokenType.ParenthesisL)) {
// scope-start selector can start with a combinator as it defaults to :scope
// Treat as nested
if (!node.setScopeStart(this._parseSelector(true))) {
return this.finish(node, ParseError.SelectorExpected, [], [TokenType.ParenthesisR])
}

if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]);
}
}

// [to (<scope-end>)]?
if (this.acceptIdent('to')) {
if (!this.accept(TokenType.ParenthesisL)) {
return this.finish(node, ParseError.LeftParenthesisExpected, [], [TokenType.CurlyL]);
}
// 'to' selector can start with a combinator as it defaults to :scope
// Treat as nested
if (!node.setScopeEnd(this._parseSelector(true))) {
return this.finish(node, ParseError.SelectorExpected, [], [TokenType.ParenthesisR])
}

if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected, [], [TokenType.CurlyL]);
}
}

return this.finish(node)
}

public _parseMedium(): nodes.Node | null {
const node = this.create(nodes.Node);
if (node.addChild(this._parseIdent())) {
Expand Down
25 changes: 17 additions & 8 deletions src/services/cssHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,32 @@ export class CSSHover {
* Build up the hover by appending inner node's information
*/
let hover: Hover | null = null;
let flagOpts: { text: string; isMedia: boolean };
let selectorContexts: string[] = [];

for (let i = 0; i < nodepath.length; i++) {
const node = nodepath[i];

if (node instanceof nodes.Scope) {
const scopeLimits = node.getChild(0)

if (scopeLimits instanceof nodes.ScopeLimits) {
const scopeName = `${scopeLimits.getName()}`
selectorContexts.push(`@scope${scopeName ? ` ${scopeName}` : ''}`);
}
}

if (node instanceof nodes.Media) {
const regex = /@media[^\{]+/g;
const matches = node.getText().match(regex);
flagOpts = {
isMedia: true,
text: matches?.[0]!,
};
const mediaList = node.getChild(0);

if (mediaList instanceof nodes.Medialist) {
const name = '@media ' + mediaList.getText();
selectorContexts.push(name)
}
}

if (node instanceof nodes.Selector) {
hover = {
contents: this.selectorPrinting.selectorToMarkedString(<nodes.Selector>node, flagOpts!),
contents: this.selectorPrinting.selectorToMarkedString(<nodes.Selector>node, selectorContexts),
range: getRange(node),
};
break;
Expand Down
15 changes: 15 additions & 0 deletions src/services/cssNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ export class CSSNavigation {
const name = '@media ' + mediaList.getText();
collect(name, SymbolKind.Module, node, mediaList, node.getDeclarations());
}
} else if (node instanceof nodes.Scope) {
let scopeName = ''

const scopeLimits = node.getChild(0)
if (scopeLimits instanceof nodes.ScopeLimits) {
scopeName = `${scopeLimits.getName()}`
}

collect(
`@scope${scopeName ? ` ${scopeName}` : ''}`,
SymbolKind.Module,
node,
scopeLimits ?? undefined,
node.getDeclarations()
)
}
return true;
});
Expand Down
10 changes: 5 additions & 5 deletions src/services/selectorPrinting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class MarkedStringPrinter {
// empty
}

public print(element: Element, flagOpts?: { isMedia: boolean; text: string }): MarkedString[] {
public print(element: Element, selectorContexts?: string[] ): MarkedString[] {
this.result = [];
if (element instanceof RootElement) {
if (element.children) {
Expand All @@ -136,8 +136,8 @@ class MarkedStringPrinter {
this.doPrint([element], 0);
}
let value;
if (flagOpts) {
value = `${flagOpts.text}\n … ` + this.result.join('\n');
if (selectorContexts) {
value = [...selectorContexts, ...this.result].join('\n')
} else {
value = this.result.join('\n');
}
Expand Down Expand Up @@ -323,10 +323,10 @@ function unescape(content: string) {
export class SelectorPrinting {
constructor(private cssDataManager: CSSDataManager) { }

public selectorToMarkedString(node: nodes.Selector, flagOpts?: { isMedia: boolean; text: string }): MarkedString[] {
public selectorToMarkedString(node: nodes.Selector, selectorContexts?: string[] ): MarkedString[] {
const root = selectorToElement(node);
if (root) {
const markedStrings = new MarkedStringPrinter('"').print(root, flagOpts);
const markedStrings = new MarkedStringPrinter('"').print(root, selectorContexts);
markedStrings.push(this.selectorToSpecificityMarkedString(node));
return markedStrings;
} else {
Expand Down
16 changes: 16 additions & 0 deletions src/test/css/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,22 @@ suite('CSS - Completion', () => {

});

test('@scope selector completion', async function () {
await testCompletionFor(`@scope (|) {`, {
items: [
{ label: 'html', resultText: '@scope (html) {' },
{ label: ':has', resultText: '@scope (:has) {' }
]
});

await testCompletionFor(`@scope to (|) {`, {
items: [
{ label: 'html', resultText: '@scope to (html) {' },
{ label: ':has', resultText: '@scope to (:has) {' }
]
});
})

});

function newRange(start: number, end: number) {
Expand Down
38 changes: 33 additions & 5 deletions src/test/css/hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,32 +75,60 @@ suite('CSS Hover', () => {
contents: [{ language: 'html', value: '<element class="foo">' }, '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)'],
});
});
});

suite('SCSS Hover', () => {
test('nested', () => {
assertHover(
'div { d|iv {} }',
{
contents: [{ language: 'html', value: '<div>\n …\n <div>' }, '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 0, 1)'],
},
'scss',
'css',
);
assertHover(
'.foo{ .bar{ @media only screen{ .|bar{ } } } }',
{
contents: [
{
language: 'html',
value: '@media only screen\n<element class="foo">\n …\n <element class="bar">\n …\n <element class="bar">',
value: '@media only screen\n<element class="foo">\n …\n <element class="bar">\n …\n <element class="bar">',
},
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)',
],
},
'scss',
'css',
);

assertHover(
'@scope (.foo) to (.bar) { .|baz{ } }',
{
contents: [
{
language: 'html',
value: '@scope .foo → .bar\n<element class="baz">',
},
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)',
],
},
'css',
);

assertHover(
'@scope (.from) to (.to) { .foo { @media print { .bar { @media only screen{ .|bar{ } } } } } }',
{
contents: [
{
language: 'html',
value: '@scope .from → .to\n@media print\n@media only screen\n<element class="foo">\n …\n <element class="bar">\n …\n <element class="bar">',
},
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (0, 1, 0)',
],
},
'css',
);
});
});

suite('SCSS Hover', () => {
test('@at-root', () => {
assertHover(
'.test { @|at-root { }',
Expand Down
9 changes: 7 additions & 2 deletions src/test/css/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ suite('CSS - Navigation', () => {
assertScopesAndSymbols(ls, '@keyframes animation {}; .class {}', 'animation,.class,[],[]');
assertScopesAndSymbols(ls, '@page :pseudo-class { margin:2in; }', '[]');
assertScopesAndSymbols(ls, '@media print { body { font-size: 10pt } }', '[body,[]]');
assertScopesAndSymbols(ls, '@scope (.foo) to (.bar) { body { font-size: 10pt } }', '[body,[]]')
assertScopesAndSymbols(ls, '@-moz-keyframes identifier { 0% { top: 0; } 50% { top: 30px; left: 20px; }}', 'identifier,[[],[]]');
assertScopesAndSymbols(ls, '@font-face { font-family: "Bitstream Vera Serif Bold"; }', '[]');
});
Expand Down Expand Up @@ -276,6 +277,9 @@ suite('CSS - Navigation', () => {

// Media Query
assertSymbolInfos(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, location: Location.create('test://test/test.css', newRange(0, 23)) }]);

// Scope
assertSymbolInfos(ls, '@scope (.foo) to (.bar) {}', [{ name: '@scope .foo → .bar', kind: SymbolKind.Module, location: Location.create('test://test/test.css', newRange(0, 26)) }]);
});

test('basic document symbols', () => {
Expand All @@ -291,8 +295,9 @@ suite('CSS - Navigation', () => {

// Media Query
assertDocumentSymbols(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, range: newRange(0, 23), selectionRange: newRange(7, 20) }]);
assertDocumentSymbols(ls, '@media screen, print {}', [{ name: '@media screen, print', kind: SymbolKind.Module, range: newRange(0, 23), selectionRange: newRange(7, 20) }]);


// Scope
assertDocumentSymbols(ls, '@scope (.foo) to (.bar) {}', [{ name: '@scope .foo → .bar', kind: SymbolKind.Module, range: newRange(0, 26), selectionRange: newRange(7, 23) }]);
});
});

Expand Down
Loading