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.ConsolePrompt = function(delegate, mimeType, element)
27{
28    WebInspector.Object.call(this);
29
30    mimeType = parseMIMEType(mimeType).type;
31
32    this._element = element || document.createElement("div");
33    this._element.classList.add(WebInspector.ConsolePrompt.StyleClassName);
34    this._element.classList.add(WebInspector.SyntaxHighlightedStyleClassName);
35
36    this._delegate = delegate || null;
37
38    this._codeMirror = CodeMirror(this.element, {
39        lineWrapping: true,
40        mode: mimeType,
41        indentWithTabs: true,
42        indentUnit: 4,
43        matchBrackets: true
44    });
45
46    var keyMap = {
47        "Up": this._handlePreviousKey.bind(this),
48        "Down": this._handleNextKey.bind(this),
49        "Ctrl-P": this._handlePreviousKey.bind(this),
50        "Ctrl-N": this._handleNextKey.bind(this),
51        "Enter": this._handleEnterKey.bind(this),
52        "Cmd-Enter": this._handleCommandEnterKey.bind(this),
53        "Esc": this._handleEscapeKey.bind(this)
54    };
55
56    this._codeMirror.addKeyMap(keyMap);
57
58    this._completionController = new WebInspector.CodeMirrorCompletionController(this._codeMirror, this);
59    this._completionController.addExtendedCompletionProvider("javascript", WebInspector.javaScriptRuntimeCompletionProvider);
60
61    this._history = [{}];
62    this._historyIndex = 0;
63};
64
65WebInspector.ConsolePrompt.StyleClassName = "console-prompt";
66WebInspector.ConsolePrompt.MaximumHistorySize = 30;
67
68WebInspector.ConsolePrompt.prototype = {
69    constructor: WebInspector.ConsolePrompt,
70
71    // Public
72
73    get element()
74    {
75        return this._element;
76    },
77
78    get delegate()
79    {
80        return this._delegate;
81    },
82
83    set delegate(delegate)
84    {
85        this._delegate = delegate || null;
86    },
87
88    set escapeKeyHandlerWhenEmpty(handler)
89    {
90        this._escapeKeyHandlerWhenEmpty = handler;
91    },
92
93    get text()
94    {
95        return this._codeMirror.getValue();
96    },
97
98    set text(text)
99    {
100        this._codeMirror.setValue(text || "");
101        this._codeMirror.clearHistory();
102        this._codeMirror.markClean();
103    },
104
105    get history()
106    {
107        this._history[this._historyIndex] = this._historyEntryForCurrentText();
108        return this._history;
109    },
110
111    set history(history)
112    {
113        this._history = history instanceof Array ? history.slice(0, WebInspector.ConsolePrompt.MaximumHistorySize) : [{}];
114        this._historyIndex = 0;
115        this._restoreHistoryEntry(0);
116    },
117
118    get focused()
119    {
120        return this._codeMirror.getWrapperElement().classList.contains("CodeMirror-focused");
121    },
122
123    focus: function()
124    {
125        this._codeMirror.focus();
126    },
127
128    shown: function()
129    {
130        this._codeMirror.refresh();
131    },
132
133    updateLayout: function()
134    {
135        this._codeMirror.refresh();
136    },
137
138    updateCompletions: function(completions, implicitSuffix)
139    {
140        this._completionController.updateCompletions(completions, implicitSuffix);
141    },
142
143    // Protected
144
145    completionControllerCompletionsNeeded: function(completionController, prefix, defaultCompletions, base, suffix, forced)
146    {
147        if (this.delegate && typeof this.delegate.consolePromptCompletionsNeeded === "function")
148            this.delegate.consolePromptCompletionsNeeded(this, defaultCompletions, base, prefix, suffix, forced);
149        else
150            this._completionController.updateCompletions(defaultCompletions);
151    },
152
153    completionControllerShouldAllowEscapeCompletion: function(completionController)
154    {
155        // Only allow escape to complete if there is text in the prompt. Otherwise allow it to pass through
156        // so escape to toggle the quick console still works.
157        return !!this.text;
158    },
159
160    // Private
161
162    _handleEscapeKey: function(codeMirror)
163    {
164        if (this.text)
165            return CodeMirror.Pass;
166
167        if (!this._escapeKeyHandlerWhenEmpty)
168            return CodeMirror.Pass;
169
170        this._escapeKeyHandlerWhenEmpty();
171    },
172
173    _handlePreviousKey: function(codeMirror)
174    {
175        if (this._codeMirror.somethingSelected())
176            return CodeMirror.Pass;
177
178        // Pass unless we are on the first line.
179        if (this._codeMirror.getCursor().line)
180            return CodeMirror.Pass;
181
182        var historyEntry = this._history[this._historyIndex + 1];
183        if (!historyEntry)
184            return CodeMirror.Pass;
185
186        this._rememberCurrentTextInHistory();
187
188        ++this._historyIndex;
189
190        this._restoreHistoryEntry(this._historyIndex);
191    },
192
193    _handleNextKey: function(codeMirror)
194    {
195        if (this._codeMirror.somethingSelected())
196            return CodeMirror.Pass;
197
198        // Pass unless we are on the last line.
199        if (this._codeMirror.getCursor().line !== this._codeMirror.lastLine())
200            return CodeMirror.Pass;
201
202        var historyEntry = this._history[this._historyIndex - 1];
203        if (!historyEntry)
204            return CodeMirror.Pass;
205
206        this._rememberCurrentTextInHistory();
207
208        --this._historyIndex;
209
210        this._restoreHistoryEntry(this._historyIndex);
211    },
212
213    _handleEnterKey: function(codeMirror, forceCommit)
214    {
215        var currentText = this.text;
216
217        // Always do nothing when there is just whitespace.
218        if (!currentText.trim())
219            return;
220
221        var cursor = this._codeMirror.getCursor();
222        var lastLine = this._codeMirror.lastLine();
223        var lastLineLength = this._codeMirror.getLine(lastLine).length;
224        var cursorIsAtLastPosition = positionsEqual(cursor, {line: lastLine, ch: lastLineLength});
225
226        function positionsEqual(a, b)
227        {
228            console.assert(a);
229            console.assert(b);
230            return a.line === b.line && a.ch === b.ch;
231        }
232
233        function commitTextOrInsertNewLine(commit)
234        {
235            if (!commit) {
236                // Only insert a new line if the previous cursor and the current cursor are in the same position.
237                if (positionsEqual(cursor, this._codeMirror.getCursor()))
238                    CodeMirror.commands.newlineAndIndent(this._codeMirror);
239                return;
240            }
241
242            var historyEntry = this._historyEntryForCurrentText();
243
244            // Replace the previous entry if it does not have text or if the text is the same.
245            if (this._history[1] && (!this._history[1].text || this._history[1].text === historyEntry.text)) {
246                this._history[1] = historyEntry;
247                this._history[0] = {};
248            } else {
249                // Replace the first history entry and push a new empty one.
250                this._history[0] = historyEntry;
251                this._history.unshift({});
252
253                // Trim the history length if needed.
254                if (this._history.length > WebInspector.ConsolePrompt.MaximumHistorySize)
255                    this._history = this._history.slice(0, WebInspector.ConsolePrompt.MaximumHistorySize);
256            }
257
258            this._historyIndex = 0;
259
260            this._codeMirror.setValue("");
261            this._codeMirror.clearHistory();
262
263            if (this.delegate && typeof this.delegate.consolePromptHistoryDidChange === "function")
264                this.delegate.consolePromptHistoryDidChange(this);
265
266            if (this.delegate && typeof this.delegate.consolePromptTextCommitted === "function")
267                this.delegate.consolePromptTextCommitted(this, currentText);
268        }
269
270        if (!forceCommit && this.delegate && typeof this.delegate.consolePromptShouldCommitText === "function") {
271            this.delegate.consolePromptShouldCommitText(this, currentText, cursorIsAtLastPosition, commitTextOrInsertNewLine.bind(this));
272            return;
273        }
274
275        commitTextOrInsertNewLine.call(this, true);
276    },
277
278    _handleCommandEnterKey: function(codeMirror)
279    {
280        this._handleEnterKey(codeMirror, true);
281    },
282
283    _restoreHistoryEntry: function(index)
284    {
285        var historyEntry = this._history[index];
286
287        this._codeMirror.setValue(historyEntry.text || "");
288
289        if (historyEntry.undoHistory)
290            this._codeMirror.setHistory(historyEntry.undoHistory);
291        else
292            this._codeMirror.clearHistory();
293
294        this._codeMirror.setCursor(historyEntry.cursor || {line: 0});
295    },
296
297    _historyEntryForCurrentText: function()
298    {
299        return {text: this.text, undoHistory: this._codeMirror.getHistory(), cursor: this._codeMirror.getCursor()};
300    },
301
302    _rememberCurrentTextInHistory: function()
303    {
304        this._history[this._historyIndex] = this._historyEntryForCurrentText();
305
306        if (this.delegate && typeof this.delegate.consolePromptHistoryDidChange === "function")
307            this.delegate.consolePromptHistoryDidChange(this);
308    }
309};
310
311WebInspector.ConsolePrompt.prototype.__proto__ = WebInspector.Object.prototype;
312