diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ff9d5389785..637c01bc656 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1469,4 +1469,126 @@ declare module 'vscode' { } //#endregion + + //#region eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 + + export class TimelineItem { + /** + * A date for when the timeline item occurred + */ + date: number; + + /** + * A human-readable string describing the source of the timeline item. This can be used for filtering by sources so keep it consistent across timeline item types. + */ + source: string; + + + /** + * A human-readable string describing the timeline item. When `falsy`, it is derived from [resourceUri](#TreeItem.resourceUri). + */ + label: string; + + /** + * Optional id for the timeline item. See [TreeItem.id](#TreeItem.id) for more details. + */ + id?: string; + + /** + * The icon path or [ThemeIcon](#ThemeIcon) for the timeline item. See [TreeItem.iconPath](#TreeItem.iconPath) for more details. + */ + iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; + + /** + * A human readable string describing less prominent details of the timeline item. See [TreeItem.description](#TreeItem.description) for more details. + */ + description?: string; + + /** + * The [uri](#Uri) of the resource representing the timeline item (if any). See [TreeItem.resourceUri](#TreeItem.resourceUri) for more details. + */ + resourceUri?: Uri; + + /** + * The tooltip text when you hover over the timeline item. + */ + tooltip?: string | undefined; + + /** + * The [command](#Command) that should be executed when the timeline item is selected. + */ + command?: Command; + + /** + * Context value of the timeline item. See [TreeItem.contextValue](#TreeItem.contextValue) for more details. + */ + contextValue?: string; + + /** + * @param label A human-readable string describing the timeline item + * @param date A date for when the timeline item occurred + * @param source A human-readable string describing the source of the timeline item + */ + constructor(label: string, date: number, source: string); + } + + export interface TimelimeAddEvent { + + /** + * An array of timeline items which have been added. + */ + readonly items: readonly TimelineItem[]; + + /** + * The uri of the file to which the timeline items belong. + */ + readonly uri: Uri; + } + + export interface TimelimeChangeEvent { + + /** + * The date after which the timeline has changed. If `undefined` the entire timeline will be reset. + */ + readonly since?: Date; + + /** + * The uri of the file to which the timeline changed. + */ + readonly uri: Uri; + } + + export interface TimelineProvider { + // onDidAdd?: Event; + // onDidChange?: Event; + id: string; + + /** + * Provide [timeline items](#TimelineItem) for a [Uri](#Uri) after a particular date. + * + * @param uri The uri of the file to provide the timeline for. + * @param since A date after which timeline items should be provided. + * @param token A cancellation token. + * @return An array of timeline items or a thenable that resolves to such. The lack of a result + * can be signaled by returning `undefined`, `null`, or an empty array. + */ + provideTimeline(uri: Uri, since: number, token: CancellationToken): ProviderResult; + } + + export namespace workspace { + /** + * Register a timeline provider. + * + * Multiple providers can be registered. In that case, providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A timeline provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTimelineProvider(selector: DocumentSelector, provider: TimelineProvider): Disposable; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index d77e37c35d8..e69aa80159d 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -60,6 +60,7 @@ import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; import './mainThreadAuthentication'; +import './mainThreadTimeline'; import 'vs/workbench/api/common/apiCommands'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts new file mode 100644 index 00000000000..c063dc1c1d1 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { ITimelineService, TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +@extHostNamedCustomer(MainContext.MainThreadTimeline) +export class MainThreadTimeline implements MainThreadTimelineShape { + + private readonly _proxy: ExtHostTimelineShape; + + constructor( + context: IExtHostContext, + @ITimelineService private readonly _timelineService: ITimelineService + ) { + this._proxy = context.getProxy(ExtHostContext.ExtHostTimeline); + } + + $getTimeline(uri: URI, since: number, token: CancellationToken): Promise { + return this._timelineService.getTimeline(uri, since, token); + } + + $registerTimelineProvider(key: string, id: string): void { + console.log(`MainThreadTimeline#registerTimelineProvider: key=${key}`); + + const proxy = this._proxy; + this._timelineService.registerTimelineProvider(key, { + id: id, + provideTimeline(uri: URI, since: number, token: CancellationToken) { + return proxy.$getTimeline(key, uri, since, token); + } + }); + } + + $unregisterTimelineProvider(key: string): void { + console.log(`MainThreadTimeline#unregisterTimelineProvider: key=${key}`); + this._timelineService.unregisterTimelineProvider(key); + } + + dispose(): void { + // noop + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 948ec4d65ad..397076d2d72 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import * as errors from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import * as path from 'vs/base/common/path'; @@ -71,6 +71,7 @@ import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; +import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -132,6 +133,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); + const extHostTimelineService = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -761,6 +763,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidTunnelsChange: (listener, thisArg?, disposables?) => { checkProposedApiEnabled(extension); return extHostTunnelService.onDidTunnelsChange(listener, thisArg, disposables); + + }, + registerTimelineProvider: (scheme: string, provider: vscode.TimelineProvider) => { + checkProposedApiEnabled(extension); + return extHostTimelineService.registerTimelineProvider(extension.identifier, { + id: provider.id, + async provideTimeline(uri: URI, since: number, token: CancellationToken) { + const results = await provider.provideTimeline(uri, since, token); + return results ?? []; + } + }); } }; @@ -984,7 +997,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Decoration: extHostTypes.Decoration, WebviewContentState: extHostTypes.WebviewContentState, UIKind: UIKind, - ColorThemeKind: extHostTypes.ColorThemeKind + ColorThemeKind: extHostTypes.ColorThemeKind, + TimelineItem: extHostTypes.TimelineItem }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 045e8ce561c..fcc3d247dbd 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -49,6 +49,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; +import { TimelineItem } from 'vs/workbench/contrib/timeline/common/timeline'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -797,6 +798,13 @@ export interface MainThreadTunnelServiceShape extends IDisposable { $setCandidateFilter(): Promise; } +export interface MainThreadTimelineShape extends IDisposable { + $registerTimelineProvider(key: string, id: string): void; + $unregisterTimelineProvider(key: string): void; + + $getTimeline(resource: UriComponents, since: number, token: CancellationToken): Promise; +} + // -- extension host export interface ExtHostCommandsShape { @@ -1441,6 +1449,12 @@ export interface ExtHostTunnelServiceShape { $onDidTunnelsChange(): Promise; } +export interface ExtHostTimelineShape { + // $registerTimelineProvider(handle: number, provider: TimelineProvider): void; + + $getTimeline(key: string, uri: UriComponents, since: number, token: CancellationToken): Promise; +} + // --- proxy identifiers export const MainContext = { @@ -1484,7 +1498,8 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadTheming: createMainId('MainThreadTheming'), - MainThreadTunnelService: createMainId('MainThreadTunnelService') + MainThreadTunnelService: createMainId('MainThreadTunnelService'), + MainThreadTimeline: createMainId('MainThreadTimeline') }; export const ExtHostContext = { @@ -1521,5 +1536,6 @@ export const ExtHostContext = { ExtHostLabelService: createMainId('ExtHostLabelService'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), - ExtHostAuthentication: createMainId('ExtHostAuthentication') + ExtHostAuthentication: createMainId('ExtHostAuthentication'), + ExtHostTimeline: createMainId('ExtHostTimeline') }; diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts new file mode 100644 index 00000000000..65340c9ab9d --- /dev/null +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { UriComponents, URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; +import { TimelineItem, TimelineProvider, toKey } from 'vs/workbench/contrib/timeline/common/timeline'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; + +export interface IExtHostTimeline extends ExtHostTimelineShape { + readonly _serviceBrand: undefined; + $getTimeline(key: string, uri: UriComponents, since: number, token: vscode.CancellationToken): Promise; +} + +export const IExtHostTimeline = createDecorator('IExtHostTimeline'); + +export class ExtHostTimeline implements IExtHostTimeline { + _serviceBrand: undefined; + + private _proxy: MainThreadTimelineShape; + + private _providers = new Map(); + + constructor( + mainContext: IMainContext, + + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadTimeline); + + this.registerTimelineProvider('bar', { + id: 'baz', + async provideTimeline(uri: URI, since: number, token: vscode.CancellationToken) { + return [ + { + id: '1', + label: 'Bar Timeline1', + description: uri.toString(true), + detail: new Date().toString(), + date: Date.now(), + source: 'log' + }, + { + id: '2', + label: 'Bar Timeline2', + description: uri.toString(true), + detail: new Date(Date.now() - 100).toString(), + date: Date.now() - 100, + source: 'log' + } + ]; + } + }); + } + + async $getTimeline(key: string, uri: UriComponents, since: number, token: vscode.CancellationToken): Promise { + const provider = this._providers.get(key); + return provider?.provideTimeline(URI.revive(uri), since, token) ?? []; + } + + registerTimelineProvider(extension: ExtensionIdentifier | string, provider: TimelineProvider): IDisposable { + console.log(`ExtHostTimeline#registerTimelineProvider: extension=${extension.toString()}, provider=${provider.id}`); + + const key = toKey(extension, provider.id); + if (this._providers.has(key)) { + throw new Error(`Timeline Provider ${key} already exists.`); + } + + this._proxy.$registerTimelineProvider(key, provider.id); + this._providers.set(key, provider); + + return toDisposable(() => { + this._providers.delete(key); + this._proxy.$unregisterTimelineProvider(key); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 19acded0ac9..3cb178d10ac 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2544,3 +2544,12 @@ export enum ColorThemeKind { } //#endregion Theming + +//#region Timeline + +@es5ClassCompat +export class TimelineItem implements vscode.TimelineItem { + constructor(public label: string, public date: number, public source: string) { } +} + +//#endregion Timeline diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css new file mode 100644 index 00000000000..a4a092d8349 --- /dev/null +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts new file mode 100644 index 00000000000..9d23ce1f427 --- /dev/null +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/browser/explorerViewlet'; +import { ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineService } from 'vs/workbench/contrib/timeline/common/timelineService'; +import { TimelinePane } from './timelinePane'; + +export class TimelinePaneDescriptor implements IViewDescriptor { + readonly id = TimelinePane.ID; + readonly name = TimelinePane.TITLE; + readonly ctorDescriptor = new SyncDescriptor(TimelinePane); + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly collapsed = true; + readonly order = 2; + readonly weight = 30; + focusCommand = { id: 'timeline.focus' }; +} + +Registry.as(ViewExtensions.ViewsRegistry).registerViews([new TimelinePaneDescriptor()], VIEW_CONTAINER); + +registerSingleton(ITimelineService, TimelineService, true); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts new file mode 100644 index 00000000000..bcab5a04e8d --- /dev/null +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./media/timelinePane'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IListVirtualDelegate, IIdentityProvider, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; +import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TimelineItem, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; + +type TreeElement = TimelineItem; + +export class TimelinePane extends ViewPane { + static readonly ID = 'timeline'; + static readonly TITLE = nls.localize('timeline', "Timeline"); + private _tree!: WorkbenchObjectTree; + private _tokenSource: CancellationTokenSource | undefined; + private _visibilityDisposables: DisposableStore | undefined; + + constructor( + options: IViewPaneOptions, + @IKeybindingService protected keybindingService: IKeybindingService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IContextKeyService protected contextKeyService: IContextKeyService, + @IConfigurationService protected configurationService: IConfigurationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IEditorService protected editorService: IEditorService, + @ITimelineService protected timelineService: ITimelineService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService); + + const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); + scopedContextKeyService.createKey('view', TimelinePane.ID); + } + + private async refresh() { + this._tokenSource?.cancel(); + this._tokenSource = new CancellationTokenSource(); + + // TODO: Deal with no uri -- use a view title? or keep the last one cached? + // TODO: Deal with no items -- use a view title? + + let children; + if (this._uri) { + const items = await this.timelineService.getTimeline(this._uri, 0, this._tokenSource.token); + children = items.map(item => ({ element: item })); + } + + this._tree.setChildren(null, children); + } + + + private _uri: URI | undefined; + + private updateUri(uri: URI | undefined) { + if (uri?.toString(true) === this._uri?.toString(true)) { + return; + } + + this._uri = uri; + this.refresh(); + } + + private onActiveEditorChanged() { + let uri; + + const editor = this.editorService.activeEditor; + if (editor) { + uri = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); + } + + this.updateUri(uri); + } + + private onProvidersChanged() { + this.refresh(); + } + + focus(): void { + super.focus(); + this._tree.domFocus(); + } + + setVisible(visible: boolean): void { + if (visible) { + this._visibilityDisposables = new DisposableStore(); + + this.timelineService.onDidChangeProviders(this.onProvidersChanged, this, this._visibilityDisposables); + + this.editorService.onDidActiveEditorChange(this.onActiveEditorChanged, this, this._visibilityDisposables); + this.onActiveEditorChanged(); + } else { + this._visibilityDisposables?.dispose(); + } + } + + protected layoutBody(height: number, width: number): void { + this._tree.layout(height, width); + } + + protected renderBody(container: HTMLElement): void { + dom.addClass(container, '.tree-explorer-viewlet-tree-view'); + const treeContainer = document.createElement('div'); + dom.addClass(treeContainer, 'customview-tree'); + dom.addClass(treeContainer, 'file-icon-themable-tree'); + dom.addClass(treeContainer, 'show-file-icons'); + container.appendChild(treeContainer); + + const renderer = this.instantiationService.createInstance(TimelineTreeRenderer); + this._tree = this.instantiationService.createInstance>( + WorkbenchObjectTree, + 'TimelinePane', + treeContainer, + new TimelineListVirtualDelegate(), + [renderer], + { + identityProvider: new TimelineIdentityProvider(), + keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider() + } + ); + } +} + + +export class TimelineElementTemplate { + static readonly id = 'TimelineElementTemplate'; + + constructor( + readonly container: HTMLElement, + readonly iconLabel: IconLabel, + // readonly iconClass: HTMLElement, + // readonly decoration: HTMLElement, + ) { } +} + +export class TimelineIdentityProvider implements IIdentityProvider { + getId(item: TimelineItem): { toString(): string; } { + return `${item.id}|${item.date}`; + } +} + + +export class TimelineKeyboardNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + getKeyboardNavigationLabel(element: TimelineItem): { toString(): string; } { + return element.label; + } +} + +export class TimelineListVirtualDelegate implements IListVirtualDelegate { + getHeight(_element: TimelineItem): number { + return 22; + } + + getTemplateId(element: TimelineItem): string { + return TimelineElementTemplate.id; + } +} + +class TimelineTreeRenderer implements ITreeRenderer { + readonly templateId: string = TimelineElementTemplate.id; + + constructor() { } + + renderTemplate(container: HTMLElement): TimelineElementTemplate { + dom.addClass(container, 'custom-view-tree-node-item'); + + const iconLabel = new IconLabel(container, { supportHighlights: true, supportCodicons: true }); + return new TimelineElementTemplate(container, iconLabel); + } + + renderElement(node: ITreeNode, index: number, template: TimelineElementTemplate, height: number | undefined): void { + const { element } = node; + + template.iconLabel.setLabel(element.label, element.description, { title: element.detail, matches: createMatches(node.filterData) }); + } + + disposeTemplate(template: TimelineElementTemplate): void { + template.iconLabel.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts new file mode 100644 index 00000000000..223648a8915 --- /dev/null +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export function toKey(extension: ExtensionIdentifier | string, source: string) { + return `${typeof extension === 'string' ? extension : ExtensionIdentifier.toKey(extension)}|${source}`; +} + +export interface TimelineItem { + date: number; + source: string; + label: string; + id?: string; + // iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; + description?: string; + detail?: string; + + // resourceUri?: Uri; + // tooltip?: string | undefined; + // command?: Command; + // collapsibleState?: TreeItemCollapsibleState; + // contextValue?: string; +} + +export interface TimelineProvider { + id: string; + // selector: DocumentSelector; + + provideTimeline(uri: URI, since: number, token: CancellationToken): Promise; +} + +export interface ITimelineService { + readonly _serviceBrand: undefined; + + onDidChangeProviders: Event; + registerTimelineProvider(key: string, provider: TimelineProvider): IDisposable; + unregisterTimelineProvider(key: string): void; + + getTimeline(uri: URI, since: number, token: CancellationToken): Promise; +} + +const TIMELINE_SERVICE_ID = 'timeline'; +export const ITimelineService = createDecorator(TIMELINE_SERVICE_ID); diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts new file mode 100644 index 00000000000..d66f28752c3 --- /dev/null +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -0,0 +1,190 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ITimelineService, TimelineProvider } from './timeline'; + +export class TimelineService implements ITimelineService { + _serviceBrand: undefined; + + private readonly _onDidChangeProviders = new Emitter(); + readonly onDidChangeProviders: Event = this._onDidChangeProviders.event; + + private readonly _providers = new Map(); + + constructor(@ILogService private readonly logService: ILogService) { + this.registerTimelineProvider('foo', { + id: 'bar', + async provideTimeline(uri: URI, since: number, token: CancellationToken) { + return [ + { + id: '1', + label: '$(git-commit) Timeline1', + description: uri.toString(true), + date: Date.now(), + source: 'internal' + }, + { + id: '2', + label: '$(git-commit) Timeline2', + description: uri.toString(true), + date: Date.now() - 100, + source: 'internal' + } + + ]; + } + }); + } + + async getTimeline(uri: URI, since: number, token: CancellationToken) { + this.logService.trace(`TimelineService#getTimeline: uri=${uri.toString(true)}`); + const requests = []; + + for (const provider of this._providers.values()) { + requests.push(provider.provideTimeline(uri, since, token)); + } + + const timelines = await raceAll(requests, 5000); + + const timeline = []; + for (const items of timelines) { + // eslint-disable-next-line eqeqeq + if (items == null || items instanceof CancellationError || items.length === 0) { + continue; + } + + timeline.push(...items); + } + + timeline.sort((a, b) => b.date - a.date); + return timeline; + } + + registerTimelineProvider(key: string, provider: TimelineProvider): IDisposable { + this.logService.trace('TimelineService#registerTimelineProvider'); + + if (this._providers.has(key)) { + throw new Error(`Timeline Provider ${key} already exists.`); + } + + this._providers.set(key, provider); + this._onDidChangeProviders.fire(); + + return { + dispose: () => { + this._providers.delete(key); + this._onDidChangeProviders.fire(); + } + }; + } + + unregisterTimelineProvider(key: string): void { + this.logService.trace('TimelineService#unregisterTimelineProvider'); + + if (!this._providers.has(key)) { + return; + } + + this._providers.delete(key); + this._onDidChangeProviders.fire(); + } +} + +function* map( + source: Iterable | IterableIterator, + mapper: (item: T) => TMapped +): Iterable { + for (const item of source) { + yield mapper(item); + } +} + +class CancellationError extends Error { + constructor(public readonly promise: TPromise, message: string) { + super(message); + } +} + +class CancellationErrorWithId extends CancellationError { + constructor(public readonly id: T, promise: TPromise, message: string) { + super(promise, message); + } +} + +function raceAll( + promises: Promise[], + timeout?: number +): Promise<(TPromise | CancellationError>)[]>; +function raceAll( + promises: Map>, + timeout?: number +): Promise>>>; +function raceAll( + ids: Iterable, + fn: (id: T) => Promise, + timeout?: number +): Promise>>>; +async function raceAll( + promisesOrIds: Promise[] | Map> | Iterable, + timeoutOrFn?: number | ((id: T) => Promise), + timeout?: number +) { + let promises; + // eslint-disable-next-line eqeqeq + if (timeoutOrFn != null && typeof timeoutOrFn !== 'number') { + promises = new Map( + map]>(promisesOrIds as Iterable, id => [id, timeoutOrFn(id)]) + ); + } else { + timeout = timeoutOrFn; + promises = promisesOrIds as Promise[] | Map>; + } + + if (promises instanceof Map) { + return new Map( + await Promise.all( + map< + [T, Promise], + Promise<[T, TPromise | CancellationErrorWithId>]> + >( + promises.entries(), + // eslint-disable-next-line eqeqeq + timeout == null + ? ([id, promise]) => promise.then(p => [id, p]) + : ([id, promise]) => + Promise.race([ + promise, + + new Promise>>(resolve => + setTimeout( + () => resolve(new CancellationErrorWithId(id, promise, 'TIMED OUT')), + timeout! + ) + ) + ]).then(p => [id, p]) + ) + ) + ); + } + + return Promise.all( + // eslint-disable-next-line eqeqeq + timeout == null + ? promises + : promises.map(p => + Promise.race([ + p, + new Promise>>(resolve => + setTimeout(() => resolve(new CancellationError(p, 'TIMED OUT')), timeout!) + ) + ]) + ) + ); +} diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index fe0a2d0d6d9..04880d14542 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -266,4 +266,7 @@ import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; // Code Actions import 'vs/workbench/contrib/codeActions/common/codeActions.contribution'; +// Timeline +import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; + //#endregion