From 45d348b03d88adff27b67e81a2219aaafa185f11 Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 17 Jul 2019 11:57:26 -0500 Subject: [PATCH] Expose API on the client --- scripts/vscode.patch | 30 ++++- src/api.ts | 310 +++++++++++++++++++++++++++++++++++++++++++ typings/api.d.ts | 27 ++++ typings/coder.d.ts | 210 +++++++++++++++++++++++++++++ 4 files changed, 574 insertions(+), 3 deletions(-) create mode 100644 src/api.ts create mode 100644 typings/api.d.ts create mode 100644 typings/coder.d.ts diff --git a/scripts/vscode.patch b/scripts/vscode.patch index e97e530ca..a7bf907c3 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -312,10 +312,34 @@ index 8e1b68eb36..2b6a0d5b15 100644 + } +} diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 1986fb6642..afbe385af6 100644 +index 1986fb6642..1bf169a4b4 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts -@@ -115,6 +115,9 @@ class CodeRendererMain extends Disposable { +@@ -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'; + + class CodeRendererMain extends Disposable { + +@@ -71,6 +72,15 @@ 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); + } + + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService }> { +@@ -115,6 +125,9 @@ class CodeRendererMain extends Disposable { const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment())); fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); @@ -325,7 +349,7 @@ index 1986fb6642..afbe385af6 100644 } const payload = await this.resolveWorkspaceInitializationPayload(); -@@ -170,4 +173,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt +@@ -170,4 +183,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt const renderer = new CodeRendererMain(domElement, options); return renderer.open(); diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 000000000..47307d61e --- /dev/null +++ b/src/api.ts @@ -0,0 +1,310 @@ +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 { 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 { 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 { 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"; +import { ViewContainerViewlet } from "vs/workbench/browser/parts/views/viewsViewlet"; +import { IConfigurationService } from "vs/platform/configuration/common/configuration"; +import { IWorkbenchLayoutService } from "vs/workbench/services/layout/browser/layoutService"; +import { ITelemetryService } from "vs/platform/telemetry/common/telemetry"; +import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace"; +import { IEditorService } from "vs/workbench/services/editor/common/editorService"; +import { IThemeService } from "vs/platform/theme/common/themeService"; +import { IContextMenuService } from "vs/platform/contextview/browser/contextView"; +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"; + +/** + * Client-side implementation of VS Code's API. + * TODO: Views aren't quite working. + * TODO: Implement menu items for views (for item actions). + * TODO: File system provider doesn't work. + */ +export const vscodeApi = (serviceCollection: ServiceCollection): typeof vscode => { + const getService = (id: ServiceIdentifier): T => serviceCollection.get(id) as T; + const commandService = getService(ICommandService); + const notificationService = getService(INotificationService); + const fileService = getService(IFileService); + const viewsRegistry = Registry.as(ViewsExtensions.ViewsRegistry); + + // It would be nice to just export what VS Code creates but it looks to me + // that it assumes it's running in the extension host and wouldn't work here. + // It is probably possible to create an extension host that runs in the + // browser's main thread, but I'm not sure how much jank that would require. + // We could have a web worker host but we want DOM access. + return { + EventEmitter: Emitter, + TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState, + FileSystemError: extHostTypes.FileSystemError, + FileType: FileType, + Uri: URI, + + commands: { + executeCommand: (commandId: string, ...args: any[]): any => { + return commandService.executeCommand(commandId, ...args); + }, + registerCommand: (id: string, command: () => void): any => { + return CommandsRegistry.registerCommand(id, command); + }, + }, + + window: { + registerTreeDataProvider: (id: string, dataProvider: ITreeViewDataProvider): void => { + const view = viewsRegistry.getView(id); + if (view) { + (view as ITreeViewDescriptor).treeView.dataProvider = dataProvider; + } + }, + showErrorMessage: (message: string): void => { + notificationService.error(message); + }, + }, + + workspace: { + registerFileSystemProvider: (scheme: string, provider: vscode.FileSystemProvider): IDisposable => { + return fileService.registerProvider(scheme, new FileSystemProvider(provider)); + }, + }, + } as any; +}; + +/** + * Coder 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 viewContainersRegistry = Registry.as(ViewsExtensions.ViewContainersRegistry); + const viewsRegistry = Registry.as(ViewsExtensions.ViewsRegistry); + const container = viewContainersRegistry.registerViewContainer(containerId); + + 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); + } + } + + const viewletDescriptor = new ViewletDescriptor( + CustomViewlet as any, + id, + containerName, + cssClass, + undefined, + URI.parse(icon), + ); + + Registry.as(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor); + + const registry = Registry.as(ActionExtensions.WorkbenchActions); + registry.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%`); + + const views = [{ + id: viewId, + name: viewName, + ctorDescriptor: { ctor: CustomTreeViewPanel }, + treeView: getService(IInstantiationService).createInstance(CustomTreeView as any, viewId, container), + }] as ITreeViewDescriptor[]; + viewsRegistry.registerViews(views, container); + }, + // Even though the enums are exactly the same, Typescript says they are + // not assignable to each other, so use `any`. I don't know if there is a + // way around this. + 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()); + }, + }, + + // @ts-ignore + MenuId: MenuId, + Severity: Severity, + // @ts-ignore + StatusbarAlignment: StatusbarAlignment, + }; +}; + +class OpenCustomViewletAction extends ShowViewletAction { + public constructor( + id: string, label: string, + @IViewletService viewletService: IViewletService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + ) { + super(id, label, id, viewletService, editorGroupService, layoutService); + } +} + +class FileSystemProvider implements IFileSystemProvider { + private readonly _onDidChange = new Emitter(); + + public readonly onDidChangeFile: Event = this._onDidChange.event; + + public readonly capabilities: FileSystemProviderCapabilities; + public readonly onDidChangeCapabilities: Event = Event.None; + + public constructor( + private readonly provider: vscode.FileSystemProvider, + ) { + this.capabilities = FileSystemProviderCapabilities.Readonly; + } + + public watch(resource: URI, opts: IWatchOptions): IDisposable { + return this.provider.watch(resource, opts); + } + + public async stat(resource: URI): Promise { + return this.provider.stat(resource); + } + + public async readFile(resource: URI): Promise { + return this.provider.readFile(resource); + } + + public async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + return this.provider.writeFile(resource, content, opts); + } + + public async delete(resource: URI, opts: FileDeleteOptions): Promise { + return this.provider.delete(resource, opts); + } + + public mkdir(_resource: URI): Promise { + throw new Error("not implemented"); + } + + public async readdir(resource: URI): Promise<[string, FileType][]> { + return this.provider.readDirectory(resource); + } + + public async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.provider.rename(resource, target, opts); + } + + public async copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise { + return this.provider.copy!(resource, target, opts); + } + + public open(_resource: URI, _opts: FileOpenOptions): Promise { + throw new Error("not implemented"); + } + + public close(_fd: number): Promise { + throw new Error("not implemented"); + } + + public read(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise { + throw new Error("not implemented"); + } + + public write(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise { + throw new Error("not implemented"); + } +} diff --git a/typings/api.d.ts b/typings/api.d.ts new file mode 100644 index 000000000..e85eddc9b --- /dev/null +++ b/typings/api.d.ts @@ -0,0 +1,27 @@ +import * as vscode from "vscode"; + +export { vscode }; + +export interface IdeReadyEvent extends CustomEvent { + readonly vscode: typeof vscode; + readonly ide: typeof coder; +} + +declare global { + interface Window { + /** + * Full VS Code extension API. + */ + vscode?: typeof vscode; + + /** + * Coder API. + */ + ide?: typeof coder; + + /** + * Listen for when the IDE API has been set and is ready to use. + */ + addEventListener(event: "ide-ready", callback: (event: IdeReadyEvent) => void): void; + } +} diff --git a/typings/coder.d.ts b/typings/coder.d.ts new file mode 100644 index 000000000..bf97e86ec --- /dev/null +++ b/typings/coder.d.ts @@ -0,0 +1,210 @@ +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, + } +}