mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Added Icons and Header Separators to Code Action Widget (ref #132109)
* added disabled hover * code cleanup on disabled option hovers * removed comments * widget enabled by default * code cleanup and fix on build * clean up on css removed unused importants * small patch for css rules * minor refactor on codeactionitems * fix on disabled option click * fix on disabled option click * added some icons but just temp * added iconws and modified widget look * added beginning logic for menu groupings * looks pretty good for a menu wooo * added headers to menu + removed extra text from option labels * minor code cleanup on group filtering * Refactoring on code action kind * changed styling based on feedback * code cleanup * First couple of fixes on PR for code action kinds * modified icons and refactoring * removed extra push * removed parsing and added code action kind for surround
This commit is contained in:
parent
abc84e0735
commit
8e456bd7ee
|
@ -30,6 +30,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
|||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded
|
||||
import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export const Context = {
|
||||
Visible: new RawContextKey<boolean>('CodeActionMenuVisible', false, localize('CodeActionMenuVisible', "Whether the code action list widget is visible"))
|
||||
|
@ -67,6 +70,8 @@ export interface ICodeActionMenuItem {
|
|||
isSeparator: boolean;
|
||||
isEnabled: boolean;
|
||||
isDocumentation: boolean;
|
||||
isHeader: boolean;
|
||||
headerTitle: string;
|
||||
index: number;
|
||||
disposables?: IDisposable[];
|
||||
}
|
||||
|
@ -85,10 +90,12 @@ export interface ICodeActionMenuTemplateData {
|
|||
detail: HTMLElement;
|
||||
decoratorRight: HTMLElement;
|
||||
disposables: IDisposable[];
|
||||
icon: HTMLElement;
|
||||
}
|
||||
|
||||
const TEMPLATE_ID = 'codeActionWidget';
|
||||
const codeActionLineHeight = 26;
|
||||
const codeActionLineHeight = 24;
|
||||
const headerLineHeight = 26;
|
||||
|
||||
class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeActionMenuTemplateData> {
|
||||
|
||||
|
@ -104,52 +111,86 @@ class CodeMenuRenderer implements IListRenderer<ICodeActionMenuItem, ICodeAction
|
|||
data.disposables = [];
|
||||
data.root = container;
|
||||
data.text = document.createElement('span');
|
||||
// data.detail = document.createElement('');
|
||||
|
||||
const iconContainer = document.createElement('div');
|
||||
iconContainer.className = 'icon-container';
|
||||
|
||||
data.icon = document.createElement('div');
|
||||
|
||||
iconContainer.append(data.icon);
|
||||
container.append(iconContainer);
|
||||
container.append(data.text);
|
||||
// container.append(data.detail);
|
||||
|
||||
return data;
|
||||
}
|
||||
renderElement(element: ICodeActionMenuItem, index: number, templateData: ICodeActionMenuTemplateData): void {
|
||||
const data: ICodeActionMenuTemplateData = templateData;
|
||||
const text = element.action.label;
|
||||
|
||||
const isSeparator = element.isSeparator;
|
||||
|
||||
element.isEnabled = element.action.enabled;
|
||||
|
||||
if (element.action instanceof CodeActionAction) {
|
||||
|
||||
// Check documentation type
|
||||
element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
|
||||
if (!element.isDocumentation) {
|
||||
|
||||
// Check if action has disabled reason
|
||||
if (element.action.action.disabled) {
|
||||
data.root.title = element.action.action.disabled;
|
||||
} else {
|
||||
const updateLabel = () => {
|
||||
const [accept, preview] = this.acceptKeybindings;
|
||||
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());
|
||||
};
|
||||
updateLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.text.textContent = text;
|
||||
|
||||
if (!element.isEnabled) {
|
||||
data.root.classList.add('option-disabled');
|
||||
data.root.style.backgroundColor = 'transparent !important';
|
||||
} else {
|
||||
data.root.classList.remove('option-disabled');
|
||||
}
|
||||
const isHeader = element.isHeader;
|
||||
|
||||
if (isSeparator) {
|
||||
data.root.classList.add('separator');
|
||||
data.root.style.height = '10px';
|
||||
} else if (isHeader) {
|
||||
const text = element.headerTitle;
|
||||
data.text.textContent = text;
|
||||
element.isEnabled = false;
|
||||
data.root.classList.add('group-header');
|
||||
} else {
|
||||
const text = element.action.label;
|
||||
data.text.textContent = text;
|
||||
element.isEnabled = element.action.enabled;
|
||||
|
||||
if (element.action instanceof CodeActionAction) {
|
||||
|
||||
// Check documentation type
|
||||
element.isDocumentation = element.action.action.kind === CodeActionMenu.documentationID;
|
||||
|
||||
if (element.isDocumentation) {
|
||||
data.text.textContent = text;
|
||||
data.root.classList.add('documentation');
|
||||
} else {
|
||||
// Icons and Label modifaction based on group
|
||||
const group = element.action.action.kind;
|
||||
|
||||
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(group)))) {
|
||||
data.icon.className = Codicon.symbolArray.classNames;
|
||||
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(group)))) {
|
||||
data.icon.className = Codicon.wrench.classNames;
|
||||
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(group)))) {
|
||||
data.icon.className = Codicon.zap.classNames;
|
||||
data.icon.style.color = `var(--vscode-editorLightBulbAutoFix-foreground)`;
|
||||
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(group)))) {
|
||||
data.icon.className = Codicon.lightBulb.classNames;
|
||||
data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`;
|
||||
} else {
|
||||
data.icon.className = Codicon.lightBulb.classNames;
|
||||
data.icon.style.color = `var(--vscode-editorLightBulb-foreground)`;
|
||||
}
|
||||
|
||||
// Check if action has disabled reason
|
||||
if (element.action.action.disabled) {
|
||||
data.root.title = element.action.action.disabled;
|
||||
} else {
|
||||
const updateLabel = () => {
|
||||
const [accept, preview] = this.acceptKeybindings;
|
||||
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());
|
||||
};
|
||||
updateLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!element.isEnabled) {
|
||||
data.root.classList.add('option-disabled');
|
||||
data.root.style.backgroundColor = 'transparent !important';
|
||||
data.icon.style.opacity = '0.4';
|
||||
} else {
|
||||
data.root.classList.remove('option-disabled');
|
||||
}
|
||||
}
|
||||
disposeTemplate(templateData: ICodeActionMenuTemplateData): void {
|
||||
templateData.disposables = dispose(templateData.disposables);
|
||||
|
@ -275,13 +316,19 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
|
|||
getHeight(element) {
|
||||
if (element.isSeparator) {
|
||||
return 10;
|
||||
} else if (element.isHeader) {
|
||||
return headerLineHeight;
|
||||
}
|
||||
return codeActionLineHeight;
|
||||
},
|
||||
getTemplateId(element) {
|
||||
return 'codeActionWidget';
|
||||
}
|
||||
}, [this.listRenderer], { keyboardSupport: false }
|
||||
}, [this.listRenderer],
|
||||
{
|
||||
keyboardSupport: false,
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
const pointerBlockDiv = document.createElement('div');
|
||||
|
@ -305,28 +352,105 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
|
|||
renderDisposables.add(this.codeActionList.value.onDidChangeSelection(e => this._onListSelection(e)));
|
||||
renderDisposables.add(this._editor.onDidLayoutChange(e => this.hideCodeActionWidget()));
|
||||
|
||||
// Populating the list widget and tracking enabled options.
|
||||
// Filters and groups code actions by their group
|
||||
const menuEntries: IAction[][] = [];
|
||||
|
||||
// Code Action Groups
|
||||
const quickfixGroup: IAction[] = [];
|
||||
const extractGroup: IAction[] = [];
|
||||
const convertGroup: IAction[] = [];
|
||||
const surroundGroup: IAction[] = [];
|
||||
const sourceGroup: IAction[] = [];
|
||||
const separatorGroup: IAction[] = [];
|
||||
const documentationGroup: IAction[] = [];
|
||||
const otherGroup: IAction[] = [];
|
||||
|
||||
inputArray.forEach((item, index) => {
|
||||
if (item instanceof CodeActionAction) {
|
||||
const optionKind = item.action.kind;
|
||||
|
||||
const currIsSeparator = item.class === 'separator';
|
||||
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(optionKind)))) {
|
||||
surroundGroup.push(item);
|
||||
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(optionKind)))) {
|
||||
quickfixGroup.push(item);
|
||||
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(optionKind)))) {
|
||||
extractGroup.push(item);
|
||||
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(optionKind)))) {
|
||||
convertGroup.push(item);
|
||||
} else if (CodeActionKind.Source.contains(new CodeActionKind(String(optionKind)))) {
|
||||
sourceGroup.push(item);
|
||||
} else if (optionKind === CodeActionMenu.documentationID) {
|
||||
documentationGroup.push(item);
|
||||
} else {
|
||||
otherGroup.push(item);
|
||||
}
|
||||
|
||||
if (currIsSeparator) {
|
||||
// set to true forever because there is a separator
|
||||
this.hasSeparator = true;
|
||||
} else if (item.id === `vs.actions.separator`) {
|
||||
separatorGroup.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
menuEntries.push(quickfixGroup, extractGroup, convertGroup, surroundGroup, sourceGroup, otherGroup, separatorGroup, documentationGroup);
|
||||
|
||||
const menuEntriesToPush = (menuID: string, entry: IAction[]) => {
|
||||
totalActionEntries.push(menuID);
|
||||
totalActionEntries.push(...entry);
|
||||
numHeaders++;
|
||||
};
|
||||
// Creates flat list of all menu entries with headers as separators
|
||||
let numHeaders = 0;
|
||||
const totalActionEntries: (IAction | string)[] = [];
|
||||
menuEntries.forEach(entry => {
|
||||
if (entry.length > 0 && entry[0] instanceof CodeActionAction) {
|
||||
const firstAction = entry[0].action.kind;
|
||||
if (CodeActionKind.SurroundWith.contains(new CodeActionKind(String(firstAction)))) {
|
||||
menuEntriesToPush(localize('codeAction.widget.id.surround', 'Surround With ...'), entry);
|
||||
} else if (CodeActionKind.QuickFix.contains(new CodeActionKind(String(firstAction)))) {
|
||||
menuEntriesToPush(localize('codeAction.widget.id.quickfix', 'Quick Fix ...'), entry);
|
||||
} else if (CodeActionKind.Extract.contains(new CodeActionKind(String(firstAction)))) {
|
||||
menuEntriesToPush(localize('codeAction.widget.id.extract', 'Extract ...'), entry);
|
||||
} else if (CodeActionKind.Convert.contains(new CodeActionKind(String(firstAction)))) {
|
||||
menuEntriesToPush(localize('codeAction.widget.id.convert', 'Convert ...'), entry);
|
||||
} else if (CodeActionKind.Source.contains(new CodeActionKind(String(firstAction)))) {
|
||||
menuEntriesToPush(localize('codeAction.widget.id.source', 'Source Action ...'), entry);
|
||||
} else if (firstAction === CodeActionMenu.documentationID) {
|
||||
totalActionEntries.push(...entry);
|
||||
}
|
||||
} else {
|
||||
// case for separator - not a code action action
|
||||
totalActionEntries.push(...entry);
|
||||
}
|
||||
|
||||
const menuItem = <ICodeActionMenuItem>{ action: inputArray[index], isEnabled: item.enabled, isSeparator: currIsSeparator, index };
|
||||
if (item.enabled) {
|
||||
this.viewItems.push(menuItem);
|
||||
});
|
||||
|
||||
// Populating the list widget and tracking enabled options.
|
||||
totalActionEntries.forEach((item, index) => {
|
||||
if (typeof item === `string`) {
|
||||
const menuItem = <ICodeActionMenuItem>{ isEnabled: false, isSeparator: false, index, isHeader: true, headerTitle: item };
|
||||
this.options.push(menuItem);
|
||||
} else {
|
||||
const currIsSeparator = item.class === 'separator';
|
||||
|
||||
if (currIsSeparator) {
|
||||
// set to true forever because there is a separator
|
||||
this.hasSeparator = true;
|
||||
}
|
||||
|
||||
const menuItem = <ICodeActionMenuItem>{ action: item, isEnabled: item.enabled, isSeparator: currIsSeparator, index };
|
||||
if (item.enabled) {
|
||||
this.viewItems.push(menuItem);
|
||||
}
|
||||
this.options.push(menuItem);
|
||||
}
|
||||
this.options.push(menuItem);
|
||||
});
|
||||
|
||||
this.codeActionList.value.splice(0, this.codeActionList.value.length, this.options);
|
||||
|
||||
const height = this.hasSeparator ? (inputArray.length - 1) * codeActionLineHeight + 10 : inputArray.length * codeActionLineHeight;
|
||||
renderMenu.style.height = String(height) + 'px';
|
||||
this.codeActionList.value.layout(height);
|
||||
// Updating list height, depending on how many separators and headers there are.
|
||||
const height = this.hasSeparator ? (totalActionEntries.length - 1) * codeActionLineHeight + 10 : totalActionEntries.length * codeActionLineHeight;
|
||||
const heightWithHeaders = height + numHeaders * headerLineHeight - numHeaders * codeActionLineHeight;
|
||||
renderMenu.style.height = String(heightWithHeaders) + 'px';
|
||||
this.codeActionList.value.layout(heightWithHeaders);
|
||||
|
||||
// For finding width dynamically (not using resize observer)
|
||||
const arr: number[] = [];
|
||||
|
@ -341,9 +465,9 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
|
|||
// resize observer - can be used in the future since list widget supports dynamic height but not width
|
||||
const maxWidth = Math.max(...arr);
|
||||
|
||||
// 40 is the additional padding for the list widget (20 left, 20 right)
|
||||
renderMenu.style.width = maxWidth + 52 + 'px';
|
||||
this.codeActionList.value?.layout(height, maxWidth);
|
||||
// 52 is the additional padding for the list widget (26 left, 26 right)
|
||||
renderMenu.style.width = maxWidth + 52 + 5 + 'px';
|
||||
this.codeActionList.value?.layout(heightWithHeaders, maxWidth);
|
||||
|
||||
// List selection
|
||||
if (this.viewItems.length < 1 || this.viewItems.every(item => item.isDocumentation)) {
|
||||
|
@ -359,7 +483,6 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
|
|||
const focusTracker = dom.trackFocus(element);
|
||||
const blurListener = focusTracker.onDidBlur(() => {
|
||||
this.hideCodeActionWidget();
|
||||
// this._contextViewService.hideContextView({ source: this });
|
||||
});
|
||||
renderDisposables.add(blurListener);
|
||||
renderDisposables.add(focusTracker);
|
||||
|
@ -468,6 +591,7 @@ export class CodeActionMenu extends Disposable implements IEditorContribution {
|
|||
return;
|
||||
}
|
||||
const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
|
||||
|
||||
if (!actionsToShow.length) {
|
||||
this._visible = false;
|
||||
return;
|
||||
|
|
|
@ -4,17 +4,17 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.codeActionMenuWidget {
|
||||
padding: 8px 0px 8px 0px;
|
||||
padding: 0px 1px 3px 0px;
|
||||
overflow: auto;
|
||||
font-size: 13px;
|
||||
border-radius: 5px;
|
||||
border-radius: 0px;
|
||||
min-width: 160px;
|
||||
z-index: 40;
|
||||
display: block;
|
||||
/* flex-direction: column;
|
||||
flex: 0 1 auto; */
|
||||
width: 100%;
|
||||
border-width: 0px;
|
||||
border: 1px solid var(--vscode-menu-separatorBackground);
|
||||
border-color: none;
|
||||
background-color: var(--vscode-menu-background);
|
||||
color: var(--vscode-menu-foreground);
|
||||
|
@ -57,7 +57,7 @@
|
|||
display: flex;
|
||||
-mox-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 26px 0px 26px;
|
||||
padding: 0px 26px 0px 10px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px 2px;
|
||||
white-space: nowrap;
|
||||
|
@ -89,6 +89,21 @@
|
|||
outline: 0px solid !important;
|
||||
}
|
||||
|
||||
.codeActionMenuWidget .monaco-list .monaco-list-row.group-header {
|
||||
padding: 3px 10px 0px 10px;
|
||||
}
|
||||
|
||||
.codeActionMenuWidget .monaco-list .monaco-list-row.documentation {
|
||||
padding: 0px 10px 0px 10px;
|
||||
}
|
||||
|
||||
|
||||
.codeActionMenuWidget .monaco-list .group-header.option-disabled {
|
||||
color: var(--vscode-textLink-activeForeground) !important;
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.codeActionMenuWidget .monaco-list .separator {
|
||||
border-bottom: 1px solid var(--vscode-menu-separatorBackground);
|
||||
padding-top: 0px !important;
|
||||
|
@ -108,3 +123,13 @@
|
|||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.codeActionMenuWidget .monaco-list-row:not(.group-header):not(.documentation) .icon-container {
|
||||
margin-top: 3px;
|
||||
margin-right: 10px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
flex-shrink: 0;
|
||||
color: var(--vscode-editorLightBulb-foreground);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,12 @@ export class CodeActionKind {
|
|||
public static readonly Empty = new CodeActionKind('');
|
||||
public static readonly QuickFix = new CodeActionKind('quickfix');
|
||||
public static readonly Refactor = new CodeActionKind('refactor');
|
||||
public static readonly Extract = CodeActionKind.Refactor.append('extract');
|
||||
public static readonly Convert = CodeActionKind.Refactor.append('rewrite');
|
||||
public static readonly Source = new CodeActionKind('source');
|
||||
public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports');
|
||||
public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll');
|
||||
public static readonly SurroundWith = CodeActionKind.Refactor.append('surround');
|
||||
|
||||
constructor(
|
||||
public readonly value: string
|
||||
|
|
|
@ -24,7 +24,7 @@ class SurroundWithSnippetCodeActionProvider implements CodeActionProvider {
|
|||
private static readonly _MAX_CODE_ACTIONS = 4;
|
||||
|
||||
private static readonly _overflowCommandCodeAction: CodeAction = {
|
||||
kind: CodeActionKind.Refactor.value,
|
||||
kind: CodeActionKind.SurroundWith.value,
|
||||
title: SurroundWithSnippetEditorAction.options.title.value,
|
||||
command: {
|
||||
id: SurroundWithSnippetEditorAction.options.id,
|
||||
|
@ -54,7 +54,7 @@ class SurroundWithSnippetCodeActionProvider implements CodeActionProvider {
|
|||
}
|
||||
actions.push({
|
||||
title: localize('codeAction', "Surround With: {0}", snippet.name),
|
||||
kind: CodeActionKind.Refactor.value,
|
||||
kind: CodeActionKind.SurroundWith.value,
|
||||
edit: asWorkspaceEdit(model, range, snippet)
|
||||
});
|
||||
}
|
||||
|
@ -72,14 +72,14 @@ class FileTemplateCodeActionProvider implements CodeActionProvider {
|
|||
|
||||
private static readonly _overflowCommandCodeAction: CodeAction = {
|
||||
title: localize('overflow.start.title', 'Start with Snippet'),
|
||||
kind: CodeActionKind.Refactor.value,
|
||||
kind: CodeActionKind.SurroundWith.value,
|
||||
command: {
|
||||
id: ApplyFileSnippetAction.Id,
|
||||
title: ''
|
||||
}
|
||||
};
|
||||
|
||||
readonly providedCodeActionKinds?: readonly string[] = [CodeActionKind.Refactor.value];
|
||||
readonly providedCodeActionKinds?: readonly string[] = [CodeActionKind.SurroundWith.value];
|
||||
|
||||
constructor(@ISnippetsService private readonly _snippetService: ISnippetsService) { }
|
||||
|
||||
|
@ -97,7 +97,7 @@ class FileTemplateCodeActionProvider implements CodeActionProvider {
|
|||
}
|
||||
actions.push({
|
||||
title: localize('title', 'Start with: {0}', snippet.name),
|
||||
kind: CodeActionKind.Refactor.value,
|
||||
kind: CodeActionKind.SurroundWith.value,
|
||||
edit: asWorkspaceEdit(model, model.getFullModelRange(), snippet)
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue