Path: blob/master/src/packages/jupyter/redux/store.ts
1447 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6The Redux Store For Jupyter Notebooks78This is used by everybody involved in using jupyter -- the project, the browser client, etc.9*/1011import { List, Map, OrderedMap, Set } from "immutable";12import { export_to_ipynb } from "@cocalc/jupyter/ipynb/export-to-ipynb";13import { KernelSpec } from "@cocalc/jupyter/ipynb/parse";14import {15Cell,16CellToolbarName,17KernelInfo,18NotebookMode,19} from "@cocalc/jupyter/types";20import {21Kernel,22Kernels,23get_kernel_selection,24} from "@cocalc/jupyter/util/misc";25import { Syntax } from "@cocalc/util/code-formatter";26import { startswith } from "@cocalc/util/misc";27import { Store } from "@cocalc/util/redux/Store";28import type { ImmutableUsageInfo } from "@cocalc/util/types/project-usage-info";29import { cloneDeep } from "lodash";3031// Used for copy/paste. We make a single global clipboard, so that32// copy/paste between different notebooks works.33let global_clipboard: any = undefined;3435export type show_kernel_selector_reasons = "bad kernel" | "user request";3637export function canonical_language(38kernel?: string | null,39kernel_info_lang?: string,40): string | undefined {41let lang;42// special case: sage is language "python", but the snippet dialog needs "sage"43if (startswith(kernel, "sage")) {44lang = "sage";45} else {46lang = kernel_info_lang;47}48return lang;49}5051export interface JupyterStoreState {52about: boolean;53backend_kernel_info: KernelInfo;54cell_list: List<string>; // list of id's of the cells, in order by pos.55cell_toolbar?: CellToolbarName;56cells: Map<string, Cell>; // map from string id to cell; the structure of a cell is complicated...57check_select_kernel_init: boolean;58closestKernel?: Kernel;59cm_options: any;60complete: any;61confirm_dialog: any;62connection_file?: string;63contents?: List<Map<string, any>>; // optional global contents info (about sections, problems, etc.)64default_kernel?: string;65directory: string;66edit_attachments?: string;67edit_cell_metadata: any;68error?: string;69fatal: string;70find_and_replace: any;71has_uncommitted_changes?: boolean;72has_unsaved_changes?: boolean;73introspect: any;74kernel_error?: string;75kernel_info?: any;76kernel_selection?: Map<string, string>;77kernel_usage?: ImmutableUsageInfo;78kernel?: string | ""; // "": means "no kernel"79kernels_by_language?: OrderedMap<string, List<string>>;80kernels_by_name?: OrderedMap<string, Map<string, string>>;81kernels?: Kernels;82keyboard_shortcuts: any;83max_output_length: number;84md_edit_ids: Set<string>;85metadata: any; // documented at https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata86mode: NotebookMode;87more_output: any;88name: string;89nbconvert_dialog: any;90nbconvert: any;91path: string;92project_id: string;93raw_ipynb: any;94read_only: boolean;95scroll: any;96sel_ids: any;97show_kernel_selector_reason?: show_kernel_selector_reasons;98show_kernel_selector: boolean;99start_time: any;100toolbar?: boolean;101widgetModelIdState: Map<string, string>; // model_id --> '' (=supported), 'loading' (definitely loading), '(widget module).(widget name)' (=if NOT supported), undefined (=not known yet)102// run progress = Percent (0-100) of runnable cells that have been run since the last103// kernel restart. (Thus markdown and empty cells are excluded.)104runProgress?: number;105}106107export const initial_jupyter_store_state: {108[K in keyof JupyterStoreState]?: JupyterStoreState[K];109} = {110check_select_kernel_init: false,111show_kernel_selector: false,112widgetModelIdState: Map(),113cell_list: List(),114cells: Map(),115};116117export class JupyterStore extends Store<JupyterStoreState> {118// manipulated in jupyter/project-actions.ts119_more_output: { [id: string]: any } = {};120121// immutable List122get_cell_list = (): List<string> => {123return this.get("cell_list") ?? List();124};125126// string[]127get_cell_ids_list(): string[] {128return this.get_cell_list().toJS();129}130131get_cell_type(id: string): "markdown" | "code" | "raw" {132// NOTE: default cell_type is "code", which is common, to save space.133// TODO: We use unsafe_getIn because maybe the cell type isn't spelled out yet, or our typescript isn't good enough.134const type = this.unsafe_getIn(["cells", id, "cell_type"], "code");135if (type != "markdown" && type != "code" && type != "raw") {136throw Error(`invalid cell type ${type} for cell ${id}`);137}138return type;139}140141get_cell_index(id: string): number {142const cell_list = this.get("cell_list");143if (cell_list == null) {144// truly fatal145throw Error("ordered list of cell id's not known");146}147const i = cell_list.indexOf(id);148if (i === -1) {149throw Error(`unknown cell id ${id}`);150}151return i;152}153154// Get the id of the cell that is delta positions from155// cell with given id (second input).156// Returns undefined if delta positions moves out of157// the notebook (so there is no such cell) or there158// is no cell with the given id; in particular,159// we do NOT wrap around.160get_cell_id(delta = 0, id: string): string | undefined {161let i: number;162try {163i = this.get_cell_index(id);164} catch (_) {165// no such cell. This can happen, e.g., https://github.com/sagemathinc/cocalc/issues/6686166return;167}168i += delta;169const cell_list = this.get("cell_list");170if (cell_list == null || i < 0 || i >= cell_list.size) {171return; // .get negative for List in immutable wraps around rather than undefined (like Python)172}173return cell_list.get(i);174}175176set_global_clipboard = (clipboard: any) => {177global_clipboard = clipboard;178};179180get_global_clipboard = () => {181return global_clipboard;182};183184get_kernel_info = (185kernel: string | null | undefined,186): KernelSpec | undefined => {187// slow/inefficient, but ok since this is rarely called188let info: any = undefined;189const kernels = this.get("kernels");190if (kernels === undefined) return;191if (kernels === null) {192return {193name: "No Kernel",194language: "",195display_name: "No Kernel",196};197}198kernels.forEach((x: any) => {199if (x.get("name") === kernel) {200info = x.toJS() as KernelSpec;201return false;202}203});204return info;205};206207// Export the Jupyer notebook to an ipynb object.208get_ipynb = (blob_store?: any) => {209if (this.get("cells") == null || this.get("cell_list") == null) {210// not sufficiently loaded yet.211return;212}213214const cell_list = this.get("cell_list");215const more_output: { [id: string]: any } = {};216for (const id of cell_list.toJS()) {217const x = this.get_more_output(id);218if (x != null) {219more_output[id] = x;220}221}222223// export_to_ipynb mutates its input... mostly not a problem, since224// we're toJS'ing most of it, but be careful with more_output.225return export_to_ipynb({226cells: this.get("cells").toJS(),227cell_list: cell_list.toJS(),228metadata: this.get("metadata")?.toJS(), // custom metadata229kernelspec: this.get_kernel_info(this.get("kernel")),230language_info: this.get_language_info(),231blob_store,232more_output: cloneDeep(more_output),233});234};235236get_language_info(): object | undefined {237for (const key of ["backend_kernel_info", "metadata"]) {238const language_info = this.unsafe_getIn([key, "language_info"]);239if (language_info != null) {240return language_info;241}242}243}244245get_cm_mode() {246let metadata_immutable = this.get("backend_kernel_info");247if (metadata_immutable == null) {248metadata_immutable = this.get("metadata");249}250let metadata: { language_info?: any; kernelspec?: any } | undefined;251if (metadata_immutable != null) {252metadata = metadata_immutable.toJS();253} else {254metadata = undefined;255}256let mode: any;257if (metadata != null) {258if (259metadata.language_info != null &&260metadata.language_info.codemirror_mode != null261) {262mode = metadata.language_info.codemirror_mode;263} else if (264metadata.language_info != null &&265metadata.language_info.name != null266) {267mode = metadata.language_info.name;268} else if (269metadata.kernelspec != null &&270metadata.kernelspec.language != null271) {272mode = metadata.kernelspec.language.toLowerCase();273}274}275if (mode == null) {276// As a fallback in case none of the metadata has been filled in yet by the backend,277// we can guess a mode from the kernel in many cases. Any mode is vastly better278// than nothing!279let kernel = this.get("kernel"); // may be better than nothing...; e.g., octave kernel has no mode.280if (kernel != null) {281kernel = kernel.toLowerCase();282// The kernel is just a string that names the kernel, so we use heuristics.283if (kernel.indexOf("python") != -1) {284if (kernel.indexOf("python3") != -1) {285mode = { name: "python", version: 3 };286} else {287mode = { name: "python", version: 2 };288}289} else if (kernel.indexOf("sage") != -1) {290mode = { name: "python", version: 3 };291} else if (kernel.indexOf("anaconda") != -1) {292mode = { name: "python", version: 3 };293} else if (kernel.indexOf("octave") != -1) {294mode = "octave";295} else if (kernel.indexOf("bash") != -1) {296mode = "shell";297} else if (kernel.indexOf("julia") != -1) {298mode = "text/x-julia";299} else if (kernel.indexOf("haskell") != -1) {300mode = "text/x-haskell";301} else if (kernel.indexOf("javascript") != -1) {302mode = "javascript";303} else if (kernel.indexOf("ir") != -1) {304mode = "r";305} else if (306kernel.indexOf("root") != -1 ||307kernel.indexOf("xeus") != -1308) {309mode = "text/x-c++src";310} else if (kernel.indexOf("gap") != -1) {311mode = "gap";312} else {313// Python 3 is probably a good fallback.314mode = { name: "python", version: 3 };315}316}317}318if (typeof mode === "string") {319mode = { name: mode }; // some kernels send a string back for the mode; others an object320}321return mode;322}323324get_more_output = (id: string) => {325// this._more_output only gets set in project-actions in326// set_more_output, for the project or compute server that327// has that extra output.328if (this._more_output != null) {329// This is ONLY used by the backend for storing and retrieving330// extra output messages.331const output = this._more_output[id];332if (output == null) {333return;334}335let { messages } = output;336337for (const x of ["discarded", "truncated"]) {338if (output[x]) {339var text;340if (x === "truncated") {341text = "WARNING: some intermediate output was truncated.\n";342} else {343text = `WARNING: at least ${output[x]} intermediate output ${344output[x] > 1 ? "messages were" : "message was"345} ${x}.\n`;346}347const warn = [{ text, name: "stderr" }];348if (messages.length > 0) {349messages = warn.concat(messages).concat(warn);350} else {351messages = warn;352}353}354}355return messages;356} else {357// client -- return what we know358const msg_list = this.getIn(["more_output", id, "mesg_list"]);359if (msg_list != null) {360return msg_list.toJS();361}362}363};364365get_default_kernel = (): string | undefined => {366const account = this.redux.getStore("account");367if (account != null) {368// TODO: getIn types369return account.getIn(["editor_settings", "jupyter", "kernel"]);370} else {371return undefined;372}373};374375get_kernel_selection = (kernels: Kernels): Map<string, string> => {376return get_kernel_selection(kernels);377};378379// NOTE: defaults for these happen to be true if not given (due to bad380// choice of name by some extension author).381is_cell_editable = (id: string): boolean => {382return this.get_cell_metadata_flag(id, "editable", true);383};384385is_cell_deletable = (id: string): boolean => {386if (!this.is_cell_editable(id)) {387// I've decided that if a cell is not editable, then it is388// automatically not deletable. Relevant facts:389// 1. It makes sense to me.390// 2. This is what Jupyter classic does.391// 3. This is NOT what JupyterLab does.392// 4. The spec doesn't mention deletable: https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata393// See my rant here: https://github.com/jupyter/notebook/issues/3700394return false;395}396return this.get_cell_metadata_flag(id, "deletable", true);397};398399get_cell_metadata_flag = (400id: string,401key: string,402default_value: boolean = false,403): boolean => {404return this.unsafe_getIn(["cells", id, "metadata", key], default_value);405};406407// canonicalize the language of the kernel408get_kernel_language = (): string | undefined => {409return canonical_language(410this.get("kernel"),411this.getIn(["kernel_info", "language"]),412);413};414415// map the kernel language to the syntax of a language we know416get_kernel_syntax = (): Syntax | undefined => {417let lang = this.get_kernel_language();418if (!lang) return undefined;419lang = lang.toLowerCase();420switch (lang) {421case "python":422case "python3":423return "python3";424case "r":425return "R";426case "c++":427case "c++17":428return "c++";429case "javascript":430return "JavaScript";431}432};433434jupyter_kernel_key = async (): Promise<string> => {435const project_id = this.get("project_id");436const projects_store = this.redux.getStore("projects");437const customize = this.redux.getStore("customize");438const computeServerId = await this.redux439.getActions(this.name)440?.getComputeServerId();441if (customize == null) {442// the customize store doesn't exist, e.g., in a compute server.443// In that case no need for a complicated jupyter kernel key as444// there is only one image.445// (??)446return `${project_id}-${computeServerId}-default`;447}448const dflt_img = await customize.getDefaultComputeImage();449const compute_image = projects_store.getIn(450["project_map", project_id, "compute_image"],451dflt_img,452);453const key = [project_id, `${computeServerId}`, compute_image].join("::");454// console.log("jupyter store / jupyter_kernel_key", key);455return key;456};457}458459460