diff --git a/src/brackets.js b/src/brackets.js
index 75aabed6d2..372b5315ff 100644
--- a/src/brackets.js
+++ b/src/brackets.js
@@ -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");
diff --git a/src/extensions/default/CSSCodeHints/main.js b/src/extensions/default/CSSCodeHints/main.js
index 9d7e0c13ba..44a3d0d9a0 100644
--- a/src/extensions/default/CSSCodeHints/main.js
+++ b/src/extensions/default/CSSCodeHints/main.js
@@ -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 = [
@@ -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
});
@@ -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
@@ -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 = $(`Emmet`);
+
+ // 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 = $("")
+ .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($("")
+ .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
@@ -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;
@@ -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;
});
diff --git a/src/extensions/default/CSSCodeHints/unittests.js b/src/extensions/default/CSSCodeHints/unittests.js
index 683440247c..b6e411b7d2 100644
--- a/src/extensions/default/CSSCodeHints/unittests.js
+++ b/src/extensions/default/CSSCodeHints/unittests.js
@@ -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);
@@ -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 () {
@@ -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 });
@@ -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 () {
diff --git a/src/preferences/AllPreferences.js b/src/preferences/AllPreferences.js
new file mode 100644
index 0000000000..ad6223603f
--- /dev/null
+++ b/src/preferences/AllPreferences.js
@@ -0,0 +1,50 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ * Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/*
+ * This file houses all the preferences used across Phoenix.
+ *
+ * To use:
+ * ```
+ * const AllPreferences = brackets.getModule("preferences/AllPreferences");
+ * function preferenceChanged() {
+ enabled = PreferencesManager.get(AllPreferences.EMMET);
+ }
+ * PreferencesManager.on("change", AllPreferences.EMMET, preferenceChanged);
+ preferenceChanged();
+ * ```
+ */
+
+define(function (require, exports, module) {
+ const PreferencesManager = require("preferences/PreferencesManager");
+ const Strings = require("strings");
+
+ // list of all the preferences
+ const PREFERENCES_LIST = {
+ EMMET: "emmet"
+ };
+
+ PreferencesManager.definePreference(PREFERENCES_LIST.EMMET, "boolean", true, {
+ description: Strings.DESCRIPTION_EMMET
+ });
+
+ module.exports = PREFERENCES_LIST;
+});
diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less
index 9326487c3b..2ee87d8bd9 100644
--- a/src/styles/brackets_core_ui_variables.less
+++ b/src/styles/brackets_core_ui_variables.less
@@ -272,3 +272,7 @@
@dark-bc-codehint-desc: #2c2c2c;
@dark-bc-codehint-desc-type-details: #46a0f5;
@dark-bc-codehint-desc-documentation:#b1b1b1;
+
+// CSS Codehint icon
+@css-codehint-icon: #2ea56c;
+@dark-css-codehint-icon: #146a41;
diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less
index 166cd46d8b..bc8203fde9 100644
--- a/src/styles/brackets_patterns_override.less
+++ b/src/styles/brackets_patterns_override.less
@@ -703,12 +703,29 @@ a:focus {
position: absolute;
right: 0;
margin-top:-2px;
- color: #2ea56c !important;
+ color: @css-codehint-icon !important;
.dark& {
- color: #146a41 !important;
+ color: @dark-css-codehint-icon !important;
}
}
+.emmet-css-code-hint {
+ visibility: hidden;
+}
+
+.codehint-menu .dropdown-menu li .highlight .emmet-css-code-hint {
+ visibility: visible;
+ position: absolute;
+ right: 0;
+ margin-top: -2px;
+ font-size: 0.85em !important;
+ font-weight: @font-weight-semibold;
+ letter-spacing: 0.3px;
+ color: @css-codehint-icon !important;
+ .dark& {
+ color: @dark-css-codehint-icon !important;
+ }
+
.emmet-code-hint {
position: absolute;
font-size: 0.5em;