UNPKG

monaco-editor-core

Version:

A browser based code editor

588 lines (587 loc) 28.1 kB
/*--------------------------------------------------------------------------------------------- * 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 InlayHintsController_1; import { isHTMLElement, ModifierKeyEmitter } from '../../../../base/browser/dom.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { DynamicCssRules } from '../../../browser/editorDom.js'; import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js'; import { EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Range } from '../../../common/core/range.js'; import * as languages from '../../../common/languages.js'; import { InjectedTextCursorStops } from '../../../common/model.js'; import { ModelDecorationInjectedTextOptions } from '../../../common/model/textModel.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { ITextModelService } from '../../../common/services/resolverService.js'; import { ClickLinkGesture } from '../../gotoSymbol/browser/link/clickLinkGesture.js'; import { InlayHintAnchor, InlayHintsFragments } from './inlayHints.js'; import { goToDefinitionWithLocation, showGoToContextMenu } from './inlayHintsLocations.js'; import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import * as colors from '../../../../platform/theme/common/colorRegistry.js'; import { themeColorFromId } from '../../../../platform/theme/common/themeService.js'; // --- hint caching service (per session) class InlayHintsCache { constructor() { this._entries = new LRUCache(50); } get(model) { const key = InlayHintsCache._key(model); return this._entries.get(key); } set(model, value) { const key = InlayHintsCache._key(model); this._entries.set(key, value); } static _key(model) { return `${model.uri.toString()}/${model.getVersionId()}`; } } const IInlayHintsCache = createDecorator('IInlayHintsCache'); registerSingleton(IInlayHintsCache, InlayHintsCache, 1 /* InstantiationType.Delayed */); // --- rendered label export class RenderedInlayHintLabelPart { constructor(item, index) { this.item = item; this.index = index; } get part() { const label = this.item.hint.label; if (typeof label === 'string') { return { label }; } else { return label[this.index]; } } } class ActiveInlayHintInfo { constructor(part, hasTriggerModifier) { this.part = part; this.hasTriggerModifier = hasTriggerModifier; } } // --- controller let InlayHintsController = class InlayHintsController { static { InlayHintsController_1 = this; } static { this.ID = 'editor.contrib.InlayHints'; } static { this._MAX_DECORATORS = 1500; } static { this._MAX_LABEL_LEN = 43; } static get(editor) { return editor.getContribution(InlayHintsController_1.ID) ?? undefined; } constructor(_editor, _languageFeaturesService, _featureDebounce, _inlayHintsCache, _commandService, _notificationService, _instaService) { this._editor = _editor; this._languageFeaturesService = _languageFeaturesService; this._inlayHintsCache = _inlayHintsCache; this._commandService = _commandService; this._notificationService = _notificationService; this._instaService = _instaService; this._disposables = new DisposableStore(); this._sessionDisposables = new DisposableStore(); this._decorationsMetadata = new Map(); this._ruleFactory = new DynamicCssRules(this._editor); this._activeRenderMode = 0 /* RenderMode.Normal */; this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 }); this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update())); this._disposables.add(_editor.onDidChangeModel(() => this._update())); this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); this._disposables.add(_editor.onDidChangeConfiguration(e => { if (e.hasChanged(142 /* EditorOption.inlayHints */)) { this._update(); } })); this._update(); } dispose() { this._sessionDisposables.dispose(); this._removeAllDecorations(); this._disposables.dispose(); } _update() { this._sessionDisposables.clear(); this._removeAllDecorations(); const options = this._editor.getOption(142 /* EditorOption.inlayHints */); if (options.enabled === 'off') { return; } const model = this._editor.getModel(); if (!model || !this._languageFeaturesService.inlayHintsProvider.has(model)) { return; } if (options.enabled === 'on') { // different "on" modes: always this._activeRenderMode = 0 /* RenderMode.Normal */; } else { // different "on" modes: offUnlessPressed, or onUnlessPressed let defaultMode; let altMode; if (options.enabled === 'onUnlessPressed') { defaultMode = 0 /* RenderMode.Normal */; altMode = 1 /* RenderMode.Invisible */; } else { defaultMode = 1 /* RenderMode.Invisible */; altMode = 0 /* RenderMode.Normal */; } this._activeRenderMode = defaultMode; this._sessionDisposables.add(ModifierKeyEmitter.getInstance().event(e => { if (!this._editor.hasModel()) { return; } const newRenderMode = e.altKey && e.ctrlKey && !(e.shiftKey || e.metaKey) ? altMode : defaultMode; if (newRenderMode !== this._activeRenderMode) { this._activeRenderMode = newRenderMode; const model = this._editor.getModel(); const copies = this._copyInlayHintsWithCurrentAnchor(model); this._updateHintsDecorators([model.getFullModelRange()], copies); scheduler.schedule(0); } })); } // iff possible, quickly update from cache const cached = this._inlayHintsCache.get(model); if (cached) { this._updateHintsDecorators([model.getFullModelRange()], cached); } this._sessionDisposables.add(toDisposable(() => { // cache items when switching files etc if (!model.isDisposed()) { this._cacheHintsForFastRestore(model); } })); let cts; const watchedProviders = new Set(); const scheduler = new RunOnceScheduler(async () => { const t1 = Date.now(); cts?.dispose(true); cts = new CancellationTokenSource(); const listener = model.onWillDispose(() => cts?.cancel()); try { const myToken = cts.token; const inlayHints = await InlayHintsFragments.create(this._languageFeaturesService.inlayHintsProvider, model, this._getHintsRanges(), myToken); scheduler.delay = this._debounceInfo.update(model, Date.now() - t1); if (myToken.isCancellationRequested) { inlayHints.dispose(); return; } // listen to provider changes for (const provider of inlayHints.provider) { if (typeof provider.onDidChangeInlayHints === 'function' && !watchedProviders.has(provider)) { watchedProviders.add(provider); this._sessionDisposables.add(provider.onDidChangeInlayHints(() => { if (!scheduler.isScheduled()) { // ignore event when request is already scheduled scheduler.schedule(); } })); } } this._sessionDisposables.add(inlayHints); this._updateHintsDecorators(inlayHints.ranges, inlayHints.items); this._cacheHintsForFastRestore(model); } catch (err) { onUnexpectedError(err); } finally { cts.dispose(); listener.dispose(); } }, this._debounceInfo.get(model)); this._sessionDisposables.add(scheduler); this._sessionDisposables.add(toDisposable(() => cts?.dispose(true))); scheduler.schedule(0); this._sessionDisposables.add(this._editor.onDidScrollChange((e) => { // update when scroll position changes // uses scrollTopChanged has weak heuristic to differenatiate between scrolling due to // typing or due to "actual" scrolling if (e.scrollTopChanged || !scheduler.isScheduled()) { scheduler.schedule(); } })); this._sessionDisposables.add(this._editor.onDidChangeModelContent((e) => { cts?.cancel(); // update less aggressive when typing const delay = Math.max(scheduler.delay, 1250); scheduler.schedule(delay); })); // mouse gestures this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0))); this._sessionDisposables.add(this._installLinkGesture()); this._sessionDisposables.add(this._installContextMenu()); } _installLinkGesture() { const store = new DisposableStore(); const gesture = store.add(new ClickLinkGesture(this._editor)); // let removeHighlight = () => { }; const sessionStore = new DisposableStore(); store.add(sessionStore); store.add(gesture.onMouseMoveOrRelevantKeyDown(e => { const [mouseEvent] = e; const labelPart = this._getInlayHintLabelPart(mouseEvent); const model = this._editor.getModel(); if (!labelPart || !model) { sessionStore.clear(); return; } // resolve the item const cts = new CancellationTokenSource(); sessionStore.add(toDisposable(() => cts.dispose(true))); labelPart.item.resolve(cts.token); // render link => when the modifier is pressed and when there is a command or location this._activeInlayHintPart = labelPart.part.command || labelPart.part.location ? new ActiveInlayHintInfo(labelPart, mouseEvent.hasTriggerModifier) : undefined; const lineNumber = model.validatePosition(labelPart.item.hint.position).lineNumber; const range = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); const lineHints = this._getInlineHintsForRange(range); this._updateHintsDecorators([range], lineHints); sessionStore.add(toDisposable(() => { this._activeInlayHintPart = undefined; this._updateHintsDecorators([range], lineHints); })); })); store.add(gesture.onCancel(() => sessionStore.clear())); store.add(gesture.onExecute(async (e) => { const label = this._getInlayHintLabelPart(e); if (label) { const part = label.part; if (part.location) { // location -> execute go to def this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor, part.location); } else if (languages.Command.is(part.command)) { // command -> execute it await this._invokeCommand(part.command, label.item); } } })); return store; } _getInlineHintsForRange(range) { const lineHints = new Set(); for (const data of this._decorationsMetadata.values()) { if (range.containsRange(data.item.anchor.range)) { lineHints.add(data.item); } } return Array.from(lineHints); } _installDblClickGesture(updateInlayHints) { return this._editor.onMouseUp(async (e) => { if (e.event.detail !== 2) { return; } const part = this._getInlayHintLabelPart(e); if (!part) { return; } e.event.preventDefault(); await part.item.resolve(CancellationToken.None); if (isNonEmptyArray(part.item.hint.textEdits)) { const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); this._editor.executeEdits('inlayHint.default', edits); updateInlayHints(); } }); } _installContextMenu() { return this._editor.onContextMenu(async (e) => { if (!(isHTMLElement(e.event.target))) { return; } const part = this._getInlayHintLabelPart(e); if (part) { await this._instaService.invokeFunction(showGoToContextMenu, this._editor, e.event.target, part); } }); } _getInlayHintLabelPart(e) { if (e.target.type !== 6 /* MouseTargetType.CONTENT_TEXT */) { return undefined; } const options = e.target.detail.injectedText?.options; if (options instanceof ModelDecorationInjectedTextOptions && options?.attachedData instanceof RenderedInlayHintLabelPart) { return options.attachedData; } return undefined; } async _invokeCommand(command, item) { try { await this._commandService.executeCommand(command.id, ...(command.arguments ?? [])); } catch (err) { this._notificationService.notify({ severity: Severity.Error, source: item.provider.displayName, message: err }); } } _cacheHintsForFastRestore(model) { const hints = this._copyInlayHintsWithCurrentAnchor(model); this._inlayHintsCache.set(model, hints); } // return inlay hints but with an anchor that reflects "updates" // that happened after receiving them, e.g adding new lines before a hint _copyInlayHintsWithCurrentAnchor(model) { const items = new Map(); for (const [id, obj] of this._decorationsMetadata) { if (items.has(obj.item)) { // an inlay item can be rendered as multiple decorations // but they will all uses the same range continue; } const range = model.getDecorationRange(id); if (range) { // update range with whatever the editor has tweaked it to const anchor = new InlayHintAnchor(range, obj.item.anchor.direction); const copy = obj.item.with({ anchor }); items.set(obj.item, copy); } } return Array.from(items.values()); } _getHintsRanges() { const extra = 30; const model = this._editor.getModel(); const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); const result = []; for (const range of visibleRanges.sort(Range.compareRangesUsingStarts)) { const extendedRange = model.validateRange(new Range(range.startLineNumber - extra, range.startColumn, range.endLineNumber + extra, range.endColumn)); if (result.length === 0 || !Range.areIntersectingOrTouching(result[result.length - 1], extendedRange)) { result.push(extendedRange); } else { result[result.length - 1] = Range.plusRange(result[result.length - 1], extendedRange); } } return result; } _updateHintsDecorators(ranges, items) { // utils to collect/create injected text decorations const newDecorationsData = []; const addInjectedText = (item, ref, content, cursorStops, attachedData) => { const opts = { content, inlineClassNameAffectsLetterSpacing: true, inlineClassName: ref.className, cursorStops, attachedData }; newDecorationsData.push({ item, classNameRef: ref, decoration: { range: item.anchor.range, options: { // className: "rangeHighlight", // DEBUG highlight to see to what range a hint is attached description: 'InlayHint', showIfCollapsed: item.anchor.range.isEmpty(), // "original" range is empty collapseOnReplaceEdit: !item.anchor.range.isEmpty(), stickiness: 0 /* TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges */, [item.anchor.direction]: this._activeRenderMode === 0 /* RenderMode.Normal */ ? opts : undefined } } }); }; const addInjectedWhitespace = (item, isLast) => { const marginRule = this._ruleFactory.createClassNameRef({ width: `${(fontSize / 3) | 0}px`, display: 'inline-block' }); addInjectedText(item, marginRule, '\u200a', isLast ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None); }; // const { fontSize, fontFamily, padding, isUniform } = this._getLayoutInfo(); const fontFamilyVar = '--code-editorInlayHintsFontFamily'; this._editor.getContainerDomNode().style.setProperty(fontFamilyVar, fontFamily); let currentLineInfo = { line: 0, totalLen: 0 }; for (const item of items) { if (currentLineInfo.line !== item.anchor.range.startLineNumber) { currentLineInfo = { line: item.anchor.range.startLineNumber, totalLen: 0 }; } if (currentLineInfo.totalLen > InlayHintsController_1._MAX_LABEL_LEN) { continue; } // whitespace leading the actual label if (item.hint.paddingLeft) { addInjectedWhitespace(item, false); } // the label with its parts const parts = typeof item.hint.label === 'string' ? [{ label: item.hint.label }] : item.hint.label; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const isFirst = i === 0; const isLast = i === parts.length - 1; const cssProperties = { fontSize: `${fontSize}px`, fontFamily: `var(${fontFamilyVar}), ${EDITOR_FONT_DEFAULTS.fontFamily}`, verticalAlign: isUniform ? 'baseline' : 'middle', unicodeBidi: 'isolate' }; if (isNonEmptyArray(item.hint.textEdits)) { cssProperties.cursor = 'default'; } this._fillInColors(cssProperties, item.hint); if ((part.command || part.location) && this._activeInlayHintPart?.part.item === item && this._activeInlayHintPart.part.index === i) { // active link! cssProperties.textDecoration = 'underline'; if (this._activeInlayHintPart.hasTriggerModifier) { cssProperties.color = themeColorFromId(colors.editorActiveLinkForeground); cssProperties.cursor = 'pointer'; } } if (padding) { if (isFirst && isLast) { // only element cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px`; cssProperties.borderRadius = `${(fontSize / 4) | 0}px`; } else if (isFirst) { // first element cssProperties.padding = `1px 0 1px ${Math.max(1, fontSize / 4) | 0}px`; cssProperties.borderRadius = `${(fontSize / 4) | 0}px 0 0 ${(fontSize / 4) | 0}px`; } else if (isLast) { // last element cssProperties.padding = `1px ${Math.max(1, fontSize / 4) | 0}px 1px 0`; cssProperties.borderRadius = `0 ${(fontSize / 4) | 0}px ${(fontSize / 4) | 0}px 0`; } else { cssProperties.padding = `1px 0 1px 0`; } } let textlabel = part.label; currentLineInfo.totalLen += textlabel.length; let tooLong = false; const over = currentLineInfo.totalLen - InlayHintsController_1._MAX_LABEL_LEN; if (over > 0) { textlabel = textlabel.slice(0, -over) + '…'; tooLong = true; } addInjectedText(item, this._ruleFactory.createClassNameRef(cssProperties), fixSpace(textlabel), isLast && !item.hint.paddingRight ? InjectedTextCursorStops.Right : InjectedTextCursorStops.None, new RenderedInlayHintLabelPart(item, i)); if (tooLong) { break; } } // whitespace trailing the actual label if (item.hint.paddingRight) { addInjectedWhitespace(item, true); } if (newDecorationsData.length > InlayHintsController_1._MAX_DECORATORS) { break; } } // collect all decoration ids that are affected by the ranges // and only update those decorations const decorationIdsToReplace = []; for (const [id, metadata] of this._decorationsMetadata) { const range = this._editor.getModel()?.getDecorationRange(id); if (range && ranges.some(r => r.containsRange(range))) { decorationIdsToReplace.push(id); metadata.classNameRef.dispose(); this._decorationsMetadata.delete(id); } } const scrollState = StableEditorScrollState.capture(this._editor); this._editor.changeDecorations(accessor => { const newDecorationIds = accessor.deltaDecorations(decorationIdsToReplace, newDecorationsData.map(d => d.decoration)); for (let i = 0; i < newDecorationIds.length; i++) { const data = newDecorationsData[i]; this._decorationsMetadata.set(newDecorationIds[i], data); } }); scrollState.restore(this._editor); } _fillInColors(props, hint) { if (hint.kind === languages.InlayHintKind.Parameter) { props.backgroundColor = themeColorFromId(colors.editorInlayHintParameterBackground); props.color = themeColorFromId(colors.editorInlayHintParameterForeground); } else if (hint.kind === languages.InlayHintKind.Type) { props.backgroundColor = themeColorFromId(colors.editorInlayHintTypeBackground); props.color = themeColorFromId(colors.editorInlayHintTypeForeground); } else { props.backgroundColor = themeColorFromId(colors.editorInlayHintBackground); props.color = themeColorFromId(colors.editorInlayHintForeground); } } _getLayoutInfo() { const options = this._editor.getOption(142 /* EditorOption.inlayHints */); const padding = options.padding; const editorFontSize = this._editor.getOption(52 /* EditorOption.fontSize */); const editorFontFamily = this._editor.getOption(49 /* EditorOption.fontFamily */); let fontSize = options.fontSize; if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { fontSize = editorFontSize; } const fontFamily = options.fontFamily || editorFontFamily; const isUniform = !padding && fontFamily === editorFontFamily && fontSize === editorFontSize; return { fontSize, fontFamily, padding, isUniform }; } _removeAllDecorations() { this._editor.removeDecorations(Array.from(this._decorationsMetadata.keys())); for (const obj of this._decorationsMetadata.values()) { obj.classNameRef.dispose(); } this._decorationsMetadata.clear(); } }; InlayHintsController = InlayHintsController_1 = __decorate([ __param(1, ILanguageFeaturesService), __param(2, ILanguageFeatureDebounceService), __param(3, IInlayHintsCache), __param(4, ICommandService), __param(5, INotificationService), __param(6, IInstantiationService) ], InlayHintsController); export { InlayHintsController }; // Prevents the view from potentially visible whitespace function fixSpace(str) { const noBreakWhitespace = '\xa0'; return str.replace(/[ \t]/g, noBreakWhitespace); } CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args) => { const [uri, range] = args; assertType(URI.isUri(uri)); assertType(Range.isIRange(range)); const { inlayHintsProvider } = accessor.get(ILanguageFeaturesService); const ref = await accessor.get(ITextModelService).createModelReference(uri); try { const model = await InlayHintsFragments.create(inlayHintsProvider, ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); const result = model.items.map(i => i.hint); setTimeout(() => model.dispose(), 0); // dispose after sending to ext host return result; } finally { ref.dispose(); } });