Bug 912260 - Make Scratchpad use CodeMirror. r=robcee r=msucan

This commit is contained in:
Anton Kovalyov 2013-09-20 11:34:10 -07:00
parent dcca82fb9d
commit e1e454ec8e
66 changed files with 10227 additions and 436 deletions

View File

@ -72,7 +72,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");

View File

@ -20,7 +20,7 @@ let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils
let { BrowserDebuggerProcess } = Cu.import("resource:///modules/devtools/DebuggerProcess.jsm", {});
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
let { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
let { SourceEditor } = Cu.import("resource:///modules/source-editor.jsm", {});
let { SourceEditor } = Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", {});
let { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
let TargetFactory = devtools.TargetFactory;
let Toolbox = devtools.Toolbox;

View File

@ -29,6 +29,15 @@ browser.jar:
content/browser/devtools/fontinspector/font-inspector.xhtml (fontinspector/font-inspector.xhtml)
content/browser/devtools/fontinspector/font-inspector.css (fontinspector/font-inspector.css)
content/browser/devtools/orion.js (sourceeditor/orion/orion.js)
content/browser/devtools/codemirror/codemirror.js (sourceeditor/codemirror/codemirror.js)
content/browser/devtools/codemirror/codemirror.css (sourceeditor/codemirror/codemirror.css)
content/browser/devtools/codemirror/javascript.js (sourceeditor/codemirror/javascript.js)
content/browser/devtools/codemirror/matchbrackets.js (sourceeditor/codemirror/matchbrackets.js)
content/browser/devtools/codemirror/comment.js (sourceeditor/codemirror/comment.js)
content/browser/devtools/codemirror/searchcursor.js (sourceeditor/codemirror/search/searchcursor.js)
content/browser/devtools/codemirror/search.js (sourceeditor/codemirror/search/search.js)
content/browser/devtools/codemirror/dialog.js (sourceeditor/codemirror/dialog/dialog.js)
content/browser/devtools/codemirror/dialog.css (sourceeditor/codemirror/dialog/dialog.css)
* content/browser/devtools/source-editor-overlay.xul (sourceeditor/source-editor-overlay.xul)
content/browser/devtools/debugger.xul (debugger/debugger.xul)
content/browser/devtools/debugger.css (debugger/debugger.css)

View File

@ -61,7 +61,7 @@ const EVENTS = {
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");

View File

@ -14,19 +14,34 @@
"use strict";
let require = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
let { Cc, Ci, Cu } = require("chrome");
let promise = require("sdk/core/promise");
let Telemetry = require("devtools/shared/telemetry");
let DevtoolsHelpers = require("devtools/shared/helpers");
let TargetFactory = require("devtools/framework/target").TargetFactory;
const SCRATCHPAD_CONTEXT_CONTENT = 1;
const SCRATCHPAD_CONTEXT_BROWSER = 2;
const BUTTON_POSITION_SAVE = 0;
const BUTTON_POSITION_CANCEL = 1;
const BUTTON_POSITION_DONT_SAVE = 2;
const BUTTON_POSITION_REVERT = 0;
const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const promise = require("sdk/core/promise");
const Telemetry = require("devtools/shared/telemetry");
const escodegen = require("escodegen/escodegen");
const Editor = require("devtools/sourceeditor/editor");
const TargetFactory = require("devtools/framework/target").TargetFactory;
const DevtoolsHelpers = require("devtools/shared/helpers");
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/source-editor.jsm");
Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
Cu.import("resource://gre/modules/jsdebugger.jsm");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
@ -54,19 +69,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
"resource://gre/modules/devtools/dbg-client.jsm");
XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () =>
Services.prefs.getIntPref("devtools.debugger.remote-timeout")
);
const SCRATCHPAD_CONTEXT_CONTENT = 1;
const SCRATCHPAD_CONTEXT_BROWSER = 2;
const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
const BUTTON_POSITION_SAVE = 0;
const BUTTON_POSITION_CANCEL = 1;
const BUTTON_POSITION_DONT_SAVE = 2;
const BUTTON_POSITION_REVERT = 0;
const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
Services.prefs.getIntPref("devtools.debugger.remote-timeout"));
// Because we have no constructor / destructor where we can log metrics we need
// to do so here.
@ -79,6 +82,7 @@ telemetry.toolOpened("scratchpad");
var Scratchpad = {
_instanceId: null,
_initialWindowTitle: document.title,
_dirty: false,
/**
* Check if provided string is a mode-line and, if it is, return an
@ -134,6 +138,26 @@ var Scratchpad = {
*/
initialized: false,
/**
* Returns the 'dirty' state of this Scratchpad.
*/
get dirty()
{
let clean = this.editor && this.editor.isClean();
return this._dirty || !clean;
},
/**
* Sets the 'dirty' state of this Scratchpad.
*/
set dirty(aValue)
{
this._dirty = aValue;
if (!aValue && this.editor)
this.editor.markClean();
this._updateTitle();
},
/**
* Retrieve the xul:notificationbox DOM element. It notifies the user when
* the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
@ -143,17 +167,6 @@ var Scratchpad = {
return document.getElementById("scratchpad-notificationbox");
},
/**
* Get the selected text from the editor.
*
* @return string
* The selected text.
*/
get selectedText()
{
return this.editor.getSelectedText();
},
/**
* Get the editor content, in the given range. If no range is given you get
* the entire editor content.
@ -169,24 +182,8 @@ var Scratchpad = {
*/
getText: function SP_getText(aStart, aEnd)
{
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);
var value = this.editor.getText();
return value.slice(aStart || 0, aEnd || value.length);
},
/**
@ -209,9 +206,8 @@ var Scratchpad = {
{
let title = this.filename || this._initialWindowTitle;
if (this.editor && this.editor.dirty) {
if (this.dirty)
title = "*" + title;
}
document.title = title;
},
@ -230,7 +226,7 @@ var Scratchpad = {
filename: this.filename,
text: this.getText(),
executionContext: this.executionContext,
saved: !this.editor.dirty,
saved: !this.dirty
};
},
@ -243,19 +239,15 @@ var Scratchpad = {
*/
setState: function SP_setState(aState)
{
if (aState.filename) {
if (aState.filename)
this.setFilename(aState.filename);
}
if (this.editor) {
this.editor.dirty = !aState.saved;
}
if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
this.dirty = !aState.saved;
if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER)
this.setBrowserContext();
}
else {
else
this.setContentContext();
}
},
/**
@ -297,36 +289,12 @@ var Scratchpad = {
},
/**
* Drop the editor selection.
* Replaces context of an editor with provided value (a string).
* Note: this method is simply a shortcut to editor.setText.
*/
deselect: function SP_deselect()
setText: function SP_setText(value)
{
this.editor.dropSelection();
},
/**
* Select a specific range in the Scratchpad editor.
*
* @param number aStart
* Selection range start.
* @param number aEnd
* Selection range end.
*/
selectRange: function SP_selectRange(aStart, 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();
return this.editor.setText(value);
},
/**
@ -380,7 +348,7 @@ var Scratchpad = {
*/
execute: function SP_execute()
{
let selection = this.selectedText || this.getText();
let selection = this.editor.getSelection() || this.getText();
return this.evaluate(selection);
},
@ -403,7 +371,7 @@ var Scratchpad = {
this.writeAsErrorComment(aError.exception).then(resolve, reject);
}
else {
this.deselect();
this.editor.dropSelection();
resolve();
}
}, reject);
@ -434,7 +402,7 @@ var Scratchpad = {
this._writePrimitiveAsComment(aResult).then(resolve, reject);
}
else {
this.deselect();
this.editor.dropSelection();
this.sidebar.open(aString, aResult).then(resolve, reject);
}
}, reject);
@ -539,7 +507,7 @@ var Scratchpad = {
}
}
});
this.setText(prettyText);
this.editor.setText(prettyText);
} catch (e) {
this.writeAsErrorComment(DevToolsUtils.safeErrorString(e));
}
@ -588,17 +556,21 @@ var Scratchpad = {
*/
writeAsComment: function SP_writeAsComment(aValue)
{
let selection = this.getSelectionRange();
let insertionPoint = selection.start != selection.end ?
selection.end : // after selected text
this.editor.getCharCount(); // after text end
let value = "\n/*\n" + aValue + "\n*/";
let newComment = "\n/*\n" + aValue + "\n*/";
if (this.editor.somethingSelected()) {
let from = this.editor.getCursor("end");
this.editor.replaceSelection(this.editor.getSelection() + value);
let to = this.editor.getPosition(this.editor.getOffset(from) + value.length);
this.editor.setSelection(from, to);
return;
}
this.setText(newComment, insertionPoint, insertionPoint);
let text = this.editor.getText();
this.editor.setText(text + value);
// Select the new comment.
this.selectRange(insertionPoint, insertionPoint + newComment.length);
let [ from, to ] = this.editor.getPosition(text.length, (text + value).length);
this.editor.setSelection(from, to);
},
/**
@ -814,8 +786,9 @@ var Scratchpad = {
this.setBrowserContext();
}
this.setText(content);
this.editor.resetUndo();
this.editor.setText(content);
this.editor.clearHistory();
document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
}
else if (!aSilentError) {
window.alert(this.strings.GetStringFromName("openFile.failed"));
@ -1090,7 +1063,8 @@ var Scratchpad = {
this.exportToFile(file, true, false, aStatus => {
if (Components.isSuccessCode(aStatus)) {
this.editor.dirty = false;
this.dirty = false;
document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
this.setRecentFile(file);
}
if (aCallback) {
@ -1113,7 +1087,7 @@ var Scratchpad = {
this.setFilename(fp.file.path);
this.exportToFile(fp.file, true, false, aStatus => {
if (Components.isSuccessCode(aStatus)) {
this.editor.dirty = false;
this.dirty = false;
this.setRecentFile(fp.file);
}
if (aCallback) {
@ -1315,76 +1289,45 @@ var Scratchpad = {
initialText = state.text;
}
this.editor = new SourceEditor();
this.editor = new Editor({
mode: Editor.modes.js,
value: initialText,
lineNumbers: true,
contextMenu: "scratchpad-text-popup"
});
let config = {
mode: SourceEditor.MODES.JAVASCRIPT,
showLineNumbers: true,
initialText: initialText,
contextMenu: "scratchpad-text-popup",
};
this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => {
var lines = initialText.split("\n");
let editorPlaceholder = document.getElementById("scratchpad-editor");
this.editor.init(editorPlaceholder, config,
this._onEditorLoad.bind(this, state));
this.editor.on("change", this._onChanged);
this.editor.focus();
this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
if (state)
this.dirty = !state.saved;
this.initialized = true;
this._triggerObservers("Ready");
this.populateRecentFilesMenu();
PreferenceObserver.init();
}).then(null, (err) => console.log(err.message));
},
/**
* The load event handler for the source editor. This method does post-load
* editor initialization.
*
* @private
* @param object aState
* The initial Scratchpad state object.
*/
_onEditorLoad: function SP__onEditorLoad(aState)
{
this.editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
this._onDirtyChanged);
this.editor.focus();
this.editor.setCaretOffset(this.editor.getCharCount());
if (aState) {
this.editor.dirty = !aState.saved;
}
this.initialized = true;
this._triggerObservers("Ready");
this.populateRecentFilesMenu();
PreferenceObserver.init();
},
/**
* Insert text at the current caret location.
*
* @param string aText
* The text you want to insert.
*/
insertTextAtCaret: function SP_insertTextAtCaret(aText)
{
let caretOffset = this.editor.getCaretOffset();
this.setText(aText, caretOffset, caretOffset);
this.editor.setCaretOffset(caretOffset + aText.length);
},
/**
* The Source Editor DirtyChanged event handler. This function updates the
* The Source Editor "change" event handler. This function updates the
* Scratchpad window title to show an asterisk when there are unsaved changes.
*
* @private
* @see SourceEditor.EVENTS.DIRTY_CHANGED
* @param object aEvent
* The DirtyChanged event object.
*/
_onDirtyChanged: function SP__onDirtyChanged(aEvent)
_onChanged: function SP__onChanged()
{
Scratchpad._updateTitle();
if (Scratchpad.filename) {
if (Scratchpad.editor.dirty) {
if (Scratchpad.dirty)
document.getElementById("sp-cmd-revert").removeAttribute("disabled");
}
else {
else
document.getElementById("sp-cmd-revert").setAttribute("disabled", true);
}
}
},
@ -1417,21 +1360,22 @@ var Scratchpad = {
}
// This event is created only after user uses 'reload and run' feature.
if (this._reloadAndRunEvent) {
if (this._reloadAndRunEvent && this.gBrowser) {
this.gBrowser.selectedBrowser.removeEventListener("load",
this._reloadAndRunEvent, true);
}
this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
this._onDirtyChanged);
PreferenceObserver.uninit();
this.editor.off("change", this._onChanged);
this.editor.destroy();
this.editor = null;
if (this._sidebar) {
this._sidebar.destroy();
this._sidebar = null;
}
this.webConsoleClient = null;
this.debuggerClient = null;
this.initialized = false;
@ -1452,7 +1396,7 @@ var Scratchpad = {
*/
promptSave: function SP_promptSave(aCallback)
{
if (this.editor.dirty) {
if (this.dirty) {
let ps = Services.prompt;
let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +

View File

@ -1,18 +1,22 @@
<?xml version="1.0"?>
#ifdef 0
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
#endif
<!DOCTYPE window [
<!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
%scratchpadDTD;
<!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
%editMenuStrings;
<!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
%sourceEditorStrings;
]>
<?xml-stylesheet href="chrome://global/skin/global.css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/scratchpad.css"?>
<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
<?xul-overlay href="chrome://browser/content/devtools/source-editor-overlay.xul"?>
<window id="main-window"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@ -27,8 +31,20 @@
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
<script type="application/javascript" src="chrome://browser/content/devtools/scratchpad.js"/>
<script type="application/javascript">
function goUpdateSourceEditorMenuItems() {
goUpdateGlobalEditMenuItems();
let commands = ['cmd_undo', 'cmd_redo', 'cmd_cut', 'cmd_paste', 'cmd_delete', 'cmd_findAgain'];
commands.forEach(goUpdateCommand);
}
</script>
<commandset id="editMenuCommands"/>
<commandset id="sourceEditorCommands"/>
<commandset id="sourceEditorCommands">
<command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')"/>
</commandset>
<commandset id="sp-commandset">
<command id="sp-cmd-newWindow" oncommand="Scratchpad.openScratchpad();"/>
@ -37,11 +53,6 @@
<command id="sp-cmd-save" oncommand="Scratchpad.saveFile();"/>
<command id="sp-cmd-saveas" oncommand="Scratchpad.saveFileAs();"/>
<command id="sp-cmd-revert" oncommand="Scratchpad.promptRevert();" disabled="true"/>
<!-- TODO: bug 650340 - implement printFile()
<command id="sp-cmd-printFile" oncommand="Scratchpad.printFile();" disabled="true"/>
-->
<command id="sp-cmd-close" oncommand="Scratchpad.close();"/>
<command id="sp-cmd-run" oncommand="Scratchpad.run();"/>
<command id="sp-cmd-inspect" oncommand="Scratchpad.inspect();"/>
@ -56,7 +67,7 @@
<command id="sp-cmd-hideSidebar" oncommand="Scratchpad.sidebar.hide();"/>
</commandset>
<keyset id="sourceEditorKeys"/>
<keyset id="editMenuKeys"/>
<keyset id="sp-keyset">
<key id="sp-key-window"
@ -75,14 +86,6 @@
key="&closeCmd.key;"
command="sp-cmd-close"
modifiers="accel"/>
<!-- TODO: bug 650340 - implement printFile
<key id="sp-key-printFile"
key="&printCmd.commandkey;"
command="sp-cmd-printFile"
modifiers="accel"/>
-->
<key id="sp-key-run"
key="&run.key;"
command="sp-cmd-run"
@ -115,10 +118,8 @@
command="sp-cmd-documentationLink"/>
</keyset>
<menubar id="sp-menubar">
<menu id="sp-file-menu" label="&fileMenu.label;"
accesskey="&fileMenu.accesskey;">
<menu id="sp-file-menu" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
<menupopup id="sp-menu-filepopup">
<menuitem id="sp-menu-newscratchpad"
label="&newWindowCmd.label;"
@ -126,16 +127,19 @@
key="sp-key-window"
command="sp-cmd-newWindow"/>
<menuseparator/>
<menuitem id="sp-menu-open"
label="&openFileCmd.label;"
command="sp-cmd-openFile"
key="sp-key-open"
accesskey="&openFileCmd.accesskey;"/>
<menu id="sp-open_recent-menu" label="&openRecentMenu.label;"
accesskey="&openRecentMenu.accesskey;"
disabled="true">
<menupopup id="sp-menu-open_recentPopup"/>
</menu>
<menuitem id="sp-menu-save"
label="&saveFileCmd.label;"
accesskey="&saveFileCmd.accesskey;"
@ -151,14 +155,6 @@
command="sp-cmd-revert"/>
<menuseparator/>
<!-- TODO: bug 650340 - implement printFile
<menuitem id="sp-menu-print"
label="&printCmd.label;"
accesskey="&printCmd.accesskey;"
command="sp-cmd-printFile"/>
<menuseparator/>
-->
<menuitem id="sp-menu-close"
label="&closeCmd.label;"
key="sp-key-close"
@ -166,25 +162,31 @@
command="sp-cmd-close"/>
</menupopup>
</menu>
<menu id="sp-edit-menu" label="&editMenu.label;"
accesskey="&editMenu.accesskey;">
<menupopup id="sp-menu_editpopup"
onpopupshowing="goUpdateSourceEditorMenuItems()">
<menuitem id="se-menu-undo"/>
<menuitem id="se-menu-redo"/>
<menuitem id="menu_undo"/>
<menuitem id="menu_redo"/>
<menuseparator/>
<menuitem id="se-menu-cut"/>
<menuitem id="se-menu-copy"/>
<menuitem id="se-menu-paste"/>
<menuitem id="menu_cut"/>
<menuitem id="menu_copy"/>
<menuitem id="menu_paste"/>
<menuseparator/>
<menuitem id="se-menu-selectAll"/>
<menuitem id="menu_selectAll"/>
<menuseparator/>
<menuitem id="se-menu-find"/>
<menuitem id="se-menu-findAgain"/>
<menuitem id="menu_find"/>
<menuitem id="menu_findAgain"/>
<menuseparator/>
<menuitem id="se-menu-gotoLine"/>
<menuitem id="se-menu-gotoLine"
label="&gotoLineCmd.label;"
accesskey="&gotoLineCmd.accesskey;"
key="key_gotoLine"
command="cmd_gotoLine"/>
</menupopup>
</menu>
<menu id="sp-execute-menu" label="&executeMenu.label;"
accesskey="&executeMenu.accesskey;">
<menupopup id="sp-menu_executepopup">
@ -288,12 +290,12 @@
<popupset id="scratchpad-popups">
<menupopup id="scratchpad-text-popup"
onpopupshowing="goUpdateSourceEditorMenuItems()">
<menuitem id="se-cMenu-cut"/>
<menuitem id="se-cMenu-copy"/>
<menuitem id="se-cMenu-paste"/>
<menuitem id="se-cMenu-delete"/>
<menuitem id="cMenu_cut"/>
<menuitem id="cMenu_copy"/>
<menuitem id="cMenu_paste"/>
<menuitem id="cMenu_delete"/>
<menuseparator/>
<menuitem id="se-cMenu-selectAll"/>
<menuitem id="cMenu_selectAll"/>
<menuseparator/>
<menuitem id="sp-text-run"
label="&run.label;"

View File

@ -19,7 +19,6 @@ MOCHITEST_BROWSER_FILES = \
browser_scratchpad_bug_669612_unsaved.js \
browser_scratchpad_bug684546_reset_undo.js \
browser_scratchpad_bug690552_display_outputs_errors.js \
browser_scratchpad_bug650345_find_ui.js \
browser_scratchpad_bug714942_goto_line_ui.js \
browser_scratchpad_bug_650760_help_key.js \
browser_scratchpad_bug_651942_recent_files.js \

View File

@ -1,97 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function browserLoad() {
gBrowser.selectedBrowser.removeEventListener("load", browserLoad, true);
openScratchpad(runTests);
}, true);
content.location = "data:text/html,<p>test the Find feature in Scratchpad";
}
function runTests(aWindow, aScratchpad)
{
let editor = aScratchpad.editor;
let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
editor.setText(text);
let needle = "foobar";
editor.setSelection(0, needle.length);
let oldPrompt = Services.prompt;
Services.prompt = {
prompt: function() { return true; },
};
let findKey = "F";
info("test Ctrl/Cmd-" + findKey + " (find)");
EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
let selection = editor.getSelection();
let newIndex = text.indexOf(needle, needle.length);
is(selection.start, newIndex, "selection.start is correct");
is(selection.end, newIndex + needle.length, "selection.end is correct");
info("test cmd_find");
aWindow.goDoCommand("cmd_find");
selection = editor.getSelection();
is(selection.start, 0, "selection.start is correct");
is(selection.end, needle.length, "selection.end is correct");
let findNextKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
let findNextKeyOptions = Services.appinfo.OS == "Darwin" ?
{accelKey: true} : {};
info("test " + findNextKey + " (findNext)");
EventUtils.synthesizeKey(findNextKey, findNextKeyOptions, aWindow);
selection = editor.getSelection();
is(selection.start, newIndex, "selection.start is correct");
is(selection.end, newIndex + needle.length, "selection.end is correct");
info("test cmd_findAgain");
aWindow.goDoCommand("cmd_findAgain");
selection = editor.getSelection();
is(selection.start, 0, "selection.start is correct");
is(selection.end, needle.length, "selection.end is correct");
let findPreviousKey = Services.appinfo.OS == "Darwin" ? "G" : "VK_F3";
let findPreviousKeyOptions = Services.appinfo.OS == "Darwin" ?
{accelKey: true, shiftKey: true} : {shiftKey: true};
info("test " + findPreviousKey + " (findPrevious)");
EventUtils.synthesizeKey(findPreviousKey, findPreviousKeyOptions, aWindow);
selection = editor.getSelection();
is(selection.start, newIndex, "selection.start is correct");
is(selection.end, newIndex + needle.length, "selection.end is correct");
info("test cmd_findPrevious");
aWindow.goDoCommand("cmd_findPrevious");
selection = editor.getSelection();
is(selection.start, 0, "selection.start is correct");
is(selection.end, needle.length, "selection.end is correct");
needle = "BAZbaz";
newIndex = text.toLowerCase().indexOf(needle.toLowerCase());
Services.prompt = {
prompt: function(aWindow, aTitle, aMessage, aValue) {
aValue.value = needle;
return true;
},
};
info("test Ctrl/Cmd-" + findKey + " (find) with a custom value");
EventUtils.synthesizeKey(findKey, {accelKey: true}, aWindow);
selection = editor.getSelection();
is(selection.start, newIndex, "selection.start is correct");
is(selection.end, newIndex + needle.length, "selection.end is correct");
Services.prompt = oldPrompt;
finish();
}

View File

@ -101,7 +101,8 @@ function fileAImported(aStatus, aFileContent)
is(gScratchpad.getText(), gFileAContent, "the editor content is correct");
gScratchpad.setText("new text", gScratchpad.getText().length);
gScratchpad.editor.replaceText("new text",
gScratchpad.editor.posFromIndex(gScratchpad.getText().length));
is(gScratchpad.getText(), gFileAContent + "new text", "text updated correctly");
gScratchpad.undo();
@ -129,7 +130,8 @@ function fileBImported(aStatus, aFileContent)
is(gScratchpad.getText(), gFileBContent,
"the editor content is still correct after undo");
gScratchpad.setText("new text", gScratchpad.getText().length);
gScratchpad.editor.replaceText("new text",
gScratchpad.editor.posFromIndex(gScratchpad.getText().length));
is(gScratchpad.getText(), gFileBContent + "new text", "text updated correctly");
gScratchpad.undo();

View File

@ -20,26 +20,23 @@ function runTests(aWindow, aScratchpad)
let editor = aScratchpad.editor;
let text = "foobar bug650345\nBug650345 bazbaz\nfoobar omg\ntest";
editor.setText(text);
editor.setCaretOffset(0);
editor.setCursor({ line: 0, ch: 0 });
let oldPrompt = Services.prompt;
let desiredValue = null;
Services.prompt = {
prompt: function(aWindow, aTitle, aMessage, aValue) {
aValue.value = desiredValue;
return true;
},
let oldPrompt = editor.openDialog;
let desiredValue;
editor.openDialog = function (text, cb) {
cb(desiredValue);
};
desiredValue = 3;
EventUtils.synthesizeKey("J", {accelKey: true}, aWindow);
is(editor.getCaretOffset(), 34, "caret offset is correct");
is(editor.getCursor().line, 2, "line is correct");
desiredValue = 2;
aWindow.goDoCommand("cmd_gotoLine")
is(editor.getCaretOffset(), 17, "caret offset is correct (again)");
Services.prompt = oldPrompt;
is(editor.getCursor().line, 1, "line is correct (again)");
editor.openDialog = oldPrompt;
finish();
}

View File

@ -28,30 +28,24 @@ function runTests()
ok(sp.editor.hasFocus(), "the editor has focus");
sp.setText("window.foo;");
sp.editor.setCaretOffset(0);
sp.editor.setCursor({ line: 0, ch: 0 });
EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
is(sp.getText(), " window.foo;", "Tab key added 5 spaces");
is(sp.editor.getCaretOffset(), 5, "caret location is correct");
is(sp.editor.getCursor().line, 0, "line is correct");
is(sp.editor.getCursor().ch, 5, "character is correct");
sp.editor.setCaretOffset(6);
sp.editor.setCursor({ line: 0, ch: 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.getText(), " w omgindow.foo;", "insertTextAtCaret() works");
is(sp.editor.getCaretOffset(), 13, "caret location is correct after update");
is(sp.editor.getCursor().line, 0, "line is correct");
is(sp.editor.getCursor().ch, 10, "character is correct");
gScratchpadWindow.close();
@ -66,16 +60,17 @@ function runTests2()
let sp = gScratchpadWindow.Scratchpad;
sp.setText("window.foo;");
sp.editor.setCaretOffset(0);
sp.editor.setCursor({ line: 0, ch: 0 });
EventUtils.synthesizeKey("VK_TAB", {}, gScratchpadWindow);
is(sp.getText(), "\twindow.foo;", "Tab key added the tab character");
is(sp.editor.getCaretOffset(), 1, "caret location is correct");
is(sp.editor.getCursor().line, 0, "line is correct");
is(sp.editor.getCursor().ch, 1, "character is correct");
Services.prefs.clearUserPref("devtools.editor.tabsize");
Services.prefs.clearUserPref("devtools.editor.expandtab");
finish();
}
}

View File

@ -32,16 +32,16 @@ function testListeners()
{
openScratchpad(function(aWin, aScratchpad) {
aScratchpad.setText("new text");
ok(isStar(aWin), "show start if scratchpad text changes");
ok(isStar(aWin), "show star if scratchpad text changes");
aScratchpad.editor.dirty = false;
aScratchpad.dirty = false;
ok(!isStar(aWin), "no star before changing text");
aScratchpad.setFilename("foo.js");
aScratchpad.setText("new text2");
ok(isStar(aWin), "shows star if scratchpad text changes");
aScratchpad.editor.dirty = false;
aScratchpad.dirty = false;
ok(!isStar(aWin), "no star if scratchpad was just saved");
aScratchpad.setText("new text3");

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
function test()
@ -51,9 +51,9 @@ function runTests()
let menuPopup = editMenu.menupopup;
ok(menuPopup, "the Edit menupopup");
let cutItem = doc.getElementById("se-menu-cut");
let cutItem = doc.getElementById("menu_cut");
ok(cutItem, "the Cut menuitem");
let pasteItem = doc.getElementById("se-menu-paste");
let pasteItem = doc.getElementById("menu_paste");
ok(pasteItem, "the Paste menuitem");
let anchor = doc.documentElement;
@ -109,7 +109,7 @@ function runTests()
};
let firstHide = function() {
sp.selectRange(0, 10);
sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 10 });
openMenu(11, 11, showAfterSelect);
};
@ -119,9 +119,9 @@ function runTests()
};
let hideAfterSelect = function() {
sp.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
sp.editor.on("change", onCut);
waitForFocus(function () {
let selectedText = sp.editor.getSelectedText();
let selectedText = sp.editor.getSelection();
ok(selectedText.length > 0, "non-empty selected text will be cut");
EventUtils.synthesizeKey("x", {accelKey: true}, gScratchpadWindow);
@ -129,7 +129,7 @@ function runTests()
};
let onCut = function() {
sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onCut);
sp.editor.off("change", onCut);
openMenu(12, 12, showAfterCut);
};
@ -140,14 +140,14 @@ function runTests()
};
let hideAfterCut = function() {
sp.editor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
sp.editor.on("change", onPaste);
waitForFocus(function () {
EventUtils.synthesizeKey("v", {accelKey: true}, gScratchpadWindow);
}, gScratchpadWindow);
};
let onPaste = function() {
sp.editor.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED, onPaste);
sp.editor.off("change", onPaste);
openMenu(13, 13, showAfterPaste);
};
@ -174,9 +174,9 @@ function runTests()
menuPopup = doc.getElementById("scratchpad-text-popup");
ok(menuPopup, "the context menupopup");
cutItem = doc.getElementById("se-cMenu-cut");
cutItem = doc.getElementById("cMenu_cut");
ok(cutItem, "the Cut menuitem");
pasteItem = doc.getElementById("se-cMenu-paste");
pasteItem = doc.getElementById("cMenu_paste");
ok(pasteItem, "the Paste menuitem");
sp.setText("bug 699130: hello world! (context menu)");

View File

@ -29,7 +29,7 @@ let menu;
function startTest()
{
gScratchpad = gScratchpadWindow.Scratchpad;
menu = gScratchpadWindow.document.getElementById("sp-menu-revert");
menu = gScratchpadWindow.document.getElementById("sp-cmd-revert");
createAndLoadTemporaryFile();
}
@ -38,7 +38,7 @@ function testAfterSaved() {
ok(menu.hasAttribute("disabled"), "The revert menu entry is disabled.");
// chancging the text in the file
gScratchpad.setText("\nfoo();", gLength, gLength);
gScratchpad.setText(gScratchpad.getText() + "\nfoo();");
// Checking the text got changed
is(gScratchpad.getText(), gFileContent + "\nfoo();",
"The text changed the first time.");
@ -60,7 +60,7 @@ function testAfterRevert() {
"The revert menu entry is disabled after reverting.");
// chancging the text in the file again
gScratchpad.setText("\nalert(foo.toSource());", gLength, gLength);
gScratchpad.setText(gScratchpad.getText() + "\nalert(foo.toSource());");
// Saving the file.
gScratchpad.saveFile(testAfterSecondSave);
}
@ -71,7 +71,7 @@ function testAfterSecondSave() {
"The revert menu entry is disabled after saving.");
// changing the text.
gScratchpad.setText("\nfoo();", gLength + 23, gLength + 23);
gScratchpad.setText(gScratchpad.getText() + "\nfoo();");
// revert menu entry should get enabled yet again.
ok(!menu.hasAttribute("disabled"),
@ -91,6 +91,7 @@ function testAfterSecondRevert() {
gFile.remove(false);
gFile = null;
gScratchpad = null;
finish();
}
function createAndLoadTemporaryFile()
@ -126,6 +127,8 @@ function tempFileSaved(aStatus)
function test()
{
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);

View File

@ -44,7 +44,7 @@ function runTests()
ok(!notificationBox.currentNotification,
"there is no notification in content context");
sp.setText("window.foobarBug636725 = 'aloha';");
sp.editor.setText("window.foobarBug636725 = 'aloha';");
ok(!content.wrappedJSObject.foobarBug636725,
"no content.foobarBug636725");
@ -71,7 +71,8 @@ function runTests()
ok(notificationBox.currentNotification,
"there is a notification in browser context");
sp.setText("2'", 31, 32);
let [ from, to ] = sp.editor.getPosition(31, 32);
sp.editor.replaceText("2'", from, to);
is(sp.getText(), "window.foobarBug636725 = 'aloha2';",
"setText() worked");
@ -87,7 +88,7 @@ function runTests()
{
method: "run",
prepare: function() {
sp.setText("gBrowser", 7);
sp.editor.replaceText("gBrowser", sp.editor.getPosition(7));
is(sp.getText(), "window.gBrowser",
"setText() worked with no end for the replace range");
@ -101,7 +102,7 @@ function runTests()
method: "run",
prepare: function() {
// Check that the sandbox is cached.
sp.setText("typeof foobarBug636725cache;");
sp.editor.setText("typeof foobarBug636725cache;");
},
then: function([, , result]) {
is(result, "undefined", "global variable does not exist");
@ -110,7 +111,7 @@ function runTests()
{
method: "run",
prepare: function() {
sp.setText("var foobarBug636725cache = 'foo';" +
sp.editor.setText("var foobarBug636725cache = 'foo';" +
"typeof foobarBug636725cache;");
},
then: function([, , result]) {
@ -121,7 +122,7 @@ function runTests()
{
method: "run",
prepare: function() {
sp.setText("var foobarBug636725cache2 = 'foo';" +
sp.editor.setText("var foobarBug636725cache2 = 'foo';" +
"typeof foobarBug636725cache2;");
},
then: function([, , result]) {
@ -137,7 +138,7 @@ function runTests()
is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT,
"executionContext is content");
sp.setText("typeof foobarBug636725cache2;");
sp.editor.setText("typeof foobarBug636725cache2;");
},
then: function([, , result]) {
is(result, "undefined",
@ -147,7 +148,7 @@ function runTests()
runAsyncCallbackTests(sp, tests).then(() => {
sp.setBrowserContext();
sp.setText("delete foobarBug636725cache;" +
sp.editor.setText("delete foobarBug636725cache;" +
"delete foobarBug636725cache2;");
sp.run().then(finish);
});

View File

@ -23,7 +23,7 @@ function runTests()
method: "run",
prepare: function() {
content.wrappedJSObject.foobarBug636725 = 1;
sp.setText("++window.foobarBug636725");
sp.editor.setText("++window.foobarBug636725");
},
then: function([code, , result]) {
is(code, sp.getText(), "code is correct");
@ -47,34 +47,15 @@ function runTests()
is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/",
"display() shows evaluation result in the textbox");
is(sp.selectedText, "\n/*\n3\n*/", "selectedText is correct");
is(sp.editor.getSelection(), "\n/*\n3\n*/", "getSelection is correct");
}
},
{
method: "run",
prepare: function() {
let selection = sp.getSelectionRange();
is(selection.start, 24, "selection.start is correct");
is(selection.end, 32, "selection.end is correct");
// Test selection run() and display().
sp.setText("window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';");
sp.selectRange(1, 2);
selection = sp.getSelectionRange();
is(selection.start, 1, "selection.start is 1");
is(selection.end, 2, "selection.end is 2");
sp.selectRange(0, 29);
selection = sp.getSelectionRange();
is(selection.start, 0, "selection.start is 0");
is(selection.end, 29, "selection.end is 29");
sp.editor.setText("window.foobarBug636725 = 'a';\n" +
"window.foobarBug636725 = 'b';");
sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 29 });
},
then: function([code, , result]) {
is(code, "window.foobarBug636725 = 'a';", "code is correct");
@ -91,10 +72,9 @@ function runTests()
{
method: "display",
prepare: function() {
sp.setText("window.foobarBug636725 = 'c';\n" +
sp.editor.setText("window.foobarBug636725 = 'c';\n" +
"window.foobarBug636725 = 'b';");
sp.selectRange(0, 22);
sp.editor.setSelection({ line: 0, ch: 0 }, { line: 0, ch: 22 });
},
then: function() {
is(content.wrappedJSObject.foobarBug636725, "a",
@ -106,27 +86,19 @@ function runTests()
"window.foobarBug636725 = 'b';",
"display() shows evaluation result in the textbox");
is(sp.selectedText, "\n/*\na\n*/", "selectedText is correct");
is(sp.editor.getSelection(), "\n/*\na\n*/", "getSelection is correct");
}
}]
}];
runAsyncCallbackTests(sp, tests).then(function() {
let selection = sp.getSelectionRange();
is(selection.start, 22, "selection.start is correct");
is(selection.end, 30, "selection.end is correct");
sp.deselect();
ok(!sp.selectedText, "selectedText is empty");
selection = sp.getSelectionRange();
is(selection.start, selection.end, "deselect() works");
ok(sp.editor.somethingSelected(), "something is selected");
sp.editor.dropSelection();
ok(!sp.editor.somethingSelected(), "something is no longer selected");
ok(!sp.editor.getSelection(), "getSelection is empty");
// Test undo/redo.
sp.setText("foo1");
sp.setText("foo2");
sp.editor.setText("foo1");
sp.editor.setText("foo2");
is(sp.getText(), "foo2", "editor content updated");
sp.undo();
is(sp.getText(), "foo1", "undo() works");

View File

@ -55,7 +55,7 @@ function fileImported(aStatus, aFileContent)
// Save the file after changes.
gFileContent += "// omg, saved!";
gScratchpad.setText(gFileContent);
gScratchpad.editor.setText(gFileContent);
gScratchpad.exportToFile(gFile.QueryInterface(Ci.nsILocalFile), true, true,
fileExported);
@ -70,7 +70,7 @@ function fileExported(aStatus)
// Attempt another file save, with confirmation which returns false.
gFileContent += "// omg, saved twice!";
gScratchpad.setText(gFileContent);
gScratchpad.editor.setText(gFileContent);
let oldConfirm = gScratchpadWindow.confirm;
let askedConfirmation = false;

View File

@ -0,0 +1,23 @@
Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Please note that some subdirectories of the CodeMirror distribution
include their own LICENSE files, and are released under different
licences.

View File

@ -0,0 +1,59 @@
This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
is a JavaScript component that provides a code editor in the browser. When
a mode is available for the language you are coding in, it will color your
code, and optionally help with indentation.
# Upgrade
Currently used version is 3.15. To upgrade, download a new version of
CodeMirror from the project's page [1] and replace all JavaScript and
CSS files inside the codemirror directory [2].
To confirm the functionality run mochitests for the following components:
* sourceeditor
* scratchpad
* debugger
* styleditor
The sourceeditor component contains imported CodeMirror tests [3]. Some
tests were commented out because we don't use that functionality within
Firefox (for example Ruby editing mode). Other than that, we don't have
any Mozilla-specific patches applied to CodeMirror itself.
# Addons
To install a new CodeMirror addon add it to the codemirror directory,
jar.mn [4] file and editor.js [5]. Also, add it to the License section
below.
# License
The following files in this directory are licensed according to the contents
in the LICENSE file:
* codemirror.css
* codemirror.js
* comment.js
* dialog/dialog.css
* dialog/dialog.js
* javascript.js
* matchbrackets.js
* search/match-highlighter.js
* search/search.js
* search/searchcursor.js
* test/codemirror.html
* test/cm_comment_test.js
* test/cm_driver.js
* test/cm_mode_javascript_test.js
* test/cm_mode_test.css
* test/cm_mode_test.js
* test/cm_test.js
# Footnotes
[1] http://codemirror.net
[2] browser/devtools/sourceeditor/codemirror
[3] browser/devtools/sourceeditor/test/browser_codemirror.js
[4] browser/devtools/jar.mn
[5] browser/devtools/sourceeditor/editor.js

View File

@ -0,0 +1,258 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
}
.CodeMirror-scroll {
/* Set scrolling behaviour here */
overflow: auto;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
}
/* CURSOR */
.CodeMirror div.CodeMirror-cursor {
border-left: 1px solid black;
z-index: 3;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor {
width: auto;
border: 0;
background: #7e7;
z-index: 1;
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
.cm-tab { display: inline-block; }
/* DEFAULT THEME */
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable {color: black;}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3 {color: #085;}
.cm-s-default .cm-property {color: black;}
.cm-s-default .cm-operator {color: black;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-error {color: #f00;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-invalidchar {color: #f00;}
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
line-height: 1;
position: relative;
overflow: hidden;
background: white;
color: black;
}
.CodeMirror-scroll {
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px; padding-right: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actuall scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
padding-bottom: 30px;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
padding-bottom: 30px;
margin-bottom: -32px;
display: inline-block;
/* Hack to make IE7 behave */
*zoom:1;
*display:inline;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-lines {
cursor: text;
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-code pre {
border-right: 30px solid transparent;
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.CodeMirror-wrap .CodeMirror-code pre {
border-right: none;
width: auto;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {
}
.CodeMirror-wrap .CodeMirror-scroll {
overflow-x: hidden;
}
.CodeMirror-measure {
position: absolute;
width: 100%; height: 0px;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-measure pre { position: static; }
.CodeMirror div.CodeMirror-cursor {
position: absolute;
visibility: hidden;
border-right: none;
width: 0;
}
.CodeMirror-focused div.CodeMirror-cursor {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* IE7 hack to prevent it from returning funny offsetTops on the spans */
.CodeMirror span { *vertical-align: text-bottom; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursor {
visibility: hidden;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
(function() {
"use strict";
var noOptions = {};
var nonWS = /[^\s\u00a0]/;
var Pos = CodeMirror.Pos;
function firstNonWS(str) {
var found = str.search(nonWS);
return found == -1 ? 0 : found;
}
CodeMirror.commands.toggleComment = function(cm) {
var from = cm.getCursor("start"), to = cm.getCursor("end");
cm.uncomment(from, to) || cm.lineComment(from, to);
};
CodeMirror.defineExtension("lineComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var commentString = options.lineComment || mode.lineComment;
if (!commentString) {
if (options.blockCommentStart || mode.blockCommentStart) {
options.fullLines = true;
self.blockComment(from, to, options);
}
return;
}
var firstLine = self.getLine(from.line);
if (firstLine == null) return;
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
var pad = options.padding == null ? " " : options.padding;
var blankLines = options.commentBlankLines || from.line == to.line;
self.operation(function() {
if (options.indent) {
var baseString = firstLine.slice(0, firstNonWS(firstLine));
for (var i = from.line; i < end; ++i) {
var line = self.getLine(i), cut = baseString.length;
if (!blankLines && !nonWS.test(line)) continue;
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
}
} else {
for (var i = from.line; i < end; ++i) {
if (blankLines || nonWS.test(self.getLine(i)))
self.replaceRange(commentString + pad, Pos(i, 0));
}
}
});
});
CodeMirror.defineExtension("blockComment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) {
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
self.lineComment(from, to, options);
return;
}
var end = Math.min(to.line, self.lastLine());
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
var pad = options.padding == null ? " " : options.padding;
if (from.line > end) return;
self.operation(function() {
if (options.fullLines != false) {
var lastLineHasText = nonWS.test(self.getLine(end));
self.replaceRange(pad + endString, Pos(end));
self.replaceRange(startString + pad, Pos(from.line, 0));
var lead = options.blockCommentLead || mode.blockCommentLead;
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
if (i != end || lastLineHasText)
self.replaceRange(lead + pad, Pos(i, 0));
} else {
self.replaceRange(endString, to);
self.replaceRange(startString, from);
}
});
});
CodeMirror.defineExtension("uncomment", function(from, to, options) {
if (!options) options = noOptions;
var self = this, mode = self.getModeAt(from);
var end = Math.min(to.line, self.lastLine()), start = Math.min(from.line, end);
// Try finding line comments
var lineString = options.lineComment || mode.lineComment, lines = [];
var pad = options.padding == null ? " " : options.padding, didSomething;
lineComment: {
if (!lineString) break lineComment;
for (var i = start; i <= end; ++i) {
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
if (i != start && found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
self.operation(function() {
for (var i = start; i <= end; ++i) {
var line = lines[i - start];
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
if (pos < 0) continue;
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
didSomething = true;
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
}
});
if (didSomething) return true;
}
// Try block comments
var startString = options.blockCommentStart || mode.blockCommentStart;
var endString = options.blockCommentEnd || mode.blockCommentEnd;
if (!startString || !endString) return false;
var lead = options.blockCommentLead || mode.blockCommentLead;
var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
if (close == -1 && start != end) {
endLine = self.getLine(--end);
close = endLine.lastIndexOf(endString);
}
if (open == -1 || close == -1) return false;
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
Pos(end, close + endString.length));
var openEnd = open + startString.length;
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
if (lead) for (var i = start + 1; i <= end; ++i) {
var line = self.getLine(i), found = line.indexOf(lead);
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
var foundEnd = found + lead.length;
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
}
});
return true;
});
})();

View File

@ -0,0 +1,32 @@
.CodeMirror-dialog {
position: absolute;
left: 0; right: 0;
background: white;
z-index: 15;
padding: .1em .8em;
overflow: hidden;
color: #333;
}
.CodeMirror-dialog-top {
border-bottom: 1px solid #eee;
top: 0;
}
.CodeMirror-dialog-bottom {
border-top: 1px solid #eee;
bottom: 0;
}
.CodeMirror-dialog input {
border: none;
outline: none;
background: transparent;
width: 20em;
color: inherit;
font-family: monospace;
}
.CodeMirror-dialog button {
font-size: 70%;
}

View File

@ -0,0 +1,80 @@
// Open simple dialogs on top of an editor. Relies on dialog.css.
(function() {
function dialogDiv(cm, template, bottom) {
var wrap = cm.getWrapperElement();
var dialog;
dialog = wrap.appendChild(document.createElement("div"));
if (bottom) {
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
} else {
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
}
dialog.innerHTML = template;
return dialog;
}
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
var dialog = dialogDiv(this, template, options && options.bottom);
var closed = false, me = this;
function close() {
if (closed) return;
closed = true;
dialog.parentNode.removeChild(dialog);
}
var inp = dialog.getElementsByTagName("input")[0], button;
if (inp) {
CodeMirror.on(inp, "keydown", function(e) {
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
if (e.keyCode == 13 || e.keyCode == 27) {
CodeMirror.e_stop(e);
close();
me.focus();
if (e.keyCode == 13) callback(inp.value);
}
});
if (options && options.onKeyUp) {
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
}
if (options && options.value) inp.value = options.value;
inp.focus();
CodeMirror.on(inp, "blur", close);
} else if (button = dialog.getElementsByTagName("button")[0]) {
CodeMirror.on(button, "click", function() {
close();
me.focus();
});
button.focus();
CodeMirror.on(button, "blur", close);
}
return close;
});
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
var dialog = dialogDiv(this, template, options && options.bottom);
var buttons = dialog.getElementsByTagName("button");
var closed = false, me = this, blurring = 1;
function close() {
if (closed) return;
closed = true;
dialog.parentNode.removeChild(dialog);
me.focus();
}
buttons[0].focus();
for (var i = 0; i < buttons.length; ++i) {
var b = buttons[i];
(function(callback) {
CodeMirror.on(b, "click", function(e) {
CodeMirror.e_preventDefault(e);
close();
if (callback) callback(me);
});
})(callbacks[i]);
CodeMirror.on(b, "blur", function() {
--blurring;
setTimeout(function() { if (blurring <= 0) close(); }, 200);
});
CodeMirror.on(b, "focus", function() { ++blurring; });
}
});
})();

View File

@ -0,0 +1,479 @@
// TODO actually recognize syntax of TypeScript constructs
CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var statementIndent = parserConfig.statementIndent;
var jsonMode = parserConfig.json;
var isTS = parserConfig.typescript;
// Tokenizer
var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
var jsKeywords = {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C,
"var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
"this": kw("this")
};
// Extend the 'normal' keywords with the TypeScript language extensions
if (isTS) {
var type = {type: "variable", style: "variable-3"};
var tsKeywords = {
// object-like things
"interface": kw("interface"),
"class": kw("class"),
"extends": kw("extends"),
"constructor": kw("constructor"),
// scope modifiers
"public": kw("public"),
"private": kw("private"),
"protected": kw("protected"),
"static": kw("static"),
"super": kw("super"),
// types
"string": type, "number": type, "bool": type, "any": type
};
for (var attr in tsKeywords) {
jsKeywords[attr] = tsKeywords[attr];
}
}
return jsKeywords;
}();
var isOperatorChar = /[+\-*&%=<>!?|~^]/;
function chain(stream, state, f) {
state.tokenize = f;
return f(stream, state);
}
function nextUntilUnescaped(stream, end) {
var escaped = false, next;
while ((next = stream.next()) != null) {
if (next == end && !escaped)
return false;
escaped = !escaped && next == "\\";
}
return escaped;
}
// Used as scratch variables to communicate multiple values without
// consing up tons of objects.
var type, content;
function ret(tp, style, cont) {
type = tp; content = cont;
return style;
}
function jsTokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'")
return chain(stream, state, jsTokenString(ch));
else if (/[\[\]{}\(\),;\:\.]/.test(ch))
return ret(ch);
else if (ch == "0" && stream.eat(/x/i)) {
stream.eatWhile(/[\da-f]/i);
return ret("number", "number");
}
else if (/\d/.test(ch) || ch == "-" && stream.eat(/\d/)) {
stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
return ret("number", "number");
}
else if (ch == "/") {
if (stream.eat("*")) {
return chain(stream, state, jsTokenComment);
}
else if (stream.eat("/")) {
stream.skipToEnd();
return ret("comment", "comment");
}
else if (state.lastType == "operator" || state.lastType == "keyword c" ||
/^[\[{}\(,;:]$/.test(state.lastType)) {
nextUntilUnescaped(stream, "/");
stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla
return ret("regexp", "string-2");
}
else {
stream.eatWhile(isOperatorChar);
return ret("operator", null, stream.current());
}
}
else if (ch == "#") {
stream.skipToEnd();
return ret("error", "error");
}
else if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
return ret("operator", null, stream.current());
}
else {
stream.eatWhile(/[\w\$_]/);
var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
ret("variable", "variable", word);
}
}
function jsTokenString(quote) {
return function(stream, state) {
if (!nextUntilUnescaped(stream, quote))
state.tokenize = jsTokenBase;
return ret("string", "string");
};
}
function jsTokenComment(stream, state) {
var maybeEnd = false, ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = jsTokenBase;
break;
}
maybeEnd = (ch == "*");
}
return ret("comment", "comment");
}
// Parser
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true};
function JSLexical(indented, column, type, align, prev, info) {
this.indented = indented;
this.column = column;
this.type = type;
this.prev = prev;
this.info = info;
if (align != null) this.align = align;
}
function inScope(state, varname) {
for (var v = state.localVars; v; v = v.next)
if (v.name == varname) return true;
}
function parseJS(state, style, type, content, stream) {
var cc = state.cc;
// Communicate our context to the combinators.
// (Less wasteful than consing up a hundred closures on every call.)
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc;
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = true;
while(true) {
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
if (combinator(type, content)) {
while(cc.length && cc[cc.length - 1].lex)
cc.pop()();
if (cx.marked) return cx.marked;
if (type == "variable" && inScope(state, content)) return "variable-2";
return style;
}
}
}
// Combinator utils
var cx = {state: null, column: null, marked: null, cc: null};
function pass() {
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
}
function cont() {
pass.apply(null, arguments);
return true;
}
function register(varname) {
function inList(list) {
for (var v = list; v; v = v.next)
if (v.name == varname) return true;
return false;
}
var state = cx.state;
if (state.context) {
cx.marked = "def";
if (inList(state.localVars)) return;
state.localVars = {name: varname, next: state.localVars};
} else {
if (inList(state.globalVars)) return;
state.globalVars = {name: varname, next: state.globalVars};
}
}
// Combinators
var defaultVars = {name: "this", next: {name: "arguments"}};
function pushcontext() {
cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
cx.state.localVars = defaultVars;
}
function popcontext() {
cx.state.localVars = cx.state.context.vars;
cx.state.context = cx.state.context.prev;
}
function pushlex(type, info) {
var result = function() {
var state = cx.state, indent = state.indented;
if (state.lexical.type == "stat") indent = state.lexical.indented;
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
};
result.lex = true;
return result;
}
function poplex() {
var state = cx.state;
if (state.lexical.prev) {
if (state.lexical.type == ")")
state.indented = state.lexical.indented;
state.lexical = state.lexical.prev;
}
}
poplex.lex = true;
function expect(wanted) {
return function(type) {
if (type == wanted) return cont();
else if (wanted == ";") return pass();
else return cont(arguments.callee);
};
}
function statement(type) {
if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
if (type == "{") return cont(pushlex("}"), block, poplex);
if (type == ";") return cont();
if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse);
if (type == "function") return cont(functiondef);
if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"),
poplex, statement, poplex);
if (type == "variable") return cont(pushlex("stat"), maybelabel);
if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
block, poplex, poplex);
if (type == "case") return cont(expression, expect(":"));
if (type == "default") return cont(expect(":"));
if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
statement, poplex, popcontext);
return pass(pushlex("stat"), expression, expect(";"), poplex);
}
function expression(type) {
return expressionInner(type, false);
}
function expressionNoComma(type) {
return expressionInner(type, true);
}
function expressionInner(type, noComma) {
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
if (type == "function") return cont(functiondef);
if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
if (type == "operator") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), commasep(expressionNoComma, "]"), poplex, maybeop);
if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeop);
return cont();
}
function maybeexpression(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expression);
}
function maybeexpressionNoComma(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expressionNoComma);
}
function maybeoperatorComma(type, value) {
if (type == ",") return cont(expression);
return maybeoperatorNoComma(type, value, false);
}
function maybeoperatorNoComma(type, value, noComma) {
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
var expr = noComma == false ? expression : expressionNoComma;
if (type == "operator") {
if (/\+\+|--/.test(value)) return cont(me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
if (type == ";") return;
if (type == "(") return cont(pushlex(")", "call"), commasep(expressionNoComma, ")"), poplex, me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
}
function maybelabel(type) {
if (type == ":") return cont(poplex, statement);
return pass(maybeoperatorComma, expect(";"), poplex);
}
function property(type) {
if (type == "variable") {cx.marked = "property"; return cont();}
}
function objprop(type, value) {
if (type == "variable") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
} else if (type == "number" || type == "string") {
cx.marked = type + " property";
}
if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expressionNoComma);
}
function getterSetter(type) {
if (type == ":") return cont(expression);
if (type != "variable") return cont(expect(":"), expression);
cx.marked = "property";
return cont(functiondef);
}
function commasep(what, end) {
function proceed(type) {
if (type == ",") {
var lex = cx.state.lexical;
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
return cont(what, proceed);
}
if (type == end) return cont();
return cont(expect(end));
}
return function(type) {
if (type == end) return cont();
else return pass(what, proceed);
};
}
function block(type) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type) {
if (type == ":") return cont(typedef);
return pass();
}
function typedef(type) {
if (type == "variable"){cx.marked = "variable-3"; return cont();}
return pass();
}
function vardef1(type, value) {
if (type == "variable") {
register(value);
return isTS ? cont(maybetype, vardef2) : cont(vardef2);
}
return pass();
}
function vardef2(type, value) {
if (value == "=") return cont(expressionNoComma, vardef2);
if (type == ",") return cont(vardef1);
}
function maybeelse(type, value) {
if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef1, expect(";"), forspec2);
if (type == ";") return cont(forspec2);
if (type == "variable") return cont(formaybein);
return pass(expression, expect(";"), forspec2);
}
function formaybein(_type, value) {
if (value == "in") return cont(expression);
return cont(maybeoperatorComma, forspec2);
}
function forspec2(type, value) {
if (type == ";") return cont(forspec3);
if (value == "in") return cont(expression);
return pass(expression, expect(";"), forspec3);
}
function forspec3(type) {
if (type != ")") cont(expression);
}
function functiondef(type, value) {
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext);
}
function funarg(type, value) {
if (type == "variable") {register(value); return isTS ? cont(maybetype) : cont();}
}
// Interface
return {
startState: function(basecolumn) {
return {
tokenize: jsTokenBase,
lastType: null,
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
globalVars: parserConfig.globalVars,
context: parserConfig.localVars && {vars: parserConfig.localVars},
indented: 0
};
},
token: function(stream, state) {
if (stream.sol()) {
if (!state.lexical.hasOwnProperty("align"))
state.lexical.align = false;
state.indented = stream.indentation();
}
if (state.tokenize != jsTokenComment && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (type == "comment") return style;
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
return parseJS(state, style, type, content, stream);
},
indent: function(state, textAfter) {
if (state.tokenize == jsTokenComment) return CodeMirror.Pass;
if (state.tokenize != jsTokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
// Kludge to prevent 'maybelse' from blocking lexical scope pops
for (var i = state.cc.length - 1; i >= 0; --i) {
var c = state.cc[i];
if (c == poplex) lexical = lexical.prev;
else if (c != maybeelse || /^else\b/.test(textAfter)) break;
}
if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? 4 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? statementIndent || indentUnit : 0);
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
else return lexical.indented + (closing ? 0 : indentUnit);
},
electricChars: ":{}",
blockCommentStart: jsonMode ? null : "/*",
blockCommentEnd: jsonMode ? null : "*/",
lineComment: jsonMode ? null : "//",
fold: "brace",
helperType: jsonMode ? "json" : "javascript",
jsonMode: jsonMode
};
});
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });

View File

@ -0,0 +1,86 @@
(function() {
var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
(document.documentMode == null || document.documentMode < 8);
var Pos = CodeMirror.Pos;
var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
function findMatchingBracket(cm, where, strict) {
var state = cm.state.matchBrackets;
var maxScanLen = (state && state.maxScanLineLength) || 10000;
var cur = where || cm.getCursor(), line = cm.getLineHandle(cur.line), pos = cur.ch - 1;
var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
if (!match) return null;
var forward = match.charAt(1) == ">", d = forward ? 1 : -1;
if (strict && forward != (pos == cur.ch)) return null;
var style = cm.getTokenTypeAt(Pos(cur.line, pos + 1));
var stack = [line.text.charAt(pos)], re = /[(){}[\]]/;
function scan(line, lineNo, start) {
if (!line.text) return;
var pos = forward ? 0 : line.text.length - 1, end = forward ? line.text.length : -1;
if (line.text.length > maxScanLen) return null;
if (start != null) pos = start + d;
for (; pos != end; pos += d) {
var ch = line.text.charAt(pos);
if (re.test(ch) && cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style) {
var match = matching[ch];
if (match.charAt(1) == ">" == forward) stack.push(ch);
else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false};
else if (!stack.length) return {pos: pos, match: true};
}
}
}
for (var i = cur.line, found, e = forward ? Math.min(i + 100, cm.lineCount()) : Math.max(-1, i - 100); i != e; i+=d) {
if (i == cur.line) found = scan(line, i, pos);
else found = scan(cm.getLineHandle(i), i);
if (found) break;
}
return {from: Pos(cur.line, pos), to: found && Pos(i, found.pos),
match: found && found.match, forward: forward};
}
function matchBrackets(cm, autoclear) {
// Disable brace matching in long lines, since it'll cause hugely slow updates
var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
var found = findMatchingBracket(cm);
if (!found || cm.getLine(found.from.line).length > maxHighlightLen ||
found.to && cm.getLine(found.to.line).length > maxHighlightLen)
return;
var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
var one = cm.markText(found.from, Pos(found.from.line, found.from.ch + 1), {className: style});
var two = found.to && cm.markText(found.to, Pos(found.to.line, found.to.ch + 1), {className: style});
// Kludge to work around the IE bug from issue #1193, where text
// input stops going to the textare whever this fires.
if (ie_lt8 && cm.state.focused) cm.display.input.focus();
var clear = function() {
cm.operation(function() { one.clear(); two && two.clear(); });
};
if (autoclear) setTimeout(clear, 800);
else return clear;
}
var currentlyHighlighted = null;
function doMatchBrackets(cm) {
cm.operation(function() {
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
if (!cm.somethingSelected()) currentlyHighlighted = matchBrackets(cm, false);
});
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init)
cm.off("cursorActivity", doMatchBrackets);
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};
cm.on("cursorActivity", doMatchBrackets);
}
});
CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
CodeMirror.defineExtension("findMatchingBracket", function(pos, strict){
return findMatchingBracket(this, pos, strict);
});
})();

View File

@ -0,0 +1,88 @@
// Highlighting text that matches the selection
//
// Defines an option highlightSelectionMatches, which, when enabled,
// will style strings that match the selection throughout the
// document.
//
// The option can be set to true to simply enable it, or to a
// {minChars, style, showToken} object to explicitly configure it.
// minChars is the minimum amount of characters that should be
// selected for the behavior to occur, and style is the token style to
// apply to the matches. This will be prefixed by "cm-" to create an
// actual CSS class name. showToken, when enabled, will cause the
// current token to be highlighted when nothing is selected.
(function() {
var DEFAULT_MIN_CHARS = 2;
var DEFAULT_TOKEN_STYLE = "matchhighlight";
function State(options) {
if (typeof options == "object") {
this.minChars = options.minChars;
this.style = options.style;
this.showToken = options.showToken;
}
if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
this.overlay = this.timeout = null;
}
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
var over = cm.state.matchHighlighter.overlay;
if (over) cm.removeOverlay(over);
clearTimeout(cm.state.matchHighlighter.timeout);
cm.state.matchHighlighter = null;
cm.off("cursorActivity", cursorActivity);
}
if (val) {
cm.state.matchHighlighter = new State(val);
highlightMatches(cm);
cm.on("cursorActivity", cursorActivity);
}
});
function cursorActivity(cm) {
var state = cm.state.matchHighlighter;
clearTimeout(state.timeout);
state.timeout = setTimeout(function() {highlightMatches(cm);}, 100);
}
function highlightMatches(cm) {
cm.operation(function() {
var state = cm.state.matchHighlighter;
if (state.overlay) {
cm.removeOverlay(state.overlay);
state.overlay = null;
}
if (!cm.somethingSelected() && state.showToken) {
var re = state.showToken === true ? /[\w$]/ : state.showToken;
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end;
if (start < end)
cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style));
return;
}
if (cm.getCursor("head").line != cm.getCursor("anchor").line) return;
var selection = cm.getSelection().replace(/^\s+|\s+$/g, "");
if (selection.length >= state.minChars)
cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style));
});
}
function boundariesAround(stream, re) {
return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
}
function makeOverlay(query, hasBoundary, style) {
return {token: function(stream) {
if (stream.match(query) &&
(!hasBoundary || boundariesAround(stream, hasBoundary)))
return style;
stream.next();
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
}};
}
})();

View File

@ -0,0 +1,131 @@
// Define search commands. Depends on dialog.js or another
// implementation of the openDialog method.
// Replace works a little oddly -- it will do the replace on the next
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
// replace by making sure the match is no longer selected when hitting
// Ctrl-G.
(function() {
function searchOverlay(query) {
if (typeof query == "string") return {token: function(stream) {
if (stream.match(query)) return "searching";
stream.next();
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
}};
return {token: function(stream) {
if (stream.match(query)) return "searching";
while (!stream.eol()) {
stream.next();
if (stream.match(query, false)) break;
}
}};
}
function SearchState() {
this.posFrom = this.posTo = this.query = null;
this.overlay = null;
}
function getSearchState(cm) {
return cm.state.search || (cm.state.search = new SearchState());
}
function getSearchCursor(cm, query, pos) {
// Heuristic: if the query string is all lowercase, do a case insensitive search.
return cm.getSearchCursor(query, pos, typeof query == "string" && query == query.toLowerCase());
}
function dialog(cm, text, shortText, f) {
if (cm.openDialog) cm.openDialog(text, f);
else f(prompt(shortText, ""));
}
function confirmDialog(cm, text, shortText, fs) {
if (cm.openConfirm) cm.openConfirm(text, fs);
else if (confirm(shortText)) fs[0]();
}
function parseQuery(query) {
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query;
}
var queryDialog =
'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
function doSearch(cm, rev) {
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);
dialog(cm, queryDialog, "Search for:", function(query) {
cm.operation(function() {
if (!query || state.query) return;
state.query = parseQuery(query);
cm.removeOverlay(state.overlay);
state.overlay = searchOverlay(state.query);
cm.addOverlay(state.overlay);
state.posFrom = state.posTo = cm.getCursor();
findNext(cm, rev);
});
});
}
function findNext(cm, rev) {cm.operation(function() {
var state = getSearchState(cm);
var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
if (!cursor.find(rev)) {
cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
if (!cursor.find(rev)) return;
}
cm.setSelection(cursor.from(), cursor.to());
state.posFrom = cursor.from(); state.posTo = cursor.to();
});}
function clearSearch(cm) {cm.operation(function() {
var state = getSearchState(cm);
if (!state.query) return;
state.query = null;
cm.removeOverlay(state.overlay);
});}
var replaceQueryDialog =
'Replace: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
var replacementQueryDialog = 'With: <input type="text" style="width: 10em"/>';
var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>";
function replace(cm, all) {
dialog(cm, replaceQueryDialog, "Replace:", function(query) {
if (!query) return;
query = parseQuery(query);
dialog(cm, replacementQueryDialog, "Replace with:", function(text) {
if (all) {
cm.operation(function() {
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
if (typeof query != "string") {
var match = cm.getRange(cursor.from(), cursor.to()).match(query);
cursor.replace(text.replace(/\$(\d)/, function(_, i) {return match[i];}));
} else cursor.replace(text);
}
});
} else {
clearSearch(cm);
var cursor = getSearchCursor(cm, query, cm.getCursor());
var advance = function() {
var start = cursor.from(), match;
if (!(match = cursor.findNext())) {
cursor = getSearchCursor(cm, query);
if (!(match = cursor.findNext()) ||
(start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
}
cm.setSelection(cursor.from(), cursor.to());
confirmDialog(cm, doReplaceConfirm, "Replace?",
[function() {doReplace(match);}, advance]);
};
var doReplace = function(match) {
cursor.replace(typeof query == "string" ? text :
text.replace(/\$(\d)/, function(_, i) {return match[i];}));
advance();
};
advance();
}
});
});
}
CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
CodeMirror.commands.findNext = doSearch;
CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
CodeMirror.commands.clearSearch = clearSearch;
CodeMirror.commands.replace = replace;
CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
})();

View File

@ -0,0 +1,143 @@
(function(){
var Pos = CodeMirror.Pos;
function SearchCursor(doc, query, pos, caseFold) {
this.atOccurrence = false; this.doc = doc;
if (caseFold == null && typeof query == "string") caseFold = false;
pos = pos ? doc.clipPos(pos) : Pos(0, 0);
this.pos = {from: pos, to: pos};
// The matches method is filled in based on the type of query.
// It takes a position and a direction, and returns an object
// describing the next occurrence of the query, or null if no
// more matches were found.
if (typeof query != "string") { // Regexp match
if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
this.matches = function(reverse, pos) {
if (reverse) {
query.lastIndex = 0;
var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
for (;;) {
query.lastIndex = cutOff;
var newMatch = query.exec(line);
if (!newMatch) break;
match = newMatch;
start = match.index;
cutOff = match.index + (match[0].length || 1);
if (cutOff == line.length) break;
}
var matchLen = (match && match[0].length) || 0;
if (!matchLen) {
if (start == 0 && line.length == 0) {match = undefined;}
else if (start != doc.getLine(pos.line).length) {
matchLen++;
}
}
} else {
query.lastIndex = pos.ch;
var line = doc.getLine(pos.line), match = query.exec(line);
var matchLen = (match && match[0].length) || 0;
var start = match && match.index;
if (start + matchLen != line.length && !matchLen) matchLen = 1;
}
if (match && matchLen)
return {from: Pos(pos.line, start),
to: Pos(pos.line, start + matchLen),
match: match};
};
} else { // String query
if (caseFold) query = query.toLowerCase();
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
var target = query.split("\n");
// Different methods for single-line and multi-line queries
if (target.length == 1) {
if (!query.length) {
// Empty string would match anything and never progress, so
// we define it to match nothing instead.
this.matches = function() {};
} else {
this.matches = function(reverse, pos) {
var line = fold(doc.getLine(pos.line)), len = query.length, match;
if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1)
: (match = line.indexOf(query, pos.ch)) != -1)
return {from: Pos(pos.line, match),
to: Pos(pos.line, match + len)};
};
}
} else {
this.matches = function(reverse, pos) {
var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(doc.getLine(ln));
var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
if (reverse ? offsetA >= pos.ch || offsetA != match.length
: offsetA <= pos.ch || offsetA != line.length - match.length)
return;
for (;;) {
if (reverse ? !ln : ln == doc.lineCount() - 1) return;
line = fold(doc.getLine(ln += reverse ? -1 : 1));
match = target[reverse ? --idx : ++idx];
if (idx > 0 && idx < target.length - 1) {
if (line != match) return;
else continue;
}
var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
return;
var start = Pos(pos.line, offsetA), end = Pos(ln, offsetB);
return {from: reverse ? end : start, to: reverse ? start : end};
}
};
}
}
}
SearchCursor.prototype = {
findNext: function() {return this.find(false);},
findPrevious: function() {return this.find(true);},
find: function(reverse) {
var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
function savePosAndFail(line) {
var pos = Pos(line, 0);
self.pos = {from: pos, to: pos};
self.atOccurrence = false;
return false;
}
for (;;) {
if (this.pos = this.matches(reverse, pos)) {
if (!this.pos.from || !this.pos.to) { console.log(this.matches, this.pos); }
this.atOccurrence = true;
return this.pos.match || true;
}
if (reverse) {
if (!pos.line) return savePosAndFail(0);
pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
}
else {
var maxLine = this.doc.lineCount();
if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
pos = Pos(pos.line + 1, 0);
}
}
},
from: function() {if (this.atOccurrence) return this.pos.from;},
to: function() {if (this.atOccurrence) return this.pos.to;},
replace: function(newText) {
if (!this.atOccurrence) return;
var lines = CodeMirror.splitLines(newText);
this.doc.replaceRange(lines, this.pos.from, this.pos.to);
this.pos.to = Pos(this.pos.from.line + lines.length - 1,
lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
}
};
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold);
});
CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this, query, pos, caseFold);
});
})();

View File

@ -0,0 +1,440 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Cc, Ci, components } = require("chrome");
const TAB_SIZE = "devtools.editor.tabsize";
const EXPAND_TAB = "devtools.editor.expandtab";
const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const promise = require("sdk/core/promise");
const events = require("devtools/shared/event-emitter");
Cu.import("resource://gre/modules/Services.jsm");
const L10N = Services.strings.createBundle(L10N_BUNDLE);
// CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
// JavaScript and CSS that is injected into an iframe in
// order to initialize a CodeMirror instance.
const CM_STYLES = [
"chrome://browser/content/devtools/codemirror/codemirror.css",
"chrome://browser/content/devtools/codemirror/dialog.css"
];
const CM_SCRIPTS = [
"chrome://browser/content/devtools/codemirror/codemirror.js",
"chrome://browser/content/devtools/codemirror/dialog.js",
"chrome://browser/content/devtools/codemirror/searchcursor.js",
"chrome://browser/content/devtools/codemirror/search.js",
"chrome://browser/content/devtools/codemirror/matchbrackets.js",
"chrome://browser/content/devtools/codemirror/comment.js"
];
const CM_IFRAME =
"data:text/html;charset=utf8,<!DOCTYPE html>" +
"<html dir='ltr'>" +
" <head>" +
" <style>" +
" html, body { height: 100%; }" +
" body { margin: 0; overflow: hidden; }" +
" .CodeMirror { width: 100%; height: 100% !important; }" +
" </style>" +
[ " <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
" </head>" +
" <body></body>" +
"</html>";
const CM_MAPPING = [
"focus",
"hasFocus",
"setCursor",
"getCursor",
"somethingSelected",
"setSelection",
"getSelection",
"replaceSelection",
"undo",
"redo",
"clearHistory",
"posFromIndex",
"openDialog"
];
const CM_JUMP_DIALOG = [
L10N.GetStringFromName("gotoLineCmd.promptTitle")
+ " <input type=text style='width: 10em'/>"
];
const editors = new WeakMap();
Editor.modes = {
text: { name: "text" },
js: { name: "javascript", url: "chrome://browser/content/devtools/codemirror/javascript.js" }
};
function ctrl(k) {
return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
}
/**
* A very thin wrapper around CodeMirror. Provides a number
* of helper methods to make our use of CodeMirror easier and
* another method, appendTo, to actually create and append
* the CodeMirror instance.
*
* Note that Editor doesn't expose CodeMirror instance to the
* outside world.
*
* Constructor accepts one argument, config. It is very
* similar to the CodeMirror configuration object so for most
* properties go to CodeMirror's documentation (see below).
*
* Other than that, it accepts one additional and optional
* property contextMenu. This property should be an ID of
* an element we can use as a context menu.
*
* This object is also an event emitter.
*
* CodeMirror docs: http://codemirror.net/doc/manual.html
*/
function Editor(config) {
const tabSize = Services.prefs.getIntPref(TAB_SIZE);
const useTabs = !Services.prefs.getBoolPref(EXPAND_TAB);
this.version = null;
this.config = {
value: "",
mode: Editor.modes.text,
indentUnit: tabSize,
tabSize: tabSize,
contextMenu: null,
matchBrackets: true,
extraKeys: {},
indentWithTabs: useTabs,
};
// Overwrite default config with user-provided, if needed.
Object.keys(config).forEach((k) => this.config[k] = config[k]);
// Additional shortcuts.
this.config.extraKeys[ctrl("J")] = (cm) => this.jumpToLine();
this.config.extraKeys[ctrl("/")] = "toggleComment";
// Disable ctrl-[ and ctrl-] because toolbox uses those
// shortcuts.
this.config.extraKeys[ctrl("[")] = false;
this.config.extraKeys[ctrl("]")] = false;
// Overwrite default tab behavior. If something is selected,
// indent those lines. If nothing is selected and we're
// indenting with tabs, insert one tab. Otherwise insert N
// whitespaces where N == indentUnit option.
this.config.extraKeys.Tab = (cm) => {
if (cm.somethingSelected())
return void cm.indentSelection("add");
if (this.config.indentWithTabs)
return void cm.replaceSelection("\t", "end", "+input");
var num = cm.getOption("indentUnit");
if (cm.getCursor().ch !== 0) num -= 1;
cm.replaceSelection(" ".repeat(num), "end", "+input");
};
events.decorate(this);
}
Editor.prototype = {
version: null,
config: null,
/**
* Appends the current Editor instance to the element specified by
* the only argument 'el'. This method actually creates and loads
* CodeMirror and all its dependencies.
*
* This method is asynchronous and returns a promise.
*/
appendTo: function (el) {
let def = promise.defer();
let cm = editors.get(this);
let doc = el.ownerDocument;
let env = doc.createElementNS(XUL_NS, "iframe");
env.flex = 1;
if (cm)
throw new Error("You can append an editor only once.");
let onLoad = () => {
// Once the iframe is loaded, we can inject CodeMirror
// and its dependencies into its DOM.
env.removeEventListener("load", onLoad, true);
let win = env.contentWindow.wrappedJSObject;
CM_SCRIPTS.forEach((url) =>
Services.scriptloader.loadSubScript(url, win, "utf8"));
// Plain text mode doesn't need any additional files,
// all other modes (js, html, etc.) do.
if (this.config.mode.name !== "text")
Services.scriptloader.loadSubScript(this.config.mode.url, win, "utf8");
// Create a CodeMirror instance add support for context menus and
// overwrite the default controller (otherwise items in the top and
// context menus won't work).
cm = win.CodeMirror(win.document.body, this.config);
cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
ev.preventDefault();
this.showContextMenu(doc, ev.screenX, ev.screenY);
}, false);
cm.on("change", () => this.emit("change"));
doc.defaultView.controllers.insertControllerAt(0, controller(this, doc.defaultView));
editors.set(this, cm);
def.resolve();
};
env.addEventListener("load", onLoad, true);
env.setAttribute("src", CM_IFRAME);
el.appendChild(env);
this.once("destroy", () => el.removeChild(env));
return def.promise;
},
/**
* Returns true if there's something to undo and false otherwise.
*/
canUndo: function () {
let cm = editors.get(this);
return cm.historySize().undo > 0;
},
/**
* Returns true if there's something to redo and false otherwise.
*/
canRedo: function () {
let cm = editors.get(this);
return cm.historySize().redo > 0;
},
/**
* Calculates and returns one or more {line, ch} objects for
* a zero-based index who's value is relative to the start of
* the editor's text.
*
* If only one argument is given, this method returns a single
* {line,ch} object. Otherwise it returns an array.
*/
getPosition: function (...args) {
let cm = editors.get(this);
let res = args.map((ind) => cm.posFromIndex(ind));
return args.length === 1 ? res[0] : res;
},
/**
* The reverse of getPosition. Similarly to getPosition this
* method returns a single value if only one argument was given
* and an array otherwise.
*/
getOffset: function (...args) {
let cm = editors.get(this);
let res = args.map((pos) => cm.indexFromPos(pos));
return args.length > 1 ? res : res[0];
},
/**
* Returns text from the text area.
*/
getText: function () {
let cm = editors.get(this);
return cm.getValue();
},
/**
* Replaces whatever is in the text area with the contents of
* the 'value' argument.
*/
setText: function (value) {
let cm = editors.get(this);
cm.setValue(value);
},
/**
* Replaces contents of a text area within the from/to {line, ch}
* range. If neither from nor to arguments are provided works
* exactly like setText. If only from object is provided, inserts
* text at that point.
*/
replaceText: function (value, from, to) {
let cm = editors.get(this);
if (!from)
return void this.setText(value);
if (!to) {
let text = cm.getRange({ line: 0, ch: 0 }, from);
return void this.setText(text + value);
}
cm.replaceRange(value, from, to);
},
/**
* Deselects contents of the text area.
*/
dropSelection: function () {
if (!this.somethingSelected())
return;
this.setCursor(this.getCursor());
},
/**
* Marks the contents as clean and returns the current
* version number.
*/
markClean: function () {
let cm = editors.get(this);
this.version = cm.changeGeneration();
return this.version;
},
/**
* Returns true if contents of the text area are
* clean i.e. no changes were made since the last version.
*/
isClean: function () {
let cm = editors.get(this);
return cm.isClean(this.version);
},
/**
* Displays a context menu at the point x:y. The first
* argument, container, should be a DOM node that contains
* a context menu element specified by the ID from
* config.contextMenu.
*/
showContextMenu: function (container, x, y) {
if (this.config.contextMenu == null)
return;
let popup = container.getElementById(this.config.contextMenu);
popup.openPopupAtScreen(x, y, true);
},
/**
* This method opens an in-editor dialog asking for a line to
* jump to. Once given, it changes cursor to that line.
*/
jumpToLine: function () {
this.openDialog(CM_JUMP_DIALOG, (line) =>
this.setCursor({ line: line - 1, ch: 0 }));
},
destroy: function () {
this.config = null;
this.version = null;
this.emit("destroy");
}
};
// Since Editor is a thin layer over CodeMirror some methods
// are mapped directly—without any changes.
CM_MAPPING.forEach(function (name) {
Editor.prototype[name] = function (...args) {
let cm = editors.get(this);
return cm[name].apply(cm, args);
};
});
/**
* Returns a controller object that can be used for
* editor-specific commands such as find, jump to line,
* copy/paste, etc.
*/
function controller(ed, view) {
return {
supportsCommand: function (cmd) {
let cm = editors.get(ed);
switch (cmd) {
case "cmd_find":
case "cmd_findAgain":
case "cmd_findPrevious":
case "cmd_gotoLine":
case "cmd_undo":
case "cmd_redo":
case "cmd_cut":
case "cmd_paste":
case "cmd_delete":
case "cmd_selectAll":
return true;
}
return false;
},
isCommandEnabled: function (cmd) {
let cm = editors.get(ed);
switch (cmd) {
case "cmd_find":
case "cmd_gotoLine":
case "cmd_selectAll":
return true;
case "cmd_findAgain":
return cm.state.search != null && cm.state.search.query != null;
case "cmd_undo":
return ed.canUndo();
case "cmd_redo":
return ed.canRedo();
case "cmd_cut":
return cm.getOption("readOnly") !== true && ed.somethingSelected();
case "cmd_delete":
return ed.somethingSelected();
case "cmd_paste":
return cm.getOption("readOnly") !== true;
}
return false;
},
doCommand: function (cmd) {
let cm = editors.get(ed);
let map = {
"cmd_selectAll": "selectAll",
"cmd_find": "find",
"cmd_undo": "undo",
"cmd_redo": "redo",
"cmd_delete": "delCharAfter",
"cmd_findAgain": "findNext"
};
if (map[cmd])
return void cm.execCommand(map[cmd]);
if (cmd === "cmd_cut")
return void view.goDoCommand("cmd_cut");
if (cmd === "cmdste")
return void view.goDoCommand("cmd_paste");
if (cmd == "cmd_gotoLine")
ed.jumpToLine(cm);
},
onEvent: function () {}
};
}
module.exports = Editor;

View File

@ -6,7 +6,10 @@
TEST_DIRS += ['test']
JS_MODULES_PATH = 'modules/devtools/sourceeditor'
EXTRA_JS_MODULES += [
'editor.js',
'source-editor-orion.jsm',
'source-editor-ui.jsm',
'source-editor.jsm',

View File

@ -10,7 +10,7 @@ const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/source-editor-ui.jsm");
Cu.import("resource:///modules/devtools/sourceeditor/source-editor-ui.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",

View File

@ -9,7 +9,7 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/source-editor-ui.jsm");
Cu.import("resource:///modules/devtools/sourceeditor/source-editor-ui.jsm");
const PREF_EDITOR_COMPONENT = "devtools.editor.component";
const SOURCEEDITOR_L10N = "chrome://browser/locale/devtools/sourceeditor.properties";
@ -20,7 +20,7 @@ try {
if (component == "ui") {
throw new Error("The ui editor component is not available.");
}
Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor-" + component + ".jsm", obj);
} catch (ex) {
Cu.reportError(ex);
Cu.reportError("SourceEditor component failed to load: " + component);
@ -30,7 +30,7 @@ try {
// Load the default editor component.
component = Services.prefs.getCharPref(PREF_EDITOR_COMPONENT);
Cu.import("resource:///modules/source-editor-" + component + ".jsm", obj);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor-" + component + ".jsm", obj);
}
// Export the SourceEditor.

View File

@ -24,4 +24,13 @@ MOCHITEST_BROWSER_FILES = \
browser_bug729960_block_bracket_jump.js \
browser_bug744021_next_prev_bracket_jump.js \
browser_bug725392_mouse_coords_char_offset.js \
browser_codemirror.js \
head.js \
codemirror.html \
cm_comment_test.js \
cm_driver.js \
cm_mode_test.css \
cm_mode_test.js \
cm_test.js \
cm_mode_javascript_test.js \
$(NULL)

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
let component = Services.prefs.getCharPref(SourceEditor.PREFS.COMPONENT);

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
waitForExplicitFinish();

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let editor;

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
waitForExplicitFinish();

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
waitForExplicitFinish();

View File

@ -7,7 +7,7 @@
function test() {
let temp = {};
Cu.import("resource:///modules/source-editor.jsm", temp);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", temp);
let SourceEditor = temp.SourceEditor;
waitForExplicitFinish();

View File

@ -0,0 +1,34 @@
/* 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";
const HOST = 'mochi.test:8888';
const URI = "http://" + HOST + "/browser/browser/devtools/sourceeditor/test/codemirror.html";
function test() {
waitForExplicitFinish();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
let browser = gBrowser.getBrowserForTab(tab);
browser.loadURI(URI);
function check() {
var win = browser.contentWindow.wrappedJSObject;
var doc = win.document;
var out = doc.getElementById("status");
if (!out || !out.classList.contains("done"))
return void setTimeout(check, 100);
ok(!win.failed, "CodeMirror tests all passed");
while (gBrowser.tabs.length > 1) gBrowser.removeCurrentTab();
finish();
}
setTimeout(check, 100);
}

View File

@ -5,7 +5,7 @@
"use strict";
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let SourceEditor = tempScope.SourceEditor;
let testWin;

View File

@ -0,0 +1,51 @@
namespace = "comment_";
(function() {
function test(name, mode, run, before, after) {
return testCM(name, function(cm) {
run(cm);
eq(cm.getValue(), after);
}, {value: before, mode: mode});
}
var simpleProg = "function foo() {\n return bar;\n}";
test("block", "javascript", function(cm) {
cm.blockComment(Pos(0, 3), Pos(3, 0), {blockCommentLead: " *"});
}, simpleProg + "\n", "/* function foo() {\n * return bar;\n * }\n */");
test("blockToggle", "javascript", function(cm) {
cm.blockComment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
cm.uncomment(Pos(0, 3), Pos(2, 0), {blockCommentLead: " *"});
}, simpleProg, simpleProg);
test("line", "javascript", function(cm) {
cm.lineComment(Pos(1, 1), Pos(1, 1));
}, simpleProg, "function foo() {\n// return bar;\n}");
test("lineToggle", "javascript", function(cm) {
cm.lineComment(Pos(0, 0), Pos(2, 1));
cm.uncomment(Pos(0, 0), Pos(2, 1));
}, simpleProg, simpleProg);
// test("fallbackToBlock", "css", function(cm) {
// cm.lineComment(Pos(0, 0), Pos(2, 1));
// }, "html {\n border: none;\n}", "/* html {\n border: none;\n} */");
// test("fallbackToLine", "ruby", function(cm) {
// cm.blockComment(Pos(0, 0), Pos(1));
// }, "def blah()\n return hah\n", "# def blah()\n# return hah\n");
test("commentRange", "javascript", function(cm) {
cm.blockComment(Pos(1, 2), Pos(1, 13), {fullLines: false});
}, simpleProg, "function foo() {\n /*return bar;*/\n}");
test("indented", "javascript", function(cm) {
cm.lineComment(Pos(1, 0), Pos(2), {indent: true});
}, simpleProg, "function foo() {\n // return bar;\n // }");
test("singleEmptyLine", "javascript", function(cm) {
cm.setCursor(1);
cm.execCommand("toggleComment");
}, "a;\n\nb;", "a;\n// \nb;");
})();

View File

@ -0,0 +1,139 @@
var tests = [], debug = null, debugUsed = new Array(), allNames = [];
function Failure(why) {this.message = why;}
Failure.prototype.toString = function() { return this.message; };
function indexOf(collection, elt) {
if (collection.indexOf) return collection.indexOf(elt);
for (var i = 0, e = collection.length; i < e; ++i)
if (collection[i] == elt) return i;
return -1;
}
function test(name, run, expectedFail) {
// Force unique names
var originalName = name;
var i = 2; // Second function would be NAME_2
while (indexOf(allNames, name) !== -1){
name = originalName + "_" + i;
i++;
}
allNames.push(name);
// Add test
tests.push({name: name, func: run, expectedFail: expectedFail});
return name;
}
var namespace = "";
function testCM(name, run, opts, expectedFail) {
return test(namespace + name, function() {
var place = document.getElementById("testground"), cm = window.cm = CodeMirror(place, opts);
var successful = false;
try {
run(cm);
successful = true;
} finally {
if ((debug && !successful) || verbose) {
place.style.visibility = "visible";
} else {
place.removeChild(cm.getWrapperElement());
}
}
}, expectedFail);
}
function runTests(callback) {
if (debug) {
if (indexOf(debug, "verbose") === 0) {
verbose = true;
debug.splice(0, 1);
}
if (debug.length < 1) {
debug = null;
}
}
var totalTime = 0;
function step(i) {
if (i === tests.length){
running = false;
return callback("done");
}
var test = tests[i], expFail = test.expectedFail, startTime = +new Date;
if (debug !== null) {
var debugIndex = indexOf(debug, test.name);
if (debugIndex !== -1) {
// Remove from array for reporting incorrect tests later
debug.splice(debugIndex, 1);
} else {
var wildcardName = test.name.split("_")[0] + "_*";
debugIndex = indexOf(debug, wildcardName);
if (debugIndex !== -1) {
// Remove from array for reporting incorrect tests later
debug.splice(debugIndex, 1);
debugUsed.push(wildcardName);
} else {
debugIndex = indexOf(debugUsed, wildcardName);
if (debugIndex == -1) return step(i + 1);
}
}
}
var threw = false;
try {
var message = test.func();
} catch(e) {
threw = true;
if (expFail) callback("expected", test.name);
else if (e instanceof Failure) callback("fail", test.name, e.message);
else {
var pos = /\bat .*?([^\/:]+):(\d+):/.exec(e.stack);
callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : ""));
}
}
if (!threw) {
if (expFail) callback("fail", test.name, message || "expected failure, but succeeded");
else callback("ok", test.name, message);
}
if (!quit) { // Run next test
var delay = 0;
totalTime += (+new Date) - startTime;
if (totalTime > 500){
totalTime = 0;
delay = 50;
}
setTimeout(function(){step(i + 1);}, delay);
} else { // Quit tests
running = false;
return null;
}
}
step(0);
}
function label(str, msg) {
if (msg) return str + " (" + msg + ")";
return str;
}
function eq(a, b, msg) {
if (a != b) throw new Failure(label(a + " != " + b, msg));
}
function eqPos(a, b, msg) {
function str(p) { return "{line:" + p.line + ",ch:" + p.ch + "}"; }
if (a == b) return;
if (a == null) throw new Failure(label("comparing null to " + str(b), msg));
if (b == null) throw new Failure(label("comparing " + str(a) + " to null", msg));
if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
}
function is(a, msg) {
if (!a) throw new Failure(label("assertion failed", msg));
}
function countTests() {
if (!debug) return tests.length;
var sum = 0;
for (var i = 0; i < tests.length; ++i) {
var name = tests[i].name;
if (indexOf(debug, name) != -1 ||
indexOf(debug, name.split("_")[0] + "_*") != -1)
++sum;
}
return sum;
}

View File

@ -0,0 +1,10 @@
(function() {
var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
MT("locals",
"[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] = [number 10]; [keyword return] [variable-2 a] + [variable-2 c] + [variable d]; }");
MT("comma-and-binop",
"[keyword function](){ [keyword var] [def x] = [number 1] + [number 2], [def y]; }");
})();

View File

@ -0,0 +1,10 @@
.mt-output .mt-token {
border: 1px solid #ddd;
white-space: pre;
font-family: "Consolas", monospace;
text-align: center;
}
.mt-output .mt-style {
font-size: x-small;
}

View File

@ -0,0 +1,192 @@
/**
* Helper to test CodeMirror highlighting modes. It pretty prints output of the
* highlighter and can check against expected styles.
*
* Mode tests are registered by calling test.mode(testName, mode,
* tokens), where mode is a mode object as returned by
* CodeMirror.getMode, and tokens is an array of lines that make up
* the test.
*
* These lines are strings, in which styled stretches of code are
* enclosed in brackets `[]`, and prefixed by their style. For
* example, `[keyword if]`. Brackets in the code itself must be
* duplicated to prevent them from being interpreted as token
* boundaries. For example `a[[i]]` for `a[i]`. If a token has
* multiple styles, the styles must be separated by ampersands, for
* example `[tag&error </hmtl>]`.
*
* See the test.js files in the css, markdown, gfm, and stex mode
* directories for examples.
*/
(function() {
function findSingle(str, pos, ch) {
for (;;) {
var found = str.indexOf(ch, pos);
if (found == -1) return null;
if (str.charAt(found + 1) != ch) return found;
pos = found + 2;
}
}
var styleName = /[\w&-_]+/g;
function parseTokens(strs) {
var tokens = [], plain = "";
for (var i = 0; i < strs.length; ++i) {
if (i) plain += "\n";
var str = strs[i], pos = 0;
while (pos < str.length) {
var style = null, text;
if (str.charAt(pos) == "[" && str.charAt(pos+1) != "[") {
styleName.lastIndex = pos + 1;
var m = styleName.exec(str);
style = m[0].replace(/&/g, " ");
var textStart = pos + style.length + 2;
var end = findSingle(str, textStart, "]");
if (end == null) throw new Error("Unterminated token at " + pos + " in '" + str + "'" + style);
text = str.slice(textStart, end);
pos = end + 1;
} else {
var end = findSingle(str, pos, "[");
if (end == null) end = str.length;
text = str.slice(pos, end);
pos = end;
}
text = text.replace(/\[\[|\]\]/g, function(s) {return s.charAt(0);});
tokens.push(style, text);
plain += text;
}
}
return {tokens: tokens, plain: plain};
}
test.mode = function(name, mode, tokens, modeName) {
var data = parseTokens(tokens);
return test((modeName || mode.name) + "_" + name, function() {
return compare(data.plain, data.tokens, mode);
});
};
function compare(text, expected, mode) {
var expectedOutput = [];
for (var i = 0; i < expected.length; i += 2) {
var sty = expected[i];
if (sty && sty.indexOf(" ")) sty = sty.split(' ').sort().join(' ');
expectedOutput.push(sty, expected[i + 1]);
}
var observedOutput = highlight(text, mode);
var pass, passStyle = "";
pass = highlightOutputsEqual(expectedOutput, observedOutput);
passStyle = pass ? 'mt-pass' : 'mt-fail';
var s = '';
if (pass) {
s += '<div class="mt-test ' + passStyle + '">';
s += '<pre>' + text.replace('&', '&amp;').replace('<', '&lt;') + '</pre>';
s += '<div class="cm-s-default">';
s += prettyPrintOutputTable(observedOutput);
s += '</div>';
s += '</div>';
return s;
} else {
s += '<div class="mt-test ' + passStyle + '">';
s += '<pre>' + text.replace('&', '&amp;').replace('<', '&lt;') + '</pre>';
s += '<div class="cm-s-default">';
s += 'expected:';
s += prettyPrintOutputTable(expectedOutput);
s += 'observed:';
s += prettyPrintOutputTable(observedOutput);
s += '</div>';
s += '</div>';
throw s;
}
}
/**
* Emulation of CodeMirror's internal highlight routine for testing. Multi-line
* input is supported.
*
* @param string to highlight
*
* @param mode the mode that will do the actual highlighting
*
* @return array of [style, token] pairs
*/
function highlight(string, mode) {
var state = mode.startState()
var lines = string.replace(/\r\n/g,'\n').split('\n');
var st = [], pos = 0;
for (var i = 0; i < lines.length; ++i) {
var line = lines[i], newLine = true;
var stream = new CodeMirror.StringStream(line);
if (line == "" && mode.blankLine) mode.blankLine(state);
/* Start copied code from CodeMirror.highlight */
while (!stream.eol()) {
var style = mode.token(stream, state), substr = stream.current();
if (style && style.indexOf(" ") > -1) style = style.split(' ').sort().join(' ');
stream.start = stream.pos;
if (pos && st[pos-2] == style && !newLine) {
st[pos-1] += substr;
} else if (substr) {
st[pos++] = style; st[pos++] = substr;
}
// Give up when line is ridiculously long
if (stream.pos > 5000) {
st[pos++] = null; st[pos++] = this.text.slice(stream.pos);
break;
}
newLine = false;
}
}
return st;
}
/**
* Compare two arrays of output from highlight.
*
* @param o1 array of [style, token] pairs
*
* @param o2 array of [style, token] pairs
*
* @return boolean; true iff outputs equal
*/
function highlightOutputsEqual(o1, o2) {
if (o1.length != o2.length) return false;
for (var i = 0; i < o1.length; ++i)
if (o1[i] != o2[i]) return false;
return true;
}
/**
* Print tokens and corresponding styles in a table. Spaces in the token are
* replaced with 'interpunct' dots (&middot;).
*
* @param output array of [style, token] pairs
*
* @return html string
*/
function prettyPrintOutputTable(output) {
var s = '<table class="mt-output">';
s += '<tr>';
for (var i = 0; i < output.length; i += 2) {
var style = output[i], val = output[i+1];
s +=
'<td class="mt-token">' +
'<span class="cm-' + String(style).replace(/ +/g, " cm-") + '">' +
val.replace(/ /g,'\xb7').replace('&', '&amp;').replace('<', '&lt;') +
'</span>' +
'</td>';
}
s += '</tr><tr>';
for (var i = 0; i < output.length; i += 2) {
s += '<td class="mt-style"><span>' + (output[i] || null) + '</span></td>';
}
s += '</table>';
return s;
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,200 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: Test Suite</title>
<link rel="stylesheet" href="chrome://browser/content/devtools/codemirror/codemirror.css">
<link rel="stylesheet" href="cm_mode_test.css">
<!--<link rel="stylesheet" href="../doc/docs.css">-->
<script src="chrome://browser/content/devtools/codemirror/codemirror.js"></script>
<script src="chrome://browser/content/devtools/codemirror/searchcursor.js"></script>
<script src="chrome://browser/content/devtools/codemirror/dialog.js"></script>
<script src="chrome://browser/content/devtools/codemirror/matchbrackets.js"></script>
<script src="chrome://browser/content/devtools/codemirror/comment.js"></script>
<script src="chrome://browser/content/devtools/codemirror/javascript.js"></script>
<!--<script src="../addon/mode/overlay.js"></script>
<script src="../addon/mode/multiplex.js"></script>
<script src="../mode/xml/xml.js"></script>
<script src="../keymap/vim.js"></script>
<script src="../keymap/emacs.js"></script>-->
<style type="text/css">
.ok {color: #090;}
.fail {color: #e00;}
.error {color: #c90;}
.done {font-weight: bold;}
#progress {
background: #45d;
color: white;
text-shadow: 0 0 1px #45d, 0 0 2px #45d, 0 0 3px #45d;
font-weight: bold;
white-space: pre;
}
#testground {
visibility: hidden;
}
#testground.offscreen {
visibility: visible;
position: absolute;
left: -10000px;
top: -10000px;
}
.CodeMirror { border: 1px solid black; }
</style>
</head>
<body>
<h1>CodeMirror: Test Suite</h1>
<p>A limited set of programmatic sanity tests for CodeMirror.</p>
<div style="border: 1px solid black; padding: 1px; max-width: 700px;">
<div style="width: 0px;" id=progress><div style="padding: 3px;">Ran <span id="progress_ran">0</span><span id="progress_total"> of 0</span> tests</div></div>
</div>
<p id=status>Please enable JavaScript...</p>
<div id=output></div>
<div id=testground></div>
<script src="cm_driver.js"></script>
<script src="cm_test.js"></script>
<script src="cm_comment_test.js"></script>
<script src="cm_mode_test.js"></script>
<script src="cm_mode_javascript_test.js"></script>
<!--<script src="doc_test.js"></script>
<script src="../mode/css/css.js"></script>
<script src="../mode/css/test.js"></script>
<script src="../mode/css/scss_test.js"></script>
<script src="../mode/xml/xml.js"></script>
<script src="../mode/htmlmixed/htmlmixed.js"></script>
<script src="../mode/ruby/ruby.js"></script>
<script src="../mode/haml/haml.js"></script>
<script src="../mode/haml/test.js"></script>
<script src="../mode/markdown/markdown.js"></script>
<script src="../mode/markdown/test.js"></script>
<script src="../mode/gfm/gfm.js"></script>
<script src="../mode/gfm/test.js"></script>
<script src="../mode/stex/stex.js"></script>
<script src="../mode/stex/test.js"></script>
<script src="../mode/xquery/xquery.js"></script>
<script src="../mode/xquery/test.js"></script>
<script src="../addon/mode/multiplex_test.js"></script>
<script src="vim_test.js"></script>
<script src="emacs_test.js"></script>-->
<script>
window.onload = runHarness;
CodeMirror.on(window, 'hashchange', runHarness);
function esc(str) {
return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
}
var output = document.getElementById("output"),
progress = document.getElementById("progress"),
progressRan = document.getElementById("progress_ran").childNodes[0],
progressTotal = document.getElementById("progress_total").childNodes[0];
var count = 0,
failed = 0,
bad = "",
running = false, // Flag that states tests are running
quit = false, // Flag to quit tests ASAP
verbose = false; // Adds message for *every* test to output
function runHarness(){
if (running) {
quit = true;
setStatus("Restarting tests...", '', true);
setTimeout(function(){runHarness();}, 500);
return;
}
if (window.location.hash.substr(1)){
debug = window.location.hash.substr(1).split(",");
} else {
debug = null;
}
quit = false;
running = true;
setStatus("Loading tests...");
count = 0;
failed = 0;
bad = "";
verbose = false;
debugUsed = Array();
totalTests = countTests();
progressTotal.nodeValue = " of " + totalTests;
progressRan.nodeValue = count;
output.innerHTML = '';
document.getElementById("testground").innerHTML = "<form>" +
"<textarea id=\"code\" name=\"code\"></textarea>" +
"<input type=submit value=ok name=submit>" +
"</form>";
runTests(displayTest);
}
function setStatus(message, className, force){
if (quit && !force) return;
if (!message) throw("must provide message");
var status = document.getElementById("status").childNodes[0];
status.nodeValue = message;
status.parentNode.className = className;
}
function addOutput(name, className, code){
var newOutput = document.createElement("dl");
var newTitle = document.createElement("dt");
newTitle.className = className;
newTitle.appendChild(document.createTextNode(name));
newOutput.appendChild(newTitle);
var newMessage = document.createElement("dd");
newMessage.innerHTML = code;
newOutput.appendChild(newTitle);
newOutput.appendChild(newMessage);
output.appendChild(newOutput);
}
function displayTest(type, name, customMessage) {
var message = "???";
if (type != "done") ++count;
progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
progressRan.nodeValue = count;
if (type == "ok") {
message = "Test '" + name + "' succeeded";
if (!verbose) customMessage = false;
} else if (type == "expected") {
message = "Test '" + name + "' failed as expected";
if (!verbose) customMessage = false;
} else if (type == "error" || type == "fail") {
++failed;
message = "Test '" + name + "' failed";
} else if (type == "done") {
if (failed) {
type += " fail";
message = failed + " failure" + (failed > 1 ? "s" : "");
} else if (count < totalTests) {
failed = totalTests - count;
type += " fail";
message = failed + " failure" + (failed > 1 ? "s" : "");
} else {
type += " ok";
message = "All passed";
}
if (debug && debug.length) {
var bogusTests = totalTests - count;
message += " — " + bogusTests + " nonexistent test" +
(bogusTests > 1 ? "s" : "") + " requested by location.hash: " +
"`" + debug.join("`, `") + "`";
} else {
progressTotal.nodeValue = '';
}
customMessage = true; // Hack to avoid adding to output
}
if (verbose && !customMessage) customMessage = message;
setStatus(message, type);
if (customMessage && customMessage.length > 0) {
addOutput(name, type, customMessage);
}
}
</script>
</body>
</html>

View File

@ -139,7 +139,7 @@ function openSourceEditorWindow(aCallback, aOptions) {
function initEditor()
{
let tempScope = {};
Cu.import("resource:///modules/source-editor.jsm", tempScope);
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm", tempScope);
let box = testWin.document.querySelector("box");
editor = new tempScope.SourceEditor();

View File

@ -16,7 +16,7 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");

View File

@ -47,4 +47,4 @@ annotation.currentLine=Current line
# a tooltip displayed in any of the editor gutters when the user hovers the
# current debugger location. The debugger can pause the JavaScript execution at
# user-defined lines.
annotation.debugLocation.title=Current step: %S
annotation.debugLocation.title=Current step: %S

View File

@ -125,6 +125,7 @@
<li><a href="about:license#vtune">VTune License</a></li>
<li><a href="about:license#webrtc">WebRTC License</a></li>
<li><a href="about:license#xiph">Xiph.org Foundation License</a></li>
<li><a href="about:license#codemirror">CodeMirror License</a></li>
</ul>
<br>
@ -4056,6 +4057,48 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</pre>
<hr>
<h1><a id="codemirror"></a>CodeMirror License</h1>
<p>This license applies to all files in
<span class="path">browser/devtools/sourceeditor/codemirror</span> and
to specified files in the <span class="path">browser/devtools/sourceeditor/test/</span>:
</p>
<ul>
<li><span class="path">cm_comment_test.js</span></li>
<li><span class="path">cm_driver.js</span></li>
<li><span class="path">cm_mode_javascript_test.js</span></li>
<li><span class="path">cm_mode_test.css</span></li>
<li><span class="path">cm_mode_test.js</span></li>
<li><span class="path">cm_test.js</span></li>
</ul>
<pre>
Copyright (C) 2013 by Marijn Haverbeke <marijnh@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Please note that some subdirectories of the CodeMirror distribution
include their own LICENSE files, and are released under different
licences.
</pre>
<hr>