Skip to content
Closed
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
92 changes: 90 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { ILabShell, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/a
import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
import { Dialog, showDialog, ReactWidget, Notification } from '@jupyterlab/apputils';
import { PageConfig } from '@jupyterlab/coreutils';
import { INotebookContent } from '@jupyterlab/nbformat';
import type {
INotebookContent,
INotebookMetadata,
IKernelspecMetadata,
ILanguageInfoMetadata
} from '@jupyterlab/nbformat';

import { customSidebar } from './sidebar';
import { SharingService } from './sharing-service';
Expand All @@ -26,7 +31,12 @@ import {
ViewOnlyNotebookPanel
} from './view-only';

import { KERNEL_DISPLAY_NAMES, switchKernel } from './kernels';
import {
KERNEL_DISPLAY_NAMES,
KERNEL_URL_TO_NAME,
KERNEL_NAME_TO_URL,
switchKernel
} from './kernels';

/**
* Generate a shareable URL for the currently active notebook.
Expand Down Expand Up @@ -75,6 +85,78 @@ async function showShareDialog(sharingService: SharingService, notebookContent:
}
}

/**
* Ensure that the notebook metadata contains the necessary language and kernelspec information.
* This function will patch the metadata if it is missing or incomplete. This happens only for
* the initial load of the notebook, so that the metadata is always present even if the kernel
* has not started yet. Subsequent changes to the notebook will change the metadata we set here.
* @param panel - The notebook panel that we ensure the metadata for.
* @param content - The notebook content to patch, if necessary.
* @returns - a boolean indicating whether the metadata was patched.
*/
function ensureLanguageMetadata(
panel: NotebookPanel | ViewOnlyNotebookPanel,
content: INotebookContent
): boolean {
const meta = (content.metadata ?? {}) as INotebookMetadata;
const before = JSON.stringify({ kernelspec: meta.kernelspec, languageInfo: meta.language_info });

// We are trying to extract the kernel name from the URL, session, or metadata; because it is
// not guaranteed that the URL param might not be sanitised by the time we access it.
const urlKernelParam = new URL(window.location.href).searchParams.get('kernel') ?? undefined;
const kernelFromUrl = urlKernelParam ? KERNEL_URL_TO_NAME[urlKernelParam] : undefined;
const kernelFromSession =
panel instanceof NotebookPanel ? panel.sessionContext.session?.kernel?.name : undefined;

const kernelName: 'xpython' | 'xr' =
(meta.kernelspec?.name as 'xpython' | 'xr' | undefined) ??
(kernelFromSession as 'xpython' | 'xr' | undefined) ??
(kernelFromUrl as 'xpython' | 'xr' | undefined) ??
'xpython';

const language: 'python' | 'r' = KERNEL_NAME_TO_URL[kernelName] as 'python' | 'r';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, typescript literal types can be cumbersome. If we end up merging this at some point, can we define KernelName and KernelLanguage literal types? e.g.

type KernelName = 'xpython' | 'xr';
type KernelLanguage = 'python' | 'r';

Then you can change KERNEL_NAME_TO_URL to use that type, and the line above becomes

const language: KernelLanguage = KERNEL_NAME_TO_URL[kernelName];

(with no need to cast).

const display: string = KERNEL_DISPLAY_NAMES[kernelName] ?? (language === 'r' ? 'R' : 'Python');

const file_extension: string =
meta.language_info?.file_extension && meta.language_info.file_extension.trim() !== ''
? meta.language_info.file_extension
: language === 'r'
? '.r'
: '.py';

// We use a placeholder until kernel.info arrives and we override it later
const version: string =
typeof meta.language_info?.version === 'string' && meta.language_info.version.trim() !== ''
? meta.language_info.version
: '0';

// Once grabbed, we patch the metadata with the kernelspec and language_info
// If the metadata already exists, we merge it with the existing metadata.
const kernelspec: IKernelspecMetadata = {
...(meta.kernelspec ?? {}),
name: kernelName,
display_name: display,
language
};
const language_info: ILanguageInfoMetadata = {
...(meta.language_info ?? {}),
name: language,
file_extension,
version
};

const patched: INotebookMetadata = {
...(content.metadata ?? {}),
kernelspec,
language_info
};

content.metadata = patched;

const after = JSON.stringify({ ks: patched.kernelspec, li: patched.language_info });
return before !== after;
}

/**
* Notebook share/save handler. This function handles both sharing a new notebook and
* updating an existing shared notebook.
Expand Down Expand Up @@ -103,6 +185,12 @@ async function handleNotebookSharing(
}
return;
}
// Ensure that the language metadata exists, even if the kernel has not started yet.
const patched = ensureLanguageMetadata(notebookPanel, notebookContent);
if (patched) {
notebookPanel.context.model.fromJSON(notebookContent);
}

if (sharedId) {
console.log('Updating notebook:', sharedId);
await sharingService.update(sharedId, notebookContent);
Expand Down
Loading