From 62719ab5449eb6a8ed69c2ab775830dd19b99ee0 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 31 Jul 2019 12:32:04 -0500 Subject: [PATCH] Clean up client API - Don't use "any" for the API type. - Remove everything from the Coder API that can eventually be done through the VS Code API. - Move the event emission to our own client to minimize patching. --- scripts/vscode.patch | 25 ++--- src/api.ts | 240 ++++++++++++++++++++----------------------- src/client.ts | 19 +++- typings/coder.d.ts | 209 +------------------------------------ 4 files changed, 138 insertions(+), 355 deletions(-) diff --git a/scripts/vscode.patch b/scripts/vscode.patch index 7795f02d..b890389a 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -573,34 +573,27 @@ index 5a758eb786..7fcacb5ca7 100644 templateData.actionBar.context = ({ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }); templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 1986fb6642..a3e4cbdb56 100644 +index 1986fb6642..453d3e3e48 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -35,6 +35,7 @@ import { SignService } from 'vs/platform/sign/browser/signService'; import { hash } from 'vs/base/common/hash'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import { ProductService } from 'vs/platform/product/browser/productService'; -+import { coderApi, vscodeApi } from 'vs/server/src/api'; ++import { initialize } from 'vs/server/src/client'; class CodeRendererMain extends Disposable { -@@ -71,6 +72,15 @@ class CodeRendererMain extends Disposable { +@@ -71,6 +72,8 @@ class CodeRendererMain extends Disposable { // Startup this.workbench.startup(); + -+ const target = window as any; -+ target.ide = coderApi(services.serviceCollection); -+ target.vscode = vscodeApi(services.serviceCollection); -+ -+ const event = new CustomEvent('ide-ready'); -+ (event as any).ide = target.ide; -+ (event as any).vscode = target.vscode; -+ window.dispatchEvent(event); ++ initialize(services.serviceCollection); } private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService }> { -@@ -114,7 +124,8 @@ class CodeRendererMain extends Disposable { +@@ -114,7 +117,8 @@ class CodeRendererMain extends Disposable { const channel = connection.getChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME); const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); @@ -1422,7 +1415,7 @@ index 306d58f915..58c603ad3d 100644 if (definition.fontCharacter || definition.fontColor) { let body = ''; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index c28adc0ad9..3d1adba3d9 100644 +index c28adc0ad9..4517c308da 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -128,7 +128,7 @@ import 'vs/workbench/services/extensions/browser/extensionService'; @@ -1447,9 +1440,3 @@ index c28adc0ad9..3d1adba3d9 100644 // Output Panel import 'vs/workbench/contrib/output/browser/output.contribution'; -@@ -356,3 +356,5 @@ import 'vs/workbench/contrib/outline/browser/outline.contribution'; - // import 'vs/workbench/contrib/issue/electron-browser/issue.contribution'; - - //#endregion -+ -+import 'vs/server/src/client'; diff --git a/src/api.ts b/src/api.ts index 129a3ffa..c7e882ea 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,25 +1,19 @@ import * as vscode from "vscode"; import { localize } from "vs/nls"; -import { Action } from "vs/base/common/actions"; -import { SyncActionDescriptor, MenuRegistry, MenuId } from "vs/platform/actions/common/actions"; +import { SyncActionDescriptor } from "vs/platform/actions/common/actions"; import { Registry } from "vs/platform/registry/common/platform"; import { IWorkbenchActionRegistry, Extensions as ActionExtensions} from "vs/workbench/common/actions"; import { CommandsRegistry, ICommandService } from "vs/platform/commands/common/commands"; -import { IStat, IWatchOptions, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileChange, FileWriteOptions, FileSystemProviderCapabilities, IFileService, FileType, FileOperation, IFileSystemProvider } from "vs/platform/files/common/files"; -import { ITextFileService } from "vs/workbench/services/textfile/common/textfiles"; -import { IModelService } from "vs/editor/common/services/modelService"; -import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal"; +import { IStat, IWatchOptions, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileChange, FileWriteOptions, FileSystemProviderCapabilities, IFileService, FileType, IFileSystemProvider } from "vs/platform/files/common/files"; import { IStorageService } from "vs/platform/storage/common/storage"; import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection"; import { INotificationService } from "vs/platform/notification/common/notification"; -import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar"; -import Severity from "vs/base/common/severity"; import { Emitter, Event } from "vs/base/common/event"; import * as extHostTypes from "vs/workbench/api/common/extHostTypes"; import { ServiceIdentifier, IInstantiationService } from "vs/platform/instantiation/common/instantiation"; import { URI } from "vs/base/common/uri"; -import { ITreeViewDataProvider, IViewsRegistry, ITreeViewDescriptor, Extensions as ViewsExtensions, IViewContainersRegistry } from "vs/workbench/common/views"; +import { ITreeItem, ITreeViewDataProvider, IViewsRegistry, ITreeViewDescriptor, Extensions as ViewsExtensions, IViewContainersRegistry, TreeItemCollapsibleState } from "vs/workbench/common/views"; import { CustomTreeViewPanel, CustomTreeView } from "vs/workbench/browser/parts/views/customView"; import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from "vs/workbench/browser/viewlet"; import { IExtensionService } from "vs/workbench/services/extensions/common/extensions"; @@ -35,6 +29,7 @@ import { IViewletService } from "vs/workbench/services/viewlet/browser/viewlet"; import { IEditorGroupsService } from "vs/workbench/services/editor/common/editorGroupsService"; import { createCSSRule } from "vs/base/browser/dom"; import { IDisposable } from "vs/base/common/lifecycle"; +import { generateUuid } from "vs/base/common/uuid"; /** * Client-side implementation of VS Code's API. @@ -42,7 +37,7 @@ import { IDisposable } from "vs/base/common/lifecycle"; * TODO: Implement menu items for views (for item actions). * TODO: File system provider doesn't work. */ -export const vscodeApi = (serviceCollection: ServiceCollection): typeof vscode => { +export const vscodeApi = (serviceCollection: ServiceCollection): Partial => { const getService = (id: ServiceIdentifier): T => serviceCollection.get(id) as T; const commandService = getService(ICommandService); const notificationService = getService(INotificationService); @@ -61,149 +56,84 @@ export const vscodeApi = (serviceCollection: ServiceCollection): typeof vscode = FileType: FileType, Uri: URI, commands: { - executeCommand: (commandId: string, ...args: any[]): any => { + executeCommand: (commandId: string, ...args: any[]): Promise => { return commandService.executeCommand(commandId, ...args); }, - registerCommand: (id: string, command: () => void): any => { + registerCommand: (id: string, command: (...args: any[]) => any): IDisposable => { return CommandsRegistry.registerCommand(id, command); }, - }, + } as Partial, window: { - registerTreeDataProvider: (id: string, dataProvider: ITreeViewDataProvider): void => { + registerTreeDataProvider: (id: string, dataProvider: vscode.TreeDataProvider): IDisposable => { + const tree = new TreeViewDataProvider(dataProvider); const view = viewsRegistry.getView(id); - if (view) { - (view as ITreeViewDescriptor).treeView.dataProvider = dataProvider; - } + (view as ITreeViewDescriptor).treeView.dataProvider = tree; + return { + dispose: () => tree.dispose(), + }; }, - showErrorMessage: (message: string): void => { + showErrorMessage: async (message: string): Promise => { notificationService.error(message); + return undefined; }, - }, + } as Partial, workspace: { registerFileSystemProvider: (scheme: string, provider: vscode.FileSystemProvider): IDisposable => { return fileService.registerProvider(scheme, new FileSystemProvider(provider)); }, - }, - } as any; + } as Partial, + } as Partial; // Without this it complains that the type isn't `| undefined`. }; /** - * Coder API. + * Coder API. This should only provide functionality that can't be made + * available through the VS Code API. */ export const coderApi = (serviceCollection: ServiceCollection): typeof coder => { const getService = (id: ServiceIdentifier): T => serviceCollection.get(id) as T; return { - workbench: { - action: Action, - syncActionDescriptor: SyncActionDescriptor, - commandRegistry: CommandsRegistry, - actionsRegistry: Registry.as(ActionExtensions.WorkbenchActions), - registerView: (viewId, viewName, containerId, containerName, icon): void => { - const cssClass = `extensionViewlet-${containerId}`; - const id = `workbench.view.extension.${containerId}`; - class CustomViewlet extends ViewContainerViewlet { - public constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITelemetryService telemetryService: ITelemetryService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IStorageService storageService: IStorageService, - @IEditorService _editorService: IEditorService, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService, - ) { - super(id, `${id}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); - } + registerView: (viewId, viewName, containerId, containerName, icon): void => { + const cssClass = `extensionViewlet-${containerId}`; + const id = `workbench.view.extension.${containerId}`; + class CustomViewlet extends ViewContainerViewlet { + public constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IEditorService _editorService: IEditorService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + ) { + super(id, `${id}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); } + } - Registry.as(ViewletExtensions.Viewlets).registerViewlet( - new ViewletDescriptor(CustomViewlet as any, id, containerName, cssClass, undefined, URI.parse(icon)), - ); + Registry.as(ViewletExtensions.Viewlets).registerViewlet( + new ViewletDescriptor(CustomViewlet as any, id, containerName, cssClass, undefined, URI.parse(icon)), + ); - Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction( - new SyncActionDescriptor(OpenCustomViewletAction as any, id, localize("showViewlet", "Show {0}", containerName)), - "View: Show {0}", - localize("view", "View"), - ); + Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction( + new SyncActionDescriptor(OpenCustomViewletAction as any, id, localize("showViewlet", "Show {0}", containerName)), + "View: Show {0}", + localize("view", "View"), + ); - // Generate CSS to show the icon in the activity bar. - const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; - createCSSRule(iconClass, `-webkit-mask: url('${icon}') no-repeat 50% 50%`); + // Generate CSS to show the icon in the activity bar. + const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`; + createCSSRule(iconClass, `-webkit-mask: url('${icon}') no-repeat 50% 50%`); - const container = Registry.as(ViewsExtensions.ViewContainersRegistry).registerViewContainer(containerId); - Registry.as(ViewsExtensions.ViewsRegistry).registerViews([{ - id: viewId, - name: viewName, - ctorDescriptor: { ctor: CustomTreeViewPanel }, - treeView: getService(IInstantiationService).createInstance(CustomTreeView as any, viewId, container), - }] as ITreeViewDescriptor[], container); - }, - menuRegistry: MenuRegistry as any, - statusbarService: getService(IStatusbarService) as any, - notificationService: getService(INotificationService), - terminalService: getService(ITerminalService), - onFileCreate: (cb): void => { - getService(IFileService).onAfterOperation((e) => { - if (e.operation === FileOperation.CREATE) { - cb(e.resource.path); - } - }); - }, - onFileMove: (cb): void => { - getService(IFileService).onAfterOperation((e) => { - if (e.operation === FileOperation.MOVE) { - cb(e.resource.path, e.target ? e.target.resource.path : undefined!); - } - }); - }, - onFileDelete: (cb): void => { - getService(IFileService).onAfterOperation((e) => { - if (e.operation === FileOperation.DELETE) { - cb(e.resource.path); - } - }); - }, - onFileSaved: (cb): void => { - getService(ITextFileService).models.onModelSaved((e) => { - cb(e.resource.path); - }); - }, - onFileCopy: (cb): void => { - getService(IFileService).onAfterOperation((e) => { - if (e.operation === FileOperation.COPY) { - cb(e.resource.path, e.target ? e.target.resource.path : undefined!); - } - }); - }, - onModelAdded: (cb): void => { - getService(IModelService).onModelAdded((e) => { - cb(e.uri.path, e.getLanguageIdentifier().language); - }); - }, - onModelRemoved: (cb): void => { - getService(IModelService).onModelRemoved((e) => { - cb(e.uri.path, e.getLanguageIdentifier().language); - }); - }, - onModelLanguageChange: (cb): void => { - getService(IModelService).onModelModeChanged((e) => { - cb(e.model.uri.path, e.model.getLanguageIdentifier().language, e.oldModeId); - }); - }, - onTerminalAdded: (cb): void => { - getService(ITerminalService).onInstanceCreated(() => cb()); - }, - onTerminalRemoved: (cb): void => { - getService(ITerminalService).onInstanceDisposed(() => cb()); - }, + const container = Registry.as(ViewsExtensions.ViewContainersRegistry).registerViewContainer(containerId); + Registry.as(ViewsExtensions.ViewsRegistry).registerViews([{ + id: viewId, + name: viewName, + ctorDescriptor: { ctor: CustomTreeViewPanel }, + treeView: getService(IInstantiationService).createInstance(CustomTreeView as any, viewId, container), + }] as ITreeViewDescriptor[], container); }, - // @ts-ignore - MenuId: MenuId, - Severity: Severity, - // @ts-ignore - StatusbarAlignment: StatusbarAlignment, }; }; @@ -282,3 +212,59 @@ class FileSystemProvider implements IFileSystemProvider { throw new Error("not implemented"); } } + +class TreeViewDataProvider implements ITreeViewDataProvider { + private readonly root = Symbol("root"); + private readonly values = new Map(); + private readonly children = new Map(); + + public constructor(private readonly provider: vscode.TreeDataProvider) {} + + public async getChildren(item?: ITreeItem): Promise { + const value = item && this.itemToValue(item); + const children = await Promise.all( + (await this.provider.getChildren(value) || []) + .map(async (childValue) => { + const treeItem = await this.provider.getTreeItem(childValue); + const handle = this.createHandle(treeItem); + this.values.set(handle, childValue); + return { + handle, + collapsibleState: TreeItemCollapsibleState.Collapsed, + }; + }) + ); + + this.clear(value || this.root, item); + this.children.set(value || this.root, children); + + return children; + } + + public dispose(): void { + throw new Error("not implemented"); + } + + private itemToValue(item: ITreeItem): T { + if (!this.values.has(item.handle)) { + throw new Error(`No element found with handle ${item.handle}`); + } + return this.values.get(item.handle)!; + } + + private clear(value: T | Symbol, item?: ITreeItem): void { + if (this.children.has(value)) { + this.children.get(value)!.map((c) => this.clear(this.itemToValue(c), c)); + this.children.delete(value); + } + if (item) { + this.values.delete(item.handle); + } + } + + private createHandle(item: vscode.TreeItem): string { + return item.id + ? `coder-tree-item-id/${item.id}` + : `coder-tree-item-uuid/${generateUuid()}`; + } +} diff --git a/src/client.ts b/src/client.ts index 7e6040a2..4ed40a30 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1 +1,18 @@ -import 'vs/css!./media/firefox'; +import { coderApi, vscodeApi } from "vs/server/src/api"; +import "vs/css!./media/firefox"; +import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection"; + +/** + * This is called by vs/workbench/browser/web.main.ts after the workbench has + * been initialized so we can initialize our own client-side code. + */ +export const initialize = (services: ServiceCollection): void => { + const target = window as any; + target.ide = coderApi(services); + target.vscode = vscodeApi(services); + + const event = new CustomEvent('ide-ready'); + (event as any).ide = target.ide; + (event as any).vscode = target.vscode; + window.dispatchEvent(event); +}; diff --git a/typings/coder.d.ts b/typings/coder.d.ts index bf97e86e..e550513e 100644 --- a/typings/coder.d.ts +++ b/typings/coder.d.ts @@ -1,210 +1,3 @@ declare namespace coder { - export interface IDisposable { - dispose(): void; - } - export interface Disposer extends IDisposable { - onDidDispose: (cb: () => void) => void; - } - export interface Event { - (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; - } - - export interface IStatusbarEntry { - readonly text: string; - readonly tooltip?: string; - readonly color?: string; - readonly command?: string; - readonly arguments?: any[]; - readonly showBeak?: boolean; - } - export interface IStatusbarService { - addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority?: number): IDisposable; - setStatusMessage(message: string, autoDisposeAfter?: number, delayBy?: number): IDisposable; - } - - export interface IAction extends IDisposable { - id: string; - label: string; - tooltip: string; - class: string | undefined; - enabled: boolean; - checked: boolean; - radio: boolean; - run(event?: any): Promise; - } - export type NotificationMessage = string | Error; - export interface INotificationProperties { - sticky?: boolean; - silent?: boolean; - } - - export interface INotificationActions { - primary?: IAction[]; - secondary?: IAction[]; - } - - export interface INotificationProgress { - infinite(): void; - total(value: number): void; - worked(value: number): void; - done(): void; - } - - export interface IPromptChoice { - label: string; - isSecondary?: boolean; - keepOpen?: boolean; - run: () => void; - } - - export interface IPromptOptions extends INotificationProperties { - onCancel?: () => void; - } - - export interface ISerializableCommandAction extends IBaseCommandAction { - // iconLocation?: { dark: UriComponents; light?: UriComponents; }; - } - - export interface IMenuItem { - command: ICommandAction; - alt?: ICommandAction; - // when?: ContextKeyExpr; - group?: "navigation" | string; - order?: number; - } - export interface IMenuRegistry { - appendMenuItem(menu: MenuId, item: IMenuItem): IDisposable; - } - - export interface IBaseCommandAction { - id: string; - title: string; - category?: string; - } - export interface ICommandAction extends IBaseCommandAction { - // iconLocation?: { dark: URI; light?: URI; }; - // precondition?: ContextKeyExpr; - // toggled?: ContextKeyExpr; - } - export interface ICommandHandler { - (accessor: any, ...args: any[]): void; - } - export interface ICommand { - id: string; - handler: ICommandHandler; - description?: ICommandHandlerDescription | null; - } - export interface ICommandHandlerDescription { - description: string; - args: { name: string; description?: string; }[]; - returns?: string; - } - export interface ICommandRegistry { - registerCommand(command: ICommand): IDisposable; - } - - export interface INotification extends INotificationProperties { - severity: Severity; - message: NotificationMessage; - source?: string; - actions?: INotificationActions; - } - export interface INotificationHandle { - readonly onDidClose: Event; - readonly progress: INotificationProgress; - updateSeverity(severity: Severity): void; - updateMessage(message: NotificationMessage): void; - updateActions(actions?: INotificationActions): void; - close(): void; - } - export interface INotificationService { - notify(notification: INotification): INotificationHandle; - info(message: NotificationMessage | NotificationMessage[]): void; - warn(message: NotificationMessage | NotificationMessage[]): void; - error(message: NotificationMessage | NotificationMessage[]): void; - prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle; - } - - export namespace client {} - - export namespace workbench { - // TODO: these types won't actually be included in the package if we try to - // import them. We'll need to recreate them. - export const action: any; // import { Action } from "vs/base/common/actions"; - export const syncActionDescriptor: any; // import { SyncActionDescriptor } from "vs/platform/actions/common/actions"; - export const statusbarService: IStatusbarService; - export const actionsRegistry: any; // import { IWorkbenchActionRegistry } from "vs/workbench/common/actions"; - export const notificationService: INotificationService; - export const menuRegistry: IMenuRegistry; - export const commandRegistry: ICommandRegistry; - export const terminalService: any; // import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal"; - - export const registerView: (viewId: string, viewName: string, containerId: string, containerName: string, icon: string) => void; - - export const onFileCreate: (cb: (path: string) => void) => void; - export const onFileMove: (cb: (path: string, target: string) => void) => void; - export const onFileDelete: (cb: (path: string) => void) => void; - export const onFileSaved: (cb: (path: string) => void) => void; - export const onFileCopy: (cb: (path: string, target: string) => void) => void; - - export const onModelAdded: (cb: (path: string, languageId: string) => void) => void; - export const onModelRemoved: (cb: (path: string, languageId: string) => void) => void; - export const onModelLanguageChange: (cb: (path: string, languageId: string, oldLanguageId: string) => void) => void; - - export const onTerminalAdded: (cb: () => void) => void; - export const onTerminalRemoved: (cb: () => void) => void; - } - - export enum Severity { - Ignore = 0, - Info = 1, - Warning = 2, - Error = 3, - } - - export enum StatusbarAlignment { - LEFT, RIGHT, - } - - export enum MenuId { - CommandPalette, - DebugBreakpointsContext, - DebugCallStackContext, - DebugConsoleContext, - DebugVariablesContext, - DebugWatchContext, - DebugToolBar, - EditorContext, - EditorTitle, - EditorTitleContext, - EmptyEditorGroupContext, - ExplorerContext, - MenubarAppearanceMenu, - MenubarDebugMenu, - MenubarEditMenu, - MenubarFileMenu, - MenubarGoMenu, - MenubarHelpMenu, - MenubarLayoutMenu, - MenubarNewBreakpointMenu, - MenubarPreferencesMenu, - MenubarRecentMenu, - MenubarSelectionMenu, - MenubarSwitchEditorMenu, - MenubarSwitchGroupMenu, - MenubarTerminalMenu, - MenubarViewMenu, - OpenEditorsContext, - ProblemsPanelContext, - SCMChangeContext, - SCMResourceContext, - SCMResourceGroupContext, - SCMSourceControl, - SCMTitle, - SearchContext, - StatusBarWindowIndicatorMenu, - TouchBarContext, - ViewItemContext, - ViewTitle, - } + export const registerView: (viewId: string, viewName: string, containerId: string, containerName: string, icon: string) => void; }