@@ -30,6 +30,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
30
30
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem' ;
31
31
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry' ;
32
32
import { IThemeService } from 'vs/platform/theme/common/themeService' ;
33
+ import 'vs/base/browser/ui/codicons/codiconStyles' ; // The codicon symbol styles are defined here and must be loaded
34
+ import 'vs/editor/contrib/symbolIcons/browser/symbolIcons' ; // The codicon symbol colors are defined here and must be loaded to get colors
35
+ import { Codicon } from 'vs/base/common/codicons' ;
33
36
34
37
export const Context = {
35
38
Visible : new RawContextKey < boolean > ( 'CodeActionMenuVisible' , false , localize ( 'CodeActionMenuVisible' , "Whether the code action list widget is visible" ) )
@@ -67,6 +70,8 @@ export interface ICodeActionMenuItem {
67
70
isSeparator : boolean ;
68
71
isEnabled : boolean ;
69
72
isDocumentation : boolean ;
73
+ isHeader : boolean ;
74
+ headerTitle : string ;
70
75
index : number ;
71
76
disposables ?: IDisposable [ ] ;
72
77
}
@@ -85,10 +90,12 @@ export interface ICodeActionMenuTemplateData {
85
90
detail : HTMLElement ;
86
91
decoratorRight : HTMLElement ;
87
92
disposables : IDisposable [ ] ;
93
+ icon : HTMLElement ;
88
94
}
89
95
90
96
const TEMPLATE_ID = 'codeActionWidget' ;
91
- const codeActionLineHeight = 26 ;
97
+ const codeActionLineHeight = 24 ;
98
+ const headerLineHeight = 26 ;
92
99
93
100
class CodeMenuRenderer implements IListRenderer < ICodeActionMenuItem , ICodeActionMenuTemplateData > {
94
101
@@ -104,52 +111,86 @@ class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeAction
104
111
data . disposables = [ ] ;
105
112
data . root = container ;
106
113
data . text = document . createElement ( 'span' ) ;
107
- // data.detail = document.createElement('');
114
+
115
+ const iconContainer = document . createElement ( 'div' ) ;
116
+ iconContainer . className = 'icon-container' ;
117
+
118
+ data . icon = document . createElement ( 'div' ) ;
119
+
120
+ iconContainer . append ( data . icon ) ;
121
+ container . append ( iconContainer ) ;
108
122
container . append ( data . text ) ;
109
- // container.append(data.detail);
110
123
111
124
return data ;
112
125
}
113
126
renderElement ( element : ICodeActionMenuItem , index : number , templateData : ICodeActionMenuTemplateData ) : void {
114
127
const data : ICodeActionMenuTemplateData = templateData ;
115
- const text = element . action . label ;
128
+
116
129
const isSeparator = element . isSeparator ;
130
+ const isHeader = element . isHeader ;
117
131
118
- element . isEnabled = element . action . enabled ;
132
+ if ( isSeparator ) {
133
+ data . root . classList . add ( 'separator' ) ;
134
+ data . root . style . height = '10px' ;
135
+ } else if ( isHeader ) {
136
+ const text = element . headerTitle ;
137
+ data . text . textContent = text ;
138
+ element . isEnabled = false ;
139
+ data . root . classList . add ( 'group-header' ) ;
140
+ } else {
141
+ const text = element . action . label ;
142
+ data . text . textContent = text ;
143
+ element . isEnabled = element . action . enabled ;
119
144
120
- if ( element . action instanceof CodeActionAction ) {
145
+ if ( element . action instanceof CodeActionAction ) {
121
146
122
- // Check documentation type
123
- element . isDocumentation = element . action . action . kind === CodeActionMenu . documentationID ;
124
- if ( ! element . isDocumentation ) {
147
+ // Check documentation type
148
+ element . isDocumentation = element . action . action . kind === CodeActionMenu . documentationID ;
125
149
126
- // Check if action has disabled reason
127
- if ( element . action . action . disabled ) {
128
- data . root . title = element . action . action . disabled ;
150
+ if ( element . isDocumentation ) {
151
+ data . text . textContent = text ;
152
+ data . root . classList . add ( 'documentation' ) ;
129
153
} else {
130
- const updateLabel = ( ) => {
131
- const [ accept , preview ] = this . acceptKeybindings ;
132
- data . root . title = localize ( { key : 'label' , comment : [ 'placeholders are keybindings, e.g "F2 to Refactor, Shift+F2 to Preview"' ] } , "{0} to Refactor, {1} to Preview" , this . keybindingService . lookupKeybinding ( accept ) ?. getLabel ( ) , this . keybindingService . lookupKeybinding ( preview ) ?. getLabel ( ) ) ;
133
- } ;
134
- updateLabel ( ) ;
154
+ // Icons and Label modifaction based on group
155
+ const group = element . action . action . kind ;
156
+
157
+ if ( CodeActionKind . SurroundWith . contains ( new CodeActionKind ( String ( group ) ) ) ) {
158
+ data . icon . className = Codicon . symbolArray . classNames ;
159
+ } else if ( CodeActionKind . Extract . contains ( new CodeActionKind ( String ( group ) ) ) ) {
160
+ data . icon . className = Codicon . wrench . classNames ;
161
+ } else if ( CodeActionKind . Convert . contains ( new CodeActionKind ( String ( group ) ) ) ) {
162
+ data . icon . className = Codicon . zap . classNames ;
163
+ data . icon . style . color = `var(--vscode-editorLightBulbAutoFix-foreground)` ;
164
+ } else if ( CodeActionKind . QuickFix . contains ( new CodeActionKind ( String ( group ) ) ) ) {
165
+ data . icon . className = Codicon . lightBulb . classNames ;
166
+ data . icon . style . color = `var(--vscode-editorLightBulb-foreground)` ;
167
+ } else {
168
+ data . icon . className = Codicon . lightBulb . classNames ;
169
+ data . icon . style . color = `var(--vscode-editorLightBulb-foreground)` ;
170
+ }
171
+
172
+ // Check if action has disabled reason
173
+ if ( element . action . action . disabled ) {
174
+ data . root . title = element . action . action . disabled ;
175
+ } else {
176
+ const updateLabel = ( ) => {
177
+ const [ accept , preview ] = this . acceptKeybindings ;
178
+ data . root . title = localize ( { key : 'label' , comment : [ 'placeholders are keybindings, e.g "F2 to Refactor, Shift+F2 to Preview"' ] } , "{0} to Refactor, {1} to Preview" , this . keybindingService . lookupKeybinding ( accept ) ?. getLabel ( ) , this . keybindingService . lookupKeybinding ( preview ) ?. getLabel ( ) ) ;
179
+ } ;
180
+ updateLabel ( ) ;
181
+ }
135
182
}
136
183
}
137
- }
138
184
139
- data . text . textContent = text ;
185
+ }
140
186
141
187
if ( ! element . isEnabled ) {
142
188
data . root . classList . add ( 'option-disabled' ) ;
143
189
data . root . style . backgroundColor = 'transparent !important' ;
190
+ data . icon . style . opacity = '0.4' ;
144
191
} else {
145
192
data . root . classList . remove ( 'option-disabled' ) ;
146
193
}
147
-
148
- if ( isSeparator ) {
149
- data . root . classList . add ( 'separator' ) ;
150
- data . root . style . height = '10px' ;
151
- }
152
-
153
194
}
154
195
disposeTemplate ( templateData : ICodeActionMenuTemplateData ) : void {
155
196
templateData . disposables = dispose ( templateData . disposables ) ;
@@ -275,13 +316,19 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
275
316
getHeight ( element ) {
276
317
if ( element . isSeparator ) {
277
318
return 10 ;
319
+ } else if ( element . isHeader ) {
320
+ return headerLineHeight ;
278
321
}
279
322
return codeActionLineHeight ;
280
323
} ,
281
324
getTemplateId ( element ) {
282
325
return 'codeActionWidget' ;
283
326
}
284
- } , [ this . listRenderer ] , { keyboardSupport : false }
327
+ } , [ this . listRenderer ] ,
328
+ {
329
+ keyboardSupport : false ,
330
+
331
+ }
285
332
) ;
286
333
287
334
const pointerBlockDiv = document . createElement ( 'div' ) ;
@@ -305,28 +352,105 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
305
352
renderDisposables . add ( this . codeActionList . value . onDidChangeSelection ( e => this . _onListSelection ( e ) ) ) ;
306
353
renderDisposables . add ( this . _editor . onDidLayoutChange ( e => this . hideCodeActionWidget ( ) ) ) ;
307
354
308
- // Populating the list widget and tracking enabled options.
355
+ // Filters and groups code actions by their group
356
+ const menuEntries : IAction [ ] [ ] = [ ] ;
357
+
358
+ // Code Action Groups
359
+ const quickfixGroup : IAction [ ] = [ ] ;
360
+ const extractGroup : IAction [ ] = [ ] ;
361
+ const convertGroup : IAction [ ] = [ ] ;
362
+ const surroundGroup : IAction [ ] = [ ] ;
363
+ const sourceGroup : IAction [ ] = [ ] ;
364
+ const separatorGroup : IAction [ ] = [ ] ;
365
+ const documentationGroup : IAction [ ] = [ ] ;
366
+ const otherGroup : IAction [ ] = [ ] ;
367
+
309
368
inputArray . forEach ( ( item , index ) => {
369
+ if ( item instanceof CodeActionAction ) {
370
+ const optionKind = item . action . kind ;
371
+
372
+ if ( CodeActionKind . SurroundWith . contains ( new CodeActionKind ( String ( optionKind ) ) ) ) {
373
+ surroundGroup . push ( item ) ;
374
+ } else if ( CodeActionKind . QuickFix . contains ( new CodeActionKind ( String ( optionKind ) ) ) ) {
375
+ quickfixGroup . push ( item ) ;
376
+ } else if ( CodeActionKind . Extract . contains ( new CodeActionKind ( String ( optionKind ) ) ) ) {
377
+ extractGroup . push ( item ) ;
378
+ } else if ( CodeActionKind . Convert . contains ( new CodeActionKind ( String ( optionKind ) ) ) ) {
379
+ convertGroup . push ( item ) ;
380
+ } else if ( CodeActionKind . Source . contains ( new CodeActionKind ( String ( optionKind ) ) ) ) {
381
+ sourceGroup . push ( item ) ;
382
+ } else if ( optionKind === CodeActionMenu . documentationID ) {
383
+ documentationGroup . push ( item ) ;
384
+ } else {
385
+ otherGroup . push ( item ) ;
386
+ }
310
387
311
- const currIsSeparator = item . class === 'separator' ;
388
+ } else if ( item . id === `vs.actions.separator` ) {
389
+ separatorGroup . push ( item ) ;
390
+ }
391
+ } ) ;
392
+
393
+ menuEntries . push ( quickfixGroup , extractGroup , convertGroup , surroundGroup , sourceGroup , otherGroup , separatorGroup , documentationGroup ) ;
312
394
313
- if ( currIsSeparator ) {
314
- // set to true forever because there is a separator
315
- this . hasSeparator = true ;
395
+ const menuEntriesToPush = ( menuID : string , entry : IAction [ ] ) => {
396
+ totalActionEntries . push ( menuID ) ;
397
+ totalActionEntries . push ( ...entry ) ;
398
+ numHeaders ++ ;
399
+ } ;
400
+ // Creates flat list of all menu entries with headers as separators
401
+ let numHeaders = 0 ;
402
+ const totalActionEntries : ( IAction | string ) [ ] = [ ] ;
403
+ menuEntries . forEach ( entry => {
404
+ if ( entry . length > 0 && entry [ 0 ] instanceof CodeActionAction ) {
405
+ const firstAction = entry [ 0 ] . action . kind ;
406
+ if ( CodeActionKind . SurroundWith . contains ( new CodeActionKind ( String ( firstAction ) ) ) ) {
407
+ menuEntriesToPush ( localize ( 'codeAction.widget.id.surround' , 'Surround With ...' ) , entry ) ;
408
+ } else if ( CodeActionKind . QuickFix . contains ( new CodeActionKind ( String ( firstAction ) ) ) ) {
409
+ menuEntriesToPush ( localize ( 'codeAction.widget.id.quickfix' , 'Quick Fix ...' ) , entry ) ;
410
+ } else if ( CodeActionKind . Extract . contains ( new CodeActionKind ( String ( firstAction ) ) ) ) {
411
+ menuEntriesToPush ( localize ( 'codeAction.widget.id.extract' , 'Extract ...' ) , entry ) ;
412
+ } else if ( CodeActionKind . Convert . contains ( new CodeActionKind ( String ( firstAction ) ) ) ) {
413
+ menuEntriesToPush ( localize ( 'codeAction.widget.id.convert' , 'Convert ...' ) , entry ) ;
414
+ } else if ( CodeActionKind . Source . contains ( new CodeActionKind ( String ( firstAction ) ) ) ) {
415
+ menuEntriesToPush ( localize ( 'codeAction.widget.id.source' , 'Source Action ...' ) , entry ) ;
416
+ } else if ( firstAction === CodeActionMenu . documentationID ) {
417
+ totalActionEntries . push ( ...entry ) ;
418
+ }
419
+ } else {
420
+ // case for separator - not a code action action
421
+ totalActionEntries . push ( ...entry ) ;
316
422
}
317
423
318
- const menuItem = < ICodeActionMenuItem > { action : inputArray [ index ] , isEnabled : item . enabled , isSeparator : currIsSeparator , index } ;
319
- if ( item . enabled ) {
320
- this . viewItems . push ( menuItem ) ;
424
+ } ) ;
425
+
426
+ // Populating the list widget and tracking enabled options.
427
+ totalActionEntries . forEach ( ( item , index ) => {
428
+ if ( typeof item === `string` ) {
429
+ const menuItem = < ICodeActionMenuItem > { isEnabled : false , isSeparator : false , index, isHeader : true , headerTitle : item } ;
430
+ this . options . push ( menuItem ) ;
431
+ } else {
432
+ const currIsSeparator = item . class === 'separator' ;
433
+
434
+ if ( currIsSeparator ) {
435
+ // set to true forever because there is a separator
436
+ this . hasSeparator = true ;
437
+ }
438
+
439
+ const menuItem = < ICodeActionMenuItem > { action : item , isEnabled : item . enabled , isSeparator : currIsSeparator , index } ;
440
+ if ( item . enabled ) {
441
+ this . viewItems . push ( menuItem ) ;
442
+ }
443
+ this . options . push ( menuItem ) ;
321
444
}
322
- this . options . push ( menuItem ) ;
323
445
} ) ;
324
446
325
447
this . codeActionList . value . splice ( 0 , this . codeActionList . value . length , this . options ) ;
326
448
327
- const height = this . hasSeparator ? ( inputArray . length - 1 ) * codeActionLineHeight + 10 : inputArray . length * codeActionLineHeight ;
328
- renderMenu . style . height = String ( height ) + 'px' ;
329
- this . codeActionList . value . layout ( height ) ;
449
+ // Updating list height, depending on how many separators and headers there are.
450
+ const height = this . hasSeparator ? ( totalActionEntries . length - 1 ) * codeActionLineHeight + 10 : totalActionEntries . length * codeActionLineHeight ;
451
+ const heightWithHeaders = height + numHeaders * headerLineHeight - numHeaders * codeActionLineHeight ;
452
+ renderMenu . style . height = String ( heightWithHeaders ) + 'px' ;
453
+ this . codeActionList . value . layout ( heightWithHeaders ) ;
330
454
331
455
// For finding width dynamically (not using resize observer)
332
456
const arr : number [ ] = [ ] ;
@@ -341,9 +465,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
341
465
// resize observer - can be used in the future since list widget supports dynamic height but not width
342
466
const maxWidth = Math . max ( ...arr ) ;
343
467
344
- // 40 is the additional padding for the list widget (20 left, 20 right)
345
- renderMenu . style . width = maxWidth + 52 + 'px' ;
346
- this . codeActionList . value ?. layout ( height , maxWidth ) ;
468
+ // 52 is the additional padding for the list widget (26 left, 26 right)
469
+ renderMenu . style . width = maxWidth + 52 + 5 + 'px' ;
470
+ this . codeActionList . value ?. layout ( heightWithHeaders , maxWidth ) ;
347
471
348
472
// List selection
349
473
if ( this . viewItems . length < 1 || this . viewItems . every ( item => item . isDocumentation ) ) {
@@ -359,7 +483,6 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
359
483
const focusTracker = dom . trackFocus ( element ) ;
360
484
const blurListener = focusTracker . onDidBlur ( ( ) => {
361
485
this . hideCodeActionWidget ( ) ;
362
- // this._contextViewService.hideContextView({ source: this });
363
486
} ) ;
364
487
renderDisposables . add ( blurListener ) ;
365
488
renderDisposables . add ( focusTracker ) ;
@@ -468,6 +591,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
468
591
return ;
469
592
}
470
593
const actionsToShow = options . includeDisabledActions ? codeActions . allActions : codeActions . validActions ;
594
+
471
595
if ( ! actionsToShow . length ) {
472
596
this . _visible = false ;
473
597
return ;
0 commit comments