Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/jupyter/redux/store.ts
Views: 687
/*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";1213import { export_to_ipynb } from "@cocalc/jupyter/ipynb/export-to-ipynb";14import { KernelSpec } from "@cocalc/jupyter/ipynb/parse";15import {16Cell,17CellToolbarName,18KernelInfo,19NotebookMode,20} from "@cocalc/jupyter/types";21import {22Kernel,23Kernels,24get_kernel_selection,25} from "@cocalc/jupyter/util/misc";26import { Syntax } from "@cocalc/util/code-formatter";27import { startswith } from "@cocalc/util/misc";28import { Store } from "@cocalc/util/redux/Store";29import type { ImmutableUsageInfo } from "@cocalc/util/types/project-usage-info";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// computeServerId -- gets optionally set on the frontend (useful for react)103computeServerId?: number;104requestedComputeServerId?: number;105// run progress = Percent (0-100) of runnable cells that have been run since the last kernel restart. (Thus markdown and empty cells are excluded.)106runProgress?: number;107}108109export const initial_jupyter_store_state: {110[K in keyof JupyterStoreState]?: JupyterStoreState[K];111} = {112check_select_kernel_init: false,113show_kernel_selector: false,114widgetModelIdState: Map(),115cell_list: List(),116cells: Map(),117};118119export class JupyterStore extends Store<JupyterStoreState> {120// manipulated in jupyter/project-actions.ts121public _more_output: any;122123// immutable List124public get_cell_list = (): List<string> => {125return this.get("cell_list") ?? List();126};127128// string[]129public get_cell_ids_list(): string[] {130return this.get_cell_list().toJS();131}132133public get_cell_type(id: string): "markdown" | "code" | "raw" {134// NOTE: default cell_type is "code", which is common, to save space.135// TODO: We use unsafe_getIn because maybe the cell type isn't spelled out yet, or our typescript isn't good enough.136const type = this.unsafe_getIn(["cells", id, "cell_type"], "code");137if (type != "markdown" && type != "code" && type != "raw") {138throw Error(`invalid cell type ${type} for cell ${id}`);139}140return type;141}142143public get_cell_index(id: string): number {144const cell_list = this.get("cell_list");145if (cell_list == null) {146// truly fatal147throw Error("ordered list of cell id's not known");148}149const i = cell_list.indexOf(id);150if (i === -1) {151throw Error(`unknown cell id ${id}`);152}153return i;154}155156// Get the id of the cell that is delta positions from157// cell with given id (second input).158// Returns undefined if delta positions moves out of159// the notebook (so there is no such cell) or there160// is no cell with the given id; in particular,161// we do NOT wrap around.162public get_cell_id(delta = 0, id: string): string | undefined {163let i: number;164try {165i = this.get_cell_index(id);166} catch (_) {167// no such cell. This can happen, e.g., https://github.com/sagemathinc/cocalc/issues/6686168return;169}170i += delta;171const cell_list = this.get("cell_list");172if (cell_list == null || i < 0 || i >= cell_list.size) {173return; // .get negative for List in immutable wraps around rather than undefined (like Python)174}175return cell_list.get(i);176}177178set_global_clipboard = (clipboard: any) => {179global_clipboard = clipboard;180};181182get_global_clipboard = () => {183return global_clipboard;184};185186get_kernel_info = (187kernel: string | null | undefined,188): KernelSpec | undefined => {189// slow/inefficient, but ok since this is rarely called190let info: any = undefined;191const kernels = this.get("kernels");192if (kernels === undefined) return;193if (kernels === null) {194return {195name: "No Kernel",196language: "",197display_name: "No Kernel",198};199}200kernels.forEach((x: any) => {201if (x.get("name") === kernel) {202info = x.toJS() as KernelSpec;203return false;204}205});206return info;207};208209// Export the Jupyer notebook to an ipynb object.210get_ipynb = (blob_store?: any) => {211if (this.get("cells") == null || this.get("cell_list") == null) {212// not sufficiently loaded yet.213return;214}215216const cell_list = this.get("cell_list");217const more_output: { [id: string]: any } = {};218for (const id of cell_list.toJS()) {219const x = this.get_more_output(id);220if (x != null) {221more_output[id] = x;222}223}224225return export_to_ipynb({226cells: this.get("cells"),227cell_list,228metadata: this.get("metadata"), // custom metadata229kernelspec: this.get_kernel_info(this.get("kernel")),230language_info: this.get_language_info(),231blob_store,232more_output,233});234};235236public get_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) return language_info;240}241}242243public get_cm_mode() {244let metadata_immutable = this.get("backend_kernel_info");245if (metadata_immutable == null) {246metadata_immutable = this.get("metadata");247}248let metadata: { language_info?: any; kernelspec?: any } | undefined;249if (metadata_immutable != null) {250metadata = metadata_immutable.toJS();251} else {252metadata = undefined;253}254let mode: any;255if (metadata != null) {256if (257metadata.language_info != null &&258metadata.language_info.codemirror_mode != null259) {260mode = metadata.language_info.codemirror_mode;261} else if (262metadata.language_info != null &&263metadata.language_info.name != null264) {265mode = metadata.language_info.name;266} else if (267metadata.kernelspec != null &&268metadata.kernelspec.language != null269) {270mode = metadata.kernelspec.language.toLowerCase();271}272}273if (mode == null) {274// As a fallback in case none of the metadata has been filled in yet by the backend,275// we can guess a mode from the kernel in many cases. Any mode is vastly better276// than nothing!277let kernel = this.get("kernel"); // may be better than nothing...; e.g., octave kernel has no mode.278if (kernel != null) {279kernel = kernel.toLowerCase();280// The kernel is just a string that names the kernel, so we use heuristics.281if (kernel.indexOf("python") != -1) {282if (kernel.indexOf("python3") != -1) {283mode = { name: "python", version: 3 };284} else {285mode = { name: "python", version: 2 };286}287} else if (kernel.indexOf("sage") != -1) {288mode = { name: "python", version: 3 };289} else if (kernel.indexOf("anaconda") != -1) {290mode = { name: "python", version: 3 };291} else if (kernel.indexOf("octave") != -1) {292mode = "octave";293} else if (kernel.indexOf("bash") != -1) {294mode = "shell";295} else if (kernel.indexOf("julia") != -1) {296mode = "text/x-julia";297} else if (kernel.indexOf("haskell") != -1) {298mode = "text/x-haskell";299} else if (kernel.indexOf("javascript") != -1) {300mode = "javascript";301} else if (kernel.indexOf("ir") != -1) {302mode = "r";303} else if (304kernel.indexOf("root") != -1 ||305kernel.indexOf("xeus") != -1306) {307mode = "text/x-c++src";308} else if (kernel.indexOf("gap") != -1) {309mode = "gap";310} else {311// Python 3 is probably a good fallback.312mode = { name: "python", version: 3 };313}314}315}316if (typeof mode === "string") {317mode = { name: mode }; // some kernels send a string back for the mode; others an object318}319return mode;320}321322get_more_output = (id: string) => {323// this._more_output only gets set in project-actions in324// set_more_output, for the project or compute server that325// has that extra output.326if (this._more_output != null) {327// This is ONLY used by the backend for storing and retrieving328// extra output messages.329const output = this._more_output[id];330if (output == null) {331return;332}333let { messages } = output;334335for (const x of ["discarded", "truncated"]) {336if (output[x]) {337var text;338if (x === "truncated") {339text = "WARNING: some intermediate output was truncated.\n";340} else {341text = `WARNING: ${output[x]} intermediate output ${342output[x] > 1 ? "messages were" : "message was"343} ${x}.\n`;344}345const warn = [{ text: text, name: "stderr" }];346if (messages.length > 0) {347messages = warn.concat(messages).concat(warn);348} else {349messages = warn;350}351}352}353return messages;354} else {355// client -- return what we know356const msg_list = this.getIn(["more_output", id, "mesg_list"]);357if (msg_list != null) {358return msg_list.toJS();359}360}361};362363get_default_kernel = (): string | undefined => {364const account = this.redux.getStore("account");365if (account != null) {366// TODO: getIn types367return account.getIn(["editor_settings", "jupyter", "kernel"]);368} else {369return undefined;370}371};372373get_kernel_selection = (kernels: Kernels): Map<string, string> => {374return get_kernel_selection(kernels);375};376377get_raw_link = (path: any) => {378return this.redux379.getProjectStore(this.get("project_id"))380.get_raw_link(path);381};382383// NOTE: defaults for these happen to be true if not given (due to bad384// choice of name by some extension author).385public is_cell_editable(id: string): boolean {386return this.get_cell_metadata_flag(id, "editable", true);387}388389public is_cell_deletable(id: string): boolean {390if (!this.is_cell_editable(id)) {391// I've decided that if a cell is not editable, then it is392// automatically not deletable. Relevant facts:393// 1. It makes sense to me.394// 2. This is what Jupyter classic does.395// 3. This is NOT what JupyterLab does.396// 4. The spec doesn't mention deletable: https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata397// See my rant here: https://github.com/jupyter/notebook/issues/3700398return false;399}400return this.get_cell_metadata_flag(id, "deletable", true);401}402403public get_cell_metadata_flag(404id: string,405key: string,406default_value: boolean = false,407): boolean {408return this.unsafe_getIn(["cells", id, "metadata", key], default_value);409}410411// canonicalize the language of the kernel412public get_kernel_language(): string | undefined {413return canonical_language(414this.get("kernel"),415this.getIn(["kernel_info", "language"]),416);417}418419// map the kernel language to the syntax of a language we know420public get_kernel_syntax(): Syntax | undefined {421let lang = this.get_kernel_language();422if (!lang) return undefined;423lang = lang.toLowerCase();424switch (lang) {425case "python":426case "python3":427return "python3";428case "r":429return "R";430case "c++":431case "c++17":432return "c++";433case "javascript":434return "JavaScript";435}436}437438public async jupyter_kernel_key(): Promise<string> {439const project_id = this.get("project_id");440const projects_store = this.redux.getStore("projects");441const customize = this.redux.getStore("customize");442const computeServerId =443this.redux.getActions(this.name)?.getComputeServerId() ?? 0;444if (customize == null) {445// the customize store doesn't exist, e.g., in a compute server.446// In that case no need for a complicated jupyter kernel key as447// there is only one image.448// (??)449return `${project_id}-${computeServerId}-default`;450}451const dflt_img = await customize.getDefaultComputeImage();452const compute_image = projects_store.getIn(453["project_map", project_id, "compute_image"],454dflt_img,455);456const key = [project_id, `${computeServerId}`, compute_image].join("::");457// console.log("jupyter store / jupyter_kernel_key", key);458return key;459}460}461462463