/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { CodeInsetProviderRegistry, getCodeInsetData, ICodeInsetData } from '../common/codeInset'; import { CodeInsetWidget, CodeInsetHelper } from './codeInsetWidget'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; // import { localize } from 'vs/nls'; export class CodeInsetController implements editorCommon.IEditorContribution { static get(editor: editorBrowser.ICodeEditor): CodeInsetController { return editor.getContribution(CodeInsetController.ID); } private static readonly ID: string = 'css.editor.codeInset'; private _isEnabled: boolean; private _globalToDispose: IDisposable[]; private _localToDispose: IDisposable[]; private _insetWidgets: CodeInsetWidget[]; private _pendingWebviews = new Map any>(); private _currentFindCodeInsetSymbolsPromise: CancelablePromise | null; private _modelChangeCounter: number; private _currentResolveCodeInsetSymbolsPromise: CancelablePromise | null; private _detectVisibleInsets: RunOnceScheduler; constructor( private _editor: editorBrowser.ICodeEditor, @IConfigurationService private readonly _configService: IConfigurationService, ) { this._isEnabled = this._configService.getValue('editor.codeInsets'); this._globalToDispose = []; this._localToDispose = []; this._insetWidgets = []; this._currentFindCodeInsetSymbolsPromise = null; this._modelChangeCounter = 0; this._globalToDispose.push(this._editor.onDidChangeModel(() => this._onModelChange())); this._globalToDispose.push(this._editor.onDidChangeModelLanguage(() => this._onModelChange())); this._globalToDispose.push(this._configService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.codeInsets')) { let prevIsEnabled = this._isEnabled; this._isEnabled = this._configService.getValue('editor.codeInsets'); if (prevIsEnabled !== this._isEnabled) { this._onModelChange(); } } })); this._globalToDispose.push(CodeInsetProviderRegistry.onDidChange(this._onModelChange, this)); this._onModelChange(); } dispose(): void { this._localDispose(); this._globalToDispose = dispose(this._globalToDispose); } acceptWebview(symbolId: string, webviewElement: WebviewElement): boolean { const pendingWebview = this._pendingWebviews.get(symbolId); if (pendingWebview) { pendingWebview(webviewElement); this._pendingWebviews.delete(symbolId); return true; } return false; } private _localDispose(): void { if (this._currentFindCodeInsetSymbolsPromise) { this._currentFindCodeInsetSymbolsPromise.cancel(); this._currentFindCodeInsetSymbolsPromise = null; this._modelChangeCounter++; } if (this._currentResolveCodeInsetSymbolsPromise) { this._currentResolveCodeInsetSymbolsPromise.cancel(); this._currentResolveCodeInsetSymbolsPromise = null; } this._localToDispose = dispose(this._localToDispose); } getId(): string { return CodeInsetController.ID; } private _onModelChange(): void { this._localDispose(); const model = this._editor.getModel(); if (!model || !this._isEnabled || !CodeInsetProviderRegistry.has(model)) { return; } for (const provider of CodeInsetProviderRegistry.all(model)) { if (typeof provider.onDidChange === 'function') { let registration = provider.onDidChange(() => scheduler.schedule()); this._localToDispose.push(registration); } } this._detectVisibleInsets = new RunOnceScheduler(() => { this._onViewportChanged(); }, 500); const scheduler = new RunOnceScheduler(() => { const counterValue = ++this._modelChangeCounter; if (this._currentFindCodeInsetSymbolsPromise) { this._currentFindCodeInsetSymbolsPromise.cancel(); } this._currentFindCodeInsetSymbolsPromise = createCancelablePromise(token => getCodeInsetData(model, token)); this._currentFindCodeInsetSymbolsPromise.then(codeInsetData => { if (counterValue === this._modelChangeCounter) { // only the last one wins this._renderCodeInsetSymbols(codeInsetData); this._detectVisibleInsets.schedule(); } }, onUnexpectedError); }, 250); this._localToDispose.push(scheduler); this._localToDispose.push(this._detectVisibleInsets); this._localToDispose.push(this._editor.onDidChangeModelContent(() => { this._editor.changeDecorations(changeAccessor => { this._editor.changeViewZones(viewAccessor => { let toDispose: CodeInsetWidget[] = []; let lastInsetLineNumber: number = -1; this._insetWidgets.forEach(inset => { if (!inset.isValid() || lastInsetLineNumber === inset.getLineNumber()) { // invalid -> Inset collapsed, attach range doesn't exist anymore // line_number -> insets should never be on the same line toDispose.push(inset); } else { inset.reposition(viewAccessor); lastInsetLineNumber = inset.getLineNumber(); } }); let helper = new CodeInsetHelper(); toDispose.forEach((l) => { l.dispose(helper, viewAccessor); this._insetWidgets.splice(this._insetWidgets.indexOf(l), 1); }); helper.commit(changeAccessor); }); }); // Compute new `visible` code insets this._detectVisibleInsets.schedule(); // Ask for all references again scheduler.schedule(); })); this._localToDispose.push(this._editor.onDidScrollChange(e => { if (e.scrollTopChanged && this._insetWidgets.length > 0) { this._detectVisibleInsets.schedule(); } })); this._localToDispose.push(this._editor.onDidLayoutChange(() => { this._detectVisibleInsets.schedule(); })); this._localToDispose.push(toDisposable(() => { if (this._editor.getModel()) { const scrollState = StableEditorScrollState.capture(this._editor); this._editor.changeDecorations((changeAccessor) => { this._editor.changeViewZones((accessor) => { this._disposeAllInsets(changeAccessor, accessor); }); }); scrollState.restore(this._editor); } else { // No accessors available this._disposeAllInsets(null, null); } })); scheduler.schedule(); } private _disposeAllInsets(decChangeAccessor: IModelDecorationsChangeAccessor | null, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor | null): void { let helper = new CodeInsetHelper(); this._insetWidgets.forEach((Inset) => Inset.dispose(helper, viewZoneChangeAccessor)); if (decChangeAccessor) { helper.commit(decChangeAccessor); } this._insetWidgets = []; } private _renderCodeInsetSymbols(symbols: ICodeInsetData[]): void { if (!this._editor.hasModel()) { return; } let maxLineNumber = this._editor.getModel().getLineCount(); let groups: ICodeInsetData[][] = []; let lastGroup: ICodeInsetData[] | undefined; for (let symbol of symbols) { let line = symbol.symbol.range.startLineNumber; if (line < 1 || line > maxLineNumber) { // invalid code Inset continue; } else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) { // on same line as previous lastGroup.push(symbol); } else { // on later line as previous lastGroup = [symbol]; groups.push(lastGroup); } } const scrollState = StableEditorScrollState.capture(this._editor); this._editor.changeDecorations(changeAccessor => { this._editor.changeViewZones(accessor => { let codeInsetIndex = 0, groupsIndex = 0, helper = new CodeInsetHelper(); while (groupsIndex < groups.length && codeInsetIndex < this._insetWidgets.length) { let symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber; let codeInsetLineNumber = this._insetWidgets[codeInsetIndex].getLineNumber(); if (codeInsetLineNumber < symbolsLineNumber) { this._insetWidgets[codeInsetIndex].dispose(helper, accessor); this._insetWidgets.splice(codeInsetIndex, 1); } else if (codeInsetLineNumber === symbolsLineNumber) { this._insetWidgets[codeInsetIndex].updateCodeInsetSymbols(groups[groupsIndex], helper); groupsIndex++; codeInsetIndex++; } else { this._insetWidgets.splice( codeInsetIndex, 0, new CodeInsetWidget(groups[groupsIndex], this._editor, helper) ); codeInsetIndex++; groupsIndex++; } } // Delete extra code insets while (codeInsetIndex < this._insetWidgets.length) { this._insetWidgets[codeInsetIndex].dispose(helper, accessor); this._insetWidgets.splice(codeInsetIndex, 1); } // Create extra symbols while (groupsIndex < groups.length) { this._insetWidgets.push(new CodeInsetWidget( groups[groupsIndex], this._editor, helper )); groupsIndex++; } helper.commit(changeAccessor); }); }); scrollState.restore(this._editor); } private _onViewportChanged(): void { if (this._currentResolveCodeInsetSymbolsPromise) { this._currentResolveCodeInsetSymbolsPromise.cancel(); this._currentResolveCodeInsetSymbolsPromise = null; } const model = this._editor.getModel(); if (!model) { return; } const allWidgetRequests: ICodeInsetData[][] = []; const insetWidgets: CodeInsetWidget[] = []; this._insetWidgets.forEach(inset => { const widgetRequests = inset.computeIfNecessary(model); if (widgetRequests) { allWidgetRequests.push(widgetRequests); insetWidgets.push(inset); } }); if (allWidgetRequests.length === 0) { return; } this._currentResolveCodeInsetSymbolsPromise = createCancelablePromise(token => { const allPromises = allWidgetRequests.map((widgetRequests, r) => { const widgetPromises = widgetRequests.map(request => { if (request.resolved) { return Promise.resolve(undefined); } let a = new Promise(resolve => { this._pendingWebviews.set(request.symbol.id, element => { request.resolved = true; insetWidgets[r].adoptWebview(element); resolve(); }); }); let b = request.provider.resolveCodeInset(model, request.symbol, token); return Promise.all([a, b]); }); return Promise.all(widgetPromises); }); return Promise.all(allPromises); }); this._currentResolveCodeInsetSymbolsPromise.then(() => { this._currentResolveCodeInsetSymbolsPromise = null; }).catch(err => { this._currentResolveCodeInsetSymbolsPromise = null; onUnexpectedError(err); }); } } registerEditorContribution(CodeInsetController); Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'editor', properties: { // ['editor.codeInsets']: { // description: localize('editor.codeInsets', "Enable/disable editor code insets"), // type: 'boolean', // default: false // } } });