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