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..1b72f11e14 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 () {
@@ -459,7 +471,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 () {
@@ -810,6 +829,68 @@ define(function (require, exports, module) {
expectNoHints(CSSCodeHints.cssPropHintProvider, " ");
});
});
+
+
+ const emmetContent = "body {\n" +
+ " m\n" +
+ " bgc\n" +
+ " m0\n" +
+ " pt10\n" +
+ " ma\n" +
+ "}";
+
+ describe("Emmet hints for CSS", function () {
+
+ beforeEach(function () {
+ setupTest(emmetContent, "css");
+ });
+
+ afterEach(function () {
+ tearDownTest();
+ });
+
+ it("should display emmet hint margin when m is pressed", function () {
+ testEditor.setCursorPos({ line: 1, ch: 3 });
+ const hints = expectHints(CSSCodeHints.cssPropHintProvider);
+ verifyAttrHints(hints, "margin");
+ expect(hints.indexOf("margin")).toBe(0);
+ });
+
+ it("should display emmet hint background-color when bgc is pressed", function () {
+ testEditor.setCursorPos({ line: 2, ch: 5 });
+ const hints = expectHints(CSSCodeHints.cssPropHintProvider);
+ verifyAttrHints(hints, "background-color");
+ expect(hints.indexOf("background-color")).toBe(0);
+ });
+
+ it("should complete margin property when m0 is pressed", function () {
+ testEditor.setCursorPos({ line: 3, ch: 4 });
+ const hints = expectHints(CSSCodeHints.cssPropHintProvider);
+ verifyAttrHints(hints, "margin: 0;");
+ expect(hints.indexOf("margin: 0;")).toBe(0);
+
+ selectHint(CSSCodeHints.cssPropHintProvider, "margin: 0;");
+ expect(testDocument.getLine(3)).toBe(" margin: 0;");
+ expectCursorAt({ line: 3, ch: 12 });
+ });
+
+ it("should complete padding-top property when pt10 is pressed", function () {
+ testEditor.setCursorPos({ line: 4, ch: 6 });
+ const hints = expectHints(CSSCodeHints.cssPropHintProvider);
+ verifyAttrHints(hints, "padding-top: 10px;");
+ expect(hints.indexOf("padding-top: 10px;")).toBe(0);
+
+ selectHint(CSSCodeHints.cssPropHintProvider, "padding-top: 10px;");
+ expect(testDocument.getLine(4)).toBe(" padding-top: 10px;");
+ expectCursorAt({ line: 4, ch: 20 });
+ });
+
+ it("should not hint margin when ma is pressed", function () {
+ testEditor.setCursorPos({ line: 5, ch: 4 });
+ const hints = expectHints(CSSCodeHints.cssPropHintProvider);
+ expect(hints.indexOf("margin")).toBe(1); // this should not be 0, as max-width comes first
+ });
+
+ });
});
});
-
diff --git a/src/extensions/default/HTMLCodeHints/emmet-snippets.js b/src/extensions/default/HTMLCodeHints/emmet-snippets.js
new file mode 100644
index 0000000000..eb8bac9975
--- /dev/null
+++ b/src/extensions/default/HTMLCodeHints/emmet-snippets.js
@@ -0,0 +1,246 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ * Original work Copyright (c) 2024 [emmet.io](https://github.com/emmetio/brackets-emmet).
+ *
+ * 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.
+ */
+
+
+/**
+ * Emmet Snippets Configuration
+ *
+ * This file defines the configuration for Emmet snippet expansion.
+ * It contains four main exports:
+ *
+ * 1. **markupSnippets**: An object that maps abbreviation keys to their expanded HTML markup.
+ * These are all the abbreviations that can be expanded into something other than the usual tags.
+ * When an abbreviation matching one of the markup snippets is passed to Emmet, it knows how to expand it.
+ * These are sourced from `thirdparty/emmet.es.js`. To update this in future, refer to that file.
+ *
+ *
+ * 2. **htmlTags**: An array of standard HTML tags that are expanded by default.
+ * This list helps determine whether an abbreviation corresponds to a valid HTML element.
+ * Although Emmet can expand any text as an HTML tag,
+ * doing so would trigger code hints for every piece of text in the editor.
+ * So, we maintain a list of standard tags;
+ * only when an abbreviation matches one of these does Emmet display the code hints.
+ *
+ * 3. **positiveSymbols**: An array of symbols that, when present in an abbreviation,
+ * indicate that the abbreviation is eligible for expansion.
+ * Examples include `.`, `#`, `>`, `+`, etc., which are used for classes, IDs, nesting,
+ * sibling selectors, attributes, and more.
+ *
+ * 4. **negativeSymbols**: An array of sequences that indicate a word should NOT be expanded.
+ * For example, the sequence `` (which begins a closing tag) signals
+ * that the abbreviation should be ignored for expansion.
+ */
+define(function (require, exports, module) {
+
+
+ const markupSnippets = {
+ "a": "a[href]",
+ "a:blank": "a[href='http://${0}' target='_blank' rel='noopener noreferrer']",
+ "a:link": "a[href='http://${0}']",
+ "a:mail": "a[href='mailto:${0}']",
+ "a:tel": "a[href='tel:+${0}']",
+ "abbr": "abbr[title]",
+ "acr|acronym": "acronym[title]",
+ "base": "base[href]/",
+ "basefont": "basefont/",
+ "br": "br/",
+ "frame": "frame/",
+ "hr": "hr/",
+ "bdo": "bdo[dir]",
+ "bdo:r": "bdo[dir=rtl]",
+ "bdo:l": "bdo[dir=ltr]",
+ "col": "col/",
+ "link": "link[rel=stylesheet href]/",
+ "link:css": "link[href='${1:style}.css']",
+ "link:print": "link[href='${1:print}.css' media=print]",
+ "link:favicon": "link[rel='shortcut icon' type=image/x-icon href='${1:favicon.ico}']",
+ "link:mf|link:manifest": "link[rel='manifest' href='${1:manifest.json}']",
+ "link:touch": "link[rel=apple-touch-icon href='${1:favicon.png}']",
+ "link:rss": "link[rel=alternate type=application/rss+xml title=RSS href='${1:rss.xml}']",
+ "link:atom": "link[rel=alternate type=application/atom+xml title=Atom href='${1:atom.xml}']",
+ "link:im|link:import": "link[rel=import href='${1:component}.html']",
+ "meta": "meta/",
+ "meta:utf": "meta[http-equiv=Content-Type content='text/html;charset=UTF-8']",
+ "meta:vp": "meta[name=viewport content='width=${1:device-width}, initial-scale=${2:1.0}']",
+ "meta:compat": "meta[http-equiv=X-UA-Compatible content='${1:IE=7}']",
+ "meta:edge": "meta:compat[content='${1:ie=edge}']",
+ "meta:redirect": "meta[http-equiv=refresh content='0; url=${1:http://example.com}']",
+ "meta:refresh": "meta[http-equiv=refresh content='${1:5}']",
+ "meta:kw": "meta[name=keywords content]",
+ "meta:desc": "meta[name=description content]",
+ "style": "style",
+ "script": "script",
+ "script:src": "script[src]",
+ "script:module": "script[type=module src]",
+ "img": "img[src alt]/",
+ "img:s|img:srcset": "img[srcset src alt]",
+ "img:z|img:sizes": "img[sizes srcset src alt]",
+ "picture": "picture",
+ "src|source": "source/",
+ "src:sc|source:src": "source[src type]",
+ "src:s|source:srcset": "source[srcset]",
+ "src:t|source:type": "source[srcset type='${1:image/}']",
+ "src:z|source:sizes": "source[sizes srcset]",
+ "src:m|source:media": "source[media='(${1:min-width: })' srcset]",
+ "src:mt|source:media:type": "source:media[type='${2:image/}']",
+ "src:mz|source:media:sizes": "source:media[sizes srcset]",
+ "src:zt|source:sizes:type": "source[sizes srcset type='${1:image/}']",
+ "iframe": "iframe[src frameborder=0]",
+ "embed": "embed[src type]/",
+ "object": "object[data type]",
+ "param": "param[name value]/",
+ "map": "map[name]",
+ "area": "area[shape coords href alt]/",
+ "area:d": "area[shape=default]",
+ "area:c": "area[shape=circle]",
+ "area:r": "area[shape=rect]",
+ "area:p": "area[shape=poly]",
+ "form": "form[action]",
+ "form:get": "form[method=get]",
+ "form:post": "form[method=post]",
+ "label": "label[for]",
+ "input": "input[type=${1:text}]/",
+ "inp": "input[name=${1} id=${1}]",
+ "input:h|input:hidden": "input[type=hidden name]",
+ "input:t|input:text": "inp[type=text]",
+ "input:search": "inp[type=search]",
+ "input:email": "inp[type=email]",
+ "input:url": "inp[type=url]",
+ "input:p|input:password": "inp[type=password]",
+ "input:datetime": "inp[type=datetime]",
+ "input:date": "inp[type=date]",
+ "input:datetime-local": "inp[type=datetime-local]",
+ "input:month": "inp[type=month]",
+ "input:week": "inp[type=week]",
+ "input:time": "inp[type=time]",
+ "input:tel": "inp[type=tel]",
+ "input:number": "inp[type=number]",
+ "input:color": "inp[type=color]",
+ "input:c|input:checkbox": "inp[type=checkbox]",
+ "input:r|input:radio": "inp[type=radio]",
+ "input:range": "inp[type=range]",
+ "input:f|input:file": "inp[type=file]",
+ "input:s|input:submit": "input[type=submit value]",
+ "input:i|input:image": "input[type=image src alt]",
+ "input:b|input:btn|input:button": "input[type=button value]",
+ "input:reset": "input:button[type=reset]",
+ "isindex": "isindex/",
+ "select": "select[name=${1} id=${1}]",
+ "select:d|select:disabled": "select[disabled.]",
+ "opt|option": "option[value]",
+ "textarea": "textarea[name=${1} id=${1}]",
+ "tarea:c|textarea:cols": "textarea[name=${1} id=${1} cols=${2:30}]",
+ "tarea:r|textarea:rows": "textarea[name=${1} id=${1} rows=${3:10}]",
+ "tarea:cr|textarea:cols:rows": "textarea[name=${1} id=${1} cols=${2:30} rows=${3:10}]",
+ "marquee": "marquee[behavior direction]",
+ "menu:c|menu:context": "menu[type=context]",
+ "menu:t|menu:toolbar": "menu[type=toolbar]",
+ "video": "video[src]",
+ "audio": "audio[src]",
+ "html:xml": "html[xmlns=http://www.w3.org/1999/xhtml]",
+ "keygen": "keygen/",
+ "command": "command/",
+ "btn:s|button:s|button:submit": "button[type=submit]",
+ "btn:r|button:r|button:reset": "button[type=reset]",
+ "btn:b|button:b|button:button": "button[type=button]",
+ "btn:d|button:d|button:disabled": "button[disabled.]",
+ "fst:d|fset:d|fieldset:d|fieldset:disabled": "fieldset[disabled.]",
+
+ "bq": "blockquote",
+ "fig": "figure",
+ "figc": "figcaption",
+ "pic": "picture",
+ "ifr": "iframe",
+ "emb": "embed",
+ "obj": "object",
+ "cap": "caption",
+ "colg": "colgroup",
+ "fst": "fieldset",
+ "btn": "button",
+ "optg": "optgroup",
+ "tarea": "textarea",
+ "leg": "legend",
+ "sect": "section",
+ "art": "article",
+ "hdr": "header",
+ "ftr": "footer",
+ "adr": "address",
+ "dlg": "dialog",
+ "str": "strong",
+ "prog": "progress",
+ "mn": "main",
+ "tem": "template",
+ "fset": "fieldset",
+ "datal": "datalist",
+ "kg": "keygen",
+ "out": "output",
+ "det": "details",
+ "sum": "summary",
+ "cmd": "command",
+ "data": "data[value]",
+ "meter": "meter[value]",
+ "time": "time[datetime]",
+
+ "ri:d|ri:dpr": "img:s",
+ "ri:v|ri:viewport": "img:z",
+ "ri:a|ri:art": "pic>src:m+img",
+ "ri:t|ri:type": "pic>src:t+img",
+
+ "!!!": "{}",
+ "doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body",
+ "!|html:5": "!!!+doc",
+
+ "c": "{}",
+ "cc:ie": "{}",
+ "cc:noie": "{${0}}"
+ };
+
+
+ const htmlTags = [
+ "a", "abbr", "address", "area", "article", "aside", "audio", "b", "base",
+ "bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption",
+ "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del",
+ "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed", "fieldset",
+ "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5",
+ "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img",
+ "input", "ins", "kbd", "label", "legend", "li", "link", "main", "map",
+ "mark", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup",
+ "option", "output", "p", "param", "picture", "pre", "progress", "q",
+ "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "small",
+ "source", "span", "strong", "style", "sub", "summary", "sup", "table",
+ "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time",
+ "title", "tr", "track", "u", "ul", "var", "video", "wbr"
+ ];
+
+
+ const positiveSymbols = [
+ '.', '#', '!', '>', '+', '^', '*', '[', ']', '{', '}', '(', ')', '&'
+ ];
+
+
+ const negativeSymbols = [
+ ''
+ ];
+
+ exports.markupSnippets = markupSnippets;
+ exports.htmlTags = htmlTags;
+ exports.positiveSymbols = positiveSymbols;
+ exports.negativeSymbols = negativeSymbols;
+});
diff --git a/src/extensions/default/HTMLCodeHints/main.js b/src/extensions/default/HTMLCodeHints/main.js
index 2dc852f033..4fe821e5e1 100644
--- a/src/extensions/default/HTMLCodeHints/main.js
+++ b/src/extensions/default/HTMLCodeHints/main.js
@@ -33,6 +33,7 @@ define(function (require, exports, module) {
CSSUtils = brackets.getModule("language/CSSUtils"),
StringMatch = brackets.getModule("utils/StringMatch"),
LiveDevelopment = brackets.getModule("LiveDevelopment/main"),
+ AllPreferences = brackets.getModule("preferences/AllPreferences"),
KeyEvent = brackets.getModule("utils/KeyEvent"),
Metrics = brackets.getModule("utils/Metrics"),
HTMLTags = require("text!HtmlTags.json"),
@@ -42,6 +43,28 @@ define(function (require, exports, module) {
require("./html-lint");
+ const {
+ markupSnippets,
+ htmlTags,
+ positiveSymbols,
+ negativeSymbols
+ } = require('./emmet-snippets');
+
+ /**
+ * The Emmet api's
+ */
+ const EXPAND_ABBR = Phoenix.libs.Emmet.expand;
+
+ /**
+ * A list of all the markup snippets that can be expanded.
+ * For ex: 'link:css', 'iframe'
+ * They expand differently as compared to normal tags.
+ * Refer to `./emmet-snippets.js` file for more info.
+ */
+ const markupSnippetsList = Object.keys(markupSnippets);
+ let enabled = true; // whether Emmet is enabled or not in preferences
+
+
let tags,
attributes;
@@ -53,6 +76,452 @@ define(function (require, exports, module) {
description: Strings.DESCRIPTION_ATTR_HINTS
});
+ /**
+ * @constructor
+ */
+ function EmmetMarkupHints() {
+ }
+
+
+ /**
+ * Checks whether hints are available for the current word where cursor is present.
+ *
+ * @param {Editor} editor - the editor instance
+ * @param {String} implicitChar - unused param [didn't remove, as we might need it in future]
+ * @returns {Boolean} - true if the abbr can be expanded otherwise false.
+ */
+ EmmetMarkupHints.prototype.hasHints = function (editor, implicitChar) {
+ if (enabled) {
+ this.editor = editor;
+
+ const wordObj = getWordBeforeCursor(editor);
+ // make sure we donot have empty spaces
+ if (wordObj.word.trim()) {
+
+ const expandedAbbr = isExpandable(editor, wordObj.word);
+ if (expandedAbbr) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+
+ /**
+ * Returns the Emmet hint for the current word before the cursor.
+ * The hint element will have an appended "Emmet" icon at bottom-rigth to indicate it's an Emmet abbreviation.
+ *
+ * @param {String} implicitChar - unused param [didn't remove, as we might need it in future]
+ */
+ EmmetMarkupHints.prototype.getHints = function (implicitChar) {
+ const wordObj = getWordBeforeCursor(this.editor);
+
+ // Check if the abbreviation is expandable
+ const expandedAbbr = isExpandable(this.editor, wordObj.word);
+ if (!expandedAbbr) {
+ return null;
+ }
+
+ // Create the formatted hint element with an appended Emmet icon
+ const formattedHint = formatEmmetHint(wordObj.word);
+
+ return {
+ hints: [formattedHint],
+ match: null,
+ selectInitial: true,
+ defaultDescriptionWidth: true,
+ handleWideResults: false
+ };
+ };
+
+
+ /**
+ * Formats an Emmet abbreviation hint by appending an icon.
+ *
+ * @param {string} abbr - The Emmet abbreviation.
+ * @returns {jQuery} - A jQuery element representing the formatted hint.
+ */
+ function formatEmmetHint(abbr) {
+ // Create the main container for the hint text.
+ var $hint = $("")
+ .addClass("emmet-hint")
+ .text(abbr);
+
+ // style in brackets_patterns_override.less file
+ let $icon = $(`Emmet`);
+
+ // Append the icon to the hint element
+ $hint.append($icon);
+
+ return $hint;
+ }
+
+
+ /**
+ * Responsible for updating the abbr with the expanded text in the editor.
+ * This function calls helper functions for this as there are,
+ * lot of complex cases that should be taken care of.
+ */
+ EmmetMarkupHints.prototype.insertHint = function () {
+ const wordObj = getWordBeforeCursor(this.editor);
+ const expandedAbbr = isExpandable(this.editor, wordObj.word);
+ updateAbbrInEditor(this.editor, wordObj, expandedAbbr);
+ return false;
+ };
+
+
+ /**
+ * Determines whether a given character is allowed as part of an Emmet abbreviation
+ *
+ * @param {String} char - The character to test
+ * @param {Boolean} insideBraces - Flag indicating if we are inside braces (e.g. {} or [])
+ * @returns True if the character is valid for an abbreviation
+ */
+ function isEmmetChar(char, insideBraces) {
+ // Valid abbreviation characters: letters, digits, and some punctuation
+ // Adjust this regex or the list as needed for your implementation
+ const validPattern = /[a-zA-Z0-9:+*<>()/!$\-@#}{]/;
+ const specialChars = new Set(['.', '#', '[', ']', '"', '=', ':', ',', '-']);
+ return validPattern.test(char) || specialChars.has(char) || (insideBraces && char === ' ');
+ }
+
+
+ /**
+ * Scans backwards from the given cursor position on a line to locate the start of the Emmet abbreviation
+ *
+ * @param {String} line - The full text of the current line
+ * @param {Number} cursorCh - The cursor's character (column) position on that line
+ * @returns The index (column) where the abbreviation starts
+ */
+ function findAbbreviationStart(line, cursorCh) {
+ let start = cursorCh;
+ let insideBraces = false;
+
+ // If the cursor is right before a closing brace, adjust it to be "inside" the braces
+ if (line.charAt(start) === '}' || line.charAt(start) === ']') {
+ start--;
+ insideBraces = true;
+ }
+
+ // Walk backwards from the cursor to find the boundary of the abbreviation
+ while (start > 0) {
+ const char = line.charAt(start - 1);
+
+ // Update our "inside braces" state based on the character
+ if (char === '}' || char === ']') {
+ insideBraces = true;
+ } else if (char === '{' || char === '[') {
+ insideBraces = false;
+ }
+
+ // If the character is valid as part of an Emmet abbreviation, continue scanning backwards
+ if (isEmmetChar(char, insideBraces)) {
+ start--;
+ } else {
+ break;
+ }
+ }
+ return start;
+ }
+
+
+ /**
+ * Retrieves the Emmet abbreviation (i.e. the word before the cursor) from the current editor state
+ *
+ * @param {Editor} editor - The editor instance
+ * @returns An object with the abbreviation and its start/end positions
+ *
+ * Format:
+ * {
+ * word: string, // the extracted abbreviation
+ * start: { line: number, ch: number },
+ * end: { line: number, ch: number }
+ * }
+ */
+ function getWordBeforeCursor(editor) {
+ const pos = editor.getCursorPos();
+ const lineText = editor.document.getLine(pos.line);
+
+ // to determine where the abbreviation starts on the line
+ const abbreviationStart = findAbbreviationStart(lineText, pos.ch);
+
+ // Optionally, adjust the end position if the cursor is immediately before a closing brace.
+ let abbreviationEnd = pos.ch;
+ if (lineText.charAt(abbreviationEnd) === '}' || lineText.charAt(abbreviationEnd) === ']') {
+ abbreviationEnd++;
+ }
+
+ const word = lineText.substring(abbreviationStart, abbreviationEnd);
+
+ return {
+ word: word,
+ start: { line: pos.line, ch: abbreviationStart },
+ end: { line: pos.line, ch: abbreviationEnd }
+ };
+ }
+
+
+ /**
+ * Calculate the indentation level for the current line
+ *
+ * @param {Editor} editor - the editor instance
+ * @param {Object} position - position object with line number
+ * @returns {String} - the indentation string
+ */
+ function getLineIndentation(editor, position) {
+ const line = editor.document.getLine(position.line);
+ const match = line.match(/^\s*/);
+ return match ? match[0] : '';
+ }
+
+
+ /**
+ * Adds proper indentation to multiline Emmet expansion
+ *
+ * @param {String} expandedText - the expanded Emmet abbreviation
+ * @param {String} baseIndent - the base indentation string
+ * @returns {String} - properly indented text
+ */
+ function addIndentation(expandedText, baseIndent) {
+ // Split into lines, preserve empty lines
+ const lines = expandedText.split(/(\r\n|\n)/g);
+
+ // Process each line
+ let result = '';
+ let isFirstLine = true;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // If it's a newline character, just add it
+ if (line === '\n' || line === '\r\n') {
+ result += line;
+ continue;
+ }
+
+ // Skip indenting empty lines
+ if (line.trim() === '') {
+ result += line;
+ continue;
+ }
+
+ // Don't indent the first line as it inherits the current indent
+ if (isFirstLine) {
+ result += line;
+ isFirstLine = false;
+ } else {
+ // Add base indent plus the existing indent in the expanded text
+ result += baseIndent + line;
+ }
+ }
+
+ return result;
+ }
+
+
+
+ /**
+ * Find the position where cursor should be placed after expansion
+ * Looks for patterns like '><', '""', ''
+ *
+ * @param {Editor} editor - The editor instance
+ * @param {String} indentedAbbr - the indented abbreviation
+ * @param {Object} startPos - Starting position {line, ch} of the expansion
+ * @returns {Object | false} - Cursor position {line, ch} or false if no pattern found
+ */
+ function findCursorPosition(editor, indentedAbbr, startPos) {
+ const totalLines = startPos.line + indentedAbbr.split('\n').length;
+
+ for (let i = startPos.line; i < totalLines; i++) {
+ const line = editor.document.getLine(i);
+
+ for (let j = 0; j < line.length - 1; j++) {
+ const pair = line[j] + line[j + 1];
+
+ if (pair === '""' || pair === "''") {
+ return { line: i, ch: j + 1 };
+ }
+ }
+ for (let j = 0; j < line.length - 1; j++) {
+ const pair = line[j] + line[j + 1];
+
+ if (pair === '><') {
+ return { line: i, ch: j + 1 };
+ }
+ }
+ }
+
+ // Look for opening and closing tag pairs with empty line in between
+ //
+ // |
+ //
+ // here in such scenarios, we want the cursor to be placed in between
+ // Look for opening and closing tag pairs with empty line in between
+ for (let i = startPos.line; i < totalLines; i++) {
+ const line = editor.document.getLine(i).trim();
+ if (line.endsWith('>') && line.includes('<') && !line.includes('')) {
+ if (editor.document.getLine(i + 1) && !editor.document.getLine(i + 1).trim()) {
+ const tempLine = editor.document.getLine(i + 2);
+ if (tempLine) {
+ const trimmedTempLine = tempLine.trim();
+ if (trimmedTempLine.includes('') && trimmedTempLine.startsWith('<')) {
+ // Get the current line's indentation by counting spaces/tabs
+ const openingTagLine = editor.document.getLine(i);
+ const indentMatch = openingTagLine.match(/^[\s\t]*/)[0];
+ // Add 4 more spaces (or equivalent tab) for inner content
+ const extraIndent = ' '; // 4 spaces for additional indentation
+
+ return {
+ line: i + 1,
+ ch: indentMatch.length + extraIndent.length
+ };
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+
+
+ /**
+ * This function is responsible to replace the abbreviation in the editor,
+ * with its expanded version
+ *
+ * @param {Editor} editor - the editor instance
+ * @param {Object} wordObj - an object in the format :
+ * {
+ * word: "", // the word before the cursor
+ * start: {line: Number, ch: Number},
+ * end: {line: Number, ch: Number}
+ * }
+ * @param {String} expandedAbbr - the expanded version of abbr that will replace the abbr
+ */
+ function updateAbbrInEditor(editor, wordObj, expandedAbbr) {
+ // Get the current line's indentation
+ const baseIndent = getLineIndentation(editor, wordObj.start);
+
+ // Add proper indentation to the expanded abbreviation
+ const indentedAbbr = addIndentation(expandedAbbr, baseIndent);
+
+ // Handle the special case for braces
+ // this check is added because in some situations such as
+ // `ul>li{Hello}` and the cursor is before the closing braces right after 'o',
+ // then when this is expanded it results in an extra closing braces at the end.
+ // so we remove the extra closing brace from the end
+ if (wordObj.word.includes('{') || wordObj.word.includes('[')) {
+ const pos = editor.getCursorPos();
+ const line = editor.document.getLine(pos.line);
+ const char = line.charAt(wordObj.end.ch);
+ const charsNext = line.charAt(wordObj.end.ch + 1);
+
+ if (char === '}' || char === ']') {
+ wordObj.end.ch += 1;
+ }
+
+ // sometimes at the end we get `"]` as extra with some abbreviations.
+ if (char === '"' && charsNext && charsNext === ']') {
+ wordObj.end.ch += 2;
+ }
+
+ }
+
+ // Replace the abbreviation
+ editor.document.replaceRange(
+ indentedAbbr,
+ wordObj.start,
+ wordObj.end
+ );
+
+ // Calculate and set the new cursor position
+ const cursorPos = findCursorPosition(editor, indentedAbbr, wordObj.start);
+ if (cursorPos) {
+ editor.setCursorPos(cursorPos.line, cursorPos.ch);
+ }
+ }
+
+
+ /**
+ * This function checks whether the abbreviation can be expanded or not.
+ * There are a lot of cases to check:
+ * There should not be any negative symbols
+ * The abbr should be either in htmlTags or in markupSnippetsList
+ * For other cases such as 'ul>li', we will check if there is any,
+ * positive word. This is done to handle complex abbreviations such as,
+ * 'ul>li' or 'li*3{Hello}'. So we check if the word includes any positive symbols.
+ *
+ * @param {Editor} editor - the editor instance
+ * @param {String} word - the abbr
+ * @returns {String | false} - returns the expanded abbr, and if cannot be expanded, returns false
+ */
+ function isExpandable(editor, word) {
+ const pos = editor.getCursorPos();
+ const line = editor.document.getLine(pos.line);
+
+ // to prevent hints from appearing in line. Also to prevent hints from appearing in comments
+ if(line.includes(' word.includes(symbol))) {
+ return false;
+ }
+
+ // the word must be either in markupSnippetsList, htmlList or it must have a positive symbol
+ // convert to lowercase only for `htmlTags` because HTML tag names are case-insensitive,
+ // but `markupSnippetsList` expands abbreviations in a non-tag manner,
+ // where the expanded abbreviation is already in lowercase.
+ if (markupSnippetsList.includes(word) ||
+ htmlTags.includes(word.toLowerCase()) ||
+ positiveSymbols.some(symbol => word.includes(symbol))) {
+
+ try {
+ const expanded = EXPAND_ABBR(word, { syntax: "html", type: "markup" });
+ return expanded;
+ } catch (error) {
+
+ // emmet api throws an error when abbr contains unclosed quotes, handling that case
+ const nextChar = line.charAt(pos.ch);
+
+ if (nextChar) {
+ // If the next character is a quote, add quote to abbr
+ if (nextChar === '"' || nextChar === "'") {
+ const modifiedWord = word + nextChar;
+
+ try {
+ const expandedModified = EXPAND_ABBR(modifiedWord, { syntax: "html", type: "markup" });
+ return expandedModified;
+ } catch (innerError) {
+ // If it still fails, return false
+ return false;
+ }
+ }
+ }
+
+ // If no quote is found or expansion fails, return false
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+
/**
* @constructor
*/
@@ -733,6 +1202,14 @@ define(function (require, exports, module) {
});
};
+ /**
+ * Checks for preference changes, to enable/disable Emmet
+ */
+ function preferenceChanged() {
+ enabled = PreferencesManager.get(AllPreferences.EMMET);
+ }
+
+
AppInit.appReady(function () {
// Parse JSON files
tags = JSON.parse(HTMLTags);
@@ -746,7 +1223,14 @@ define(function (require, exports, module) {
CodeHintManager.registerHintProvider(attrHints, ["html"], 0);
NewFileContentManager.registerContentProvider(newDocContentProvider, ["html"], 0);
+ PreferencesManager.on("change", AllPreferences.EMMET, preferenceChanged);
+ preferenceChanged();
+
+ var emmetMarkupHints = new EmmetMarkupHints();
+ CodeHintManager.registerHintProvider(emmetMarkupHints, ["html", "php", "jsp"], 0);
+
// For unit testing
+ exports.emmetHintProvider = emmetMarkupHints;
exports.tagHintProvider = tagHints;
exports.attrHintProvider = attrHints;
});
diff --git a/src/extensions/default/HTMLCodeHints/unittests.js b/src/extensions/default/HTMLCodeHints/unittests.js
index 16406741b2..d53fadf725 100644
--- a/src/extensions/default/HTMLCodeHints/unittests.js
+++ b/src/extensions/default/HTMLCodeHints/unittests.js
@@ -94,6 +94,14 @@ define(function (require, exports, module) {
expect(hintList[0]).toBe(expectedFirstHint);
}
+ // Helper function for testing cursor position
+ function fixPos(pos) {
+ if (!("sticky" in pos)) {
+ pos.sticky = null;
+ }
+ return pos;
+ }
+
describe("Tag hint provider", function () {
@@ -685,5 +693,127 @@ define(function (require, exports, module) {
});
+ describe("Emmet hint provider", function () {
+
+ it("should display emmet hint and expand to boilerplate code on ! press", function () {
+
+ let emmetBoilerPlate = [
+ "",
+ "",
+ "",
+ " ",
+ " ",
+ " Document",
+ "",
+ "",
+ " ",
+ "",
+ ""
+ ];
+
+ testDocument.setText("!");
+ testEditor.setCursorPos({ line: 0, ch: 1 });
+ const hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ // get the hintText from the code hint
+ const hintText = hints[0][0].textContent;
+ expect(hintText).toBe("!Emmet"); // this should be same as the setText plus the Emmet
+
+ // also test after inserting the hint
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+
+ for(let i = 0; i <= 10; i++) {
+ expect(testDocument.getLine(i)).toBe(emmetBoilerPlate[i]);
+ }
+
+ // make sure the cursor is between the body tag
+ expect(fixPos(testEditor.getCursorPos())).toEql(fixPos({line: 8, ch: 1}));
+ });
+
+
+ it("should display emmet hint and expand to doctype html initial line on !!! press", function () {
+
+ let emmetBoilerPlate = "";
+
+ testDocument.setText("!!!");
+ testEditor.setCursorPos({ line: 0, ch: 3 });
+ const hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ const hintText = hints[0][0].textContent;
+ expect(hintText).toBe("!!!Emmet");
+
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+ expect(testDocument.getLine(0)).toBe(emmetBoilerPlate);
+
+ expect(fixPos(testEditor.getCursorPos())).toEql(fixPos({line: 0, ch: 15}));
+ });
+
+ it("should not display hints when two or more than three exclamation marks are present", function () {
+ testDocument.setText("!!");
+ testEditor.setCursorPos({ line: 0, ch: 2 });
+ expectNoHints(HTMLCodeHints.emmetHintProvider);
+
+ testDocument.setText("!!!!");
+ testEditor.setCursorPos({ line: 0, ch: 4 });
+ expectNoHints(HTMLCodeHints.emmetHintProvider);
+ });
+
+ it("should not display emmet hints on < key press", function () {
+ testDocument.setText("<");
+ testEditor.setCursorPos({ line: 0, ch: 1 });
+ expectNoHints(HTMLCodeHints.emmetHintProvider);
+ });
+
+ it("should add class name id name if abbr contains . and #", function () {
+ testDocument.setText("div.hello#world");
+ testEditor.setCursorPos({ line: 0, ch: 15 });
+
+ const hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ const hintText = hints[0][0].textContent;
+ expect(hintText).toBe("div.hello#worldEmmet");
+
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+ expect(testDocument.getLine(0)).toBe("");
+
+ expect(fixPos(testEditor.getCursorPos())).toEql(fixPos({line: 0, ch: 30}));
+ });
+
+ it("./# should expand to a div with empty class/id name and set cursor in between quotes", function() {
+ testDocument.setText(".");
+ testEditor.setCursorPos({ line: 0, ch: 1 });
+ let hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+ expect(fixPos(testEditor.getCursorPos())).toEql(fixPos({line: 0, ch: 12}));
+
+ testDocument.setText("#");
+ testEditor.setCursorPos({ line: 0, ch: 1 });
+ hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+ expect(fixPos(testEditor.getCursorPos())).toEql(fixPos({line: 0, ch: 9}));
+ });
+
+ it("should expand emmet snippet with * and {}", function() {
+ const emmetSnippetResult = "\n" +
+ " - hello world
\n" +
+ " - hello world
\n" +
+ " - hello world
\n" +
+ " - hello world
\n" +
+ "
";
+ testDocument.setText("ul>li*4{hello world}");
+ testEditor.setCursorPos({ line: 0, ch: 19 });
+ const hints = expectHints(HTMLCodeHints.emmetHintProvider);
+
+ const hintText = hints[0][0].textContent;
+ expect(hintText).toBe("ul>li*4{hello world}Emmet");
+
+ HTMLCodeHints.emmetHintProvider.insertHint(hints[0]);
+ expect(testDocument.getText()).toBe(emmetSnippetResult);
+ });
+ });
+
+
}); // describe("HTML Code Hinting"
});
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index 19df7702f1..47d48061f1 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -1264,6 +1264,9 @@ define({
"DESCRIPTION_HIDE_FIRST": "true to show the first Indent Guide line else false.",
"DESCRIPTION_CSS_COLOR_PREVIEW": "true to display color previews in the gutter, else false.",
+ // Emmet
+ "DESCRIPTION_EMMET": "true to enable Emmet, else false.",
+
// Git extension
"ENABLE_GIT": "Enable Git",
"ACTION": "Action",
@@ -1537,4 +1540,4 @@ define({
// surveys
"SURVEY_TITLE_VOTE_FOR_FEATURES_YOU_WANT": "Vote for the features you want to see next!"
-});
+});
\ No newline at end of file
diff --git a/src/preferences/AllPreferences.js b/src/preferences/AllPreferences.js
new file mode 100644
index 0000000000..4664246d0c
--- /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;
+});
\ No newline at end of file
diff --git a/src/styles/brackets_core_ui_variables.less b/src/styles/brackets_core_ui_variables.less
index 9326487c3b..49dee4f002 100644
--- a/src/styles/brackets_core_ui_variables.less
+++ b/src/styles/brackets_core_ui_variables.less
@@ -41,7 +41,7 @@
*/
-:root {
+ :root {
--bc-toast-danger-bg-color: #FF5C33;
--bc-toast-error-bg-color: #f74687;
--bc-toast-success-bg-color: #82b839;
@@ -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;
\ No newline at end of file
diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less
index eda14f4ef5..23e42dc6b9 100644
--- a/src/styles/brackets_patterns_override.less
+++ b/src/styles/brackets_patterns_override.less
@@ -703,9 +703,39 @@ 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.85em;
+ font-weight: @font-weight-semibold;
+ right: 4px;
+ bottom: 0px;
+ color: @css-codehint-icon !important;
+ .dark& {
+ color: @dark-css-codehint-icon !important;
}
}
diff --git a/test/SpecRunner.js b/test/SpecRunner.js
index 0e0bf45447..bcb7fcbf2e 100644
--- a/test/SpecRunner.js
+++ b/test/SpecRunner.js
@@ -248,6 +248,7 @@ define(function (require, exports, module) {
require("worker/ExtensionsWorker");
require("thirdparty/tinycolor");
require("widgets/NotificationUI");
+ require("preferences/AllPreferences");
// Load modules that self-register and just need to get included in the test-runner window
require("document/ChangedDocumentTracker");