debt - decouple webviews from code insets, move things to /browser/-layer, change inset api proposal to push style, re #66418

This commit is contained in:
Johannes Rieken 2019-06-04 12:18:06 +02:00
parent 3dc76ed11f
commit 6f1da34c2e
22 changed files with 726 additions and 1326 deletions

View file

@ -38,10 +38,6 @@
"name": "vs/workbench/contrib/codeEditor",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/codeinset",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/callHierarchy",
"project": "vscode-workbench"

View file

@ -137,32 +137,20 @@ declare module 'vscode' {
// #region Joh - code insets
/**
*/
export class CodeInset {
range: Range;
height?: number;
constructor(range: Range, height?: number);
export interface WebviewEditorInset {
readonly editor: TextEditor;
readonly range: Range;
readonly webview: Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export interface CodeInsetProvider {
onDidChangeCodeInsets?: Event<void>;
provideCodeInsets(document: TextDocument, token: CancellationToken): ProviderResult<CodeInset[]>;
resolveCodeInset(codeInset: CodeInset, webview: Webview, token: CancellationToken): ProviderResult<CodeInset>;
}
export namespace languages {
/**
* Register a code inset provider.
*
*/
export function registerCodeInsetProvider(selector: DocumentSelector, provider: CodeInsetProvider): Disposable;
export namespace window {
export function createWebviewTextEditorInset(editor: TextEditor, range: Range, options?: WebviewOptions): WebviewEditorInset;
}
//#endregion
//#region Joh - read/write in chunks
export interface FileSystemProvider {

View file

@ -1,70 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
// --- other interested parties
import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint';
import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint';
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
// --- mainThread participants
import './mainThreadClipboard';
import './mainThreadCommands';
import './mainThreadConfiguration';
import './mainThreadConsole';
import './mainThreadDebugService';
import './mainThreadDecorations';
import './mainThreadDiagnostics';
import './mainThreadDialogs';
import './mainThreadDocumentContentProviders';
import './mainThreadDocuments';
import './mainThreadDocumentsAndEditors';
import './mainThreadEditor';
import './mainThreadEditors';
import './mainThreadErrors';
import './mainThreadExtensionService';
import './mainThreadFileSystem';
import './mainThreadFileSystemEventService';
import './mainThreadHeapService';
import './mainThreadKeytar';
import './mainThreadLanguageFeatures';
import './mainThreadLanguages';
import './mainThreadLogService';
import './mainThreadMessageService';
import './mainThreadOutputService';
import './mainThreadProgress';
import './mainThreadQuickOpen';
import './mainThreadSaveParticipant';
import './mainThreadSCM';
import './mainThreadSearch';
import './mainThreadStatusBar';
import './mainThreadStorage';
import './mainThreadTelemetry';
import './mainThreadTerminalService';
import './mainThreadTreeViews';
import './mainThreadUrls';
import './mainThreadWindow';
import './mainThreadWorkspace';
import './mainThreadComments';
import './mainThreadTask';
import 'vs/workbench/api/common/apiCommands';
export class ExtensionPoints implements IWorkbenchContribution {
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
// Classes that handle extension points...
this.instantiationService.createInstance(JSONValidationExtensionPoint);
this.instantiationService.createInstance(ColorExtensionPoint);
this.instantiationService.createInstance(LanguageConfigurationFileHandler);
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExtensionPoints, LifecyclePhase.Starting);

View file

@ -3,5 +3,70 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './extensionHost.contribution.common';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
// --- other interested parties
import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint';
import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint';
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
// --- mainThread participants
import './mainThreadCodeInsets';
import './mainThreadClipboard';
import './mainThreadCommands';
import './mainThreadConfiguration';
import './mainThreadConsole';
import './mainThreadDebugService';
import './mainThreadDecorations';
import './mainThreadDiagnostics';
import './mainThreadDialogs';
import './mainThreadDocumentContentProviders';
import './mainThreadDocuments';
import './mainThreadDocumentsAndEditors';
import './mainThreadEditor';
import './mainThreadEditors';
import './mainThreadErrors';
import './mainThreadExtensionService';
import './mainThreadFileSystem';
import './mainThreadFileSystemEventService';
import './mainThreadHeapService';
import './mainThreadKeytar';
import './mainThreadLanguageFeatures';
import './mainThreadLanguages';
import './mainThreadLogService';
import './mainThreadMessageService';
import './mainThreadOutputService';
import './mainThreadProgress';
import './mainThreadQuickOpen';
import './mainThreadSaveParticipant';
import './mainThreadSCM';
import './mainThreadSearch';
import './mainThreadStatusBar';
import './mainThreadStorage';
import './mainThreadTelemetry';
import './mainThreadTerminalService';
import './mainThreadTreeViews';
import './mainThreadUrls';
import './mainThreadWindow';
import './mainThreadWebview';
import './mainThreadWorkspace';
import './mainThreadComments';
import './mainThreadTask';
import 'vs/workbench/api/common/apiCommands';
export class ExtensionPoints implements IWorkbenchContribution {
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
// Classes that handle extension points...
this.instantiationService.createInstance(JSONValidationExtensionPoint);
this.instantiationService.createInstance(ColorExtensionPoint);
this.instantiationService.createInstance(LanguageConfigurationFileHandler);
}
}
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExtensionPoints, LifecyclePhase.Starting);

View file

@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriComponents, URI } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { MainContext, MainThreadEditorInsetsShape, IExtHostContext, ExtHostEditorInsetsShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from '../common/extHostCustomers';
import { IRange } from 'vs/editor/common/core/range';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IWebviewService, Webview } from 'vs/workbench/contrib/webview/common/webview';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IActiveCodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser';
// todo@joh move these things back into something like contrib/insets
class EditorWebviewZone implements IViewZone {
readonly domNode: HTMLElement;
readonly afterLineNumber: number;
readonly afterColumn: number;
readonly heightInLines: number;
private _id: number;
// suppressMouseDown?: boolean | undefined;
// heightInPx?: number | undefined;
// minWidthInPx?: number | undefined;
// marginDomNode?: HTMLElement | null | undefined;
// onDomNodeTop?: ((top: number) => void) | undefined;
// onComputedHeight?: ((height: number) => void) | undefined;
constructor(
readonly editor: IActiveCodeEditor,
readonly range: IRange,
readonly webview: Webview,
) {
this.domNode = document.createElement('div');
this.afterLineNumber = range.startLineNumber;
this.afterColumn = range.startColumn;
this.heightInLines = range.endLineNumber - range.startLineNumber;
editor.changeViewZones(accessor => this._id = accessor.addZone(this));
webview.mountTo(this.domNode);
}
dispose(): void {
this.editor.changeViewZones(accessor => accessor.removeZone(this._id));
}
}
@extHostNamedCustomer(MainContext.MainThreadEditorInsets)
export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
private readonly _proxy: ExtHostEditorInsetsShape;
private readonly _disposables = new DisposableStore();
private readonly _insets = new Map<number, EditorWebviewZone>();
constructor(
context: IExtHostContext,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
@IWebviewService private readonly _webviewService: IWebviewService,
) {
this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
}
dispose(): void {
this._disposables.dispose();
}
async $createEditorInset(handle: number, id: string, uri: UriComponents, range: IRange, options: modes.IWebviewOptions): Promise<void> {
let editor: IActiveCodeEditor | undefined;
id = id.substr(0, id.indexOf(',')); //todo@joh HACK
for (const candidate of this._editorService.listCodeEditors()) {
if (candidate.getId() === id && candidate.hasModel() && candidate.getModel()!.uri.toString() === URI.revive(uri).toString()) {
editor = candidate;
break;
}
}
if (!editor) {
setTimeout(() => this._proxy.$onDidDispose(handle));
return;
}
const disposables = new DisposableStore();
const webview = this._webviewService.createWebview({
enableFindWidget: false,
allowSvgs: false,
extension: undefined
}, {
allowScripts: options.enableScripts
});
const webviewZone = new EditorWebviewZone(editor, range, webview);
const remove = () => {
disposables.dispose();
this._proxy.$onDidDispose(handle);
this._insets.delete(handle);
};
disposables.add(editor.onDidChangeModel(remove));
disposables.add(editor.onDidDispose(remove));
disposables.add(webviewZone);
disposables.add(webview);
disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg)));
this._insets.set(handle, webviewZone);
}
$disposeEditorInset(handle: number): void {
const inset = this._insets.get(handle);
if (inset) {
this._insets.delete(handle);
inset.dispose();
}
}
$setHtml(handle: number, value: string): void {
const inset = this._insets.get(handle);
if (inset) {
inset.webview.html = value;
}
}
$setOptions(handle: number, options: modes.IWebviewOptions): void {
const inset = this._insets.get(handle);
if (inset) {
inset.webview.options = options;
}
}
$postMessage(handle: number, value: any): Promise<boolean> {
const inset = this._insets.get(handle);
if (inset) {
inset.webview.sendMessage(value);
return Promise.resolve(true);
}
return Promise.resolve(false);
}
}

View file

@ -11,14 +11,13 @@ import * as search from 'vs/workbench/contrib/search/common/search';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Position as EditorPosition } from 'vs/editor/common/core/position';
import { Range as EditorRange, IRange } from 'vs/editor/common/core/range';
import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto, LinkDto, CallHierarchyDto, SuggestDataDto } from '../common/extHost.protocol';
import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CallHierarchyDto, SuggestDataDto } from '../common/extHost.protocol';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { URI } from 'vs/base/common/uri';
import { Selection } from 'vs/editor/common/core/selection';
import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { IHeapService } from 'vs/workbench/services/heap/common/heap';
@ -178,35 +177,6 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
}
}
// -- code inset
$registerCodeInsetSupport(handle: number, selector: ISerializedDocumentFilter[], eventHandle: number): void {
const provider = <codeInset.CodeInsetProvider>{
provideCodeInsets: (model: ITextModel, token: CancellationToken): CodeInsetDto[] | Thenable<CodeInsetDto[]> => {
return this._proxy.$provideCodeInsets(handle, model.uri, token).then(dto => {
if (dto) { dto.forEach(obj => this._heapService.trackObject(obj)); }
return dto;
});
},
resolveCodeInset: (model: ITextModel, codeInset: CodeInsetDto, token: CancellationToken): CodeInsetDto | Thenable<CodeInsetDto> => {
return this._proxy.$resolveCodeInset(handle, model.uri, codeInset, token).then(obj => {
this._heapService.trackObject(obj);
return obj;
});
}
};
if (typeof eventHandle === 'number') {
const emitter = new Emitter<codeInset.CodeInsetProvider>();
this._registrations[eventHandle] = emitter;
provider.onDidChange = emitter.event;
}
const langSelector = selector;
this._registrations[handle] = codeInset.CodeInsetProviderRegistry.register(langSelector, provider);
}
// --- declaration
$registerDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void {

View file

@ -3,46 +3,365 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { UriComponents } from 'vs/base/common/uri';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as map from 'vs/base/common/map';
import { URI, UriComponents } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { MainContext, MainThreadWebviewsShape, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol';
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers';
import { IProductService } from 'vs/platform/product/common/product';
@extHostNamedCustomer(MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape {
$createWebviewPanel(handle: string, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void {
throw new Error('Method not implemented.');
private static readonly standardSupportedLinkSchemes = new Set([
'http',
'https',
'mailto',
'vscode',
'vscode-insider',
]);
private static revivalPool = 0;
private readonly _proxy: ExtHostWebviewsShape;
private readonly _webviews = new Map<WebviewPanelHandle, WebviewEditorInput>();
private readonly _revivers = new Map<string, IDisposable>();
private _activeWebview: WebviewPanelHandle | undefined = undefined;
constructor(
context: IExtHostContext,
@ILifecycleService lifecycleService: ILifecycleService,
@IExtensionService extensionService: IExtensionService,
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
@IEditorService private readonly _editorService: IEditorService,
@IWebviewEditorService private readonly _webviewService: IWebviewEditorService,
@IOpenerService private readonly _openerService: IOpenerService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IProductService private readonly _productService: IProductService,
) {
super();
this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
this._register(_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this));
this._register(_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this));
// This reviver's only job is to activate webview extensions
// This should trigger the real reviver to be registered from the extension host side.
this._register(_webviewService.registerReviver({
canRevive: (webview) => {
const viewType = webview.state.viewType;
if (viewType) {
extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
}
return false;
},
reviveWebview: () => { throw new Error('not implemented'); }
}));
this._register(lifecycleService.onBeforeShutdown(e => {
e.veto(this._onBeforeShutdown());
}, this));
}
$createWebviewCodeInset(handle: number, symbolId: string, options: modes.IWebviewOptions, extensionId: ExtensionIdentifier | undefined, extensionLocation: UriComponents | undefined): void {
throw new Error('Method not implemented.');
public $createWebviewPanel(
handle: WebviewPanelHandle,
viewType: string,
title: string,
showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean },
options: WebviewInputOptions,
extensionId: ExtensionIdentifier,
extensionLocation: UriComponents
): void {
const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
if (showOptions) {
mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
}
const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), {
location: URI.revive(extensionLocation),
id: extensionId
}, this.createWebviewEventDelegate(handle));
webview.state = {
viewType: viewType,
state: undefined
};
this._webviews.set(handle, webview);
/* __GDPR__
"webviews:createWebviewPanel" : {
"extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value });
}
$disposeWebview(handle: string): void {
throw new Error('Method not implemented.');
public $disposeWebview(handle: WebviewPanelHandle): void {
const webview = this.getWebview(handle);
webview.dispose();
}
$reveal(handle: string, showOptions: WebviewPanelShowOptions): void {
throw new Error('Method not implemented.');
public $setTitle(handle: WebviewPanelHandle, value: string): void {
const webview = this.getWebview(handle);
webview.setName(value);
}
$setTitle(handle: string, value: string): void {
throw new Error('Method not implemented.');
public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void {
const webview = this.getWebview(handle);
webview.iconPath = reviveWebviewIcon(value);
}
$setIconPath(handle: string, value: { light: UriComponents; dark: UriComponents; } | undefined): void {
throw new Error('Method not implemented.');
public $setHtml(handle: WebviewPanelHandle, value: string): void {
const webview = this.getWebview(handle);
webview.html = value;
}
$setHtml(handle: string | number, value: string): void {
throw new Error('Method not implemented.');
public $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void {
const webview = this.getWebview(handle);
webview.setOptions(reviveWebviewOptions(options as any /*todo@mat */));
}
$setOptions(handle: string | number, options: modes.IWebviewOptions): void {
throw new Error('Method not implemented.');
public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
const webview = this.getWebview(handle);
if (webview.isDisposed()) {
return;
}
const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
if (targetGroup) {
this._webviewService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
}
}
$postMessage(handle: string | number, value: any): Promise<boolean> {
throw new Error('Method not implemented.');
public async $postMessage(handle: WebviewPanelHandle, message: any): Promise<boolean> {
const webview = this.getWebview(handle);
const editors = this._editorService.visibleControls
.filter(e => e instanceof WebviewEditor)
.map(e => e as WebviewEditor)
.filter(e => e.input!.matches(webview));
if (editors.length > 0) {
editors[0].sendMessage(message);
return true;
}
if (webview.webview) {
webview.webview.sendMessage(message);
return true;
}
return false;
}
$registerSerializer(viewType: string): void {
throw new Error('Method not implemented.');
public $registerSerializer(viewType: string): void {
if (this._revivers.has(viewType)) {
throw new Error(`Reviver for ${viewType} already registered`);
}
this._revivers.set(viewType, this._webviewService.registerReviver({
canRevive: (webview) => {
return webview.state && webview.state.viewType === viewType;
},
reviveWebview: async (webview): Promise<void> => {
const viewType = webview.state.viewType;
const handle = 'revival-' + MainThreadWebviews.revivalPool++;
this._webviews.set(handle, webview);
webview._events = this.createWebviewEventDelegate(handle);
let state = undefined;
if (webview.state.state) {
try {
state = JSON.parse(webview.state.state);
} catch {
// noop
}
}
try {
await this._proxy.$deserializeWebviewPanel(handle, viewType, webview.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webview.group || 0), webview.options);
} catch (error) {
onUnexpectedError(error);
webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
}
}
}));
}
$unregisterSerializer(viewType: string): void {
throw new Error('Method not implemented.');
public $unregisterSerializer(viewType: string): void {
const reviver = this._revivers.get(viewType);
if (!reviver) {
throw new Error(`No reviver for ${viewType} registered`);
}
reviver.dispose();
this._revivers.delete(viewType);
}
private getInternalWebviewId(viewType: string): string {
return `mainThreadWebview-${viewType}`;
}
private _onBeforeShutdown(): boolean {
this._webviews.forEach((webview) => {
if (!webview.isDisposed() && webview.state && this._revivers.has(webview.state.viewType)) {
webview.state.state = webview.webviewState;
}
});
return false; // Don't veto shutdown
}
private createWebviewEventDelegate(handle: WebviewPanelHandle) {
return {
onDidClickLink: (uri: URI) => this.onDidClickLink(handle, uri),
onMessage: (message: any) => this._proxy.$onMessage(handle, message),
onDispose: () => {
this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
this._webviews.delete(handle);
});
}
};
}
private onActiveEditorChanged() {
const activeEditor = this._editorService.activeControl;
let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined;
if (activeEditor && activeEditor.input instanceof WebviewEditorInput) {
for (const handle of map.keys(this._webviews)) {
const input = this._webviews.get(handle)!;
if (input.matches(activeEditor.input)) {
newActiveWebview = { input, handle };
break;
}
}
}
if (newActiveWebview && newActiveWebview.handle === this._activeWebview) {
// Webview itself unchanged but position may have changed
this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
active: true,
visible: true,
position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || 0)
});
return;
}
// Broadcast view state update for currently active
if (typeof this._activeWebview !== 'undefined') {
const oldActiveWebview = this._webviews.get(this._activeWebview);
if (oldActiveWebview) {
this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, {
active: false,
visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)),
position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || 0),
});
}
}
// Then for newly active
if (newActiveWebview) {
this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
active: true,
visible: true,
position: editorGroupToViewColumn(this._editorGroupService, activeEditor ? activeEditor.group : ACTIVE_GROUP),
});
this._activeWebview = newActiveWebview.handle;
} else {
this._activeWebview = undefined;
}
}
private onVisibleEditorsChanged(): void {
this._webviews.forEach((input, handle) => {
for (const workbenchEditor of this._editorService.visibleControls) {
if (workbenchEditor.input && workbenchEditor.input.matches(input)) {
const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group!);
input.updateGroup(workbenchEditor.group!.id);
this._proxy.$onDidChangeWebviewPanelViewState(handle, {
active: handle === this._activeWebview,
visible: true,
position: editorPosition
});
break;
}
}
});
}
private onDidClickLink(handle: WebviewPanelHandle, link: URI): void {
if (!link) {
return;
}
const webview = this.getWebview(handle);
if (this.isSupportedLink(webview, link)) {
this._openerService.open(link);
}
}
private isSupportedLink(webview: WebviewEditorInput, link: URI): boolean {
if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
return true;
}
if (this._productService.urlProtocol === link.scheme) {
return true;
}
return !!webview.options.enableCommandUris && link.scheme === 'command';
}
private getWebview(handle: WebviewPanelHandle): WebviewEditorInput {
const webview = this._webviews.get(handle);
if (!webview) {
throw new Error('Unknown webview handle:' + handle);
}
return webview;
}
private static getDeserializationFailedContents(viewType: string) {
return `<!DOCTYPE html>
<html>
<head>
<base href="https://code.visualstudio.com/raw/">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-core-resource: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
</head>
<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
</html>`;
}
}
function reviveWebviewOptions(options: WebviewInputOptions): WebviewInputOptions {
return {
...options,
localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
};
}
function reviveWebviewIcon(
value: { light: UriComponents, dark: UriComponents } | undefined
): { light: URI, dark: URI } | undefined {
if (!value) {
return undefined;
}
return {
light: URI.revive(value.light),
dark: URI.revive(value.dark)
};
}

View file

@ -42,7 +42,6 @@ import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { ResolvedAuthority, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset';
import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { IRelativePattern } from 'vs/base/common/glob';
import { IRemoteConsoleLog } from 'vs/base/common/console';
@ -333,7 +332,6 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
$unregister(handle: number): void;
$registerDocumentSymbolProvider(handle: number, selector: ISerializedDocumentFilter[], label: string): void;
$registerCodeLensSupport(handle: number, selector: ISerializedDocumentFilter[], eventHandle: number | undefined): void;
$registerCodeInsetSupport(handle: number, selector: ISerializedDocumentFilter[], eventHandle: number | undefined): void;
$emitCodeLensEvent(eventHandle: number, event?: any): void;
$registerDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void;
$registerDeclarationSupport(handle: number, selector: ISerializedDocumentFilter[]): void;
@ -510,9 +508,21 @@ export interface MainThreadTelemetryShape extends IDisposable {
$publicLog(eventName: string, data?: any): void;
}
export type WebviewPanelHandle = string;
export interface MainThreadEditorInsetsShape extends IDisposable {
$createEditorInset(handle: number, editorId: string, document: UriComponents, range: IRange, options: modes.IWebviewOptions): Promise<void>;
$disposeEditorInset(handle: number): void;
export type WebviewInsetHandle = number;
$setHtml(handle: number, value: string): void;
$setOptions(handle: number, options: modes.IWebviewOptions): void;
$postMessage(handle: number, value: any): Promise<boolean>;
}
export interface ExtHostEditorInsetsShape {
$onDidDispose(handle: number): void;
$onDidReceiveMessage(handle: number, message: any): void;
}
export type WebviewPanelHandle = string;
export interface WebviewPanelShowOptions {
readonly viewColumn?: EditorViewColumn;
@ -521,15 +531,14 @@ export interface WebviewPanelShowOptions {
export interface MainThreadWebviewsShape extends IDisposable {
$createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void;
$createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: modes.IWebviewOptions, extensionId: ExtensionIdentifier | undefined, extensionLocation: UriComponents | undefined): void;
$disposeWebview(handle: WebviewPanelHandle): void;
$reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void;
$setTitle(handle: WebviewPanelHandle, value: string): void;
$setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void;
$setHtml(handle: WebviewPanelHandle | WebviewInsetHandle, value: string): void;
$setOptions(handle: WebviewPanelHandle | WebviewInsetHandle, options: modes.IWebviewOptions): void;
$postMessage(handle: WebviewPanelHandle | WebviewInsetHandle, value: any): Promise<boolean>;
$setHtml(handle: WebviewPanelHandle, value: string): void;
$setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void;
$postMessage(handle: WebviewPanelHandle, value: any): Promise<boolean>;
$registerSerializer(viewType: string): void;
$unregisterSerializer(viewType: string): void;
@ -1005,8 +1014,6 @@ export interface CodeLensDto extends ObjectIdentifier {
command?: CommandDto;
}
export type CodeInsetDto = ObjectIdentifier & codeInset.ICodeInsetSymbol;
export interface CallHierarchyDto {
_id: number;
kind: modes.SymbolKind;
@ -1021,8 +1028,6 @@ export interface ExtHostLanguageFeaturesShape {
$provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise<modes.DocumentSymbol[] | undefined>;
$provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise<CodeLensDto[]>;
$resolveCodeLens(handle: number, symbol: CodeLensDto, token: CancellationToken): Promise<CodeLensDto | undefined>;
$provideCodeInsets(handle: number, resource: UriComponents, token: CancellationToken): Promise<CodeInsetDto[] | undefined>;
$resolveCodeInset(handle: number, resource: UriComponents, symbol: CodeInsetDto, token: CancellationToken): Promise<CodeInsetDto>;
$provideDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<DefinitionLinkDto[]>;
$provideDeclaration(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<DefinitionLinkDto[]>;
$provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<DefinitionLinkDto[]>;
@ -1248,6 +1253,7 @@ export const MainContext = {
MainThreadDocuments: createMainId<MainThreadDocumentsShape>('MainThreadDocuments'),
MainThreadDocumentContentProviders: createMainId<MainThreadDocumentContentProvidersShape>('MainThreadDocumentContentProviders'),
MainThreadTextEditors: createMainId<MainThreadTextEditorsShape>('MainThreadTextEditors'),
MainThreadEditorInsets: createMainId<MainThreadEditorInsetsShape>('MainThreadEditorInsets'),
MainThreadErrors: createMainId<MainThreadErrorsShape>('MainThreadErrors'),
MainThreadTreeViews: createMainId<MainThreadTreeViewsShape>('MainThreadTreeViews'),
MainThreadKeytar: createMainId<MainThreadKeytarShape>('MainThreadKeytar'),
@ -1298,6 +1304,7 @@ export const ExtHostContext = {
ExtHostWorkspace: createExtId<ExtHostWorkspaceShape>('ExtHostWorkspace'),
ExtHostWindow: createExtId<ExtHostWindowShape>('ExtHostWindow'),
ExtHostWebviews: createExtId<ExtHostWebviewsShape>('ExtHostWebviews'),
ExtHostEditorInsets: createExtId<ExtHostEditorInsetsShape>('ExtHostEditorInsets'),
ExtHostProgress: createMainId<ExtHostProgressShape>('ExtHostProgress'),
ExtHostComments: createMainId<ExtHostCommentsShape>('ExtHostComments'),
ExtHostStorage: createMainId<ExtHostStorageShape>('ExtHostStorage'),

View file

@ -0,0 +1,130 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import * as vscode from 'vscode';
import { MainThreadEditorInsetsShape } from './extHost.protocol';
import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor';
export class ExtHostEditorInsets implements ExtHostEditorInsets {
private _handlePool = 0;
private _disposables = new DisposableStore();
private _insets = new Map<number, { editor: vscode.TextEditor, inset: vscode.WebviewEditorInset, onDidReceiveMessage: Emitter<any> }>();
constructor(
private readonly _proxy: MainThreadEditorInsetsShape,
private readonly _editors: ExtHostEditors
) {
// dispose editor inset whenever the hosting editor goes away
this._disposables.add(_editors.onDidChangeVisibleTextEditors(() => {
const visibleEditor = _editors.getVisibleTextEditors();
this._insets.forEach(value => {
if (visibleEditor.indexOf(value.editor) < 0) {
value.inset.dispose(); // will remove from `this._insets`
}
});
}));
}
dispose(): void {
this._insets.forEach(value => value.inset.dispose());
this._disposables.dispose();
}
createWebviewEditorInset(editor: vscode.TextEditor, range: vscode.Range, options?: vscode.WebviewOptions): vscode.WebviewEditorInset {
let apiEditor: ExtHostTextEditor | undefined;
for (const candidate of this._editors.getVisibleTextEditors()) {
if (candidate === editor) {
apiEditor = <ExtHostTextEditor>candidate;
break;
}
}
if (!apiEditor) {
throw new Error('not a visible editor');
}
const that = this;
const handle = this._handlePool++;
const onDidReceiveMessage = new Emitter<any>();
const onDidDispose = new Emitter<void>();
const webview = new class implements vscode.Webview {
private _html: string = '';
private _options: vscode.WebviewOptions;
set options(value: vscode.WebviewOptions) {
this._options = value;
that._proxy.$setOptions(handle, value);
}
get options(): vscode.WebviewOptions {
return this._options;
}
set html(value: string) {
this._html = value;
that._proxy.$setHtml(handle, value);
}
get html(): string {
return this._html;
}
get onDidReceiveMessage(): vscode.Event<any> {
return onDidReceiveMessage.event;
}
postMessage(message: any): Thenable<boolean> {
return that._proxy.$postMessage(handle, message);
}
};
const inset = new class implements vscode.WebviewEditorInset {
readonly editor: vscode.TextEditor = editor;
readonly range: vscode.Range = range;
readonly webview: vscode.Webview = webview;
readonly onDidDispose: vscode.Event<void> = onDidDispose.event;
dispose(): void {
if (that._insets.has(handle)) {
that._insets.delete(handle);
that._proxy.$disposeEditorInset(handle);
onDidDispose.fire();
// final cleanup
onDidDispose.dispose();
onDidReceiveMessage.dispose();
}
}
};
this._proxy.$createEditorInset(handle, apiEditor.id, apiEditor.document.uri, typeConverters.Range.from(range), options || {});
this._insets.set(handle, { editor, inset, onDidReceiveMessage });
return inset;
}
$onDidDispose(handle: number): void {
const value = this._insets.get(handle);
if (value) {
value.inset.dispose();
}
}
$onDidReceiveMessage(handle: number, message: any): void {
const value = this._insets.get(handle);
if (value) {
value.onDidReceiveMessage.fire(message);
}
}
}

View file

@ -15,7 +15,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics';
import { asPromise } from 'vs/base/common/async';
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto, SuggestDataDto, LinksListDto, ChainedCacheId } from './extHost.protocol';
import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, SuggestDataDto, LinksListDto, ChainedCacheId } from './extHost.protocol';
import { regExpLeadsToEndlessLoop, regExpFlags } from 'vs/base/common/strings';
import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range as EditorRange } from 'vs/editor/common/core/range';
@ -25,9 +25,6 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtHostWebview } from 'vs/workbench/api/common/extHostWebview';
import * as codeInset from 'vs/workbench/contrib/codeinset/common/codeInset';
import { generateUuid } from 'vs/base/common/uuid';
import * as callHierarchy from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { LRUCache } from 'vs/base/common/map';
import { IURITransformer } from 'vs/base/common/uriIpc';
@ -152,47 +149,6 @@ class CodeLensAdapter {
}
}
class CodeInsetAdapter {
constructor(
private readonly _documents: ExtHostDocuments,
private readonly _heapService: ExtHostHeapService,
private readonly _provider: vscode.CodeInsetProvider
) { }
provideCodeInsets(resource: URI, token: CancellationToken): Promise<CodeInsetDto[] | undefined> {
const doc = this._documents.getDocument(resource);
return asPromise(() => this._provider.provideCodeInsets(doc, token)).then(insets => {
if (Array.isArray(insets)) {
return insets.map(inset => {
const $ident = this._heapService.keep(inset);
const id = generateUuid();
return {
$ident,
id,
range: typeConvert.Range.from(inset.range),
height: inset.height
};
});
}
return undefined;
});
}
resolveCodeInset(symbol: CodeInsetDto, webview: vscode.Webview, token: CancellationToken): Promise<CodeInsetDto> {
const inset = this._heapService.get<vscode.CodeInset>(ObjectIdentifier.of(symbol));
if (!inset) {
return Promise.resolve(symbol);
}
return asPromise(() => this._provider.resolveCodeInset(inset, webview, token)).then(newInset => {
newInset = newInset || inset;
return symbol;
});
}
}
function convertToLocationLinks(value: vscode.Definition): modes.LocationLink[] {
return value ? asArray(value).map(typeConvert.DefinitionLink.from) : [];
}
@ -1040,7 +996,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov
| DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter
| RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter
| SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter
| ColorProviderAdapter | FoldingProviderAdapter | CodeInsetAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter;
| ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter;
class AdapterData {
constructor(
@ -1061,7 +1017,6 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
private _diagnostics: ExtHostDiagnostics;
private _adapter = new Map<number, AdapterData>();
private readonly _logService: ILogService;
private _webviewProxy: MainThreadWebviewsShape;
constructor(
mainContext: IMainContext,
@ -1079,7 +1034,6 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
this._heapService = heapMonitor;
this._diagnostics = diagnostics;
this._logService = logService;
this._webviewProxy = mainContext.getProxy(MainContext.MainThreadWebviews);
}
private _transformDocumentSelector(selector: vscode.DocumentSelector): Array<ISerializedDocumentFilter> {
@ -1202,37 +1156,6 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(symbol, token), undefined);
}
// --- code insets
registerCodeInsetProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CodeInsetProvider): vscode.Disposable {
const handle = this._nextHandle();
const eventHandle = typeof provider.onDidChangeCodeInsets === 'function' ? this._nextHandle() : undefined;
this._adapter.set(handle, new AdapterData(new CodeInsetAdapter(this._documents, this._heapService, provider), extension));
this._proxy.$registerCodeInsetSupport(handle, this._transformDocumentSelector(selector), eventHandle);
let result = this._createDisposable(handle);
if (eventHandle !== undefined && provider.onDidChangeCodeInsets) {
const subscription = provider.onDidChangeCodeInsets(_ => this._proxy.$emitCodeLensEvent(eventHandle));
result = Disposable.from(result, subscription);
}
return result;
}
$provideCodeInsets(handle: number, resource: UriComponents, token: CancellationToken): Promise<codeInset.ICodeInsetSymbol[] | undefined> {
return this._withAdapter(handle, CodeInsetAdapter, adapter => adapter.provideCodeInsets(URI.revive(resource), token), undefined);
}
$resolveCodeInset(handle: number, _resource: UriComponents, symbol: codeInset.ICodeInsetSymbol, token: CancellationToken): Promise<codeInset.ICodeInsetSymbol> {
const webviewHandle = Math.random();
const webview = new ExtHostWebview(webviewHandle, this._webviewProxy, { enableScripts: true });
return this._withAdapter(handle, CodeInsetAdapter, async (adapter, extension) => {
await this._webviewProxy.$createWebviewCodeInset(webviewHandle, symbol.id, { enableCommandUris: true, enableScripts: true }, extension ? extension.identifier : undefined, extension ? extension.extensionLocation : undefined);
return adapter.resolveCodeInset(symbol, webview, token);
}, symbol);
}
// --- declaration
registerDefinitionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable {

View file

@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
import * as vscode from 'vscode';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState, WebviewInsetHandle } from './extHost.protocol';
import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol';
import { Disposable } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import * as modes from 'vs/editor/common/modes';
@ -16,7 +16,7 @@ import * as modes from 'vs/editor/common/modes';
type IconPath = URI | { light: URI, dark: URI };
export class ExtHostWebview implements vscode.Webview {
private readonly _handle: WebviewPanelHandle | WebviewInsetHandle;
private readonly _handle: WebviewPanelHandle;
private readonly _proxy: MainThreadWebviewsShape;
private _html: string;
private _options: vscode.WebviewOptions;
@ -26,7 +26,7 @@ export class ExtHostWebview implements vscode.Webview {
public readonly onDidReceiveMessage: Event<any> = this._onMessageEmitter.event;
constructor(
handle: WebviewPanelHandle | WebviewInsetHandle,
handle: WebviewPanelHandle,
proxy: MainThreadWebviewsShape,
options: vscode.WebviewOptions
) {

View file

@ -1,7 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import '../browser/extensionHost.contribution.common';
import './mainThreadWebview';

View file

@ -1,437 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as map from 'vs/base/common/map';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import * as modes from 'vs/editor/common/modes';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import product from 'vs/platform/product/node/product';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewInsetHandle, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/common/extHost.protocol';
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
import { CodeInsetController } from 'vs/workbench/contrib/codeinset/electron-browser/codeInset.contribution';
import { WebviewEditor } from 'vs/workbench/contrib/webview/browser/webviewEditor';
import { WebviewEditorInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput';
import { ICreateWebViewShowOptions, IWebviewEditorService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewEditorService';
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ACTIVE_GROUP, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadWebviews)
export class MainThreadWebviews extends Disposable implements MainThreadWebviewsShape {
private static readonly standardSupportedLinkSchemes = new Set([
'http',
'https',
'mailto',
product.urlProtocol,
'vscode',
'vscode-insiders'
]);
private static revivalPool = 0;
private readonly _proxy: ExtHostWebviewsShape;
private readonly _webviews = new Map<WebviewPanelHandle, WebviewEditorInput>();
private readonly _webviewsElements = new Map<WebviewInsetHandle, WebviewElement>();
private readonly _revivers = new Map<string, IDisposable>();
private _activeWebview: WebviewPanelHandle | undefined = undefined;
constructor(
context: IExtHostContext,
@ILifecycleService lifecycleService: ILifecycleService,
@IExtensionService extensionService: IExtensionService,
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
@IEditorService private readonly _editorService: IEditorService,
@IWebviewEditorService private readonly _webviewService: IWebviewEditorService,
@IOpenerService private readonly _openerService: IOpenerService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
) {
super();
this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews);
this._register(_editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this));
this._register(_editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this));
// This reviver's only job is to activate webview extensions
// This should trigger the real reviver to be registered from the extension host side.
this._register(_webviewService.registerReviver({
canRevive: (webview) => {
const viewType = webview.state.viewType;
if (viewType) {
extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
}
return false;
},
reviveWebview: () => { throw new Error('not implemented'); }
}));
this._register(lifecycleService.onBeforeShutdown(e => {
e.veto(this._onBeforeShutdown());
}, this));
}
public $createWebviewPanel(
handle: WebviewPanelHandle,
viewType: string,
title: string,
showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean },
options: WebviewInputOptions,
extensionId: ExtensionIdentifier,
extensionLocation: UriComponents
): void {
const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
if (showOptions) {
mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
}
const webview = this._webviewService.createWebview(this.getInternalWebviewId(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), {
location: URI.revive(extensionLocation),
id: extensionId
}, this.createWebviewEventDelegate(handle));
webview.state = {
viewType: viewType,
state: undefined
};
this._webviews.set(handle, webview);
/* __GDPR__
"webviews:createWebviewPanel" : {
"extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value });
}
$createWebviewCodeInset(
handle: WebviewInsetHandle,
symbolId: string,
options: modes.IWebviewOptions,
extensionId: ExtensionIdentifier,
extensionLocation: UriComponents
): void {
// todo@joh main is for the lack of a code-inset service
// which we maybe wanna have... this is how it now works
// 1) create webview element
// 2) find the code inset controller that request it
// 3) let the controller adopt the widget
// 4) continue to forward messages to the webview
const webview = this._instantiationService.createInstance(
WebviewElement,
{
extension: {
location: URI.revive(extensionLocation),
id: extensionId
},
enableFindWidget: false,
},
{
allowScripts: options.enableScripts,
}
);
let found = false;
for (const editor of this._codeEditorService.listCodeEditors()) {
const ctrl = CodeInsetController.get(editor);
if (ctrl && ctrl.acceptWebview(symbolId, webview)) {
found = true;
break;
}
}
if (!found) {
webview.dispose();
return;
}
// this will leak... the adopted webview will be disposed by the
// code inset controller. we might need a dispose-event here so that
// we can clean up things.
this._webviewsElements.set(handle, webview);
}
public $disposeWebview(handle: WebviewPanelHandle): void {
const webview = this.getWebview(handle);
webview.dispose();
}
public $setTitle(handle: WebviewPanelHandle, value: string): void {
const webview = this.getWebview(handle);
webview.setName(value);
}
public $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents } | undefined): void {
const webview = this.getWebview(handle);
webview.iconPath = reviveWebviewIcon(value);
}
public $setHtml(handle: WebviewPanelHandle | WebviewInsetHandle, value: string): void {
if (typeof handle === 'number') {
this.getWebviewElement(handle).html = value;
} else {
const webview = this.getWebview(handle);
webview.html = value;
}
}
public $setOptions(handle: WebviewPanelHandle | WebviewInsetHandle, options: modes.IWebviewOptions): void {
if (typeof handle === 'number') {
this.getWebviewElement(handle).options = reviveWebviewOptions(options as any /*todo@mat */);
} else {
const webview = this.getWebview(handle);
webview.setOptions(reviveWebviewOptions(options as any /*todo@mat */));
}
}
public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void {
const webview = this.getWebview(handle);
if (webview.isDisposed()) {
return;
}
const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
if (targetGroup) {
this._webviewService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
}
}
public async $postMessage(handle: WebviewPanelHandle | WebviewInsetHandle, message: any): Promise<boolean> {
if (typeof handle === 'number') {
this.getWebviewElement(handle).sendMessage(message);
return true;
} else {
const webview = this.getWebview(handle);
const editors = this._editorService.visibleControls
.filter(e => e instanceof WebviewEditor)
.map(e => e as WebviewEditor)
.filter(e => e.input!.matches(webview));
if (editors.length > 0) {
editors[0].sendMessage(message);
return true;
}
if (webview.webview) {
webview.webview.sendMessage(message);
return true;
}
return false;
}
}
public $registerSerializer(viewType: string): void {
if (this._revivers.has(viewType)) {
throw new Error(`Reviver for ${viewType} already registered`);
}
this._revivers.set(viewType, this._webviewService.registerReviver({
canRevive: (webview) => {
return webview.state && webview.state.viewType === viewType;
},
reviveWebview: async (webview): Promise<void> => {
const viewType = webview.state.viewType;
const handle = 'revival-' + MainThreadWebviews.revivalPool++;
this._webviews.set(handle, webview);
webview._events = this.createWebviewEventDelegate(handle);
let state = undefined;
if (webview.state.state) {
try {
state = JSON.parse(webview.state.state);
} catch {
// noop
}
}
try {
await this._proxy.$deserializeWebviewPanel(handle, viewType, webview.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webview.group || 0), webview.options);
} catch (error) {
onUnexpectedError(error);
webview.html = MainThreadWebviews.getDeserializationFailedContents(viewType);
}
}
}));
}
public $unregisterSerializer(viewType: string): void {
const reviver = this._revivers.get(viewType);
if (!reviver) {
throw new Error(`No reviver for ${viewType} registered`);
}
reviver.dispose();
this._revivers.delete(viewType);
}
private getInternalWebviewId(viewType: string): string {
return `mainThreadWebview-${viewType}`;
}
private _onBeforeShutdown(): boolean {
this._webviews.forEach((webview) => {
if (!webview.isDisposed() && webview.state && this._revivers.has(webview.state.viewType)) {
webview.state.state = webview.webviewState;
}
});
return false; // Don't veto shutdown
}
private createWebviewEventDelegate(handle: WebviewPanelHandle) {
return {
onDidClickLink: (uri: URI) => this.onDidClickLink(handle, uri),
onMessage: (message: any) => this._proxy.$onMessage(handle, message),
onDispose: () => {
this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
this._webviews.delete(handle);
});
}
};
}
private onActiveEditorChanged() {
const activeEditor = this._editorService.activeControl;
let newActiveWebview: { input: WebviewEditorInput, handle: WebviewPanelHandle } | undefined = undefined;
if (activeEditor && activeEditor.input instanceof WebviewEditorInput) {
for (const handle of map.keys(this._webviews)) {
const input = this._webviews.get(handle)!;
if (input.matches(activeEditor.input)) {
newActiveWebview = { input, handle };
break;
}
}
}
if (newActiveWebview && newActiveWebview.handle === this._activeWebview) {
// Webview itself unchanged but position may have changed
this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
active: true,
visible: true,
position: editorGroupToViewColumn(this._editorGroupService, newActiveWebview.input.group || 0)
});
return;
}
// Broadcast view state update for currently active
if (typeof this._activeWebview !== 'undefined') {
const oldActiveWebview = this._webviews.get(this._activeWebview);
if (oldActiveWebview) {
this._proxy.$onDidChangeWebviewPanelViewState(this._activeWebview, {
active: false,
visible: this._editorService.visibleControls.some(editor => !!editor.input && editor.input.matches(oldActiveWebview)),
position: editorGroupToViewColumn(this._editorGroupService, oldActiveWebview.group || 0),
});
}
}
// Then for newly active
if (newActiveWebview) {
this._proxy.$onDidChangeWebviewPanelViewState(newActiveWebview.handle, {
active: true,
visible: true,
position: editorGroupToViewColumn(this._editorGroupService, activeEditor ? activeEditor.group : ACTIVE_GROUP),
});
this._activeWebview = newActiveWebview.handle;
} else {
this._activeWebview = undefined;
}
}
private onVisibleEditorsChanged(): void {
this._webviews.forEach((input, handle) => {
for (const workbenchEditor of this._editorService.visibleControls) {
if (workbenchEditor.input && workbenchEditor.input.matches(input)) {
const editorPosition = editorGroupToViewColumn(this._editorGroupService, workbenchEditor.group!);
input.updateGroup(workbenchEditor.group!.id);
this._proxy.$onDidChangeWebviewPanelViewState(handle, {
active: handle === this._activeWebview,
visible: true,
position: editorPosition
});
break;
}
}
});
}
private onDidClickLink(handle: WebviewPanelHandle, link: URI): void {
if (!link) {
return;
}
const webview = this.getWebview(handle);
if (this.isSupportedLink(webview, link)) {
this._openerService.open(link);
}
}
private isSupportedLink(webview: WebviewEditorInput, link: URI): boolean {
if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
return true;
}
return !!webview.options.enableCommandUris && link.scheme === 'command';
}
private getWebview(handle: WebviewPanelHandle): WebviewEditorInput {
const webview = this._webviews.get(handle);
if (!webview) {
throw new Error('Unknown webview handle:' + handle);
}
return webview;
}
private getWebviewElement(handle: number): WebviewElement {
const webview = this._webviewsElements.get(handle);
if (!webview) {
throw new Error('Unknown webview handle:' + handle);
}
return webview;
}
private static getDeserializationFailedContents(viewType: string) {
return `<!DOCTYPE html>
<html>
<head>
<base href="https://code.visualstudio.com/raw/">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-core-resource: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
</head>
<body>${localize('errorMessage', "An error occurred while restoring view:{0}", viewType)}</body>
</html>`;
}
}
function reviveWebviewOptions(options: WebviewInputOptions): WebviewInputOptions {
return {
...options,
localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
};
}
function reviveWebviewIcon(
value: { light: UriComponents, dark: UriComponents } | undefined
): { light: URI, dark: URI } | undefined {
if (!value) {
return undefined;
}
return {
light: URI.revive(value.light),
dark: URI.revive(value.dark)
};
}

View file

@ -67,6 +67,7 @@ import { withNullAsUndefined } from 'vs/base/common/types';
import { values } from 'vs/base/common/collections';
import { Schemas } from 'vs/base/common/network';
import { IURITransformer } from 'vs/base/common/uriIpc';
import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets';
export interface IExtensionApiFactory {
(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode;
@ -109,6 +110,7 @@ export function createApiFactory(
const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService));
rpcProtocol.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace);
rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration);
const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors));
const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol));
const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics, extHostLogService));
const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures));
@ -306,10 +308,6 @@ export function createApiFactory(
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
},
registerCodeInsetProvider(selector: vscode.DocumentSelector, provider: vscode.CodeInsetProvider): vscode.Disposable {
checkProposedApiEnabled(extension);
return extHostLanguageFeatures.registerCodeInsetProvider(extension, checkSelector(selector), provider);
},
registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable {
return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider);
},
@ -486,6 +484,10 @@ export function createApiFactory(
createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel {
return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options);
},
createWebviewTextEditorInset(editor: vscode.TextEditor, range: vscode.Range, options: vscode.WebviewOptions): vscode.WebviewEditorInset {
checkProposedApiEnabled(extension);
return extHostEditorInsets.createWebviewEditorInset(editor, range, options);
},
createTerminal(nameOrOptions?: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal {
if (typeof nameOrOptions === 'object') {
nameOrOptions.runInBackground = nameOrOptions.runInBackground && extension.enableProposedApi;

View file

@ -1,70 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITextModel } from 'vs/editor/common/model';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { mergeSort } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry';
import { ProviderResult } from 'vs/editor/common/modes';
import { IRange } from 'vs/editor/common/core/range';
export interface ICodeInsetSymbol {
id: string;
range: IRange;
height?: number;
}
export interface CodeInsetProvider {
onDidChange?: Event<this>;
provideCodeInsets(model: ITextModel, token: CancellationToken): ProviderResult<ICodeInsetSymbol[]>;
resolveCodeInset(model: ITextModel, codeInset: ICodeInsetSymbol, token: CancellationToken): ProviderResult<ICodeInsetSymbol>;
}
export const CodeInsetProviderRegistry = new LanguageFeatureRegistry<CodeInsetProvider>();
export interface ICodeInsetData {
symbol: ICodeInsetSymbol;
provider: CodeInsetProvider;
resolved?: boolean;
}
export function getCodeInsetData(model: ITextModel, token: CancellationToken): Promise<ICodeInsetData[]> {
const symbols: ICodeInsetData[] = [];
const providers = CodeInsetProviderRegistry.ordered(model);
const promises = providers.map(provider =>
Promise.resolve(provider.provideCodeInsets(model, token)).then(result => {
if (Array.isArray(result)) {
for (let symbol of result) {
symbols.push({ symbol, provider });
}
}
}).catch(onUnexpectedExternalError));
return Promise.all(promises).then(() => {
return mergeSort(symbols, (a, b) => {
// sort by lineNumber, provider-rank, and column
if (a.symbol.range.startLineNumber < b.symbol.range.startLineNumber) {
return -1;
} else if (a.symbol.range.startLineNumber > b.symbol.range.startLineNumber) {
return 1;
} else if (providers.indexOf(a.provider) < providers.indexOf(b.provider)) {
return -1;
} else if (providers.indexOf(a.provider) > providers.indexOf(b.provider)) {
return 1;
} else if (a.symbol.range.startColumn < b.symbol.range.startColumn) {
return -1;
} else if (a.symbol.range.startColumn > b.symbol.range.startColumn) {
return 1;
} else {
return 0;
}
});
});
}

View file

@ -1,353 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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<string, (element: WebviewElement) => any>();
private _currentFindCodeInsetSymbolsPromise: CancelablePromise<ICodeInsetData[]> | null;
private _modelChangeCounter: number;
private _currentResolveCodeInsetSymbolsPromise: CancelablePromise<any> | null;
private _detectVisibleInsets: RunOnceScheduler;
constructor(
private _editor: editorBrowser.ICodeEditor,
@IConfigurationService private readonly _configService: IConfigurationService,
) {
this._isEnabled = this._configService.getValue<boolean>('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<boolean>('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<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
id: 'editor',
properties: {
// ['editor.codeInsets']: {
// description: localize('editor.codeInsets', "Enable/disable editor code insets"),
// type: 'boolean',
// default: false
// }
}
});

View file

@ -1,8 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .code-inset {
z-index: 10;
}

View file

@ -1,193 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./codeInsetWidget';
import { Range } from 'vs/editor/common/core/range';
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
import { ICodeInsetData } from '../common/codeInset';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { IModelDeltaDecoration, IModelDecorationsChangeAccessor, ITextModel } from 'vs/editor/common/model';
import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement';
export interface IDecorationIdCallback {
(decorationId: string): void;
}
export class CodeInsetHelper {
private _removeDecorations: string[];
private _addDecorations: IModelDeltaDecoration[];
private _addDecorationsCallbacks: IDecorationIdCallback[];
constructor() {
this._removeDecorations = [];
this._addDecorations = [];
this._addDecorationsCallbacks = [];
}
addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {
this._addDecorations.push(decoration);
this._addDecorationsCallbacks.push(callback);
}
removeDecoration(decorationId: string): void {
this._removeDecorations.push(decorationId);
}
commit(changeAccessor: IModelDecorationsChangeAccessor): void {
let resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
for (let i = 0, len = resultingDecorations.length; i < len; i++) {
this._addDecorationsCallbacks[i](resultingDecorations[i]);
}
}
}
export class CodeInsetWidget {
private readonly _editor: editorBrowser.ICodeEditor;
private _webview: WebviewElement;
private _viewZone: editorBrowser.IViewZone;
private _viewZoneId?: number = undefined;
private _decorationIds: string[];
private _data: ICodeInsetData[];
private _range: Range;
constructor(
data: ICodeInsetData[], // all the insets on the same line (often just one)
editor: editorBrowser.ICodeEditor,
helper: CodeInsetHelper
) {
this._editor = editor;
this._data = data;
this._decorationIds = new Array<string>(this._data.length);
this._data.forEach((codeInsetData, i) => {
helper.addDecoration({
range: codeInsetData.symbol.range,
options: ModelDecorationOptions.EMPTY
}, id => this._decorationIds[i] = id);
// the range contains all insets on this line
if (!this._range) {
this._range = Range.lift(codeInsetData.symbol.range);
} else {
this._range = Range.plusRange(this._range, codeInsetData.symbol.range);
}
});
}
public dispose(helper: CodeInsetHelper, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor | null): void {
while (this._decorationIds.length) {
const decoration = this._decorationIds.pop();
if (decoration) {
helper.removeDecoration(decoration);
}
}
if (viewZoneChangeAccessor) {
if (typeof this._viewZoneId !== 'undefined') {
viewZoneChangeAccessor.removeZone(this._viewZoneId);
}
this._viewZone = undefined!;
}
if (this._webview) {
this._webview.dispose();
}
}
public isValid(): boolean {
return this._editor.hasModel() && this._decorationIds.some((id, i) => {
const range = this._editor.getModel()!.getDecorationRange(id);
const symbol = this._data[i].symbol;
return !!range && Range.isEmpty(symbol.range) === range.isEmpty();
});
}
public updateCodeInsetSymbols(data: ICodeInsetData[], helper: CodeInsetHelper): void {
while (this._decorationIds.length) {
const decoration = this._decorationIds.pop();
if (decoration) {
helper.removeDecoration(decoration);
}
}
this._data = data;
this._decorationIds = new Array<string>(this._data.length);
this._data.forEach((codeInsetData, i) => {
helper.addDecoration({
range: codeInsetData.symbol.range,
options: ModelDecorationOptions.EMPTY
}, id => this._decorationIds[i] = id);
});
}
public computeIfNecessary(model: ITextModel): ICodeInsetData[] {
// Read editor current state
for (let i = 0; i < this._decorationIds.length; i++) {
const range = model.getDecorationRange(this._decorationIds[i]);
if (range) {
this._data[i].symbol.range = range;
}
}
return this._data;
}
public getLineNumber(): number {
if (this._editor.hasModel()) {
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
if (range) {
return range.startLineNumber;
}
}
return -1;
}
public adoptWebview(webview: WebviewElement): void {
const lineNumber = this._range.endLineNumber;
this._editor.changeViewZones(accessor => {
if (this._viewZoneId) {
accessor.removeZone(this._viewZoneId);
this._webview.dispose();
}
const div = document.createElement('div');
div.className = 'code-inset';
webview.mountTo(div);
webview.onMessage((e: { type: string, payload: any }) => {
// The webview contents can use a "size-info" message to report its size.
if (e && e.type === 'size-info') {
const margin = e.payload.height > 0 ? 5 : 0;
this._viewZone.heightInPx = e.payload.height + margin;
this._editor.changeViewZones(accessor => {
if (this._viewZoneId) {
accessor.layoutZone(this._viewZoneId);
}
});
}
});
this._viewZone = {
afterLineNumber: lineNumber,
heightInPx: 50,
domNode: div
};
this._viewZoneId = accessor.addZone(this._viewZone);
this._webview = webview;
});
}
public reposition(viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
if (this.isValid() && this._editor.hasModel()) {
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
if (range) {
this._viewZone.afterLineNumber = range.endLineNumber;
}
if (this._viewZoneId) {
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
}
}
}
}

View file

@ -327,4 +327,4 @@ export class RevivedWebviewEditorInput extends WebviewEditorInput {
}
return super.resolve();
}
}
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { MainThreadWebviews } from 'vs/workbench/api/electron-browser/mainThreadWebview';
import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview';
import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview';
import { mock } from 'vs/workbench/test/electron-browser/api/mock';
import * as vscode from 'vscode';

View file

@ -7,7 +7,7 @@
import 'vs/editor/editor.all';
import 'vs/workbench/api/electron-browser/extensionHost.contribution';
import 'vs/workbench/api/browser/extensionHost.contribution';
import 'vs/workbench/electron-browser/main.contribution';
import 'vs/workbench/browser/workbench.contribution';
@ -324,9 +324,6 @@ import 'vs/workbench/contrib/outline/browser/outline.contribution';
// Experiments
import 'vs/workbench/contrib/experiments/electron-browser/experiments.contribution';
// Code Insets
import 'vs/workbench/contrib/codeinset/electron-browser/codeInset.contribution';
// Issues
import 'vs/workbench/contrib/issue/electron-browser/issue.contribution';

View file

@ -322,9 +322,6 @@ import 'vs/workbench/contrib/outline/browser/outline.contribution';
// Experiments
// import 'vs/workbench/contrib/experiments/electron-browser/experiments.contribution';
// Code Insets
// import 'vs/workbench/contrib/codeinset/electron-browser/codeInset.contribution';
// Issues
// import 'vs/workbench/contrib/issue/electron-browser/issue.contribution';