3a3aadf25da307bc0889c25178336bc9d7a4a1aa
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CodeMirrorAdditions.js
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 (function () {
27     // By default CodeMirror defines syntax highlighting styles based on token
28     // only and shared styles between modes. This limiting and does not match
29     // what we have done in the Web Inspector. So this modifies the XML, CSS
30     // and JavaScript modes to supply two styles for each token. One for the
31     // token and one with the mode name.
32
33     function tokenizeLinkString(stream, state)
34     {
35         console.assert(state._linkQuoteCharacter !== undefined);
36
37         // Eat the string until the same quote is found that started the string.
38         // If this is unquoted, then eat until whitespace or common parse errors.
39         if (state._linkQuoteCharacter)
40             stream.eatWhile(new RegExp("[^" + state._linkQuoteCharacter + "]"));
41         else
42             stream.eatWhile(/[^\s\u00a0=<>\"\']/);
43
44         // If the stream isn't at the end of line then we found the end quote.
45         // In the case, change _linkTokenize to parse the end of the link next.
46         // Otherwise _linkTokenize will stay as-is to parse more of the link.
47         if (!stream.eol())
48             state._linkTokenize = tokenizeEndOfLinkString;
49
50         return "link";
51     }
52
53     function tokenizeEndOfLinkString(stream, state)
54     {
55         console.assert(state._linkQuoteCharacter !== undefined);
56         console.assert(state._linkBaseStyle);
57
58         // Eat the quote character to style it with the base style.
59         if (state._linkQuoteCharacter)
60             stream.eat(state._linkQuoteCharacter);
61
62         var style = state._linkBaseStyle;
63
64         // Clean up the state.
65         delete state._linkTokenize;
66         delete state._linkQuoteCharacter;
67         delete state._linkBaseStyle;
68         delete state._srcSetTokenizeState;
69
70         return style;
71     }
72
73     function tokenizeSrcSetString(stream, state)
74     {
75         console.assert(state._linkQuoteCharacter !== undefined);
76
77         if (state._srcSetTokenizeState === "link") {
78             // Eat the string until a space, comma, or ending quote.
79             // If this is unquoted, then eat until whitespace or common parse errors.
80             if (state._linkQuoteCharacter)
81                 stream.eatWhile(new RegExp("[^\\s," + state._linkQuoteCharacter + "]"));
82             else
83                 stream.eatWhile(/[^\s,\u00a0=<>\"\']/);
84         } else {
85             // Eat the string until a comma, or ending quote.
86             // If this is unquoted, then eat until whitespace or common parse errors.
87             stream.eatSpace();
88             if (state._linkQuoteCharacter)
89                 stream.eatWhile(new RegExp("[^," + state._linkQuoteCharacter + "]"));
90             else
91                 stream.eatWhile(/[^\s\u00a0=<>\"\']/);
92             stream.eatWhile(/[\s,]/);
93         }
94
95         // If the stream isn't at the end of line and we found the end quote
96         // change _linkTokenize to parse the end of the link next. Otherwise
97         // _linkTokenize will stay as-is to parse more of the srcset.
98         if (stream.eol() || (!state._linkQuoteCharacter || stream.peek() === state._linkQuoteCharacter))
99             state._linkTokenize = tokenizeEndOfLinkString;
100
101         // Link portion.
102         if (state._srcSetTokenizeState === "link") {
103             state._srcSetTokenizeState = "descriptor";
104             return "link";
105         }
106
107         // Descriptor portion.
108         state._srcSetTokenizeState = "link";
109         return state._linkBaseStyle;
110     }
111
112     function extendedXMLToken(stream, state)
113     {
114         if (state._linkTokenize) {
115             // Call the link tokenizer instead.
116             var style = state._linkTokenize(stream, state);
117             return style && (style + " m-" + this.name);
118         }
119
120         // Remember the start position so we can rewind if needed.
121         var startPosition = stream.pos;
122         var style = this._token(stream, state);
123         if (style === "attribute") {
124             // Look for "href" or "src" attributes. If found then we should
125             // expect a string later that should get the "link" style instead.
126             var text = stream.current().toLowerCase();
127             if (text === "src" || /\bhref\b/.test(text))
128                 state._expectLink = true;
129             else if (text === "srcset")
130                 state._expectSrcSet = true;
131             else {
132                 delete state._expectLink;
133                 delete state._expectSrcSet;
134             }
135         } else if (state._expectLink && style === "string") {
136             var current = stream.current();
137
138             // Unless current token is empty quotes, consume quote character
139             // and tokenize link next.
140             if (current !== "\"\"" && current !== "''") {
141                 delete state._expectLink;
142
143                 // This is a link, so setup the state to process it next.
144                 state._linkTokenize = tokenizeLinkString;
145                 state._linkBaseStyle = style;
146
147                 // The attribute may or may not be quoted.
148                 var quote = current[0];
149
150                 state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
151
152                 // Rewind the stream to the start of this token.
153                 stream.pos = startPosition;
154
155                 // Eat the open quote of the string so the string style
156                 // will be used for the quote character.
157                 if (state._linkQuoteCharacter)
158                     stream.eat(state._linkQuoteCharacter);
159             }
160         } else if (state._expectSrcSet && style === "string") {
161             var current = stream.current();
162
163             // Unless current token is empty quotes, consume quote character
164             // and tokenize link next.
165             if (current !== "\"\"" && current !== "''") {
166                 delete state._expectSrcSet;
167
168                 // This is a link, so setup the state to process it next.
169                 state._srcSetTokenizeState = "link";
170                 state._linkTokenize = tokenizeSrcSetString;
171                 state._linkBaseStyle = style;
172
173                 // The attribute may or may not be quoted.
174                 var quote = current[0];
175
176                 state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
177
178                 // Rewind the stream to the start of this token.
179                 stream.pos = startPosition;
180
181                 // Eat the open quote of the string so the string style
182                 // will be used for the quote character.
183                 if (state._linkQuoteCharacter)
184                     stream.eat(state._linkQuoteCharacter);
185             }
186         } else if (style) {
187             // We don't expect other tokens between attribute and string since
188             // spaces and the equal character are not tokenized. So if we get
189             // another token before a string then we stop expecting a link.
190             delete state._expectLink;
191             delete state._expectSrcSet;
192         }
193
194         return style && (style + " m-" + this.name);
195     }
196
197     function tokenizeCSSURLString(stream, state)
198     {
199         console.assert(state._urlQuoteCharacter);
200
201         // If we are an unquoted url string, return whitespace blocks as a whitespace token (null).
202         if (state._unquotedURLString && stream.eatSpace())
203             return null;
204
205         var ch = null;
206         var escaped = false;
207         var reachedEndOfURL = false;
208         var lastNonWhitespace = stream.pos;
209         var quote = state._urlQuoteCharacter;
210
211         // Parse characters until the end of the stream/line or a proper end quote character.
212         while ((ch = stream.next()) != null) {
213             if (ch === quote && !escaped) {
214                 reachedEndOfURL = true;
215                 break;
216             }
217             escaped = !escaped && ch === "\\";
218             if (!/[\s\u00a0]/.test(ch))
219                 lastNonWhitespace = stream.pos;
220         }
221
222         // If we are an unquoted url string, do not include trailing whitespace, rewind to the last real character.
223         if (state._unquotedURLString)
224             stream.pos = lastNonWhitespace;
225
226         // If we have reached the proper the end of the url string, switch to the end tokenizer to reset the state.
227         if (reachedEndOfURL) {
228             if (!state._unquotedURLString)
229                 stream.backUp(1);
230             this._urlTokenize = tokenizeEndOfCSSURLString;
231         }
232
233         return "link";
234     }
235
236     function tokenizeEndOfCSSURLString(stream, state)
237     {
238         console.assert(state._urlQuoteCharacter);
239         console.assert(state._urlBaseStyle);
240
241         // Eat the quote character to style it with the base style.
242         if (!state._unquotedURLString)
243             stream.eat(state._urlQuoteCharacter);
244
245         var style = state._urlBaseStyle;
246
247         delete state._urlTokenize;
248         delete state._urlQuoteCharacter;
249         delete state._urlBaseStyle;
250
251         return style;
252     }
253
254     function extendedCSSToken(stream, state)
255     {
256         var hexColorRegex = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
257
258         if (state._urlTokenize) {
259             // Call the link tokenizer instead.
260             var style = state._urlTokenize(stream, state);
261             return style && (style + " m-" + (this.alternateName || this.name));
262         }
263
264         // Remember the start position so we can rewind if needed.
265         var startPosition = stream.pos;
266         var style = this._token(stream, state);
267
268         if (style) {
269             if (style === "atom") {
270                 if (stream.current() === "url") {
271                     // If the current text is "url" then we should expect the next string token to be a link.
272                     state._expectLink = true;
273                 } else if (hexColorRegex.test(stream.current()))
274                     style = style + " hex-color";
275             } else if (style === "error") {
276                 if (state.state=== "atBlock" || state.state === "atBlock_parens") {
277                     switch (stream.current()) {
278                     case "prefers-color-scheme":
279                     case     "light":
280                     case     "dark":
281                     case "prefers-reduced-motion":
282                     case     "reduce":
283                     case     "no-preference":
284                     case "inverted-colors":
285                     case     "inverted":
286                     case "color-gamut":
287                     case     "p3":
288                     case     "rec2020":
289                     case "display-mode":
290                     case     "fullscreen":
291                     case     "standalone":
292                     case     "minimal-ui":
293                     case     "browser":
294                     case /*-webkit-*/"video-playable-inline":
295                     case /*-webkit-*/"transform-2d":
296                     case /*-webkit-*/"transform-3d":
297                         style = "property";
298                         break;
299                     }
300                 }
301             } else if (state._expectLink) {
302                 delete state._expectLink;
303
304                 if (style === "string") {
305                     // This is a link, so setup the state to process it next.
306                     state._urlTokenize = tokenizeCSSURLString;
307                     state._urlBaseStyle = style;
308
309                     // The url may or may not be quoted.
310                     var quote = stream.current()[0];
311                     state._urlQuoteCharacter = quote === "'" || quote === "\"" ? quote : ")";
312                     state._unquotedURLString = state._urlQuoteCharacter === ")";
313
314                     // Rewind the stream to the start of this token.
315                     stream.pos = startPosition;
316
317                     // Eat the open quote of the string so the string style
318                     // will be used for the quote character.
319                     if (!state._unquotedURLString)
320                         stream.eat(state._urlQuoteCharacter);
321                 }
322             }
323         }
324
325         return style && (style + " m-" + (this.alternateName || this.name));
326     }
327
328     function extendedJavaScriptToken(stream, state)
329     {
330         // CodeMirror moves the original token function to _token when we extended it.
331         // So call it to get the style that we will add an additional class name to.
332         var style = this._token(stream, state);
333
334         if (style === "number" && stream.current().endsWith("n"))
335             style += " bigint";
336
337         return style && (style + " m-" + (this.alternateName || this.name));
338     }
339
340     function scrollCursorIntoView(codeMirror, event)
341     {
342         // We don't want to use the default implementation since it can cause massive jumping
343         // when the editor is contained inside overflow elements.
344         event.preventDefault();
345
346         function delayedWork()
347         {
348             // Don't try to scroll unless the editor is focused.
349             if (!codeMirror.getWrapperElement().classList.contains("CodeMirror-focused"))
350                 return;
351
352             // The cursor element can contain multiple cursors. The first one is the blinky cursor,
353             // which is the one we want to scroll into view. It can be missing, so check first.
354             var cursorElement = codeMirror.getScrollerElement().getElementsByClassName("CodeMirror-cursor")[0];
355             if (cursorElement)
356                 cursorElement.scrollIntoViewIfNeeded(false);
357         }
358
359         // We need to delay this because CodeMirror can fire scrollCursorIntoView as a view is being blurred
360         // and another is being focused. The blurred editor still has the focused state when this event fires.
361         // We don't want to scroll the blurred editor into view, only the focused editor.
362         setTimeout(delayedWork, 0);
363     }
364
365     CodeMirror.extendMode("css", {token: extendedCSSToken});
366     CodeMirror.extendMode("xml", {token: extendedXMLToken});
367     CodeMirror.extendMode("javascript", {token: extendedJavaScriptToken});
368
369     CodeMirror.defineInitHook(function(codeMirror) {
370         codeMirror.on("scrollCursorIntoView", scrollCursorIntoView);
371     });
372
373     let whitespaceStyleElement = null;
374     let whitespaceCountsWithStyling = new Set;
375     CodeMirror.defineOption("showWhitespaceCharacters", false, function(cm, value, old) {
376         if (!value || (old && old !== CodeMirror.Init)) {
377             cm.removeOverlay("whitespace");
378             return;
379         }
380
381         cm.addOverlay({
382             name: "whitespace",
383             token(stream) {
384                 if (stream.peek() === " ") {
385                     let count = 0;
386                     while (stream.peek() === " ") {
387                         ++count;
388                         stream.next();
389                     }
390
391                     if (!whitespaceCountsWithStyling.has(count)) {
392                         whitespaceCountsWithStyling.add(count);
393
394                         if (!whitespaceStyleElement)
395                             whitespaceStyleElement = document.head.appendChild(document.createElement("style"));
396
397                         const middleDot = "\\00B7";
398
399                         let styleText = whitespaceStyleElement.textContent;
400                         styleText += `.show-whitespace-characters .CodeMirror .cm-whitespace-${count}::before {`;
401                         styleText += `content: "${middleDot.repeat(count)}";`;
402                         styleText += `}`;
403
404                         whitespaceStyleElement.textContent = styleText;
405                     }
406
407                     return `whitespace whitespace-${count}`;
408                 }
409
410                 while (!stream.eol() && stream.peek() !== " ")
411                     stream.next();
412
413                 return null;
414             }
415         });
416     });
417
418     CodeMirror.defineExtension("hasLineClass", function(line, where, className) {
419         // This matches the arguments to addLineClass and removeLineClass.
420         var classProperty = where === "text" ? "textClass" : (where === "background" ? "bgClass" : "wrapClass");
421         var lineInfo = this.lineInfo(line);
422         if (!lineInfo)
423             return false;
424
425         if (!lineInfo[classProperty])
426             return false;
427
428         // Test for the simple case.
429         if (lineInfo[classProperty] === className)
430             return true;
431
432         // Do a quick check for the substring. This is faster than a regex, which requires escaping the input first.
433         var index = lineInfo[classProperty].indexOf(className);
434         if (index === -1)
435             return false;
436
437         // Check that it is surrounded by spaces. Add padding spaces first to work with beginning and end of string cases.
438         var paddedClass = " " + lineInfo[classProperty] + " ";
439         return paddedClass.indexOf(" " + className + " ", index) !== -1;
440     });
441
442     CodeMirror.defineExtension("setUniqueBookmark", function(position, options) {
443         var marks = this.findMarksAt(position);
444         for (var i = 0; i < marks.length; ++i) {
445             if (marks[i].__uniqueBookmark) {
446                 marks[i].clear();
447                 break;
448             }
449         }
450
451         var uniqueBookmark = this.setBookmark(position, options);
452         uniqueBookmark.__uniqueBookmark = true;
453         return uniqueBookmark;
454     });
455
456     CodeMirror.defineExtension("toggleLineClass", function(line, where, className) {
457         if (this.hasLineClass(line, where, className)) {
458             this.removeLineClass(line, where, className);
459             return false;
460         }
461
462         this.addLineClass(line, where, className);
463         return true;
464     });
465
466     CodeMirror.defineExtension("alterNumberInRange", function(amount, startPosition, endPosition, updateSelection) {
467         // We don't try if the range is multiline, pass to another key handler.
468         if (startPosition.line !== endPosition.line)
469             return false;
470
471         if (updateSelection) {
472             // Remember the cursor position/selection.
473             var selectionStart = this.getCursor("start");
474             var selectionEnd = this.getCursor("end");
475         }
476
477         var line = this.getLine(startPosition.line);
478
479         var foundPeriod = false;
480
481         var start = NaN;
482         var end = NaN;
483
484         for (var i = startPosition.ch; i >= 0; --i) {
485             var character = line.charAt(i);
486
487             if (character === ".") {
488                 if (foundPeriod)
489                     break;
490                 foundPeriod = true;
491             } else if (character !== "-" && character !== "+" && isNaN(parseInt(character))) {
492                 // Found the end already, just scan backwards.
493                 if (i === startPosition.ch) {
494                     end = i;
495                     continue;
496                 }
497
498                 break;
499             }
500
501             start = i;
502         }
503
504         if (isNaN(end)) {
505             for (var i = startPosition.ch + 1; i < line.length; ++i) {
506                 var character = line.charAt(i);
507
508                 if (character === ".") {
509                     if (foundPeriod) {
510                         end = i;
511                         break;
512                     }
513
514                     foundPeriod = true;
515                 } else if (isNaN(parseInt(character))) {
516                     end = i;
517                     break;
518                 }
519
520                 end = i + 1;
521             }
522         }
523
524         // No number range found, pass to another key handler.
525         if (isNaN(start) || isNaN(end))
526             return false;
527
528         var number = parseFloat(line.substring(start, end));
529
530         // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
531         // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
532         var alteredNumber = Number((number + amount).toFixed(6));
533         var alteredNumberString = alteredNumber.toString();
534
535         var from = {line: startPosition.line, ch: start};
536         var to = {line: startPosition.line, ch: end};
537
538         this.replaceRange(alteredNumberString, from, to);
539
540         if (updateSelection) {
541             var previousLength = to.ch - from.ch;
542             var newLength = alteredNumberString.length;
543
544             // Fix up the selection so it follows the increase or decrease in the replacement length.
545             // selectionStart/End may the same object if there is no selection. If that is the case
546             // make only one modification to prevent a double adjustment, and keep it a single object
547             // to avoid CodeMirror inadvertently creating an actual selection range.
548             let diff = newLength - previousLength;
549             if (selectionStart === selectionEnd)
550                 selectionStart.ch += diff;
551             else {
552                 if (selectionStart.ch > from.ch)
553                     selectionStart.ch += diff;
554                 if (selectionEnd.ch > from.ch)
555                     selectionEnd.ch += diff;
556             }
557
558             this.setSelection(selectionStart, selectionEnd);
559         }
560
561         return true;
562     });
563
564     function alterNumber(amount, codeMirror)
565     {
566         function findNumberToken(position)
567         {
568             // CodeMirror includes the unit in the number token, so searching for
569             // number tokens is the best way to get both the number and unit.
570             var token = codeMirror.getTokenAt(position);
571             if (token && token.type && /\bnumber\b/.test(token.type))
572                 return token;
573             return null;
574         }
575
576         var position = codeMirror.getCursor("head");
577         var token = findNumberToken(position);
578
579         if (!token) {
580             // If the cursor is at the outside beginning of the token, the previous
581             // findNumberToken wont find it. So check the next column for a number too.
582             position.ch += 1;
583             token = findNumberToken(position);
584         }
585
586         if (!token)
587             return CodeMirror.Pass;
588
589         var foundNumber = codeMirror.alterNumberInRange(amount, {ch: token.start, line: position.line}, {ch: token.end, line: position.line}, true);
590         if (!foundNumber)
591             return CodeMirror.Pass;
592     }
593
594     CodeMirror.defineExtension("rectsForRange", function(range) {
595         var lineRects = [];
596
597         for (var line = range.start.line; line <= range.end.line; ++line) {
598             var lineContent = this.getLine(line);
599
600             var startChar = line === range.start.line ? range.start.ch : (lineContent.length - lineContent.trimLeft().length);
601             var endChar = line === range.end.line ? range.end.ch : lineContent.length;
602             var firstCharCoords = this.cursorCoords({ch: startChar, line});
603             var endCharCoords = this.cursorCoords({ch: endChar, line});
604
605             // Handle line wrapping.
606             if (firstCharCoords.bottom !== endCharCoords.bottom) {
607                 var maxY = -Number.MAX_VALUE;
608                 for (var ch = startChar; ch <= endChar; ++ch) {
609                     var coords = this.cursorCoords({ch, line});
610                     if (coords.bottom > maxY) {
611                         if (ch > startChar) {
612                             var maxX = Math.ceil(this.cursorCoords({ch: ch - 1, line}).right);
613                             lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
614                         }
615                         var minX = Math.floor(coords.left);
616                         var minY = Math.floor(coords.top);
617                         maxY = Math.ceil(coords.bottom);
618                     }
619                 }
620                 maxX = Math.ceil(coords.right);
621                 lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
622             } else {
623                 var minX = Math.floor(firstCharCoords.left);
624                 var minY = Math.floor(firstCharCoords.top);
625                 var maxX = Math.ceil(endCharCoords.right);
626                 var maxY = Math.ceil(endCharCoords.bottom);
627                 lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
628             }
629         }
630         return lineRects;
631     });
632
633     let mac = WI.Platform.name === "mac";
634
635     CodeMirror.keyMap["default"] = {
636         "Alt-Up": alterNumber.bind(null, 1),
637         "Ctrl-Alt-Up": alterNumber.bind(null, 0.1),
638         "Shift-Alt-Up": alterNumber.bind(null, 10),
639         "Alt-PageUp": alterNumber.bind(null, 10),
640         "Shift-Alt-PageUp": alterNumber.bind(null, 100),
641         "Alt-Down": alterNumber.bind(null, -1),
642         "Ctrl-Alt-Down": alterNumber.bind(null, -0.1),
643         "Shift-Alt-Down": alterNumber.bind(null, -10),
644         "Alt-PageDown": alterNumber.bind(null, -10),
645         "Shift-Alt-PageDown": alterNumber.bind(null, -100),
646         "Cmd-/": "toggleComment",
647         "Cmd-D": "selectNextOccurrence",
648         "Shift-Tab": "indentLess",
649         fallthrough: mac ? "macDefault" : "pcDefault"
650     };
651
652     {
653         // CodeMirror's default behavior is to always insert a tab ("\t") regardless of `indentWithTabs`.
654         let original = CodeMirror.commands.insertTab;
655         CodeMirror.commands.insertTab = function(cm) {
656             if (cm.options.indentWithTabs)
657                 original(cm);
658             else
659                 CodeMirror.commands.insertSoftTab(cm);
660         };
661     }
662
663     // Register some extra MIME-types for CodeMirror. These are in addition to the
664     // ones CodeMirror already registers, like text/html, text/javascript, etc.
665     var extraXMLTypes = ["text/xml", "text/xsl"];
666     extraXMLTypes.forEach(function(type) {
667         CodeMirror.defineMIME(type, "xml");
668     });
669
670     var extraHTMLTypes = ["application/xhtml+xml", "image/svg+xml"];
671     extraHTMLTypes.forEach(function(type) {
672         CodeMirror.defineMIME(type, "htmlmixed");
673     });
674
675     var extraJavaScriptTypes = ["text/ecmascript", "application/javascript", "application/ecmascript", "application/x-javascript",
676         "text/x-javascript", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/jscript", "text/livescript"];
677     extraJavaScriptTypes.forEach(function(type) {
678         CodeMirror.defineMIME(type, "javascript");
679     });
680
681     var extraJSONTypes = ["application/x-json", "text/x-json", "application/vnd.api+json"];
682     extraJSONTypes.forEach(function(type) {
683         CodeMirror.defineMIME(type, {name: "javascript", json: true});
684     });
685
686     // FIXME: Add WHLSL specific modes.
687     CodeMirror.defineMIME("x-pipeline/x-compute", "x-shader/x-vertex");
688     CodeMirror.defineMIME("x-pipeline/x-fragment", "x-shader/x-fragment");
689     CodeMirror.defineMIME("x-pipeline/x-vertex", "x-shader/x-vertex");
690 })();
691
692 WI.compareCodeMirrorPositions = function(a, b)
693 {
694     var lineCompare = a.line - b.line;
695     if (lineCompare !== 0)
696         return lineCompare;
697
698     var aColumn = "ch" in a ? a.ch : Number.MAX_VALUE;
699     var bColumn = "ch" in b ? b.ch : Number.MAX_VALUE;
700     return aColumn - bColumn;
701 };
702
703 WI.walkTokens = function(cm, mode, initialPosition, callback)
704 {
705     let state = CodeMirror.copyState(mode, cm.getTokenAt(initialPosition).state);
706     if (state.localState)
707         state = state.localState;
708
709     let lineCount = cm.lineCount();
710     let abort = false;
711     for (let lineNumber = initialPosition.line; !abort && lineNumber < lineCount; ++lineNumber) {
712         let line = cm.getLine(lineNumber);
713         let stream = new CodeMirror.StringStream(line);
714         if (lineNumber === initialPosition.line)
715             stream.start = stream.pos = initialPosition.ch;
716
717         while (!stream.eol()) {
718             let tokenType = mode.token(stream, state);
719             if (!callback(tokenType, stream.current())) {
720                 abort = true;
721                 break;
722             }
723             stream.start = stream.pos;
724         }
725     }
726
727     if (!abort)
728         callback(null);
729 };
730
731 WI.tokenizeCSSValue = function(cssValue)
732 {
733     const rulePrefix = "*{X:";
734     let cssRule = rulePrefix + cssValue + "}";
735     let tokens = [];
736
737     let mode = CodeMirror.getMode({indentUnit: 0}, "text/css");
738     let state = CodeMirror.startState(mode);
739     let stream = new CodeMirror.StringStream(cssRule);
740
741     function processToken(token, tokenType, column) {
742         if (column < rulePrefix.length)
743             return;
744
745         if (token === "}" && !tokenType)
746             return;
747
748         tokens.push({value: token, type: tokenType});
749     }
750
751     while (!stream.eol()) {
752         let style = mode.token(stream, state);
753         let value = stream.current();
754         processToken(value, style, stream.start);
755         stream.start = stream.pos;
756     }
757
758     return tokens;
759 };