Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom widgets support for notebook outputs #13517

Merged
merged 4 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/filesystem/src/browser/file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ export interface FileSystemProviderCapabilitiesChangeEvent {
}

export interface FileSystemProviderReadOnlyMessageChangeEvent {
/** The affected file system provider for which this event was fired. */
provider: FileSystemProvider;
/** The uri for which the provider is registered */
scheme: string;
/** The new read only message */
message: MarkdownString | undefined;
/** The affected file system provider for which this event was fired. */
provider: FileSystemProvider;
/** The uri for which the provider is registered */
scheme: string;
/** The new read only message */
message: MarkdownString | undefined;
}

/**
Expand Down Expand Up @@ -378,7 +378,7 @@ export class FileService {
providerDisposables.push(provider.onFileWatchError(() => this.handleFileWatchError()));
providerDisposables.push(provider.onDidChangeCapabilities(() => this.onDidChangeFileSystemProviderCapabilitiesEmitter.fire({ provider, scheme })));
if (ReadOnlyMessageFileSystemProvider.is(provider)) {
providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire({ provider, scheme, message})));
providerDisposables.push(provider.onDidChangeReadOnlyMessage(message => this.onDidChangeFileSystemProviderReadOnlyMessageEmitter.fire({ provider, scheme, message })));
}

return Disposable.create(() => {
Expand Down
10 changes: 10 additions & 0 deletions packages/notebook/src/browser/notebook-editor-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,14 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
this.viewportService.dispose();
super.dispose();
}

protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.notebookEditorService.notebookEditorFocusChanged(this, true);
}

protected override onAfterHide(msg: Message): void {
super.onAfterHide(msg);
this.notebookEditorService.notebookEditorFocusChanged(this, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,7 @@ export class NotebookEditorWidgetService {
@postConstruct()
protected init(): void {
this.applicationShell.onDidChangeActiveWidget(event => {
if (event.newValue instanceof NotebookEditorWidget) {
if (event.newValue !== this.focusedEditor) {
this.focusedEditor = event.newValue;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true);
this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor);
}
} else if (event.newValue) {
// Only unfocus editor if a new widget has been focused
this.focusedEditor = undefined;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true);
this.onDidChangeFocusedEditorEmitter.fire(undefined);
}
this.notebookEditorFocusChanged(event.newValue as NotebookEditorWidget, event.newValue instanceof NotebookEditorWidget);
});
}

Expand Down Expand Up @@ -92,4 +81,18 @@ export class NotebookEditorWidgetService {
return Array.from(this.notebookEditors.values());
}

notebookEditorFocusChanged(editor: NotebookEditorWidget, focus: boolean): void {
if (focus) {
if (editor !== this.focusedEditor) {
this.focusedEditor = editor;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, true);
this.onDidChangeFocusedEditorEmitter.fire(this.focusedEditor);
}
} else if (this.focusedEditor) {
this.focusedEditor = undefined;
this.contextKeyService.setContext(NOTEBOOK_EDITOR_FOCUSED, false);
this.onDidChangeFocusedEditorEmitter.fire(undefined);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
CellExecution, NotebookEditorWidgetService, NotebookExecutionStateService,
NotebookKernelChangeEvent, NotebookKernelService, NotebookService
} from '@theia/notebook/lib/browser';
import { combinedDisposable } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookKernelSourceAction } from '@theia/notebook/lib/common';
import { NotebookDto } from './notebook-dto';
Expand Down Expand Up @@ -153,6 +152,20 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain {
}
});
});
this.notebookKernelService.onDidChangeSelectedKernel(e => {
if (e.newKernel) {
const newKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.newKernel)?.[0];
if (newKernelHandle !== undefined) {
this.proxy.$acceptNotebookAssociation(newKernelHandle, e.notebook.toComponents(), true);
}
} else {
const oldKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.oldKernel)?.[0];
if (oldKernelHandle !== undefined) {
this.proxy.$acceptNotebookAssociation(oldKernelHandle, e.notebook.toComponents(), false);
}

}
});
}

async $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise<boolean> {
Expand Down Expand Up @@ -196,16 +209,8 @@ export class NotebookKernelsMainImpl implements NotebookKernelsMain {
}
}(handle, data, this.languageService);

const listener = this.notebookKernelService.onDidChangeSelectedKernel(e => {
if (e.oldKernel === kernel.id) {
this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), false);
} else if (e.newKernel === kernel.id) {
this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), true);
}
});

const registration = this.notebookKernelService.registerKernel(kernel);
this.kernels.set(handle, [kernel, combinedDisposable(listener, registration)]);
this.kernels.set(handle, [kernel, registration]);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {

if (this.editor) {
this.toDispose.push(this.editor.onDidPostKernelMessage(message => {
// console.log('from extension customKernelMessage ', JSON.stringify(message));
this.webviewWidget.sendMessage({
type: 'customKernelMessage',
message
});
}));

this.toDispose.push(this.editor.onPostRendererMessage(messageObj => {
// console.log('from extension customRendererMessage ', JSON.stringify(messageObj));
this.webviewWidget.sendMessage({
type: 'customRendererMessage',
...messageObj
Expand Down Expand Up @@ -188,6 +190,7 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
this.updateOutput({ newOutputs: this.cell.outputs, start: 0, deleteCount: 0 });
break;
case 'customRendererMessage':
// console.log('from webview customRendererMessage ', message.rendererId, '', JSON.stringify(message.message));
this.messagingService.getScoped(this.editor.id).postMessage(message.rendererId, message.message);
break;
case 'didRenderOutput':
Expand All @@ -197,6 +200,7 @@ export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
this.editor.node.getElementsByClassName('theia-notebook-viewport')[0].children[0].scrollBy(message.deltaX, message.deltaY);
break;
case 'customKernelMessage':
// console.log('from webview customKernelMessage ', JSON.stringify(message.message));
this.editor.recieveKernelMessage(message.message);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,5 +573,13 @@ export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
});
window.addEventListener('wheel', handleWheel);

(document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).originalAppendChild = document.head.appendChild;
(document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).appendChild = function appendChild<T extends Node>(node: T): T {
if (node instanceof HTMLScriptElement && node.src.includes('webviewuuid')) {
node.src = node.src.replace('webviewuuid', location.hostname.split('.')[0]);
}
return this.originalAppendChild(node);
};

theia.postMessage(<webviewCommunication.WebviewInitialized>{ type: 'initialized' });
}
30 changes: 22 additions & 8 deletions packages/plugin-ext/src/plugin/notebook/notebook-kernels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@

import {
CellExecuteUpdateDto, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain,
NotebookKernelSourceActionDto, NotebookOutputDto, PluginModel, PluginPackage, PLUGIN_RPC_CONTEXT
NotebookKernelSourceActionDto, NotebookOutputDto, PluginModel, PLUGIN_RPC_CONTEXT
} from '../../common';
import { RPCProtocol } from '../../common/rpc-protocol';
import { UriComponents } from '../../common/uri-components';
import { CancellationTokenSource, Disposable, DisposableCollection, Emitter, Path } from '@theia/core';
import { CancellationTokenSource, Disposable, DisposableCollection, Emitter } from '@theia/core';
import { Cell } from './notebook-document';
import { NotebooksExtImpl } from './notebooks';
import { NotebookCellOutputConverter, NotebookCellOutputItem, NotebookKernelSourceAction } from '../type-converters';
Expand All @@ -34,6 +34,8 @@ import { CommandRegistryImpl } from '../command-registry';
import { NotebookCellOutput, NotebookRendererScript, URI } from '../types-impl';
import { toUriComponents } from '../../main/browser/hierarchy/hierarchy-types-converters';
import type * as theia from '@theia/plugin';
import { WebviewsExtImpl } from '../webviews';
import { WorkspaceExtImpl } from '../workspace';

interface KernelData {
extensionId: string;
Expand Down Expand Up @@ -64,8 +66,21 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
rpc: RPCProtocol,
private readonly notebooks: NotebooksExtImpl,
private readonly commands: CommandRegistryImpl,
private readonly webviews: WebviewsExtImpl,
workspace: WorkspaceExtImpl
) {
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN);

// call onDidChangeSelection for all kernels after trust is granted to inform extensions they can set the kernel as assoiciated
// the jupyter extension for example does not set kernel association after trust is granted
workspace.onDidGrantWorkspaceTrust(() => {
this.kernelData.forEach(kernel => {
kernel.associatedNotebooks.forEach(async (_, uri) => {
const notebook = await this.notebooks.waitForNotebookDocument(URI.parse(uri));
kernel.onDidChangeSelection.fire({ selected: true, notebook: notebook.apiNotebook });
});
});
});
}

private currentHandle = 0;
Expand Down Expand Up @@ -216,8 +231,7 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
return that.proxy.$postMessage(handle, 'notebook:' + editor?.notebook.uri.toString(), message);
},
asWebviewUri(localResource: theia.Uri): theia.Uri {
const basePath = PluginPackage.toPluginUrl(extension, '');
return URI.from({ path: new Path(basePath).join(localResource.path).toString(), scheme: 'https' });
return that.webviews.toGeneralWebviewResource(extension, localResource);
}
};

Expand Down Expand Up @@ -294,20 +308,20 @@ export class NotebookKernelsExtImpl implements NotebookKernelsExt {
};
}

async $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): Promise<void> {
async $acceptNotebookAssociation(handle: number, uri: UriComponents, selected: boolean): Promise<void> {
const obj = this.kernelData.get(handle);
if (obj) {
// update data structure
const notebook = await this.notebooks.waitForNotebookDocument(URI.from(uri));
if (value) {
if (selected) {
obj.associatedNotebooks.set(notebook.uri.toString(), true);
} else {
obj.associatedNotebooks.delete(notebook.uri.toString());
}
console.debug(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), value);
console.debug(`NotebookController[${handle}] ASSOCIATE notebook`, notebook.uri.toString(), selected);
// send event
obj.onDidChangeSelection.fire({
selected: value,
selected: selected,
notebook: notebook.apiNotebook
});
}
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ export function createAPIFactory(
const notebooksExt = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT, new NotebooksExtImpl(rpc, commandRegistry, editorsAndDocumentsExt, documents));
const notebookEditors = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT, new NotebookEditorsExtImpl(notebooksExt));
const notebookRenderers = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT, new NotebookRenderersExtImpl(rpc, notebooksExt));
const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry));
const notebookKernels = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT, new NotebookKernelsExtImpl(rpc, notebooksExt, commandRegistry, webviewExt, workspaceExt));
const notebookDocuments = rpc.set(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT, new NotebookDocumentsExtImpl(notebooksExt));
const statusBarMessageRegistryExt = new StatusBarMessageRegistryExt(rpc);
const terminalExt = rpc.set(MAIN_RPC_CONTEXT.TERMINAL_EXT, new TerminalServiceExtImpl(rpc));
Expand Down Expand Up @@ -744,7 +744,7 @@ export function createAPIFactory(
registerTextDocumentContentProvider(scheme: string, provider: theia.TextDocumentContentProvider): theia.Disposable {
return workspaceExt.registerTextDocumentContentProvider(scheme, provider);
},
registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString}):
registerFileSystemProvider(scheme: string, provider: theia.FileSystemProvider, options?: { isCaseSensitive?: boolean, isReadonly?: boolean | MarkdownString }):
theia.Disposable {
return fileSystemExt.registerFileSystemProvider(scheme, provider, options);
},
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-ext/src/plugin/webviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type-
import { Disposable, WebviewPanelTargetArea, URI } from './types-impl';
import { WorkspaceExtImpl } from './workspace';
import { PluginIconPath } from './plugin-icon-path';
import { PluginModel, PluginPackage } from '../common';

@injectable()
export class WebviewsExtImpl implements WebviewsExt {
Expand Down Expand Up @@ -196,6 +197,14 @@ export class WebviewsExtImpl implements WebviewsExt {
return undefined;
}

toGeneralWebviewResource(extension: PluginModel, resource: theia.Uri): theia.Uri {
const extensionUri = URI.parse(extension.packageUri);
const relativeResourcePath = resource.path.replace(extensionUri.path, '');
const basePath = PluginPackage.toPluginUrl(extension, '') + relativeResourcePath;

return URI.parse(this.initData!.webviewResourceRoot.replace('{{uuid}}', 'webviewUUID')).with({ path: basePath });
}

public deleteWebview(handle: string): void {
this.webviews.delete(handle);
}
Expand Down
Loading