Bug 1783858 - [devtools] Directly pass text and mode when creating a new CodeMirror document in the debugger. r=ochameau.

We used to create the document, then set the text and the mode, which seemed to
trigger unecessary updates in CodeMirror.
Since CodeMirror document can take an initial value and mode, we make this a possibility
in the source editor, and use it from the debugger codebase.

We take this as an opportunity to move the `getMode` function to `source-document.js` as
it's only used from there, and put the logic to not highlight big files there.
The unit test for the function are moved to the same folder the function now lives in,
and are adapted to the new signature.

Differential Revision: https://phabricator.services.mozilla.com/D154096
This commit is contained in:
Nicolas Chevobbe 2022-08-11 12:48:49 +00:00
Родитель f125931e67
Коммит 4a3e629847
5 изменённых файлов: 337 добавлений и 346 удалений

Просмотреть файл

@ -2,11 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { getMode } from "../source";
import { isWasm, getWasmLineNumberFormatter, renderWasmText } from "../wasm";
import { isMinified } from "../isMinified";
import { resizeBreakpointGutter, resizeToggleButton } from "../ui";
import { javascriptLikeExtensions } from "../source";
let sourceDocs = {};
@ -72,10 +71,8 @@ export function updateDocuments(updater) {
}
export function clearEditor(editor) {
const doc = editor.createDocument();
const doc = editor.createDocument("", { name: "text" });
editor.replaceDocument(doc);
editor.setText("");
editor.setMode({ name: "text" });
resetLineNumberFormat(editor);
}
@ -85,11 +82,8 @@ export function showLoading(editor) {
if (doc) {
editor.replaceDocument(doc);
} else {
doc = editor.createDocument();
doc = editor.createDocument(L10N.getStr("loadingText"), { name: "text" });
setDocument("loading", doc);
doc.setValue(L10N.getStr("loadingText"));
editor.replaceDocument(doc);
editor.setMode({ name: "text" });
}
}
@ -100,36 +94,118 @@ export function showErrorMessage(editor, msg) {
} else {
error = L10N.getFormatStr("errorLoadingText3", msg);
}
const doc = editor.createDocument();
const doc = editor.createDocument(error, { name: "text" });
editor.replaceDocument(doc);
editor.setText(error);
editor.setMode({ name: "text" });
resetLineNumberFormat(editor);
}
function setEditorText(editor, sourceId, content) {
if (content.type === "wasm") {
const wasmLines = renderWasmText(sourceId, content);
// cm will try to split into lines anyway, saving memory
const wasmText = { split: () => wasmLines, match: () => false };
editor.setText(wasmText);
} else {
editor.setText(content.value);
}
}
const contentTypeModeMap = {
"text/javascript": { name: "javascript" },
"text/typescript": { name: "javascript", typescript: true },
"text/coffeescript": { name: "coffeescript" },
"text/typescript-jsx": {
name: "jsx",
base: { name: "javascript", typescript: true },
},
"text/jsx": { name: "jsx" },
"text/x-elm": { name: "elm" },
"text/x-clojure": { name: "clojure" },
"text/x-clojurescript": { name: "clojure" },
"text/wasm": { name: "text" },
"text/html": { name: "htmlmixed" },
};
function setMode(editor, source, sourceTextContent, symbols) {
// Disable modes for minified files with 1+ million characters Bug 1569829
const languageMimeMap = [
{ ext: "c", mode: "text/x-csrc" },
{ ext: "kt", mode: "text/x-kotlin" },
{ ext: "cpp", mode: "text/x-c++src" },
{ ext: "m", mode: "text/x-objectivec" },
{ ext: "rs", mode: "text/x-rustsrc" },
{ ext: "hx", mode: "text/x-haxe" },
];
/**
* Returns Code Mirror mode for source content type
*/
// eslint-disable-next-line complexity
export function getMode(source, sourceTextContent, symbols) {
const content = sourceTextContent.value;
// Disable modes for minified files with 1+ million characters (See Bug 1569829).
if (
content.type === "text" &&
isMinified(source, sourceTextContent) &&
content.value.length > 1000000
) {
return;
return { name: "text" };
}
const mode = getMode(source, content, symbols);
const extension = source.displayURL.fileExtension;
if (content.type !== "text") {
return { name: "text" };
}
const { contentType, value: text } = content;
if (extension === "jsx" || (symbols && symbols.hasJsx)) {
if (symbols && symbols.hasTypes) {
return { name: "text/typescript-jsx" };
}
return { name: "jsx" };
}
if (symbols && symbols.hasTypes) {
if (symbols.hasJsx) {
return { name: "text/typescript-jsx" };
}
return { name: "text/typescript" };
}
// check for C and other non JS languages
const result = languageMimeMap.find(({ ext }) => extension === ext);
if (result !== undefined) {
return { name: result.mode };
}
// if the url ends with a known Javascript-like URL, provide JavaScript mode.
// uses the first part of the URL to ignore query string
if (javascriptLikeExtensions.find(ext => ext === extension)) {
return { name: "javascript" };
}
// Use HTML mode for files in which the first non whitespace
// character is `<` regardless of extension.
const isHTMLLike = text.match(/^\s*</);
if (!contentType) {
if (isHTMLLike) {
return { name: "htmlmixed" };
}
return { name: "text" };
}
// // @flow or /* @flow */
if (text.match(/^\s*(\/\/ @flow|\/\* @flow \*\/)/)) {
return contentTypeModeMap["text/typescript"];
}
if (/script|elm|jsx|clojure|wasm|html/.test(contentType)) {
if (contentType in contentTypeModeMap) {
return contentTypeModeMap[contentType];
}
return contentTypeModeMap["text/javascript"];
}
if (isHTMLLike) {
return { name: "htmlmixed" };
}
return { name: "text" };
}
function setMode(editor, source, sourceTextContent, symbols) {
const mode = getMode(source, sourceTextContent, symbols);
const currentMode = editor.codeMirror.getOption("mode");
if (!currentMode || currentMode.name != mode.name) {
editor.setMode(mode);
@ -154,11 +230,22 @@ export function showSourceText(editor, source, sourceTextContent, symbols) {
return doc;
}
const doc = editor.createDocument();
const content = sourceTextContent.value;
let editorText;
if (content.type === "wasm") {
const wasmLines = renderWasmText(source.id, content);
// cm will try to split into lines anyway, saving memory
editorText = { split: () => wasmLines, match: () => false };
} else {
editorText = content.value;
}
const doc = editor.createDocument(
editorText,
getMode(source, sourceTextContent, symbols)
);
setDocument(source.id, doc);
editor.replaceDocument(doc);
setEditorText(editor, source.id, sourceTextContent.value);
setMode(editor, source, sourceTextContent, symbols);
updateLineNumberFormat(editor, source.id);
}

Просмотреть файл

@ -0,0 +1,215 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
import { getMode } from "../source-documents.js";
import {
makeMockSourceWithContent,
makeMockWasmSourceWithContent,
} from "../../test-mockup";
const defaultSymbolDeclarations = {
classes: [],
functions: [],
memberExpressions: [],
callExpressions: [],
objectProperties: [],
identifiers: [],
imports: [],
comments: [],
literals: [],
hasJsx: false,
hasTypes: false,
framework: undefined,
};
describe("source-documents", () => {
describe("getMode", () => {
it("// ", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/javascript",
"// @flow"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("/* @flow */", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/javascript",
" /* @flow */"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("mixed html", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"",
" <html"
);
expect(getMode(source, source.content)).toEqual({ name: "htmlmixed" });
});
it("elm", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/x-elm",
'main = text "Hello, World!"'
);
expect(getMode(source, source.content)).toEqual({ name: "elm" });
});
it("returns jsx if contentType jsx is given", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/jsx",
"<h1></h1>"
);
expect(getMode(source, source.content)).toEqual({ name: "jsx" });
});
it("returns jsx if sourceMetaData says it's a react component", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"",
"<h1></h1>"
);
expect(
getMode(source, source.content, {
...defaultSymbolDeclarations,
hasJsx: true,
})
).toEqual({ name: "jsx" });
});
it("returns jsx if the fileExtension is .jsx", () => {
const source = makeMockSourceWithContent(
"myComponent.jsx",
undefined,
"",
"<h1></h1>"
);
expect(getMode(source, source.content)).toEqual({ name: "jsx" });
});
it("returns text/x-haxe if the file extension is .hx", () => {
const source = makeMockSourceWithContent(
"myComponent.hx",
undefined,
"",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "text/x-haxe" });
});
it("typescript", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/typescript",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("typescript-jsx", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/typescript-jsx",
"<h1></h1>"
);
expect(getMode(source, source.content).base).toEqual({
name: "javascript",
typescript: true,
});
});
it("cross-platform clojure(script) with reader conditionals", () => {
const source = makeMockSourceWithContent(
"my-clojurescript-source-with-reader-conditionals.cljc",
undefined,
"text/x-clojure",
"(defn str->int [s] " +
" #?(:clj (java.lang.Integer/parseInt s) " +
" :cljs (js/parseInt s)))"
);
expect(getMode(source, source.content)).toEqual({ name: "clojure" });
});
it("clojurescript", () => {
const source = makeMockSourceWithContent(
"my-clojurescript-source.cljs",
undefined,
"text/x-clojurescript",
"(+ 1 2 3)"
);
expect(getMode(source, source.content)).toEqual({ name: "clojure" });
});
it("coffeescript", () => {
const source = makeMockSourceWithContent(
undefined,
undefined,
"text/coffeescript",
"x = (a) -> 3"
);
expect(getMode(source, source.content)).toEqual({ name: "coffeescript" });
});
it("wasm", () => {
const source = makeMockWasmSourceWithContent({
binary: "\x00asm\x01\x00\x00\x00",
});
expect(getMode(source, source.content.value)).toEqual({ name: "text" });
});
it("marko", () => {
const source = makeMockSourceWithContent(
"http://localhost.com:7999/increment/sometestfile.marko",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
it("es6", () => {
const source = makeMockSourceWithContent(
"http://localhost.com:7999/increment/sometestfile.es6",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
it("vue", () => {
const source = makeMockSourceWithContent(
"http://localhost.com:7999/increment/sometestfile.vue?query=string",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
});
});

Просмотреть файл

@ -28,7 +28,7 @@ export const sourceTypes = {
vue: "vue",
};
const javascriptLikeExtensions = ["marko", "es6", "vue", "jsm"];
export const javascriptLikeExtensions = ["marko", "es6", "vue", "jsm"];
function getPath(source) {
const { path } = source.displayURL;
@ -295,22 +295,6 @@ export function getFileURL(source, truncate = true) {
return resolveFileURL(url, getUnicodeUrl, truncate);
}
const contentTypeModeMap = {
"text/javascript": { name: "javascript" },
"text/typescript": { name: "javascript", typescript: true },
"text/coffeescript": { name: "coffeescript" },
"text/typescript-jsx": {
name: "jsx",
base: { name: "javascript", typescript: true },
},
"text/jsx": { name: "jsx" },
"text/x-elm": { name: "elm" },
"text/x-clojure": { name: "clojure" },
"text/x-clojurescript": { name: "clojure" },
"text/wasm": { name: "text" },
"text/html": { name: "htmlmixed" },
};
export function getSourcePath(url) {
if (!url) {
return "";
@ -342,100 +326,6 @@ export function getSourceLineCount(content) {
return count + 1;
}
/**
*
* Checks if a source is minified based on some heuristics
* @param key
* @param text
* @return boolean
* @memberof utils/source
* @static
*/
/**
*
* Returns Code Mirror mode for source content type
* @param contentType
* @return String
* @memberof utils/source
* @static
*/
// eslint-disable-next-line complexity
export function getMode(source, content, symbols) {
const extension = source.displayURL.fileExtension;
if (content.type !== "text") {
return { name: "text" };
}
const { contentType, value: text } = content;
if (extension === "jsx" || (symbols && symbols.hasJsx)) {
if (symbols && symbols.hasTypes) {
return { name: "text/typescript-jsx" };
}
return { name: "jsx" };
}
if (symbols && symbols.hasTypes) {
if (symbols.hasJsx) {
return { name: "text/typescript-jsx" };
}
return { name: "text/typescript" };
}
const languageMimeMap = [
{ ext: "c", mode: "text/x-csrc" },
{ ext: "kt", mode: "text/x-kotlin" },
{ ext: "cpp", mode: "text/x-c++src" },
{ ext: "m", mode: "text/x-objectivec" },
{ ext: "rs", mode: "text/x-rustsrc" },
{ ext: "hx", mode: "text/x-haxe" },
];
// check for C and other non JS languages
const result = languageMimeMap.find(({ ext }) => extension === ext);
if (result !== undefined) {
return { name: result.mode };
}
// if the url ends with a known Javascript-like URL, provide JavaScript mode.
// uses the first part of the URL to ignore query string
if (javascriptLikeExtensions.find(ext => ext === extension)) {
return { name: "javascript" };
}
// Use HTML mode for files in which the first non whitespace
// character is `<` regardless of extension.
const isHTMLLike = text.match(/^\s*</);
if (!contentType) {
if (isHTMLLike) {
return { name: "htmlmixed" };
}
return { name: "text" };
}
// // @flow or /* @flow */
if (text.match(/^\s*(\/\/ @flow|\/\* @flow \*\/)/)) {
return contentTypeModeMap["text/typescript"];
}
if (/script|elm|jsx|clojure|wasm|html/.test(contentType)) {
if (contentType in contentTypeModeMap) {
return contentTypeModeMap[contentType];
}
return contentTypeModeMap["text/javascript"];
}
if (isHTMLLike) {
return { name: "htmlmixed" };
}
return { name: "text" };
}
export function isInlineScript(source) {
return source.introductionType === "scriptElement";
}

Просмотреть файл

@ -7,7 +7,6 @@ import {
getTruncatedFileName,
getFileURL,
getDisplayPath,
getMode,
getSourceLineCount,
isThirdParty,
isJavaScript,
@ -27,21 +26,6 @@ import {
} from "../test-mockup";
import { isFulfilled } from "../async-value.js";
const defaultSymbolDeclarations = {
classes: [],
functions: [],
memberExpressions: [],
callExpressions: [],
objectProperties: [],
identifiers: [],
imports: [],
comments: [],
literals: [],
hasJsx: false,
hasTypes: false,
framework: undefined,
};
describe("sources", () => {
const unicode = "\u6e2c";
const encodedUnicode = encodeURIComponent(unicode);
@ -285,194 +269,6 @@ describe("sources", () => {
});
});
describe("getMode", () => {
it("// ", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/javascript",
"// @flow"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("/* @flow */", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/javascript",
" /* @flow */"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("mixed html", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"",
" <html"
);
expect(getMode(source, source.content)).toEqual({ name: "htmlmixed" });
});
it("elm", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/x-elm",
'main = text "Hello, World!"'
);
expect(getMode(source, source.content)).toEqual({ name: "elm" });
});
it("returns jsx if contentType jsx is given", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/jsx",
"<h1></h1>"
);
expect(getMode(source, source.content)).toEqual({ name: "jsx" });
});
it("returns jsx if sourceMetaData says it's a react component", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"",
"<h1></h1>"
);
expect(
getMode(source, source.content, {
...defaultSymbolDeclarations,
hasJsx: true,
})
).toEqual({ name: "jsx" });
});
it("returns jsx if the fileExtension is .jsx", () => {
const source = makeMockSourceAndContent(
"myComponent.jsx",
undefined,
"",
"<h1></h1>"
);
expect(getMode(source, source.content)).toEqual({ name: "jsx" });
});
it("returns text/x-haxe if the file extension is .hx", () => {
const source = makeMockSourceAndContent(
"myComponent.hx",
undefined,
"",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "text/x-haxe" });
});
it("typescript", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/typescript",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({
name: "javascript",
typescript: true,
});
});
it("typescript-jsx", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/typescript-jsx",
"<h1></h1>"
);
expect(getMode(source, source.content).base).toEqual({
name: "javascript",
typescript: true,
});
});
it("cross-platform clojure(script) with reader conditionals", () => {
const source = makeMockSourceAndContent(
"my-clojurescript-source-with-reader-conditionals.cljc",
undefined,
"text/x-clojure",
"(defn str->int [s] " +
" #?(:clj (java.lang.Integer/parseInt s) " +
" :cljs (js/parseInt s)))"
);
expect(getMode(source, source.content)).toEqual({ name: "clojure" });
});
it("clojurescript", () => {
const source = makeMockSourceAndContent(
"my-clojurescript-source.cljs",
undefined,
"text/x-clojurescript",
"(+ 1 2 3)"
);
expect(getMode(source, source.content)).toEqual({ name: "clojure" });
});
it("coffeescript", () => {
const source = makeMockSourceAndContent(
undefined,
undefined,
"text/coffeescript",
"x = (a) -> 3"
);
expect(getMode(source, source.content)).toEqual({ name: "coffeescript" });
});
it("wasm", () => {
const source = makeMockWasmSourceWithContent({
binary: "\x00asm\x01\x00\x00\x00",
});
expect(getMode(source, source.content.value)).toEqual({ name: "text" });
});
it("marko", () => {
const source = makeMockSourceAndContent(
"http://localhost.com:7999/increment/sometestfile.marko",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
it("es6", () => {
const source = makeMockSourceAndContent(
"http://localhost.com:7999/increment/sometestfile.es6",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
it("vue", () => {
const source = makeMockSourceAndContent(
"http://localhost.com:7999/increment/sometestfile.vue?query=string",
undefined,
"does not matter",
"function foo(){}"
);
expect(getMode(source, source.content)).toEqual({ name: "javascript" });
});
});
describe("getSourceLineCount", () => {
it("should give us the amount bytes for wasm source", () => {
const { content } = makeMockWasmSourceWithContent({

Просмотреть файл

@ -564,10 +564,13 @@ Editor.prototype = {
/**
* Creates a CodeMirror Document
*
* @param {String} text: Initial text of the document
* @param {Object|String} mode: Mode of the document. See https://codemirror.net/5/doc/manual.html#option_mode
* @returns CodeMirror.Doc
*/
createDocument() {
return new this.Doc("");
createDocument(text = "", mode) {
return new this.Doc(text, mode);
},
/**