merge fx-team to m-c

This commit is contained in:
Rob Campbell 2011-08-13 11:21:59 -03:00
commit 5a56c6a415
27 changed files with 11145 additions and 199 deletions

View File

@ -58,7 +58,7 @@ $(NSINSTALL) -D $(dir) && \
$(PYTHON) $(MOZILLA_DIR)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $(srcdir)/$(dir)/install.rdf.in > $(dir)/install.rdf && \
cd $(dir) && \
$(ZIP) -r9XD $(DISTROEXT)/$(dir).xpi install.rdf && \
cd $(srcdir)/$(dir) && \
cd $(call core_abspath,$(srcdir)/$(dir)) && \
$(ZIP) -r9XD $(DISTROEXT)/$(dir).xpi * -x install.rdf.in
endef # do not remove the blank line!

View File

@ -1045,6 +1045,17 @@ pref("devtools.hud.loglimit.console", 200);
pref("devtools.editor.tabsize", 4);
pref("devtools.editor.expandtab", true);
// Tells which component you want to use for source editing in developer tools.
//
// Available components:
// "textarea" - this is a basic text editor, like an HTML <textarea>.
//
// "orion" - this is the Orion source code editor from the Eclipse project. It
// provides programmer-specific editor features such as syntax highlighting,
// indenting and bracket recognition. It may not be appropriate for all
// locales (esp. RTL) or a11y situations.
pref("devtools.editor.component", "textarea");
// Whether the character encoding menu is under the main Firefox button. This
// preference is a string so that localizers can alter it.
pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");

View File

@ -48,6 +48,7 @@ include $(topsrcdir)/config/config.mk
DIRS = \
webconsole \
scratchpad \
sourceeditor \
$(NULL)
ifdef ENABLE_TESTS

View File

@ -2,3 +2,5 @@ browser.jar:
content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml)
* content/browser/scratchpad.xul (scratchpad/scratchpad.xul)
* content/browser/scratchpad.js (scratchpad/scratchpad.js)
content/browser/orion.js (sourceeditor/orion/orion.js)
content/browser/orion.css (sourceeditor/orion/orion.css)

View File

@ -48,6 +48,8 @@
* https://bugzilla.mozilla.org/show_bug.cgi?id=653934
*/
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
@ -56,6 +58,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource:///modules/PropertyPanel.jsm");
Cu.import("resource:///modules/source-editor.jsm");
const SCRATCHPAD_CONTEXT_CONTENT = 1;
const SCRATCHPAD_CONTEXT_BROWSER = 2;
@ -64,9 +67,6 @@ const SCRATCHPAD_L10N = "chrome://browser/locale/scratchpad.properties";
const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
const PREF_TABSIZE = "devtools.editor.tabsize";
const PREF_EXPANDTAB = "devtools.editor.expandtab";
/**
* The scratchpad object handles the Scratchpad window functionality.
*/
@ -83,12 +83,6 @@ var Scratchpad = {
*/
executionContext: SCRATCHPAD_CONTEXT_CONTENT,
/**
* Retrieve the xul:textbox DOM element. This element holds the source code
* the user writes and executes.
*/
get textbox() document.getElementById("scratchpad-textbox"),
/**
* Retrieve the xul:statusbarpanel DOM element. The status bar tells the
* current code execution context.
@ -96,12 +90,46 @@ var Scratchpad = {
get statusbarStatus() document.getElementById("scratchpad-status"),
/**
* Get the selected text from the textbox.
* Get the selected text from the editor.
*
* @return string
* The selected text.
*/
get selectedText()
get selectedText() this.editor.getSelectedText(),
/**
* Get the editor content, in the given range. If no range is given you get
* the entire editor content.
*
* @param number [aStart=0]
* Optional, start from the given offset.
* @param number [aEnd=content char count]
* Optional, end offset for the text you want. If this parameter is not
* given, then the text returned goes until the end of the editor
* content.
* @return string
* The text in the given range.
*/
getText: function SP_getText(aStart, aEnd)
{
return this.textbox.value.substring(this.textbox.selectionStart,
this.textbox.selectionEnd);
return this.editor.getText(aStart, aEnd);
},
/**
* Replace text in the source editor with the given text, in the given range.
*
* @param string aText
* The text you want to put into the editor.
* @param number [aStart=0]
* Optional, the start offset, zero based, from where you want to start
* replacing text in the editor.
* @param number [aEnd=char count]
* Optional, the end offset, zero based, where you want to stop
* replacing text in the editor.
*/
setText: function SP_setText(aText, aStart, aEnd)
{
this.editor.setText(aText, aStart, aEnd);
},
/**
@ -124,11 +152,6 @@ var Scratchpad = {
return recentWin ? recentWin.gBrowser : null;
},
insertIntro: function SP_insertIntro()
{
this.textbox.value = this.strings.GetStringFromName("scratchpadIntro");
},
/**
* Cached Cu.Sandbox object for the active tab content window object.
*/
@ -197,15 +220,15 @@ var Scratchpad = {
},
/**
* Drop the textbox selection.
* Drop the editor selection.
*/
deselect: function SP_deselect()
{
this.textbox.selectionEnd = this.textbox.selectionStart;
this.editor.dropSelection();
},
/**
* Select a specific range in the Scratchpad xul:textbox.
* Select a specific range in the Scratchpad editor.
*
* @param number aStart
* Selection range start.
@ -214,8 +237,19 @@ var Scratchpad = {
*/
selectRange: function SP_selectRange(aStart, aEnd)
{
this.textbox.selectionStart = aStart;
this.textbox.selectionEnd = aEnd;
this.editor.setSelection(aStart, aEnd);
},
/**
* Get the current selection range.
*
* @return object
* An object with two properties, start and end, that give the
* selection range (zero based offsets).
*/
getSelectionRange: function SP_getSelection()
{
return this.editor.getSelection();
},
/**
@ -293,19 +327,19 @@ var Scratchpad = {
},
/**
* Execute the selected text (if any) or the entire textbox content in the
* Execute the selected text (if any) or the entire editor content in the
* current context.
*/
run: function SP_run()
{
let selection = this.selectedText || this.textbox.value;
let selection = this.selectedText || this.getText();
let result = this.evalForContext(selection);
this.deselect();
return [selection, result];
},
/**
* Execute the selected text (if any) or the entire textbox content in the
* Execute the selected text (if any) or the entire editor content in the
* current context. The resulting object is opened up in the Property Panel
* for inspection.
*/
@ -319,34 +353,29 @@ var Scratchpad = {
},
/**
* Execute the selected text (if any) or the entire textbox content in the
* current context. The evaluation result is inserted into the textbox after
* the selected text, or at the end of the textbox value if there is no
* Execute the selected text (if any) or the entire editor content in the
* current context. The evaluation result is inserted into the editor after
* the selected text, or at the end of the editor content if there is no
* selected text.
*/
display: function SP_display()
{
let selectionStart = this.textbox.selectionStart;
let selectionEnd = this.textbox.selectionEnd;
if (selectionStart == selectionEnd) {
selectionEnd = this.textbox.value.length;
}
let selection = this.getSelectionRange();
let insertionPoint = selection.start != selection.end ?
selection.end : // after selected text
this.editor.getCharCount(); // after text end
let [selection, result] = this.run();
let [selectedText, result] = this.run();
if (!result) {
return;
}
let firstPiece = this.textbox.value.slice(0, selectionEnd);
let lastPiece = this.textbox.value.
slice(selectionEnd, this.textbox.value.length);
let newComment = "/*\n" + result.toString() + "\n*/";
this.textbox.value = firstPiece + newComment + lastPiece;
this.setText(newComment, insertionPoint, insertionPoint);
// Select the added comment.
this.selectRange(firstPiece.length, firstPiece.length + newComment.length);
// Select the new comment.
this.selectRange(insertionPoint, insertionPoint + newComment.length);
},
/**
@ -442,12 +471,12 @@ var Scratchpad = {
let fs = Cc["@mozilla.org/network/file-output-stream;1"].
createInstance(Ci.nsIFileOutputStream);
let modeFlags = 0x02 | 0x08 | 0x20;
fs.init(aFile, modeFlags, 0644, fs.DEFER_OPEN);
fs.init(aFile, modeFlags, 420 /* 0644 */, fs.DEFER_OPEN);
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let input = converter.convertToInputStream(this.textbox.value);
let input = converter.convertToInputStream(this.getText());
let self = this;
NetUtil.asyncCopy(input, fs, function(aStatus) {
@ -488,7 +517,7 @@ var Scratchpad = {
if (Components.isSuccessCode(aStatus)) {
content = NetUtil.readInputStreamToString(aInputStream,
aInputStream.available());
self.textbox.value = content;
self.setText(content);
}
else if (!aSilentError) {
window.alert(self.strings.GetStringFromName("openFile.failed"));
@ -615,10 +644,17 @@ var Scratchpad = {
},
/**
* The Scratchpad window DOMContentLoaded event handler.
* The Scratchpad window DOMContentLoaded event handler. This method
* initializes the Scratchpad window and source editor.
*
* @param nsIDOMEvent aEvent
*/
onLoad: function SP_onLoad()
onLoad: function SP_onLoad(aEvent)
{
if (aEvent.target != document) {
return;
}
let chromeContextMenu = document.getElementById("sp-menu-browser");
let errorConsoleMenu = document.getElementById("sp-menu-errorConsole");
let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
@ -632,55 +668,107 @@ var Scratchpad = {
chromeContextCommand.removeAttribute("disabled");
}
let tabsize = Services.prefs.getIntPref(PREF_TABSIZE);
if (tabsize < 1) {
// tabsize is invalid, clear back to the default value.
Services.prefs.clearUserPref(PREF_TABSIZE);
tabsize = Services.prefs.getIntPref(PREF_TABSIZE);
}
this.editor = new SourceEditor();
let expandtab = Services.prefs.getBoolPref(PREF_EXPANDTAB);
this._tabCharacter = expandtab ? (new Array(tabsize + 1)).join(" ") : "\t";
this.textbox.style.MozTabSize = tabsize;
let config = {
mode: SourceEditor.MODES.JAVASCRIPT,
showLineNumbers: true,
placeholderText: this.strings.GetStringFromName("scratchpadIntro"),
};
// Force LTR direction (otherwise the textbox inherits the locale direction)
this.textbox.style.direction = "ltr";
this.insertIntro();
// Make the Tab key work.
this.textbox.addEventListener("keypress", this.onKeypress.bind(this), false);
this.textbox.focus();
let editorPlaceholder = document.getElementById("scratchpad-editor");
this.editor.init(editorPlaceholder, config, this.onEditorLoad.bind(this));
},
/**
* The textbox keypress event handler which allows users to indent code using
* the Tab key.
*
* @param nsIDOMEvent aEvent
* The load event handler for the source editor. This method does post-load
* editor initialization.
*/
onKeypress: function SP_onKeypress(aEvent)
onEditorLoad: function SP_onEditorLoad()
{
if (aEvent.keyCode == aEvent.DOM_VK_TAB) {
this.insertTextAtCaret(this._tabCharacter);
aEvent.preventDefault();
}
this.editor.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
this.onContextMenu);
this.editor.focus();
this.editor.setCaretOffset(this.editor.getCharCount());
},
/**
* Insert text at the current caret location.
*
* @param string aText
* The text you want to insert.
*/
insertTextAtCaret: function SP_insertTextAtCaret(aText)
{
let firstPiece = this.textbox.value.substring(0, this.textbox.selectionStart);
let lastPiece = this.textbox.value.substring(this.textbox.selectionEnd);
this.textbox.value = firstPiece + aText + lastPiece;
let caretOffset = this.editor.getCaretOffset();
this.setText(aText, caretOffset, caretOffset);
this.editor.setCaretOffset(caretOffset + aText.length);
},
let newCaretPosition = firstPiece.length + aText.length;
this.selectRange(newCaretPosition, newCaretPosition);
/**
* The contextmenu event handler for the source editor. This method opens the
* Scratchpad context menu popup at the pointer location.
*
* @param object aEvent
* An event object coming from the SourceEditor. This object needs to
* hold the screenX and screenY properties.
*/
onContextMenu: function SP_onContextMenu(aEvent)
{
let menu = document.getElementById("scratchpad-text-popup");
if (menu.state == "closed") {
menu.openPopupAtScreen(aEvent.screenX, aEvent.screenY, true);
}
},
/**
* The popupshowing event handler for the Edit menu. This method updates the
* enabled/disabled state of the Undo and Redo commands, based on the editor
* state such that the menu items render correctly for the user when the menu
* shows.
*/
onEditPopupShowing: function SP_onEditPopupShowing()
{
let undo = document.getElementById("sp-cmd-undo");
undo.setAttribute("disabled", !this.editor.canUndo());
let redo = document.getElementById("sp-cmd-redo");
redo.setAttribute("disabled", !this.editor.canRedo());
},
/**
* Undo the last action of the user.
*/
undo: function SP_undo()
{
this.editor.undo();
},
/**
* Redo the previously undone action.
*/
redo: function SP_redo()
{
this.editor.redo();
},
/**
* The Scratchpad window unload event handler. This method unloads/destroys
* the source editor.
*
* @param nsIDOMEvent aEvent
*/
onUnload: function SP_onUnload(aEvent)
{
if (aEvent.target != document) {
return;
}
this.resetContext();
this.editor.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
this.onContextMenu);
this.editor.destroy();
this.editor = null;
},
};
@ -689,4 +777,4 @@ XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
});
addEventListener("DOMContentLoaded", Scratchpad.onLoad.bind(Scratchpad), false);
addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);

View File

@ -78,6 +78,8 @@
<command id="sp-cmd-resetContext" oncommand="Scratchpad.resetContext();"/>
<command id="sp-cmd-errorConsole" oncommand="Scratchpad.openErrorConsole();" disabled="true"/>
<command id="sp-cmd-webConsole" oncommand="Scratchpad.openWebConsole();"/>
<command id="sp-cmd-undo" oncommand="Scratchpad.undo();" disabled="true"/>
<command id="sp-cmd-redo" oncommand="Scratchpad.redo();" disabled="true"/>
</commandset>
<keyset id="sp-keyset">
@ -116,8 +118,10 @@
key="&pasteCmd.key;"
modifiers="accel"/>
<key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/>
<key id="key_undo" key="&undoCmd.key;" modifiers="accel"/>
<key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift"/>
<key id="key_undo" key="&undoCmd.key;" modifiers="accel"
oncommand="Scratchpad.undo();"/>
<key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift"
oncommand="Scratchpad.redo();"/>
<key id="sp-key-run"
key="&run.key;"
command="sp-cmd-run"
@ -185,19 +189,18 @@
<menu id="sp-edit-menu" label="&editMenu.label;"
accesskey="&editMenu.accesskey;">
<menupopup id="sp-menu_editpopup">
<menupopup id="sp-menu_editpopup"
onpopupshowing="Scratchpad.onEditPopupShowing()">
<menuitem id="sp-menu-undo"
label="&undoCmd.label;"
key="key_undo"
accesskey="&undoCmd.accesskey;"
disabled="true"
command="cmd_undo"/>
command="sp-cmd-undo"/>
<menuitem id="sp-menu-redo"
label="&redoCmd.label;"
key="key_redo"
disabled="true"
accesskey="&redoCmd.accesskey;"
command="cmd_redo"/>
command="sp-cmd-redo"/>
<menuseparator/>
<menuitem id="sp-menu-cut"
label="&cutCmd.label;"
@ -329,12 +332,8 @@
</menupopup>
</popupset>
<textbox id="scratchpad-textbox"
class="monospace"
multiline="true"
flex="1"
context="scratchpad-text-popup"
placeholder="&textbox.placeholder1;" />
<hbox id="scratchpad-editor" flex="1" context="scratchpad-text-popup" />
<statusbar id="scratchpad-statusbar" align="end">
<statusbarpanel id="scratchpad-status"
label="&contentContext.label;"

View File

@ -31,34 +31,33 @@ function runTests()
let sp = gScratchpadWindow.Scratchpad;
ok(sp, "Scratchpad object exists in new window");
is(gScratchpadWindow.document.activeElement, sp.textbox.inputField,
"The textbox has focus");
ok(sp.editor.hasFocus(), "the editor has focus");
is(sp.textbox.style.MozTabSize, 5, "-moz-tab-size is correct");
sp.textbox.value = "window.foo;";
sp.selectRange(1, 3);
sp.setText("window.foo;");
sp.editor.setCaretOffset(0);
EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
is(sp.textbox.value, "w dow.foo;",
"Tab key added 5 spaces");
is(sp.getText(), " window.foo;", "Tab key added 5 spaces");
is(sp.textbox.selectionStart, 6, "caret location is correct");
is(sp.editor.getCaretOffset(), 5, "caret location is correct");
is(sp.textbox.selectionStart, sp.textbox.selectionEnd,
"caret location is correct, confirmed");
sp.editor.setCaretOffset(6);
EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
is(sp.getText(), " w indow.foo;",
"Tab key added 4 spaces");
is(sp.editor.getCaretOffset(), 10, "caret location is correct");
// Test the new insertTextAtCaret() method.
sp.insertTextAtCaret("omg");
is(sp.textbox.value, "w omgdow.foo;", "insertTextAtCaret() works");
is(sp.getText(), " w omgindow.foo;", "insertTextAtCaret() works");
is(sp.textbox.selectionStart, 9, "caret location is correct after update");
is(sp.textbox.selectionStart, sp.textbox.selectionEnd,
"caret location is correct, confirmed");
is(sp.editor.getCaretOffset(), 13, "caret location is correct after update");
gScratchpadWindow.close();
@ -74,36 +73,14 @@ function runTests2()
let sp = gScratchpadWindow.Scratchpad;
is(sp.textbox.style.MozTabSize, 6, "-moz-tab-size is correct");
sp.textbox.value = "window.foo;";
sp.selectRange(1, 3);
sp.setText("window.foo;");
sp.editor.setCaretOffset(0);
EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
is(sp.textbox.value, "w\tdow.foo;", "Tab key added the tab character");
is(sp.getText(), "\twindow.foo;", "Tab key added the tab character");
is(sp.textbox.selectionStart, 2, "caret location is correct");
is(sp.textbox.selectionStart, sp.textbox.selectionEnd,
"caret location is correct, confirmed");
gScratchpadWindow.close();
// check with an invalid tabsize value.
Services.prefs.setIntPref("devtools.editor.tabsize", 0);
Services.prefs.setBoolPref("devtools.editor.expandtab", true);
gScratchpadWindow = Scratchpad.openScratchpad();
gScratchpadWindow.addEventListener("load", runTests3, false);
}
function runTests3()
{
gScratchpadWindow.removeEventListener("load", arguments.callee, false);
let sp = gScratchpadWindow.Scratchpad;
is(sp.textbox.style.MozTabSize, 4, "-moz-tab-size is correct");
is(sp.editor.getCaretOffset(), 1, "caret location is correct");
Services.prefs.clearUserPref("devtools.editor.tabsize");
Services.prefs.clearUserPref("devtools.editor.expandtab");

View File

@ -48,8 +48,7 @@ function runTests()
is(statusbar.getAttribute("label"), contentMenu.getAttribute("label"),
"statusbar label is correct");
ok(sp.textbox, "textbox exists");
sp.textbox.value = "window.foobarBug636725 = 'aloha';";
sp.setText("window.foobarBug636725 = 'aloha';");
ok(!content.wrappedJSObject.foobarBug636725,
"no content.foobarBug636725");
@ -73,7 +72,10 @@ function runTests()
is(statusbar.getAttribute("label"), chromeMenu.getAttribute("label"),
"statusbar label is correct");
sp.textbox.value = "window.foobarBug636725 = 'aloha2';";
sp.setText("2'", 31, 33);
ok(sp.getText(), "window.foobarBug636725 = 'aloha2';",
"setText() worked");
ok(!window.foobarBug636725, "no window.foobarBug636725");
@ -81,20 +83,23 @@ function runTests()
is(window.foobarBug636725, "aloha2", "window.foobarBug636725 has been set");
sp.textbox.value = "window.gBrowser";
sp.setText("gBrowser", 7);
ok(sp.getText(), "window.gBrowser",
"setText() worked with no end for the replace range");
is(typeof sp.run()[1].addTab, "function",
"chrome context has access to chrome objects");
// Check that the sandbox is cached.
sp.textbox.value = "typeof foobarBug636725cache;";
sp.setText("typeof foobarBug636725cache;");
is(sp.run()[1], "undefined", "global variable does not exist");
sp.textbox.value = "var foobarBug636725cache = 'foo';";
sp.setText("var foobarBug636725cache = 'foo';");
sp.run();
sp.textbox.value = "typeof foobarBug636725cache;";
sp.setText("typeof foobarBug636725cache;");
is(sp.run()[1], "string",
"global variable exists across two different executions");
@ -103,10 +108,10 @@ function runTests()
is(sp.run()[1], "undefined",
"global variable no longer exists after calling resetContext()");
sp.textbox.value = "var foobarBug636725cache2 = 'foo';";
sp.setText("var foobarBug636725cache2 = 'foo';");
sp.run();
sp.textbox.value = "typeof foobarBug636725cache2;";
sp.setText("typeof foobarBug636725cache2;");
is(sp.run()[1], "string",
"global variable exists across two different executions");

View File

@ -28,85 +28,103 @@ function runTests()
content.wrappedJSObject.foobarBug636725 = 1;
ok(sp.textbox, "textbox exists");
sp.textbox.value = "++window.foobarBug636725";
sp.setText("++window.foobarBug636725");
let exec = sp.run();
is(exec[0], sp.textbox.value, "execute()[0] is correct");
is(exec[0], sp.getText(), "run()[0] is correct");
is(exec[1], content.wrappedJSObject.foobarBug636725,
"execute()[1] is correct");
"run()[1] is correct");
is(sp.textbox.value, "++window.foobarBug636725",
"execute() does not change the textbox value");
is(sp.getText(), "++window.foobarBug636725",
"run() does not change the editor content");
is(content.wrappedJSObject.foobarBug636725, 2,
"execute() updated window.foobarBug636725");
"run() updated window.foobarBug636725");
sp.display();
is(content.wrappedJSObject.foobarBug636725, 3,
"print() updated window.foobarBug636725");
"display() updated window.foobarBug636725");
is(sp.textbox.value, "++window.foobarBug636725/*\n3\n*/",
"print() shows evaluation result in the textbox");
is(sp.getText(), "++window.foobarBug636725/*\n3\n*/",
"display() shows evaluation result in the textbox");
is(sp.selectedText, "/*\n3\n*/", "selectedText is correct");
is(sp.textbox.selectionStart, 24, "selectionStart is correct");
is(sp.textbox.selectionEnd, 31, "selectionEnd is correct");
let selection = sp.getSelectionRange();
is(selection.start, 24, "selection.start is correct");
is(selection.end, 31, "selection.end is correct");
// Test selection execute() and print().
// Test selection run() and display().
sp.textbox.value = "window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';";
sp.setText("window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';");
sp.selectRange(1, 2);
is(sp.textbox.selectionStart, 1, "selectionStart is 1");
is(sp.textbox.selectionEnd, 2, "selectionEnd is 2");
selection = sp.getSelectionRange();
is(selection.start, 1, "selection.start is 1");
is(selection.end, 2, "selection.end is 2");
sp.selectRange(0, 29);
is(sp.textbox.selectionStart, 0, "selectionStart is 0");
is(sp.textbox.selectionEnd, 29, "selectionEnd is 29");
selection = sp.getSelectionRange();
is(selection.start, 0, "selection.start is 0");
is(selection.end, 29, "selection.end is 29");
exec = sp.run();
is(exec[0], "window.foobarBug636725 = 'a';",
"execute()[0] is correct");
"run()[0] is correct");
is(exec[1], "a",
"execute()[1] is correct");
"run()[1] is correct");
is(sp.textbox.value, "window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';",
"execute() does not change the textbox value");
is(sp.getText(), "window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';",
"run() does not change the textbox value");
is(content.wrappedJSObject.foobarBug636725, "a",
"execute() worked for the selected range");
"run() worked for the selected range");
sp.textbox.value = "window.foobarBug636725 = 'c';\n" +
"window.foobarBug636725 = 'b';";
sp.setText("window.foobarBug636725 = 'c';\n" +
"window.foobarBug636725 = 'b';");
sp.selectRange(0, 22);
sp.display();
is(content.wrappedJSObject.foobarBug636725, "a",
"print() worked for the selected range");
"display() worked for the selected range");
is(sp.textbox.value, "window.foobarBug636725" +
"/*\na\n*/" +
" = 'c';\n" +
"window.foobarBug636725 = 'b';",
"print() shows evaluation result in the textbox");
is(sp.getText(), "window.foobarBug636725" +
"/*\na\n*/" +
" = 'c';\n" +
"window.foobarBug636725 = 'b';",
"display() shows evaluation result in the textbox");
is(sp.selectedText, "/*\na\n*/", "selectedText is correct");
is(sp.textbox.selectionStart, 22, "selectionStart is correct");
is(sp.textbox.selectionEnd, 29, "selectionEnd is correct");
selection = sp.getSelectionRange();
is(selection.start, 22, "selection.start is correct");
is(selection.end, 29, "selection.end is correct");
sp.deselect();
ok(!sp.selectedText, "selectedText is empty");
is(sp.textbox.selectionStart, sp.textbox.selectionEnd, "deselect() works");
selection = sp.getSelectionRange();
is(selection.start, selection.end, "deselect() works");
// Test undo/redo.
sp.setText("foo1");
sp.setText("foo2");
is(sp.getText(), "foo2", "editor content updated");
sp.undo();
is(sp.getText(), "foo1", "undo() works");
sp.redo();
is(sp.getText(), "foo2", "redo() works");
gScratchpadWindow.close();
gScratchpadWindow = null;

View File

@ -74,12 +74,12 @@ function fileImported(aStatus, aFileContent)
is(aFileContent, gFileContent,
"received data is correct");
is(gScratchpad.textbox.value, gFileContent,
"the textbox.value is correct");
is(gScratchpad.getText(), gFileContent,
"the editor content is correct");
// Save the file after changes.
gFileContent += "// omg, saved!";
gScratchpad.textbox.value = gFileContent;
gScratchpad.setText(gFileContent);
gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), true, true,
fileExported);
@ -94,7 +94,7 @@ function fileExported(aStatus)
// Attempt another file save, with confirmation which returns false.
gFileContent += "// omg, saved twice!";
gScratchpad.textbox.value = gFileContent;
gScratchpad.setText(gFileContent);
let oldConfirm = gScratchpadWindow.confirm;
let askedConfirmation = false;

View File

@ -27,8 +27,7 @@ function runTests()
let sp = gScratchpadWindow.Scratchpad;
ok(sp.textbox, "textbox exists");
sp.textbox.value = "document";
sp.setText("document");
sp.inspect();

View File

@ -58,8 +58,7 @@ function runTests()
is(statusbar.getAttribute("label"), contentMenu.getAttribute("label"),
"statusbar label is correct");
ok(sp.textbox, "textbox exists");
sp.textbox.value = "window.foosbug653108 = 'aloha';";
sp.setText("window.foosbug653108 = 'aloha';");
ok(!content.wrappedJSObject.foosbug653108,
"no content.foosbug653108");
@ -78,12 +77,12 @@ function runTests2() {
ok(!window.foosbug653108, "no window.foosbug653108");
sp.textbox.value = "window.foosbug653108";
sp.setText("window.foosbug653108");
let result = sp.run();
isnot(result, "aloha", "window.foosbug653108 is not aloha");
sp.textbox.value = "window.foosbug653108 = 'ahoyhoy';";
sp.setText("window.foosbug653108 = 'ahoyhoy';");
sp.run();
is(content.wrappedJSObject.foosbug653108, "ahoyhoy",
@ -97,7 +96,7 @@ function runTests3() {
gBrowser.selectedBrowser.removeEventListener("load", runTests3, true);
// Check that the sandbox is not cached.
sp.textbox.value = "typeof foosbug653108;";
sp.setText("typeof foosbug653108;");
is(sp.run()[1], "undefined", "global variable does not exist");
gScratchpadWindow.close();

View File

@ -41,6 +41,8 @@ function runTests()
"sp-menu-resetContext": "resetContext",
"sp-menu-errorConsole": "openErrorConsole",
"sp-menu-webConsole": "openWebConsole",
"sp-menu-undo": "undo",
"sp-menu-redo": "redo",
};
let lastMethodCalled = null;

View File

@ -0,0 +1,57 @@
#
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Source Editor.
#
# The Initial Developer of the Original Code is Mozilla Foundation.
#
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Rob Campbell <rcampbell@mozilla.com>
# Mihai Sucan <mihai.sucan@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
DEPTH = ../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
include $(DEPTH)/config/autoconf.mk
ifdef ENABLE_TESTS
DIRS += test
endif
EXTRA_JS_MODULES = \
source-editor.jsm \
source-editor-orion.jsm \
source-editor-textarea.jsm \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,29 @@
Eclipse Distribution License - v 1.0
Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the Eclipse Foundation, Inc. nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,81 @@
#!/usr/bin/env node
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Source Editor component.
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mihai Sucan <mihai.sucan@gmail.com> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK *****/
var copy = require('dryice').copy;
const ORION_EDITOR = "org.eclipse.orion.client.editor/web";
var js_src = copy.createDataObject();
copy({
source: [
ORION_EDITOR + "/orion/textview/keyBinding.js",
ORION_EDITOR + "/orion/textview/rulers.js",
ORION_EDITOR + "/orion/textview/undoStack.js",
ORION_EDITOR + "/orion/textview/textModel.js",
ORION_EDITOR + "/orion/textview/textView.js",
ORION_EDITOR + "/orion/editor/htmlGrammar.js",
ORION_EDITOR + "/orion/editor/textMateStyler.js",
ORION_EDITOR + "/examples/textview/textStyler.js",
],
dest: js_src,
});
copy({
source: js_src,
dest: "orion.js",
});
var css_src = copy.createDataObject();
copy({
source: [
ORION_EDITOR + "/orion/textview/textview.css",
ORION_EDITOR + "/orion/textview/rulers.css",
ORION_EDITOR + "/examples/textview/textstyler.css",
ORION_EDITOR + "/examples/editor/htmlStyles.css",
],
dest: css_src,
});
copy({
source: css_src,
dest: "orion.css",
});

View File

@ -0,0 +1,20 @@
# Introduction
This is the Orion editor packaged for Mozilla.
The Orion editor web site: http://www.eclipse.org/orion
# Upgrade
To upgrade Orion to a newer version see the UPGRADE file.
Orion version: git clone from 2011-07-06 (after the 0.2 release)
commit hash b19bc0b0f4e2843823bb1b8c8b4a64395c59e617
# License
The following files are licensed according to the contents in the LICENSE
file:
orion.js
orion.css

View File

@ -0,0 +1,20 @@
Upgrade notes:
1. Get the Orion client source code from:
http://www.eclipse.org/orion
2. Install Dryice from:
https://github.com/mozilla/dryice
You also need nodejs for Dryice to run:
http://nodejs.org
3. Copy Makefile.dryice.js to:
org.eclipse.orion.client/bundles/
4. Execute Makefile.dryice.js. You should get orion.js and orion.css.
5. Copy the two files back here.
6. Make a new build of Firefox.

View File

@ -0,0 +1,115 @@
.view {
background-color: white;
}
.viewContainer {
font-family: monospace;
font-size: 10pt;
}
.viewContent {
}.ruler_annotation {
background-color: #e1ebfb;
width: 16px;
}
.ruler_annotation_todo {
}
.ruler_annotation_todo_overview {
background-color: lightgreen;
border: 1px solid green;
}
.ruler_annotation_breakpoint {
}
.ruler_annotation_breakpoint_overview {
background-color: lightblue;
border: 1px solid blue;
}
.ruler_lines {
background-color: #e1ebfb;
border-right: 1px solid #b1badf;
text-align: right;
}
.ruler_overview {
background-color: #e1ebfb;
}
.ruler_lines_even {
background-color: #e1ebfb;
}
.ruler_lines_odd {
background-color: white;
}
.token_comment {
color: green;
}
.token_javadoc {
color: #00008F;
}
.token_string {
color: blue;
}
.token_keyword {
color: darkred;
font-weight: bold;
}
.token_bracket_outline {
outline: 1px solid red;
}
.token_bracket {
color: white;
background-color: grey;
}
.token_space {
background-image: url('/examples/textview/images/white_space.png');
background-repeat: no-repeat;
background-position: center center;
}
.token_tab {
background-image: url('/examples/textview/images/white_tab.png');
background-repeat: no-repeat;
background-position: left center;
}
.line_caret {
background-color: #EAF2FE;
}/* Styling for html syntax highlighting */
.entity-name-tag {
color: #3f7f7f;
}
.entity-other-attribute-name {
color: #7f007f;
}
.punctuation-definition-comment {
color: #3f5fbf;
}
.comment {
color: #3f5fbf
}
.string-quoted {
color: #2a00ff;
font-style: italic;
}
.invalid {
color: red;
font-weight: bold;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,762 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Source Editor component (Orion editor).
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mihai Sucan <mihai.sucan@gmail.com> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK *****/
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
const ORION_SCRIPT = "chrome://browser/content/orion.js";
const ORION_IFRAME = "data:text/html;charset=utf8,<!DOCTYPE html>" +
"<html style='height:100%' dir='ltr'>" +
"<body style='height:100%;margin:0;overflow:hidden'>" +
"<div id='editor' style='height:100%'></div>" +
"</body></html>";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
/**
* Predefined themes for syntax highlighting. This objects maps
* SourceEditor.THEMES to Orion CSS files.
*/
const ORION_THEMES = {
textmate: "chrome://browser/content/orion.css",
};
/**
* Known editor events you can listen for. This object maps SourceEditor.EVENTS
* to Orion events.
*/
const ORION_EVENTS = {
ContextMenu: "ContextMenu",
TextChanged: "ModelChanged",
Selection: "Selection",
};
var EXPORTED_SYMBOLS = ["SourceEditor"];
/**
* The SourceEditor object constructor. The SourceEditor component allows you to
* provide users with an editor tailored to the specific needs of editing source
* code, aimed primarily at web developers.
*
* The editor used here is Eclipse Orion (see http://www.eclipse.org/orion).
*
* @constructor
*/
function SourceEditor() {
// Update the SourceEditor defaults from user preferences.
SourceEditor.DEFAULTS.TAB_SIZE =
Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
SourceEditor.DEFAULTS.EXPAND_TAB =
Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
}
SourceEditor.prototype = {
_view: null,
_iframe: null,
_undoStack: null,
_lines_ruler: null,
_styler: null,
_mode: null,
_expandTab: null,
_tabSize: null,
/**
* The editor container element.
* @type nsIDOMElement
*/
parentElement: null,
/**
* Initialize the editor.
*
* @param nsIDOMElement aElement
* The DOM element where you want the editor to show.
* @param object aConfig
* Editor configuration object. Properties:
* - placeholderText - the text you want to be shown by default.
* - theme - the syntax highlighting theme you want. You can use one
* of the predefined themes, or you can point to your CSS file.
* - mode - the editor mode, based on the file type you want to edit.
* You can use one of the predefined modes.
* - tabSize - define how many spaces to use for a tab character.
* - expandTab - tells if you want tab characters to be expanded to
* spaces.
* - readOnly - make the editor read only.
* - showLineNumbers - display the line numbers gutter.
* - undoLimit - how many steps should the undo stack hold.
* - keys - is an array of objects that allows you to define custom
* editor keyboard bindings. Each object can have:
* - action - name of the editor action to invoke.
* - code - keyCode for the shortcut.
* - accel - boolean for the Accel key (cmd/ctrl).
* - shift - boolean for the Shift key.
* - alt - boolean for the Alt key.
* - callback - optional function to invoke, if the action is not
* predefined in the editor.
* @param function [aCallback]
* Function you want to execute once the editor is loaded and
* initialized.
*/
init: function SE_init(aElement, aConfig, aCallback)
{
if (this._iframe) {
throw new Error("SourceEditor is already initialized!");
}
let doc = aElement.ownerDocument;
this._iframe = doc.createElementNS(XUL_NS, "iframe");
this._iframe.flex = 1;
let onIframeLoad = (function() {
this._iframe.removeEventListener("load", onIframeLoad, true);
Services.scriptloader.loadSubScript(ORION_SCRIPT,
this._iframe.contentWindow.wrappedJSObject, "utf8");
this._onLoad(aCallback);
}).bind(this);
this._iframe.addEventListener("load", onIframeLoad, true);
this._iframe.setAttribute("src", ORION_IFRAME);
aElement.appendChild(this._iframe);
this.parentElement = aElement;
this._config = aConfig;
},
/**
* The editor iframe load event handler.
*
* @private
* @param function [aCallback]
* Optional function invoked when the editor completes loading.
*/
_onLoad: function SE__onLoad(aCallback)
{
let config = this._config;
let window = this._iframe.contentWindow.wrappedJSObject;
let textview = window.orion.textview;
this._expandTab = typeof config.expandTab != "undefined" ?
config.expandTab : SourceEditor.DEFAULTS.EXPAND_TAB;
this._tabSize = config.tabSize || SourceEditor.DEFAULTS.TAB_SIZE;
let theme = config.theme || SourceEditor.DEFAULTS.THEME;
let stylesheet = theme in ORION_THEMES ? ORION_THEMES[theme] : theme;
this._view = new textview.TextView({
model: new textview.TextModel(config.placeholderText),
parent: "editor",
stylesheet: stylesheet,
tabSize: this._tabSize,
readonly: config.readOnly,
});
if (config.showLineNumbers) {
this._lines_ruler = new textview.LineNumberRuler("left",
{styleClass: "ruler_lines", style: {minWidth: "1.4em"}},
{styleClass: "ruler_lines_even"}, {styleClass: "ruler_lines_even"});
this._view.addRuler(this._lines_ruler);
}
this.setMode(config.mode || SourceEditor.DEFAULTS.MODE);
this._undoStack = new textview.UndoStack(this._view,
config.undoLimit || SourceEditor.DEFAULTS.UNDO_LIMIT);
this._initEditorFeatures();
(config.keys || []).forEach(function(aKey) {
let binding = new textview.KeyBinding(aKey.code, aKey.accel, aKey.shift,
aKey.alt);
this._view.setKeyBinding(binding, aKey.action);
if (aKey.callback) {
this._view.setAction(aKey.action, aKey.callback);
}
}, this);
if (aCallback) {
aCallback(this);
}
},
/**
* Initialize the custom Orion editor features.
* @private
*/
_initEditorFeatures: function SE__initEditorFeatures()
{
let window = this._iframe.contentWindow.wrappedJSObject;
let textview = window.orion.textview;
this._view.setAction("tab", this._doTab.bind(this));
let shiftTabKey = new textview.KeyBinding(Ci.nsIDOMKeyEvent.DOM_VK_TAB,
false, true);
this._view.setAction("Unindent Lines", this._doUnindentLines.bind(this));
this._view.setKeyBinding(shiftTabKey, "Unindent Lines");
this._view.setAction("enter", this._doEnter.bind(this));
if (this._expandTab) {
this._view.setAction("deletePrevious", this._doDeletePrevious.bind(this));
}
},
/**
* The "tab" editor action implementation. This adds support for expanded tabs
* to spaces, and support for the indentation of multiple lines at once.
* @private
*/
_doTab: function SE__doTab()
{
let indent = "\t";
let selection = this.getSelection();
let model = this._model;
let firstLine = model.getLineAtOffset(selection.start);
let firstLineStart = model.getLineStart(firstLine);
let lastLineOffset = selection.end > selection.start ?
selection.end - 1 : selection.end;
let lastLine = model.getLineAtOffset(lastLineOffset);
if (this._expandTab) {
let offsetFromLineStart = firstLine == lastLine ?
selection.start - firstLineStart : 0;
let spaces = this._tabSize - (offsetFromLineStart % this._tabSize);
indent = (new Array(spaces + 1)).join(" ");
}
// Do selection indentation.
if (firstLine != lastLine) {
let lines = [""];
let lastLineEnd = model.getLineEnd(lastLine, true);
let selectedLines = lastLine - firstLine + 1;
for (let i = firstLine; i <= lastLine; i++) {
lines.push(model.getLine(i, true));
}
this.startCompoundChange();
this.setText(lines.join(indent), firstLineStart, lastLineEnd);
let newSelectionStart = firstLineStart == selection.start ?
selection.start : selection.start + indent.length;
let newSelectionEnd = selection.end + (selectedLines * indent.length);
this._view.setSelection(newSelectionStart, newSelectionEnd);
this.endCompoundChange();
} else {
this.setText(indent, selection.start, selection.end);
}
return true;
},
/**
* The "deletePrevious" editor action implementation. This adds unindentation
* support to the Backspace key implementation.
* @private
*/
_doDeletePrevious: function SE__doDeletePrevious()
{
let selection = this.getSelection();
if (selection.start == selection.end && this._expandTab) {
let model = this._model;
let lineIndex = model.getLineAtOffset(selection.start);
let lineStart = model.getLineStart(lineIndex);
let offset = selection.start - lineStart;
if (offset >= this._tabSize && (offset % this._tabSize) == 0) {
let text = this.getText(lineStart, selection.start);
if (!/[^ ]/.test(text)) {
this.setText("", selection.start - this._tabSize, selection.end);
return true;
}
}
}
return false;
},
/**
* The "Unindent lines" editor action implementation. This method is invoked
* when the user presses Shift-Tab.
* @private
*/
_doUnindentLines: function SE__doUnindentLines()
{
let indent = "\t";
let selection = this.getSelection();
let model = this._model;
let firstLine = model.getLineAtOffset(selection.start);
let lastLineOffset = selection.end > selection.start ?
selection.end - 1 : selection.end;
let lastLine = model.getLineAtOffset(lastLineOffset);
if (this._expandTab) {
indent = (new Array(this._tabSize + 1)).join(" ");
}
let lines = [];
for (let line, i = firstLine; i <= lastLine; i++) {
line = model.getLine(i, true);
if (line.indexOf(indent) != 0) {
return false;
}
lines.push(line.substring(indent.length));
}
let firstLineStart = model.getLineStart(firstLine);
let lastLineStart = model.getLineStart(lastLine);
let lastLineEnd = model.getLineEnd(lastLine, true);
this.startCompoundChange();
this.setText(lines.join(""), firstLineStart, lastLineEnd);
let selectedLines = lastLine - firstLine + 1;
let newSelectionStart = firstLineStart == selection.start ?
selection.start :
Math.max(firstLineStart,
selection.start - indent.length);
let newSelectionEnd = selection.end - (selectedLines * indent.length) +
(selection.end == lastLineStart + 1 ? 1 : 0);
if (firstLine == lastLine) {
newSelectionEnd = Math.max(lastLineStart, newSelectionEnd);
}
this._view.setSelection(newSelectionStart, newSelectionEnd);
this.endCompoundChange();
return true;
},
/**
* The editor Enter action implementation, which adds simple automatic
* indentation based on the previous line when the user presses the Enter key.
* @private
*/
_doEnter: function SE__doEnter()
{
let selection = this.getSelection();
if (selection.start != selection.end) {
return false;
}
let model = this._model;
let lineIndex = model.getLineAtOffset(selection.start);
let lineText = model.getLine(lineIndex);
let lineStart = model.getLineStart(lineIndex);
let index = 0;
let lineOffset = selection.start - lineStart;
while (index < lineOffset && /[ \t]/.test(lineText.charAt(index))) {
index++;
}
if (!index) {
return false;
}
let prefix = lineText.substring(0, index);
index = lineOffset;
while (index < lineText.length &&
/[ \t]/.test(lineText.charAt(index++))) {
selection.end++;
}
this.setText(this.getLineDelimiter() + prefix, selection.start,
selection.end);
return true;
},
/**
* Get the Orion Model, the TextModel object instance we use.
* @private
* @type object
*/
get _model() {
return this._view.getModel();
},
/**
* Get the editor element.
*
* @return nsIDOMElement
* In this implementation a xul:iframe holds the editor.
*/
get editorElement() {
return this._iframe;
},
/**
* Add an event listener to the editor. You can use one of the known events.
*
* @see SourceEditor.EVENTS
*
* @param string aEventType
* The event type you want to listen for.
* @param function aCallback
* The function you want executed when the event is triggered.
* @param mixed [aData]
* Optional data to pass to the callback when the event is triggered.
*/
addEventListener:
function SE_addEventListener(aEventType, aCallback, aData)
{
if (aEventType in ORION_EVENTS) {
this._view.addEventListener(ORION_EVENTS[aEventType], true,
aCallback, aData);
} else {
throw new Error("SourceEditor.addEventListener() unknown event " +
"type " + aEventType);
}
},
/**
* Remove an event listener from the editor. You can use one of the known
* events.
*
* @see SourceEditor.EVENTS
*
* @param string aEventType
* The event type you have a listener for.
* @param function aCallback
* The function you have as the event handler.
* @param mixed [aData]
* The optional data passed to the callback.
*/
removeEventListener:
function SE_removeEventListener(aEventType, aCallback, aData)
{
if (aEventType in ORION_EVENTS) {
this._view.removeEventListener(ORION_EVENTS[aEventType], true,
aCallback, aData);
} else {
throw new Error("SourceEditor.removeEventListener() unknown event " +
"type " + aEventType);
}
},
/**
* Undo a change in the editor.
*/
undo: function SE_undo()
{
this._undoStack.undo();
},
/**
* Redo a change in the editor.
*/
redo: function SE_redo()
{
this._undoStack.redo();
},
/**
* Check if there are changes that can be undone.
*
* @return boolean
* True if there are changes that can be undone, false otherwise.
*/
canUndo: function SE_canUndo()
{
return this._undoStack.canUndo();
},
/**
* Check if there are changes that can be repeated.
*
* @return boolean
* True if there are changes that can be repeated, false otherwise.
*/
canRedo: function SE_canRedo()
{
return this._undoStack.canRedo();
},
/**
* Start a compound change in the editor. Compound changes are grouped into
* only one change that you can undo later, after you invoke
* endCompoundChange().
*/
startCompoundChange: function SE_startCompoundChange()
{
this._undoStack.startCompoundChange();
},
/**
* End a compound change in the editor.
*/
endCompoundChange: function SE_endCompoundChange()
{
this._undoStack.endCompoundChange();
},
/**
* Focus the editor.
*/
focus: function SE_focus()
{
this._view.focus();
},
/**
* Check if the editor has focus.
*
* @return boolean
* True if the editor is focused, false otherwise.
*/
hasFocus: function SE_hasFocus()
{
return this._iframe.ownerDocument.activeElement === this._iframe;
},
/**
* Get the editor content, in the given range. If no range is given you get
* the entire editor content.
*
* @param number [aStart=0]
* Optional, start from the given offset.
* @param number [aEnd=content char count]
* Optional, end offset for the text you want. If this parameter is not
* given, then the text returned goes until the end of the editor
* content.
* @return string
* The text in the given range.
*/
getText: function SE_getText(aStart, aEnd)
{
return this._view.getText(aStart, aEnd);
},
/**
* Get the number of characters in the editor content.
*
* @return number
* The number of editor content characters.
*/
getCharCount: function SE_getCharCount()
{
return this._model.getCharCount();
},
/**
* Get the selected text.
*
* @return string
* The currently selected text.
*/
getSelectedText: function SE_getSelectedText()
{
let selection = this.getSelection();
return this.getText(selection.start, selection.end);
},
/**
* Replace text in the source editor with the given text, in the given range.
*
* @param string aText
* The text you want to put into the editor.
* @param number [aStart=0]
* Optional, the start offset, zero based, from where you want to start
* replacing text in the editor.
* @param number [aEnd=char count]
* Optional, the end offset, zero based, where you want to stop
* replacing text in the editor.
*/
setText: function SE_setText(aText, aStart, aEnd)
{
this._view.setText(aText, aStart, aEnd);
},
/**
* Drop the current selection / deselect.
*/
dropSelection: function SE_dropSelection()
{
this.setCaretOffset(this.getCaretOffset());
},
/**
* Select a specific range in the editor.
*
* @param number aStart
* Selection range start.
* @param number aEnd
* Selection range end.
*/
setSelection: function SE_setSelection(aStart, aEnd)
{
this._view.setSelection(aStart, aEnd, true);
},
/**
* Get the current selection range.
*
* @return object
* An object with two properties, start and end, that give the
* selection range (zero based offsets).
*/
getSelection: function SE_getSelection()
{
return this._view.getSelection();
},
/**
* Get the current caret offset.
*
* @return number
* The current caret offset.
*/
getCaretOffset: function SE_getCaretOffset()
{
return this._view.getCaretOffset();
},
/**
* Set the caret offset.
*
* @param number aOffset
* The new caret offset you want to set.
*/
setCaretOffset: function SE_setCaretOffset(aOffset)
{
this._view.setCaretOffset(aOffset, true);
},
/**
* Get the line delimiter used in the document being edited.
*
* @return string
* The line delimiter.
*/
getLineDelimiter: function SE_getLineDelimiter()
{
return this._model.getLineDelimiter();
},
/**
* Set the source editor mode to the file type you are editing.
*
* @param string aMode
* One of the predefined SourceEditor.MODES.
*/
setMode: function SE_setMode(aMode)
{
if (this._styler) {
this._styler.destroy();
this._styler = null;
}
let window = this._iframe.contentWindow.wrappedJSObject;
let TextStyler = window.examples.textview.TextStyler;
let TextMateStyler = window.orion.editor.TextMateStyler;
let HtmlGrammar = window.orion.editor.HtmlGrammar;
switch (aMode) {
case SourceEditor.MODES.JAVASCRIPT:
case SourceEditor.MODES.CSS:
this._styler = new TextStyler(this._view, aMode);
break;
case SourceEditor.MODES.HTML:
case SourceEditor.MODES.XML:
this._styler = new TextMateStyler(this._view, HtmlGrammar.grammar);
break;
}
this._mode = aMode;
},
/**
* Get the current source editor mode.
*
* @return string
* Returns one of the predefined SourceEditor.MODES.
*/
getMode: function SE_getMode()
{
return this._mode;
},
/**
* Setter for the read-only state of the editor.
* @param boolean aValue
* Tells if you want the editor to read-only or not.
*/
set readOnly(aValue)
{
this._view.readonly = aValue;
},
/**
* Getter for the read-only state of the editor.
* @type boolean
*/
get readOnly()
{
return this._view.readonly;
},
/**
* Destroy/uninitialize the editor.
*/
destroy: function SE_destroy()
{
this._view.destroy();
this.parentElement.removeChild(this._iframe);
this.parentElement = null;
this._iframe = null;
this._undoStack = null;
this._styler = null;
this._lines_ruler = null;
this._view = null;
this._config = null;
},
};

View File

@ -0,0 +1,782 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Source Editor component (textarea fallback).
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mihai Sucan <mihai.sucan@gmail.com> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK *****/
"use strict";
const Cu = Components.utils;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var EXPORTED_SYMBOLS = ["SourceEditor"];
/**
* The SourceEditor object constructor. The SourceEditor component allows you to
* provide users with an editor tailored to the specific needs of editing source
* code, aimed primarily at web developers.
*
* The editor used here is a simple textarea. This is used as a fallback
* mechanism for when the user disables the code editor feature.
*
* @constructor
*/
function SourceEditor() {
// Update the SourceEditor defaults from user preferences.
SourceEditor.DEFAULTS.TAB_SIZE =
Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
SourceEditor.DEFAULTS.EXPAND_TAB =
Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB);
this._listeners = {};
this._lastSelection = {};
}
SourceEditor.prototype = {
_textbox: null,
_editor: null,
_listeners: null,
_lineDelimiter: null,
_editActionListener: null,
_expandTab: null,
_tabSize: null,
/**
* The editor container element.
* @type nsIDOMElement
*/
parentElement: null,
/**
* Initialize the editor.
*
* @param nsIDOMElement aElement
* The DOM element where you want the editor to show.
* @param object aConfig
* Editor configuration object. Properties:
* - placeholderText - the text you want to be shown by default.
* - mode - the editor mode, based on the file type you want to edit.
* You can use one of the predefined modes.
* - tabSize - define how many spaces to use for a tab character.
* - expandTab - tells if you want tab characters to be expanded to
* spaces.
* - readOnly - make the editor read only.
* - undoLimit - how many steps should the undo stack hold.
* @param function [aCallback]
* Function you want to execute once the editor is loaded and
* initialized.
*/
init: function SE_init(aElement, aConfig, aCallback)
{
if (this._textbox) {
throw new Error("SourceEditor is already initialized!");
}
let doc = aElement.ownerDocument;
let win = doc.defaultView;
this._textbox = doc.createElementNS(XUL_NS, "textbox");
this._textbox.flex = 1;
this._textbox.setAttribute("multiline", true);
this._textbox.setAttribute("dir", "ltr");
aElement.appendChild(this._textbox);
this.parentElement = aElement;
this._editor = this._textbox.editor;
this._expandTab = aConfig.expandTab !== undefined ?
aConfig.expandTab : SourceEditor.DEFAULTS.EXPAND_TAB;
this._tabSize = aConfig.tabSize || SourceEditor.DEFAULTS.TAB_SIZE;
this._textbox.style.MozTabSize = this._tabSize;
this._textbox.setAttribute("value", aConfig.placeholderText);
this._textbox.setAttribute("class", "monospace");
this._textbox.style.direction = "ltr";
this._textbox.readOnly = aConfig.readOnly;
// Make sure that the SourceEditor Selection events are fired properly.
// Also make sure that the Tab key inserts spaces when expandTab is true.
this._textbox.addEventListener("select", this._onSelect.bind(this), false);
this._textbox.addEventListener("keypress", this._onKeyPress.bind(this), false);
this._textbox.addEventListener("keyup", this._onSelect.bind(this), false);
this._textbox.addEventListener("click", this._onSelect.bind(this), false);
// Mimic the mode change.
this.setMode(aConfig.mode || SourceEditor.DEFAULTS.MODE);
this._editor.transactionManager.maxTransactionCount =
aConfig.undoLimit || SourceEditor.DEFAULTS.UNDO_LIMIT;
// Make sure that the transactions stack is clean.
this._editor.transactionManager.clear();
this._editor.resetModificationCount();
// Add the edit action listener so we can fire the SourceEditor TextChanged
// events.
this._editActionListener = new EditActionListener(this);
this._editor.addEditActionListener(this._editActionListener);
this._lineDelimiter = win.navigator.platform.indexOf("Win") > -1 ?
"\r\n" : "\n";
this._config = aConfig;
if (aCallback) {
aCallback(this);
}
},
/**
* The textbox keypress event handler allows users to indent code using the
* Tab key.
*
* @private
* @param nsIDOMEvent aEvent
* The DOM object for the event.
*/
_onKeyPress: function SE__onKeyPress(aEvent)
{
if (aEvent.keyCode != aEvent.DOM_VK_TAB || aEvent.shiftKey ||
aEvent.metaKey || aEvent.ctrlKey || aEvent.altKey) {
return;
}
aEvent.preventDefault();
let caret = this.getCaretOffset();
let indent = "\t";
if (this._expandTab) {
let text = this._textbox.value;
let lineStart = caret;
while (lineStart > 0) {
let c = text.charAt(lineStart - 1);
if (c == "\r" || c == "\n") {
break;
}
lineStart--;
}
let offset = caret - lineStart;
let spaces = this._tabSize - (offset % this._tabSize);
indent = (new Array(spaces + 1)).join(" ");
}
this.setText(indent, caret, caret);
this.setCaretOffset(caret + indent.length);
},
/**
* The textbox keyup, click and select event handler tracks selection
* changes. This method invokes the SourceEditor Selection event handlers.
*
* @see SourceEditor.EVENTS.SELECTION
* @private
*/
_onSelect: function SE__onSelect()
{
let selection = this.getSelection();
selection.collapsed = selection.start == selection.end;
if (selection.collapsed && this._lastSelection.collapsed) {
this._lastSelection = selection;
return; // just a cursor move.
}
if (this._lastSelection.start != selection.start ||
this._lastSelection.end != selection.end) {
let sendEvent = {
oldValue: {start: this._lastSelection.start,
end: this._lastSelection.end},
newValue: {start: selection.start, end: selection.end},
};
let listeners = this._listeners[SourceEditor.EVENTS.SELECTION] || [];
listeners.forEach(function(aListener) {
aListener.callback.call(null, sendEvent, aListener.data);
}, this);
this._lastSelection = selection;
}
},
/**
* The TextChanged event dispatcher. This method is called when a change in
* the text occurs. All of the SourceEditor TextChanged event handlers are
* notified about the lower level change.
*
* @see SourceEditor.EVENTS.TEXT_CHANGED
* @see EditActionListener
*
* @private
*
* @param object aEvent
* The TextChanged event object that is going to be sent to the
* SourceEditor event handlers.
*/
_onTextChanged: function SE__onTextChanged(aEvent)
{
let listeners = this._listeners[SourceEditor.EVENTS.TEXT_CHANGED] || [];
listeners.forEach(function(aListener) {
aListener.callback.call(null, aEvent, aListener.data);
}, this);
},
/**
* Get the editor element.
*
* @return nsIDOMElement
* In this implementation a xul:textbox is returned.
*/
get editorElement() {
return this._textbox;
},
/**
* Add an event listener to the editor. You can use one of the known events.
*
* @see SourceEditor.EVENTS
*
* @param string aEventType
* The event type you want to listen for.
* @param function aCallback
* The function you want executed when the event is triggered.
* @param mixed [aData]
* Optional data to pass to the callback when the event is triggered.
*/
addEventListener:
function SE_addEventListener(aEventType, aCallback, aData)
{
const EVENTS = SourceEditor.EVENTS;
let listener = {
type: aEventType,
data: aData,
callback: aCallback,
};
if (aEventType == EVENTS.CONTEXT_MENU) {
listener.domType = "contextmenu";
listener.target = this._textbox;
listener.handler = this._onContextMenu.bind(this, listener);
listener.target.addEventListener(listener.domType, listener.handler, false);
}
if (!(aEventType in this._listeners)) {
this._listeners[aEventType] = [];
}
this._listeners[aEventType].push(listener);
},
/**
* Remove an event listener from the editor. You can use one of the known
* events.
*
* @see SourceEditor.EVENTS
*
* @param string aEventType
* The event type you have a listener for.
* @param function aCallback
* The function you have as the event handler.
* @param mixed [aData]
* The optional data passed to the callback.
*/
removeEventListener:
function SE_removeEventListener(aEventType, aCallback, aData)
{
let listeners = this._listeners[aEventType];
if (!listeners) {
throw new Error("SourceEditor.removeEventListener() called for an " +
"unknown event.");
}
const EVENTS = SourceEditor.EVENTS;
this._listeners[aEventType] = listeners.filter(function(aListener) {
let isSameListener = aListener.type == aEventType &&
aListener.callback === aCallback &&
aListener.data === aData;
if (isSameListener && aListener.domType) {
aListener.target.removeEventListener(aListener.domType,
aListener.handler, false);
}
return !isSameListener;
}, this);
},
/**
* The xul:textbox contextmenu event handler. This is used a wrapper for each
* contextmenu event listener added by the SourceEditor client.
*
* @param object aListener
* The object that holds listener information, see this._listener and
* this.addEventListener().
* @param nsIDOMEvent aDOMEvent
* The nsIDOMEvent object that triggered the context menu.
*/
_onContextMenu: function SE__onContextMenu(aListener, aDOMEvent)
{
let input = this._textbox.inputField;
let rect = this._textbox.getBoundingClientRect();
// Prepare the event object we send to the event handler.
let sendEvent = {
x: aDOMEvent.clientX - rect.left + input.scrollLeft,
y: aDOMEvent.clientY - rect.top + input.scrollTop,
screenX: aDOMEvent.screenX,
screenY: aDOMEvent.screenY,
};
aDOMEvent.preventDefault();
aListener.callback.call(null, sendEvent, aListener.data);
},
/**
* Undo a change in the editor.
*/
undo: function SE_undo()
{
this._editor.undo(1);
},
/**
* Redo a change in the editor.
*/
redo: function SE_redo()
{
this._editor.redo(1);
},
/**
* Check if there are changes that can be undone.
*
* @return boolean
* True if there are changes that can be undone, false otherwise.
*/
canUndo: function SE_canUndo()
{
let isEnabled = {};
let canUndo = {};
this._editor.canUndo(isEnabled, canUndo);
return canUndo.value;
},
/**
* Check if there are changes that can be repeated.
*
* @return boolean
* True if there are changes that can be repeated, false otherwise.
*/
canRedo: function SE_canRedo()
{
let isEnabled = {};
let canRedo = {};
this._editor.canRedo(isEnabled, canRedo);
return canRedo.value;
},
/**
* Start a compound change in the editor. Compound changes are grouped into
* only one change that you can undo later, after you invoke
* endCompoundChange().
*/
startCompoundChange: function SE_startCompoundChange()
{
this._editor.beginTransaction();
},
/**
* End a compound change in the editor.
*/
endCompoundChange: function SE_endCompoundChange()
{
this._editor.endTransaction();
},
/**
* Focus the editor.
*/
focus: function SE_focus()
{
this._textbox.focus();
},
/**
* Check if the editor has focus.
*
* @return boolean
* True if the editor is focused, false otherwise.
*/
hasFocus: function SE_hasFocus()
{
return this._textbox.ownerDocument.activeElement ===
this._textbox.inputField;
},
/**
* Get the editor content, in the given range. If no range is given you get
* the entire editor content.
*
* @param number [aStart=0]
* Optional, start from the given offset.
* @param number [aEnd=content char count]
* Optional, end offset for the text you want. If this parameter is not
* given, then the text returned goes until the end of the editor
* content.
* @return string
* The text in the given range.
*/
getText: function SE_getText(aStart, aEnd)
{
let value = this._textbox.value || "";
if (aStart === undefined || aStart === null) {
aStart = 0;
}
if (aEnd === undefined || aEnd === null) {
aEnd = value.length;
}
return value.substring(aStart, aEnd);
},
/**
* Get the number of characters in the editor content.
*
* @return number
* The number of editor content characters.
*/
getCharCount: function SE_getCharCount()
{
return this._textbox.textLength;
},
/**
* Get the selected text.
*
* @return string
* The currently selected text.
*/
getSelectedText: function SE_getSelectedText()
{
let selection = this.getSelection();
return selection.start != selection.end ?
this.getText(selection.start, selection.end) : "";
},
/**
* Replace text in the source editor with the given text, in the given range.
*
* @param string aText
* The text you want to put into the editor.
* @param number [aStart=0]
* Optional, the start offset, zero based, from where you want to start
* replacing text in the editor.
* @param number [aEnd=char count]
* Optional, the end offset, zero based, where you want to stop
* replacing text in the editor.
*/
setText: function SE_setText(aText, aStart, aEnd)
{
if (aStart === undefined) {
this._textbox.value = aText;
} else {
if (aEnd === undefined) {
aEnd = this._textbox.textLength;
}
let value = this._textbox.value || "";
let removedText = value.substring(aStart, aEnd);
let prefix = value.substr(0, aStart);
let suffix = value.substr(aEnd);
if (suffix) {
this._editActionListener._setTextRangeEvent = {
start: aStart,
removedCharCount: removedText.length,
addedCharCount: aText.length,
};
}
this._textbox.value = prefix + aText + suffix;
}
},
/**
* Drop the current selection / deselect.
*/
dropSelection: function SE_dropSelection()
{
let selection = this._editor.selection;
selection.collapse(selection.focusNode, selection.focusOffset);
this._onSelect();
},
/**
* Select a specific range in the editor.
*
* @param number aStart
* Selection range start.
* @param number aEnd
* Selection range end.
*/
setSelection: function SE_setSelection(aStart, aEnd)
{
this._textbox.setSelectionRange(aStart, aEnd);
this._onSelect();
},
/**
* Get the current selection range.
*
* @return object
* An object with two properties, start and end, that give the
* selection range (zero based offsets).
*/
getSelection: function SE_getSelection()
{
return {
start: this._textbox.selectionStart,
end: this._textbox.selectionEnd
};
},
/**
* Get the current caret offset.
*
* @return number
* The current caret offset.
*/
getCaretOffset: function SE_getCaretOffset()
{
let selection = this.getSelection();
return selection.start < selection.end ?
selection.end : selection.start;
},
/**
* Set the caret offset.
*
* @param number aOffset
* The new caret offset you want to set.
*/
setCaretOffset: function SE_setCaretOffset(aOffset)
{
this.setSelection(aOffset, aOffset);
},
/**
* Get the line delimiter used in the document being edited.
*
* @return string
* The line delimiter.
*/
getLineDelimiter: function SE_getLineDelimiter()
{
return this._lineDelimiter;
},
/**
* Set the source editor mode to the file type you are editing.
*
* Note: this implementation makes no difference between any of the available
* modes.
*
* @param string aMode
* One of the predefined SourceEditor.MODES.
*/
setMode: function SE_setMode(aMode)
{
// nothing to do here
},
/**
* Get the current source editor mode.
*
* @return string
* Returns one of the predefined SourceEditor.MODES. In this
* implementation SourceEditor.MODES.TEXT is always returned.
*/
getMode: function SE_getMode()
{
return SourceEditor.MODES.TEXT;
},
/**
* Setter for the read-only state of the editor.
* @param boolean aValue
* Tells if you want the editor to read-only or not.
*/
set readOnly(aValue)
{
this._textbox.readOnly = aValue;
},
/**
* Getter for the read-only state of the editor.
* @type boolean
*/
get readOnly()
{
return this._textbox.readOnly;
},
/**
* Destroy/uninitialize the editor.
*/
destroy: function SE_destroy()
{
for (let eventType in this._listeners) {
this._listeners[eventType].forEach(function(aListener) {
if (aListener.domType) {
aListener.target.removeEventListener(aListener.domType,
aListener.handler, false);
}
}, this);
}
this._editor.removeEditActionListener(this._editActionListener);
this.parentElement.removeChild(this._textbox);
this.parentElement = null;
this._editor = null;
this._textbox = null;
this._config = null;
this._listeners = null;
this._lastSelection = null;
this._editActionListener = null;
},
};
/**
* The nsIEditActionListener for the nsIEditor of the xul:textbox used by the
* SourceEditor. This listener traces text changes such that SourceEditor
* TextChanged event handlers get their events.
*
* @see
* http://mxr.mozilla.org/mozilla-central/source/editor/idl/nsIEditActionListener.idl
*
* @constructor
* @param object aSourceEditor
* An instance of the SourceEditor to notify when text changes happen.
*/
function EditActionListener(aSourceEditor) {
this._sourceEditor = aSourceEditor;
}
EditActionListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIEditActionListener]),
WillCreateNode: function() { },
DidCreateNode: function() { },
WillInsertNode: function() { },
DidInsertNode: function EAL_DidInsertNode(aNode)
{
if (aNode.nodeType != aNode.TEXT_NODE) {
return;
}
let event;
if (this._setTextRangeEvent) {
event = this._setTextRangeEvent;
delete this._setTextRangeEvent;
} else {
event = {
start: 0,
removedCharCount: 0,
addedCharCount: aNode.textContent.length,
};
}
this._sourceEditor._onTextChanged(event);
},
WillDeleteNode: function() { },
DidDeleteNode: function() { },
WillSplitNode: function() { },
DidSplitNode: function() { },
WillJoinNodes: function() { },
DidJoinNodes: function() { },
WillInsertText: function() { },
DidInsertText: function EAL_DidInsertText(aTextNode, aOffset, aString)
{
let event = {
start: aOffset,
removedCharCount: 0,
addedCharCount: aString.length,
};
this._sourceEditor._onTextChanged(event);
},
WillDeleteText: function() { },
DidDeleteText: function EAL_DidDeleteText(aTextNode, aOffset, aLength)
{
let event = {
start: aOffset,
removedCharCount: aLength,
addedCharCount: 0,
};
this._sourceEditor._onTextChanged(event);
},
WillDeleteSelection: function EAL_WillDeleteSelection()
{
if (this._setTextRangeEvent) {
return;
}
let selection = this._sourceEditor.getSelection();
let str = this._sourceEditor.getSelectedText();
let event = {
start: selection.start,
removedCharCount: str.length,
addedCharCount: 0,
};
this._sourceEditor._onTextChanged(event);
},
DidDeleteSelection: function() { },
};

View File

@ -0,0 +1,144 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Source Editor component.
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Mihai Sucan <mihai.sucan@gmail.com> (original author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK *****/
"use strict";
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
const PREF_EDITOR_COMPONENT = "devtools.editor.component";
var component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
var obj = {};
try {
Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
} catch (ex) {
Cu.reportError(ex);
Cu.reportError("SourceEditor component failed to load: " + component);
// If the component does not exist, clear the user pref back to the default.
Services.prefs.clearUserPref(PREF_EDITOR_COMPONENT);
// Load the default editor component.
component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
}
// Export the SourceEditor.
var SourceEditor = obj.SourceEditor;
var EXPORTED_SYMBOLS = ["SourceEditor"];
// Add the constants used by all SourceEditors.
/**
* Known SourceEditor preferences.
*/
SourceEditor.PREFS = {
TAB_SIZE: "devtools.editor.tabsize",
EXPAND_TAB: "devtools.editor.expandtab",
COMPONENT: PREF_EDITOR_COMPONENT,
};
/**
* Predefined source editor modes for JavaScript, CSS and other languages.
*/
SourceEditor.MODES = {
JAVASCRIPT: "js",
CSS: "css",
TEXT: "text",
HTML: "html",
XML: "xml",
};
/**
* Predefined themes for syntax highlighting.
*/
SourceEditor.THEMES = {
TEXTMATE: "textmate",
};
/**
* Source editor configuration defaults.
*/
SourceEditor.DEFAULTS = {
MODE: SourceEditor.MODES.TEXT,
THEME: SourceEditor.THEMES.TEXTMATE,
UNDO_LIMIT: 200,
TAB_SIZE: 4, // overriden by pref
EXPAND_TAB: true, // overriden by pref
};
/**
* Known editor events you can listen for.
*/
SourceEditor.EVENTS = {
/**
* The contextmenu event is fired when the editor context menu is invoked. The
* event object properties:
* - x - the pointer location on the x axis, relative to the document the
* user is editing.
* - y - the pointer location on the y axis, relative to the document the
* user is editing.
* - screenX - the pointer location on the x axis, relative to the screen.
* This value comes from the DOM contextmenu event.screenX property.
* - screenY - the pointer location on the y axis, relative to the screen.
* This value comes from the DOM contextmenu event.screenY property.
*/
CONTEXT_MENU: "ContextMenu",
/**
* The TextChanged event is fired when the editor content changes. The event
* object properties:
* - start - the character offset in the document where the change has
* occured.
* - removedCharCount - the number of characters removed from the document.
* - addedCharCount - the number of characters added to the document.
*/
TEXT_CHANGED: "TextChanged",
/**
* The Selection event is fired when the editor selection changes. The event
* object properties:
* - oldValue - the old selection range.
* - newValue - the new selection range.
* Both ranges are objects which hold two properties: start and end.
*/
SELECTION: "Selection",
};

View File

@ -0,0 +1,51 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is Source Editor test code.
#
# The Initial Developer of the Original Code is Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Rob Campbell <rcampbell@mozilla.com> (Original Author)
# Mihai Sucan <mihai.sucan@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either of the GNU General Public License Version 2 or later (the "GPL"),
# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
DEPTH = ../../../..
topsrcdir = @top_srcdir@
srcdir = @srcdir@
VPATH = @srcdir@
relativesrcdir = browser/devtools/sourceeditor/test
include $(DEPTH)/config/autoconf.mk
include $(topsrcdir)/config/rules.mk
_BROWSER_TEST_FILES = \
browser_sourceeditor_initialization.js \
libs:: $(_BROWSER_TEST_FILES)
$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)

View File

@ -0,0 +1,361 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Cu.import("resource:///modules/source-editor.jsm");
let testWin;
let testDoc;
let editor;
function test()
{
waitForExplicitFinish();
const windowUrl = "data:text/xml,<?xml version='1.0'?>" +
"<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'" +
" title='test for bug 660784' width='600' height='500'><hbox flex='1'/></window>";
const windowFeatures = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
testWin = Services.ww.openWindow(null, windowUrl, "_blank", windowFeatures, null);
testWin.addEventListener("load", initEditor, false);
}
function initEditor()
{
testWin.removeEventListener("load", initEditor, false);
testDoc = testWin.document;
let hbox = testDoc.querySelector("hbox");
editor = new SourceEditor();
let config = {
showLineNumbers: true,
placeholderText: "foobarbaz",
tabSize: 7,
expandTab: true,
};
editor.init(hbox, config, editorLoaded);
}
function editorLoaded()
{
ok(editor.editorElement, "editor loaded");
is(editor.parentElement, testDoc.querySelector("hbox"),
"parentElement is correct");
editor.focus();
is(editor.getMode(), SourceEditor.DEFAULTS.MODE, "default editor mode");
// Test general editing methods.
ok(!editor.canUndo(), "canUndo() works (nothing to undo), just loaded");
ok(!editor.readOnly, "editor is not read-only");
is(editor.getText(), "foobarbaz", "placeholderText works");
is(editor.getText().length, editor.getCharCount(),
"getCharCount() is correct");
is(editor.getText(3, 5), "ba", "getText() range works");
editor.setText("source-editor");
is(editor.getText(), "source-editor", "setText() works");
editor.setText("code", 0, 6);
is(editor.getText(), "code-editor", "setText() range works");
ok(editor.canUndo(), "canUndo() works (things to undo)");
ok(!editor.canRedo(), "canRedo() works (nothing to redo yet)");
editor.undo();
is(editor.getText(), "source-editor", "undo() works");
ok(editor.canRedo(), "canRedo() works (things to redo)");
editor.redo();
is(editor.getText(), "code-editor", "redo() works");
// Test selection methods.
editor.setSelection(0, 4);
is(editor.getSelectedText(), "code", "getSelectedText() works");
let selection = editor.getSelection();
ok(selection.start == 0 && selection.end == 4, "getSelection() works");
editor.dropSelection();
selection = editor.getSelection();
ok(selection.start == 4 && selection.end == 4, "dropSelection() works");
editor.setCaretOffset(7);
is(editor.getCaretOffset(), 7, "setCaretOffset() works");
// Test grouped changes.
editor.setText("foobar");
editor.startCompoundChange();
editor.setText("foo1");
editor.setText("foo2");
editor.setText("foo3");
editor.endCompoundChange();
is(editor.getText(), "foo3", "editor content is correct");
editor.undo();
is(editor.getText(), "foobar", "compound change undo() works");
editor.redo();
is(editor.getText(), "foo3", "compound change redo() works");
// Minimal keyboard usage tests.
ok(editor.hasFocus(), "editor has focus");
editor.setText("code-editor");
editor.setCaretOffset(7);
EventUtils.synthesizeKey(".", {}, testWin);
is(editor.getText(), "code-ed.itor", "focus() and typing works");
EventUtils.synthesizeKey("a", {}, testWin);
is(editor.getText(), "code-ed.aitor", "typing works");
is(editor.getCaretOffset(), 9, "caret moved");
EventUtils.synthesizeKey("VK_LEFT", {}, testWin);
is(editor.getCaretOffset(), 8, "caret moved to the left");
EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
is(editor.getSelectedText(), "code-ed.aitor",
"select all worked");
EventUtils.synthesizeKey("x", {accelKey: true}, testWin);
ok(!editor.getText(), "cut works");
EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
is(editor.getText(), "code-ed.aitorcode-ed.aitor", "paste works");
editor.setText("foo");
EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
EventUtils.synthesizeKey("c", {accelKey: true}, testWin);
EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
is(editor.getText(), "foofoo", "ctrl-a, c, v, v works");
is(editor.getCaretOffset(), 6, "caret location is correct");
EventUtils.synthesizeKey(".", {}, testWin);
EventUtils.synthesizeKey("VK_TAB", {}, testWin);
is(editor.getText(), "foofoo. ", "Tab works");
is(editor.getCaretOffset(), 14, "caret location is correct");
// Test the Tab key.
editor.setText("a\n b\n c");
editor.setCaretOffset(0);
EventUtils.synthesizeKey("VK_TAB", {}, testWin);
is(editor.getText(), " a\n b\n c", "Tab works");
// Code editor specific tests. These are not applicable when the textarea
// fallback is used.
let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);
if (component != "textarea") {
editor.setMode(SourceEditor.MODES.JAVASCRIPT);
is(editor.getMode(), SourceEditor.MODES.JAVASCRIPT, "setMode() works");
editor.setSelection(0, editor.getCharCount() - 1);
EventUtils.synthesizeKey("VK_TAB", {}, testWin);
is(editor.getText(), " a\n b\n c", "lines indented");
EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}, testWin);
is(editor.getText(), " a\n b\n c", "lines outdented (shift-tab)");
testBackspaceKey();
testReturnKey();
}
// Test the read-only mode.
editor.setText("foofoo");
editor.readOnly = true;
EventUtils.synthesizeKey("b", {}, testWin);
is(editor.getText(), "foofoo", "editor is now read-only (keyboard)");
editor.setText("foobar");
is(editor.getText(), "foobar", "editor allows programmatic changes (setText)");
editor.readOnly = false;
editor.setCaretOffset(editor.getCharCount());
EventUtils.synthesizeKey("-", {}, testWin);
is(editor.getText(), "foobar-", "editor is now editable again");
// Test the Selection event.
editor.setText("foobarbaz");
editor.setSelection(1, 4);
let event = null;
let eventHandler = function(aEvent) {
event = aEvent;
};
editor.addEventListener(SourceEditor.EVENTS.SELECTION, eventHandler);
editor.setSelection(0, 3);
ok(event, "selection event fired");
ok(event.oldValue.start == 1 && event.oldValue.end == 4,
"event.oldValue is correct");
ok(event.newValue.start == 0 && event.newValue.end == 3,
"event.newValue is correct");
event = null;
editor.dropSelection();
ok(event, "selection dropped");
ok(event.oldValue.start == 0 && event.oldValue.end == 3,
"event.oldValue is correct");
ok(event.newValue.start == 3 && event.newValue.end == 3,
"event.newValue is correct");
event = null;
EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
ok(event, "select all worked");
ok(event.oldValue.start == 3 && event.oldValue.end == 3,
"event.oldValue is correct");
ok(event.newValue.start == 0 && event.newValue.end == 9,
"event.newValue is correct");
event = null;
editor.removeEventListener(SourceEditor.EVENTS.SELECTION, eventHandler);
editor.dropSelection();
ok(!event, "selection event listener removed");
// Test the TextChanged event.
editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, eventHandler);
EventUtils.synthesizeKey(".", {}, testWin);
ok(event, "the TextChanged event fired after keypress");
is(event.start, 9, "event.start is correct");
is(event.removedCharCount, 0, "event.removedCharCount is correct");
is(event.addedCharCount, 1, "event.addedCharCount is correct");
let chars = editor.getText().length;
event = null;
EventUtils.synthesizeKey("a", {accelKey: true}, testWin);
EventUtils.synthesizeKey("c", {accelKey: true}, testWin);
editor.setCaretOffset(chars);
EventUtils.synthesizeKey("v", {accelKey: true}, testWin);
ok(event, "the TextChanged event fired after paste");
is(event.start, chars, "event.start is correct");
is(event.removedCharCount, 0, "event.removedCharCount is correct");
is(event.addedCharCount, chars, "event.addedCharCount is correct");
editor.setText("line1\nline2\nline3");
chars = editor.getText().length;
event = null;
editor.setText("a\nline4\nline5", chars);
ok(event, "the TextChanged event fired after setText()");
is(event.start, chars, "event.start is correct");
is(event.removedCharCount, 0, "event.removedCharCount is correct");
is(event.addedCharCount, 13, "event.addedCharCount is correct");
editor.setText("line3b\nline4b\nfoo", 12, 24);
ok(event, "the TextChanged event fired after setText() again");
is(event.start, 12, "event.start is correct");
is(event.removedCharCount, 12, "event.removedCharCount is correct");
is(event.addedCharCount, 17, "event.addedCharCount is correct");
editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, eventHandler);
// Done.
editor.destroy();
ok(!editor.parentElement && !editor.editorElement, "destroy() works");
testWin.close();
testWin = testDoc = editor = null;
finish();
}
function testBackspaceKey()
{
editor.setText(" a\n b\n c");
editor.setCaretOffset(7);
EventUtils.synthesizeKey("VK_BACK_SPACE", {}, testWin);
is(editor.getText(), "a\n b\n c", "line outdented (Backspace)");
editor.undo();
editor.setCaretOffset(6);
EventUtils.synthesizeKey("VK_BACK_SPACE", {}, testWin);
is(editor.getText(), " a\n b\n c", "backspace one char works");
}
function testReturnKey()
{
editor.setText(" a\n b\n c");
editor.setCaretOffset(8);
EventUtils.synthesizeKey("VK_RETURN", {}, testWin);
EventUtils.synthesizeKey("x", {}, testWin);
let lineDelimiter = editor.getLineDelimiter();
ok(lineDelimiter, "we have the line delimiter");
is(editor.getText(), " a" + lineDelimiter + " x\n b\n c",
"return maintains indentation");
editor.setCaretOffset(12 + lineDelimiter.length);
EventUtils.synthesizeKey("z", {}, testWin);
EventUtils.synthesizeKey("VK_RETURN", {}, testWin);
EventUtils.synthesizeKey("y", {}, testWin);
is(editor.getText(), " a" + lineDelimiter +
" z" + lineDelimiter + " yx\n b\n c",
"return maintains indentation (again)");
}

View File

@ -114,10 +114,3 @@
<!ENTITY webConsoleCmd.label "Web Console">
<!ENTITY webConsoleCmd.accesskey "W">
<!ENTITY webConsoleCmd.commandkey "k">
<!-- LOCALIZATION NOTE (textbox.placeholder1): This is some placeholder text
- that appears when the Scratchpad's text area is empty and unfocused.
- It should be a one-line JavaScript comment, i.e., preceded by '//'
-->
<!ENTITY textbox.placeholder1 "// Enter some JavaScript, select it, right click and select Run, Inspect or Display.">

View File

@ -118,6 +118,7 @@
<li><a href="about:license#chromium">Chromium License</a></li>
<li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
<li><a href="about:license#expat">Expat License</a></li>
<li><a href="about:license#edl">Eclipse Distribution License</a></li>
<li><a href="about:license#firebug">Firebug License</a></li>
<li><a href="about:license#gfx-font-list">gfxFontList License</a></li>
<li><a href="about:license#gears">Google Gears License</a></li>
@ -2334,6 +2335,45 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
<hr>
<h1><a name="edl"></a>Eclipse Distribution License</h1>
<p>This license applies to certain files in the directory
<span class="path">browser/devtools/sourceeditor/orion/</span>.</p>
<pre>
Eclipse Distribution License - v 1.0
Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the Eclipse Foundation, Inc. nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</pre>
<hr>
<h1><a name="firebug"></a>Firebug License</h1>
<p>This license applies to the code