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
