New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Less root functions, lookups, anonymous mixins #135

Open
wants to merge 4 commits into
base: master
from

Conversation

Projects
None yet
4 participants
@matthew-dean
Copy link
Contributor

matthew-dean commented Dec 2, 2018

This fixes a number of errors in the language service parser for CSS/Less (that are not covered in Microsoft/vscode#43087).

  • In Less, functions can be called anywhere to create a node, including any stylesheet or ruleset root. Fixed.
  • Custom functions can accept mixin-like arguments (such as accepting rulesets), and arguments can be separated with a semi-colon. Fixed.
  • Adds support for map lookups @ruleset[@lookup] or #ns.mixin(@arg)[prop]
  • Adds support for property references ($prop and interpolated ${prop})
  • Less is more forgiving of property and variable names than the CSS parser. This is particularly important after Less 3.5 which allowed the use of rulesets as maps, so declarations may be values like 100: 100 or even 10px: true if someone wanted to get crazy*. The CSS parser parses them as any number of token types (number or dimension), so there's a regex parsing function added in the Less parser to be more forgiving of property names.
  • Adds support for anonymous mixins, introduced with the each() function
  • Added if(), boolean, range(), and each() functions to Less completions
  • Fixes nested at-rule children of nested at-rules not being marked as nested... (Note: in Less, there are more at-rules than @supports and @media that bubble, but I notice the core CSS parser only supports those two types as being nested. Not sure if that's worth addressing or not.)
  • Fixes spelling for nestedProperties
  • Fixes detached ruleset declarations (assignment to a variable) no longer needing to terminate in a semi-colon (can end in } like any other block)
  • Fixes the disparity between mixin body and detached ruleset body. They support the same node types. Not sure why that was originally parsed differently here.
  • Fixes @supports ( as being parsed as a VariableCall. Checks for whitespace.

* The declaration of 100 as a property name is not crazy; it's actually a real-world use case of a conversion of Bootstrap 4 maps to Less maps. See: https://github.com/seanCodes/bootstrap-less-port/blob/35390325fad4e23e037396dd19081a57d4189579/less/_variables.less#L24

@msftclas

This comment has been minimized.

Copy link

msftclas commented Dec 2, 2018

CLA assistant check
All CLA requirements met.

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Dec 2, 2018

Just to note: I tested this through just the regular (and updated) tests on this repo. I haven't tried this in a local development build of VSCode.

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Dec 3, 2018

One thing that's still kind of outstanding is that because rulesets can be maps in Less, the warning and squiggly lines about "unknown property"s is somewhat useless. Is there a way for those unknown property flags to not be set in a Less document? I think just coloring the keyword differently would be enough, but the squiggly line is quite obnoxious. (That would be true for CSS in general IMO. It's okay to visually distinguish known and unknown, but I wouldn't hit the user over the head.)

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Dec 7, 2018

...Ping?

@octref

This comment has been minimized.

Copy link
Member

octref commented Dec 16, 2018

Sorry, @aeschli was on vacation and so was I. I'll review it in December.

@octref octref added this to the December 2018 milestone Dec 16, 2018

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Dec 16, 2018

@octref Ok no worries, just wanted to make sure it didn't get lost!

@octref
Copy link
Member

octref left a comment

Note: in Less, there are more at-rules than @supports and @media that bubble, but I notice the core CSS parser only supports those two types as being nested. Not sure if that's worth addressing or not.

That's fine for now.

Overall looks good, just a few questions to understand the less features being added.

assertNode('$color', parser, parser._parseVariable.bind(parser));
assertNode('$$color', parser, parser._parseVariable.bind(parser));
assertNode('@$color', parser, parser._parseVariable.bind(parser));
assertNode('$@color', parser, parser._parseVariable.bind(parser));

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

What are these? I don't know much about less, but from http://lesscss.org/features I understand these usages:

@color: #fff;

.foo {
  color: @color;
  background-color: $color;
}

Can you give me an example that uses @$, $@ and $$?

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

This is a combination of two things.

  1. Less supports variables and (like PHP) variable variables. Meaning, the name (identifier) of a variable being referenced can itself be variable. So: @@var means "return the value of a variable with the id that @var resolves to. If @var is foo then @foo is looked up/returned.
  2. In a 2.x release, support for property referencing ($prop) was added. Basically it just means that properties are (more or less) variables.

Well it's not explicitly documented, you can technically then combine these features of variable variables, where the property identifier being looked up is variable, or the variable id being looked up is variable based on a property name.

Why someone would ever do this, I don't know, it's just a normal side-effect of combining these features.

This comment has been minimized.

@octref

octref Jan 7, 2019

Member

This doesn't compile with less 3.9:

@color: #fff;

@lookup: 'color';

.foo {
  color: @@lookup;
  background-color: $@lookup;
}

I guess we can add more less-specific language features in the future. Now as long as VS Code doesn't fail at parsing these less files, users can get by.

This comment has been minimized.

@matthew-dean

matthew-dean Jan 7, 2019

Author Contributor

@octref You're right that that usage may not compile, but this does:

@dr: {
  prop: value;
}

.box {
  @lookup: prop;
  foo: @dr[$@lookup];
}

For lookup values, I think I did the VSCode parsing to use any of the variable reference form. So, yeah, it may not compile, but VSCode should still not throw an unrecoverable parsing error.

It's also possible the code you wrote will work at some point, just for consistency.


assertNode('func(a, b; bar)', parser, parser._parseRuleSetDeclaration.bind(parser));
assertNode('func({a: b();}, bar)', parser, parser._parseRuleSetDeclaration.bind(parser));
assertNode('func(.(@val) {})', parser, parser._parseRuleSetDeclaration.bind(parser));

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

Is this allowed?

This in lessc 3.9 gives me errors:

func(@x) {
  @x();
}

.foo {
  func({a: b();});
}
 less-test > lessc test.less
ParseError: Unrecognised input in /Users/octref/Code/work/less-test/test.less on line 12, column 10:
11 
12 func(@x) {
13   @x();

I thought functions only start with dot, like .func() {}?

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

You're thinking of mixins. Mixins start with . or #, and those are defined in your stylesheet. Functions are defined in JavaScript. They accept a node (or nodes), and return a node. lighten() and darken() are examples of Less functions. After Less made it easier for users to define functions using @plugin, the restriction on functions being only in a property's value was dropped. This way, authors can call a function that returns a ruleset, for example. Less also later added functions that are meant to be called around rules, such as each().

});

test('Interpolation', function () {
let parser = new LESSParser();
assertNode('.@{name} { }', parser, parser._parseRuleset.bind(parser));
assertNode('.${name} { }', parser, parser._parseRuleset.bind(parser));

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

I believe this is "Adds support for property references ($prop and interpolated ${prop})" you mentioned, but I couldn't find any such example on less doc. Can you give an example?

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

Similar to what's mentioned above, this is just a normal side-effect of treating properties as variables. That is, you can do interpolation in other values. Would this particular syntax in the above assertion ever make sense? Not really. But it's valid Less, which should be what the parser is checking for.

}

public _acceptInterpolatedIdent(node: nodes.Node): boolean {
public acceptRegexp(regEx: RegExp): boolean {

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

Maybe move this to cssparser.

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

I could probably do that.

return false;
}

public _parseRegexp(regEx: RegExp): nodes.Node {

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

Same as above, seems useful enough that we can have it in cssparser.


while (accept() ||
node.addChild(this._parseInterpolation() ||
this.try(delimWithInterpolation))) {

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

You can the conditions in the same line. It's hard to read what's inside the while because of no indentation.

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

Fair enough. Or maybe for really long conditions, move them all to an indented form?

As in:

while(
  condition() ||
  condition2()
) {
}

?

This comment has been minimized.

@octref

octref Jan 7, 2019

Member

Yeah that's better. this.try and hasContent on the same indent level was confusing to me.

@@ -361,9 +488,10 @@ export class LESSParser extends cssParser.Parser {
}

public _parseInterpolation(): nodes.Node {
// @{name}
// @{name} Variable or
// ${name} Property

This comment has been minimized.

@octref

octref Jan 2, 2019

Member

Same as the other ${prop} confusion. Can you point me to the docs that describe this feature?

This comment has been minimized.

@matthew-dean

matthew-dean Jan 3, 2019

Author Contributor

The documentation is very brief and doesn't cover the interpolated form. It's here: http://lesscss.org/features/#variables-feature-properties-as-variables-new-

It should probably be added to Less docs, but it's valid. In a lot of cases for these, I pulled code directly from Less unit tests and added them to the language service tests. In fact, I opened Less unit tests in VSCode to see what was failing first, before adding language service tests.

@octref

octref approved these changes Jan 7, 2019

Copy link
Member

octref left a comment

Just move parse/accept regex to base parser and this should be good to go. Thanks!

@aeschli

This comment has been minimized.

Copy link
Contributor

aeschli commented Jan 9, 2019

@matthew-dean Thanks a lot for your help! We're happy to have your expertise!

Most of the changes are good. To (hopefully) make it simpler to work on this, I already pushed some of the uncontroversial changes, see 5e7a590

What I don't like so much are changes related to the map lookups. The lookup is modelled as a child to variables and mixins. I think it is cleaner if we do it in the term, like an an operator (e.g. the % operator).

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Jan 9, 2019

@aeschli

What I don't like so much are changes related to the map lookups. The lookup is modelled as a child to variables and mixins. I think it is cleaner if we do it in the term, like an an operator (e.g. the % operator).

I'm unclear what you're saying...?

aeschli added a commit that referenced this pull request Jan 10, 2019

@aeschli

This comment has been minimized.

Copy link
Contributor

aeschli commented Jan 10, 2019

@matthew-dean My suggestion would be to add the support to parseTerm. Something like:

	public _parseTerm(): nodes.Term {
		let term = this._parseSimpleTerm();
		if (term && this.accept(TokenType.BracketL)) {
			// TODO: check for name, index of variable maybe even just `term`?
			if (!this.accept(TokenType.BracketR)) {
				//return error ...
			}
		}
		return term;
	}


	public _parseSimpleTerm(): nodes.Term {
		let term = super._parseTerm();
		if (term) { return term; }

		term = <nodes.Term>this.create(nodes.Term);
		if (term.setExpression(this._parseVariable()) ||
			term.setExpression(this._parseEscaped()) ||
			term.setExpression(this._tryParseMixinReference())) {

			return <nodes.Term>this.finish(term);
		}

		return null;
	}

That allows indexed access of every term which is probably more than necessary, but would keep the variables and mixins as is.

@octref

This comment has been minimized.

Copy link
Member

octref commented Feb 2, 2019

Although some of the changes are already in, we'll put the changes on release notes for next month when we merge the whole PR.

@matthew-dean

This comment has been minimized.

Copy link
Contributor Author

matthew-dean commented Feb 5, 2019

Sorry, will try to get back to this soon and the requested changes when I can find some time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment