Skip to content

Commit

Permalink
added CSSAtRuleCodeHints and CSSPseudoSelectorHints to extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsuhn authored and humphd committed Dec 22, 2017
1 parent 21e91f1 commit a120418
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/extensions/bramble-extensions.json
Expand Up @@ -10,6 +10,12 @@
{
"path": "extensions/default/HTMLCodeHints"
},
{
"path": "extensions/default/CSSAtRuleCodeHints"
},
{
"path": "extensions/default/CSSPseudoSelectorHints"
},
{
"path": "extensions/default/HtmlEntityCodeHints",
"less": {
Expand Down
10 changes: 10 additions & 0 deletions src/extensions/default/CSSAtRuleCodeHints/AtRulesDef.json
@@ -0,0 +1,10 @@
{
"@charset": "Defines the character set used by the style sheet.",
"@import": "Tells the CSS engine to include an external style sheet.",
"@namespace": "Tells the CSS engine that all its content must be considered prefixed with an XML namespace.",
"@media": "A conditional group rule which will apply its content if the device meets the criteria of the condition defined using a media query.",
"@supports": "A conditional group rule which will apply its content if the browser meets the criteria of the given condition.",
"@page": "Describes the aspect of layout changes which will be applied when printing the document.",
"@font-face": "Describes the aspect of an external font to be downloaded.",
"@keyframes": "Describes the aspect of intermediate steps in a CSS animation sequence."
}
118 changes: 118 additions & 0 deletions src/extensions/default/CSSAtRuleCodeHints/main.js
@@ -0,0 +1,118 @@
/*
* Copyright (c) 2017 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/

define(function (require, exports, module) {
"use strict";

// Load dependent modules
var AppInit = brackets.getModule("utils/AppInit"),
CodeHintManager = brackets.getModule("editor/CodeHintManager"),
AtRulesText = require("text!AtRulesDef.json"),
AtRules = JSON.parse(AtRulesText);


/**
* @constructor
*/
function AtRuleHints() {
}

// As we are only going to provide @rules name hints
// we should claim that we don't have hints for anything else
AtRuleHints.prototype.hasHints = function (editor, implicitChar) {
var pos = editor.getCursorPos(),
token = editor._codeMirror.getTokenAt(pos),
cmState;

this.editor = editor;

if (token.state.base && token.state.base.localState) {
cmState = token.state.base.localState;
} else {
cmState = token.state.localState || token.state;
}

// 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"))) {
this.filter = token.string;
return true;
} else {
this.filter = null;
return false;
}
};

AtRuleHints.prototype.getHints = function (implicitChar) {
var pos = this.editor.getCursorPos(),
token = this.editor._codeMirror.getTokenAt(pos);

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

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

// Filter the property list based on the token string
var result = Object.keys(AtRules).filter(function (key) {
if (key.indexOf(token.string) === 0) {
return key;
}
}).sort();

return {
hints: result,
match: this.filter,
selectInitial: true,
defaultDescriptionWidth: true,
handleWideResults: false
};
};


/**
* Inserts a given @<rule> hint into the current editor context.
*
* @param {string} completion
* The hint to be inserted into the editor context.
*
* @return {boolean}
* Indicates whether the manager should follow hint insertion with an
* additional explicit hint request.
*/
AtRuleHints.prototype.insertHint = function (completion) {
var cursor = this.editor.getCursorPos();
this.editor.document.replaceRange(completion, {line: cursor.line, ch: this.token.start}, {line: cursor.line, ch: this.token.end});
return false;
};

AppInit.appReady(function () {
// Register code hint providers
var restrictedBlockHints = new AtRuleHints();
CodeHintManager.registerHintProvider(restrictedBlockHints, ["css", "less", "scss"], 0);

// For unit testing
exports.restrictedBlockHints = restrictedBlockHints;
});
});
52 changes: 52 additions & 0 deletions src/extensions/default/CSSPseudoSelectorHints/PseudoSelectors.json
@@ -0,0 +1,52 @@
{
"selectors":
{
"active": {"desc": "Selects the active link"},
"checked": {"desc": "Selects every checked <input> element"},
"default": {"desc": "Selects every UI element that is the default among a group of similar elements"},
"dir(direction)": {"desc": "Selects every element whose text direction is 'direction'", "text": "dir()"},
"disabled": {"desc": "Selects every disabled <input> element"},
"empty": {"desc": "Selects every element that has no children/text (including text nodes)"},
"enabled": {"desc": "Selects every enabled <input> element"},
"first-child": {"desc": "Selects every element that is the first child of its parent"},
"first-of-type": {"desc": "Selects every element that is the first element identified by 'type' of its parent"},
"focus": {"desc": "Selects the input element which has focus"},
"focus-within": {"desc": "Selects every element which or whose descendant has focus"},
"fullscreen": {"desc": "Selects the element being in full-screen mode"},
"hover": {"desc": "Selects elements on mouse over"},
"in-range": {"desc": "Selects input elements with a value within a specified range"},
"indeterminate": {"desc": "Selects every indeterminate checkbox or radio button"},
"invalid": {"desc": "Selects all input elements with an invalid value"},
"lang(language)": {"desc": "Selects every element with a lang attribute equal to 'language'", "text": "lang()"},
"last-child": {"desc": "Selects every element that is the last child of its parent"},
"last-of-type": {"desc": "Selects every element that is the last element of its parent"},
"link": {"desc": "Selects all unvisited links"},
"matches(selectors)": {"desc": "Selects every element that is matched by one or more selectors in the 'selectors' list", "text": "matches()"},
"not(selector)": {"desc": "Selects every element that is not an element identified by 'selector'", "text": "not()"},
"nth-child(n)": {"desc": "Selects every element that is the second child of its parent", "text": "nth-child()"},
"nth-last-child(n)": {"desc": "Selects every element that is the second child of its parent, counting from the last child", "text": "nth-last-child()"},
"nth-last-of-type(n)": {"desc": "Selects every element that is the nth element of its parent, counting from the last child", "text": "nth-last-of-type()"},
"nth-of-type(n)": {"desc": "Selects every element that is the nth element of its parent", "text": "nth-of-type(n)"},
"only-child": {"desc": "Selects every element that is the only child of its parent"},
"only-of-type": {"desc": "Selects every element that is the only element of this type of its parent"},
"optional": {"desc": "Selects input elements with no 'required' attribute"},
"out-of-range": {"desc": "Selects input elements with a value outside a specified range"},
"placeholder-shown": {"desc": "Selects all <input> and <textarea> elements currently showing placeholder text"},
"read-only": {"desc": "Selects input elements with the 'readonly' attribute specified"},
"read-write": {"desc": "Selects input elements with the 'readonly' attribute NOT specified"},
"required": {"desc": "Selects input elements with the 'required' attribute specified"},
"root": {"desc": "Selects the document's root element"},
"target": {"desc": "Selects the current active element (clicked on a URL containing that anchor name)"},
"valid": {"desc": "Selects all input elements with a valid value"},
"visited": {"desc": "Selects all visited links"}
},
"elements":
{
"after": {"desc": "Insert something after the content of each element identified by this selector"},
"before": {"desc": "Insert something before the content of each element identified by this selector"},
"first-letter": {"desc": "Selects the first letter of every element identified by this selector"},
"first-line": {"desc": "Selects the first line of every element identified by this selector"},
"placeholder": {"desc": "Selects the placeholder text of <input> and <textarea> elements"},
"selection": {"desc": "Selects the portion of an element identified by this selector that is selected by a user"}
}
}
181 changes: 181 additions & 0 deletions src/extensions/default/CSSPseudoSelectorHints/main.js
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2017 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/

define(function (require, exports, module) {
"use strict";

// Load dependent modules
var AppInit = brackets.getModule("utils/AppInit"),
CodeHintManager = brackets.getModule("editor/CodeHintManager"),
TokenUtils = brackets.getModule("utils/TokenUtils"),
PseudoRulesText = require("text!PseudoSelectors.json"),
PseudoRules = JSON.parse(PseudoRulesText);


var TOKEN_TYPE_PSEUDO_SELECTOR = 0,
TOKEN_TYPE_PSEUDO_ELEMENT = 1,
PUNCTUATION_CHAR = ':';

function _getPseudoContext(token, cursorText, ctx) {
var slicedToken,
contextType = -1;

// Magic code to get around CM's 'pseudo' identification logic
// As per CSS3 spec :
// -> ':' identifies pseudo selectors
// -> '::' identifies pseudo elements
// We should strictly check for single or double occurance of ':' by slicing
// the line text till the token start position

if (token.state.state === "pseudo") {
slicedToken = cursorText.substr(0, token.start + 1).slice(-3);
} else if (token.type === "variable-3") {
slicedToken = cursorText.substr(0, token.start).slice(-3);
}

if (!slicedToken) {
//We get here when in SCSS mode and the cursor is right after ':'
//Test the previous token first
TokenUtils.movePrevToken(ctx);
if (ctx.token.string === PUNCTUATION_CHAR) {
//We are in pseudo elemnt context ('::')
contextType = TOKEN_TYPE_PSEUDO_ELEMENT;
} else {
contextType = TOKEN_TYPE_PSEUDO_SELECTOR;
}
} else {
if (slicedToken.slice(-2) === "::") {
contextType = TOKEN_TYPE_PSEUDO_ELEMENT;
} else if (slicedToken.slice(-1) === ":") {
contextType = TOKEN_TYPE_PSEUDO_SELECTOR;
}
}

return contextType;
}

/**
* @constructor
*/
function PsudoSelectorHints() {
}

function _validatePseudoContext(token) {
return token.state.state === "pseudo" || token.type === "variable-3" || token.string === PUNCTUATION_CHAR;
}

// As we are only going to provide :<pseudo> name hints
// we should claim that we don't have hints for anything else
PsudoSelectorHints.prototype.hasHints = function (editor, implicitChar) {
var pos = editor.getCursorPos(),
token = editor._codeMirror.getTokenAt(pos);

this.editor = editor;

// Check if we are at ':' pseudo rule or in 'variable-3' 'def' context
return _validatePseudoContext(token);
};

PsudoSelectorHints.prototype.getHints = function (implicitChar) {
var pos = this.editor.getCursorPos(),
token = this.editor._codeMirror.getTokenAt(pos),
filter = token.type === "variable-3" ? token.string : "",
lineTillCursor = this.editor._codeMirror.getLine(pos.line),
ctx = TokenUtils.getInitialContext(this.editor._codeMirror, pos);

if (!_validatePseudoContext(token)) {
return null;
}

// validate and keep the context in scope so that it can be used while getting description
this.context = _getPseudoContext(token, lineTillCursor, ctx);

// If we are not able to find context, don't proceed
if (this.context === -1) {
return null;
}

this.token = token;

// Filter the property list based on the token string
var result = Object.keys(this.context === TOKEN_TYPE_PSEUDO_SELECTOR ? PseudoRules.selectors : PseudoRules.elements).filter(function (key) {
if (key.indexOf(filter) === 0) {
return key;
}
}).sort();

return {
hints: result,
match: filter,
selectInitial: true,
defaultDescriptionWidth: true,
handleWideResults: false
};
};

/**
* Inserts a given ':<pseudo>' hint into the current editor context.
*
* @param {string} completion
* The hint to be inserted into the editor context.
*
* @return {boolean}
* Indicates whether the manager should follow hint insertion with an
* additional explicit hint request.
*/
PsudoSelectorHints.prototype.insertHint = function (completion) {
var cursor = this.editor.getCursorPos();
var startPos = {line: cursor.line, ch: this.token.start},
endPos = {line: cursor.line, ch: this.token.end};

if (this.token.state.state === "pseudo") {
// We have just started the 'pseudo' context, start replacing the current token by leaving ':' char
startPos.ch = startPos.ch + 1;
endPos = startPos;
}

if (this.context === TOKEN_TYPE_PSEUDO_SELECTOR) {
// If the hint label contains annotated data for illustration, then we might have
// different text to be inserted.
completion = PseudoRules.selectors[completion].text || completion;
}

this.editor.document.replaceRange(completion, startPos, endPos);

if (completion.slice(-1) === ")") {
cursor = this.editor.getCursorPos();
this.editor.setCursorPos({line: cursor.line, ch: cursor.ch - 1});
}

return false;
};

AppInit.appReady(function () {
// Register code hint providers
var pseudoSelectorHints = new PsudoSelectorHints();
CodeHintManager.registerHintProvider(pseudoSelectorHints, ["css", "scss", "less"], 0);

// For test
exports.pseudoSelectorHints = pseudoSelectorHints;
});
});

0 comments on commit a120418

Please sign in to comment.