315 lines
13 KiB
Text
315 lines
13 KiB
Text
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
|