CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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