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 = - 'Search: (Use /re/ syntax for regexp search)'; 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 = - ' (Use /re/ syntax for regexp search)'; - var replacementQueryDialog = 'With: '; - var doReplaceConfirm = 'Replace? '; - 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 = '' + (all ? 'Replace all:' : 'Replace:') + ''; - 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('' + text + '', + cm.openNotification('' + text + '', {bottom: true, duration: 5000}); } else { alert(text); } } - function makePrompt(prefix, desc) { - var raw = '' + - (prefix || "") + ''; - if (desc) - raw += ' ' + desc + ''; - 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