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/frontend/codemirror/extensions/edit-selection.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
import * as CodeMirror from "codemirror";
7
8
import { redux } from "@cocalc/frontend/app-framework";
9
import {
10
commands as EDIT_COMMANDS,
11
FONT_FACES,
12
} from "@cocalc/frontend/editors/editor-button-bar";
13
import { getLocale } from "@cocalc/frontend/i18n";
14
import { markdown_to_html } from "@cocalc/frontend/markdown";
15
import { open_new_tab, sagews_canonical_mode } from "@cocalc/frontend/misc";
16
import { defaults, required, startswith } from "@cocalc/util/misc";
17
import { ai_gen_formula } from "./ai-formula";
18
19
/*
20
Apply an edit to the selected text in an editor; works with one or more
21
selections. What happens depends on the mode. This is used to implement an
22
editor on top of codemirror, e.g., to provide features like "make the selected
23
text be in italics" or "comment out the selected text".
24
*/
25
26
// The plugin is async; awaiting it can take a while, since it might have to
27
// wait for user to respond to dialog boxes.
28
CodeMirror.defineExtension(
29
"edit_selection",
30
async function (opts: {
31
cmd: string;
32
args?: string | number;
33
mode?: string;
34
project_id?: string;
35
}): Promise<void> {
36
opts = defaults(opts, {
37
cmd: required,
38
args: undefined,
39
mode: undefined,
40
project_id: undefined,
41
});
42
// @ts-ignore
43
const cm = this;
44
45
// Special cases -- link/image/SpecialChar commands handle themselves:
46
switch (opts.cmd) {
47
case "link":
48
await cm.insert_link();
49
return;
50
case "image":
51
await cm.insert_image();
52
return;
53
case "SpecialChar":
54
await cm.insert_special_char();
55
return;
56
}
57
58
const default_mode = opts.mode ?? cm.get_edit_mode();
59
const canonical_mode = (name) => sagews_canonical_mode(name, default_mode);
60
61
const { args, cmd, project_id } = opts;
62
63
// FUTURE: will have to make this more sophisticated, so it can
64
// deal with nesting, spans, etc.
65
const strip = function (
66
src: string,
67
left: string,
68
right: string,
69
): string | undefined {
70
left = left.toLowerCase();
71
right = right.toLowerCase();
72
const src0 = src.toLowerCase();
73
const i = src0.indexOf(left);
74
if (i !== -1) {
75
const j = src0.lastIndexOf(right);
76
if (j !== -1) {
77
return (
78
src.slice(0, i) +
79
src.slice(i + left.length, j) +
80
src.slice(j + right.length)
81
);
82
}
83
}
84
// Nothing got striped -- returns undefined to
85
// indicate that there was no wrapping to strip.
86
};
87
88
const selections = cm.listSelections();
89
for (let selection of selections) {
90
let left = "";
91
const mode = canonical_mode(cm.getModeAt(selection.head).name);
92
const from = selection.from();
93
const to = selection.to();
94
let src = cm.getRange(from, to);
95
const start_line_beginning = from.ch === 0;
96
const until_line_ending = cm.getLine(to.line).length === to.ch;
97
98
let mode1 = mode;
99
const data_for_mode = EDIT_COMMANDS[mode1];
100
/* console.log("edit_selection", {
101
args,
102
cmd,
103
default_mode,
104
selection,
105
src,
106
data_for_mode,
107
});*/
108
109
if (data_for_mode == null) {
110
// TODO: better way to alert that this isn't going to work?
111
console.warn(`mode '${mode1}' is not defined!`);
112
return;
113
}
114
var how = data_for_mode[cmd];
115
if (how == null) {
116
if (["md", "mediawiki", "rst"].indexOf(mode1) != -1) {
117
// html fallback for markdown
118
mode1 = "html";
119
} else if (mode1 === "python") {
120
// Sage fallback in python mode. FUTURE: There should be a Sage mode.
121
mode1 = "sage";
122
}
123
how = EDIT_COMMANDS[mode1][cmd];
124
}
125
126
// trim whitespace
127
let i = 0;
128
let j = src.length - 1;
129
if (how != null && (how.trim ?? true)) {
130
while (i < src.length && /\s/.test(src[i])) {
131
i += 1;
132
}
133
while (j > 0 && /\s/.test(src[j])) {
134
j -= 1;
135
}
136
}
137
j += 1;
138
const left_white = src.slice(0, i);
139
const right_white = src.slice(j);
140
src = src.slice(i, j);
141
let src0 = src;
142
143
let done: boolean = false;
144
145
// this is an abuse, but having external links to the documentation is good
146
if (how?.url != null) {
147
open_new_tab(how.url);
148
done = true;
149
}
150
151
if (how?.wrap != null) {
152
const { space } = how.wrap;
153
left = how.wrap.left ?? "";
154
const right = how.wrap.right ?? "";
155
const process = function (src: string): string {
156
let src1;
157
if (how.strip != null) {
158
// Strip out any tags/wrapping from conflicting modes.
159
for (let c of how.strip) {
160
const { wrap } = EDIT_COMMANDS[mode1][c];
161
if (wrap != null) {
162
src1 = strip(src, wrap.left ?? "", wrap.right ?? "");
163
if (src1 != null) {
164
src = src1;
165
if (space && src[0] === " ") {
166
src = src.slice(1);
167
}
168
}
169
}
170
}
171
}
172
173
src1 = strip(src, left, right);
174
if (src1) {
175
// strip the wrapping
176
src = src1;
177
if (space && src[0] === " ") {
178
src = src.slice(1);
179
}
180
} else {
181
// do the wrapping
182
src = `${left}${space ? " " : ""}${
183
src ? src : how.default ?? ""
184
}${right}`;
185
}
186
return src;
187
};
188
189
if (how.wrap.multi) {
190
src = src.split("\n").map(process).join("\n");
191
} else {
192
src = process(src);
193
}
194
if (how.wrap.newline) {
195
src = "\n" + src + "\n";
196
if (!start_line_beginning) {
197
src = "\n" + src;
198
}
199
if (!until_line_ending) {
200
src += "\n";
201
}
202
}
203
done = true;
204
}
205
206
if (how?.insert != null) {
207
// to insert the code snippet right below, next line
208
// SMELL: no idea what the strip(...) above is actually doing
209
// no additional newline, if nothing is selected and at start of line
210
if (selection.empty() && from.ch === 0) {
211
src = how.insert;
212
} else {
213
// this also inserts a new line, if cursor is inside/end of line
214
src = `${src}\n${how.insert}`;
215
}
216
done = true;
217
}
218
219
switch (cmd) {
220
case "font_size":
221
if (["html", "md", "mediawiki"].includes(mode)) {
222
for (let i = 1; i <= 7; i++) {
223
const src1 = strip(src, `<font size=${i}>`, "</font>");
224
if (src1) {
225
src = src1;
226
}
227
}
228
if (args !== "3") {
229
src = `<font size=${args}>${src}</font>`;
230
}
231
done = true;
232
} else if (mode === "tex") {
233
// we need 6 latex sizes, for size 1 to 7 (default 3, at index 2)
234
const latex_sizes = [
235
"tiny",
236
"footnotesize",
237
"normalsize",
238
"large",
239
"LARGE",
240
"huge",
241
"Huge",
242
];
243
if (args) {
244
i = typeof args == "string" ? parseInt(args) : args;
245
if ([1, 2, 3, 4, 5, 6, 7].indexOf(i) != -1) {
246
const size = latex_sizes[i - 1];
247
src = `{\\${size} ${src}}`;
248
}
249
}
250
done = true;
251
}
252
break;
253
254
case "font_size_new":
255
if (["html", "md", "mediawiki"].includes(mode)) {
256
src0 = src.toLowerCase().trim();
257
if (startswith(src0, "<span style='font-size")) {
258
i = src.indexOf(">");
259
j = src.lastIndexOf("<");
260
src = src.slice(i + 1, j);
261
}
262
if (args !== "medium") {
263
src = `<span style='font-size:${args}'>${src}</span>`;
264
}
265
done = true;
266
} else if (mode === "tex") {
267
// we need 6 latex sizes, for size 1 to 7 (default 3, at index 2)
268
const latex_sizes = [
269
"tiny",
270
"footnotesize",
271
"normalsize",
272
"large",
273
"LARGE",
274
"huge",
275
"Huge",
276
];
277
if (args) {
278
i = typeof args == "string" ? parseInt(args) : args;
279
if ([1, 2, 3, 4, 5, 6, 7].indexOf(i) != -1) {
280
const size = latex_sizes[i - 1];
281
src = `{\\${size} ${src}}`;
282
}
283
}
284
done = true;
285
}
286
break;
287
288
case "color":
289
if (["html", "md", "mediawiki"].includes(mode)) {
290
src0 = src.toLowerCase().trim();
291
if (startswith(src0, "<span style='color")) {
292
i = src.indexOf(">");
293
j = src.lastIndexOf("<");
294
src = src.slice(i + 1, j);
295
}
296
src = `<span style='color:${args}'>${src}</span>`;
297
done = true;
298
} else if (mode == "tex") {
299
const pre = cm.getValue().includes("\\usepackage[HTML]{xcolor}")
300
? ""
301
: "\n\\usepackage[HTML]{xcolor} % put this in your preamble to enable color\n";
302
src = `${pre}\\textcolor[HTML]{${
303
typeof args == "string" && args.startsWith("#")
304
? args.slice(1)
305
: args
306
}}{${src.trim()}}`;
307
done = true;
308
}
309
break;
310
311
case "background-color":
312
if (["html", "md", "mediawiki"].includes(mode)) {
313
src0 = src.toLowerCase().trim();
314
if (startswith(src0, "<span style='background")) {
315
i = src.indexOf(">");
316
j = src.lastIndexOf("<");
317
src = src.slice(i + 1, j);
318
}
319
src = `<span style='background-color:${args}'>${src}</span>`;
320
done = true;
321
}
322
break;
323
324
case "font_face": // old -- still used in some old non-react editors
325
if (["html", "md", "mediawiki"].includes(mode)) {
326
for (const face of FONT_FACES) {
327
const src1 = strip(src, `<font face='${face}'>`, "</font>");
328
if (src1) {
329
src = src1;
330
}
331
}
332
src = `<font face='${args}'>${src}</font>`;
333
done = true;
334
}
335
break;
336
337
case "font_family": // new -- html5 style
338
if (["html", "md", "mediawiki"].includes(mode)) {
339
src0 = src.toLowerCase().trim();
340
if (startswith(src0, "<span style='font-family")) {
341
i = src.indexOf(">");
342
j = src.lastIndexOf("<");
343
src = src.slice(i + 1, j);
344
}
345
if (!src) {
346
src = " ";
347
}
348
src = `<span style='font-family:${args}'>${src}</span>`;
349
done = true;
350
}
351
break;
352
353
case "clean":
354
if (mode === "html") {
355
// do *something* to make the html more valid; of course, we could
356
// do a lot more...
357
src = $("<div>").html(src).html();
358
done = true;
359
}
360
break;
361
362
case "unformat":
363
if (mode === "html") {
364
src = $("<div>").html(src).text();
365
done = true;
366
} else if (mode === "md") {
367
src = $("<div>").html(markdown_to_html(src)).text();
368
done = true;
369
}
370
break;
371
372
case "ai_formula":
373
if (project_id != null) {
374
const account_store = redux.getStore("account");
375
const locale = getLocale(account_store.get("other_settings"));
376
377
src = await ai_gen_formula({
378
mode,
379
text: src,
380
project_id,
381
locale,
382
});
383
}
384
done = true;
385
break;
386
}
387
388
if (!done) {
389
if ((window as any).DEBUG && how == null) {
390
console.warn(
391
`CodeMirror/edit_selection: unknown for mode1='${mode1}' and cmd='${cmd}'`,
392
);
393
}
394
395
// TODO: should we show an alert or something??
396
console.warn(`not implemented. cmd='${cmd}' mode='${mode1}'`);
397
continue;
398
}
399
400
if (src === src0) {
401
continue;
402
}
403
404
cm.focus();
405
cm.replaceRange(left_white + src + right_white, from, to);
406
407
if (how?.insert == null && how?.wrap == null) {
408
if (selection.empty()) {
409
// restore cursor
410
const delta = left.length ?? 0;
411
cm.setCursor({ line: from.line, ch: to.ch + delta });
412
} else {
413
// now select the new range
414
const delta = src.length - src0.length;
415
cm.extendSelection(from, { line: to.line, ch: to.ch + delta });
416
}
417
}
418
}
419
},
420
);
421
422