Skip to content

Commit 05ddcd7

Browse files
authored
Merge pull request #2 from devvaannsh/emmet-css
Emmet css
2 parents 63d2662 + d3152b3 commit 05ddcd7

File tree

6 files changed

+255
-7
lines changed

6 files changed

+255
-7
lines changed

src/brackets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ define(function (require, exports, module) {
121121
// load modules for later use
122122
require("utils/Global");
123123
require("editor/CSSInlineEditor");
124+
require("preferences/AllPreferences");
124125
require("project/WorkingSetSort");
125126
require("search/QuickOpen");
126127
require("search/QuickOpenHelper");

src/extensions/default/CSSCodeHints/main.js

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,17 @@ define(function (require, exports, module) {
3535
KeyEvent = brackets.getModule("utils/KeyEvent"),
3636
LiveDevelopment = brackets.getModule("LiveDevelopment/main"),
3737
Metrics = brackets.getModule("utils/Metrics"),
38+
AllPreferences = brackets.getModule("preferences/AllPreferences"),
3839
CSSProperties = require("text!CSSProperties.json"),
3940
properties = JSON.parse(CSSProperties);
4041

42+
/**
43+
* Emmet API:
44+
* This provides a function to expand abbreviations into full CSS properties.
45+
*/
46+
const EXPAND_ABBR = Phoenix.libs.Emmet.expand;
47+
let enabled = true; // whether Emmet is enabled or not in preferences
48+
4149
require("./css-lint");
4250

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

71+
// Stores a list of all CSS properties along with their corresponding MDN URLs.
72+
// This is used by Emmet code hints to ensure users can still access MDN documentation.
73+
// the Emmet icon serves as a clickable link that redirects to the MDN page for the property (if available).
74+
// This object follows the structure:
75+
// { PROPERTY_NAME: MDN_URL }
76+
const MDN_PROPERTIES_URLS = {};
77+
6378
PreferencesManager.definePreference("codehint.CssPropHints", "boolean", true, {
6479
description: Strings.DESCRIPTION_CSS_PROP_HINTS
6580
});
@@ -248,7 +263,7 @@ define(function (require, exports, module) {
248263
}
249264

250265
/**
251-
* Returns a list of availble CSS propertyname or -value hints if possible for the current
266+
* Returns a list of available CSS property name or -value hints if possible for the current
252267
* editor context.
253268
*
254269
* @param {Editor} implicitChar
@@ -374,11 +389,97 @@ define(function (require, exports, module) {
374389
const propertyKey = computedPropertyKeys[resultItem.sourceIndex];
375390
if(properties[propertyKey] && properties[propertyKey].MDN_URL){
376391
resultItem.MDN_URL = properties[propertyKey].MDN_URL;
392+
MDN_PROPERTIES_URLS[propertyKey] = resultItem.MDN_URL;
393+
}
394+
}
395+
396+
// pushedHints stores all the hints that will be displayed to the user
397+
let pushedHints = formatHints(result);
398+
399+
// make sure that emmet feature is on in preferences
400+
if(enabled) {
401+
402+
// needle gives the current word before cursor, make sure that it exists
403+
// also needle shouldn't contain `-`, because for example if user typed:
404+
// `box-siz` then in that case it is very obvious that user wants to type `box-sizing`
405+
// but emmet expands it `box: siz;`. So we prevent calling emmet when needle has `-`.
406+
if(needle && !needle.includes('-')) {
407+
408+
// wrapped in try catch block because EXPAND_ABBR might throw error when it gets unexpected
409+
// characters such as `, =, etc
410+
try {
411+
let expandedAbbr = EXPAND_ABBR(needle, { syntax: "css", type: "stylesheet" });
412+
if(expandedAbbr && isEmmetExpandable(needle, expandedAbbr)) {
413+
414+
// if the expandedAbbr doesn't have any numbers, we should split the expandedAbbr to,
415+
// get its first word before `:`.
416+
// For instance, `m` expands to `margin: ;`. Here the `: ;` is unnecessary.
417+
// Also, `bgc` expands to `background-color: #fff;`. Here we don't need the `: #fff;`
418+
// as we have cssIntelligence to display hints based on the property
419+
if(!isEmmetAbbrNumeric(expandedAbbr)) {
420+
expandedAbbr = expandedAbbr.split(':')[0];
421+
}
422+
423+
// token is required for highlighting the matched part. It gives access to
424+
// stringRanges property. Refer to `formatHints()` function in this file for more detail
425+
const [token] = StringMatch.codeHintsSort(needle, [expandedAbbr]);
426+
427+
// this displays an emmet icon at the side of the hint
428+
// this gives an idea to the user that the hint is coming from Emmet
429+
let $icon = $(`<a class="emmet-css-code-hint" style="text-decoration: none">Emmet</a>`);
430+
431+
// if MDN_URL is available for the property, add the href attribute to redirect to mdn
432+
if(MDN_PROPERTIES_URLS[expandedAbbr]) {
433+
$icon.attr("href", MDN_PROPERTIES_URLS[expandedAbbr]);
434+
$icon.attr("title", Strings.DOCS_MORE_LINK_MDN_TITLE);
435+
}
436+
437+
const $emmetHintObj = $("<span>")
438+
.addClass("brackets-css-hints brackets-hints")
439+
.attr("data-val", expandedAbbr);
440+
441+
// for highlighting the already-typed characters
442+
if (token.stringRanges) {
443+
token.stringRanges.forEach(function (range) {
444+
if (range.matched) {
445+
$emmetHintObj.append($("<span>")
446+
.text(range.text)
447+
.addClass("matched-hint"));
448+
} else {
449+
$emmetHintObj.append(range.text);
450+
}
451+
});
452+
} else {
453+
// fallback
454+
$emmetHintObj.text(expandedAbbr);
455+
}
456+
457+
// add the emmet icon to the final hint object
458+
$emmetHintObj.append($icon);
459+
460+
if(pushedHints) {
461+
462+
// to remove duplicate hints. one comes from emmet and other from default css hints.
463+
// we remove the default css hints and push emmet hint at the beginning.
464+
for(let i = 0; i < pushedHints.length; i++) {
465+
if(pushedHints[i][0].getAttribute('data-val') === expandedAbbr) {
466+
pushedHints.splice(i, 1);
467+
break;
468+
}
469+
}
470+
pushedHints.unshift($emmetHintObj);
471+
} else {
472+
pushedHints = $emmetHintObj;
473+
}
474+
}
475+
} catch (e) {
476+
// pass
477+
}
377478
}
378479
}
379480

380481
return {
381-
hints: formatHints(result),
482+
hints: pushedHints,
382483
match: null, // the CodeHintManager should not format the results
383484
selectInitial: selectInitial,
384485
handleWideResults: false
@@ -387,6 +488,34 @@ define(function (require, exports, module) {
387488
return null;
388489
};
389490

491+
/**
492+
* Checks whether the emmet abbr should be expanded or not.
493+
* For instance: EXPAND_ABBR function always expands a value passed to it.
494+
* if we pass 'xyz', then there's no CSS property matching to it, but it still expands this to `xyz: ;`.
495+
* So, make sure that `needle + ': ;'` doesn't add to expandedAbbr
496+
*
497+
* @param {String} needle the word before the cursor
498+
* @param {String} expandedAbbr the expanded abbr returned by EXPAND_ABBR emmet api
499+
* @returns {boolean} true if emmet should be expanded, otherwise false
500+
*/
501+
function isEmmetExpandable(needle, expandedAbbr) {
502+
return needle + ': ;' !== expandedAbbr;
503+
}
504+
505+
/**
506+
* Checks whether the expandedAbbr has any number.
507+
* For instance: `m0` expands to `margin: 0;`, so we need to display the whole thing in the code hint
508+
* Here, we also make sure that abbreviations which has `#`, `,` should not be included, because
509+
* `color` expands to `color: #000;` or `color: rgb(0, 0, 0)`. So this actually has numbers, but we don't want to display this.
510+
*
511+
* @param {String} expandedAbbr the expanded abbr returned by EXPAND_ABBR emmet api
512+
* @returns {boolean} true if expandedAbbr has numbers (and doesn't include '#') otherwise false.
513+
*/
514+
function isEmmetAbbrNumeric(expandedAbbr) {
515+
return expandedAbbr.match(/\d/) !== null && !expandedAbbr.includes('#') && !expandedAbbr.includes(',');
516+
}
517+
518+
390519
const HISTORY_PREFIX = "Live_hint_";
391520
let hintSessionId = 0, isInLiveHighlightSession = false;
392521

@@ -578,13 +707,32 @@ define(function (require, exports, module) {
578707
this.editor.setCursorPos(newCursor);
579708
}
580709

710+
// If the cursor is just after a semicolon that means that,
711+
// the CSS property is fully specified,
712+
// so we don't need to continue showing hints for its value.
713+
const cursorPos = this.editor.getCursorPos();
714+
if(this.editor.getCharacterAtPosition({line: cursorPos.line, ch: cursorPos.ch - 1}) === ';') {
715+
keepHints = false;
716+
}
717+
581718
return keepHints;
582719
};
583720

721+
/**
722+
* Checks for preference changes, to enable/disable Emmet
723+
*/
724+
function preferenceChanged() {
725+
enabled = PreferencesManager.get(AllPreferences.EMMET);
726+
}
727+
728+
584729
AppInit.appReady(function () {
585730
var cssPropHints = new CssPropHints();
586731
CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 1);
587732

733+
PreferencesManager.on("change", AllPreferences.EMMET, preferenceChanged);
734+
preferenceChanged();
735+
588736
// For unit testing
589737
exports.cssPropHintProvider = cssPropHints;
590738
});

src/extensions/default/CSSCodeHints/unittests.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ define(function (require, exports, module) {
110110
expect(hintList[0]).toBe(expectedFirstHint);
111111
}
112112

113+
function verifySecondAttrHint(hintList, expectedSecondHint) {
114+
expect(hintList.indexOf("div")).toBe(-1);
115+
expect(hintList[1]).toBe(expectedSecondHint);
116+
}
113117

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

172176
var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
173-
verifyAttrHints(hintList, "background-color"); // filtered on "b" ,
174-
// background color should come at top as its boosted for UX
177+
verifyAttrHints(hintList, "bottom"); // filtered on "b" ,
178+
// bottom should come at top as it is coming from emmet, and it has the highest priority
179+
});
180+
181+
it("should list the second prop-name hint starting with 'b'", function () {
182+
testEditor.setCursorPos({ line: 6, ch: 2 });
183+
184+
var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
185+
verifySecondAttrHint(hintList, "background-color"); // filtered on "b" ,
186+
// background-color should be displayed at second. as first will be bottom coming from emmet
175187
});
176188

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

259+
it("should expand m0 to margin: 0; when Emmet hint is used", function () {
260+
testDocument.replaceRange("m0", { line: 6, ch: 2 });
261+
testEditor.setCursorPos({ line: 6, ch: 4 });
262+
263+
selectHint(CSSCodeHints.cssPropHintProvider, "margin: 0;");
264+
expect(testDocument.getLine(6)).toBe(" margin: 0;");
265+
});
266+
267+
247268
it("should insert colon prop-name selected", function () {
248269
// insert semicolon after previous rule to avoid incorrect tokenizing
249270
testDocument.replaceRange(";", { line: 6, ch: 2 });
@@ -459,7 +480,14 @@ define(function (require, exports, module) {
459480
testEditor.setCursorPos({ line: 6, ch: 2 });
460481

461482
var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
462-
verifyAttrHints(hintList, "background-color"); // filtered on "b"
483+
verifyAttrHints(hintList, "bottom"); // filtered on "b"
484+
});
485+
486+
it("should list the second prop-name hint starting with 'b' for style value context", function () {
487+
testEditor.setCursorPos({ line: 6, ch: 2 });
488+
489+
var hintList = expectHints(CSSCodeHints.cssPropHintProvider);
490+
verifySecondAttrHint(hintList, "background-color"); // second result when filtered on "b"
463491
});
464492

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

src/preferences/AllPreferences.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* GNU AGPL-3.0 License
3+
*
4+
* Copyright (c) 2021 - present core.ai . All rights reserved.
5+
* Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved.
6+
*
7+
* This program is free software: you can redistribute it and/or modify it
8+
* under the terms of the GNU Affero General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful, but WITHOUT
13+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
15+
* for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
19+
*
20+
*/
21+
22+
/*
23+
* This file houses all the preferences used across Phoenix.
24+
*
25+
* To use:
26+
* ```
27+
* const AllPreferences = brackets.getModule("preferences/AllPreferences");
28+
* function preferenceChanged() {
29+
enabled = PreferencesManager.get(AllPreferences.EMMET);
30+
}
31+
* PreferencesManager.on("change", AllPreferences.EMMET, preferenceChanged);
32+
preferenceChanged();
33+
* ```
34+
*/
35+
36+
define(function (require, exports, module) {
37+
const PreferencesManager = require("preferences/PreferencesManager");
38+
const Strings = require("strings");
39+
40+
// list of all the preferences
41+
const PREFERENCES_LIST = {
42+
EMMET: "emmet"
43+
};
44+
45+
PreferencesManager.definePreference(PREFERENCES_LIST.EMMET, "boolean", true, {
46+
description: Strings.DESCRIPTION_EMMET
47+
});
48+
49+
module.exports = PREFERENCES_LIST;
50+
});

src/styles/brackets_core_ui_variables.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,7 @@
272272
@dark-bc-codehint-desc: #2c2c2c;
273273
@dark-bc-codehint-desc-type-details: #46a0f5;
274274
@dark-bc-codehint-desc-documentation:#b1b1b1;
275+
276+
// CSS Codehint icon
277+
@css-codehint-icon: #2ea56c;
278+
@dark-css-codehint-icon: #146a41;

src/styles/brackets_patterns_override.less

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -703,12 +703,29 @@ a:focus {
703703
position: absolute;
704704
right: 0;
705705
margin-top:-2px;
706-
color: #2ea56c !important;
706+
color: @css-codehint-icon !important;
707707
.dark& {
708-
color: #146a41 !important;
708+
color: @dark-css-codehint-icon !important;
709709
}
710710
}
711711

712+
.emmet-css-code-hint {
713+
visibility: hidden;
714+
}
715+
716+
.codehint-menu .dropdown-menu li .highlight .emmet-css-code-hint {
717+
visibility: visible;
718+
position: absolute;
719+
right: 0;
720+
margin-top: -2px;
721+
font-size: 0.85em !important;
722+
font-weight: @font-weight-semibold;
723+
letter-spacing: 0.3px;
724+
color: @css-codehint-icon !important;
725+
.dark& {
726+
color: @dark-css-codehint-icon !important;
727+
}
728+
712729
.emmet-code-hint {
713730
position: absolute;
714731
font-size: 0.5em;

0 commit comments

Comments
 (0)