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