Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

CSS AtRules, Pseudo elements and Pseudo selector code hints #13295

Merged
merged 7 commits into from
Apr 27, 2017

Conversation

swmitra
Copy link
Collaborator

@swmitra swmitra commented Apr 10, 2017

This PR adds CSS restricted block and pseudo selector/element code hints. In the implementation, priority of the hint providers have been kept at 0 while the priority of the default CSS code hint provider has been increased to 1. This has been done to ensure no additional overhead in case of CSS property:value code hints. Attached some screen shots of the corresponding code hints for illustration.

  • @rules
    atrules

  • Pseudo selectors
    pseudoselector

  • Pseudo elements
    pseudoelements

Copying @ingorichter @petetnt @ficristo @zaggino @peterflynn for code review.

Note: Additional description have been captured in the json def files to enable description tooltip at a later point of time.

Copy link
Collaborator

@petetnt petetnt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job, some initial comments! The feature itself seems to work just fine!

if (this.filter) {

// Filter the property list based on the token string
var result = $.map(Object.keys(AtRules), function (key, value) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer Array.prototype.map here, maybe with something like:

Object.keys(AtRules).map(function (key) {
  if (key.indexOf(token.string) === 0) {
    return key
  }
}).filter(function (key) {
  return key;
}).sort();

this.editor = editor;

// Check if we are at ':' pseudo rule or in 'variable-3' 'def' context
if (token.state.state === "pseudo" || token.type === "variable-3") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return token.state.state === "pseudo" || token.type === "variable-3"


if (this.context !== -1) {
// Filter the property list based on the token string
var result = $.map(this.context === TOKEN_TYPE_PSEUDO_SELECTOR ? Object.keys(PseudoRules.selectors) : Object.keys(PseudoRules.elements), function (value, key) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer Array.prototype.map, maybe do the context check first.

var keys = this.context === TOKEN_TYPE_PSEUDO_SELECTOR ? Object.keys(PseudoRules.selectors) : Object.keys(PseudoRules.elements);

var result = keys.map(...)

handleWideResults: false
};
} else {
return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other similar function returns null but I guess it doesn't matter

this.editor.setCursorPos({line: cursor.line, ch: cursor.ch - 1});
}

return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to return anything?

return false;
};

function _init() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty method

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT unused, remove.

{
"selectors" :
{
"active" : { "desc": "Selects the active link"},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalize whitespace in this file between the keys and values

@@ -468,7 +472,7 @@ define(function (require, exports, module) {

AppInit.appReady(function () {
var cssPropHints = new CssPropHints();
CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 0);
CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: What's the second parameter?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why we need this change?

Copy link
Collaborator Author

@swmitra swmitra Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mentioned it in the opening note of the PR 😄 .

In the implementation, priority of the hint providers have been kept at 0 while the priority of the default CSS code hint provider has been increased to 1. This has been done to ensure no additional overhead in case of CSS property:value code hints.

To explain, the second parameter is for priority. CodeHintManager keeps the providers for a particular mode in order of priority, If there are priority collisions, then the lists are sorted based on loading order(this is not in our control). CodeHintManager first checks the current doc mode and then iterates over this sorted providers by calling 'hasHints', once it gets affirmative answer from a particular provider, it breaks there and selects that provider as the current session hint provider. By increasing the priority of the existing provider I am trying to ensure that, it gets the opportunity first to provide hints ( as hasHints() of this provider will be called first). This will ensure that, there is zero overhead on the current property:value hint provider.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swmitra Sorry I already forgot about the opening note of PR 😸 thanks for the detailed explanation!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@ficristo
Copy link
Collaborator

This fixes #12486



/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, $ */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove both lines. Nowadays they are applyed globally.
And ideally there should be a jsdoc with a brief description of this file.

}

// Check if we are at '@' rule 'def' context
if ((token.type === "def" && cmState.context.type === "at") || (token.type === "variable-2" && (cmState.context.type === "top" || cmState.context.type === "block"))) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Split in two lines

this.filter = token.string;
this.token = token;

if (this.filter) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer an early return here:

if (!this.filter) {
  return null;
}

var result...

*
* @return {boolean}
* Indicates whether the manager should follow hint insertion with an
* additional explicit hint request.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use an uniform style to doc comments: the description should be always inline or always at new line.
I think the current style is on the same line.

/**
* Inserts a given @<rule> hint into the current editor context.
*
* @param {string} hint
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong param name



/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, $ */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As before, remove.

}

// As we are only going to provide :<pseudo> name hints
// we should claim that we don't have hints for anything else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use JSDoc and add the params

this.token = token;


if (this.context !== -1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return.

return false;
};

function _init() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT unused, remove.

@@ -468,7 +472,7 @@ define(function (require, exports, module) {

AppInit.appReady(function () {
var cssPropHints = new CssPropHints();
CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 0);
CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why we need this change?

@ficristo
Copy link
Collaborator

ficristo commented Apr 10, 2017

I briefly tested, but I cannot use the pseudo selectors in a SCSS file, but I can in a CSS one.

Do you have thought of using multiple extensions like suggested by @redmunds in #12486 (comment)?

I don't like much the requires with side effects in main.js, so if we go on this route,
do you think is possible to have AtRuleCodeHints, PseudoSelectorHints and CssPropHints files which exports their class and a main.js file with a single AppInit.appReady function that create an instance for every of the three classes?

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 11, 2017

@Petent @ficristo I have addressed all the review comments in my last commit. Also extracted these providers as separate default extensions. Need your review once more 😄

@ficristo
Copy link
Collaborator

In general LGTM, but I still cannot see pseudo selector hints for SCSS code.
Also add some tests.

I've use this snippet to test:

@charset "utf8";
/*comment*/

.test1: {
}

.test2:: {
}

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 12, 2017

@ficristo You probably have 'brackets-saas' extension installed. That extension registers 'SaasHints' provider with high priority and claims to provide hints ( 'hasHints' call returns true) even when it can't.
Please disable that extension and try. It should work.

@ficristo
Copy link
Collaborator

I disabled all my extensions and I can still reproduce.
If you write .test: in a SCSS file the hints work.
If you write .test:: {}, move the cursor after the last : and use Ctrl-space to ask hints, nothing happen. But the same works in a CSS file.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

@ficristo WIth the SCSS tokenizer in CM2, the token identifier is not able to detect the pseudo context in the second scenario you have mentioned. I have filed an issue with CM codemirror/codemirror5#4688 just now. Lets wait for a response from @marijnh.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

@ficristo Please refer to the CM issue and the discussion at codemirror/codemirror5#4688, we might have to live with this issue for sometime 😞 . But fortunately the moment you put the first char after :, you will get hints if the typed characters matches any of them. I tried to get some workaround with regex, but it's not going to work anyway as CM matches /:\s*\{/ and combines them to a single token. So distinguishing between /:\s*\{/ and /::\s*\{/ is virtually impossible for me (unless someone can figure out a hack not dependent on the CM token).

@marijnh
Copy link

marijnh commented Apr 13, 2017

Just figuring out what the SCSS mode is doing and fixing it is going to be a lot easier than trying to work around this from the outside.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

Thanks @marijnh for joining in 👍
I was just going through some of the syntax's in SCSS and most likely need to spend some more time to abstractly make sure that I don't break anything while fixing the CM CSS mode parser 😄

@ficristo
Copy link
Collaborator

According to http://sass-lang.com/documentation/file.SASS_REFERENCE.html#nested_properties you can "split" properties like this:

.test {
  font: {
    family: serif;
  }
}

instead of writing

.test {
  font-family: serif;
}

I don't know what we can do about it for our case.

@swmitra is the "problem" also present with :: ?

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

yes @ficristo. The regex used in the mode parser matches both the cases. This is what it does for punctuation : handling in mode/css/css.js @line 752 -

":": function(stream) {
        if (stream.match(/\s*\{/))
          return [null, "{"];
        return false;
      }

Now if I remove this or try to provide a punctuation handler without wrapping the /\s{/ , our property:value handling will go for a toss 😞 in the following SCSS construct

.test {
  font: {
    family: serif;
  }
}

I can think of one particular way though where we look backward for CSS property keywords and decide whether to identify this token as 'pseudo' or 'variable-3'. If the backward lookup tells us that the preceding token is actually a CSS property keyword then we don't handle the punctuation to identify as the start of 'pseudo'.

@marijnh Badly need your input here...

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

@marijnh I see that the following codemirror/codemirror5@2c46d1b fixes the issue 👍
[Edit]
I was excited to see the commit and the issue getting closed. It's not exactly fixed the problem here. The token string is : though, but state has not changed.
[Edit]
Thanks a lot @marijnh . it works now 👍 with some modification in the hint provider.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 13, 2017

@ficristo The issue in SCSS mode is fixed now using CM codemirror/codemirror5@2c46d1b and my last commit. Can you please check it once?

@ficristo
Copy link
Collaborator

To see the last changes in action we need to wait for a CodeMirror upgrade, is it correct?
Also this PR needs some tests.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 17, 2017

Yes @ficristo, I will upgrade CM once a release is available with the mentioned commit. I am writing tests for both the extensions...

Copy link
Contributor

@ingorichter ingorichter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of nits. Otherwise LGTM

}

// Filter the property list based on the token string
var result = Object.keys(AtRules).map(function (key) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but I think filter would be more appropriate than map for the first loop. This would also make the following filter loop unnecessary.

"active": {"desc": "Selects the active link"},
"checked": {"desc": "Selects every checked <input> element"},
"disabled": {"desc": "Selects every disabled <input> element"},
"empty" : {"desc": "Selects every element that has no children/text (including text nodes)"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: formatting is a bit off

},
"elements" :
{
"after": {"desc": "Insert something after the content of each element identified by this selctor"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: selector

"elements" :
{
"after": {"desc": "Insert something after the content of each element identified by this selctor"},
"before": {"desc": "Insert something before the content of each element identified by this selctor"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same selector spelling

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically all instances of selctor in this object ;-)

return key;
}
}).filter(function (key) {
return key;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replacing the previous map call with filter should make this obsolete

this.editor = editor;

// Check if we are at ':' pseudo rule or in 'variable-3' 'def' context
return token.state.state === "pseudo" || token.type === "variable-3" || token.string === PUNCTUATION_CHAR;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this could be a function determining the state. The same statement (negated) is used in getHints and could be reused there. Plus, it makes testing this function in isolation easier.

@swmitra
Copy link
Collaborator Author

swmitra commented Apr 26, 2017

@petetnt @ingorichter @ficristo Can you guys please have a look at this PR now?
CM dependency has been resolved with #13331 and also I have addressed all review comments and added unit tests for all style modes (css, less, scss).

@ficristo
Copy link
Collaborator

There is still the nit of @ingorichter to fix. No time to look again.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants