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