Skip to content

Commit 4a275b2

Browse files
authoredFeb 21, 2024··
Improve notebook kernel/renderer messaging (#13401)
1 parent 6e8031b commit 4a275b2

19 files changed

+362
-69
lines changed
 

‎packages/notebook/src/browser/notebook-editor-widget.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export function createNotebookEditorWidgetContainer(parent: interfaces.Container
4646

4747
const NotebookEditorProps = Symbol('NotebookEditorProps');
4848

49+
interface RenderMessage {
50+
rendererId: string;
51+
message: unknown;
52+
}
53+
4954
export interface NotebookEditorProps {
5055
uri: URI,
5156
readonly notebookType: string,
@@ -87,6 +92,18 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
8792
protected readonly onDidChangeReadOnlyEmitter = new Emitter<boolean | MarkdownString>();
8893
readonly onDidChangeReadOnly = this.onDidChangeReadOnlyEmitter.event;
8994

95+
protected readonly onPostKernelMessageEmitter = new Emitter<unknown>();
96+
readonly onPostKernelMessage = this.onPostKernelMessageEmitter.event;
97+
98+
protected readonly onDidPostKernelMessageEmitter = new Emitter<unknown>();
99+
readonly onDidPostKernelMessage = this.onDidPostKernelMessageEmitter.event;
100+
101+
protected readonly onPostRendererMessageEmitter = new Emitter<RenderMessage>();
102+
readonly onPostRendererMessage = this.onPostRendererMessageEmitter.event;
103+
104+
protected readonly onDidReceiveKernelMessageEmitter = new Emitter<unknown>();
105+
readonly onDidRecieveKernelMessage = this.onDidReceiveKernelMessageEmitter.event;
106+
90107
protected readonly renderers = new Map<CellKind, CellRenderer>();
91108
protected _model?: NotebookModel;
92109
protected _ready: Deferred<NotebookModel> = new Deferred();
@@ -190,4 +207,24 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
190207
super.onAfterDetach(msg);
191208
this.notebookEditorService.removeNotebookEditor(this);
192209
}
210+
211+
postKernelMessage(message: unknown): void {
212+
this.onDidPostKernelMessageEmitter.fire(message);
213+
}
214+
215+
postRendererMessage(rendererId: string, message: unknown): void {
216+
this.onPostRendererMessageEmitter.fire({ rendererId, message });
217+
}
218+
219+
recieveKernelMessage(message: unknown): void {
220+
this.onDidReceiveKernelMessageEmitter.fire(message);
221+
}
222+
223+
override dispose(): void {
224+
this.onDidChangeModelEmitter.dispose();
225+
this.onDidPostKernelMessageEmitter.dispose();
226+
this.onDidReceiveKernelMessageEmitter.dispose();
227+
this.onPostRendererMessageEmitter.dispose();
228+
super.dispose();
229+
}
193230
}

‎packages/notebook/src/browser/notebook-renderer-registry.ts

+19
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export interface NotebookRendererInfo {
3030
readonly requiresMessaging: boolean;
3131
}
3232

33+
export interface NotebookPreloadInfo {
34+
readonly type: string;
35+
readonly entrypoint: string;
36+
}
37+
3338
@injectable()
3439
export class NotebookRendererRegistry {
3540

@@ -39,6 +44,12 @@ export class NotebookRendererRegistry {
3944
return this._notebookRenderers;
4045
}
4146

47+
private readonly _staticNotebookPreloads: NotebookPreloadInfo[] = [];
48+
49+
get staticNotebookPreloads(): readonly NotebookPreloadInfo[] {
50+
return this._staticNotebookPreloads;
51+
}
52+
4253
registerNotebookRenderer(type: NotebookRendererDescriptor, basePath: string): Disposable {
4354
let entrypoint;
4455
if (typeof type.entrypoint === 'string') {
@@ -62,5 +73,13 @@ export class NotebookRendererRegistry {
6273
this._notebookRenderers.splice(this._notebookRenderers.findIndex(renderer => renderer.id === type.id), 1);
6374
});
6475
}
76+
77+
registerStaticNotebookPreload(type: string, entrypoint: string, basePath: string): Disposable {
78+
const staticPreload = { type, entrypoint: new Path(basePath).join(entrypoint).toString() };
79+
this._staticNotebookPreloads.push(staticPreload);
80+
return Disposable.create(() => {
81+
this._staticNotebookPreloads.splice(this._staticNotebookPreloads.indexOf(staticPreload), 1);
82+
});
83+
}
6584
}
6685

‎packages/notebook/src/browser/renderers/cell-output-webview.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
import { Disposable } from '@theia/core';
1818
import { NotebookCellModel } from '../view-model/notebook-cell-model';
19+
import { NotebookModel } from '../view-model/notebook-model';
1920

2021
export const CellOutputWebviewFactory = Symbol('outputWebviewFactory');
2122

22-
export type CellOutputWebviewFactory = (cell: NotebookCellModel) => Promise<CellOutputWebview>;
23+
export type CellOutputWebviewFactory = (cell: NotebookCellModel, notebook: NotebookModel) => Promise<CellOutputWebview>;
2324

2425
export interface CellOutputWebview extends Disposable {
2526

‎packages/notebook/src/browser/service/notebook-kernel-service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export interface NotebookKernel {
5454
// ID of the extension providing this kernel
5555
readonly extensionId: string;
5656

57+
readonly localResourceRoot: URI;
58+
readonly preloadUris: URI[];
59+
readonly preloadProvides: string[];
60+
61+
readonly handle: number;
5762
label: string;
5863
description?: string;
5964
detail?: string;

‎packages/notebook/src/browser/service/notebook-renderer-messaging-service.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
*--------------------------------------------------------------------------------------------*/
2020

2121
import { Emitter } from '@theia/core';
22-
import { injectable } from '@theia/core/shared/inversify';
22+
import { inject, injectable } from '@theia/core/shared/inversify';
2323
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
24+
import { NotebookEditorWidgetService } from './notebook-editor-widget-service';
2425

2526
interface RendererMessage {
2627
editorId: string;
@@ -50,6 +51,9 @@ export class NotebookRendererMessagingService implements Disposable {
5051
private readonly willActivateRendererEmitter = new Emitter<string>();
5152
readonly onWillActivateRenderer = this.willActivateRendererEmitter.event;
5253

54+
@inject(NotebookEditorWidgetService)
55+
private readonly editorWidgetService: NotebookEditorWidgetService;
56+
5357
private readonly activations = new Map<string /* rendererId */, undefined | RendererMessage[]>();
5458
private readonly scopedMessaging = new Map<string /* editorId */, RendererMessaging>();
5559

@@ -86,6 +90,10 @@ export class NotebookRendererMessagingService implements Disposable {
8690

8791
const messaging: RendererMessaging = {
8892
postMessage: (rendererId, message) => this.postMessage(editorId, rendererId, message),
93+
receiveMessage: async (rendererId, message) => {
94+
this.editorWidgetService.getNotebookEditor(editorId)?.postRendererMessage(rendererId, message);
95+
return true;
96+
},
8997
dispose: () => this.scopedMessaging.delete(editorId),
9098
};
9199

‎packages/notebook/src/browser/view/notebook-code-cell-view.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class NotebookCodeCellRenderer implements CellRenderer {
6161
</div>
6262
</div>
6363
<div className='theia-notebook-cell-with-sidebar'>
64-
<NotebookCodeCellOutputs cell={cell} outputWebviewFactory={this.cellOutputWebviewFactory}
64+
<NotebookCodeCellOutputs cell={cell} notebook={notebookModel} outputWebviewFactory={this.cellOutputWebviewFactory}
6565
renderSidebar={() =>
6666
this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, notebookModel, cell, cell.outputs[0])} />
6767
</div>
@@ -166,6 +166,7 @@ export class NotebookCodeCellStatus extends React.Component<NotebookCodeCellStat
166166

167167
interface NotebookCellOutputProps {
168168
cell: NotebookCellModel;
169+
notebook: NotebookModel;
169170
outputWebviewFactory: CellOutputWebviewFactory;
170171
renderSidebar: () => React.ReactNode;
171172
}
@@ -182,14 +183,14 @@ export class NotebookCodeCellOutputs extends React.Component<NotebookCellOutputP
182183
}
183184

184185
override async componentDidMount(): Promise<void> {
185-
const { cell, outputWebviewFactory } = this.props;
186+
const { cell, notebook, outputWebviewFactory } = this.props;
186187
this.toDispose.push(cell.onDidChangeOutputs(async () => {
187188
if (!this.outputsWebviewPromise && cell.outputs.length > 0) {
188-
this.outputsWebviewPromise = outputWebviewFactory(cell).then(webview => {
189+
this.outputsWebviewPromise = outputWebviewFactory(cell, notebook).then(webview => {
189190
this.outputsWebview = webview;
190191
this.forceUpdate();
191192
return webview;
192-
});
193+
});
193194
this.forceUpdate();
194195
} else if (this.outputsWebviewPromise && cell.outputs.length === 0 && cell.internalMetadata.runEndTime) {
195196
(await this.outputsWebviewPromise).dispose();
@@ -199,7 +200,7 @@ export class NotebookCodeCellOutputs extends React.Component<NotebookCellOutputP
199200
}
200201
}));
201202
if (cell.outputs.length > 0) {
202-
this.outputsWebviewPromise = outputWebviewFactory(cell).then(webview => {
203+
this.outputsWebviewPromise = outputWebviewFactory(cell, notebook).then(webview => {
203204
this.outputsWebview = webview;
204205
this.forceUpdate();
205206
return webview;

‎packages/plugin-ext/src/common/plugin-api-rpc.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2288,7 +2288,7 @@ export const MAIN_RPC_CONTEXT = {
22882288
NOTEBOOKS_EXT: createProxyIdentifier<NotebooksExt>('NotebooksExt'),
22892289
NOTEBOOK_DOCUMENTS_EXT: createProxyIdentifier<NotebookDocumentsExt>('NotebookDocumentsExt'),
22902290
NOTEBOOK_EDITORS_EXT: createProxyIdentifier<NotebookEditorsExt>('NotebookEditorsExt'),
2291-
NOTEBOOK_RENDERERS_EXT: createProxyIdentifier<NotebookRenderersExt>('NotebooksExt'),
2291+
NOTEBOOK_RENDERERS_EXT: createProxyIdentifier<NotebookRenderersExt>('NotebooksRenderersExt'),
22922292
NOTEBOOK_KERNELS_EXT: createProxyIdentifier<NotebookKernelsExt>('NotebookKernelsExt'),
22932293
TERMINAL_EXT: createProxyIdentifier<TerminalServiceExt>('TerminalServiceExt'),
22942294
OUTPUT_CHANNEL_REGISTRY_EXT: createProxyIdentifier<OutputChannelRegistryExt>('OutputChannelRegistryExt'),
@@ -2488,7 +2488,7 @@ export interface NotebookKernelDto {
24882488
id: string;
24892489
notebookType: string;
24902490
extensionId: string;
2491-
// extensionLocation: UriComponents;
2491+
extensionLocation: UriComponents;
24922492
label: string;
24932493
detail?: string;
24942494
description?: string;

‎packages/plugin-ext/src/common/plugin-protocol.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export interface PluginPackageContribution {
103103
terminal?: PluginPackageTerminal;
104104
notebooks?: PluginPackageNotebook[];
105105
notebookRenderer?: PluginNotebookRendererContribution[];
106+
notebookPreload?: PluginPackageNotebookPreload[];
106107
}
107108

108109
export interface PluginPackageNotebook {
@@ -120,6 +121,11 @@ export interface PluginNotebookRendererContribution {
120121
readonly requiresMessaging?: 'always' | 'optional' | 'never'
121122
}
122123

124+
export interface PluginPackageNotebookPreload {
125+
type: string;
126+
entrypoint: string;
127+
}
128+
123129
export interface PluginPackageAuthenticationProvider {
124130
id: string;
125131
label: string;
@@ -610,8 +616,8 @@ export interface PluginContribution {
610616
terminalProfiles?: TerminalProfile[];
611617
notebooks?: NotebookContribution[];
612618
notebookRenderer?: NotebookRendererContribution[];
619+
notebookPreload?: notebookPreloadContribution[];
613620
}
614-
615621
export interface NotebookContribution {
616622
type: string;
617623
displayName: string;
@@ -627,6 +633,11 @@ export interface NotebookRendererContribution {
627633
readonly requiresMessaging?: 'always' | 'optional' | 'never'
628634
}
629635

636+
export interface notebookPreloadContribution {
637+
type: string;
638+
entrypoint: string;
639+
}
640+
630641
export interface AuthenticationProviderInformation {
631642
id: string;
632643
label: string;

‎packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,19 @@ export class TheiaPluginScanner extends AbstractPluginScanner {
341341
try {
342342
contributions.notebooks = rawPlugin.contributes.notebooks;
343343
} catch (err) {
344-
console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err);
344+
console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.notebooks, err);
345345
}
346346

347347
try {
348348
contributions.notebookRenderer = rawPlugin.contributes.notebookRenderer;
349349
} catch (err) {
350-
console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks'.`, rawPlugin.contributes.authentication, err);
350+
console.error(`Could not read '${rawPlugin.name}' contribution 'notebook-renderer'.`, rawPlugin.contributes.notebookRenderer, err);
351+
}
352+
353+
try {
354+
contributions.notebookPreload = rawPlugin.contributes.notebookPreload;
355+
} catch (err) {
356+
console.error(`Could not read '${rawPlugin.name}' contribution 'notebooks-preload'.`, rawPlugin.contributes.notebookPreload, err);
351357
}
352358

353359
try {

‎packages/plugin-ext/src/main/browser/notebooks/notebook-kernels-main.ts

+46-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import { UriComponents } from '@theia/core/lib/common/uri';
2323
import { LanguageService } from '@theia/core/lib/browser/language-service';
2424
import { CellExecuteUpdateDto, CellExecutionCompleteDto, MAIN_RPC_CONTEXT, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain } from '../../../common';
2525
import { RPCProtocol } from '../../../common/rpc-protocol';
26-
import { CellExecution, NotebookExecutionStateService, NotebookKernelChangeEvent, NotebookKernelService, NotebookService } from '@theia/notebook/lib/browser';
26+
import {
27+
CellExecution, NotebookEditorWidgetService, NotebookExecutionStateService,
28+
NotebookKernelChangeEvent, NotebookKernelService, NotebookService
29+
} from '@theia/notebook/lib/browser';
2730
import { combinedDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
2831
import { interfaces } from '@theia/core/shared/inversify';
2932
import { NotebookKernelSourceAction } from '@theia/notebook/lib/common';
@@ -54,7 +57,7 @@ abstract class NotebookKernel {
5457
return this.preloads.map(p => p.provides).flat();
5558
}
5659

57-
constructor(data: NotebookKernelDto, private languageService: LanguageService) {
60+
constructor(public readonly handle: number, data: NotebookKernelDto, private languageService: LanguageService) {
5861
this.id = data.id;
5962
this.viewType = data.notebookType;
6063
this.extensionId = data.extensionId;
@@ -65,6 +68,7 @@ abstract class NotebookKernel {
6568
this.detail = data.detail;
6669
this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? data.supportedLanguages : languageService.languages.map(lang => lang.id);
6770
this.implementsExecutionOrder = data.supportsExecutionOrder ?? false;
71+
this.localResourceRoot = URI.fromComponents(data.extensionLocation);
6872
this.preloads = data.preloads?.map(u => ({ uri: URI.fromComponents(u.uri), provides: u.provides })) ?? [];
6973
}
7074

@@ -125,6 +129,7 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain {
125129
private notebookService: NotebookService;
126130
private languageService: LanguageService;
127131
private notebookExecutionStateService: NotebookExecutionStateService;
132+
private notebookEditorWidgetService: NotebookEditorWidgetService;
128133

129134
private readonly executions = new Map<number, CellExecution>();
130135

@@ -138,10 +143,46 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain {
138143
this.notebookExecutionStateService = container.get(NotebookExecutionStateService);
139144
this.notebookService = container.get(NotebookService);
140145
this.languageService = container.get(LanguageService);
146+
this.notebookEditorWidgetService = container.get(NotebookEditorWidgetService);
147+
148+
this.notebookEditorWidgetService.onDidAddNotebookEditor(editor => {
149+
editor.onDidRecieveKernelMessage(async message => {
150+
const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(editor.model!);
151+
if (kernel) {
152+
this.proxy.$acceptKernelMessageFromRenderer(kernel.handle, editor.id, message);
153+
}
154+
});
155+
});
141156
}
142157

143-
$postMessage(handle: number, editorId: string | undefined, message: unknown): Promise<boolean> {
144-
throw new Error('Method not implemented.');
158+
async $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise<boolean> {
159+
const tuple = this.kernels.get(handle);
160+
if (!tuple) {
161+
throw new Error('kernel already disposed');
162+
}
163+
const [kernel] = tuple;
164+
let didSend = false;
165+
for (const editor of this.notebookEditorWidgetService.getNotebookEditors()) {
166+
if (!editor.model) {
167+
continue;
168+
}
169+
if (this.notebookKernelService.getMatchingKernel(editor.model).selected !== kernel) {
170+
// different kernel
171+
continue;
172+
}
173+
if (editorId === undefined) {
174+
// all editors
175+
editor.postKernelMessage(message);
176+
didSend = true;
177+
} else if (editor.id === editorId) {
178+
// selected editors
179+
editor.postKernelMessage(message);
180+
didSend = true;
181+
break;
182+
}
183+
}
184+
return didSend;
185+
145186
}
146187

147188
async $addKernel(handle: number, data: NotebookKernelDto): Promise<void> {
@@ -153,7 +194,7 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain {
153194
async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise<void> {
154195
await that.proxy.$cancelCells(handle, uri.toComponents(), handles);
155196
}
156-
}(data, this.languageService);
197+
}(handle, data, this.languageService);
157198

158199
const listener = this.notebookKernelService.onDidChangeSelectedKernel(e => {
159200
if (e.oldKernel === kernel.id) {

‎packages/plugin-ext/src/main/browser/notebooks/renderers/cell-output-webview.tsx

+70-19
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020

2121
import * as React from '@theia/core/shared/react';
2222
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
23-
import { NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry, NotebookEditorWidgetService, NotebookCellOutputsSplice } from '@theia/notebook/lib/browser';
2423
import { generateUuid } from '@theia/core/lib/common/uuid';
24+
import {
25+
NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry,
26+
NotebookEditorWidgetService, NotebookCellOutputsSplice, NOTEBOOK_EDITOR_ID_PREFIX, NotebookKernelService, NotebookEditorWidget
27+
} from '@theia/notebook/lib/browser';
2528
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
2629
import { WebviewWidget } from '../../webview/webview';
2730
import { Message, WidgetManager } from '@theia/core/lib/browser';
@@ -31,12 +34,15 @@ import { ChangePreferredMimetypeMessage, FromWebviewMessage, OutputChangedMessag
3134
import { CellUri } from '@theia/notebook/lib/common';
3235
import { Disposable, DisposableCollection, nls, QuickPickService } from '@theia/core';
3336
import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model';
37+
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
3438

3539
const CellModel = Symbol('CellModel');
40+
const Notebook = Symbol('NotebookModel');
3641

37-
export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel): interfaces.Container {
42+
export function createCellOutputWebviewContainer(ctx: interfaces.Container, cell: NotebookCellModel, notebook: NotebookModel): interfaces.Container {
3843
const child = ctx.createChild();
3944
child.bind(CellModel).toConstantValue(cell);
45+
child.bind(Notebook).toConstantValue(notebook);
4046
child.bind(CellOutputWebviewImpl).toSelf().inSingletonScope();
4147
return child;
4248
}
@@ -50,6 +56,9 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
5056
@inject(CellModel)
5157
protected readonly cell: NotebookCellModel;
5258

59+
@inject(Notebook)
60+
protected readonly notebook: NotebookModel;
61+
5362
@inject(WidgetManager)
5463
protected readonly widgetManager: WidgetManager;
5564

@@ -62,11 +71,16 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
6271
@inject(NotebookEditorWidgetService)
6372
protected readonly notebookEditorWidgetService: NotebookEditorWidgetService;
6473

74+
@inject(NotebookKernelService)
75+
protected readonly notebookKernelService: NotebookKernelService;
76+
6577
@inject(QuickPickService)
6678
protected readonly quickPickService: QuickPickService;
6779

6880
readonly id = generateUuid();
6981

82+
protected editor: NotebookEditorWidget | undefined;
83+
7084
protected readonly elementRef = React.createRef<HTMLDivElement>();
7185
protected outputPresentationListeners: DisposableCollection = new DisposableCollection();
7286

@@ -76,11 +90,30 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
7690

7791
@postConstruct()
7892
protected async init(): Promise<void> {
93+
this.editor = this.notebookEditorWidgetService.getNotebookEditor(NOTEBOOK_EDITOR_ID_PREFIX + CellUri.parse(this.cell.uri)?.notebook);
94+
7995
this.toDispose.push(this.cell.onDidChangeOutputs(outputChange => this.updateOutput(outputChange)));
8096
this.toDispose.push(this.cell.onDidChangeOutputItems(output => {
81-
this.updateOutput({start: this.cell.outputs.findIndex(o => o.outputId === output.outputId), deleteCount: 1, newOutputs: [output]});
97+
this.updateOutput({ start: this.cell.outputs.findIndex(o => o.outputId === output.outputId), deleteCount: 1, newOutputs: [output] });
8298
}));
8399

100+
if (this.editor) {
101+
this.toDispose.push(this.editor.onDidPostKernelMessage(message => {
102+
this.webviewWidget.sendMessage({
103+
type: 'customKernelMessage',
104+
message
105+
});
106+
}));
107+
108+
this.toDispose.push(this.editor.onPostRendererMessage(messageObj => {
109+
this.webviewWidget.sendMessage({
110+
type: 'customRendererMessage',
111+
...messageObj
112+
});
113+
}));
114+
115+
}
116+
84117
this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id });
85118
this.webviewWidget.setContentOptions({ allowScripts: true });
86119
this.webviewWidget.setHTML(await this.createWebviewContent());
@@ -134,8 +167,8 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
134167

135168
private async requestOutputPresentationUpdate(output: NotebookCellOutputModel): Promise<void> {
136169
const selectedMime = await this.quickPickService.show(
137-
output.outputs.map(item => ({label: item.mime})),
138-
{description: nls.localizeByDefault('Select mimetype to render for current output' )});
170+
output.outputs.map(item => ({ label: item.mime })),
171+
{ description: nls.localizeByDefault('Select mimetype to render for current output') });
139172
if (selectedMime) {
140173
this.webviewWidget.sendMessage({
141174
type: 'changePreferredMimetype',
@@ -146,35 +179,52 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
146179
}
147180

148181
private handleWebviewMessage(message: FromWebviewMessage): void {
182+
if (!this.editor) {
183+
throw new Error('No editor found for cell output webview');
184+
}
185+
149186
switch (message.type) {
150187
case 'initialized':
151-
this.updateOutput({newOutputs: this.cell.outputs, start: 0, deleteCount: 0});
188+
this.updateOutput({ newOutputs: this.cell.outputs, start: 0, deleteCount: 0 });
152189
break;
153190
case 'customRendererMessage':
154-
this.messagingService.getScoped('').postMessage(message.rendererId, message.message);
191+
this.messagingService.getScoped(this.editor.id).postMessage(message.rendererId, message.message);
155192
break;
156193
case 'didRenderOutput':
157194
this.webviewWidget.setIframeHeight(message.contentHeight + 5);
158195
break;
159196
case 'did-scroll-wheel':
160-
this.notebookEditorWidgetService.getNotebookEditor(`notebook:${CellUri.parse(this.cell.uri)?.notebook}`)?.node.scrollBy(message.deltaX, message.deltaY);
197+
this.editor.node.scrollBy(message.deltaX, message.deltaY);
198+
break;
199+
case 'customKernelMessage':
200+
this.editor.recieveKernelMessage(message.message);
161201
break;
162202
}
163203
}
164204

205+
getPreloads(): string[] {
206+
const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(this.notebook);
207+
const kernelPreloads = kernel?.preloadUris.map(uri => uri.toString()) ?? [];
208+
209+
const staticPreloads = this.notebookRendererRegistry.staticNotebookPreloads
210+
.filter(preload => preload.type === this.notebook.viewType)
211+
.map(preload => preload.entrypoint);
212+
return kernelPreloads.concat(staticPreloads);
213+
}
214+
165215
private async createWebviewContent(): Promise<string> {
166216
const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust();
167217
const preloads = this.preloadsScriptString(isWorkspaceTrusted);
168218
const content = `
169-
<html>
170-
<head>
171-
<meta charset="UTF-8">
172-
</head>
173-
<body>
174-
<script type="module">${preloads}</script>
175-
</body>
176-
</html>
177-
`;
219+
<html>
220+
<head>
221+
<meta charset="UTF-8">
222+
</head>
223+
<body>
224+
<script type="module">${preloads}</script>
225+
</body>
226+
</html>
227+
`;
178228
return content;
179229
}
180230

@@ -186,13 +236,14 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
186236
lineLimit: 30,
187237
outputScrolling: false,
188238
outputWordWrap: false,
189-
}
239+
},
240+
staticPreloadsData: this.getPreloads()
190241
};
191242
// TS will try compiling `import()` in webviewPreloads, so use a helper function instead
192243
// of using `import(...)` directly
193244
return `
194245
const __import = (x) => import(x);
195-
(${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`;
246+
(${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`;
196247
}
197248

198249
dispose(): void {

‎packages/plugin-ext/src/main/browser/notebooks/renderers/output-webview-internal.ts

+93-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ export interface PreloadContext {
5858
readonly isWorkspaceTrusted: boolean;
5959
readonly rendererData: readonly webviewCommunication.RendererMetadata[];
6060
readonly renderOptions: RenderOptions;
61+
readonly staticPreloadsData: readonly string[];
62+
}
63+
64+
interface KernelPreloadContext {
65+
readonly onDidReceiveKernelMessage: Event<unknown>;
66+
postKernelMessage(data: unknown): void;
67+
}
68+
69+
interface KernelPreloadModule {
70+
activate(ctx: KernelPreloadContext): Promise<void> | void;
6171
}
6272

6373
export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
@@ -98,6 +108,36 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
98108

99109
const settingChange: EmitterLike<RenderOptions> = createEmitter<RenderOptions>();
100110

111+
const onDidReceiveKernelMessage = createEmitter<unknown>();
112+
113+
function createKernelContext(): KernelPreloadContext {
114+
return Object.freeze({
115+
onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
116+
postKernelMessage: (data: unknown) => {
117+
theia.postMessage({ type: 'customKernelMessage', message: data });
118+
}
119+
});
120+
}
121+
122+
async function runKernelPreload(url: string): Promise<void> {
123+
try {
124+
return activateModuleKernelPreload(url);
125+
} catch (e) {
126+
console.error(e);
127+
throw e;
128+
}
129+
}
130+
131+
async function activateModuleKernelPreload(url: string): Promise<void> {
132+
const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, '');
133+
const module: KernelPreloadModule = (await __import(`${baseUri}/${url}`)) as KernelPreloadModule;
134+
if (!module.activate) {
135+
console.error(`Notebook preload '${url}' was expected to be a module but it does not export an 'activate' function`);
136+
return;
137+
}
138+
return module.activate(createKernelContext());
139+
}
140+
101141
class Output {
102142
readonly outputId: string;
103143
renderedItem?: rendererApi.OutputItem;
@@ -160,6 +200,10 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
160200
if (this.rendererApi) {
161201
return this.rendererApi;
162202
}
203+
204+
// Preloads need to be loaded before loading renderers.
205+
await kernelPreloads.waitForAllCurrent();
206+
163207
const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, '');
164208
const rendererModule = await __import(`${baseUri}/${this.data.entrypoint.uri}`) as { activate: rendererApi.ActivationFunction };
165209
this.rendererApi = await rendererModule.activate(this.createRendererContext());
@@ -196,7 +240,9 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
196240

197241
if (this.data.requiresMessaging) {
198242
context.onDidReceiveMessage = this.onMessageEvent.event;
199-
context.postMessage = message => theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message });
243+
context.postMessage = message => {
244+
theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message });
245+
};
200246
}
201247

202248
return Object.freeze(context);
@@ -385,6 +431,42 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
385431
}
386432
}();
387433

434+
const kernelPreloads = new class {
435+
private readonly preloads = new Map<string /* uri */, Promise<unknown>>();
436+
437+
/**
438+
* Returns a promise that resolves when the given preload is activated.
439+
*/
440+
public waitFor(uri: string): Promise<unknown> {
441+
return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`));
442+
}
443+
444+
/**
445+
* Loads a preload.
446+
* @param uri URI to load from
447+
* @param originalUri URI to show in an error message if the preload is invalid.
448+
*/
449+
public load(uri: string): Promise<unknown> {
450+
const promise = Promise.all([
451+
runKernelPreload(uri),
452+
this.waitForAllCurrent(),
453+
]);
454+
455+
this.preloads.set(uri, promise);
456+
return promise;
457+
}
458+
459+
/**
460+
* Returns a promise that waits for all currently-registered preloads to
461+
* activate before resolving.
462+
*/
463+
public waitForAllCurrent(): Promise<unknown[]> {
464+
return Promise.all([...this.preloads.values()].map(p => p.catch(err => err)));
465+
}
466+
};
467+
468+
await Promise.all(ctx.staticPreloadsData.map(preload => kernelPreloads.load(preload)));
469+
388470
function clearOutput(output: Output): void {
389471
output.clear();
390472
output.element.remove();
@@ -460,7 +542,6 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
460542

461543
window.addEventListener('message', async rawEvent => {
462544
const event = rawEvent as ({ data: webviewCommunication.ToWebviewMessage });
463-
464545
switch (event.data.type) {
465546
case 'updateRenderers':
466547
renderers.updateRendererData(event.data.rendererData);
@@ -478,6 +559,16 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
478559
clearOutput(outputs.splice(index, 1)[0]);
479560
renderers.render(outputs[index], event.data.mimeType, undefined, new AbortController().signal);
480561
break;
562+
case 'customKernelMessage':
563+
onDidReceiveKernelMessage.fire(event.data.message);
564+
break;
565+
case 'preload': {
566+
const resources = event.data.resources;
567+
for (const uri of resources) {
568+
kernelPreloads.load(uri);
569+
}
570+
break;
571+
}
481572
}
482573
});
483574
window.addEventListener('wheel', handleWheel);

‎packages/plugin-ext/src/main/browser/notebooks/renderers/webview-communication.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,17 @@ export interface ChangePreferredMimetypeMessage {
4949
readonly mimeType: string;
5050
}
5151

52-
export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage;
52+
export interface KernelMessage {
53+
readonly type: 'customKernelMessage';
54+
readonly message: unknown;
55+
}
56+
57+
export interface PreloadMessage {
58+
readonly type: 'preload';
59+
readonly resources: string[];
60+
}
61+
62+
export type ToWebviewMessage = UpdateRenderersMessage | OutputChangedMessage | ChangePreferredMimetypeMessage | CustomRendererMessage | KernelMessage | PreloadMessage;
5363

5464
export interface WebviewInitialized {
5565
readonly type: 'initialized';
@@ -66,7 +76,7 @@ export interface WheelMessage {
6676
readonly deltaX: number;
6777
}
6878

69-
export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage;
79+
export type FromWebviewMessage = WebviewInitialized | OnDidRenderOutput | WheelMessage | CustomRendererMessage | KernelMessage;
7080

7181
export interface Output {
7282
id: string

‎packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts

+8
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,14 @@ export class PluginContributionHandler {
447447
}
448448
}
449449

450+
if (contributions.notebookPreload) {
451+
for (const preload of contributions.notebookPreload) {
452+
pushContribution(`notebookPreloads.${preload.type}:${preload.entrypoint}`,
453+
() => this.notebookRendererRegistry.registerStaticNotebookPreload(preload.type, preload.entrypoint, PluginPackage.toPluginUrl(plugin.metadata.model, ''))
454+
);
455+
}
456+
}
457+
450458
return toDispose;
451459
}
452460

‎packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar
8888
import { CellOutputWebviewFactory } from '@theia/notebook/lib/browser';
8989
import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './notebooks/renderers/cell-output-webview';
9090
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
91+
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
9192

9293
export default new ContainerModule((bind, unbind, isBound, rebind) => {
9394

@@ -262,7 +263,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
262263
return provider.createProxy<LanguagePackService>(languagePackServicePath);
263264
}).inSingletonScope();
264265

265-
bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel) =>
266-
createCellOutputWebviewContainer(ctx.container, cell).getAsync(CellOutputWebviewImpl)
266+
bind(CellOutputWebviewFactory).toFactory(ctx => async (cell: NotebookCellModel, notebook: NotebookModel) =>
267+
createCellOutputWebviewContainer(ctx.container, cell, notebook).getAsync(CellOutputWebviewImpl)
267268
);
268269
});

‎packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts

+25-25
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@
1818
* Licensed under the MIT License. See License.txt in the project root for license information.
1919
*--------------------------------------------------------------------------------------------*/
2020

21-
import { CancellationToken } from '@theia/plugin';
2221
import {
23-
CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain, NotebookKernelSourceActionDto, NotebookOutputDto, PLUGIN_RPC_CONTEXT
22+
CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain,
23+
NotebookKernelSourceActionDto, NotebookOutputDto, PluginModel, PluginPackage, PLUGIN_RPC_CONTEXT
2424
} from '../../common';
2525
import { RPCProtocol } from '../../common/rpc-protocol';
2626
import { UriComponents } from '../../common/uri-components';
27-
import * as theia from '@theia/plugin';
28-
import { CancellationTokenSource, Disposable, DisposableCollection, Emitter } from '@theia/core';
27+
import { CancellationTokenSource, Disposable, DisposableCollection, Emitter, Path } from '@theia/core';
2928
import { Cell } from './notebook-document';
3029
import { NotebooksExtImpl } from './notebooks';
3130
import { NotebookCellOutputConverter, NotebookCellOutputItem, NotebookKernelSourceAction } from '../type-converters';
3231
import { timeout, Deferred } from '@theia/core/lib/common/promise-util';
3332
import { CellExecutionUpdateType, NotebookCellExecutionState } from '@theia/notebook/lib/common';
3433
import { CommandRegistryImpl } from '../command-registry';
3534
import { NotebookCellOutput, NotebookRendererScript, URI } from '../types-impl';
35+
import { toUriComponents } from '../../main/browser/hierarchy/hierarchy-types-converters';
36+
import type * as theia from '@theia/plugin';
3637

3738
interface KernelData {
3839
extensionId: string;
@@ -62,28 +63,28 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
6263
constructor(
6364
rpc: RPCProtocol,
6465
private readonly notebooks: NotebooksExtImpl,
65-
private readonly commands: CommandRegistryImpl
66+
private readonly commands: CommandRegistryImpl,
6667
) {
6768
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN);
6869
}
6970

7071
private currentHandle = 0;
7172

72-
createNotebookController(extensionId: string, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[],
73+
createNotebookController(extension: PluginModel, id: string, viewType: string, label: string, handler?: (cells: theia.NotebookCell[],
7374
notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable<void>, rendererScripts?: NotebookRendererScript[]): theia.NotebookController {
7475

7576
for (const kernelData of this.kernelData.values()) {
76-
if (kernelData.controller.id === id && extensionId === kernelData.extensionId) {
77+
if (kernelData.controller.id === id && extension.id === kernelData.extensionId) {
7778
throw new Error(`notebook controller with id '${id}' ALREADY exist`);
7879
}
7980
}
8081

8182
const handle = this.currentHandle++;
8283
const that = this;
8384

84-
console.debug(`NotebookController[${handle}], CREATED by ${extensionId}, ${id}`);
85+
console.debug(`NotebookController[${handle}], CREATED by ${extension.id}, ${id}`);
8586

86-
const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extensionId}'`);
87+
const defaultExecuteHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.id}'`);
8788

8889
let isDisposed = false;
8990
const commandDisposables = new DisposableCollection();
@@ -92,10 +93,12 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
9293
const onDidReceiveMessage = new Emitter<{ editor: theia.NotebookEditor; message: unknown }>();
9394

9495
const data: NotebookKernelDto = {
95-
id: createKernelId(extensionId, id),
96+
id: createKernelId(extension.id, id),
9697
notebookType: viewType,
97-
extensionId: extensionId,
98-
label: label || extensionId,
98+
extensionId: extension.id,
99+
extensionLocation: toUriComponents(extension.packageUri),
100+
label: label || extension.id,
101+
preloads: rendererScripts?.map(preload => ({ uri: toUriComponents(preload.uri.toString()), provides: preload.provides })) ?? []
99102
};
100103

101104
//
@@ -131,12 +134,11 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
131134
get id(): string { return id; },
132135
get notebookType(): string { return data.notebookType; },
133136
onDidChangeSelectedNotebooks: onDidChangeSelection.event,
134-
onDidReceiveMessage: onDidReceiveMessage.event,
135137
get label(): string {
136138
return data.label;
137139
},
138140
set label(value) {
139-
data.label = value ?? extensionId;
141+
data.label = value ?? extension.id;
140142
update();
141143
},
142144
get detail(): string {
@@ -168,11 +170,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
168170
update();
169171
},
170172
get rendererScripts(): NotebookRendererScript[] {
171-
return data.rendererScripts ?? [];
172-
},
173-
set rendererScripts(value) {
174-
data.rendererScripts = value;
175-
update();
173+
return data.preloads?.map(preload => (new NotebookRendererScript(URI.from(preload.uri), preload.provides))) ?? [];
176174
},
177175
get executeHandler(): (cells: theia.NotebookCell[], notebook: theia.NotebookDocument, controller: theia.NotebookController) => void | Thenable<void> {
178176
return executeHandler;
@@ -197,7 +195,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
197195
Array.from(associatedNotebooks.keys()).map(u => u.toString()));
198196
throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`);
199197
}
200-
return that.createNotebookCellExecution(cell, createKernelId(extensionId, this.id));
198+
return that.createNotebookCellExecution(cell, createKernelId(extension.id, this.id));
201199
},
202200
dispose: () => {
203201
if (!isDisposed) {
@@ -213,16 +211,18 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
213211
updateNotebookAffinity(notebook, priority): void {
214212
that.proxy.$updateNotebookPriority(handle, notebook.uri, priority);
215213
},
214+
onDidReceiveMessage: onDidReceiveMessage.event,
216215
async postMessage(message: unknown, editor?: theia.NotebookEditor): Promise<boolean> {
217-
return Promise.resolve(true); // TODO needs implementation
216+
return that.proxy.$postMessage(handle, 'notebook:' + editor?.notebook.uri.toString(), message);
218217
},
219218
asWebviewUri(localResource: theia.Uri): theia.Uri {
220-
throw new Error('Method not implemented.');
219+
const basePath = PluginPackage.toPluginUrl(extension, '');
220+
return URI.from({ path: new Path(basePath).join(localResource.path).toString(), scheme: 'https' });
221221
}
222222
};
223223

224224
this.kernelData.set(handle, {
225-
extensionId: extensionId,
225+
extensionId: extension.id,
226226
controller,
227227
onDidReceiveMessage,
228228
onDidChangeSelection,
@@ -376,7 +376,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
376376
// Proposed Api though seems needed by jupyter for telemetry
377377
}
378378

379-
async $provideKernelSourceActions(handle: number, token: CancellationToken): Promise<NotebookKernelSourceActionDto[]> {
379+
async $provideKernelSourceActions(handle: number, token: theia.CancellationToken): Promise<NotebookKernelSourceActionDto[]> {
380380
const provider = this.kernelSourceActionProviders.get(handle);
381381
if (provider) {
382382
const disposables = new DisposableCollection();
@@ -496,7 +496,7 @@ class NotebookCellExecutionTask implements Disposable {
496496
asApiObject(): theia.NotebookCellExecution {
497497
const that = this;
498498
const result: theia.NotebookCellExecution = {
499-
get token(): CancellationToken { return that.tokenSource.token; },
499+
get token(): theia.CancellationToken { return that.tokenSource.token; },
500500
get cell(): theia.NotebookCell { return that.cell.apiCell; },
501501
get executionOrder(): number | undefined { return that.executionOrder; },
502502
set executionOrder(v: number | undefined) {

‎packages/plugin-ext/src/plugin/notebook/notebook-renderers.ts

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export class NotebookRenderersExtImpl implements NotebookRenderersExt {
4343
const messaging: theia.NotebookRendererMessaging = {
4444
onDidReceiveMessage: (listener, thisArg, disposables) => this.getOrCreateEmitterFor(rendererId).event(listener, thisArg, disposables),
4545
postMessage: (message, editorOrAlias) => {
46-
4746
const extHostEditor = editorOrAlias && NotebookEditor.apiEditorsToExtHost.get(editorOrAlias);
4847
return this.proxy.$postMessage(extHostEditor?.id, rendererId, message);
4948
},

‎packages/plugin-ext/src/plugin/plugin-context.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,7 @@ export function createAPIFactory(
11831183
controller: theia.NotebookController) => void | Thenable<void>,
11841184
rendererScripts?: NotebookRendererScript[]
11851185
) {
1186-
return notebookKernels.createNotebookController(plugin.model.id, id, notebookType, label, handler, rendererScripts);
1186+
return notebookKernels.createNotebookController(plugin.model, id, notebookType, label, handler, rendererScripts);
11871187
},
11881188
createRendererMessaging(rendererId) {
11891189
return notebookRenderers.createRendererMessaging(rendererId);

‎packages/plugin-ext/src/plugin/webviews.ts

+4
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ export class WebviewsExtImpl implements WebviewsExt {
203203
public getWebview(handle: string): WebviewImpl | undefined {
204204
return this.webviews.get(handle);
205205
}
206+
207+
public getResourceRoot(): string | undefined {
208+
return this.initData?.webviewResourceRoot;
209+
}
206210
}
207211

208212
export class WebviewImpl implements theia.Webview {

0 commit comments

Comments
 (0)
Please sign in to comment.