CodeMirrorAdditions.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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. (function () {
  26. // By default CodeMirror defines syntax highlighting styles based on token
  27. // only and shared styles between modes. This limiting and does not match
  28. // what we have done in the Web Inspector. So this modifies the XML, CSS
  29. // and JavaScript modes to supply two styles for each token. One for the
  30. // token and one with the mode name.
  31. function tokenizeLinkString(stream, state)
  32. {
  33. console.assert(state._linkQuoteCharacter !== undefined);
  34. // Eat the string until the same quote is found that started the string.
  35. // If this is unquoted, then eat until whitespace or common parse errors.
  36. if (state._linkQuoteCharacter)
  37. stream.eatWhile(new RegExp("[^" + state._linkQuoteCharacter + "]"));
  38. else
  39. stream.eatWhile(/[^\s\u00a0=<>\"\']/);
  40. // If the stream isn't at the end of line then we found the end quote.
  41. // In the case, change _linkTokenize to parse the end of the link next.
  42. // Otherwise _linkTokenize will stay as-is to parse more of the link.
  43. if (!stream.eol())
  44. state._linkTokenize = tokenizeEndOfLinkString;
  45. return "link";
  46. }
  47. function tokenizeEndOfLinkString(stream, state)
  48. {
  49. console.assert(state._linkQuoteCharacter !== undefined);
  50. console.assert(state._linkBaseStyle);
  51. // Eat the quote character to style it with the base style.
  52. if (state._linkQuoteCharacter)
  53. stream.eat(state._linkQuoteCharacter);
  54. var style = state._linkBaseStyle;
  55. // Clean up the state.
  56. delete state._linkTokenize;
  57. delete state._linkQuoteCharacter;
  58. delete state._linkBaseStyle;
  59. return style;
  60. }
  61. function extendedXMLToken(stream, state)
  62. {
  63. if (state._linkTokenize) {
  64. // Call the link tokenizer instead.
  65. var style = state._linkTokenize(stream, state);
  66. return style && (style + " m-" + this.name);
  67. }
  68. // Remember the start position so we can rewind if needed.
  69. var startPosition = stream.pos;
  70. var style = this._token(stream, state);
  71. if (style === "attribute") {
  72. // Look for "href" or "src" attributes. If found then we should
  73. // expect a string later that should get the "link" style instead.
  74. var text = stream.current().toLowerCase();
  75. if (text === "href" || text === "src")
  76. state._expectLink = true;
  77. else
  78. delete state._expectLink;
  79. } else if (state._expectLink && style === "string") {
  80. delete state._expectLink;
  81. // This is a link, so setup the state to process it next.
  82. state._linkTokenize = tokenizeLinkString;
  83. state._linkBaseStyle = style;
  84. // The attribute may or may not be quoted.
  85. var quote = stream.current()[0];
  86. state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
  87. // Rewind the steam to the start of this token.
  88. stream.pos = startPosition;
  89. // Eat the open quote of the string so the string style
  90. // will be used for the quote character.
  91. if (state._linkQuoteCharacter)
  92. stream.eat(state._linkQuoteCharacter);
  93. } else if (style) {
  94. // We don't expect other tokens between attribute and string since
  95. // spaces and the equal character are not tokenized. So if we get
  96. // another token before a string then we stop expecting a link.
  97. delete state._expectLink;
  98. }
  99. return style && (style + " m-" + this.name);
  100. }
  101. function tokenizeCSSURLString(stream, state)
  102. {
  103. console.assert(state._urlQuoteCharacter);
  104. // If we are an unquoted url string, return whitespace blocks as a whitespace token (null).
  105. if (state._unquotedURLString && stream.eatSpace())
  106. return null;
  107. var ch = null;
  108. var escaped = false;
  109. var reachedEndOfURL = false;
  110. var lastNonWhitespace = stream.pos;
  111. var quote = state._urlQuoteCharacter;
  112. // Parse characters until the end of the stream/line or a proper end quote character.
  113. while ((ch = stream.next()) != null) {
  114. if (ch == quote && !escaped) {
  115. reachedEndOfURL = true;
  116. break;
  117. }
  118. escaped = !escaped && ch === "\\";
  119. if (!/[\s\u00a0]/.test(ch))
  120. lastNonWhitespace = stream.pos;
  121. }
  122. // If we are an unquoted url string, do not include trailing whitespace, rewind to the last real character.
  123. if (state._unquotedURLString)
  124. stream.pos = lastNonWhitespace;
  125. // If we have reached the proper the end of the url string, switch to the end tokenizer to reset the state.
  126. if (reachedEndOfURL) {
  127. if (!state._unquotedURLString)
  128. stream.backUp(1);
  129. this._urlTokenize = tokenizeEndOfCSSURLString;
  130. }
  131. return "link";
  132. }
  133. function tokenizeEndOfCSSURLString(stream, state)
  134. {
  135. console.assert(state._urlQuoteCharacter);
  136. console.assert(state._urlBaseStyle);
  137. // Eat the quote character to style it with the base style.
  138. if (!state._unquotedURLString)
  139. stream.eat(state._urlQuoteCharacter);
  140. var style = state._urlBaseStyle;
  141. delete state._urlTokenize;
  142. delete state._urlQuoteCharacter;
  143. delete state._urlBaseStyle;
  144. return style;
  145. }
  146. function extendedCSSToken(stream, state)
  147. {
  148. if (state._urlTokenize) {
  149. // Call the link tokenizer instead.
  150. var style = state._urlTokenize(stream, state);
  151. return style && (style + " m-" + (this.alternateName || this.name));
  152. }
  153. // Remember the start position so we can rewind if needed.
  154. var startPosition = stream.pos;
  155. var style = this._token(stream, state);
  156. if (style) {
  157. if (style === "variable-2" && stream.current() === "url") {
  158. // If the current text is "url" then we should expect the next string token to be a link.
  159. state._expectLink = true;
  160. } else if (state._expectLink && style === "string") {
  161. // We expected a string and got it. This is a link. Parse it the way we want it.
  162. delete state._expectLink;
  163. // This is a link, so setup the state to process it next.
  164. state._urlTokenize = tokenizeCSSURLString;
  165. state._urlBaseStyle = style;
  166. // The url may or may not be quoted.
  167. var quote = stream.current()[0];
  168. state._urlQuoteCharacter = quote === "'" || quote === "\"" ? quote : ")";
  169. state._unquotedURLString = state._urlQuoteCharacter === ")";
  170. // Rewind the steam to the start of this token.
  171. stream.pos = startPosition;
  172. // Eat the open quote of the string so the string style
  173. // will be used for the quote character.
  174. if (!state._unquotedURLString)
  175. stream.eat(state._urlQuoteCharacter);
  176. } else if (state._expectLink) {
  177. // We expected a string and didn't get one. Cleanup.
  178. delete state._expectLink;
  179. }
  180. }
  181. return style && (style + " m-" + (this.alternateName || this.name));
  182. }
  183. function extendedToken(stream, state)
  184. {
  185. // CodeMirror moves the original token function to _token when we extended it.
  186. // So call it to get the style that we will add an additional class name to.
  187. var style = this._token(stream, state);
  188. return style && (style + " m-" + (this.alternateName || this.name));
  189. }
  190. function extendedCSSRuleStartState(base)
  191. {
  192. // CodeMirror moves the original token function to _startState when we extended it.
  193. // So call it to get the original start state that we will modify.
  194. var state = this._startState(base);
  195. // Start the stack off like it has already parsed a rule. This causes everything
  196. // after to be parsed as properties in a rule.
  197. state.stack = ["rule"];
  198. return state;
  199. }
  200. CodeMirror.extendMode("css-base", {token: extendedCSSToken, alternateName: "css"});
  201. CodeMirror.extendMode("xml", {token: extendedXMLToken});
  202. CodeMirror.extendMode("javascript", {token: extendedToken});
  203. CodeMirror.defineMode("css-rule", CodeMirror.modes.css);
  204. CodeMirror.extendMode("css-rule", {startState: extendedCSSRuleStartState});
  205. CodeMirror.defineExtension("hasLineClass", function(line, where, className) {
  206. // This matches the arguments to addLineClass and removeLineClass.
  207. var classProperty = (where === "text" ? "textClass" : (where == "background" ? "bgClass" : "wrapClass"));
  208. var lineInfo = this.lineInfo(line);
  209. if (!lineInfo)
  210. return false;
  211. if (!lineInfo[classProperty])
  212. return false;
  213. // Test for the simple case.
  214. if (lineInfo[classProperty] === className)
  215. return true;
  216. // Do a quick check for the substring. This is faster than a regex, which requires escaping the input first.
  217. var index = lineInfo[classProperty].indexOf(className);
  218. if (index === -1)
  219. return false;
  220. // Check that it is surrounded by spaces. Add padding spaces first to work with beginning and end of string cases.
  221. var paddedClass = " " + lineInfo[classProperty] + " ";
  222. return paddedClass.indexOf(" " + className + " ", index) !== -1;
  223. });
  224. CodeMirror.defineExtension("toggleLineClass", function(line, where, className) {
  225. if (this.hasLineClass(line, where, className)) {
  226. this.removeLineClass(line, where, className);
  227. return false;
  228. }
  229. this.addLineClass(line, where, className);
  230. return true;
  231. });
  232. function alterNumber(amount, codeMirror)
  233. {
  234. var selectionAnchor = codeMirror.getCursor("anchor");
  235. var selectionHead = codeMirror.getCursor("head");
  236. // We don't try if the range is multiline, pass to another key handler.
  237. if (selectionAnchor.line !== selectionHead.line)
  238. return CodeMirror.Pass;
  239. var line = codeMirror.getLine(selectionAnchor.line);
  240. var foundPeriod = false;
  241. var start = NaN;
  242. var end = NaN;
  243. for (var i = selectionAnchor.ch; i >= 0; --i) {
  244. var character = line.charAt(i);
  245. if (character === ".") {
  246. if (foundPeriod)
  247. break;
  248. foundPeriod = true;
  249. } else if (character !== "-" && character !== "+" && isNaN(parseInt(character))) {
  250. // Found the end already, just scan backwards.
  251. if (i === selectionAnchor.ch) {
  252. end = i;
  253. continue;
  254. }
  255. break;
  256. }
  257. start = i;
  258. }
  259. if (isNaN(end)) {
  260. for (var i = selectionAnchor.ch + 1; i < line.length; ++i) {
  261. var character = line.charAt(i);
  262. if (character === ".") {
  263. if (foundPeriod) {
  264. end = i;
  265. break;
  266. }
  267. foundPeriod = true;
  268. } else if (isNaN(parseInt(character))) {
  269. end = i;
  270. break;
  271. }
  272. end = i + 1;
  273. }
  274. }
  275. // No number range found, pass to another key handler.
  276. if (isNaN(start) || isNaN(end))
  277. return CodeMirror.Pass;
  278. var number = parseFloat(line.substring(start, end));
  279. if (number < 1 && number >= -1 && amount === 1)
  280. amount = 0.1;
  281. else if (number <= 1 && number > -1 && amount === -1)
  282. amount = -0.1;
  283. // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
  284. // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
  285. var alteredNumber = Number((number + amount).toFixed(6));
  286. var alteredNumberString = alteredNumber.toString();
  287. var from = {line: selectionAnchor.line, ch: start};
  288. var to = {line: selectionAnchor.line, ch: end};
  289. codeMirror.replaceRange(alteredNumberString, from, to);
  290. var newTo = {line: selectionAnchor.line, ch: from.ch + alteredNumberString.length};
  291. // Fix up the selection so it follows the increase or decrease in the replacement length.
  292. if (selectionHead.ch >= to.ch)
  293. selectionHead = newTo;
  294. if (selectionAnchor.ch >= to.ch)
  295. selectionAnchor = newTo;
  296. codeMirror.setSelection(selectionAnchor, selectionHead);
  297. }
  298. function ignoreKey(codeMirror)
  299. {
  300. // Do nothing to ignore the key.
  301. }
  302. CodeMirror.keyMap["default"] = {
  303. "Alt-Up": alterNumber.bind(null, 1),
  304. "Shift-Alt-Up": alterNumber.bind(null, 10),
  305. "Alt-PageUp": alterNumber.bind(null, 10),
  306. "Shift-Alt-PageUp": alterNumber.bind(null, 100),
  307. "Alt-Down": alterNumber.bind(null, -1),
  308. "Shift-Alt-Down": alterNumber.bind(null, -10),
  309. "Alt-PageDown": alterNumber.bind(null, -10),
  310. "Shift-Alt-PageDown": alterNumber.bind(null, -100),
  311. "Cmd-/": "toggleComment",
  312. "Shift-Tab": ignoreKey,
  313. fallthrough: "macDefault"
  314. };
  315. // Register some extra MIME-types for CodeMirror. These are in addition to the
  316. // ones CodeMirror already registers, like text/html, text/javascript, etc.
  317. const extraXMLTypes = ["text/xml", "text/xsl"];
  318. extraXMLTypes.forEach(function(type) {
  319. CodeMirror.defineMIME(type, "xml");
  320. });
  321. const extraHTMLTypes = ["application/xhtml+xml", "image/svg+xml"];
  322. extraHTMLTypes.forEach(function(type) {
  323. CodeMirror.defineMIME(type, "htmlmixed");
  324. });
  325. const extraJavaScriptTypes = ["text/ecmascript", "application/javascript", "application/ecmascript", "application/x-javascript",
  326. "text/x-javascript", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/jscript", "text/livescript"];
  327. extraJavaScriptTypes.forEach(function(type) {
  328. CodeMirror.defineMIME(type, "javascript");
  329. });
  330. const extraJSONTypes = ["application/x-json", "text/x-json"];
  331. extraJSONTypes.forEach(function(type) {
  332. CodeMirror.defineMIME(type, {name: "javascript", json: true});
  333. });
  334. })();
  335. WebInspector.compareCodeMirrorPositions = function(a, b)
  336. {
  337. var lineCompare = a.line - b.line;
  338. if (lineCompare !== 0)
  339. return lineCompare;
  340. var aColumn = "ch" in a ? a.ch : Number.MAX_VALUE;
  341. var bColumn = "ch" in b ? b.ch : Number.MAX_VALUE;
  342. return aColumn - bColumn;
  343. };