├── CREDITS ├── Makefile ├── README.md ├── bootstrap.js ├── chrome.manifest ├── chrome ├── icon.png ├── jsterm.js ├── jsterm.xul └── vendor │ ├── coffee-script.js │ ├── livescript.js │ └── prelude-ls.js ├── install.rdf ├── jsterm.xpi ├── locale └── en-US │ ├── jsterm.dtd │ └── jsterm.properties └── skin ├── jsterm.css └── orion.css /CREDITS: -------------------------------------------------------------------------------- 1 | * Authors: @paulrouget @paulmillr 2 | * Icon: http://soundforge.deviantart.com/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FILES = chrome/ \ 2 | locale/ \ 3 | bootstrap.js \ 4 | chrome.manifest \ 5 | install.rdf 6 | 7 | all: 8 | rm -f jsterm.xpi && zip -r jsterm.xpi $(FILES) 9 | wget --post-file=$(PWD)/jsterm.xpi http://localhost:8888/ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminal for Firefox 2 | 3 | **Warning:** the addon is being integrated into Firefox natively. It is no longer supported. 4 | 5 | This Firefox addon contains full-featured console that supports 6 | JS, [CoffeeScript](http://coffeescript.org) and [LiveScript](http://livescript.net). 7 | 8 | Info and screencast are available at http://paulrouget.com/e/jsterm/. 9 | To switch between languages, use `:js`, `:coffee` & `:livescript`. 10 | The language you choose will be saved for the next console session. 11 | 12 | Screenshot: 13 | 14 | ![](http://i.imgur.com/qUby4pc.png) 15 | 16 | ## Installation 17 | 18 | Drag’n’drop .xpi on your Firefox. 19 | 20 | If you prefer stable versions, you can 21 | [install the addon from addons.mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/javascript-terminal/). 22 | -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 1 | const Cu = Components.utils; 2 | const Cc = Components.classes; 3 | const Ci = Components.interfaces; 4 | 5 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 6 | Cu.import("resource://gre/modules/Services.jsm"); 7 | Cu.import("resource:///modules/devtools/gDevTools.jsm"); 8 | 9 | /* Depending on the version of Firefox, promise module can have different path */ 10 | try { Cu.import("resource://gre/modules/commonjs/promise/core.js"); } catch(e) {} 11 | try { Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); } catch(e) {} 12 | 13 | XPCOMUtils.defineLazyGetter(this, "osString", 14 | function() Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS); 15 | 16 | const jstermProps = "chrome://jsterm/locale/jsterm.properties"; 17 | let jstermStrings = Services.strings.createBundle(jstermProps); 18 | 19 | let jstermDefinition = { 20 | id: "jsterm", 21 | key: jstermStrings.GetStringFromName("JSTerm.commandkey"), 22 | ordinal: 0, 23 | modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", 24 | icon: "chrome://browser/skin/devtools/tool-webconsole.png", 25 | url: "chrome://jsterm/content/jsterm.xul", 26 | label: jstermStrings.GetStringFromName("JSTerm.label"), 27 | tooltip: jstermStrings.GetStringFromName("JSTerm.tooltip"), 28 | 29 | isTargetSupported: function(target) { 30 | return target.isLocalTab; 31 | }, 32 | 33 | build: function(iframeWindow, toolbox) { 34 | iframeWindow.JSTermUI.init(JSTermGlobalHistory, toolbox); 35 | return Promise.resolve(iframeWindow.JSTermUI); 36 | } 37 | }; 38 | 39 | function startup() { 40 | gDevTools.registerTool(jstermDefinition); 41 | } 42 | 43 | function shutdown() { 44 | gDevTools.unregisterTool(jstermDefinition); 45 | } 46 | 47 | function install() {} 48 | function uninstall() {} 49 | 50 | let JSTermGlobalHistory = { 51 | _limit: 100, // Should be a pref 52 | _entries: [], 53 | 54 | _cut: function() { 55 | let newStart = this._entries.length - this._limit; 56 | if (newStart <= 0) return; 57 | 58 | this._entries = this._entries.slice(newStart); 59 | 60 | for (let cursor of this._cursors) { 61 | if (cursor) { 62 | cursor.idx -= newStart; 63 | cursor.origin -= newStart; 64 | } 65 | } 66 | }, 67 | 68 | add: function(aEntry) { 69 | if (!aEntry) { 70 | return; 71 | } 72 | if (this._entries.length) { 73 | let lastEntry = this._entries[this._entries.length - 1]; 74 | if (lastEntry == aEntry) 75 | return; 76 | } 77 | this._entries.push(aEntry); 78 | 79 | if (this._entries.length > this._limit) { 80 | this._cut(); 81 | } 82 | }, 83 | 84 | initFromPref: function() { 85 | let history = []; 86 | 87 | // Try to load history from pref 88 | if (Services.prefs.prefHasUserValue("devtools.jsterm.history")) { 89 | try { 90 | history = JSON.parse(Services.prefs.getCharPref("devtools.jsterm.history")); 91 | } catch(e) { 92 | // User pref is malformated. 93 | Cu.reportError("Could not parse pref `devtools.jsterm.history`: " + e); 94 | } 95 | } 96 | 97 | if (Array.isArray(history)) { 98 | this._entries = history; 99 | } else { 100 | Cu.reportError("History (devtools.jsterm.history) is malformated."); 101 | this._entries = []; 102 | } 103 | }, 104 | 105 | saveToPref: function() { 106 | Services.prefs.setCharPref("devtools.jsterm.history", JSON.stringify(this._entries)); 107 | }, 108 | 109 | _cursors: [], 110 | getCursor: function(aInitialValue) { 111 | let cursor = {idx: this._entries.length, 112 | origin: this._entries.length, 113 | initialEntry: aInitialValue}; 114 | this._cursors.push(cursor); 115 | return cursor; 116 | }, 117 | 118 | releaseCursor: function(cursor) { 119 | this._cursors[cursor.idx] = null; 120 | }, 121 | 122 | getEntryForCursor: function(cursor) { 123 | if (cursor.idx < 0) { 124 | return ""; 125 | } else if (cursor.idx < cursor.origin) { 126 | return this._entries[cursor.idx]; 127 | } else { 128 | return cursor.initialEntry; 129 | } 130 | }, 131 | 132 | canGoBack: function(cursor) { 133 | return (cursor.idx > 0) 134 | }, 135 | 136 | canGoForward: function(cursor) { 137 | return (cursor.idx < cursor.origin); 138 | }, 139 | 140 | goBack: function(cursor) { 141 | if (this.canGoBack(cursor)) { 142 | cursor.idx--; 143 | return true; 144 | } else { 145 | return false; 146 | } 147 | }, 148 | 149 | goForward: function(cursor) { 150 | if (this.canGoForward(cursor)) { 151 | cursor.idx++; 152 | return true; 153 | } else { 154 | return false; 155 | } 156 | }, 157 | } 158 | JSTermGlobalHistory.initFromPref(); 159 | -------------------------------------------------------------------------------- /chrome.manifest: -------------------------------------------------------------------------------- 1 | content jsterm chrome/ 2 | locale jsterm en-US locale/en-US/ 3 | skin jsterm classic/1.0 skin/ 4 | -------------------------------------------------------------------------------- /chrome/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulrouget/firefox-jsterm/cd6e9b17551aa98bf1714adc1d3a44745c62a3e4/chrome/icon.png -------------------------------------------------------------------------------- /chrome/jsterm.js: -------------------------------------------------------------------------------- 1 | let Cu = Components.utils; 2 | let Ci = Components.interfaces; 3 | Cu.import("resource:///modules/source-editor.jsm"); 4 | Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm"); 5 | Cu.import("resource://gre/modules/Services.jsm"); 6 | Cu.import("resource:///modules/devtools/VariablesView.jsm"); 7 | 8 | /** 9 | * Todo 10 | * . keybindings for linux & windows 11 | * . Use jsm's 12 | * . delete listeners & map 13 | * . underline the current autocompletion item 14 | * . :connect (remote protocol) 15 | * . ctrl-r 16 | */ 17 | 18 | const JSTERM_MARK = "orion.annotation.jstermobject"; 19 | 20 | const compilers = { 21 | js: function(input) { 22 | return input; 23 | }, 24 | coffee: function(input) { 25 | return CoffeeScript.compile(input, {bare: true}).trim(); 26 | }, 27 | livescript: function(input) { 28 | return LiveScript.compile(input, {bare: true}).trim(); 29 | } 30 | }; 31 | 32 | let serializeNode = function(node) { 33 | var tag = node.tagName.toLowerCase(); 34 | var id = node.id ? '#' + node.id : ''; 35 | return '<' + tag + id + '>'; 36 | }; 37 | 38 | let JSTermUI = { 39 | input: new SourceEditor(), 40 | output: new SourceEditor(), 41 | objects: new Map(), 42 | printQueue: "", 43 | printTimeout: null, 44 | logCompiledCode: false, 45 | 46 | close: function() { 47 | this.toolbox.destroy(); 48 | }, 49 | 50 | registerCommands: function() { 51 | this.commands = [ 52 | {name: ":close", help: "close terminal", 53 | exec: this.close.bind(this)}, 54 | {name: ":clear", help: "clear screen", 55 | exec: this.clear.bind(this)}, 56 | {name: ":help", help: "show this help", 57 | exec: this.help.bind(this)}, 58 | {name: ":js", help: "switch to JS language", 59 | exec: this.switchToLanguage.bind(this, 'js')}, 60 | {name: ":coffee", help: "switch to CoffeeScript language", 61 | exec: this.switchToLanguage.bind(this, 'coffee')}, 62 | {name: ":livescript", help: "switch to LiveScript language", 63 | exec: this.switchToLanguage.bind(this, 'livescript')}, 64 | {name: ":logCompiled", help: "log compiled code for non-js languages", 65 | exec: this.logCompiled.bind(this)}, 66 | {name: ":content", help: "switch to Content mode", 67 | exec: this.switchToContentMode.bind(this)}, 68 | {name: ":chrome", help: "switch to Chrome mode", 69 | exec: this.switchToChromeMode.bind(this)}, 70 | {name: ":toggleLightTheme", help: "Toggle the light (white) theme", 71 | exec: this.toggleLightTheme.bind(this)}, 72 | {name: "ls", hidden: true, exec: this.ls.bind(this)}, 73 | ]; 74 | }, 75 | 76 | get multiline() { 77 | return this.inputContainer.classList.contains("multiline"); 78 | }, 79 | 80 | set multiline(val) { 81 | if (val) 82 | this.inputContainer.classList.add("multiline"); 83 | else 84 | this.inputContainer.classList.remove("multiline"); 85 | }, 86 | 87 | focus: function() { 88 | this.input.focus(); 89 | }, 90 | 91 | //init: function(aManager, aGlobalHistory, aBrowser, aContent, aChrome, aDefaultContent) { 92 | init: function(aGlobalHistory, aToolbox) { 93 | this.toolbox = aToolbox; 94 | 95 | this.content = this.toolbox.target.tab.linkedBrowser.contentWindow; 96 | this.chrome = this.toolbox.target.tab.ownerDocument.defaultView; 97 | 98 | this.version = "n/a"; 99 | this.chrome.AddonManager.getAddonByID("jsterm@paulrouget.com", function(addon) { 100 | this.version = addon.version; 101 | }.bind(this)); 102 | 103 | this.registerCommands(); 104 | 105 | this.handleKeys = this.handleKeys.bind(this); 106 | this.handleClick = this.handleClick.bind(this); 107 | this.focus = this.focus.bind(this); 108 | this.container = document.querySelector("#editors-container"); 109 | 110 | let defaultInputText = ""; 111 | let defaultOutputText = "// type ':help' for help\n// Report bug here: https://github.com/paulrouget/firefox-jsterm/issues"; 112 | 113 | this.history = new JSTermLocalHistory(aGlobalHistory); 114 | 115 | let outputContainer = document.querySelector("#output-container"); 116 | this.inputContainer = document.querySelector("#input-container"); 117 | this.output.init(outputContainer, { 118 | initialText: defaultOutputText, 119 | mode: SourceEditor.MODES.JAVASCRIPT, 120 | readOnly: true, 121 | theme: "chrome://jsterm/skin/orion.css", 122 | }, this.initOutput.bind(this)); 123 | 124 | this.input.init(this.inputContainer, { 125 | initialText: defaultInputText, 126 | mode: SourceEditor.MODES.JAVASCRIPT, 127 | theme: "chrome://jsterm/skin/orion.css", 128 | }, this.initInput.bind(this)); 129 | 130 | this.variableView = new VariablesView(document.querySelector("#variables")); 131 | 132 | let pref = "devtools.jsterm.language"; 133 | if (Services.prefs.prefHasUserValue(pref)) { 134 | this.languageName = Services.prefs.getCharPref(pref); 135 | } else { 136 | this.languageName = 'js'; 137 | } 138 | this.compile = compilers[this.languageName]; 139 | 140 | try { // This might be too early. But still, we try. 141 | if (Services.prefs.getBoolPref("devtools.jsterm.lightTheme")) { 142 | this._setLightTheme(); 143 | } 144 | } catch(e){} 145 | }, 146 | 147 | switchToChromeMode: function() { 148 | let label = document.querySelector("#completion-candidates > label"); 149 | this.sb = this.buildSandbox(this.chrome); 150 | this.print("// Switched to chrome mode."); 151 | if (this.completion) this.completion.destroy(); 152 | this.completion = new JSCompletion(this.input, label, this.sb); 153 | this.inputContainer.classList.add("chrome"); 154 | window.document.title = "JSTerm: (chrome) " + this.chrome.document.title; 155 | }, 156 | 157 | switchToLanguage: function(language) { 158 | this.languageName = language; 159 | Services.prefs.setCharPref("devtools.jsterm.language", language); 160 | this.compile = compilers[language].bind(this); 161 | 162 | if (language == "livescript") { 163 | for (let key in prelude) { 164 | this.defineSandboxProp(key, prelude[key]); 165 | } 166 | } 167 | }, 168 | 169 | logCompiled: function() { 170 | this.logCompiledCode = !this.logCompiledCode; 171 | }, 172 | 173 | switchToContentMode: function() { 174 | let label = document.querySelector("#completion-candidates > label"); 175 | let needMessage = !!this.sb; 176 | this.sb = this.buildSandbox(this.content); 177 | if (this.completion) this.completion.destroy(); 178 | this.completion = new JSCompletion(this.input, label, this.sb); 179 | 180 | if (needMessage) { 181 | this.print("// Switched to content mode."); 182 | } 183 | this.inputContainer.classList.remove("chrome"); 184 | window.document.title = "JSTerm: " + this.content.document.title; 185 | }, 186 | 187 | buildSandbox: function(win) { 188 | let sb = Cu.Sandbox(win, {sandboxPrototype: win, wantXrays: false}); 189 | this.target = win; 190 | sb.print = this.print.bind(this); 191 | 192 | this.defineSandboxProp('$', function(aSelector) { 193 | return win.document.querySelector(aSelector); 194 | }, sb); 195 | 196 | this.defineSandboxProp('$$', function(aSelector) { 197 | return win.document.querySelectorAll(aSelector); 198 | }, sb); 199 | 200 | if (this.languageName == "livescript") { 201 | for (let key in prelude) { 202 | this.defineSandboxProp(key, prelude[key], sb); 203 | } 204 | } 205 | 206 | return sb; 207 | }, 208 | 209 | defineSandboxProp: function(name, prop, sandbox = this.sb) { 210 | if (hasOwnProperty.call(sandbox, name)) return; 211 | try { 212 | sandbox[name] = prop 213 | } catch(ex) {} 214 | }, 215 | 216 | print: function(msg = "", startWith = "\n", isAnObject = false, object = null) { 217 | clearTimeout(this.printTimeout); 218 | 219 | if (isAnObject) { 220 | // let's do that synchronously, because we want to add a mark 221 | if (this.printQueue) { 222 | // flush 223 | this.output.setText(this.printQueue, this.output.getCharCount()); 224 | this.printQueue = ""; 225 | } 226 | this.output.setText(startWith + msg, this.output.getCharCount()); 227 | let line = this.output.getLineCount() - 1; 228 | this.objects.set(line, object); 229 | this.markRange(line); 230 | 231 | } else { 232 | this.printQueue += startWith + msg; 233 | 234 | this.printTimeout = setTimeout(function printCommit() { 235 | this.output.setText(this.printQueue, this.output.getCharCount()); 236 | this.printQueue = ""; 237 | }.bind(this), 0); 238 | } 239 | }, 240 | 241 | initOutput: function() { 242 | try { 243 | if (Services.prefs.getBoolPref("devtools.jsterm.lightTheme")) { 244 | this._setLightTheme(); 245 | } 246 | } catch(e){} 247 | 248 | this.makeEditorFitContent(this.output); 249 | this.ensureInputIsAlwaysVisible(this.output); 250 | this.output._annotationStyler.addAnnotationType(JSTERM_MARK); 251 | this.output.editorElement.addEventListener("click", this.handleClick, true); 252 | this.output.editorElement.addEventListener("keyup", this.focus, true); 253 | }, 254 | 255 | initInput: function() { 256 | try { 257 | if (Services.prefs.getBoolPref("devtools.jsterm.lightTheme")) { 258 | this._setLightTheme(); 259 | } 260 | } catch(e){} 261 | 262 | this.switchToContentMode(); 263 | 264 | this.makeEditorFitContent(this.input); 265 | this.ensureInputIsAlwaysVisible(this.input); 266 | this.input.editorElement.addEventListener("keydown", this.handleKeys, true); 267 | 268 | this.input.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, function() { 269 | this.multiline = this.isMultiline(this.input.getText()); 270 | }.bind(this)); 271 | 272 | this.input.editorElement.ownerDocument.defaultView.setTimeout(function() { 273 | this.input.focus(); 274 | }.bind(this), 0); 275 | }, 276 | 277 | makeEditorFitContent: function(editor) { 278 | let lineHeight = editor._view.getLineHeight(); 279 | editor.previousLineCount = editor.getLineCount(); 280 | this.setEditorSize(editor, Math.max(lineHeight * editor.previousLineCount, 1)); 281 | editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, function() { 282 | let count = editor.getLineCount(); 283 | if (count != editor.previousLineCount) { 284 | editor.previousLineCount = count; 285 | this.setEditorSize(editor, lineHeight * count); 286 | } 287 | }.bind(this)); 288 | }, 289 | 290 | setEditorSize: function(e, height) { 291 | let winHeight = e.editorElement.ownerDocument.defaultView.innerHeight; 292 | // We want to resize if the editor doesn't overflow on the Y axis. 293 | e.editorElement.style.minHeight = 294 | e.editorElement.style.maxHeight = 295 | e.editorElement.style.height = 296 | (e._view.getLineHeight() * e.getLineCount() + 297 | this.input.editorElement.scrollHeight <= winHeight 298 | ? (height) + "px" 299 | : ""); 300 | }, 301 | 302 | ensureInputIsAlwaysVisible: function(editor) { 303 | editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, function() { 304 | this.container.scrollTop = this.container.scrollTopMax; 305 | }.bind(this)); 306 | }, 307 | 308 | newEntry: function(rawCode) { 309 | if (this.evaluating) return; 310 | this.evaluating = true; 311 | 312 | this.history.stopBrowsing(); 313 | this.history.add(rawCode); 314 | 315 | this.input.setText(""); 316 | this.multiline = false; 317 | 318 | if (rawCode == "") { 319 | this.print(); 320 | this.onceEntryResultPrinted(); 321 | return; 322 | } 323 | 324 | for (let cmd of this.commands) { 325 | if (cmd.name == rawCode) { 326 | this.print(rawCode); 327 | cmd.exec(); 328 | this.onceEntryResultPrinted(); 329 | return; 330 | } 331 | } 332 | 333 | let code; 334 | 335 | try { 336 | code = this.compile(rawCode); 337 | } catch(ex) { 338 | this.dumpEntryResult('', ex.toString().slice(7), rawCode); 339 | this.onceEntryResultPrinted(); 340 | return; 341 | } 342 | 343 | var output = this.languageName != 'js' && this.logCompiledCode ? 344 | rawCode + '\n\n/*' + code + '*/' : rawCode; 345 | this.print(output); 346 | 347 | let error, result; 348 | try { 349 | result = Cu.evalInSandbox(code, this.sb, "1.8", "JSTerm", 1); 350 | } catch (ex) { 351 | error = ex; 352 | } 353 | 354 | this.dumpEntryResult(result, error, rawCode); 355 | this.onceEntryResultPrinted(); 356 | }, 357 | 358 | onceEntryResultPrinted: function() { 359 | /* Ugly hack to scrollback */ 360 | this.output.editorElement.contentDocument.querySelector("iframe") 361 | .contentDocument.querySelector(".view").scrollLeft = 0; 362 | 363 | /* Clear Selection if any */ 364 | let cursor = this.output.getLineStart(this.output.getLineCount() - 1); 365 | this.output.setSelection(cursor, cursor); 366 | 367 | this.evaluating = false; 368 | }, 369 | 370 | dumpEntryResult: function(result, error, code) { 371 | if (error) { 372 | error = error.toString(); 373 | if (this.isMultiline(error) || this.isMultiline(code)) { 374 | this.print("/* error:\n" + error + "\n*/"); 375 | } else { 376 | this.print(" // error: " + error, startWith = ""); 377 | } 378 | return; 379 | } 380 | 381 | let maxLength = 80; 382 | let type = ({}).toString.call(result).slice(8, -1); 383 | let isAnObject = typeof result == "object"; 384 | let elementClass = /^HTML\w+Element$/; 385 | 386 | let resultStr; 387 | if (result == null) { 388 | resultStr = "" + result; 389 | isAnObject = false; 390 | } else if (type == "String") { 391 | resultStr = "\"" + result + "\""; 392 | } else if (type == "NodeList") { 393 | let isEmpty = result.length == 0; 394 | let tagNames = [].slice.call(result).map(serializeNode); 395 | resultStr = "[" + tagNames.join(", ") + "]"; 396 | } else if (elementClass.test(type)) { 397 | resultStr = serializeNode(result); 398 | } else if (isAnObject && 'length' in result) { 399 | let serialized = [].slice.call(result) 400 | .map(function(item) { 401 | let cls = toString.call(item).slice(8, -1); 402 | if (elementClass.test(cls)) { 403 | return serializeNode(item); 404 | } else { 405 | return item; 406 | } 407 | }) 408 | .join(", "); 409 | resultStr = "[" + serialized + "]"; 410 | } else { 411 | resultStr = result.toString(); 412 | } 413 | 414 | if (code == resultStr) { 415 | return; 416 | } 417 | 418 | // TODO: Check for long output that looks shitty. 419 | // if (resultStr.length > maxLength) { 420 | // resultStr = resultStr.slice(0, maxLength) + ' ...'; 421 | // } 422 | 423 | if (isAnObject) { 424 | resultStr += " [+]"; 425 | } 426 | 427 | if (this.isMultiline(resultStr)) { 428 | if (type == "Function") { 429 | resultStr = "\n" + resultStr; 430 | } else { 431 | resultStr = "\n/*\n" + resultStr + "\n*/"; 432 | } 433 | } else { 434 | if (this.isMultiline(code)) { 435 | resultStr = "\n// " + resultStr; 436 | } else { 437 | resultStr = " // " + resultStr; 438 | } 439 | } 440 | 441 | this.print(resultStr, startWith = "", isAnObject, isAnObject ? result : null); 442 | }, 443 | 444 | isMultiline: function(text) { 445 | return text.indexOf("\n") > -1; 446 | }, 447 | 448 | clear: function() { 449 | this.objects = new Map(); 450 | this.output.setText(""); 451 | this.hideObjInspector(); 452 | }, 453 | 454 | help: function() { 455 | let text = "/**"; 456 | text += "\n * JSTerm (version " + this.version + ")"; 457 | text += "\n * "; 458 | text += "\n * 'Return' to evaluate entry,"; 459 | text += "\n * 'Tab' for autocompletion,"; 460 | text += "\n * 'Ctrl-l' clear screen,"; 461 | text += "\n * 'Ctrl-d' close term,"; 462 | text += "\n * 'up/down' to browser history,"; 463 | text += "\n * 'Shift+Return' to switch to multiline editing,"; 464 | text += "\n * 'Shift+Return' to evaluate multiline entry,"; 465 | text += "\n * "; 466 | text += "\n * Use 'print(aString)' to dump text in the terminal,"; 467 | text += "\n * Click on [+] to inspect an object,"; 468 | text += "\n * "; 469 | text += "\n * Commands:"; 470 | for (let cmd of this.commands) { 471 | if (cmd.help) { 472 | text += "\n * " + cmd.name + " - " + cmd.help; 473 | } 474 | } 475 | text += "\n * "; 476 | text += "\n * Bugs? Suggestions? Questions? -> https://github.com/paulrouget/firefox-jsterm/issues"; 477 | text += "\n */"; 478 | this.print(text); 479 | }, 480 | 481 | handleKeys: function(e) { 482 | let code = this.input.getText(); 483 | 484 | if (e.keyCode != 38 && e.keyCode != 40) { 485 | this.history.stopBrowsing(); 486 | } 487 | 488 | if (e.keyCode == 13 && e.shiftKey) { 489 | if (this.multiline) { 490 | e.stopPropagation(); 491 | e.preventDefault(); 492 | this.newEntry(code); 493 | } else { 494 | this.multiline = true; 495 | } 496 | } 497 | 498 | if (e.keyCode == 13 && !e.shiftKey) { 499 | if (this.multiline) { 500 | // Do nothing. 501 | } else { 502 | e.stopPropagation(); 503 | e.preventDefault(); 504 | this.newEntry(code); 505 | } 506 | } 507 | 508 | if (e.keyCode == 68 && e.ctrlKey) { 509 | e.stopPropagation(); 510 | e.preventDefault(); 511 | this.close(); 512 | } 513 | if (e.keyCode == 76 && e.ctrlKey) { 514 | e.stopPropagation(); 515 | e.preventDefault(); 516 | this.clear(); 517 | } 518 | 519 | if (e.keyCode == 38) { 520 | if (!this.history.isBrowsing() && this.multiline) { 521 | return; 522 | } 523 | e.stopPropagation(); 524 | e.preventDefault(); 525 | if (!this.history.isBrowsing() ) { 526 | this.history.startBrowsing(this.input.getText()); 527 | } 528 | let entry = this.history.goBack(); 529 | if (entry) { 530 | JSTermUI.input.setText(entry); 531 | JSTermUI.input.setCaretPosition(JSTermUI.input.getLineCount(), 1000); 532 | } 533 | } 534 | if (e.keyCode == 40) { 535 | if (this.history.isBrowsing()) { 536 | e.stopPropagation(); 537 | e.preventDefault(); 538 | let entry = this.history.goForward(); 539 | JSTermUI.input.setText(entry); 540 | JSTermUI.input.setCaretPosition(JSTermUI.input.getLineCount(), 1000); 541 | } 542 | } 543 | }, 544 | 545 | handleClick: function(e) { 546 | if (e.target.parentNode && e.target.parentNode.lineIndex) { 547 | let idx = e.target.parentNode.lineIndex; 548 | if (this.objects.has(idx)) { 549 | let obj = this.objects.get(idx); 550 | e.stopPropagation(); 551 | this.inspect(obj); 552 | } 553 | } 554 | }, 555 | 556 | markRange: function(line) { 557 | let annotation = { 558 | type: JSTERM_MARK, 559 | start: this.output.getLineStart(line), 560 | end: this.output.getLineEnd(line), 561 | title: "Object", 562 | lineStyle: {styleClass: "annotationLine object"}, 563 | } 564 | this.output._annotationModel.addAnnotation(annotation); 565 | }, 566 | 567 | 568 | destroy: function() { 569 | this.input.editorElement.removeEventListener("keydown", this.handleKeys, true); 570 | if (this.completion) this.completion.destroy(); 571 | this.completion = null; 572 | this.treeview = null; 573 | this.input = null; 574 | this.output = null; 575 | this.objects = null; 576 | this.printQueue = null; 577 | this.printTimeout = null; 578 | this.compile = null; 579 | }, 580 | 581 | inspect: function(obj) { 582 | let box = document.querySelector("#variables"); 583 | box.hidden = false; 584 | this.variableView.rawObject = obj; 585 | this.focus(); 586 | }, 587 | 588 | hideObjInspector: function() { 589 | this.variableView.empty(); 590 | let box = document.querySelector("#variables"); 591 | box.hidden = true; 592 | }, 593 | 594 | getContent: function() { 595 | return { 596 | input: this.input.getText(), 597 | output: this.output.getText(), 598 | }; 599 | }, 600 | 601 | ls: function() { 602 | this.print("// Did you just type \"ls\"? You know this is not a unix shell, right?"); 603 | }, 604 | 605 | toggleLightTheme: function() { 606 | let isLight = document.documentElement.classList.contains("light"); 607 | 608 | Services.prefs.setBoolPref("devtools.jsterm.lightTheme", !isLight); 609 | 610 | if (isLight) { 611 | this._setDarkTheme(); 612 | } else { 613 | this._setLightTheme(); 614 | } 615 | }, 616 | 617 | _setLightTheme: function() { 618 | document.documentElement.classList.add("light"); 619 | let inputView = this.input.editorElement.contentDocument.querySelector("iframe") 620 | .contentDocument.querySelector(".view"); 621 | inputView.classList.add("light"); 622 | let outputView = this.output.editorElement.contentDocument.querySelector("iframe") 623 | .contentDocument.querySelector(".view"); 624 | outputView.classList.add("light"); 625 | }, 626 | 627 | _setDarkTheme: function() { 628 | document.documentElement.classList.remove("light"); 629 | let inputView = this.input.editorElement.contentDocument.querySelector("iframe") 630 | .contentDocument.querySelector(".view"); 631 | inputView.classList.remove("light"); 632 | let outputView = this.output.editorElement.contentDocument.querySelector("iframe") 633 | .contentDocument.querySelector(".view"); 634 | outputView.classList.remove("light"); 635 | }, 636 | } 637 | 638 | 639 | 640 | /* Auto Completion */ 641 | 642 | function JSCompletion(editor, candidatesWidget, sandbox) { 643 | this.editor = editor; 644 | this.candidatesWidget = candidatesWidget; 645 | 646 | this.handleKeys = this.handleKeys.bind(this); 647 | 648 | this.editor.editorElement.addEventListener("keydown", this.handleKeys, true); 649 | 650 | this.buildDictionnary(); 651 | 652 | this.sb = sandbox; 653 | } 654 | 655 | JSCompletion.prototype = { 656 | buildDictionnary: function() { 657 | let JSKeywords = "break delete case do catch else class export continue finally const for debugger function default if import this in throw instanceof try let typeof new var return void super while switch with"; 658 | this.dictionnary = JSKeywords.split(" "); 659 | for (let cmd of JSTermUI.commands) { 660 | if (!cmd.hidden) { 661 | this.dictionnary.push(cmd.name); 662 | } 663 | } 664 | }, 665 | handleKeys: function(e) { 666 | if (e.keyCode == 9) { 667 | this.handleTab(e); 668 | } else { 669 | this.stopCompletion(); 670 | } 671 | }, 672 | handleTab: function(e) { 673 | if (this.isCompleting) { 674 | this.continueCompleting(); 675 | e.stopPropagation(); 676 | e.preventDefault(); 677 | return; 678 | } 679 | 680 | // Can we complete? 681 | let caret = this.editor.getCaretPosition(); 682 | if (caret.col == 0) return; 683 | 684 | let lines = this.editor.getText().split("\n"); 685 | let line = lines[caret.line] 686 | let previousChar = line[caret.col - 1]; 687 | 688 | if (!previousChar.match(/\w|\.|:/i)) return; 689 | 690 | // Initiate Completion 691 | e.preventDefault(); 692 | e.stopPropagation(); 693 | 694 | let root = line.substr(0, caret.col); 695 | 696 | let candidates = JSPropertyProvider(this.sb, root); 697 | 698 | let completeFromDict = false; 699 | if (candidates && candidates.matchProp) { 700 | if (root.length == candidates.matchProp.length) { 701 | completeFromDict = true; 702 | } else { 703 | let charBeforeProp = root[root.length - candidates.matchProp.length - 1]; 704 | if (charBeforeProp.match(/\s|{|;|\(/)) { 705 | completeFromDict = true; 706 | } 707 | } 708 | } 709 | if (completeFromDict) { 710 | for (let word of this.dictionnary) { 711 | if (word.indexOf(candidates.matchProp) == 0) { 712 | candidates.matches.push(word); 713 | } 714 | } 715 | } 716 | 717 | if (!candidates || candidates.matches.length == 0) return; 718 | 719 | let offset = this.editor.getCaretOffset(); 720 | 721 | // if one candidate 722 | if (candidates.matches.length == 1) { 723 | let suffix = candidates.matches[0].substr(candidates.matchProp.length); 724 | this.editor.setText(suffix, offset, offset); 725 | return; 726 | } 727 | 728 | // if several candidate 729 | 730 | let commonPrefix = candidates.matches.reduce(function(commonPrefix, nextValue) { 731 | if (commonPrefix == "") 732 | return ""; 733 | 734 | if (!commonPrefix) 735 | return nextValue; 736 | 737 | if (commonPrefix.length > nextValue.length) { 738 | commonPrefix = commonPrefix.substr(0, nextValue.length); 739 | } 740 | let res = ""; 741 | let idx = 0; 742 | for (let p = 0; p < commonPrefix.length; p++) { 743 | let c = commonPrefix[p]; 744 | if (nextValue[idx++] == c) 745 | res += c; 746 | else 747 | break; 748 | } 749 | return res; 750 | }); 751 | 752 | if (commonPrefix) { 753 | let suffix = commonPrefix.substr(candidates.matchProp.length); 754 | this.editor.setText(suffix, offset, offset); 755 | offset += suffix.length; 756 | candidates.matchProp = commonPrefix; 757 | } 758 | 759 | this.whereToInsert = {start: offset, end: offset}; 760 | this.candidates = candidates; 761 | this.candidatesWidget.setAttribute("value", this.candidates.matches.join(" ")); 762 | this.isCompleting = true; 763 | 764 | if (this.candidates.matches[0] == this.candidates.matchProp) 765 | this.candidatesIndex = 0; 766 | else 767 | this.candidatesIndex = -1; 768 | }, 769 | 770 | continueCompleting: function() { 771 | this.candidatesIndex++; 772 | if (this.candidatesIndex == this.candidates.matches.length) { 773 | this.candidatesIndex = 0; 774 | } 775 | 776 | let prefixLength = this.candidates.matchProp.length; 777 | let suffix = this.candidates.matches[this.candidatesIndex].substr(prefixLength); 778 | this.editor.setText(suffix, this.whereToInsert.start, this.whereToInsert.end); 779 | this.whereToInsert.end = this.whereToInsert.start + suffix.length; 780 | }, 781 | 782 | stopCompletion: function() { 783 | if (!this.isCompleting) return; 784 | this.candidatesWidget.setAttribute("value", ""); 785 | this.isCompleting = false; 786 | this.candidates = null; 787 | }, 788 | destroy: function() { 789 | this.editor.editorElement.removeEventListener("keydown", this.handleKeys, true); 790 | this.editor = null; 791 | }, 792 | } 793 | 794 | /** HISTORY **/ 795 | 796 | function JSTermLocalHistory(aGlobalHistory) { 797 | this.global = aGlobalHistory; 798 | } 799 | JSTermLocalHistory.prototype = { 800 | _browsing: false, 801 | isBrowsing: function() { 802 | return this._browsing; 803 | }, 804 | startBrowsing: function(aInitialValue) { 805 | this._browsing = true; 806 | this.cursor = this.global.getCursor(aInitialValue); 807 | }, 808 | stopBrowsing: function() { 809 | if (this.isBrowsing()) { 810 | this._browsing = false; 811 | this.global.releaseCursor(this.cursor); 812 | this.cursor = null; 813 | } 814 | }, 815 | add: function(entry) { 816 | this.global.add(entry); 817 | }, 818 | canGoBack: function() { 819 | return this.isBrowsing() && this.global.canGoBack(this.cursor); 820 | }, 821 | canGoForward: function() { 822 | return this.isBrowsing() && this.global.canGoForward(this.cursor); 823 | }, 824 | goBack: function() { 825 | if (this.canGoBack()) { 826 | this.global.goBack(this.cursor); 827 | let entry = this.global.getEntryForCursor(this.cursor); 828 | return entry; 829 | } 830 | return null; 831 | }, 832 | goForward: function() { 833 | if (this.canGoForward()) { 834 | this.global.goForward(this.cursor); 835 | let entry = this.global.getEntryForCursor(this.cursor); 836 | return entry; 837 | } 838 | return null; 839 | }, 840 | } 841 | -------------------------------------------------------------------------------- /chrome/jsterm.xul: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | %JSTermDTD; 10 | ]> 11 | 12 | 21 | 22 |