Skip to content

Commit 4361e94

Browse files
committed
Expose API on the client
1 parent fcf4efb commit 4361e94

File tree

4 files changed

+574
-3
lines changed

4 files changed

+574
-3
lines changed

scripts/vscode.patch

+27-3
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,34 @@ index 8e1b68eb36..2b6a0d5b15 100644
312312
+ }
313313
+}
314314
diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts
315-
index 1986fb6642..afbe385af6 100644
315+
index 1986fb6642..1bf169a4b4 100644
316316
--- a/src/vs/workbench/browser/web.main.ts
317317
+++ b/src/vs/workbench/browser/web.main.ts
318-
@@ -115,6 +115,9 @@ class CodeRendererMain extends Disposable {
318+
@@ -35,6 +35,7 @@ import { SignService } from 'vs/platform/sign/browser/signService';
319+
import { hash } from 'vs/base/common/hash';
320+
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
321+
import { ProductService } from 'vs/platform/product/browser/productService';
322+
+import { coderApi, vscodeApi } from 'vs/server/src/api';
323+
324+
class CodeRendererMain extends Disposable {
325+
326+
@@ -71,6 +72,15 @@ class CodeRendererMain extends Disposable {
327+
328+
// Startup
329+
this.workbench.startup();
330+
+
331+
+ const target = window as any;
332+
+ target.ide = coderApi(services.serviceCollection);
333+
+ target.vscode = vscodeApi(services.serviceCollection);
334+
+
335+
+ const event = new CustomEvent('ide-ready');
336+
+ (event as any).ide = target.ide;
337+
+ (event as any).vscode = target.vscode;
338+
+ window.dispatchEvent(event);
339+
}
340+
341+
private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService }> {
342+
@@ -115,6 +125,9 @@ class CodeRendererMain extends Disposable {
319343
const remoteFileSystemProvider = this._register(new RemoteExtensionsFileSystemProvider(channel, remoteAgentService.getEnvironment()));
320344

321345
fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider);
@@ -325,7 +349,7 @@ index 1986fb6642..afbe385af6 100644
325349
}
326350

327351
const payload = await this.resolveWorkspaceInitializationPayload();
328-
@@ -170,4 +173,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt
352+
@@ -170,4 +183,4 @@ export function main(domElement: HTMLElement, options: IWorkbenchConstructionOpt
329353
const renderer = new CodeRendererMain(domElement, options);
330354

331355
return renderer.open();

src/api.ts

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import * as vscode from "vscode";
2+
3+
import { localize } from "vs/nls";
4+
import { Action } from "vs/base/common/actions";
5+
import { SyncActionDescriptor, MenuRegistry, MenuId } from "vs/platform/actions/common/actions";
6+
import { Registry } from "vs/platform/registry/common/platform";
7+
import { IWorkbenchActionRegistry, Extensions as ActionExtensions} from "vs/workbench/common/actions";
8+
import { CommandsRegistry, ICommandService } from "vs/platform/commands/common/commands";
9+
import { IStat, IWatchOptions, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileChange, FileWriteOptions, FileSystemProviderCapabilities, IFileService, FileType, FileOperation, IFileSystemProvider } from "vs/platform/files/common/files";
10+
import { ITextFileService } from "vs/workbench/services/textfile/common/textfiles";
11+
import { IModelService } from "vs/editor/common/services/modelService";
12+
import { ITerminalService } from "vs/workbench/contrib/terminal/common/terminal";
13+
import { IStorageService } from "vs/platform/storage/common/storage";
14+
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
15+
import { INotificationService } from "vs/platform/notification/common/notification";
16+
import { IStatusbarService, StatusbarAlignment } from "vs/platform/statusbar/common/statusbar";
17+
import Severity from "vs/base/common/severity";
18+
import { Emitter, Event } from "vs/base/common/event";
19+
import * as extHostTypes from "vs/workbench/api/common/extHostTypes";
20+
import { ServiceIdentifier, IInstantiationService } from "vs/platform/instantiation/common/instantiation";
21+
import { URI } from "vs/base/common/uri";
22+
import { ITreeViewDataProvider, IViewsRegistry, ITreeViewDescriptor, Extensions as ViewsExtensions, IViewContainersRegistry } from "vs/workbench/common/views";
23+
import { CustomTreeViewPanel, CustomTreeView } from "vs/workbench/browser/parts/views/customView";
24+
import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from "vs/workbench/browser/viewlet";
25+
import { IExtensionService } from "vs/workbench/services/extensions/common/extensions";
26+
import { ViewContainerViewlet } from "vs/workbench/browser/parts/views/viewsViewlet";
27+
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
28+
import { IWorkbenchLayoutService } from "vs/workbench/services/layout/browser/layoutService";
29+
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
30+
import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace";
31+
import { IEditorService } from "vs/workbench/services/editor/common/editorService";
32+
import { IThemeService } from "vs/platform/theme/common/themeService";
33+
import { IContextMenuService } from "vs/platform/contextview/browser/contextView";
34+
import { IViewletService } from "vs/workbench/services/viewlet/browser/viewlet";
35+
import { IEditorGroupsService } from "vs/workbench/services/editor/common/editorGroupsService";
36+
import { createCSSRule } from "vs/base/browser/dom";
37+
import { IDisposable } from "vs/base/common/lifecycle";
38+
39+
/**
40+
* Client-side implementation of VS Code's API.
41+
* TODO: Views aren't quite working.
42+
* TODO: Implement menu items for views (for item actions).
43+
* TODO: File system provider doesn't work.
44+
*/
45+
export const vscodeApi = (serviceCollection: ServiceCollection): typeof vscode => {
46+
const getService = <T>(id: ServiceIdentifier<T>): T => serviceCollection.get<T>(id) as T;
47+
const commandService = getService(ICommandService);
48+
const notificationService = getService(INotificationService);
49+
const fileService = getService(IFileService);
50+
const viewsRegistry = Registry.as<IViewsRegistry>(ViewsExtensions.ViewsRegistry);
51+
52+
// It would be nice to just export what VS Code creates but it looks to me
53+
// that it assumes it's running in the extension host and wouldn't work here.
54+
// It is probably possible to create an extension host that runs in the
55+
// browser's main thread, but I'm not sure how much jank that would require.
56+
// We could have a web worker host but we want DOM access.
57+
return {
58+
EventEmitter: Emitter,
59+
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
60+
FileSystemError: extHostTypes.FileSystemError,
61+
FileType: FileType,
62+
Uri: URI,
63+
64+
commands: {
65+
executeCommand: (commandId: string, ...args: any[]): any => {
66+
return commandService.executeCommand(commandId, ...args);
67+
},
68+
registerCommand: (id: string, command: () => void): any => {
69+
return CommandsRegistry.registerCommand(id, command);
70+
},
71+
},
72+
73+
window: {
74+
registerTreeDataProvider: (id: string, dataProvider: ITreeViewDataProvider): void => {
75+
const view = viewsRegistry.getView(id);
76+
if (view) {
77+
(view as ITreeViewDescriptor).treeView.dataProvider = dataProvider;
78+
}
79+
},
80+
showErrorMessage: (message: string): void => {
81+
notificationService.error(message);
82+
},
83+
},
84+
85+
workspace: {
86+
registerFileSystemProvider: (scheme: string, provider: vscode.FileSystemProvider): IDisposable => {
87+
return fileService.registerProvider(scheme, new FileSystemProvider(provider));
88+
},
89+
},
90+
} as any;
91+
};
92+
93+
/**
94+
* Coder API.
95+
*/
96+
export const coderApi = (serviceCollection: ServiceCollection): typeof coder => {
97+
const getService = <T>(id: ServiceIdentifier<T>): T => serviceCollection.get<T>(id) as T;
98+
99+
return {
100+
workbench: {
101+
action: Action,
102+
syncActionDescriptor: SyncActionDescriptor,
103+
commandRegistry: CommandsRegistry,
104+
actionsRegistry: Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions),
105+
registerView: (viewId, viewName, containerId, containerName, icon): void => {
106+
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewsExtensions.ViewContainersRegistry);
107+
const viewsRegistry = Registry.as<IViewsRegistry>(ViewsExtensions.ViewsRegistry);
108+
const container = viewContainersRegistry.registerViewContainer(containerId);
109+
110+
const cssClass = `extensionViewlet-${containerId}`;
111+
const id = `workbench.view.extension.${containerId}`;
112+
113+
class CustomViewlet extends ViewContainerViewlet {
114+
public constructor(
115+
@IConfigurationService configurationService: IConfigurationService,
116+
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
117+
@ITelemetryService telemetryService: ITelemetryService,
118+
@IWorkspaceContextService contextService: IWorkspaceContextService,
119+
@IStorageService storageService: IStorageService,
120+
@IEditorService _editorService: IEditorService,
121+
@IInstantiationService instantiationService: IInstantiationService,
122+
@IThemeService themeService: IThemeService,
123+
@IContextMenuService contextMenuService: IContextMenuService,
124+
@IExtensionService extensionService: IExtensionService,
125+
) {
126+
super(id, `${id}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
127+
}
128+
}
129+
130+
const viewletDescriptor = new ViewletDescriptor(
131+
CustomViewlet as any,
132+
id,
133+
containerName,
134+
cssClass,
135+
undefined,
136+
URI.parse(icon),
137+
);
138+
139+
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor);
140+
141+
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
142+
registry.registerWorkbenchAction(
143+
new SyncActionDescriptor(OpenCustomViewletAction as any, id, localize("showViewlet", "Show {0}", containerName)),
144+
"View: Show {0}",
145+
localize("view", "View"),
146+
);
147+
148+
// Generate CSS to show the icon in the activity bar
149+
const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`;
150+
createCSSRule(iconClass, `-webkit-mask: url('${icon}') no-repeat 50% 50%`);
151+
152+
const views = [{
153+
id: viewId,
154+
name: viewName,
155+
ctorDescriptor: { ctor: CustomTreeViewPanel },
156+
treeView: getService(IInstantiationService).createInstance(CustomTreeView as any, viewId, container),
157+
}] as ITreeViewDescriptor[];
158+
viewsRegistry.registerViews(views, container);
159+
},
160+
// Even though the enums are exactly the same, Typescript says they are
161+
// not assignable to each other, so use `any`. I don't know if there is a
162+
// way around this.
163+
menuRegistry: MenuRegistry as any,
164+
statusbarService: getService(IStatusbarService) as any,
165+
notificationService: getService(INotificationService),
166+
terminalService: getService(ITerminalService),
167+
168+
onFileCreate: (cb): void => {
169+
getService<IFileService>(IFileService).onAfterOperation((e) => {
170+
if (e.operation === FileOperation.CREATE) {
171+
cb(e.resource.path);
172+
}
173+
});
174+
},
175+
onFileMove: (cb): void => {
176+
getService<IFileService>(IFileService).onAfterOperation((e) => {
177+
if (e.operation === FileOperation.MOVE) {
178+
cb(e.resource.path, e.target ? e.target.resource.path : undefined!);
179+
}
180+
});
181+
},
182+
onFileDelete: (cb): void => {
183+
getService<IFileService>(IFileService).onAfterOperation((e) => {
184+
if (e.operation === FileOperation.DELETE) {
185+
cb(e.resource.path);
186+
}
187+
});
188+
},
189+
onFileSaved: (cb): void => {
190+
getService<ITextFileService>(ITextFileService).models.onModelSaved((e) => {
191+
cb(e.resource.path);
192+
});
193+
},
194+
onFileCopy: (cb): void => {
195+
getService<IFileService>(IFileService).onAfterOperation((e) => {
196+
if (e.operation === FileOperation.COPY) {
197+
cb(e.resource.path, e.target ? e.target.resource.path : undefined!);
198+
}
199+
});
200+
},
201+
202+
onModelAdded: (cb): void => {
203+
getService<IModelService>(IModelService).onModelAdded((e) => {
204+
cb(e.uri.path, e.getLanguageIdentifier().language);
205+
});
206+
},
207+
onModelRemoved: (cb): void => {
208+
getService<IModelService>(IModelService).onModelRemoved((e) => {
209+
cb(e.uri.path, e.getLanguageIdentifier().language);
210+
});
211+
},
212+
onModelLanguageChange: (cb): void => {
213+
getService<IModelService>(IModelService).onModelModeChanged((e) => {
214+
cb(e.model.uri.path, e.model.getLanguageIdentifier().language, e.oldModeId);
215+
});
216+
},
217+
218+
onTerminalAdded: (cb): void => {
219+
getService<ITerminalService>(ITerminalService).onInstanceCreated(() => cb());
220+
},
221+
onTerminalRemoved: (cb): void => {
222+
getService<ITerminalService>(ITerminalService).onInstanceDisposed(() => cb());
223+
},
224+
},
225+
226+
// @ts-ignore
227+
MenuId: MenuId,
228+
Severity: Severity,
229+
// @ts-ignore
230+
StatusbarAlignment: StatusbarAlignment,
231+
};
232+
};
233+
234+
class OpenCustomViewletAction extends ShowViewletAction {
235+
public constructor(
236+
id: string, label: string,
237+
@IViewletService viewletService: IViewletService,
238+
@IEditorGroupsService editorGroupService: IEditorGroupsService,
239+
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
240+
) {
241+
super(id, label, id, viewletService, editorGroupService, layoutService);
242+
}
243+
}
244+
245+
class FileSystemProvider implements IFileSystemProvider {
246+
private readonly _onDidChange = new Emitter<IFileChange[]>();
247+
248+
public readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChange.event;
249+
250+
public readonly capabilities: FileSystemProviderCapabilities;
251+
public readonly onDidChangeCapabilities: Event<void> = Event.None;
252+
253+
public constructor(
254+
private readonly provider: vscode.FileSystemProvider,
255+
) {
256+
this.capabilities = FileSystemProviderCapabilities.Readonly;
257+
}
258+
259+
public watch(resource: URI, opts: IWatchOptions): IDisposable {
260+
return this.provider.watch(resource, opts);
261+
}
262+
263+
public async stat(resource: URI): Promise<IStat> {
264+
return this.provider.stat(resource);
265+
}
266+
267+
public async readFile(resource: URI): Promise<Uint8Array> {
268+
return this.provider.readFile(resource);
269+
}
270+
271+
public async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
272+
return this.provider.writeFile(resource, content, opts);
273+
}
274+
275+
public async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
276+
return this.provider.delete(resource, opts);
277+
}
278+
279+
public mkdir(_resource: URI): Promise<void> {
280+
throw new Error("not implemented");
281+
}
282+
283+
public async readdir(resource: URI): Promise<[string, FileType][]> {
284+
return this.provider.readDirectory(resource);
285+
}
286+
287+
public async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
288+
return this.provider.rename(resource, target, opts);
289+
}
290+
291+
public async copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
292+
return this.provider.copy!(resource, target, opts);
293+
}
294+
295+
public open(_resource: URI, _opts: FileOpenOptions): Promise<number> {
296+
throw new Error("not implemented");
297+
}
298+
299+
public close(_fd: number): Promise<void> {
300+
throw new Error("not implemented");
301+
}
302+
303+
public read(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise<number> {
304+
throw new Error("not implemented");
305+
}
306+
307+
public write(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise<number> {
308+
throw new Error("not implemented");
309+
}
310+
}

typings/api.d.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as vscode from "vscode";
2+
3+
export { vscode };
4+
5+
export interface IdeReadyEvent extends CustomEvent<void> {
6+
readonly vscode: typeof vscode;
7+
readonly ide: typeof coder;
8+
}
9+
10+
declare global {
11+
interface Window {
12+
/**
13+
* Full VS Code extension API.
14+
*/
15+
vscode?: typeof vscode;
16+
17+
/**
18+
* Coder API.
19+
*/
20+
ide?: typeof coder;
21+
22+
/**
23+
* Listen for when the IDE API has been set and is ready to use.
24+
*/
25+
addEventListener(event: "ide-ready", callback: (event: IdeReadyEvent) => void): void;
26+
}
27+
}

0 commit comments

Comments
 (0)