README (13416B)
1 This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror 2 is a JavaScript component that provides a code editor in the browser. When 3 a mode is available for the language you are coding in, it will color your 4 code, and optionally help with indentation. 5 6 # CodeMirror 6 7 8 We're currently migrating to CodeMirror 6, which means we have bundle for version 6 _and_ 5, 9 until we successfully migrated all consumers to CodeMirror 6. 10 11 For version 6, we're generating a bundle (codemirror6/codemirror6.bundle.mjs) using rollup. 12 The entry point for the bundle is codemirror6/index.mjs, where we export all the classes 13 and functions that the editor needs. When adding new exported item, the bundle needs to 14 be updated, which can be done by running: 15 > cd devtools/client/shared/sourceeditor 16 > npm install 17 > npm run build-cm6 18 19 This will produced a minified bundle, which might not be ideal if you're debugging an issue or profiling. 20 You can get an unminified bundle by running: 21 > npm run build-cm6-unminified 22 23 The generated bundle can be configurated in rollup.config.mjs. 24 25 # CodeMirror 5 26 27 ## CodeMirror 5 Upgrade 28 29 Currently used version is 5.58.1. To upgrade: download a new version of 30 CodeMirror from the project's page [1] and replace all JavaScript and 31 CSS files inside the codemirror directory [2]. 32 33 Then to recreate codemirror.bundle.js: 34 > cd devtools/client/shared/sourceeditor 35 > npm install 36 > npm run build 37 38 When investigating an issue in CodeMirror, you might want to have a non-minified bundle. 39 You can do this by running `npm run build-unminified` instead of `npm run build`. 40 41 To confirm the functionality run mochitests for the following components: 42 43 * sourceeditor 44 * debugger 45 * styleditor 46 * netmonitor 47 * webconsole 48 49 The sourceeditor component contains imported CodeMirror tests [3]. 50 51 * Some tests were commented out because we don't use that functionality 52 within Firefox (for example Ruby editing mode). Be careful when updating 53 files test/codemirror.html and test/vimemacs.html; they were modified to 54 co-exist with Mozilla's testing infrastructure. Basically, vimemacs.html 55 is a copy of codemirror.html but only with VIM and Emacs mode tests 56 enabled. 57 * In cm_comment_test.js comment out fallbackToBlock and fallbackToLine 58 tests. 59 * The search addon (search.js) was slightly modified to make search 60 UI localizable (see patch below). 61 62 Other than that, we don't have any Mozilla-specific patches applied to 63 CodeMirror itself. 64 65 ## Addons 66 67 To install a new CodeMirror 5 addon add it to the codemirror directory, 68 jar.mn [4] file and editor.js [5]. Also, add it to the License section 69 below. 70 71 ## License 72 73 The following files in this directory and devtools/client/shared/sourceeditor/test/codemirror/ 74 are licensed according to the contents in the LICENSE file. 75 76 ## Localization patches 77 78 diff --git a/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js b/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js 79 --- a/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js 80 +++ b/devtools/client/shared/sourceeditor/codemirror/addon/search/search.js 81 @@ -93,32 +93,47 @@ 82 } else { 83 query = parseString(query) 84 } 85 if (typeof query == "string" ? query == "" : query.test("")) 86 query = /x^/; 87 return query; 88 } 89 90 - var queryDialog = 91 - '<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>'; 92 93 function startSearch(cm, state, query) { 94 state.queryText = query; 95 state.query = parseQuery(query); 96 cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query)); 97 state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query)); 98 cm.addOverlay(state.overlay); 99 if (cm.showMatchesOnScrollbar) { 100 if (state.annotate) { state.annotate.clear(); state.annotate = null; } 101 state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query)); 102 } 103 } 104 105 function doSearch(cm, rev, persistent, immediate) { 106 + // We used to only build this input the first time the search was triggered and 107 + // reuse it again on subsequent search. 108 + // Unfortunately, this doesn't play well with the `persistent` parameter; 109 + // new event listeners are added to the input each time `persistentDialog` is called, 110 + // which would make a single `Enter` key trigger multiple "findNext" actions, making 111 + // it look like the search would skip some results. 112 + const doc = cm.getWrapperElement().ownerDocument; 113 + const inp = doc.createElement("input"); 114 + 115 + inp.type = "search"; 116 + inp.classList.add("cm5-search-input"); 117 + inp.placeholder = cm.l10n("findCmd.promptMessage"); 118 + inp.addEventListener("focus", () => inp.select()); 119 + 120 + const queryDialog = doc.createElement("div"); 121 + queryDialog.classList.add("cm5-search-container"); 122 + queryDialog.appendChild(inp); 123 124 var state = getSearchState(cm); 125 if (state.query) return findNext(cm, rev); 126 var q = cm.getSelection() || state.lastQuery; 127 if (q instanceof RegExp && q.source == "x^") q = null 128 if (persistent && cm.openDialog) { 129 var hiding = null 130 var searchNext = function(query, event) { 131 CodeMirror.e_stop(event); 132 @@ -181,56 +196,110 @@ 133 var state = getSearchState(cm); 134 state.lastQuery = state.query; 135 if (!state.query) return; 136 state.query = state.queryText = null; 137 cm.removeOverlay(state.overlay); 138 if (state.annotate) { state.annotate.clear(); state.annotate = null; } 139 });} 140 141 - var replaceQueryDialog = 142 - ' <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>'; 143 - var replacementQueryDialog = '<span class="CodeMirror-search-label">With:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>'; 144 - var doReplaceConfirm = '<span class="CodeMirror-search-label">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>'; 145 - 146 function replaceAll(cm, query, text) { 147 cm.operation(function() { 148 for (var cursor = getSearchCursor(cm, query); cursor.findNext();) { 149 if (typeof query != "string") { 150 var match = cm.getRange(cursor.from(), cursor.to()).match(query); 151 cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];})); 152 } else cursor.replace(text); 153 } 154 }); 155 } 156 157 function replace(cm, all) { 158 if (cm.getOption("readOnly")) return; 159 var query = cm.getSelection() || getSearchState(cm).lastQuery; 160 - var dialogText = '<span class="CodeMirror-search-label">' + (all ? 'Replace all:' : 'Replace:') + '</span>'; 161 - dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) { 162 + 163 + let doc = cm.getWrapperElement().ownerDocument; 164 + 165 + // `searchLabel` is used as part of `replaceQueryFragment` and as a separate 166 + // argument by itself, so it should be cloned. 167 + let searchLabel = doc.createElement("span"); 168 + searchLabel.classList.add("CodeMirror-search-label"); 169 + searchLabel.textContent = all ? "Replace all:" : "Replace:"; 170 + 171 + let replaceQueryFragment = doc.createDocumentFragment(); 172 + replaceQueryFragment.appendChild(searchLabel.cloneNode(true)); 173 + 174 + let searchField = doc.createElement("input"); 175 + searchField.setAttribute("type", "text"); 176 + searchField.classList.add("cm5-search-replace-input"); 177 + replaceQueryFragment.appendChild(searchField); 178 + 179 + let searchHint = doc.createElement("span"); 180 + searchHint.classList.add("cm5-search-replace-hint"); 181 + searchHint.textContent = "(Use /re/ syntax for regexp search)"; 182 + replaceQueryFragment.appendChild(searchHint); 183 + 184 + dialog(cm, replaceQueryFragment, searchLabel, query, function(query) { 185 if (!query) return; 186 query = parseQuery(query); 187 - dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) { 188 + 189 + let replacementQueryFragment = doc.createDocumentFragment(); 190 + 191 + let replaceWithLabel = searchLabel.cloneNode(false); 192 + replaceWithLabel.textContent = "With:"; 193 + replacementQueryFragment.appendChild(replaceWithLabel); 194 + 195 + let replaceField = doc.createElement("input"); 196 + replaceField.setAttribute("type", "text"); 197 + replaceField.classList.add("cm5-search-replace-input"); 198 + replacementQueryFragment.appendChild(replaceField); 199 + 200 + dialog(cm, replacementQueryFragment, "Replace with:", "", function(text) { 201 text = parseString(text) 202 if (all) { 203 replaceAll(cm, query, text) 204 } else { 205 clearSearch(cm); 206 var cursor = getSearchCursor(cm, query, cm.getCursor("from")); 207 var advance = function() { 208 var start = cursor.from(), match; 209 if (!(match = cursor.findNext())) { 210 cursor = getSearchCursor(cm, query); 211 if (!(match = cursor.findNext()) || 212 (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return; 213 } 214 cm.setSelection(cursor.from(), cursor.to()); 215 - cm.scrollIntoView({from: cursor.from(), to: cursor.to()}); 216 - confirmDialog(cm, doReplaceConfirm, "Replace?", 217 + cm.scrollIntoView({ from: cursor.from(), to: cursor.to() }); 218 + 219 + let replaceConfirmFragment = doc.createDocumentFragment(); 220 + 221 + let replaceConfirmLabel = searchLabel.cloneNode(false); 222 + replaceConfirmLabel.textContent = "Replace?"; 223 + replaceConfirmFragment.appendChild(replaceConfirmLabel); 224 + 225 + let yesButton = doc.createElement("button"); 226 + yesButton.textContent = "Yes"; 227 + replaceConfirmFragment.appendChild(yesButton); 228 + 229 + let noButton = doc.createElement("button"); 230 + noButton.textContent = "No"; 231 + replaceConfirmFragment.appendChild(noButton); 232 + 233 + let allButton = doc.createElement("button"); 234 + allButton.textContent = "All"; 235 + replaceConfirmFragment.appendChild(allButton); 236 + 237 + let stopButton = doc.createElement("button"); 238 + stopButton.textContent = "Stop"; 239 + replaceConfirmFragment.appendChild(stopButton); 240 + 241 + confirmDialog(cm, replaceConfirmFragment, "Replace?", 242 [function() {doReplace(match);}, advance, 243 function() {replaceAll(cm, query, text)}]); 244 }; 245 var doReplace = function(match) { 246 cursor.replace(typeof query == "string" ? text : 247 text.replace(/\$(\d)/g, function(_, i) {return match[i];})); 248 advance(); 249 }; 250 251 ## Other patches 252 253 ```diff 254 diff --git a/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js b/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js 255 --- a/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js 256 +++ b/devtools/client/shared/sourceeditor/codemirror/keymap/vim.js 257 @@ -4144,23 +4144,41 @@ 258 } 259 function showConfirm(cm, text) { 260 if (cm.openNotification) { 261 - cm.openNotification('<span style="color: red">' + text + '</span>', 262 + cm.openNotification('<span class="cm5-vim-notification-error">' + text + '</span>', 263 {bottom: true, duration: 5000}); 264 } else { 265 alert(text); 266 } 267 } 268 - function makePrompt(prefix, desc) { 269 - var raw = '<span style="font-family: monospace; white-space: pre">' + 270 - (prefix || "") + '<input type="text"></span>'; 271 - if (desc) 272 - raw += ' <span style="color: #888">' + desc + '</span>'; 273 - return raw; 274 + function makePrompt(cm, prefix, desc) { 275 + const doc = cm.getWrapperElement().ownerDocument; 276 + const fragment = doc.createDocumentFragment(); 277 + const promptEl = doc.createElement("span"); 278 + promptEl.classList.add("cm5-vim-prompt"); 279 + 280 + let inputParent = promptEl; 281 + if (prefix) { 282 + const labelEl = doc.createElement("label"); 283 + labelEl.append(doc.createTextNode(prefix)); 284 + promptEl.append(labelEl); 285 + inputParent = labelEl; 286 + } 287 + const inputEl = doc.createElement("input"); 288 + inputParent.append(inputEl); 289 + fragment.append(promptEl); 290 + 291 + if (desc) { 292 + const descriptionEl = doc.createElement("span"); 293 + descriptionEl.classList.add("cm5-vim-prompt-description"); 294 + descriptionEl.append(doc.createTextNode(desc)); 295 + fragment.append(descriptionEl); 296 + } 297 + return fragment; 298 } 299 var searchPromptDesc = '(Javascript regexp)'; 300 function showPrompt(cm, options) { 301 var shortText = (options.prefix || '') + ' ' + (options.desc || ''); 302 - var prompt = makePrompt(options.prefix, options.desc); 303 + var prompt = makePrompt(cm, options.prefix, options.desc); 304 dialog(cm, prompt, shortText, options.onClose, options); 305 } 306 function regexEqual(r1, r2) { 307 ``` 308 309 # Footnotes 310 311 [1] http://codemirror.net 312 [2] devtools/client/shared/sourceeditor/codemirror 313 [3] devtools/client/shared/sourceeditor/test/browser_codemirror.js 314 [4] devtools/client/jar.mn 315 [5] devtools/client/shared/sourceeditor/editor.js