@@ -30,6 +30,8 @@ define(function (require, exports, module) {
3030 PreferencesManager = brackets . getModule ( "preferences/PreferencesManager" ) ,
3131 Strings = brackets . getModule ( "strings" ) ,
3232 NewFileContentManager = brackets . getModule ( "features/NewFileContentManager" ) ,
33+ CSSUtils = brackets . getModule ( "language/CSSUtils" ) ,
34+ StringMatch = brackets . getModule ( "utils/StringMatch" ) ,
3335 HTMLTags = require ( "text!HtmlTags.json" ) ,
3436 HTMLAttributes = require ( "text!HtmlAttributes.json" ) ,
3537 HTMLTemplate = require ( "text!template.html" ) ,
@@ -219,6 +221,52 @@ define(function (require, exports, module) {
219221 } ) ;
220222 } ;
221223
224+ const MAX_CLASS_HINTS = 250 ;
225+ function formatHints ( hints ) {
226+ StringMatch . basicMatchSort ( hints ) ;
227+ if ( hints . length > MAX_CLASS_HINTS ) {
228+ hints = hints . splice ( 0 , MAX_CLASS_HINTS ) ;
229+ }
230+ return hints . map ( function ( token ) {
231+ let $hintObj = $ ( "<span>" ) . addClass ( "brackets-html-hints brackets-hints" ) ;
232+
233+ // highlight the matched portion of each hint
234+ if ( token . stringRanges ) {
235+ token . stringRanges . forEach ( function ( item ) {
236+ if ( item . matched ) {
237+ $hintObj . append ( $ ( "<span>" )
238+ . text ( item . text )
239+ . addClass ( "matched-hint" ) ) ;
240+ } else {
241+ $hintObj . append ( item . text ) ;
242+ }
243+ } ) ;
244+ } else {
245+ $hintObj . text ( token . label ) ;
246+ }
247+ $hintObj . attr ( "data-val" , token . label ) ;
248+ return $hintObj ;
249+ } ) ;
250+ }
251+
252+ function _getAllClassHints ( query ) {
253+ let queryStr = query . queryStr ;
254+ // "class1 class2" have multiple classes. the last part is the query to hint
255+ const segments = queryStr . split ( " " ) ;
256+ queryStr = segments [ segments . length - 1 ] ;
257+ const deferred = $ . Deferred ( ) ;
258+ CSSUtils . getAllCssSelectorsInProject ( { includeClasses : true } ) . then ( hints => {
259+ const result = $ . map ( hints , function ( pvalue ) {
260+ pvalue = pvalue . slice ( 1 ) ; // remove.
261+ return StringMatch . stringMatch ( pvalue , queryStr , { preferPrefixMatches : true } ) ;
262+ } ) ;
263+ const validHints = formatHints ( result ) ;
264+ validHints . alreadyMatched = true ;
265+ deferred . resolve ( validHints ) ;
266+ } ) . catch ( console . error ) ;
267+ return deferred ;
268+ }
269+
222270 /**
223271 * Helper function that determines the possible value hints for a given html tag/attribute name pair
224272 *
@@ -242,6 +290,10 @@ define(function (require, exports, module) {
242290 // "script/type", "link/type" and "button/type".
243291 var hints = [ ] ;
244292
293+ if ( attrName === "class" ) {
294+ return _getAllClassHints ( query ) ;
295+ }
296+
245297 var tagPlusAttr = tagName + "/" + attrName ,
246298 attrInfo = attributes [ tagPlusAttr ] || attributes [ attrName ] ;
247299
@@ -326,10 +378,10 @@ define(function (require, exports, module) {
326378 }
327379
328380 // If we're at an attribute value, check if it's an attribute name that has hintable values.
329- if ( this . tagInfo . attr . name ) {
330- var hints = this . _getValueHintsForAttr ( { queryStr : query } ,
331- this . tagInfo . tagName ,
332- this . tagInfo . attr . name ) ;
381+ const attrName = this . tagInfo . attr . name ;
382+ if ( attrName && attrName !== "class" ) { // class hints are always computed later
383+ let hints = this . _getValueHintsForAttr ( { queryStr : query } ,
384+ this . tagInfo . tagName , attrName ) ;
333385 if ( hints instanceof Array ) {
334386 // If we got synchronous hints, check if we have something we'll actually use
335387 var i , foundPrefix = false ;
@@ -452,7 +504,7 @@ define(function (require, exports, module) {
452504 hints . done ( function ( asyncHints ) {
453505 deferred . resolveWith ( this , [ {
454506 hints : asyncHints ,
455- match : query . queryStr ,
507+ match : asyncHints . alreadyMatched ? null : query . queryStr ,
456508 selectInitial : true ,
457509 handleWideResults : false
458510 } ] ) ;
@@ -487,6 +539,7 @@ define(function (require, exports, module) {
487539 replaceExistingOne = this . tagInfo . attr . valueAssigned ,
488540 endQuote = "" ,
489541 shouldReplace = true ,
542+ positionWithinAttributeVal = false ,
490543 textAfterCursor ;
491544
492545 if ( tokenType === HTMLUtils . ATTR_NAME ) {
@@ -518,6 +571,18 @@ define(function (require, exports, module) {
518571 charCount = this . tagInfo . attr . value . length ;
519572 }
520573
574+ if ( this . tagInfo . attr . name === "class" ) {
575+ // css class hints
576+ completion = completion . data ( "val" ) ;
577+ // "anotherClass class<cursor>name" . completion = classics , we have to match a prefix after space
578+ const textBeforeCursor = this . tagInfo . attr . value . slice ( 0 , offset ) ;
579+ let lastSegment = textBeforeCursor . split ( " " ) ;
580+ lastSegment = lastSegment [ lastSegment . length - 1 ] ;
581+ offset = lastSegment . length ;
582+ charCount = offset ;
583+ positionWithinAttributeVal = true ;
584+ }
585+
521586 if ( ! this . tagInfo . attr . hasEndQuote ) {
522587 endQuote = this . tagInfo . attr . quoteChar ;
523588 if ( endQuote ) {
@@ -542,7 +607,10 @@ define(function (require, exports, module) {
542607 }
543608 }
544609
545- if ( insertedName ) {
610+ if ( positionWithinAttributeVal ) {
611+ this . editor . setCursorPos ( start . line , start . ch + completion . length ) ;
612+ // we're now inside the double-quotes we just inserted
613+ } else if ( insertedName ) {
546614 this . editor . setCursorPos ( start . line , start . ch + completion . length - 1 ) ;
547615
548616 // Since we're now inside the double-quotes we just inserted,
0 commit comments