CodeMirrorCompletionController.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  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. WebInspector.CodeMirrorCompletionController = function(codeMirror, delegate, stopCharactersRegex)
  26. {
  27. WebInspector.Object.call(this);
  28. console.assert(codeMirror);
  29. this._codeMirror = codeMirror;
  30. this._stopCharactersRegex = stopCharactersRegex || null;
  31. this._delegate = delegate || null;
  32. this._startOffset = NaN;
  33. this._endOffset = NaN;
  34. this._lineNumber = NaN;
  35. this._prefix = "";
  36. this._completions = [];
  37. this._suggestionsView = new WebInspector.CompletionSuggestionsView(this);
  38. this._keyMap = {
  39. "Up": this._handleUpKey.bind(this),
  40. "Down": this._handleDownKey.bind(this),
  41. "Right": this._handleRightOrEnterKey.bind(this),
  42. "Esc": this._handleEscapeKey.bind(this),
  43. "Enter": this._handleRightOrEnterKey.bind(this),
  44. "Tab": this._handleTabKey.bind(this),
  45. "Cmd-A": this._handleHideKey.bind(this),
  46. "Cmd-Z": this._handleHideKey.bind(this),
  47. "Shift-Cmd-Z": this._handleHideKey.bind(this),
  48. "Cmd-Y": this._handleHideKey.bind(this)
  49. };
  50. this._handleChangeListener = this._handleChange.bind(this);
  51. this._handleCursorActivityListener = this._handleCursorActivity.bind(this);
  52. this._handleHideActionListener = this._handleHideAction.bind(this);
  53. this._codeMirror.addKeyMap(this._keyMap);
  54. this._codeMirror.on("change", this._handleChangeListener);
  55. this._codeMirror.on("cursorActivity", this._handleCursorActivityListener);
  56. this._codeMirror.on("blur", this._handleHideActionListener);
  57. this._codeMirror.on("scroll", this._handleHideActionListener);
  58. };
  59. WebInspector.CodeMirrorCompletionController.GenericStopCharactersRegex = /[\s=:;,]/;
  60. WebInspector.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap = {"css": /[\s:;,{}()]/, "javascript": /[\s=:;,!+\-*/%&|^~?<>.{}()[\]]/};
  61. WebInspector.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap = {"javascript": /[\s=:;,!+\-*/%&|^~?<>]/};
  62. WebInspector.CodeMirrorCompletionController.OpenBracketCharactersRegex = /[({[]/;
  63. WebInspector.CodeMirrorCompletionController.CloseBracketCharactersRegex = /[)}\]]/;
  64. WebInspector.CodeMirrorCompletionController.MatchingBrackets = {"{": "}", "(": ")", "[": "]", "}": "{", ")": "(", "]": "["};
  65. WebInspector.CodeMirrorCompletionController.CompletionHintStyleClassName = "completion-hint";
  66. WebInspector.CodeMirrorCompletionController.CompletionsHiddenDelay = 250;
  67. WebInspector.CodeMirrorCompletionController.CompletionTypingDelay = 250;
  68. WebInspector.CodeMirrorCompletionController.CompletionOrigin = "+completion";
  69. WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin = "+delete-completion";
  70. WebInspector.CodeMirrorCompletionController.prototype = {
  71. constructor: WebInspector.CodeMirrorCompletionController,
  72. // Public
  73. get delegate()
  74. {
  75. return this._delegate;
  76. },
  77. updateCompletions: function(completions, implicitSuffix)
  78. {
  79. if (isNaN(this._startOffset) || isNaN(this._endOffset) || isNaN(this._lineNumber))
  80. return;
  81. if (!completions || !completions.length) {
  82. this.hideCompletions();
  83. return;
  84. }
  85. this._completions = completions;
  86. if (typeof implicitSuffix === "string")
  87. this._implicitSuffix = implicitSuffix;
  88. var from = {line: this._lineNumber, ch: this._startOffset};
  89. var to = {line: this._lineNumber, ch: this._endOffset};
  90. var firstCharCoords = this._codeMirror.cursorCoords(from);
  91. var lastCharCoords = this._codeMirror.cursorCoords(to);
  92. var bounds = new WebInspector.Rect(firstCharCoords.left, firstCharCoords.top, lastCharCoords.right - firstCharCoords.left, firstCharCoords.bottom - firstCharCoords.top);
  93. // Try to restore the previous selected index, otherwise just select the first.
  94. var index = this._currentCompletion ? completions.indexOf(this._currentCompletion) : 0;
  95. if (index === -1)
  96. index = 0;
  97. if (this._forced || completions.length > 1 || completions[index] !== this._prefix) {
  98. // Update and show the suggestion list.
  99. this._suggestionsView.update(completions, index);
  100. this._suggestionsView.show(bounds);
  101. } else if (this._implicitSuffix) {
  102. // The prefix and the completion exactly match, but there is an implicit suffix.
  103. // Just hide the suggestion list and keep the completion hint for the implicit suffix.
  104. this._suggestionsView.hide();
  105. } else {
  106. // The prefix and the completion exactly match, hide the completions. Return early so
  107. // the completion hint isn't updated.
  108. this.hideCompletions();
  109. return;
  110. }
  111. this._applyCompletionHint(completions[index]);
  112. },
  113. isCompletionChange: function(change)
  114. {
  115. return this._ignoreChange || change.origin === WebInspector.CodeMirrorCompletionController.CompletionOrigin || change.origin === WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin;
  116. },
  117. isShowingCompletions: function()
  118. {
  119. return this._suggestionsView.visible || (this._completionHintMarker && this._completionHintMarker.find());
  120. },
  121. isHandlingClickEvent: function()
  122. {
  123. return this._suggestionsView.isHandlingClickEvent();
  124. },
  125. hideCompletions: function()
  126. {
  127. this._suggestionsView.hide();
  128. this._removeCompletionHint();
  129. this._startOffset = NaN;
  130. this._endOffset = NaN;
  131. this._lineNumber = NaN;
  132. this._prefix = "";
  133. this._completions = [];
  134. this._implicitSuffix = "";
  135. this._forced = false;
  136. if (this._completionDelayTimeout) {
  137. clearTimeout(this._completionDelayTimeout)
  138. delete this._completionDelayTimeout;
  139. }
  140. delete this._currentCompletion;
  141. delete this._ignoreNextCursorActivity;
  142. },
  143. close: function()
  144. {
  145. this._codeMirror.removeKeyMap(this._keyMap);
  146. this._codeMirror.off("change", this._handleChangeListener);
  147. this._codeMirror.off("cursorActivity", this._handleCursorActivityListener);
  148. this._codeMirror.off("blur", this._handleHideActionListener);
  149. this._codeMirror.off("scroll", this._handleHideActionListener);
  150. },
  151. // Protected
  152. completionSuggestionsSelectedCompletion: function(suggestionsView, completionText)
  153. {
  154. this._applyCompletionHint(completionText);
  155. },
  156. completionSuggestionsClickedCompletion: function(suggestionsView, completionText)
  157. {
  158. // The clicked suggestion causes the editor to loose focus. Restore it so the user can keep typing.
  159. this._codeMirror.focus();
  160. this._applyCompletionHint(completionText);
  161. this._commitCompletionHint();
  162. },
  163. // Private
  164. get _currentReplacementText()
  165. {
  166. return this._currentCompletion + this._implicitSuffix;
  167. },
  168. _hasPendingCompletion: function()
  169. {
  170. return !isNaN(this._startOffset) && !isNaN(this._endOffset) && !isNaN(this._lineNumber);
  171. },
  172. _notifyCompletionsHiddenSoon: function()
  173. {
  174. function notify()
  175. {
  176. if (this._completionHintMarker)
  177. return;
  178. if (this._delegate && typeof this._delegate.completionControllerCompletionsHidden === "function")
  179. this._delegate.completionControllerCompletionsHidden(this);
  180. }
  181. if (this._notifyCompletionsHiddenIfNeededTimeout)
  182. clearTimeout(this._notifyCompletionsHiddenIfNeededTimeout);
  183. this._notifyCompletionsHiddenIfNeededTimeout = setTimeout(notify.bind(this), WebInspector.CodeMirrorCompletionController.CompletionsHiddenDelay);
  184. },
  185. _applyCompletionHint: function(completionText)
  186. {
  187. console.assert(completionText);
  188. if (!completionText)
  189. return;
  190. function update()
  191. {
  192. this._currentCompletion = completionText;
  193. this._removeCompletionHint(true, true);
  194. var replacementText = this._currentReplacementText;
  195. var from = {line: this._lineNumber, ch: this._startOffset};
  196. var cursor = {line: this._lineNumber, ch: this._endOffset};
  197. var to = {line: this._lineNumber, ch: this._startOffset + replacementText.length};
  198. this._codeMirror.replaceRange(replacementText, from, cursor, WebInspector.CodeMirrorCompletionController.CompletionOrigin);
  199. this._removeLastChangeFromHistory();
  200. this._codeMirror.setCursor(cursor);
  201. if (cursor.ch !== to.ch)
  202. this._completionHintMarker = this._codeMirror.markText(cursor, to, {className: WebInspector.CodeMirrorCompletionController.CompletionHintStyleClassName});
  203. }
  204. this._ignoreChange = true;
  205. this._ignoreNextCursorActivity = true;
  206. this._codeMirror.operation(update.bind(this));
  207. delete this._ignoreChange;
  208. },
  209. _commitCompletionHint: function()
  210. {
  211. function update()
  212. {
  213. this._removeCompletionHint(true, true);
  214. var replacementText = this._currentReplacementText;
  215. var from = {line: this._lineNumber, ch: this._startOffset};
  216. var cursor = {line: this._lineNumber, ch: this._endOffset};
  217. var to = {line: this._lineNumber, ch: this._startOffset + replacementText.length};
  218. var lastChar = this._currentCompletion.charAt(this._currentCompletion.length - 1);
  219. var isClosing = ")]}".indexOf(lastChar);
  220. if (isClosing !== -1)
  221. to.ch -= 1 + this._implicitSuffix.length;
  222. this._codeMirror.replaceRange(replacementText, from, cursor, WebInspector.CodeMirrorCompletionController.CompletionOrigin);
  223. // Don't call _removeLastChangeFromHistory here to allow the committed completion to be undone.
  224. this._codeMirror.setCursor(to);
  225. this.hideCompletions();
  226. }
  227. this._ignoreChange = true;
  228. this._ignoreNextCursorActivity = true;
  229. this._codeMirror.operation(update.bind(this));
  230. delete this._ignoreChange;
  231. },
  232. _removeLastChangeFromHistory: function()
  233. {
  234. var history = this._codeMirror.getHistory();
  235. // We don't expect a undone history. But if there is one clear it. If could lead to undefined behavior.
  236. console.assert(!history.undone.length);
  237. history.undone = [];
  238. // Pop the last item from the done history.
  239. console.assert(history.done.length);
  240. history.done.pop();
  241. this._codeMirror.setHistory(history);
  242. },
  243. _removeCompletionHint: function(nonatomic, dontRestorePrefix)
  244. {
  245. if (!this._completionHintMarker)
  246. return;
  247. this._notifyCompletionsHiddenSoon();
  248. function update()
  249. {
  250. var range = this._completionHintMarker.find();
  251. if (range) {
  252. this._completionHintMarker.clear();
  253. this._codeMirror.replaceRange("", range.from, range.to, WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin);
  254. this._removeLastChangeFromHistory();
  255. }
  256. this._completionHintMarker = null;
  257. if (dontRestorePrefix)
  258. return;
  259. console.assert(!isNaN(this._startOffset));
  260. console.assert(!isNaN(this._endOffset));
  261. console.assert(!isNaN(this._lineNumber));
  262. var from = {line: this._lineNumber, ch: this._startOffset};
  263. var to = {line: this._lineNumber, ch: this._endOffset};
  264. this._codeMirror.replaceRange(this._prefix, from, to, WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin);
  265. this._removeLastChangeFromHistory();
  266. }
  267. if (nonatomic) {
  268. update.call(this);
  269. return;
  270. }
  271. this._ignoreChange = true;
  272. this._codeMirror.operation(update.bind(this));
  273. delete this._ignoreChange;
  274. },
  275. _scanStringForExpression: function(modeName, string, startOffset, direction, allowMiddleAndEmpty, includeStopCharacter, ignoreInitialUnmatchedOpenBracket, stopCharactersRegex)
  276. {
  277. console.assert(direction === -1 || direction === 1);
  278. var stopCharactersRegex = stopCharactersRegex || this._stopCharactersRegex || WebInspector.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap[modeName] || WebInspector.CodeMirrorCompletionController.GenericStopCharactersRegex;
  279. function isStopCharacter(character)
  280. {
  281. return stopCharactersRegex.test(character);
  282. }
  283. function isOpenBracketCharacter(character)
  284. {
  285. return WebInspector.CodeMirrorCompletionController.OpenBracketCharactersRegex.test(character);
  286. }
  287. function isCloseBracketCharacter(character)
  288. {
  289. return WebInspector.CodeMirrorCompletionController.CloseBracketCharactersRegex.test(character);
  290. }
  291. function matchingBracketCharacter(character)
  292. {
  293. return WebInspector.CodeMirrorCompletionController.MatchingBrackets[character];
  294. }
  295. var endOffset = Math.min(startOffset, string.length);
  296. var endOfLineOrWord = endOffset === string.length || isStopCharacter(string.charAt(endOffset));
  297. if (!endOfLineOrWord && !allowMiddleAndEmpty)
  298. return null;
  299. var bracketStack = [];
  300. var bracketOffsetStack = [];
  301. var lastCloseBracketOffset = NaN;
  302. var startOffset = endOffset;
  303. var firstOffset = endOffset + direction;
  304. for (var i = firstOffset; direction > 0 ? i < string.length : i >= 0; i += direction) {
  305. var character = string.charAt(i);
  306. // Ignore stop characters when we are inside brackets.
  307. if (isStopCharacter(character) && !bracketStack.length)
  308. break;
  309. if (isCloseBracketCharacter(character)) {
  310. bracketStack.push(character);
  311. bracketOffsetStack.push(i);
  312. } else if (isOpenBracketCharacter(character)) {
  313. if ((!ignoreInitialUnmatchedOpenBracket || i !== firstOffset) && (!bracketStack.length || matchingBracketCharacter(character) !== bracketStack.lastValue))
  314. break;
  315. bracketOffsetStack.pop();
  316. bracketStack.pop();
  317. }
  318. startOffset = i + (direction > 0 ? 1 : 0);
  319. }
  320. if (bracketOffsetStack.length)
  321. startOffset = bracketOffsetStack.pop() + 1;
  322. if (includeStopCharacter && startOffset > 0 && startOffset < string.length)
  323. startOffset += direction;
  324. if (direction > 0) {
  325. var tempEndOffset = endOffset;
  326. endOffset = startOffset;
  327. startOffset = tempEndOffset;
  328. }
  329. return {string: string.substring(startOffset, endOffset), startOffset: startOffset, endOffset: endOffset};
  330. },
  331. _completeAtCurrentPosition: function(force)
  332. {
  333. if (this._codeMirror.somethingSelected()) {
  334. this.hideCompletions();
  335. return;
  336. }
  337. if (this._completionDelayTimeout) {
  338. clearTimeout(this._completionDelayTimeout);
  339. delete this._completionDelayTimeout;
  340. }
  341. this._removeCompletionHint(true, true);
  342. var cursor = this._codeMirror.getCursor();
  343. var token = this._codeMirror.getTokenAt(cursor);
  344. // Don't try to complete inside comments.
  345. if (token.type && /\bcomment\b/.test(token.type)) {
  346. this.hideCompletions();
  347. return;
  348. }
  349. var mode = this._codeMirror.getMode();
  350. var innerMode = CodeMirror.innerMode(mode, token.state).mode;
  351. var modeName = innerMode.alternateName || innerMode.name;
  352. var lineNumber = cursor.line;
  353. var lineString = this._codeMirror.getLine(lineNumber);
  354. var backwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, -1, force);
  355. if (!backwardScanResult) {
  356. this.hideCompletions();
  357. return;
  358. }
  359. var forwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, 1, true, true);
  360. var suffix = forwardScanResult.string;
  361. this._ignoreNextCursorActivity = true;
  362. this._startOffset = backwardScanResult.startOffset;
  363. this._endOffset = backwardScanResult.endOffset;
  364. this._lineNumber = lineNumber;
  365. this._prefix = backwardScanResult.string;
  366. this._completions = [];
  367. this._implicitSuffix = "";
  368. this._forced = force;
  369. var baseExpressionStopCharactersRegex = WebInspector.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap[modeName];
  370. if (baseExpressionStopCharactersRegex)
  371. var baseScanResult = this._scanStringForExpression(modeName, lineString, this._startOffset, -1, true, false, true, baseExpressionStopCharactersRegex);
  372. if (!force && !backwardScanResult.string && (!baseScanResult || !baseScanResult.string)) {
  373. this.hideCompletions();
  374. return;
  375. }
  376. var defaultCompletions = [];
  377. switch (modeName) {
  378. case "css":
  379. defaultCompletions = this._generateCSSCompletions(token, baseScanResult ? baseScanResult.string : null, suffix);
  380. break;
  381. case "javascript":
  382. defaultCompletions = this._generateJavaScriptCompletions(token, baseScanResult ? baseScanResult.string : null, suffix);
  383. break;
  384. }
  385. if (this._delegate && typeof this._delegate.completionControllerCompletionsNeeded === "function")
  386. this._delegate.completionControllerCompletionsNeeded(this, this._prefix, defaultCompletions, baseScanResult ? baseScanResult.string : null, suffix, force);
  387. else
  388. this.updateCompletions(defaultCompletions);
  389. },
  390. _generateCSSCompletions: function(mainToken, base, suffix)
  391. {
  392. // We only support completion inside CSS rules.
  393. if (!mainToken.state || !mainToken.state.stack || !mainToken.state.stack.contains("rule"))
  394. return [];
  395. var token = mainToken;
  396. var lineNumber = this._lineNumber;
  397. // Scan backwards looking for the current property.
  398. while (token.state.stack.lastValue === "propertyValue") {
  399. // Found the beginning of the line. Go to the previous line.
  400. if (!token.start) {
  401. --lineNumber;
  402. // No more lines, stop.
  403. if (lineNumber < 0)
  404. break;
  405. }
  406. // Get the previous token.
  407. token = this._codeMirror.getTokenAt({line: lineNumber, ch: token.start ? token.start : Number.MAX_VALUE});
  408. }
  409. // If we have a property token and it's not the main token, then we are working on
  410. // the value for that property and should complete allowed values.
  411. if (mainToken !== token && token.type && /\bproperty\b/.test(token.type)) {
  412. var propertyName = token.string;
  413. // If there is a suffix and it isn't a semicolon, then we should use a space since
  414. // the user is editing in the middle.
  415. this._implicitSuffix = suffix && suffix !== ";" ? " " : ";";
  416. // Don't use an implicit suffix if it would be the same as the existing suffix.
  417. if (this._implicitSuffix === suffix)
  418. this._implicitSuffix = "";
  419. return WebInspector.CSSKeywordCompletions.forProperty(propertyName).startsWith(this._prefix);
  420. }
  421. this._implicitSuffix = suffix !== ":" ? ": " : "";
  422. // Complete property names.
  423. return WebInspector.CSSCompletions.cssNameCompletions.startsWith(this._prefix);
  424. },
  425. _generateJavaScriptCompletions: function(mainToken, base, suffix)
  426. {
  427. // If there is a base expression then we should not attempt to match any keywords or variables.
  428. // Allow only open bracket characters at the end of the base, otherwise leave completions with
  429. // a base up to the delegate to figure out.
  430. if (base && !/[({[]$/.test(base))
  431. return [];
  432. var matchingWords = [];
  433. const prefix = this._prefix;
  434. const declaringVariable = mainToken.state.lexical.type === "vardef";
  435. const insideSwitch = mainToken.state.lexical.prev ? mainToken.state.lexical.prev.info === "switch" : false;
  436. const insideBlock = mainToken.state.lexical.prev ? mainToken.state.lexical.prev.type === "}" : false;
  437. const insideParenthesis = mainToken.state.lexical.type === ")";
  438. const insideBrackets = mainToken.state.lexical.type === "]";
  439. const allKeywords = ["break", "case", "catch", "const", "continue", "debugger", "default", "delete", "do", "else", "false", "finally", "for", "function", "if", "in",
  440. "Infinity", "instanceof", "NaN", "new", "null", "return", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "with"];
  441. const valueKeywords = ["false", "Infinity", "NaN", "null", "this", "true", "undefined"];
  442. const allowedKeywordsInsideBlocks = allKeywords.keySet();
  443. const allowedKeywordsWhenDeclaringVariable = valueKeywords.keySet();
  444. const allowedKeywordsInsideParenthesis = valueKeywords.concat(["function"]).keySet();
  445. const allowedKeywordsInsideBrackets = allowedKeywordsInsideParenthesis;
  446. const allowedKeywordsOnlyInsideSwitch = ["case", "default"].keySet();
  447. function matchKeywords(keywords)
  448. {
  449. matchingWords = matchingWords.concat(keywords.filter(function(word) {
  450. if (!insideSwitch && word in allowedKeywordsOnlyInsideSwitch)
  451. return false;
  452. if (insideBlock && !(word in allowedKeywordsInsideBlocks))
  453. return false;
  454. if (insideBrackets && !(word in allowedKeywordsInsideBrackets))
  455. return false;
  456. if (insideParenthesis && !(word in allowedKeywordsInsideParenthesis))
  457. return false;
  458. if (declaringVariable && !(word in allowedKeywordsWhenDeclaringVariable))
  459. return false;
  460. return word.startsWith(prefix);
  461. }));
  462. }
  463. function matchVariables()
  464. {
  465. function filterVariables(variables)
  466. {
  467. for (var variable = variables; variable; variable = variable.next) {
  468. // Don't match the variable if this token is in a variable declaration.
  469. // Otherwise the currently typed text will always match and that isn't useful.
  470. if (declaringVariable && variable.name === prefix)
  471. continue;
  472. if (variable.name.startsWith(prefix) && !matchingWords.contains(variable.name))
  473. matchingWords.push(variable.name);
  474. }
  475. }
  476. var context = mainToken.state.context;
  477. while (context) {
  478. filterVariables(context.vars);
  479. context = context.prev;
  480. }
  481. filterVariables(mainToken.state.globalVars);
  482. }
  483. switch (suffix.substring(0, 1)) {
  484. case "":
  485. case " ":
  486. matchVariables();
  487. matchKeywords(allKeywords);
  488. break;
  489. case ".":
  490. case "[":
  491. matchVariables();
  492. matchKeywords(["false", "Infinity", "NaN", "this", "true"]);
  493. break;
  494. case "(":
  495. matchVariables();
  496. matchKeywords(["catch", "else", "for", "function", "if", "return", "switch", "throw", "while", "with"]);
  497. break;
  498. case "{":
  499. matchKeywords(["do", "else", "finally", "return", "try"]);
  500. break;
  501. case ":":
  502. if (insideSwitch)
  503. matchKeywords(["case", "default"]);
  504. break;
  505. case ";":
  506. matchVariables();
  507. matchKeywords(valueKeywords);
  508. matchKeywords(["break", "continue", "debugger", "return", "void"]);
  509. break;
  510. }
  511. return matchingWords;
  512. },
  513. _handleUpKey: function(codeMirror)
  514. {
  515. if (!this._hasPendingCompletion())
  516. return CodeMirror.Pass;
  517. if (!this.isShowingCompletions())
  518. return;
  519. this._suggestionsView.selectPrevious();
  520. },
  521. _handleDownKey: function(codeMirror)
  522. {
  523. if (!this._hasPendingCompletion())
  524. return CodeMirror.Pass;
  525. if (!this.isShowingCompletions())
  526. return;
  527. this._suggestionsView.selectNext();
  528. },
  529. _handleRightOrEnterKey: function(codeMirror)
  530. {
  531. if (!this._hasPendingCompletion())
  532. return CodeMirror.Pass;
  533. if (!this.isShowingCompletions())
  534. return;
  535. this._commitCompletionHint();
  536. },
  537. _handleEscapeKey: function(codeMirror)
  538. {
  539. var delegateImplementsShouldAllowEscapeCompletion = this._delegate && typeof this._delegate.completionControllerShouldAllowEscapeCompletion === "function";
  540. if (this._hasPendingCompletion())
  541. this.hideCompletions();
  542. else if (this._codeMirror.getOption("readOnly"))
  543. return CodeMirror.Pass;
  544. else if (!delegateImplementsShouldAllowEscapeCompletion || this._delegate.completionControllerShouldAllowEscapeCompletion(this))
  545. this._completeAtCurrentPosition(true);
  546. else
  547. return CodeMirror.Pass;
  548. },
  549. _handleTabKey: function(codeMirror)
  550. {
  551. if (!this._hasPendingCompletion())
  552. return CodeMirror.Pass;
  553. if (!this.isShowingCompletions())
  554. return;
  555. console.assert(this._completions.length);
  556. if (!this._completions.length)
  557. return;
  558. console.assert(this._currentCompletion);
  559. if (!this._currentCompletion)
  560. return;
  561. // Commit the current completion if there is only one suggestion.
  562. if (this._completions.length === 1) {
  563. this._commitCompletionHint();
  564. return;
  565. }
  566. var prefixLength = this._prefix.length;
  567. var commonPrefix = this._completions[0];
  568. for (var i = 1; i < this._completions.length; ++i) {
  569. var completion = this._completions[i];
  570. var lastIndex = Math.min(commonPrefix.length, completion.length);
  571. for (var j = prefixLength; j < lastIndex; ++j) {
  572. if (commonPrefix[j] !== completion[j]) {
  573. commonPrefix = commonPrefix.substr(0, j);
  574. break;
  575. }
  576. }
  577. }
  578. // Commit the current completion if there is no common prefix that is longer.
  579. if (commonPrefix === this._prefix) {
  580. this._commitCompletionHint();
  581. return;
  582. }
  583. // Set the prefix to the common prefix so _applyCompletionHint will insert the
  584. // common prefix as commited text. Adjust _endOffset to match the new prefix.
  585. this._prefix = commonPrefix;
  586. this._endOffset = this._startOffset + commonPrefix.length;
  587. this._applyCompletionHint(this._currentCompletion);
  588. },
  589. _handleChange: function(codeMirror, change)
  590. {
  591. if (this.isCompletionChange(change))
  592. return;
  593. this._ignoreNextCursorActivity = true;
  594. if (!change.origin || change.origin.charAt(0) !== "+") {
  595. this.hideCompletions();
  596. return;
  597. }
  598. // Only complete on delete if we are showing completions already.
  599. if (change.origin === "+delete" && !this._hasPendingCompletion())
  600. return;
  601. if (this._completionDelayTimeout) {
  602. clearTimeout(this._completionDelayTimeout);
  603. delete this._completionDelayTimeout;
  604. }
  605. if (this._hasPendingCompletion())
  606. this._completeAtCurrentPosition(false);
  607. else
  608. this._completionDelayTimeout = setTimeout(this._completeAtCurrentPosition.bind(this, false), WebInspector.CodeMirrorCompletionController.CompletionTypingDelay);
  609. },
  610. _handleCursorActivity: function(codeMirror)
  611. {
  612. if (this._ignoreChange)
  613. return;
  614. if (this._ignoreNextCursorActivity) {
  615. delete this._ignoreNextCursorActivity;
  616. return;
  617. }
  618. this.hideCompletions();
  619. },
  620. _handleHideKey: function(codeMirror)
  621. {
  622. this.hideCompletions();
  623. return CodeMirror.Pass;
  624. },
  625. _handleHideAction: function(codeMirror)
  626. {
  627. // Clicking a suggestion causes the editor to blur. We don't want to hide completions in this case.
  628. if (this.isHandlingClickEvent())
  629. return;
  630. this.hideCompletions();
  631. }
  632. };
  633. WebInspector.CodeMirrorCompletionController.prototype.__proto__ = WebInspector.Object.prototype;