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