diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 2ad45164371c4..9159859427b60 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -38,6 +38,34 @@ declare module 'vscode' { //#endregion + // #region Joh - code insets + + /** + */ + export class CodeInset { + range: Range; + height?: number; + constructor(range: Range, height?: number); + } + + export interface CodeInsetProvider { + onDidChangeCodeInsets?: Event; + provideCodeInsets(document: TextDocument, token: CancellationToken): ProviderResult; + resolveCodeInset(codeInset: CodeInset, webview: Webview, token: CancellationToken): ProviderResult; + } + + export namespace languages { + + /** + * Register a code inset provider. + * + */ + export function registerCodeInsetProvider(selector: DocumentSelector, provider: CodeInsetProvider): Disposable; + } + + //#endregion + + //#region Joh - selection range provider export class SelectionRangeKind { diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 48f55cbce28ba..20854ecf66bc9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -11,7 +11,7 @@ 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 } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, WorkspaceSymbolDto, CodeActionDto, reviveWorkspaceEditDto, ISerializedDocumentFilter, DefinitionLinkDto, ISerializedSignatureHelpProviderMetadata, CodeInsetDto } from '../node/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IHeapService } from './mainThreadHeapService'; @@ -20,6 +20,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import { URI } from 'vs/base/common/uri'; import { Selection } from 'vs/editor/common/core/selection'; +import * as codeInset from 'vs/workbench/contrib/codeinset/codeInset'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -160,6 +161,35 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } } + // -- code inset + + $registerCodeInsetSupport(handle: number, selector: ISerializedDocumentFilter[], eventHandle: number): void { + + const provider = { + provideCodeInsets: (model: ITextModel, token: CancellationToken): CodeInsetDto[] | Thenable => { + 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 => { + 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(); + this._registrations[eventHandle] = emitter; + provider.onDidChange = emitter.event; + } + + const langSelector = typeConverters.LanguageSelector.from(selector); + this._registrations[handle] = codeInset.CodeInsetProviderRegistry.register(langSelector, provider); + } + // --- declaration $registerDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index d88f7ee8162ba..2c740a314d4fb 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostContext, ExtHostWebviewsShape, IExtHostContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelShowOptions, WebviewInsetHandle } from 'vs/workbench/api/node/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/shared/editor'; import { WebviewEditor } from 'vs/workbench/contrib/webview/electron-browser/webviewEditor'; import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput'; @@ -21,6 +21,11 @@ import { extHostNamedCustomer } from './extHostCustomers'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { CodeInsetController } from 'vs/workbench/contrib/codeinset/codeInset.contribution'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { @@ -35,6 +40,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv private readonly _proxy: ExtHostWebviewsShape; private readonly _webviews = new Map(); + private readonly _webviewsElements = new Map(); private readonly _revivers = new Set(); private _activeWebview: WebviewPanelHandle | undefined = undefined; @@ -47,7 +53,10 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv @IWebviewEditorService private readonly _webviewService: IWebviewEditorService, @IOpenerService private readonly _openerService: IOpenerService, @IExtensionService private readonly _extensionService: IExtensionService, - @ITelemetryService private readonly _telemetryService: ITelemetryService + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IPartService private readonly _partService: IPartService, ) { this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); _editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._toDispose); @@ -96,6 +105,44 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value }); } + $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: vscode.WebviewOptions, 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, + this._partService.getContainer(Parts.EDITOR_PART), + { + useSameOriginForRoot: true, + extensionLocation: URI.revive(extensionLocation) + }, + { + 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(); @@ -111,14 +158,22 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv webview.iconPath = reviveWebviewIcon(value); } - public $setHtml(handle: WebviewPanelHandle, value: string): void { - const webview = this.getWebview(handle); - webview.html = value; + public $setHtml(handle: WebviewPanelHandle | WebviewInsetHandle, value: string): void { + if (typeof handle === 'number') { + this._webviewsElements.get(handle).contents = value; + } else { + const webview = this.getWebview(handle); + webview.html = value; + } } - public $setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void { - const webview = this.getWebview(handle); - webview.setOptions(reviveWebviewOptions(options)); + public $setOptions(handle: WebviewPanelHandle | WebviewInsetHandle, options: vscode.WebviewOptions): void { + if (typeof handle === 'number') { + this._webviewsElements.get(handle).options = reviveWebviewOptions(options); + } else { + const webview = this.getWebview(handle); + webview.setOptions(reviveWebviewOptions(options)); + } } public $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void { @@ -132,18 +187,24 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._webviewService.revealWebview(webview, targetGroup || this._editorGroupService.getGroup(webview.group), !!showOptions.preserveFocus); } - public $postMessage(handle: WebviewPanelHandle, message: any): Promise { - 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)); + public $postMessage(handle: WebviewPanelHandle | WebviewInsetHandle, message: any): Promise { + if (typeof handle === 'number') { + this._webviewsElements.get(handle).sendMessage(message); + return Promise.resolve(true); - for (const editor of editors) { - editor.sendMessage(message); - } + } 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)); + + for (const editor of editors) { + editor.sendMessage(message); + } - return Promise.resolve(editors.length > 0); + return Promise.resolve(editors.length > 0); + } } public $registerSerializer(viewType: string): void { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 1854d3639a21e..c590299823d93 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -304,6 +304,10 @@ 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); }, @@ -474,7 +478,7 @@ export function createApiFactory( return extHostOutputService.createOutputChannel(name); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebview(extension, viewType, title, showOptions, options); + return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); }, createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { if (typeof nameOrOptions === 'object') { @@ -754,6 +758,7 @@ export function createApiFactory( CodeActionKind: extHostTypes.CodeActionKind, CodeActionTrigger: extHostTypes.CodeActionTrigger, CodeLens: extHostTypes.CodeLens, + CodeInset: extHostTypes.CodeInset, Color: extHostTypes.Color, ColorInformation: extHostTypes.ColorInformation, ColorPresentation: extHostTypes.ColorPresentation, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index a366eb27bda94..68596ed1466ee 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -45,6 +45,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteConsoleLog } from 'vs/base/node/console'; +import * as codeInset from 'vs/workbench/contrib/codeinset/codeInset'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -299,6 +300,7 @@ 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): void; $emitCodeLensEvent(eventHandle: number, event?: any): void; $registerDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void; $registerDeclarationSupport(handle: number, selector: ISerializedDocumentFilter[]): void; @@ -467,6 +469,8 @@ export interface MainThreadTelemetryShape extends IDisposable { export type WebviewPanelHandle = string; +export type WebviewInsetHandle = number; + export interface WebviewPanelShowOptions { readonly viewColumn?: EditorViewColumn; readonly preserveFocus?: boolean; @@ -474,13 +478,15 @@ export interface WebviewPanelShowOptions { export interface MainThreadWebviewsShape extends IDisposable { $createWebviewPanel(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void; + $createWebviewCodeInset(handle: WebviewInsetHandle, symbolId: string, options: vscode.WebviewOptions, extensionLocation: UriComponents): 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, value: string): void; - $setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void; - $postMessage(handle: WebviewPanelHandle, value: any): Promise; + + $setHtml(handle: WebviewPanelHandle | WebviewInsetHandle, value: string): void; + $setOptions(handle: WebviewPanelHandle | WebviewInsetHandle, options: vscode.WebviewOptions): void; + $postMessage(handle: WebviewPanelHandle | WebviewInsetHandle, value: any): Promise; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; @@ -892,10 +898,14 @@ export interface CodeLensDto extends ObjectIdentifier { command?: CommandDto; } +export type CodeInsetDto = ObjectIdentifier & codeInset.ICodeInsetSymbol; + export interface ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideCodeLenses(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveCodeLens(handle: number, resource: UriComponents, symbol: CodeLensDto, token: CancellationToken): Promise; + $provideCodeInsets(handle: number, resource: UriComponents, token: CancellationToken): Promise; + $resolveCodeInset(handle: number, resource: UriComponents, symbol: CodeInsetDto, token: CancellationToken): Promise; $provideDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideDeclaration(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 9a1f33723b366..5afbf25903db7 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -15,7 +15,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { asPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto } from './extHost.protocol'; +import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, WorkspaceSymbolDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto, ISerializedDocumentFilter, WorkspaceEditDto, ISerializedSignatureHelpProviderMetadata, LinkDto, CodeLensDto, MainThreadWebviewsShape, CodeInsetDto } 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'; @@ -26,6 +26,9 @@ import { IExtensionDescription } from 'vs/workbench/services/extensions/common/e import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtHostWebview } from 'vs/workbench/api/node/extHostWebview'; +import * as codeInset from 'vs/workbench/contrib/codeinset/codeInset'; +import { generateUuid } from 'vs/base/common/uuid'; // --- adapter @@ -144,6 +147,47 @@ class CodeLensAdapter { } } +class CodeInsetAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _heapService: ExtHostHeapService, + private readonly _provider: vscode.CodeInsetProvider + ) { } + + provideCodeInsets(resource: URI, token: CancellationToken): Promise { + const doc = this._documents.getDocumentData(resource).document; + 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 { + + const inset = this._heapService.get(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[] { if (Array.isArray(value)) { return (value as (vscode.DefinitionLink | vscode.Location)[]).map(typeConvert.DefinitionLink.from); @@ -916,7 +960,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter - | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter; + | ColorProviderAdapter | FoldingProviderAdapter | CodeInsetAdapter | DeclarationAdapter | SelectionRangeAdapter; class AdapterData { constructor( @@ -941,6 +985,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { private _diagnostics: ExtHostDiagnostics; private _adapter = new Map(); private readonly _logService: ILogService; + private _webviewProxy: MainThreadWebviewsShape; constructor( mainContext: IMainContext, @@ -958,6 +1003,7 @@ 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): ISerializedDocumentFilter[] { @@ -1007,7 +1053,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return ExtHostLanguageFeatures._handlePool++; } - private _withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A) => Promise): Promise { + private _withAdapter(handle: number, ctor: { new(...args: any[]): A }, callback: (adapter: A, extenson: IExtensionDescription) => Promise): Promise { const data = this._adapter.get(handle); if (!data) { return Promise.reject(new Error('no adapter found')); @@ -1019,7 +1065,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { t1 = Date.now(); this._logService.trace(`[${data.extension.identifier.value}] INVOKE provider '${(ctor as any).name}'`); } - let p = callback(data.adapter); + let p = callback(data.adapter, data.extension); const extension = data.extension; if (extension) { Promise.resolve(p).then( @@ -1084,6 +1130,37 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, CodeLensAdapter, adapter => adapter.resolveCodeLens(URI.revive(resource), symbol, token)); } + // --- 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) { + const subscription = provider.onDidChangeCodeInsets(_ => this._proxy.$emitCodeLensEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + + return result; + } + + $provideCodeInsets(handle: number, resource: UriComponents, token: CancellationToken): Promise { + return this._withAdapter(handle, CodeInsetAdapter, adapter => adapter.provideCodeInsets(URI.revive(resource), token)); + } + + $resolveCodeInset(handle: number, _resource: UriComponents, symbol: codeInset.ICodeInsetSymbol, token: CancellationToken): Promise { + 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.extensionLocation); + return adapter.resolveCodeInset(symbol, webview, token); + }); + } + // --- declaration registerDefinitionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 08e61091cfd3a..625ba1018ee82 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1136,6 +1136,19 @@ export class CodeLens { } } + +export class CodeInset { + + range: Range; + height?: number; + + constructor(range: Range, height?: number) { + this.range = range; + this.height = height; + } +} + + @es5ClassCompat export class MarkdownString { diff --git a/src/vs/workbench/api/node/extHostWebview.ts b/src/vs/workbench/api/node/extHostWebview.ts index f3851ed60885f..65fab620a3606 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -8,14 +8,14 @@ import { URI } from 'vs/base/common/uri'; import * as typeConverters from 'vs/workbench/api/node/extHostTypeConverters'; import { EditorViewColumn } from 'vs/workbench/api/shared/editor'; import * as vscode from 'vscode'; -import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState } from './extHost.protocol'; +import { ExtHostWebviewsShape, IMainContext, MainContext, MainThreadWebviewsShape, WebviewPanelHandle, WebviewPanelViewState, WebviewInsetHandle } from './extHost.protocol'; import { Disposable } from './extHostTypes'; import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; type IconPath = URI | { light: URI, dark: URI }; export class ExtHostWebview implements vscode.Webview { - private readonly _handle: WebviewPanelHandle; + private readonly _handle: WebviewPanelHandle | WebviewInsetHandle; private readonly _proxy: MainThreadWebviewsShape; private _html: string; private _options: vscode.WebviewOptions; @@ -25,7 +25,7 @@ export class ExtHostWebview implements vscode.Webview { public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; constructor( - handle: WebviewPanelHandle, + handle: WebviewPanelHandle | WebviewInsetHandle, proxy: MainThreadWebviewsShape, options: vscode.WebviewOptions ) { @@ -243,7 +243,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadWebviews); } - public createWebview( + public createWebviewPanel( extension: IExtensionDescription, viewType: string, title: string, diff --git a/src/vs/workbench/contrib/codeinset/codeInset.contribution.ts b/src/vs/workbench/contrib/codeinset/codeInset.contribution.ts new file mode 100644 index 0000000000000..57e84679755aa --- /dev/null +++ b/src/vs/workbench/contrib/codeinset/codeInset.contribution.ts @@ -0,0 +1,352 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './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 { localize } from 'vs/nls.mock'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; + +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; + private _modelChangeCounter: number; + private _currentResolveCodeInsetSymbolsPromise: CancelablePromise; + 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 { + if (this._pendingWebviews.has(symbolId)) { + this._pendingWebviews.get(symbolId)(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, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): 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.getModel()) { + return; + } + + let maxLineNumber = this._editor.getModel().getLineCount(); + let groups: ICodeInsetData[][] = []; + let lastGroup: ICodeInsetData[]; + + 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(void 0); + } + 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 + } + } +}); diff --git a/src/vs/workbench/contrib/codeinset/codeInset.ts b/src/vs/workbench/contrib/codeinset/codeInset.ts new file mode 100644 index 0000000000000..cb37c59d8f9d6 --- /dev/null +++ b/src/vs/workbench/contrib/codeinset/codeInset.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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; + provideCodeInsets(model: ITextModel, token: CancellationToken): ProviderResult; + resolveCodeInset(model: ITextModel, codeInset: ICodeInsetSymbol, token: CancellationToken): ProviderResult; +} + +export const CodeInsetProviderRegistry = new LanguageFeatureRegistry(); + +export interface ICodeInsetData { + symbol: ICodeInsetSymbol; + provider: CodeInsetProvider; + resolved?: boolean; +} + +export function getCodeInsetData(model: ITextModel, token: CancellationToken): Promise { + + 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; + } + }); + }); +} diff --git a/src/vs/workbench/contrib/codeinset/codeInsetWidget.css b/src/vs/workbench/contrib/codeinset/codeInsetWidget.css new file mode 100644 index 0000000000000..113a5a1fbb440 --- /dev/null +++ b/src/vs/workbench/contrib/codeinset/codeInsetWidget.css @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .codelens-decoration { + overflow: hidden; + display: inline-block; + text-overflow: ellipsis; +} + +.monaco-editor .codelens-decoration > span, +.monaco-editor .codelens-decoration > a { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + white-space: nowrap; + vertical-align: sub; +} + +.monaco-editor .codelens-decoration > a { + text-decoration: none; +} + +.monaco-editor .codelens-decoration > a:hover { + text-decoration: underline; + cursor: pointer; +} + +.monaco-editor .codelens-decoration.invisible-cl { + opacity: 0; +} + +@keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } +@-moz-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } +@-o-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } +@-webkit-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } } + +.monaco-editor .codelens-decoration.fadein { + -webkit-animation: fadein 0.5s linear; + -moz-animation: fadein 0.5s linear; + -o-animation: fadein 0.5s linear; + animation: fadein 0.5s linear; +} diff --git a/src/vs/workbench/contrib/codeinset/codeInsetWidget.ts b/src/vs/workbench/contrib/codeinset/codeInsetWidget.ts new file mode 100644 index 0000000000000..12562860a9a33 --- /dev/null +++ b/src/vs/workbench/contrib/codeinset/codeInsetWidget.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * 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 './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(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): void { + while (this._decorationIds.length) { + helper.removeDecoration(this._decorationIds.pop()); + } + if (viewZoneChangeAccessor) { + viewZoneChangeAccessor.removeZone(this._viewZoneId); + this._viewZone = undefined; + } + if (this._webview) { + this._webview.dispose(); + } + } + + public isValid(): boolean { + return 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) { + helper.removeDecoration(this._decorationIds.pop()); + } + this._data = data; + this._decorationIds = new Array(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 { + 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'); + 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 => { + 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]); + this._viewZone.afterLineNumber = range.endLineNumber; + viewZoneChangeAccessor.layoutZone(this._viewZoneId); + } + } +} diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 79cda62b62dbf..4f2604715a045 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -30,6 +30,7 @@ export interface WebviewContentOptions { readonly allowScripts?: boolean; readonly svgWhiteList?: string[]; readonly localResourceRoots?: ReadonlyArray; + readonly disableFindView?: boolean; } interface IKeydownEvent { @@ -347,14 +348,18 @@ export class WebviewElement extends Disposable { this._send('devtools-opened'); })); - this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); + if (!this.options || !this.options.disableFindView) { + this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); + } this.style(this._themeService.getTheme()); this._register(this._themeService.onThemeChange(this.style, this)); } public mountTo(parent: HTMLElement) { - parent.appendChild(this._webviewFindWidget.getDomNode()!); + if (this._webviewFindWidget) { + parent.appendChild(this._webviewFindWidget.getDomNode()!); + } parent.appendChild(this._webview); } @@ -482,8 +487,9 @@ export class WebviewElement extends Disposable { const activeTheme = ApiThemeClassName.fromTheme(theme); this._send('styles', styles, activeTheme); - this._webviewFindWidget.updateTheme(theme); - + if (this._webviewFindWidget) { + this._webviewFindWidget.updateTheme(theme); + } } public layout(): void { @@ -551,11 +557,15 @@ export class WebviewElement extends Disposable { } public showFind() { - this._webviewFindWidget.reveal(); + if (this._webviewFindWidget) { + this._webviewFindWidget.reveal(); + } } public hideFind() { - this._webviewFindWidget.hide(); + if (this._webviewFindWidget) { + this._webviewFindWidget.hide(); + } } public reload() { diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index debe4a9b00320..50f88b96a4b45 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -175,6 +175,9 @@ 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/codeInset.contribution'; + // Issues import 'vs/workbench/contrib/issue/electron-browser/issue.contribution';