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