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)'; + var queryDialog; 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) { + if (!queryDialog) { + let doc = cm.getWrapperElement().ownerDocument; + let inp = doc.createElement("input"); + + inp.type = "search"; + inp.placeholder = cm.l10n("findCmd.promptMessage"); + inp.style.marginInlineStart = "1em"; + inp.style.marginInlineEnd = "1em"; + inp.style.flexGrow = "1"; + inp.addEventListener("focus", () => inp.select()); + + queryDialog = doc.createElement("div"); + queryDialog.appendChild(inp); + queryDialog.style.display = "flex"; + } + 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.setAttribute("style", "width: 10em"); + searchField.classList.add("CodeMirror-search-field"); + replaceQueryFragment.appendChild(searchField); + + let searchHint = doc.createElement("span"); + searchHint.setAttribute("style", "color: #888"); + searchHint.classList.add("CodeMirror-search-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.setAttribute("style", "width: 10em"); + replaceField.classList.add("CodeMirror-search-field"); + 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(); }; # 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