Move LESS to extension

This commit is contained in:
Martin Aeschlimann
2016-06-13 20:08:43 +02:00
parent 0517cce224
commit 1e7486aa63
11 changed files with 1252 additions and 8 deletions

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import {LESSParser} from '../../parser/lessParser';
import {LESSCompletion} from '../../services/lessCompletion';
import * as nodes from '../../parser/cssNodes';
import {TextDocument, Position} from 'vscode-languageserver';
import {assertCompletion, ItemDescription} from '../css/completion.test';
suite('LESS - Completions', () => {
let testCompletionFor = function (value: string, stringBefore: string, expected: { count?: number, items?: ItemDescription[] }): Thenable<void> {
let idx = stringBefore ? value.indexOf(stringBefore) + stringBefore.length : 0;
let completionProvider = new LESSCompletion();
let document = TextDocument.create('test://test/test.less', 'less', 0, value);
let position = Position.create(0, idx);
let jsonDoc = new LESSParser().parseStylesheet(document);
return completionProvider.doComplete(document, position, jsonDoc).then(list => {
if (expected.count) {
assert.equal(list.items, expected.count);
}
if (expected.items) {
for (let item of expected.items) {
assertCompletion(list, item, document);
}
}
});
};
test('sylesheet', function (testDone): any {
Promise.all([
testCompletionFor('body { ', '{ ', {
items: [
{ label: 'display' },
{ label: 'background' }
]
}),
testCompletionFor('body { ver', 'ver', {
items: [
{ label: 'vertical-align' }
]
}),
testCompletionFor('body { word-break: ', ': ', {
items: [
{ label: 'keep-all' }
]
}),
testCompletionFor('body { inner { vertical-align: }', ': ', {
items: [
{ label: 'bottom' }
]
}),
testCompletionFor('@var1: 3; body { inner { vertical-align: }', 'align: ', {
items: [
{ label: '@var1' }
]
}),
testCompletionFor('.foo { background-color: d', 'background-color: d', {
items: [
{ label: 'darken' },
{ label: 'desaturate' }
]
})
]).then(() => testDone(), (error) => testDone(error));
});
});

View File

@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {LESSParser} from '../../parser/lessParser';
import * as nodes from '../../parser/cssNodes';
import {assertScopeBuilding, assertSymbolsInScope, assertScopesAndSymbols, assertHighlights} from '../css/navigation.test';
suite('LESS - Symbols', () => {
test('scope building', function () {
let p = new LESSParser();
assertScopeBuilding(p, '@let: blue');
assertScopeBuilding(p, '.class { .nested {} }', { offset: 7, length: 14 }, { offset: 17, length: 2 });
});
test('symbols in scopes', function () {
let p = new LESSParser();
assertSymbolsInScope(p, '@let: iable;', 0, { name: '@let', type: nodes.ReferenceType.Variable });
assertSymbolsInScope(p, '@let: iable;', 11, { name: '@let', type: nodes.ReferenceType.Variable });
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 11, { name: '@let', type: nodes.ReferenceType.Variable }, { name: '.class', type: nodes.ReferenceType.Rule });
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 21, { name: '@color', type: nodes.ReferenceType.Variable });
assertSymbolsInScope(p, '@let: iable; .class { @color: blue; }', 36, { name: '@color', type: nodes.ReferenceType.Variable });
assertSymbolsInScope(p, '@namespace "x"; .mixin() {}', 0, { name: '.mixin', type: nodes.ReferenceType.Mixin });
assertSymbolsInScope(p, '.mixin() { .nested() {} }', 10, { name: '.nested', type: nodes.ReferenceType.Mixin });
assertSymbolsInScope(p, '.mixin() { .nested() {} }', 11);
assertSymbolsInScope(p, '@keyframes animation {};', 0, { name: 'animation', type: nodes.ReferenceType.Keyframe });
});
test('scopes and symbols', function () {
let p = new LESSParser();
assertScopesAndSymbols(p, '@var1: 1; @var2: 2; .foo { @var3: 3; }', '@var1,@var2,.foo,[@var3]');
assertScopesAndSymbols(p, '.mixin1 { @var0: 1} .mixin2(@var1) { @var3: 3 }', '.mixin1,.mixin2,[@var0],[@var1,@var3]');
assertScopesAndSymbols(p, 'a b { @var0: 1; c { d { } } }', '[@var0,c,[d,[]]]');
});
test('mark highlights', function (testDone) {
let p = new LESSParser();
Promise.all([
assertHighlights(p, '@var1: 1; @var2: /**/@var1;', '/**/', 2, 1, '@var1'),
assertHighlights(p, '@var1: 1; p { @var2: /**/@var1; }', '/**/', 2, 1, '@var1'),
assertHighlights(p, 'r1 { @var1: 1; p1: @var1;} r2,r3 { @var1: 1; p1: /**/@var1 + @var1;}', '/**/', 3, 1, '@var1'),
assertHighlights(p, '.r1 { r1: 1em; } r2 { r1: 2em; /**/.r1;}', '/**/', 2, 1, '.r1'),
assertHighlights(p, '.r1(@p1) { r1: @p1; } r2 { r1: 2em; /**/.r1(2px); }', '/**/', 2, 1, '.r1'),
assertHighlights(p, '/**/.r1(@p1) { r1: @p1; } r2 { r1: 2em; .r1(2px); }', '/**/', 2, 1, '.r1'),
assertHighlights(p, '@p1 : 1; .r1(@p1) { r1: /**/@p1; }', '/**/', 2, 1, '@p1'),
assertHighlights(p, '/**/@p1 : 1; .r1(@p1) { r1: @p1; }', '/**/', 1, 1, '@p1'),
assertHighlights(p, '@p1 : 1; .r1(/**/@p1) { r1: @p1; }', '/**/', 2, 1, '@p1'),
]).then(() => testDone(), (error) => testDone(error));
});
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {assertNodes} from '../css/nodes.test';
import * as nodes from '../../parser/cssNodes';
import {LESSParser} from '../../parser/lessParser';
suite('LESS - Nodes', () => {
function ruleset(input: string): nodes.RuleSet {
let parser = new LESSParser();
let node = parser.internalParse(input, parser._parseRuleset);
return node;
}
test('RuleSet', function () {
assertNodes(ruleset, 'selector { prop: value }', 'ruleset,...,selector,...,declaration,...,property,...,expression');
assertNodes(ruleset, 'selector { prop; }', 'ruleset,...,selector,...,selector');
assertNodes(ruleset, 'selector { prop {} }', 'ruleset,...,ruleset');
});
});

View File

@@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import {Parser} from '../../parser/cssParser';
import {TokenType} from '../../parser/cssScanner';
import * as nodes from '../../parser/cssNodes';
import {ParseError} from '../../parser/cssErrors';
import {LESSParser} from '../../parser/lessParser';
import {assertNode, assertNoNode, assertError} from '../css/parser.test';
suite('LESS - Parser', () => {
test('Variable', function() {
let parser = new LESSParser();
assertNode('@color', parser, parser._parseVariable.bind(parser));
assertNode('@co42lor', parser, parser._parseVariable.bind(parser));
assertNode('@-co42lor', parser, parser._parseVariable.bind(parser));
assertNode('@@foo', parser, parser._parseVariable.bind(parser));
assertNode('@@@foo', parser, parser._parseVariable.bind(parser));
assertNode('@12ooo', parser, parser._parseVariable.bind(parser));
assertNoNode('@ @foo', parser, parser._parseFunction.bind(parser));
assertNoNode('@-@foo', parser, parser._parseFunction.bind(parser));
});
test('Media', function() {
let parser = new LESSParser();
assertNode('@media @phone {}', parser, parser._parseMedia.bind(parser));
});
test('VariableDeclaration', function() {
let parser = new LESSParser();
assertNode('@color: #F5F5F5', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@color: 0', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@color: 255', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@color: 25.5', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@color: 25px', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@color: 25.5px', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@primary-font: "wf_SegoeUI","Segoe UI","Segoe","Segoe WP"', parser, parser._parseVariableDeclaration.bind(parser));
assertNode('@greeting: `"hello".toUpperCase() + "!";`', parser, parser._parseVariableDeclaration.bind(parser));
});
test('MixinDeclaration', function() {
let parser = new LESSParser();
assertNode('.color (@color: 25.5px) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.color(@color: 25.5px) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.color(@color) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.color(@color; @border) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.color() { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.color( ) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@a) when (@a > 10), (@a < -10) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@a) when (isnumber(@a)) and (@a > 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@b) when not (@b >= 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@b) when not (@b > 0) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@a, @rest...) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
assertNode('.mixin (@a) when (lightness(@a) >= 50%) { }', parser, parser._tryParseMixinDeclaration.bind(parser));
});
test('MixinReference', function() {
let parser = new LESSParser();
assertNode('.box-shadow(0 0 5px, 30%)', parser, parser._parseMixinReference.bind(parser));
assertNode('.box-shadow', parser, parser._parseMixinReference.bind(parser));
assertNode('.mixin(10) !important', parser, parser._parseMixinReference.bind(parser));
});
test('MixinParameter', function() {
let parser = new LESSParser();
assertNode('@_', parser, parser._parseMixinParameter.bind(parser));
assertNode('@let: value', parser, parser._parseMixinParameter.bind(parser));
assertNode('@let', parser, parser._parseMixinParameter.bind(parser));
assertNode('@rest...', parser, parser._parseMixinParameter.bind(parser));
assertNode('...', parser, parser._parseMixinParameter.bind(parser));
assertNode('value', parser, parser._parseMixinParameter.bind(parser));
assertNode('"string"', parser, parser._parseMixinParameter.bind(parser));
assertNode('50%', parser, parser._parseMixinParameter.bind(parser));
});
test('Parser - function', function() {
let parser = new LESSParser();
assertNode('%()', parser, parser._parseFunction.bind(parser));
assertNoNode('% ()', parser, parser._parseFunction.bind(parser));
});
test('Expr', function() {
let parser = new LESSParser();
assertNode('(@let + 20)', parser, parser._parseExpr.bind(parser));
assertNode('(@let - 20)', parser, parser._parseExpr.bind(parser));
assertNode('(@let * 20)', parser, parser._parseExpr.bind(parser));
assertNode('(@let / 20)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 - @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 * @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 / @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 / 20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + 20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + 20 + 20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + 20 + 20 + 20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + 20 + @let + 20 + 20 + @let)', parser, parser._parseExpr.bind(parser));
assertNode('(20 + 20)', parser, parser._parseExpr.bind(parser));
assertNode('(@var1 + @var2)', parser, parser._parseExpr.bind(parser));
assertNode('((@let + 5) * 2)', parser, parser._parseExpr.bind(parser));
assertNode('((@let + (5 + 2)) * 2)', parser, parser._parseExpr.bind(parser));
assertNode('(@let + ((5 + 2) * 2))', parser, parser._parseExpr.bind(parser));
assertNode('@color', parser, parser._parseExpr.bind(parser));
assertNode('@color, @color', parser, parser._parseExpr.bind(parser));
assertNode('@color, 42%', parser, parser._parseExpr.bind(parser));
assertNode('@color, 42%, @color', parser, parser._parseExpr.bind(parser));
assertNode('@color - (@color + 10%)', parser, parser._parseExpr.bind(parser));
assertNode('(@base + @filler)', parser, parser._parseExpr.bind(parser));
assertNode('(100% / 2 + @filler)', parser, parser._parseExpr.bind(parser));
assertNode('100% / 2 + @filler', parser, parser._parseExpr.bind(parser));
});
test('LessOperator', function() {
let parser = new LESSParser();
assertNode('>=', parser, parser._parseOperator.bind(parser));
assertNode('>', parser, parser._parseOperator.bind(parser));
assertNode('<', parser, parser._parseOperator.bind(parser));
assertNode('=<', parser, parser._parseOperator.bind(parser));
});
test('Extend', function() {
let parser = new LESSParser();
assertNode('nav { &:extend(.inline); }', parser, parser._parseRuleset.bind(parser));
});
test('Declaration', function() {
let parser = new LESSParser();
assertNode('border: thin solid 1px', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: @color', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: blue', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: (20 / @let)', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: (20 / 20 + @let)', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: func(@red)', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: desaturate(@red, 10%)', parser, parser._parseDeclaration.bind(parser));
assertNode('dummy: desaturate(16, 10%)', parser, parser._parseDeclaration.bind(parser));
assertNode('color: @base-color + #111', parser, parser._parseDeclaration.bind(parser));
assertNode('color: 100% / 2 + @ref', parser, parser._parseDeclaration.bind(parser));
assertNode('border: (@width * 2) solid black', parser, parser._parseDeclaration.bind(parser));
assertNode('property: @class', parser, parser._parseDeclaration.bind(parser));
assertNode('prop-erty: fnc(@t, 10%)', parser, parser._parseDeclaration.bind(parser));
});
test('Stylesheet', function() {
let parser = new LESSParser();
assertNode('.color (@radius: 5px){ -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
assertNode('.color (@radius: 5px){ -border-radius: @radius }', parser, parser._parseStylesheet.bind(parser));
assertNode('.color (@radius: 5px){ -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
assertNode('.color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
assertNode('.color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 } .color (@radius: 5px) { -border-radius: #F5F5F5 }', parser, parser._parseStylesheet.bind(parser));
assertNode('.mixin (@a, @rest...) {}', parser, parser._parseStylesheet.bind(parser));
assertNode('.mixin (@a) when (lightness(@a) >= 50%) { background-color: black;}', parser, parser._parseStylesheet.bind(parser));
assertNode('.some-mixin { font-weight:bold; } h1 { .some-mixin; font-size:40px; }', parser, parser._parseStylesheet.bind(parser));
assertNode('@color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
assertNode('@color: #F5F5F5; @color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
assertNode('@color: #F5F5F5; @color: #F5F5F5; @color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
assertNode('@color: #F5F5F5; .color (@radius: 5px) { -border-radius: #F5F5F5 } @color: #F5F5F5;', parser, parser._parseStylesheet.bind(parser));
assertNode('@import-once "lib";', parser, parser._parseStylesheet.bind(parser));
assertNode('@import-once (css) "hello";', parser, parser._parseStylesheet.bind(parser));
assertError('@import-once () "hello";', parser, parser._parseStylesheet.bind(parser), ParseError.IdentifierExpected);
assertError('@import-once (less);', parser, parser._parseStylesheet.bind(parser), ParseError.URIOrStringExpected);
});
test('Ruleset', function() {
let parser = new LESSParser();
assertNode('.selector { prop: erty @let 1px; }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { .mixin; }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { .mixin(1px); .mixin(blue, 1px, \'farboo\') }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { .mixin(blue; 1px;\'farboo\') }', parser, parser._parseRuleset.bind(parser));
assertNode('selector:active { property:value; nested:hover {}}', parser, parser._parseRuleset.bind(parser));
assertNode('selector {}', parser, parser._parseRuleset.bind(parser));
assertNode('selector { property: declaration }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { @variable: declaration }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { nested {}}', parser, parser._parseRuleset.bind(parser));
assertNode('selector { nested, a, b {}}', parser, parser._parseRuleset.bind(parser));
assertNode('selector { property: value; property: value; }', parser, parser._parseRuleset.bind(parser));
assertNode('selector { property: value; @keyframes foo {} @-moz-keyframes foo {}}', parser, parser._parseRuleset.bind(parser));
});
test('term', function() {
let parser = new LESSParser();
assertNode('%(\'repetitions: %S file: %S\', 1 + 2, "directory/file.less")', parser, parser._parseTerm.bind(parser));
assertNode('~"ms:alwaysHasItsOwnSyntax.For.Stuff()"', parser, parser._parseTerm.bind(parser)); // less syntax
});
test('Nested Ruleset', function() {
let parser = new LESSParser();
assertNode('.class1 { @let: 1; .class { @let: 2; three: @let; let: 3; } one: @let; }', parser, parser._parseRuleset.bind(parser));
assertNode('.class1 { @let: 1; > .class2 { display: none; } }', parser, parser._parseRuleset.bind(parser));
});
test('Selector Interpolation', function() {
let parser = new LESSParser();
assertNode('.@{name} { }', parser, parser._parseRuleset.bind(parser));
assertNode('~"@{name}" { }', parser, parser._parseRuleset.bind(parser));
assertError('~{ }', parser, parser._parseStylesheet.bind(parser), ParseError.StringLiteralExpected);
assertError('@', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.LeftCurlyExpected);
assertError('@{', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.IdentifierExpected);
assertError('@{dd', parser, parser._parseSelectorInterpolation.bind(parser), ParseError.RightCurlyExpected);
});
test('Selector Combinator', function() {
let parser = new LESSParser();
assertNode('&:hover', parser, parser._parseSimpleSelector.bind(parser));
assertNode('&.float', parser, parser._parseSimpleSelector.bind(parser));
assertNode('&-foo', parser, parser._parseSimpleSelector.bind(parser));
assertNode('&--&', parser, parser._parseSimpleSelector.bind(parser));
});
});

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import {Scanner, TokenType} from '../../parser/cssScanner';
import {LESSScanner} from '../../parser/lessScanner';
function assertSingleToken(source: string, len: number, offset: number, text: string, type: TokenType): void {
let scan = new LESSScanner();
scan.setSource(source);
let token = scan.scan();
assert.equal(token.len, len);
assert.equal(token.offset, offset);
assert.equal(token.text, text);
assert.equal(token.type, type);
}
suite('LESS - Scanner', () => {
test('Test Escaped JavaScript', function () {
assertSingleToken('`', 1, 0, '`', TokenType.BadEscapedJavaScript);
assertSingleToken('`a', 2, 0, '`a', TokenType.BadEscapedJavaScript);
assertSingleToken('`let a = "ssss"`', 16, 0, '`let a = "ssss"`', TokenType.EscapedJavaScript);
assertSingleToken('`let a = "ss\ns"`', 16, 0, '`let a = "ss\ns"`', TokenType.EscapedJavaScript);
});
// less deactivated comments
test('Test Token SingleLineComment', function () {
assertSingleToken('//', 0, 2, '', TokenType.EOF);
assertSingleToken('//this is a comment test', 0, 24, '', TokenType.EOF);
assertSingleToken('// this is a comment test', 0, 25, '', TokenType.EOF);
assertSingleToken('// this is a\na', 1, 13, 'a', TokenType.Ident);
assertSingleToken('// this is a\n// more\n \n/* comment */a', 1, 38, 'a', TokenType.Ident);
});
});