849e9022f153b25caaec2803d9f27bcafd649431
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / CodeMirrorCompletionController.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 WebInspector.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
70 WebInspector.CodeMirrorCompletionController.GenericStopCharactersRegex = /[\s=:;,]/;
71 WebInspector.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap = {"css": /[\s:;,{}()]/, "javascript": /[\s=:;,!+\-*/%&|^~?<>.{}()[\]]/};
72 WebInspector.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap = {"javascript": /[\s=:;,!+\-*/%&|^~?<>]/};
73 WebInspector.CodeMirrorCompletionController.OpenBracketCharactersRegex = /[({[]/;
74 WebInspector.CodeMirrorCompletionController.CloseBracketCharactersRegex = /[)}\]]/;
75 WebInspector.CodeMirrorCompletionController.MatchingBrackets = {"{": "}", "(": ")", "[": "]", "}": "{", ")": "(", "]": "["};
76 WebInspector.CodeMirrorCompletionController.CompletionHintStyleClassName = "completion-hint";
77 WebInspector.CodeMirrorCompletionController.CompletionsHiddenDelay = 250;
78 WebInspector.CodeMirrorCompletionController.CompletionTypingDelay = 250;
79 WebInspector.CodeMirrorCompletionController.CompletionOrigin = "+completion";
80 WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin = "+delete-completion";
81
82 WebInspector.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 || !mainToken.state.state || !mainToken.state.state === "block")
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
824 WebInspector.CodeMirrorCompletionController.prototype.__proto__ = WebInspector.Object.prototype;