@@ -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 } ) ;
0 commit comments