monaco-editor-core
Version:
A browser based code editor
426 lines (425 loc) • 20.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var LightBulbWidget_1;
import * as dom from '../../../../base/browser/dom.js';
import { Gesture } from '../../../../base/browser/touch.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import './lightBulbWidget.css';
import { GlyphMarginLane } from '../../../common/model.js';
import { ModelDecorationOptions } from '../../../common/model/textModel.js';
import { computeIndentLevel } from '../../../common/model/utils.js';
import { autoFixCommandId, quickFixCommandId } from './codeAction.js';
import * as nls from '../../../../nls.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js';
import { Range } from '../../../common/core/range.js';
const GUTTER_LIGHTBULB_ICON = registerIcon('gutter-lightbulb', Codicon.lightBulb, nls.localize('gutterLightbulbWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor.'));
const GUTTER_LIGHTBULB_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-auto-fix', Codicon.lightbulbAutofix, nls.localize('gutterLightbulbAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and a quick fix is available.'));
const GUTTER_LIGHTBULB_AIFIX_ICON = registerIcon('gutter-lightbulb-sparkle', Codicon.lightbulbSparkle, nls.localize('gutterLightbulbAIFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix is available.'));
const GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-aifix-auto-fix', Codicon.lightbulbSparkleAutofix, nls.localize('gutterLightbulbAIFixAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.'));
const GUTTER_SPARKLE_FILLED_ICON = registerIcon('gutter-lightbulb-sparkle-filled', Codicon.sparkleFilled, nls.localize('gutterLightbulbSparkleFilledWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.'));
var LightBulbState;
(function (LightBulbState) {
LightBulbState.Hidden = { type: 0 /* Type.Hidden */ };
class Showing {
constructor(actions, trigger, editorPosition, widgetPosition) {
this.actions = actions;
this.trigger = trigger;
this.editorPosition = editorPosition;
this.widgetPosition = widgetPosition;
this.type = 1 /* Type.Showing */;
}
}
LightBulbState.Showing = Showing;
})(LightBulbState || (LightBulbState = {}));
let LightBulbWidget = class LightBulbWidget extends Disposable {
static { LightBulbWidget_1 = this; }
static { this.GUTTER_DECORATION = ModelDecorationOptions.register({
description: 'codicon-gutter-lightbulb-decoration',
glyphMarginClassName: ThemeIcon.asClassName(Codicon.lightBulb),
glyphMargin: { position: GlyphMarginLane.Left },
stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
}); }
static { this.ID = 'editor.contrib.lightbulbWidget'; }
static { this._posPref = [0 /* ContentWidgetPositionPreference.EXACT */]; }
constructor(_editor, _keybindingService) {
super();
this._editor = _editor;
this._keybindingService = _keybindingService;
this._onClick = this._register(new Emitter());
this.onClick = this._onClick.event;
this._state = LightBulbState.Hidden;
this._gutterState = LightBulbState.Hidden;
this._iconClasses = [];
this.lightbulbClasses = [
'codicon-' + GUTTER_LIGHTBULB_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AUTO_FIX_ICON.id,
'codicon-' + GUTTER_LIGHTBULB_AIFIX_ICON.id,
'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id
];
this.gutterDecoration = LightBulbWidget_1.GUTTER_DECORATION;
this._domNode = dom.$('div.lightBulbWidget');
this._domNode.role = 'listbox';
this._register(Gesture.ignoreTarget(this._domNode));
this._editor.addContentWidget(this);
this._register(this._editor.onDidChangeModelContent(_ => {
// cancel when the line in question has been removed
const editorModel = this._editor.getModel();
if (this.state.type !== 1 /* LightBulbState.Type.Showing */ || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) {
this.hide();
}
if (this.gutterState.type !== 1 /* LightBulbState.Type.Showing */ || !editorModel || this.gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) {
this.gutterHide();
}
}));
this._register(dom.addStandardDisposableGenericMouseDownListener(this._domNode, e => {
if (this.state.type !== 1 /* LightBulbState.Type.Showing */) {
return;
}
// Make sure that focus / cursor location is not lost when clicking widget icon
this._editor.focus();
e.preventDefault();
// a bit of extra work to make sure the menu
// doesn't cover the line-text
const { top, height } = dom.getDomNodePagePosition(this._domNode);
const lineHeight = this._editor.getOption(67 /* EditorOption.lineHeight */);
let pad = Math.floor(lineHeight / 3);
if (this.state.widgetPosition.position !== null && this.state.widgetPosition.position.lineNumber < this.state.editorPosition.lineNumber) {
pad += lineHeight;
}
this._onClick.fire({
x: e.posx,
y: top + height + pad,
actions: this.state.actions,
trigger: this.state.trigger,
});
}));
this._register(dom.addDisposableListener(this._domNode, 'mouseenter', (e) => {
if ((e.buttons & 1) !== 1) {
return;
}
// mouse enters lightbulb while the primary/left button
// is being pressed -> hide the lightbulb
this.hide();
}));
this._register(Event.runAndSubscribe(this._keybindingService.onDidUpdateKeybindings, () => {
this._preferredKbLabel = this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined;
this._quickFixKbLabel = this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined;
this._updateLightBulbTitleAndIcon();
}));
this._register(this._editor.onMouseDown(async (e) => {
if (!e.target.element || !this.lightbulbClasses.some(cls => e.target.element && e.target.element.classList.contains(cls))) {
return;
}
if (this.gutterState.type !== 1 /* LightBulbState.Type.Showing */) {
return;
}
// Make sure that focus / cursor location is not lost when clicking widget icon
this._editor.focus();
// a bit of extra work to make sure the menu
// doesn't cover the line-text
const { top, height } = dom.getDomNodePagePosition(e.target.element);
const lineHeight = this._editor.getOption(67 /* EditorOption.lineHeight */);
let pad = Math.floor(lineHeight / 3);
if (this.gutterState.widgetPosition.position !== null && this.gutterState.widgetPosition.position.lineNumber < this.gutterState.editorPosition.lineNumber) {
pad += lineHeight;
}
this._onClick.fire({
x: e.event.posx,
y: top + height + pad,
actions: this.gutterState.actions,
trigger: this.gutterState.trigger,
});
}));
}
dispose() {
super.dispose();
this._editor.removeContentWidget(this);
if (this._gutterDecorationID) {
this._removeGutterDecoration(this._gutterDecorationID);
}
}
getId() {
return 'LightBulbWidget';
}
getDomNode() {
return this._domNode;
}
getPosition() {
return this._state.type === 1 /* LightBulbState.Type.Showing */ ? this._state.widgetPosition : null;
}
update(actions, trigger, atPosition) {
if (actions.validActions.length <= 0) {
this.gutterHide();
return this.hide();
}
const hasTextFocus = this._editor.hasTextFocus();
if (!hasTextFocus) {
this.gutterHide();
return this.hide();
}
const options = this._editor.getOptions();
if (!options.get(65 /* EditorOption.lightbulb */).enabled) {
this.gutterHide();
return this.hide();
}
const model = this._editor.getModel();
if (!model) {
this.gutterHide();
return this.hide();
}
const { lineNumber, column } = model.validatePosition(atPosition);
const tabSize = model.getOptions().tabSize;
const fontInfo = this._editor.getOptions().get(50 /* EditorOption.fontInfo */);
const lineContent = model.getLineContent(lineNumber);
const indent = computeIndentLevel(lineContent, tabSize);
const lineHasSpace = fontInfo.spaceWidth * indent > 22;
const isFolded = (lineNumber) => {
return lineNumber > 2 && this._editor.getTopForLineNumber(lineNumber) === this._editor.getTopForLineNumber(lineNumber - 1);
};
// Check for glyph margin decorations of any kind
const currLineDecorations = this._editor.getLineDecorations(lineNumber);
let hasDecoration = false;
if (currLineDecorations) {
for (const decoration of currLineDecorations) {
const glyphClass = decoration.options.glyphMarginClassName;
if (glyphClass && !this.lightbulbClasses.some(className => glyphClass.includes(className))) {
hasDecoration = true;
break;
}
}
}
let effectiveLineNumber = lineNumber;
let effectiveColumnNumber = 1;
if (!lineHasSpace) {
// Checks if line is empty or starts with any amount of whitespace
const isLineEmptyOrIndented = (lineNumber) => {
const lineContent = model.getLineContent(lineNumber);
return /^\s*$|^\s+/.test(lineContent) || lineContent.length <= effectiveColumnNumber;
};
if (lineNumber > 1 && !isFolded(lineNumber - 1)) {
const lineCount = model.getLineCount();
const endLine = lineNumber === lineCount;
const prevLineEmptyOrIndented = lineNumber > 1 && isLineEmptyOrIndented(lineNumber - 1);
const nextLineEmptyOrIndented = !endLine && isLineEmptyOrIndented(lineNumber + 1);
const currLineEmptyOrIndented = isLineEmptyOrIndented(lineNumber);
const notEmpty = !nextLineEmptyOrIndented && !prevLineEmptyOrIndented;
// check above and below. if both are blocked, display lightbulb in the gutter.
if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) {
this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, {
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
preference: LightBulbWidget_1._posPref
});
this.renderGutterLightbub();
return this.hide();
}
else if (prevLineEmptyOrIndented || endLine || (prevLineEmptyOrIndented && !currLineEmptyOrIndented)) {
effectiveLineNumber -= 1;
}
else if (nextLineEmptyOrIndented || (notEmpty && currLineEmptyOrIndented)) {
effectiveLineNumber += 1;
}
}
else if (lineNumber === 1 && (lineNumber === model.getLineCount() || !isLineEmptyOrIndented(lineNumber + 1) && !isLineEmptyOrIndented(lineNumber))) {
// special checks for first line blocked vs. not blocked.
this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, {
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
preference: LightBulbWidget_1._posPref
});
if (hasDecoration) {
this.gutterHide();
}
else {
this.renderGutterLightbub();
return this.hide();
}
}
else if ((lineNumber < model.getLineCount()) && !isFolded(lineNumber + 1)) {
effectiveLineNumber += 1;
}
else if (column * fontInfo.spaceWidth < 22) {
// cannot show lightbulb above/below and showing
// it inline would overlay the cursor...
return this.hide();
}
effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1;
}
this.state = new LightBulbState.Showing(actions, trigger, atPosition, {
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
preference: LightBulbWidget_1._posPref
});
if (this._gutterDecorationID) {
this._removeGutterDecoration(this._gutterDecorationID);
this.gutterHide();
}
const validActions = actions.validActions;
const actionKind = actions.validActions[0].action.kind;
if (validActions.length !== 1 || !actionKind) {
this._editor.layoutContentWidget(this);
return;
}
this._editor.layoutContentWidget(this);
}
hide() {
if (this.state === LightBulbState.Hidden) {
return;
}
this.state = LightBulbState.Hidden;
this._editor.layoutContentWidget(this);
}
gutterHide() {
if (this.gutterState === LightBulbState.Hidden) {
return;
}
if (this._gutterDecorationID) {
this._removeGutterDecoration(this._gutterDecorationID);
}
this.gutterState = LightBulbState.Hidden;
}
get state() { return this._state; }
set state(value) {
this._state = value;
this._updateLightBulbTitleAndIcon();
}
get gutterState() { return this._gutterState; }
set gutterState(value) {
this._gutterState = value;
this._updateGutterLightBulbTitleAndIcon();
}
_updateLightBulbTitleAndIcon() {
this._domNode.classList.remove(...this._iconClasses);
this._iconClasses = [];
if (this.state.type !== 1 /* LightBulbState.Type.Showing */) {
return;
}
let icon;
let autoRun = false;
if (this.state.actions.allAIFixes) {
icon = Codicon.sparkleFilled;
if (this.state.actions.validActions.length === 1) {
autoRun = true;
}
}
else if (this.state.actions.hasAutoFix) {
if (this.state.actions.hasAIFix) {
icon = Codicon.lightbulbSparkleAutofix;
}
else {
icon = Codicon.lightbulbAutofix;
}
}
else if (this.state.actions.hasAIFix) {
icon = Codicon.lightbulbSparkle;
}
else {
icon = Codicon.lightBulb;
}
this._updateLightbulbTitle(this.state.actions.hasAutoFix, autoRun);
this._iconClasses = ThemeIcon.asClassNameArray(icon);
this._domNode.classList.add(...this._iconClasses);
}
_updateGutterLightBulbTitleAndIcon() {
if (this.gutterState.type !== 1 /* LightBulbState.Type.Showing */) {
return;
}
let icon;
let autoRun = false;
if (this.gutterState.actions.allAIFixes) {
icon = GUTTER_SPARKLE_FILLED_ICON;
if (this.gutterState.actions.validActions.length === 1) {
autoRun = true;
}
}
else if (this.gutterState.actions.hasAutoFix) {
if (this.gutterState.actions.hasAIFix) {
icon = GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON;
}
else {
icon = GUTTER_LIGHTBULB_AUTO_FIX_ICON;
}
}
else if (this.gutterState.actions.hasAIFix) {
icon = GUTTER_LIGHTBULB_AIFIX_ICON;
}
else {
icon = GUTTER_LIGHTBULB_ICON;
}
this._updateLightbulbTitle(this.gutterState.actions.hasAutoFix, autoRun);
const GUTTER_DECORATION = ModelDecorationOptions.register({
description: 'codicon-gutter-lightbulb-decoration',
glyphMarginClassName: ThemeIcon.asClassName(icon),
glyphMargin: { position: GlyphMarginLane.Left },
stickiness: 1 /* TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges */,
});
this.gutterDecoration = GUTTER_DECORATION;
}
/* Gutter Helper Functions */
renderGutterLightbub() {
const selection = this._editor.getSelection();
if (!selection) {
return;
}
if (this._gutterDecorationID === undefined) {
this._addGutterDecoration(selection.startLineNumber);
}
else {
this._updateGutterDecoration(this._gutterDecorationID, selection.startLineNumber);
}
}
_addGutterDecoration(lineNumber) {
this._editor.changeDecorations((accessor) => {
this._gutterDecorationID = accessor.addDecoration(new Range(lineNumber, 0, lineNumber, 0), this.gutterDecoration);
});
}
_removeGutterDecoration(decorationId) {
this._editor.changeDecorations((accessor) => {
accessor.removeDecoration(decorationId);
this._gutterDecorationID = undefined;
});
}
_updateGutterDecoration(decorationId, lineNumber) {
this._editor.changeDecorations((accessor) => {
accessor.changeDecoration(decorationId, new Range(lineNumber, 0, lineNumber, 0));
accessor.changeDecorationOptions(decorationId, this.gutterDecoration);
});
}
_updateLightbulbTitle(autoFix, autoRun) {
if (this.state.type !== 1 /* LightBulbState.Type.Showing */) {
return;
}
if (autoRun) {
this.title = nls.localize('codeActionAutoRun', "Run: {0}", this.state.actions.validActions[0].action.title);
}
else if (autoFix && this._preferredKbLabel) {
this.title = nls.localize('preferredcodeActionWithKb', "Show Code Actions. Preferred Quick Fix Available ({0})", this._preferredKbLabel);
}
else if (!autoFix && this._quickFixKbLabel) {
this.title = nls.localize('codeActionWithKb', "Show Code Actions ({0})", this._quickFixKbLabel);
}
else if (!autoFix) {
this.title = nls.localize('codeAction', "Show Code Actions");
}
}
set title(value) {
this._domNode.title = value;
}
};
LightBulbWidget = LightBulbWidget_1 = __decorate([
__param(1, IKeybindingService)
], LightBulbWidget);
export { LightBulbWidget };