diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index d8ee1d9226b..ad236835214 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -600,6 +600,11 @@ export interface IEditorOptions { * Defaults to true. */ codeLens?: boolean; + /** + * Show code insets + * Defaults to true. + */ + codeInsets?: boolean; /** * Control the behavior and rendering of the code action lightbulb. */ @@ -996,6 +1001,7 @@ export interface EditorContribOptions { readonly selectionHighlight: boolean; readonly occurrencesHighlight: boolean; readonly codeLens: boolean; + readonly codeInsets: boolean; readonly folding: boolean; readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; @@ -2051,6 +2057,7 @@ export class EditorOptionsValidator { selectionHighlight: _boolean(opts.selectionHighlight, defaults.selectionHighlight), occurrencesHighlight: _boolean(opts.occurrencesHighlight, defaults.occurrencesHighlight), codeLens: _boolean(opts.codeLens, defaults.codeLens), + codeInsets: _boolean(opts.codeInsets, defaults.codeInsets), folding: _boolean(opts.folding, defaults.folding), foldingStrategy: _stringSet<'auto' | 'indentation'>(opts.foldingStrategy, defaults.foldingStrategy, ['auto', 'indentation']), showFoldingControls: _stringSet<'always' | 'mouseover'>(opts.showFoldingControls, defaults.showFoldingControls, ['always', 'mouseover']), @@ -2164,6 +2171,7 @@ export class InternalEditorOptionsFactory { selectionHighlight: (accessibilityIsOn ? false : opts.contribInfo.selectionHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED occurrencesHighlight: (accessibilityIsOn ? false : opts.contribInfo.occurrencesHighlight), // DISABLED WHEN SCREEN READER IS ATTACHED codeLens: (accessibilityIsOn ? false : opts.contribInfo.codeLens), // DISABLED WHEN SCREEN READER IS ATTACHED + codeInsets: (accessibilityIsOn ? false : opts.contribInfo.codeInsets), folding: (accessibilityIsOn ? false : opts.contribInfo.folding), // DISABLED WHEN SCREEN READER IS ATTACHED foldingStrategy: opts.contribInfo.foldingStrategy, showFoldingControls: opts.contribInfo.showFoldingControls, @@ -2653,6 +2661,7 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { selectionHighlight: true, occurrencesHighlight: true, codeLens: true, + codeInsets: true, folding: true, foldingStrategy: 'auto', showFoldingControls: 'mouseover', diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6126701924d..895cbc0a206 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -9,7 +9,7 @@ import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { isObject } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -1321,6 +1321,19 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ProviderResult; } +export interface ICodeInsetSymbol { + range: IRange; + id?: string; + height?: number; + webviewHandle?: string; +} +export interface CodeInsetProvider { + onDidChange?: Event; + extensionLocation: UriComponents; + provideCodeInsets(model: model.ITextModel, token: CancellationToken): ProviderResult; + resolveCodeInset?(model: model.ITextModel, codeInset: ICodeInsetSymbol, token: CancellationToken): ProviderResult; +} + // --- feature registries ------ /** @@ -1383,6 +1396,11 @@ export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const CodeInsetProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index fd3598b5a28..74e424d84c2 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -188,7 +188,7 @@ export class MarkerNavigationWidget extends ZoneWidget { } private _applyTheme(theme: ITheme) { - this._backgroundColor = theme.getColor(editorMarkerNavigationBackground); + this._backgroundColor = theme.getColor(editorMarkerNavigationBackground) || undefined; let colorId = editorMarkerNavigationError; if (this._severity === MarkerSeverity.Warning) { colorId = editorMarkerNavigationWarning; diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index 4fd0bbebf59..23068af099a 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -279,8 +279,8 @@ export class ReferenceWidget extends PeekViewWidget { arrowColor: borderColor, frameColor: borderColor, headerBackgroundColor: theme.getColor(peekViewTitleBackground) || Color.transparent, - primaryHeadingColor: theme.getColor(peekViewTitleForeground), - secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) + primaryHeadingColor: theme.getColor(peekViewTitleForeground) || undefined, + secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) || undefined }); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b994864125b..f3f4f799dc4 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2934,6 +2934,11 @@ declare namespace monaco.editor { * Defaults to true. */ codeLens?: boolean; + /** + * Show code insets + * Defaults to true. + */ + codeInsets?: boolean; /** * Control the behavior and rendering of the code action lightbulb. */ @@ -3271,6 +3276,7 @@ declare namespace monaco.editor { readonly selectionHighlight: boolean; readonly occurrencesHighlight: boolean; readonly codeLens: boolean; + readonly codeInsets: boolean; readonly folding: boolean; readonly foldingStrategy: 'auto' | 'indentation'; readonly showFoldingControls: 'always' | 'mouseover'; @@ -5414,6 +5420,20 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: ICodeLensSymbol, token: CancellationToken): ProviderResult; } + export interface ICodeInsetSymbol { + range: IRange; + id?: string; + height?: number; + webviewHandle?: string; + } + + export interface CodeInsetProvider { + onDidChange?: IEvent; + extensionLocation: UriComponents; + provideCodeInsets(model: editor.ITextModel, token: CancellationToken): ProviderResult; + resolveCodeInset?(model: editor.ITextModel, codeInset: ICodeInsetSymbol, token: CancellationToken): ProviderResult; + } + export interface ILanguageExtensionPoint { id: string; extensions?: string[]; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 978c31dd58c..de92b6b7a37 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2235,6 +2235,24 @@ declare module 'vscode' { resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; } + + /** + */ + 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; + } + + /** * Information about where a symbol is defined. * @@ -7855,6 +7873,12 @@ declare module 'vscode' { */ export function registerCodeLensProvider(selector: DocumentSelector, provider: CodeLensProvider): Disposable; + /** + * Register a code inset provider. + * + */ + export function registerCodeInsetProvider(selector: DocumentSelector, provider: CodeInsetProvider): Disposable; + /** * Register a definition provider. * diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 48f55cbce28..6a23c63ccd7 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 { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape { @@ -160,6 +161,36 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } } + // -- code inset + + $registerCodeInsetSupport(handle: number, selector: ISerializedDocumentFilter[], eventHandle: number, extension: IExtensionDescription): 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; + }); + }, + extensionLocation: extension.extensionLocation + }; + + 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] = modes.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 d88f7ee8162..5b063318d5e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -22,9 +22,14 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; + +export let mainThreadWebviews: MainThreadWebviews; + @extHostNamedCustomer(MainContext.MainThreadWebviews) export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviver { + _serviceBrand: any; + private static readonly viewType = 'mainThreadWebview'; private static readonly standardSupportedLinkSchemes = ['http', 'https', 'mailto']; @@ -49,6 +54,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv @IExtensionService private readonly _extensionService: IExtensionService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { + mainThreadWebviews = this; this._proxy = context.getProxy(ExtHostContext.ExtHostWebviews); _editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._toDispose); _editorService.onDidVisibleEditorsChange(this.onVisibleEditorsChanged, this, this._toDispose); @@ -64,7 +70,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._toDispose = dispose(this._toDispose); } - public $createWebviewPanel( + public $createWebview( handle: WebviewPanelHandle, viewType: string, title: string, @@ -96,6 +102,23 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extensionId.value }); } + public createInsetWebview( + handle: WebviewPanelHandle, + parent: HTMLElement, + options: WebviewInputOptions, + extensionLocation: UriComponents + ): WebviewEditorInput { + const webview = this._webviewService.createInsetWebview(parent, reviveWebviewOptions(options), URI.revive(extensionLocation), this.createWebviewEventDelegate(handle)); + webview.state = { + viewType: webview.viewType, + state: undefined + }; + + this._webviews.set(handle, webview); + this._activeWebview = handle; + return webview; + } + public $disposeWebview(handle: WebviewPanelHandle): void { const webview = this.getWebview(handle); webview.dispose(); @@ -289,7 +312,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv } } - private getWebview(handle: WebviewPanelHandle): WebviewEditorInput { + public getWebview(handle: WebviewPanelHandle): WebviewEditorInput { const webview = this._webviews.get(handle); if (!webview) { throw new Error('Unknown webview handle:' + handle); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 1854d3639a2..ab17d0ac50e 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -304,6 +304,9 @@ 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 { + return extHostLanguageFeatures.registerCodeInsetProvider(extension, checkSelector(selector), provider); + }, registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { return extHostLanguageFeatures.registerDefinitionProvider(extension, checkSelector(selector), provider); }, @@ -754,6 +757,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 a366eb27bda..cee76079207 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -299,6 +299,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, extension: IExtensionDescription): void; $emitCodeLensEvent(eventHandle: number, event?: any): void; $registerDefinitionSupport(handle: number, selector: ISerializedDocumentFilter[]): void; $registerDeclarationSupport(handle: number, selector: ISerializedDocumentFilter[]): void; @@ -473,7 +474,7 @@ 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; + $createWebview(handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): void; $disposeWebview(handle: WebviewPanelHandle): void; $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; $setTitle(handle: WebviewPanelHandle, value: string): void; @@ -499,6 +500,18 @@ export interface ExtHostWebviewsShape { $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: vscode.WebviewOptions): Promise; } +// export type CodeInsetWebviewHandle = string; + +// export interface ExtHostCodeInsetWebviewsShape { +// } + +// export interface ExtHostCodeInsetWebviewShape extends IDisposable { +// $setHtml(handle: WebviewPanelHandle, value: string): void; +// $setOptions(handle: WebviewPanelHandle, options: vscode.WebviewOptions): void; +// $postMessage(handle: WebviewPanelHandle, value: any): Promise; +// } + + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -892,10 +905,14 @@ export interface CodeLensDto extends ObjectIdentifier { command?: CommandDto; } +export type CodeInsetDto = ObjectIdentifier & modes.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; @@ -1157,6 +1174,7 @@ export const ExtHostContext = { ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), + // ExtHostCodeInsetWebviews: createExtId('ExtHostWebviews'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), ExtHostStorage: createMainId('ExtHostStorage'), diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index ef757ca94f0..fbee5b2cf77 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -146,6 +146,13 @@ export class ExtHostApiCommands { ], returns: 'A promise that resolves to an array of CodeLens-instances.' }); + this._register('vscode.executeCodeInsetProvider', this._executeCodeInsetProvider, { + description: 'Execute CodeInset provider.', + args: [ + { name: 'uri', description: 'Uri of a text document', constraint: URI } + ], + returns: 'A promise that resolves to an array of CodeInset-instances.' + }); this._register('vscode.executeFormatDocumentProvider', this._executeFormatDocumentProvider, { description: 'Execute document format provider.', args: [ @@ -514,6 +521,14 @@ export class ExtHostApiCommands { } + private _executeCodeInsetProvider(resource: URI): Thenable { + const args = { resource }; + return this._commands.executeCommand('_executeCodeInsetProvider', args) + .then(tryMapWith(item => + new types.CodeInset( + typeConverters.Range.to(item.range)))); + } + private _executeFormatDocumentProvider(resource: URI, options: vscode.FormattingOptions): Promise { const args = { resource, diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 9a1f33723b3..222f08069aa 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 } 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,7 @@ 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'; // --- adapter @@ -144,6 +145,52 @@ 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 id = this._heapService.keep(inset); + return ObjectIdentifier.mixin({ + range: typeConvert.Range.from(inset.range), + height: inset.height + }, id); + }); + } else { + return undefined; + } + }); + } + + resolveCodeInset(symbol: modes.ICodeInsetSymbol, webview: vscode.Webview, token: CancellationToken): Promise { + + const inset = this._heapService.get(ObjectIdentifier.of(symbol)); + if (!inset) { + return undefined; + } + + let resolve: Promise; + if (typeof this._provider.resolveCodeInset !== 'function') { + resolve = Promise.resolve(inset); + } else { + resolve = asPromise(() => this._provider.resolveCodeInset(inset, webview, token)); + } + + return resolve.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 +963,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 +988,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 +1006,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[] { @@ -1084,6 +1133,38 @@ 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, extension); + 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: modes.ICodeInsetSymbol, token: CancellationToken): Promise { + const webview = new ExtHostWebview(symbol.webviewHandle, this._webviewProxy, { enableScripts: true }); + webview.html = ''; + const x = this._withAdapter(handle, CodeInsetAdapter, adapter => adapter.resolveCodeInset(symbol, webview, token)); + return x; + } + + $createCodeInsetWebview(handle: number) { + } + // --- 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 08e61091cfd..f70a2acbe2b 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1136,6 +1136,21 @@ export class CodeLens { } } + +export class CodeInset { + + range: Range; + isResolved: boolean; + height?: number; + + constructor(range: Range, height?: number) { + this.range = range; + this.isResolved = false; + 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 f3851ed6088..2dfa8440244 100644 --- a/src/vs/workbench/api/node/extHostWebview.ts +++ b/src/vs/workbench/api/node/extHostWebview.ts @@ -257,7 +257,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { }; const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel(handle, viewType, title, webviewShowOptions, options, extension.identifier, extension.extensionLocation); + this._proxy.$createWebview(handle, viewType, title, webviewShowOptions, options, extension.identifier, extension.extensionLocation); const webview = new ExtHostWebview(handle, this._proxy, options); const panel = new ExtHostWebviewPanel(handle, this._proxy, viewType, title, viewColumn, options, webview); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts index 08ede924c14..0bc7afbd713 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -100,18 +100,18 @@ export class QuickInputBox { style(theme: ITheme) { this.inputBox.style({ - inputForeground: theme.getColor(inputForeground), - inputBackground: theme.getColor(inputBackground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), + inputForeground: theme.getColor(inputForeground) || undefined, + inputBackground: theme.getColor(inputBackground) || undefined, + inputBorder: theme.getColor(inputBorder) || undefined, + inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground) || undefined, + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground) || undefined, + inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder) || undefined, + inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground) || undefined, + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground) || undefined, + inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder) || undefined, + inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground) || undefined, + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground) || undefined, + inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) || undefined, }); } diff --git a/src/vs/workbench/contrib/files/electron-browser/views/explorerView.ts b/src/vs/workbench/contrib/files/electron-browser/views/explorerView.ts index bff9758fa30..495d10b9fc4 100644 --- a/src/vs/workbench/contrib/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/electron-browser/views/explorerView.ts @@ -344,6 +344,11 @@ export class ExplorerView extends ViewletPanel { if (e.browserEvent instanceof MouseEvent) { isDoubleClick = e.browserEvent.detail === 2; + + if (!this.tree.openOnSingleClick && !isDoubleClick) { + return; + } + isMiddleClick = e.browserEvent.button === 1; const isLeftButton = e.browserEvent.button === 0; diff --git a/src/vs/workbench/contrib/scm/electron-browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/electron-browser/dirtydiffDecorator.ts index 77f28716b69..327f5654149 100644 --- a/src/vs/workbench/contrib/scm/electron-browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/electron-browser/dirtydiffDecorator.ts @@ -365,8 +365,8 @@ class DirtyDiffWidget extends PeekViewWidget { arrowColor: borderColor, frameColor: borderColor, headerBackgroundColor: theme.getColor(peekViewTitleBackground) || Color.transparent, - primaryHeadingColor: theme.getColor(peekViewTitleForeground), - secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) + primaryHeadingColor: theme.getColor(peekViewTitleForeground) || undefined, + secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) || undefined }); } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js b/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js index 5fb945732cc..36d3759320d 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js +++ b/src/vs/workbench/contrib/webview/electron-browser/webview-pre.js @@ -243,7 +243,7 @@ delete window.frameElement; `; - newDocument.head.prepend(defaultScript, newDocument.head.firstChild); + newDocument.head.prepend(defaultScript); } // apply default styles diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts index 10dbe724d17..3bc86337748 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewEditorService.ts @@ -12,6 +12,8 @@ import * as vscode from 'vscode'; import { WebviewEditorInput } from './webviewEditorInput'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { equals } from 'vs/base/common/arrays'; +import { WebviewElement } from 'vs/workbench/contrib/webview/electron-browser/webviewElement'; +import { IPartService, Parts } from 'vs/workbench/services/part/common/partService'; export const IWebviewEditorService = createDecorator('webviewEditorService'); @@ -32,6 +34,13 @@ export interface IWebviewEditorService { events: WebviewEvents ): WebviewEditorInput; + createInsetWebview( + parent: HTMLElement, + options: vscode.WebviewOptions, + extensionLocation: URI, + events: WebviewEvents + ): WebviewEditorInput; + reviveWebview( viewType: string, id: number, @@ -97,6 +106,7 @@ export class WebviewEditorService implements IWebviewEditorService { @IEditorService private readonly _editorService: IEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, + @IPartService private readonly _partService: IPartService, ) { } createWebview( @@ -112,6 +122,21 @@ export class WebviewEditorService implements IWebviewEditorService { return webviewInput; } + createInsetWebview( + parent: HTMLElement, + options: vscode.WebviewOptions, + extensionLocation: URI, + events: WebviewEvents + ): WebviewEditorInput { + const webviewEditorInput = this._instantiationService.createInstance(WebviewEditorInput, 'codeinset', undefined, '', options, {}, events, extensionLocation, undefined); + webviewEditorInput.webview = this._instantiationService.createInstance(WebviewElement, + this._partService.getContainer(Parts.EDITOR_PART), + { allowSvgs: true, useSameOriginForRoot: true }, + { allowScripts: true, disableFindView: true }); + webviewEditorInput.webview.mountTo(parent); + return webviewEditorInput; + } + revealWebview( webview: WebviewEditorInput, group: IEditorGroup, diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 79cda62b62d..cc5a97d8898 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -13,12 +13,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; -import { areWebviewInputOptionsEqual } from './webviewEditorService'; import { WebviewFindWidget } from './webviewFindWidget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { endsWith } from 'vs/base/common/strings'; import { isMacintosh } from 'vs/base/common/platform'; +import { equals } from 'vs/base/common/arrays'; export interface WebviewOptions { readonly allowSvgs?: boolean; @@ -30,8 +30,23 @@ export interface WebviewContentOptions { readonly allowScripts?: boolean; readonly svgWhiteList?: string[]; readonly localResourceRoots?: ReadonlyArray; + readonly disableFindView?: boolean; } + +export function areWebviewContentOptionsEqual(a: WebviewContentOptions, b: WebviewContentOptions): boolean { + const sameArray = (a1: ReadonlyArray, a2: ReadonlyArray) => + (a.localResourceRoots === b.localResourceRoots + || (Array.isArray(a.localResourceRoots) && Array.isArray(b.localResourceRoots) + && equals(a.localResourceRoots, b.localResourceRoots, (a, b) => a.toString() === b.toString()))); + + return a.allowScripts === b.allowScripts + && a.disableFindView === b.disableFindView + && sameArray(a.svgWhiteList, b.svgWhiteList) + && sameArray(a.localResourceRoots, b.localResourceRoots); +} + + interface IKeydownEvent { key: string; keyCode: number; @@ -248,6 +263,7 @@ export class WebviewElement extends Disposable { this._webview.setAttribute('partition', `webview${Date.now()}`); this._webview.setAttribute('webpreferences', 'contextIsolation=yes'); + this._webview.setAttribute('autosize', 'on'); this._webview.style.flex = '0 1'; this._webview.style.width = '0'; @@ -347,14 +363,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); } @@ -382,6 +402,9 @@ export class WebviewElement extends Disposable { private readonly _onMessage = this._register(new Emitter()); public readonly onMessage = this._onMessage.event; + private readonly _onLayout = this._register(new Emitter<{ width: number, height: number }>()); + public readonly onLayout = this._onLayout.event; + private _send(channel: string, ...args: any[]): void { this._ready .then(() => this._webview.send(channel, ...args)) @@ -397,7 +420,7 @@ export class WebviewElement extends Disposable { } public set options(value: WebviewContentOptions) { - if (this._contentOptions && areWebviewInputOptionsEqual(value, this._contentOptions)) { + if (this._contentOptions && areWebviewContentOptionsEqual(value, this._contentOptions)) { return; } @@ -419,7 +442,7 @@ export class WebviewElement extends Disposable { } public update(value: string, options: WebviewContentOptions, retainContextWhenHidden: boolean) { - if (retainContextWhenHidden && value === this._contents && this._contentOptions && areWebviewInputOptionsEqual(options, this._contentOptions)) { + if (retainContextWhenHidden && value === this._contents && this._contentOptions && areWebviewContentOptionsEqual(options, this._contentOptions)) { return; } this._contents = value; @@ -482,8 +505,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 { @@ -501,6 +525,11 @@ export class WebviewElement extends Disposable { } contents.setZoomFactor(factor); + if (!this._webview || !this._webview.parentElement) { + return; + } + + this._onLayout.fire({ width: this._webview.clientWidth, height: this._webview.clientHeight }); }); } @@ -551,11 +580,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/parts/codeinset/codeInset.contribution.ts b/src/vs/workbench/parts/codeinset/codeInset.contribution.ts new file mode 100644 index 00000000000..813d6010a9d --- /dev/null +++ b/src/vs/workbench/parts/codeinset/codeInset.contribution.ts @@ -0,0 +1,335 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// import { Registry } from 'vs/platform/registry/common/platform'; +// import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +// import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +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 } from 'vs/editor/common/modes'; +import { CodeInsetWidget, CodeInsetHelper } from './codeInsetWidget'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { getCodeInsetData, ICodeInsetData } from './codeinset'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { MainThreadWebviews } from 'vs/workbench/api/electron-browser/mainThreadWebview'; + +export class CodeInsetController implements editorCommon.IEditorContribution { + + private static readonly ID: string = 'css.editor.codeInset'; + + private _isEnabled: boolean; + + private _globalToDispose: IDisposable[]; + private _localToDispose: IDisposable[]; + private _insetWidgets: CodeInsetWidget[]; + private _currentFindCodeInsetSymbolsPromise: CancelablePromise; + private _modelChangeCounter: number; + private _currentResolveCodeInsetSymbolsPromise: CancelablePromise; + private _detectVisibleInsets: RunOnceScheduler; + private _mainThreadWebviews: MainThreadWebviews; + + constructor( + private _editor: editorBrowser.ICodeEditor, + @ICommandService private readonly _commandService: ICommandService, + @INotificationService private readonly _notificationService: INotificationService, + @IExtensionService private readonly _extensionService: IExtensionService + ) { + this._isEnabled = this._editor.getConfiguration().contribInfo.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._editor.onDidChangeConfiguration(() => { + let prevIsEnabled = this._isEnabled; + this._isEnabled = this._editor.getConfiguration().contribInfo.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); + } + + 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, + this._commandService, this._notificationService, + () => this._detectVisibleInsets.schedule())); + 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, + this._commandService, this._notificationService, + () => this._detectVisibleInsets.schedule())); + groupsIndex++; + } + + helper.commit(changeAccessor); + }); + }); + + scrollState.restore(this._editor); + } + + + private getWebviewService(): MainThreadWebviews { + if (!this._mainThreadWebviews) { + this._mainThreadWebviews = this._extensionService.getNamedCustomer('MainThreadWebviews'); + } + return this._mainThreadWebviews; + } + + + 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 => { + const symbol = request.symbol; + if (typeof request.provider.resolveCodeInset === 'function') { + const mainThreadWebviews = this.getWebviewService(); + symbol.webviewHandle = insetWidgets[r].createWebview(mainThreadWebviews, request.provider.extensionLocation); + return request.provider.resolveCodeInset(model, symbol, token); + } + return Promise.resolve(void 0); + }); + + return Promise.all(widgetPromises); + }); + + return Promise.all(allPromises); + }); + + this._currentResolveCodeInsetSymbolsPromise.then(() => { + this._currentResolveCodeInsetSymbolsPromise = null; + }).catch(err => { + this._currentResolveCodeInsetSymbolsPromise = null; + onUnexpectedError(err); + }); + } +} + +registerEditorContribution(CodeInsetController); diff --git a/src/vs/workbench/parts/codeinset/codeInset.ts b/src/vs/workbench/parts/codeinset/codeInset.ts new file mode 100644 index 00000000000..2382d610985 --- /dev/null +++ b/src/vs/workbench/parts/codeinset/codeInset.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; +import { ICodeInsetSymbol, CodeInsetProvider, CodeInsetProviderRegistry } from 'vs/editor/common/modes'; +import { ITextModel } from 'vs/editor/common/model'; +import { onUnexpectedExternalError, illegalArgument } from 'vs/base/common/errors'; +import { mergeSort } from 'vs/base/common/arrays'; +import { URI } from 'vs/base/common/uri'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +export interface ICodeInsetData { + symbol: ICodeInsetSymbol; + provider: CodeInsetProvider; +} + +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; + } + }); + }); +} + +registerLanguageCommand('_executeCodeInsetProvider', function (accessor, args) { + let { resource, itemResolveCount } = args; + if (!(resource instanceof URI)) { + throw illegalArgument(); + } + + const model = accessor.get(IModelService).getModel(resource); + if (!model) { + throw illegalArgument(); + } + + const result: ICodeInsetSymbol[] = []; + return getCodeInsetData(model, CancellationToken.None).then(value => { + + let resolve: Thenable[] = []; + + for (const item of value) { + if (typeof itemResolveCount === 'undefined') { + result.push(item.symbol); + } else if (itemResolveCount-- > 0) { + resolve.push(Promise.resolve(item.provider.resolveCodeInset(model, item.symbol, CancellationToken.None)) + .then(symbol => result.push(symbol))); + } + } + + return Promise.all(resolve); + + }).then(() => { + return result; + }); +}); diff --git a/src/vs/workbench/parts/codeinset/codeInsetWidget.css b/src/vs/workbench/parts/codeinset/codeInsetWidget.css new file mode 100644 index 00000000000..113a5a1fbb4 --- /dev/null +++ b/src/vs/workbench/parts/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/parts/codeinset/codeInsetWidget.ts b/src/vs/workbench/parts/codeinset/codeInsetWidget.ts new file mode 100644 index 00000000000..6b67ac7b5b3 --- /dev/null +++ b/src/vs/workbench/parts/codeinset/codeInsetWidget.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ICommandService } from 'vs/platform/commands/common/commands'; +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 { INotificationService } from 'vs/platform/notification/common/notification'; +import { WebviewEditorInput } from 'vs/workbench/contrib/webview/electron-browser/webviewEditorInput'; +import { mainThreadWebviews, MainThreadWebviews } from 'vs/workbench/api/electron-browser/mainThreadWebview'; +import { UriComponents } from 'vs/base/common/uri'; + + +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 _viewZone: editorBrowser.IViewZone; + private _viewZoneId?: number = undefined; + private _decorationIds: string[]; + private _data: ICodeInsetData[]; + private _webview: WebviewEditorInput | undefined; + private _webviewHandle: string | undefined; + private _range: Range; + + constructor( + data: ICodeInsetData[], // all the insets on the same line (often just one) + editor: editorBrowser.ICodeEditor, + helper: CodeInsetHelper, + commandService: ICommandService, + notificationService: INotificationService, + updateCallabck: Function + ) { + 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 { + console.log('DISPOSE'); + while (this._decorationIds.length) { + helper.removeDecoration(this._decorationIds.pop()); + } + if (viewZoneChangeAccessor) { + viewZoneChangeAccessor.removeZone(this._viewZoneId); + this._viewZone = undefined; + } + } + + 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 get webview() { return this._webview; } + + static webviewPool = 1; + + public createWebview(mainThreadWebview: MainThreadWebviews, extensionLocation: UriComponents) { + if (this._webviewHandle) { return this._webviewHandle; } + + const lineNumber = this._range.endLineNumber; + this._editor.changeViewZones(accessor => { + if (this._viewZoneId) { + this._webview.dispose(); + accessor.removeZone(this._viewZoneId); + } + const div = document.createElement('div'); + + this._webviewHandle = CodeInsetWidget.webviewPool++ + ''; + this._webview = mainThreadWebviews.createInsetWebview(this._webviewHandle, div, { enableScripts: true }, extensionLocation); + + const webview = this._webview.webview; + 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); + }); + return this._webviewHandle; + } + + 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/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index d40c975542c..247fcde06ff 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -222,6 +222,9 @@ export interface IExtensionService extends ICpuProfilerTarget { * Stops the extension host. */ stopExtensionHost(): void; + + getNamedCustomer?(sid: string): any; + } export interface ICpuProfilerTarget { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index 277ad1cbd42..f3a9fe81a77 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -48,6 +48,7 @@ export class ExtensionHostProcessManager extends Disposable { private _extensionHostProcessRPCProtocol: RPCProtocol; private readonly _extensionHostProcessCustomers: IDisposable[]; private readonly _extensionHostProcessWorker: IExtensionHostStarter; + private readonly _namedCustomerById: { [sid: string]: any } = {}; /** * winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object. */ @@ -183,6 +184,7 @@ export class ExtensionHostProcessManager extends Disposable { const instance = this._instantiationService.createInstance(ctor, extHostContext); this._extensionHostProcessCustomers.push(instance); this._extensionHostProcessRPCProtocol.set(id, instance); + this._namedCustomerById[id.sid] = instance; } // Customers @@ -205,6 +207,10 @@ export class ExtensionHostProcessManager extends Disposable { }); } + public getNamedCustomer(sid: string): any { + return this._namedCustomerById[sid]; + } + public activateByEvent(activationEvent: string): Promise { if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) { return NO_OP_VOID_PROMISE; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index d7d69bb07c9..2dd7b5fe86b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -417,6 +417,11 @@ export class ExtensionService extends Disposable implements IExtensionService { this._stopExtensionHostProcess(); } + public getNamedCustomer(sid: string): any { + const ncs = this._extensionHostProcessManagers.map(m => m.getNamedCustomer(sid)).filter(c => c); + return ncs.length ? ncs[0] : undefined; + } + private _stopExtensionHostProcess(): void { let previouslyActivatedExtensionIds: ExtensionIdentifier[] = []; this._extensionHostActiveExtensions.forEach((value) => { diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index debe4a9b003..f63e71db1cd 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -175,6 +175,10 @@ import 'vs/workbench/contrib/outline/browser/outline.contribution'; // Experiments import 'vs/workbench/contrib/experiments/electron-browser/experiments.contribution'; +// Code Insets +import 'vs/workbench/parts/codeinset/codeInset.contribution'; + +//#endregion // Issues import 'vs/workbench/contrib/issue/electron-browser/issue.contribution';