Skip to content
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

Emmet Integration for stylesheets #2129

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9253416
feat: emmet integration
devvaannsh Feb 6, 2025
d4d7bda
feat: display hints for emmet abbr that can be expanded
devvaannsh Feb 6, 2025
58004f1
fix: effective handling of closing braces in emmet abbr
devvaannsh Feb 7, 2025
046c08c
chore: also support tag names when in uppercase
devvaannsh Feb 7, 2025
0a68995
fix: emmet preferences enable/disable not working
devvaannsh Feb 7, 2025
17cb388
feat: display icon at the side of code hints. Helps in identifying i…
devvaannsh Feb 7, 2025
63d2662
chore: improve readability and add support for php and jsp
devvaannsh Feb 7, 2025
9f14cb3
feat: emmet integration for stylesheets
devvaannsh Feb 12, 2025
c168331
chore: add emmet icon at the side of emmet hint
devvaannsh Feb 13, 2025
9e7efdc
fix: add highlight to already typed characters for emmet hints
devvaannsh Feb 13, 2025
10eaeb8
fix: css hints getting displayed even when the css property is fully …
devvaannsh Feb 13, 2025
5375e7c
fix: integ tests failing because emmet hints are now given max priority
devvaannsh Feb 13, 2025
3d159b5
fix: check cursor position instead of line ending for keepHints
devvaannsh Feb 14, 2025
061d0cb
refactor: emmet icon is now a clickable link that redirects to MDN pa…
devvaannsh Feb 14, 2025
0894786
refactor: moved codehints icon colors to core_ui_variables file
devvaannsh Feb 14, 2025
84bcace
feat: add allPreferences module and enable Emmet preference
devvaannsh Feb 14, 2025
582dee6
chore: add unit-tests for emmet-stylesheets
devvaannsh Feb 14, 2025
d3152b3
Merge branch 'main' into emmet-css
devvaannsh Feb 15, 2025
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
1 change: 1 addition & 0 deletions src/brackets.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ define(function (require, exports, module) {
// load modules for later use
require("utils/Global");
require("editor/CSSInlineEditor");
require("preferences/AllPreferences");
require("project/WorkingSetSort");
require("search/QuickOpen");
require("search/QuickOpenHelper");
Expand Down
152 changes: 150 additions & 2 deletions src/extensions/default/CSSCodeHints/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,17 @@ define(function (require, exports, module) {
KeyEvent = brackets.getModule("utils/KeyEvent"),
LiveDevelopment = brackets.getModule("LiveDevelopment/main"),
Metrics = brackets.getModule("utils/Metrics"),
AllPreferences = brackets.getModule("preferences/AllPreferences"),
CSSProperties = require("text!CSSProperties.json"),
properties = JSON.parse(CSSProperties);

/**
* Emmet API:
* This provides a function to expand abbreviations into full CSS properties.
*/
const EXPAND_ABBR = Phoenix.libs.Emmet.expand;
let enabled = true; // whether Emmet is enabled or not in preferences

require("./css-lint");

const BOOSTED_PROPERTIES = [
Expand All @@ -60,6 +68,13 @@ define(function (require, exports, module) {
const cssWideKeywords = ['initial', 'inherit', 'unset', 'var()', 'calc()'];
let computedProperties, computedPropertyKeys;

// Stores a list of all CSS properties along with their corresponding MDN URLs.
// This is used by Emmet code hints to ensure users can still access MDN documentation.
// the Emmet icon serves as a clickable link that redirects to the MDN page for the property (if available).
// This object follows the structure:
// { PROPERTY_NAME: MDN_URL }
const MDN_PROPERTIES_URLS = {};

PreferencesManager.definePreference("codehint.CssPropHints", "boolean", true, {
description: Strings.DESCRIPTION_CSS_PROP_HINTS
});
Expand Down Expand Up @@ -248,7 +263,7 @@ define(function (require, exports, module) {
}

/**
* Returns a list of availble CSS propertyname or -value hints if possible for the current
* Returns a list of available CSS property name or -value hints if possible for the current
* editor context.
*
* @param {Editor} implicitChar
Expand Down Expand Up @@ -374,11 +389,97 @@ define(function (require, exports, module) {
const propertyKey = computedPropertyKeys[resultItem.sourceIndex];
if(properties[propertyKey] && properties[propertyKey].MDN_URL){
resultItem.MDN_URL = properties[propertyKey].MDN_URL;
MDN_PROPERTIES_URLS[propertyKey] = resultItem.MDN_URL;
}
}

// pushedHints stores all the hints that will be displayed to the user
let pushedHints = formatHints(result);

// make sure that emmet feature is on in preferences
if(enabled) {

// needle gives the current word before cursor, make sure that it exists
// also needle shouldn't contain `-`, because for example if user typed:
// `box-siz` then in that case it is very obvious that user wants to type `box-sizing`
// but emmet expands it `box: siz;`. So we prevent calling emmet when needle has `-`.
if(needle && !needle.includes('-')) {

// wrapped in try catch block because EXPAND_ABBR might throw error when it gets unexpected
// characters such as `, =, etc
try {
let expandedAbbr = EXPAND_ABBR(needle, { syntax: "css", type: "stylesheet" });
if(expandedAbbr && isEmmetExpandable(needle, expandedAbbr)) {

// if the expandedAbbr doesn't have any numbers, we should split the expandedAbbr to,
// get its first word before `:`.
// For instance, `m` expands to `margin: ;`. Here the `: ;` is unnecessary.
// Also, `bgc` expands to `background-color: #fff;`. Here we don't need the `: #fff;`
// as we have cssIntelligence to display hints based on the property
if(!isEmmetAbbrNumeric(expandedAbbr)) {
expandedAbbr = expandedAbbr.split(':')[0];
}

// token is required for highlighting the matched part. It gives access to
// stringRanges property. Refer to `formatHints()` function in this file for more detail
const [token] = StringMatch.codeHintsSort(needle, [expandedAbbr]);

// this displays an emmet icon at the side of the hint
// this gives an idea to the user that the hint is coming from Emmet
let $icon = $(`<a class="emmet-css-code-hint" style="text-decoration: none">Emmet</a>`);

// if MDN_URL is available for the property, add the href attribute to redirect to mdn
if(MDN_PROPERTIES_URLS[expandedAbbr]) {
$icon.attr("href", MDN_PROPERTIES_URLS[expandedAbbr]);
$icon.attr("title", Strings.DOCS_MORE_LINK_MDN_TITLE);
}

const $emmetHintObj = $("<span>")
.addClass("brackets-css-hints brackets-hints")
.attr("data-val", expandedAbbr);

// for highlighting the already-typed characters
if (token.stringRanges) {
token.stringRanges.forEach(function (range) {
if (range.matched) {
$emmetHintObj.append($("<span>")
.text(range.text)
.addClass("matched-hint"));
} else {
$emmetHintObj.append(range.text);
}
});
} else {
// fallback
$emmetHintObj.text(expandedAbbr);
}

// add the emmet icon to the final hint object
$emmetHintObj.append($icon);

if(pushedHints) {

// to remove duplicate hints. one comes from emmet and other from default css hints.
// we remove the default css hints and push emmet hint at the beginning.
for(let i = 0; i < pushedHints.length; i++) {
if(pushedHints[i][0].getAttribute('data-val') === expandedAbbr) {
pushedHints.splice(i, 1);
break;
}
}
pushedHints.unshift($emmetHintObj);
} else {
pushedHints = $emmetHintObj;
}
}
} catch (e) {
// pass
}
}
}

return {
hints: formatHints(result),
hints: pushedHints,
match: null, // the CodeHintManager should not format the results
selectInitial: selectInitial,
handleWideResults: false
Expand All @@ -387,6 +488,34 @@ define(function (require, exports, module) {
return null;
};

/**
* Checks whether the emmet abbr should be expanded or not.
* For instance: EXPAND_ABBR function always expands a value passed to it.
* if we pass 'xyz', then there's no CSS property matching to it, but it still expands this to `xyz: ;`.
* So, make sure that `needle + ': ;'` doesn't add to expandedAbbr
*
* @param {String} needle the word before the cursor
* @param {String} expandedAbbr the expanded abbr returned by EXPAND_ABBR emmet api
* @returns {boolean} true if emmet should be expanded, otherwise false
*/
function isEmmetExpandable(needle, expandedAbbr) {
return needle + ': ;' !== expandedAbbr;
}

/**
* Checks whether the expandedAbbr has any number.
* For instance: `m0` expands to `margin: 0;`, so we need to display the whole thing in the code hint
* Here, we also make sure that abbreviations which has `#`, `,` should not be included, because
* `color` expands to `color: #000;` or `color: rgb(0, 0, 0)`. So this actually has numbers, but we don't want to display this.
*
* @param {String} expandedAbbr the expanded abbr returned by EXPAND_ABBR emmet api
* @returns {boolean} true if expandedAbbr has numbers (and doesn't include '#') otherwise false.
*/
function isEmmetAbbrNumeric(expandedAbbr) {
return expandedAbbr.match(/\d/) !== null && !expandedAbbr.includes('#') && !expandedAbbr.includes(',');
}


const HISTORY_PREFIX = "Live_hint_";
let hintSessionId = 0, isInLiveHighlightSession = false;

Expand Down Expand Up @@ -578,13 +707,32 @@ define(function (require, exports, module) {
this.editor.setCursorPos(newCursor);
}

// If the cursor is just after a semicolon that means that,
// the CSS property is fully specified,
// so we don't need to continue showing hints for its value.
const cursorPos = this.editor.getCursorPos();
if(this.editor.getCharacterAtPosition({line: cursorPos.line, ch: cursorPos.ch - 1}) === ';') {
keepHints = false;
}

return keepHints;
};

/**
* Checks for preference changes, to enable/disable Emmet
*/
function preferenceChanged() {
enabled = PreferencesManager.get(AllPreferences.EMMET);
}


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

PreferencesManager.on("change", AllPreferences.EMMET, preferenceChanged);
preferenceChanged();

// For unit testing
exports.cssPropHintProvider = cssPropHints;
});
Expand Down
34 changes: 31 additions & 3 deletions src/extensions/default/CSSCodeHints/unittests.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ define(function (require, exports, module) {
expect(hintList[0]).toBe(expectedFirstHint);
}

function verifySecondAttrHint(hintList, expectedSecondHint) {
expect(hintList.indexOf("div")).toBe(-1);
expect(hintList[1]).toBe(expectedSecondHint);
}

function selectHint(provider, expectedHint, implicitChar) {
var hintList = expectHints(provider, implicitChar);
Expand Down Expand Up @@ -170,8 +174,16 @@ define(function (require, exports, module) {
testEditor.setCursorPos({ line: 6, ch: 2 });

var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
verifyAttrHints(hintList, "background-color"); // filtered on "b" ,
// background color should come at top as its boosted for UX
verifyAttrHints(hintList, "bottom"); // filtered on "b" ,
// bottom should come at top as it is coming from emmet, and it has the highest priority
});

it("should list the second prop-name hint starting with 'b'", function () {
testEditor.setCursorPos({ line: 6, ch: 2 });

var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
verifySecondAttrHint(hintList, "background-color"); // filtered on "b" ,
// background-color should be displayed at second. as first will be bottom coming from emmet
});

it("should list all prop-name hints starting with 'bord' ", function () {
Expand Down Expand Up @@ -244,6 +256,15 @@ define(function (require, exports, module) {
testDocument = null;
});

it("should expand m0 to margin: 0; when Emmet hint is used", function () {
testDocument.replaceRange("m0", { line: 6, ch: 2 });
testEditor.setCursorPos({ line: 6, ch: 4 });

selectHint(CSSCodeHints.cssPropHintProvider, "margin: 0;");
expect(testDocument.getLine(6)).toBe(" margin: 0;");
});


it("should insert colon prop-name selected", function () {
// insert semicolon after previous rule to avoid incorrect tokenizing
testDocument.replaceRange(";", { line: 6, ch: 2 });
Expand Down Expand Up @@ -459,7 +480,14 @@ define(function (require, exports, module) {
testEditor.setCursorPos({ line: 6, ch: 2 });

var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
verifyAttrHints(hintList, "background-color"); // filtered on "b"
verifyAttrHints(hintList, "bottom"); // filtered on "b"
});

it("should list the second prop-name hint starting with 'b' for style value context", function () {
testEditor.setCursorPos({ line: 6, ch: 2 });

var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
verifySecondAttrHint(hintList, "background-color"); // second result when filtered on "b"
});

it("should list all prop-name hints starting with 'bord' for style value context", function () {
Expand Down
Loading
Loading