The input element wasn't rendered anymore ("Flattening unsafe node"), so build
a document fragment instead of constructing the string for the prompt element.
Instead of adding inline style, we put CSS classes on the elements
for which we add rules in mozilla.css.
The styles are slightly tweaked so we use the colors from the DevTools theme,
and show proper focus indicator.
Differential Revision: https://phabricator.services.mozilla.com/D242525
316 lines
13 KiB
Plaintext
316 lines
13 KiB
Plaintext
This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
|
|
is a JavaScript component that provides a code editor in the browser. When
|
|
a mode is available for the language you are coding in, it will color your
|
|
code, and optionally help with indentation.
|
|
|
|
# CodeMirror 6
|
|
|
|
We're currently migrating to CodeMirror 6, which means we have bundle for version 6 _and_ 5,
|
|
until we successfully migrated all consumers to CodeMirror 6.
|
|
|
|
For version 6, we're generating a bundle (codemirror6/codemirror6.bundle.mjs) using rollup.
|
|
The entry point for the bundle is codemirror6/index.mjs, where we export all the classes
|
|
and functions that the editor needs. When adding new exported item, the bundle needs to
|
|
be updated, which can be done by running:
|
|
> cd devtools/client/shared/sourceeditor
|
|
> npm install
|
|
> npm run build-cm6
|
|
|
|
This will produced a minified bundle, which might not be ideal if you're debugging an issue or profiling.
|
|
You can get an unminified bundle by running:
|
|
> npm run build-cm6-unminified
|
|
|
|
The generated bundle can be configurated in rollup.config.mjs.
|
|
|
|
# CodeMirror 5
|
|
|
|
## CodeMirror 5 Upgrade
|
|
|
|
Currently used version is 5.58.1. To upgrade: download a new version of
|
|
CodeMirror from the project's page [1] and replace all JavaScript and
|
|
CSS files inside the codemirror directory [2].
|
|
|
|
Then to recreate codemirror.bundle.js:
|
|
> cd devtools/client/shared/sourceeditor
|
|
> npm install
|
|
> npm run build
|
|
|
|
When investigating an issue in CodeMirror, you might want to have a non-minified bundle.
|
|
You can do this by running `npm run build-unminified` instead of `npm run build`.
|
|
|
|
To confirm the functionality run mochitests for the following components:
|
|
|
|
* sourceeditor
|
|
* debugger
|
|
* styleditor
|
|
* netmonitor
|
|
* webconsole
|
|
|
|
The sourceeditor component contains imported CodeMirror tests [3].
|
|
|
|
* Some tests were commented out because we don't use that functionality
|
|
within Firefox (for example Ruby editing mode). Be careful when updating
|
|
files test/codemirror.html and test/vimemacs.html; they were modified to
|
|
co-exist with Mozilla's testing infrastructure. Basically, vimemacs.html
|
|
is a copy of codemirror.html but only with VIM and Emacs mode tests
|
|
enabled.
|
|
* In cm_comment_test.js comment out fallbackToBlock and fallbackToLine
|
|
tests.
|
|
* The search addon (search.js) was slightly modified to make search
|
|
UI localizable (see patch below).
|
|
|
|
Other than that, we don't have any Mozilla-specific patches applied to
|
|
CodeMirror itself.
|
|
|
|
## Addons
|
|
|
|
To install a new CodeMirror 5 addon add it to the codemirror directory,
|
|
jar.mn [4] file and editor.js [5]. Also, add it to the License section
|
|
below.
|
|
|
|
## License
|
|
|
|
The following files in this directory and devtools/client/shared/sourceeditor/test/codemirror/
|
|
are licensed according to the contents in the LICENSE file.
|
|
|
|
## Localization patches
|
|
|
|
diff --git a/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js b/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js
|
|
--- a/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js
|
|
+++ b/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js
|
|
@@ -93,32 +93,47 @@
|
|
} else {
|
|
query = parseString(query)
|
|
}
|
|
if (typeof query == "string" ? query == "" : query.test(""))
|
|
query = /x^/;
|
|
return query;
|
|
}
|
|
|
|
- var queryDialog =
|
|
- '<span class="CodeMirror-search-label">Search:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
|
|
|
function startSearch(cm, state, query) {
|
|
state.queryText = query;
|
|
state.query = parseQuery(query);
|
|
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
|
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
|
cm.addOverlay(state.overlay);
|
|
if (cm.showMatchesOnScrollbar) {
|
|
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
|
state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
|
|
}
|
|
}
|
|
|
|
function doSearch(cm, rev, persistent, immediate) {
|
|
+ // We used to only build this input the first time the search was triggered and
|
|
+ // reuse it again on subsequent search.
|
|
+ // Unfortunately, this doesn't play well with the `persistent` parameter;
|
|
+ // new event listeners are added to the input each time `persistentDialog` is called,
|
|
+ // which would make a single `Enter` key trigger multiple "findNext" actions, making
|
|
+ // it look like the search would skip some results.
|
|
+ const doc = cm.getWrapperElement().ownerDocument;
|
|
+ const inp = doc.createElement("input");
|
|
+
|
|
+ inp.type = "search";
|
|
+ inp.classList.add("cm5-search-input");
|
|
+ inp.placeholder = cm.l10n("findCmd.promptMessage");
|
|
+ inp.addEventListener("focus", () => inp.select());
|
|
+
|
|
+ const queryDialog = doc.createElement("div");
|
|
+ queryDialog.classList.add("cm5-search-container");
|
|
+ queryDialog.appendChild(inp);
|
|
|
|
var state = getSearchState(cm);
|
|
if (state.query) return findNext(cm, rev);
|
|
var q = cm.getSelection() || state.lastQuery;
|
|
if (q instanceof RegExp && q.source == "x^") q = null
|
|
if (persistent && cm.openDialog) {
|
|
var hiding = null
|
|
var searchNext = function(query, event) {
|
|
CodeMirror.e_stop(event);
|
|
@@ -181,56 +196,110 @@
|
|
var state = getSearchState(cm);
|
|
state.lastQuery = state.query;
|
|
if (!state.query) return;
|
|
state.query = state.queryText = null;
|
|
cm.removeOverlay(state.overlay);
|
|
if (state.annotate) { state.annotate.clear(); state.annotate = null; }
|
|
});}
|
|
|
|
- var replaceQueryDialog =
|
|
- ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
|
|
- var replacementQueryDialog = '<span class="CodeMirror-search-label">With:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
|
|
- var doReplaceConfirm = '<span class="CodeMirror-search-label">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>';
|
|
-
|
|
function replaceAll(cm, query, text) {
|
|
cm.operation(function() {
|
|
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
|
|
if (typeof query != "string") {
|
|
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
|
|
cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
|
} else cursor.replace(text);
|
|
}
|
|
});
|
|
}
|
|
|
|
function replace(cm, all) {
|
|
if (cm.getOption("readOnly")) return;
|
|
var query = cm.getSelection() || getSearchState(cm).lastQuery;
|
|
- var dialogText = '<span class="CodeMirror-search-label">' + (all ? 'Replace all:' : 'Replace:') + '</span>';
|
|
- dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
|
|
+
|
|
+ let doc = cm.getWrapperElement().ownerDocument;
|
|
+
|
|
+ // `searchLabel` is used as part of `replaceQueryFragment` and as a separate
|
|
+ // argument by itself, so it should be cloned.
|
|
+ let searchLabel = doc.createElement("span");
|
|
+ searchLabel.classList.add("CodeMirror-search-label");
|
|
+ searchLabel.textContent = all ? "Replace all:" : "Replace:";
|
|
+
|
|
+ let replaceQueryFragment = doc.createDocumentFragment();
|
|
+ replaceQueryFragment.appendChild(searchLabel.cloneNode(true));
|
|
+
|
|
+ let searchField = doc.createElement("input");
|
|
+ searchField.setAttribute("type", "text");
|
|
+ searchField.classList.add("cm5-search-replace-input");
|
|
+ replaceQueryFragment.appendChild(searchField);
|
|
+
|
|
+ let searchHint = doc.createElement("span");
|
|
+ searchHint.classList.add("cm5-search-replace-hint");
|
|
+ searchHint.textContent = "(Use /re/ syntax for regexp search)";
|
|
+ replaceQueryFragment.appendChild(searchHint);
|
|
+
|
|
+ dialog(cm, replaceQueryFragment, searchLabel, query, function(query) {
|
|
if (!query) return;
|
|
query = parseQuery(query);
|
|
- dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
|
|
+
|
|
+ let replacementQueryFragment = doc.createDocumentFragment();
|
|
+
|
|
+ let replaceWithLabel = searchLabel.cloneNode(false);
|
|
+ replaceWithLabel.textContent = "With:";
|
|
+ replacementQueryFragment.appendChild(replaceWithLabel);
|
|
+
|
|
+ let replaceField = doc.createElement("input");
|
|
+ replaceField.setAttribute("type", "text");
|
|
+ replaceField.classList.add("cm5-search-replace-input");
|
|
+ replacementQueryFragment.appendChild(replaceField);
|
|
+
|
|
+ dialog(cm, replacementQueryFragment, "Replace with:", "", function(text) {
|
|
text = parseString(text)
|
|
if (all) {
|
|
replaceAll(cm, query, text)
|
|
} else {
|
|
clearSearch(cm);
|
|
var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
|
|
var advance = function() {
|
|
var start = cursor.from(), match;
|
|
if (!(match = cursor.findNext())) {
|
|
cursor = getSearchCursor(cm, query);
|
|
if (!(match = cursor.findNext()) ||
|
|
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
|
|
}
|
|
cm.setSelection(cursor.from(), cursor.to());
|
|
- cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
|
|
- confirmDialog(cm, doReplaceConfirm, "Replace?",
|
|
+ cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
|
|
+
|
|
+ let replaceConfirmFragment = doc.createDocumentFragment();
|
|
+
|
|
+ let replaceConfirmLabel = searchLabel.cloneNode(false);
|
|
+ replaceConfirmLabel.textContent = "Replace?";
|
|
+ replaceConfirmFragment.appendChild(replaceConfirmLabel);
|
|
+
|
|
+ let yesButton = doc.createElement("button");
|
|
+ yesButton.textContent = "Yes";
|
|
+ replaceConfirmFragment.appendChild(yesButton);
|
|
+
|
|
+ let noButton = doc.createElement("button");
|
|
+ noButton.textContent = "No";
|
|
+ replaceConfirmFragment.appendChild(noButton);
|
|
+
|
|
+ let allButton = doc.createElement("button");
|
|
+ allButton.textContent = "All";
|
|
+ replaceConfirmFragment.appendChild(allButton);
|
|
+
|
|
+ let stopButton = doc.createElement("button");
|
|
+ stopButton.textContent = "Stop";
|
|
+ replaceConfirmFragment.appendChild(stopButton);
|
|
+
|
|
+ confirmDialog(cm, replaceConfirmFragment, "Replace?",
|
|
[function() {doReplace(match);}, advance,
|
|
function() {replaceAll(cm, query, text)}]);
|
|
};
|
|
var doReplace = function(match) {
|
|
cursor.replace(typeof query == "string" ? text :
|
|
text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
|
|
advance();
|
|
};
|
|
|
|
## Other patches
|
|
|
|
```diff
|
|
diff --git a/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js b/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js
|
|
--- a/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js
|
|
+++ b/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js
|
|
@@ -4144,23 +4144,41 @@
|
|
}
|
|
function showConfirm(cm, text) {
|
|
if (cm.openNotification) {
|
|
- cm.openNotification('<span style="color: red">' + text + '</span>',
|
|
+ cm.openNotification('<span class="cm5-vim-notification-error">' + text + '</span>',
|
|
{bottom: true, duration: 5000});
|
|
} else {
|
|
alert(text);
|
|
}
|
|
}
|
|
- function makePrompt(prefix, desc) {
|
|
- var raw = '<span style="font-family: monospace; white-space: pre">' +
|
|
- (prefix || "") + '<input type="text"></span>';
|
|
- if (desc)
|
|
- raw += ' <span style="color: #888">' + desc + '</span>';
|
|
- return raw;
|
|
+ function makePrompt(cm, prefix, desc) {
|
|
+ const doc = cm.getWrapperElement().ownerDocument;
|
|
+ const fragment = doc.createDocumentFragment();
|
|
+ const promptEl = doc.createElement("span");
|
|
+ promptEl.classList.add("cm5-vim-prompt");
|
|
+
|
|
+ let inputParent = promptEl;
|
|
+ if (prefix) {
|
|
+ const labelEl = doc.createElement("label");
|
|
+ labelEl.append(doc.createTextNode(prefix));
|
|
+ promptEl.append(labelEl);
|
|
+ inputParent = labelEl;
|
|
+ }
|
|
+ const inputEl = doc.createElement("input");
|
|
+ inputParent.append(inputEl);
|
|
+ fragment.append(promptEl);
|
|
+
|
|
+ if (desc) {
|
|
+ const descriptionEl = doc.createElement("span");
|
|
+ descriptionEl.classList.add("cm5-vim-prompt-description");
|
|
+ descriptionEl.append(doc.createTextNode(desc));
|
|
+ fragment.append(descriptionEl);
|
|
+ }
|
|
+ return fragment;
|
|
}
|
|
var searchPromptDesc = '(Javascript regexp)';
|
|
function showPrompt(cm, options) {
|
|
var shortText = (options.prefix || '') + ' ' + (options.desc || '');
|
|
- var prompt = makePrompt(options.prefix, options.desc);
|
|
+ var prompt = makePrompt(cm, options.prefix, options.desc);
|
|
dialog(cm, prompt, shortText, options.onClose, options);
|
|
}
|
|
function regexEqual(r1, r2) {
|
|
```
|
|
|
|
# Footnotes
|
|
|
|
[1] http://codemirror.net
|
|
[2] devtools/client/shared/sourceeditor/codemirror
|
|
[3] devtools/client/shared/sourceeditor/test/browser_codemirror.js
|
|
[4] devtools/client/jar.mn
|
|
[5] devtools/client/shared/sourceeditor/editor.js
|