Skip to content

Commit

Permalink
custom widgets support for notebook outputs (#13517)
Browse files Browse the repository at this point in the history
* working different ipywidget like ipydatagrid

Signed-off-by: Jonah Iden <[email protected]>

* better focus management for notebook editors

Signed-off-by: Jonah Iden <[email protected]>

* fixed notebook editor fucos focus

Signed-off-by: Jonah Iden <[email protected]>

* update extensions with associated notebooks after workspace trusted

Signed-off-by: Jonah Iden <[email protected]>

---------

Signed-off-by: Jonah Iden <[email protected]>
  • Loading branch information
jonah-iden authored Mar 25, 2024
1 parent 4b413bd commit 29aa635
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 39 deletions.
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

0 comments on commit 29aa635

Please sign in to comment.