merge m-c to fx-team

This commit is contained in:
Tim Taubert 2014-01-19 10:42:28 +01:00
commit 5982a00404
126 changed files with 3397 additions and 1281 deletions

View File

@ -22,4 +22,4 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more.
Updates to NSS seem to require a clobber due bug 959928.
Bug 917896 requires a clobber due to bug 961339.

View File

@ -21,8 +21,7 @@ let CustomizationHandler = {
},
isCustomizing: function() {
return document.documentElement.hasAttribute("customizing") ||
document.documentElement.hasAttribute("customize-exiting");
return document.documentElement.hasAttribute("customizing");
},
_customizationStarting: function() {

View File

@ -23,11 +23,10 @@ var FullZoom = {
// From nsEventStateManager.h.
ACTION_ZOOM: 3,
// This maps browser outer window IDs to monotonically increasing integer
// tokens. _browserTokenMap[outerID] is increased each time the zoom is
// changed in the browser whose outer window ID is outerID. See
// _getBrowserToken and _ignorePendingZoomAccesses.
_browserTokenMap: new Map(),
// This maps the browser to monotonically increasing integer
// tokens. _browserTokenMap[browser] is increased each time the zoom is
// changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
_browserTokenMap: new WeakMap(),
get siteSpecific() {
return this._siteSpecificPref;
@ -46,10 +45,6 @@ var FullZoom = {
// Initialization & Destruction
init: function FullZoom_init() {
// Bug 691614 - zooming support for electrolysis
if (gMultiProcessBrowser)
return;
// Listen for scrollwheel events so we can save scrollwheel-based changes.
window.addEventListener("DOMMouseScroll", this, false);
@ -65,19 +60,12 @@ var FullZoom = {
// Listen for changes to the browser.zoom branch so we can enable/disable
// updating background tabs and per-site saving and restoring of zoom levels.
gPrefService.addObserver("browser.zoom.", this, true);
Services.obs.addObserver(this, "outer-window-destroyed", false);
},
destroy: function FullZoom_destroy() {
// Bug 691614 - zooming support for electrolysis
if (gMultiProcessBrowser)
return;
gPrefService.removeObserver("browser.zoom.", this);
this._cps2.removeObserverForName(this.name, this);
window.removeEventListener("DOMMouseScroll", this, false);
Services.obs.removeObserver(this, "outer-window-destroyed");
},
@ -156,10 +144,6 @@ var FullZoom = {
break;
}
break;
case "outer-window-destroyed":
let outerID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
this._browserTokenMap.delete(outerID);
break;
}
},
@ -208,7 +192,7 @@ var FullZoom = {
// zoom should be set to the new global preference now that the global
// preference has changed.
let hasPref = false;
let ctxt = this._loadContextFromWindow(browser.contentWindow);
let ctxt = this._loadContextFromBrowser(browser);
let token = this._getBrowserToken(browser);
this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
handleResult: function () hasPref = true,
@ -233,10 +217,6 @@ var FullZoom = {
* (optional) browser object displaying the document
*/
onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
// Bug 691614 - zooming support for electrolysis
if (gMultiProcessBrowser)
return;
// Ignore all pending async zoom accesses in the browser. Pending accesses
// that started before the location change will be prevented from applying
// to the new location.
@ -256,7 +236,7 @@ var FullZoom = {
}
// Media documents should always start at 1, and are not affected by prefs.
if (!aIsTabSwitch && browser.contentDocument.mozSyntheticDocument) {
if (!aIsTabSwitch && browser.isSyntheticDocument) {
ZoomManager.setZoomForBrowser(browser, 1);
// _ignorePendingZoomAccesses already called above, so no need here.
this._notifyOnLocationChange();
@ -264,7 +244,7 @@ var FullZoom = {
}
// See if the zoom pref is cached.
let ctxt = this._loadContextFromWindow(browser.contentWindow);
let ctxt = this._loadContextFromBrowser(browser);
let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
if (pref) {
this._applyPrefToZoom(pref.value, browser,
@ -326,7 +306,7 @@ var FullZoom = {
reset: function FullZoom_reset() {
let browser = gBrowser.selectedBrowser;
let token = this._getBrowserToken(browser);
this._getGlobalValue(browser.contentWindow, function (value) {
this._getGlobalValue(browser, function (value) {
if (token.isCurrent) {
ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
this._ignorePendingZoomAccesses(browser);
@ -364,11 +344,10 @@ var FullZoom = {
return;
}
// aBrowser.contentDocument is sometimes gone because this method is called
// The browser is sometimes half-destroyed because this method is called
// by content pref service callbacks, which themselves can be called at any
// time, even after browsers are closed.
if (!aBrowser.contentDocument ||
aBrowser.contentDocument.mozSyntheticDocument) {
if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
this._executeSoon(aCallback);
return;
}
@ -381,7 +360,7 @@ var FullZoom = {
}
let token = this._getBrowserToken(aBrowser);
this._getGlobalValue(aBrowser.contentWindow, function (value) {
this._getGlobalValue(aBrowser, function (value) {
if (token.isCurrent) {
ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
this._ignorePendingZoomAccesses(aBrowser);
@ -400,12 +379,12 @@ var FullZoom = {
Services.obs.notifyObservers(null, "browser-fullZoom:zoomChange", "");
if (!this.siteSpecific ||
gInPrintPreviewMode ||
browser.contentDocument.mozSyntheticDocument)
browser.isSyntheticDocument)
return;
this._cps2.set(browser.currentURI.spec, this.name,
ZoomManager.getZoomForBrowser(browser),
this._loadContextFromWindow(browser.contentWindow), {
this._loadContextFromBrowser(browser), {
handleCompletion: function () {
this._isNextContentPrefChangeInternal = true;
}.bind(this),
@ -419,9 +398,9 @@ var FullZoom = {
*/
_removePref: function FullZoom__removePref(browser) {
Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
if (browser.contentDocument.mozSyntheticDocument)
if (browser.isSyntheticDocument)
return;
let ctxt = this._loadContextFromWindow(browser.contentWindow);
let ctxt = this._loadContextFromBrowser(browser);
this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
handleCompletion: function () {
this._isNextContentPrefChangeInternal = true;
@ -445,19 +424,18 @@ var FullZoom = {
* @return An object with an "isCurrent" getter.
*/
_getBrowserToken: function FullZoom__getBrowserToken(browser) {
let outerID = this._browserOuterID(browser);
let map = this._browserTokenMap;
if (!map.has(outerID))
map.set(outerID, 0);
if (!map.has(browser))
map.set(browser, 0);
return {
token: map.get(outerID),
token: map.get(browser),
get isCurrent() {
// At this point, the browser may have been destructed and unbound but
// its outer ID not removed from the map because outer-window-destroyed
// hasn't been received yet. In that case, the browser is unusable, it
// has no properties, so return false. Check for this case by getting a
// property, say, docShell.
return map.get(outerID) === this.token && browser.docShell;
return map.get(browser) === this.token && browser.parentNode;
},
};
},
@ -470,17 +448,8 @@ var FullZoom = {
* @param browser Pending accesses in this browser will be ignored.
*/
_ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
let outerID = this._browserOuterID(browser);
let map = this._browserTokenMap;
map.set(outerID, (map.get(outerID) || 0) + 1);
},
_browserOuterID: function FullZoom__browserOuterID(browser) {
return browser.
contentWindow.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDOMWindowUtils).
outerWindowID;
map.set(browser, (map.get(browser) || 0) + 1);
},
_ensureValid: function FullZoom__ensureValid(aValue) {
@ -507,12 +476,12 @@ var FullZoom = {
* level. It's not always possible to avoid them, though. As a convenience,
* then, this method takes a callback and returns nothing.
*
* @param window The content window pertaining to the zoom.
* @param browser The browser pertaining to the zoom.
* @param callback Synchronously or asynchronously called when done. It's
* bound to this object (FullZoom) and called as:
* callback(prefValue)
*/
_getGlobalValue: function FullZoom__getGlobalValue(window, callback) {
_getGlobalValue: function FullZoom__getGlobalValue(browser, callback) {
// * !("_globalValue" in this) => global value not yet cached.
// * this._globalValue === undefined => global value known not to exist.
// * Otherwise, this._globalValue is a number, the global value.
@ -521,7 +490,7 @@ var FullZoom = {
return;
}
let value = undefined;
this._cps2.getGlobal(this.name, this._loadContextFromWindow(window), {
this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), {
handleResult: function (pref) value = pref.value,
handleCompletion: function (reason) {
this._globalValue = this._ensureValid(value);
@ -531,16 +500,13 @@ var FullZoom = {
},
/**
* Gets the load context from the given window.
* Gets the load context from the given Browser.
*
* @param window The window whose load context will be returned.
* @return The nsILoadContext of the given window.
* @param Browser The Browser whose load context will be returned.
* @return The nsILoadContext of the given Browser.
*/
_loadContextFromWindow: function FullZoom__loadContextFromWindow(window) {
return window.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsILoadContext);
_loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
return browser.loadContext;
},
/**

View File

@ -3710,11 +3710,11 @@ var XULBrowserWindow = {
// Try not to instantiate gCustomizeMode as much as possible,
// so don't use CustomizeMode.jsm to check for URI or customizing.
let customizingURI = "about:customizing";
if (location == customizingURI &&
!CustomizationHandler.isCustomizing()) {
if (location == customizingURI) {
gCustomizeMode.enter();
} else if (location != customizingURI &&
CustomizationHandler.isCustomizing()) {
(CustomizationHandler.isEnteringCustomizeMode ||
CustomizationHandler.isCustomizing())) {
gCustomizeMode.exit();
}
}

View File

@ -2829,6 +2829,18 @@
onget="return this.mCurrentBrowser.securityUI;"
readonly="true"/>
<property name="fullZoom"
onget="return this.mCurrentBrowser.fullZoom;"
onset="this.mCurrentBrowser.fullZoom = val;"/>
<property name="textZoom"
onget="return this.mCurrentBrowser.textZoom;"
onset="this.mCurrentBrowser.textZoom = val;"/>
<property name="isSyntheticDocument"
onget="return this.mCurrentBrowser.isSyntheticDocument;"
readonly="true"/>
<method name="_handleKeyEvent">
<parameter name="aEvent"/>
<body><![CDATA[
@ -2948,7 +2960,7 @@
switch (aMessage.name) {
case "DOMTitleChanged": {
let tab = this._getTabForBrowser(browser);
if (!tab)
if (!tab || tab.hasAttribute("pending"))
return;
let titleChanged = this.setTabTitle(tab);
if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
@ -2970,8 +2982,7 @@
if (!tab)
return;
this.selectedTab = tab;
let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
focusManager.activeWindow = window;
window.focus();
break;
}
}
@ -3191,6 +3202,9 @@
return;
var tab = this._getTabForContentWindow(contentWin);
if (tab.hasAttribute("pending"))
return;
var titleChanged = this.setTabTitle(tab);
if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
tab.setAttribute("titlechanged", "true");

View File

@ -8,6 +8,7 @@
&customizeMode.menuAndToolbars.header;
</label>
<vbox id="customization-palette" flex="1"/>
<spacer flex="1"/>
<hbox>
<button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>

View File

@ -365,9 +365,9 @@ const CustomizableWidgets = [{
//XXXgijs in some tests we get called very early, and there's no docShell on the
// tabbrowser. This breaks the zoom toolkit code (see bug 897410). Don't let that happen:
let zoomFactor = 100;
if (window.gBrowser.docShell) {
try {
zoomFactor = Math.floor(window.ZoomManager.zoom * 100);
}
} catch (e) {}
zoomResetButton.setAttribute("label", CustomizableUI.getLocalizedProperty(
buttons[1], "label", [zoomFactor]
));

View File

@ -74,8 +74,13 @@ CustomizeMode.prototype = {
return this.document.getElementById("PanelUI-contents");
},
get _handler() {
return this.window.CustomizationHandler;
},
toggle: function() {
if (this._transitioning) {
if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
return;
}
if (this._customizing) {
@ -86,10 +91,20 @@ CustomizeMode.prototype = {
},
enter: function() {
if (this._customizing || this._transitioning) {
this._wantToBeInCustomizeMode = true;
if (this._customizing || this._handler.isEnteringCustomizeMode) {
return;
}
// Exiting; want to re-enter once we've done that.
if (this._handler.isExitingCustomizeMode) {
LOG("Attempted to enter while we're in the middle of exiting. " +
"We'll exit after we've entered");
return;
}
// We don't need to switch to kAboutURI, or open a new tab at
// kAboutURI if we're already on it.
if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) {
@ -100,6 +115,8 @@ CustomizeMode.prototype = {
let window = this.window;
let document = this.document;
this._handler.isEnteringCustomizeMode = true;
Task.spawn(function() {
// We shouldn't start customize mode until after browser-delayed-startup has finished:
if (!this.window.gBrowserInit.delayedStartupFinished) {
@ -212,19 +229,35 @@ CustomizeMode.prototype = {
// Show the palette now that the transition has finished.
this.visiblePalette.hidden = false;
this._handler.isEnteringCustomizeMode = false;
this.dispatchToolboxEvent("customizationready");
if (!this._wantToBeInCustomizeMode) {
this.exit();
}
}.bind(this)).then(null, function(e) {
ERROR(e);
// We should ensure this has been called, and calling it again doesn't hurt:
window.PanelUI.endBatchUpdate();
});
this._handler.isEnteringCustomizeMode = false;
}.bind(this));
},
exit: function() {
if (!this._customizing || this._transitioning) {
this._wantToBeInCustomizeMode = false;
if (!this._customizing || this._handler.isExitingCustomizeMode) {
return;
}
// Entering; want to exit once we've done that.
if (this._handler.isEnteringCustomizeMode) {
LOG("Attempted to exit while we're in the middle of entering. " +
"We'll exit after we've entered");
return;
}
this._handler.isExitingCustomizeMode = true;
CustomizableUI.removeListener(this);
this.document.removeEventListener("keypress", this);
@ -307,7 +340,13 @@ CustomizeMode.prototype = {
let custBrowser = this.browser.selectedBrowser;
if (custBrowser.canGoBack) {
// If there's history to this tab, just go back.
custBrowser.goBack();
// Note that this throws an exception if the previous document has a
// problematic URL (e.g. about:idontexist)
try {
custBrowser.goBack();
} catch (ex) {
ERROR(ex);
}
} else {
// If we can't go back, we're removing the about:customization tab.
// We only do this if we're the top window for this window (so not
@ -332,13 +371,19 @@ CustomizeMode.prototype = {
this.window.PanelUI.endBatchUpdate();
this._changed = false;
this._transitioning = false;
this._handler.isExitingCustomizeMode = false;
this.dispatchToolboxEvent("aftercustomization");
CustomizableUI.notifyEndCustomizing(this.window);
if (this._wantToBeInCustomizeMode) {
this.enter();
}
}.bind(this)).then(null, function(e) {
ERROR(e);
// We should ensure this has been called, and calling it again doesn't hurt:
window.PanelUI.endBatchUpdate();
});
this._handler.isExitingCustomizeMode = false;
}.bind(this));
},
/**

View File

@ -19,6 +19,7 @@ skip-if = os == "mac"
[browser_886323_buildArea_removable_nodes.js]
[browser_887438_currentset_shim.js]
[browser_888817_currentset_updating.js]
[browser_889120_customize_tab_merging.js]
[browser_890140_orphaned_placeholders.js]
[browser_890262_destroyWidget_after_add_to_panel.js]
[browser_892955_isWidgetRemovable_for_removed_widgets.js]

View File

@ -110,7 +110,6 @@ function checkPalette(id, method) {
}
let otherWin;
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Moving widgets in two windows, one with customize mode and one without, should work.
add_task(function MoveWidgetsInTwoWindows() {
@ -139,6 +138,5 @@ add_task(function MoveWidgetsInTwoWindows() {
});
add_task(function asyncCleanup() {
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});
});

View File

@ -4,8 +4,6 @@
"use strict";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Dragging an item from the palette to another button in the panel should work.
add_task(function() {
yield startCustomizing();
@ -64,6 +62,5 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -4,7 +4,6 @@
"use strict";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
requestLongerTimeout(5);
// Dragging the zoom controls to be before the print button should not move any controls.
@ -462,6 +461,5 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -0,0 +1,44 @@
/* 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 kTestToolbarId = "test-empty-drag";
// Attempting to switch quickly from one tab to another to see whether the state changes
// correctly.
add_task(function CheckBasicCustomizeMode() {
yield startCustomizing();
ok(CustomizationHandler.isCustomizing(), "We should be in customize mode");
yield endCustomizing();
ok(!CustomizationHandler.isCustomizing(), "We should not be in customize mode");
});
add_task(function CheckQuickCustomizeModeSwitch() {
let tab1 = gBrowser.addTab("about:newtab");
gBrowser.selectedTab = tab1;
let tab2 = gBrowser.addTab("about:customizing");
let tab3 = gBrowser.addTab("about:newtab");
gBrowser.selectedTab = tab2;
try {
yield waitForCondition(() => CustomizationHandler.isEnteringCustomizeMode);
} catch (ex) {
Cu.reportError(ex);
}
ok(CustomizationHandler.isEnteringCustomizeMode, "Should be entering customize mode");
gBrowser.selectedTab = tab3;
try {
yield waitForCondition(() => !CustomizationHandler.isEnteringCustomizeMode && !CustomizationHandler.isCustomizing());
} catch (ex) {
Cu.reportError(ex);
}
ok(!CustomizationHandler.isCustomizing(), "Should not be entering customize mode");
gBrowser.removeTab(tab1);
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab3);
});
add_task(function asyncCleanup() {
yield endCustomizing();
});

View File

@ -4,8 +4,6 @@
"use strict";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// One orphaned item should have two placeholders next to it.
add_task(function() {
yield startCustomizing();
@ -158,7 +156,6 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -52,8 +52,8 @@ add_task(function() {
ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
ok(CustomizableUI.inDefaultState, "Should start in default state.");
window.resizeTo(380, window.outerHeight);
yield waitForCondition(() => navbar.hasAttribute("overflowing"));
window.resizeTo(360, window.outerHeight);
yield waitForCondition(() => navbar.getAttribute("overflowing") == "true");
ok(!navbar.querySelector("#search-container"), "Search container should be overflowing");
let searchbar = document.getElementById("searchbar");

View File

@ -108,8 +108,8 @@ add_task(function() {
ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
ok(CustomizableUI.inDefaultState, "Should start in default state.");
window.resizeTo(380, window.outerHeight);
yield waitForCondition(() => navbar.hasAttribute("overflowing"));
window.resizeTo(360, window.outerHeight);
yield waitForCondition(() => navbar.getAttribute("overflowing") == "true");
ok(!navbar.querySelector("#" + kSearchBox), "Search container should be overflowing");
let placements = CustomizableUI.getWidgetIdsInArea(navbar.id);
let searchboxPlacement = placements.indexOf(kSearchBox);

View File

@ -7,8 +7,6 @@
let navbar;
let skippedItem;
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Attempting to drag a skipintoolbarset item should work.
add_task(function() {
navbar = document.getElementById("nav-bar");
@ -35,6 +33,5 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
skippedItem.remove();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -4,8 +4,6 @@
"use strict";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Customize mode reset button should revert correctly
add_task(function() {
yield startCustomizing();
@ -23,6 +21,5 @@ add_task(function() {
});
add_task(function asyncCleanup() {
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -5,7 +5,6 @@
"use strict";
const kTestToolbarId = "test-empty-drag";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Attempting to drag an item to an empty container should work.
add_task(function() {
@ -23,6 +22,5 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -4,8 +4,6 @@
"use strict";
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
// Attempting to drag the menubar to the navbar shouldn't work.
add_task(function() {
yield startCustomizing();
@ -23,6 +21,5 @@ add_task(function() {
add_task(function asyncCleanup() {
yield endCustomizing();
Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
yield resetCustomization();
});

View File

@ -14,6 +14,9 @@ let ChromeUtils = {};
let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils);
Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
registerCleanupFunction(() => Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck"));
let {synthesizeDragStart, synthesizeDrop} = ChromeUtils;
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

View File

@ -479,11 +479,22 @@ nsBrowserContentHandler.prototype = {
}
if (cmdLine.handleFlag("silent", false))
cmdLine.preventDefault = true;
if (cmdLine.handleFlag("private-window", false)) {
openWindow(null, this.chromeURL, "_blank",
"chrome,dialog=no,private,all" + this.getFeatures(cmdLine),
"about:privatebrowsing");
cmdLine.preventDefault = true;
try {
var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false);
if (privateWindowParam) {
var uri = resolveURIInternal(cmdLine, privateWindowParam);
handURIToExistingBrowser(uri, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, true);
cmdLine.preventDefault = true;
}
} catch (e if e.result == Components.results.NS_ERROR_INVALID_ARG) {
// NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param.
if (cmdLine.handleFlag("private-window", false)) {
openWindow(null, this.chromeURL, "_blank",
"chrome,dialog=no,private,all" + this.getFeatures(cmdLine),
"about:privatebrowsing");
cmdLine.preventDefault = true;
}
}
var searchParam = cmdLine.handleFlagWithParam("search", false);
@ -685,19 +696,22 @@ nsBrowserContentHandler.prototype = {
};
var gBrowserContentHandler = new nsBrowserContentHandler();
function handURIToExistingBrowser(uri, location, cmdLine)
function handURIToExistingBrowser(uri, location, cmdLine, forcePrivate)
{
if (!shouldLoadURI(uri))
return;
// Do not open external links in private windows, unless we're in perma-private mode
var allowPrivate = PrivateBrowsingUtils.permanentPrivateBrowsing;
// Unless using a private window is forced, open external links in private
// windows only if we're in perma-private mode.
var allowPrivate = forcePrivate || PrivateBrowsingUtils.permanentPrivateBrowsing;
var navWin = RecentWindow.getMostRecentBrowserWindow({private: allowPrivate});
if (!navWin) {
// if we couldn't load it in an existing window, open a new one
openWindow(null, gBrowserContentHandler.chromeURL, "_blank",
"chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine),
uri.spec);
var features = "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine);
if (forcePrivate) {
features += ",private";
}
openWindow(null, gBrowserContentHandler.chromeURL, "_blank", features, uri.spec);
return;
}

View File

@ -342,11 +342,16 @@ let FormDataListener = {
*/
let PageStyleListener = {
init: function () {
Services.obs.addObserver(this, "author-style-disabled-changed", true);
Services.obs.addObserver(this, "style-sheet-applicable-state-changed", true);
Services.obs.addObserver(this, "author-style-disabled-changed", false);
Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false);
gFrameTree.addObserver(this);
},
uninit: function () {
Services.obs.removeObserver(this, "author-style-disabled-changed");
Services.obs.removeObserver(this, "style-sheet-applicable-state-changed");
},
observe: function (subject, topic) {
let frame = subject.defaultView;
@ -367,8 +372,7 @@ let PageStyleListener = {
MessageQueue.push("pageStyle", () => null);
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
};
/**
@ -419,10 +423,14 @@ let DocShellCapabilitiesListener = {
let SessionStorageListener = {
init: function () {
addEventListener("MozStorageChanged", this);
Services.obs.addObserver(this, "browser:purge-domain-data", true);
Services.obs.addObserver(this, "browser:purge-domain-data", false);
gFrameTree.addObserver(this);
},
uninit: function () {
Services.obs.removeObserver(this, "browser:purge-domain-data");
},
handleEvent: function (event) {
// Ignore events triggered by localStorage or globalStorage changes.
if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) {
@ -450,8 +458,7 @@ let SessionStorageListener = {
this.collect();
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference])
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
};
/**
@ -656,3 +663,13 @@ SessionStorageListener.init();
ScrollPositionListener.init();
DocShellCapabilitiesListener.init();
PrivacyListener.init();
addEventListener("unload", () => {
// Remove all registered nsIObservers.
PageStyleListener.uninit();
SessionStorageListener.uninit();
// We don't need to take care of any gFrameTree observers as the gFrameTree
// will die with the content script. The same goes for the privacy transition
// observer that will die with the docShell when the tab is closed.
});

View File

@ -142,7 +142,6 @@ ContentRestoreInternal.prototype = {
SessionHistory.restore(this.docShell, tabData);
// Add a listener to watch for reloads.
let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
let listener = new HistoryListener(this.docShell, reloadCallback);
webNavigation.sessionHistory.addSHistoryListener(listener);
this._historyListener = listener;

View File

@ -629,6 +629,30 @@ let SessionStoreInternal = {
if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
// Notify the tabbrowser that the tab chrome has been restored.
let tab = this._getTabForBrowser(browser);
let tabData = browser.__SS_data;
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activePageData = tabData.entries[tabData.index - 1] || null;
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
}
// Restore the tab icon.
if ("image" in tabData) {
win.gBrowser.setIcon(tab, tabData.image);
}
let event = win.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
tab.dispatchEvent(event);
@ -2722,33 +2746,11 @@ let SessionStoreInternal = {
browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
{tabData: tabData, epoch: epoch});
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activePageData = tabData.entries[activeIndex] || null;
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
}
// Restore tab attributes.
if ("attributes" in tabData) {
TabAttributes.set(tab, tabData.attributes);
}
// Restore the tab icon.
if ("image" in tabData) {
tabbrowser.setIcon(tab, tabData.image);
}
// This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
// it ensures each window will have its selected tab loaded.
if (aRestoreImmediately || tabbrowser.selectedBrowser == browser) {

View File

@ -62,6 +62,7 @@ support-files =
[browser_formdata_xpath.js]
[browser_frametree.js]
[browser_global_store.js]
[browser_label_and_icon.js]
[browser_merge_closed_tabs.js]
[browser_pageshow.js]
[browser_pageStyle.js]

View File

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Make sure that tabs are restored on demand as otherwise the tab will start
* loading immediately and we can't check its icon and label.
*/
add_task(function setup() {
Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
});
});
/**
* Ensure that a pending tab has label and icon correctly set.
*/
add_task(function test_label_and_icon() {
// Create a new tab.
let tab = gBrowser.addTab("about:robots");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Retrieve the tab state.
SyncHandlers.get(browser).flush();
let state = ss.getTabState(tab);
gBrowser.removeTab(tab);
browser = null;
// Open a new tab to restore into.
let tab = gBrowser.addTab("about:blank");
ss.setTabState(tab, state);
yield promiseTabRestoring(tab);
// Check that label and icon are set for the restoring tab.
ok(gBrowser.getIcon(tab).startsWith("data:image/png;"), "icon is set");
is(tab.label, "Gort! Klaatu barada nikto!", "label is set");
// Cleanup.
gBrowser.removeTab(tab);
});
function promiseTabRestoring(tab) {
let deferred = Promise.defer();
tab.addEventListener("SSTabRestoring", function onRestoring() {
tab.removeEventListener("SSTabRestoring", onRestoring);
deferred.resolve();
});
return deferred.promise;
}

View File

@ -212,6 +212,7 @@ support-files =
[browser_dbg_variables-view-05.js]
[browser_dbg_variables-view-accessibility.js]
[browser_dbg_variables-view-data.js]
[browser_dbg_variables-view-edit-cancel.js]
[browser_dbg_variables-view-edit-getset-01.js]
[browser_dbg_variables-view-edit-getset-02.js]
[browser_dbg_variables-view-edit-value.js]

View File

@ -39,7 +39,7 @@ function test() {
ok(gContextMenu,
"The source editor's context menupopup is available.");
ok(gEditor.isReadOnly(),
ok(gEditor.getOption("readOnly"),
"The source editor is read only.");
gEditor.focus();

View File

@ -0,0 +1,53 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that canceling a name change correctly unhides the separator and
* value elements.
*/
const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
function test() {
Task.spawn(function*() {
let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
let win = panel.panelWin;
let vars = win.DebuggerView.Variables;
win.DebuggerView.WatchExpressions.addExpression("this");
// Allow this generator function to yield first.
executeSoon(() => debuggee.ermahgerd());
yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS);
let exprScope = vars.getScopeAtIndex(0);
let {target} = exprScope.get("this");
let name = target.querySelector(".title > .name");
let separator = target.querySelector(".separator");
let value = target.querySelector(".value");
is(separator.hidden, false,
"The separator element should not be hidden.");
is(value.hidden, false,
"The value element should not be hidden.");
for (let key of ["ESCAPE", "ENTER"]) {
EventUtils.sendMouseEvent({ type: "dblclick" }, name, win);
is(separator.hidden, true,
"The separator element should be hidden.");
is(value.hidden, true,
"The value element should be hidden.");
EventUtils.sendKey(key, win);
is(separator.hidden, false,
"The separator element should not be hidden.");
is(value.hidden, false,
"The value element should not be hidden.");
}
yield resumeDebuggerThenCloseAndFinish(panel);
});
}

View File

@ -10,6 +10,7 @@ support-files =
browser_inspector_select_last_selected.html
browser_inspector_select_last_selected2.html
browser_inspector_bug_848731_reset_selection_on_delete.html
browser_inspector_bug_958456_highlight_comments.html
head.js
[browser_inspector_basic_highlighter.js]
@ -45,4 +46,5 @@ support-files =
[browser_inspector_bug_848731_reset_selection_on_delete.js]
[browser_inspector_bug_922125_destroy_on_navigate.js]
[browser_inspector_bug_952294_tooltips_dimensions.js]
[browser_inspector_bug_958456_highlight_comments.js]
[browser_inspector_bug_958169_switch_to_inspector_on_pick.js]

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Inspector Highlighter Test</title>
</head>
<body>
<p></p>
<div id="id1">Visible div 1</div>
<!-- Invisible comment node -->
<div id="id2">Visible div 2</div>
<script type="text/javascript">/*Invisible script node*/</script>
<div id="id3">Visible div 3</div>
<div id="id4" style="display:none;">Invisible div node</div>
</body>
</html>

View File

@ -0,0 +1,107 @@
/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 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/. */
// Test that hovering over the markup-view's containers doesn't always show the
// highlighter, depending on the type of node hovered over.
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
let promise = devtools.require("sdk/core/promise");
let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const TEST_PAGE = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.html";
let inspector, markupView, doc;
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
doc = content.document;
waitForFocus(function() {
openInspector((aInspector, aToolbox) => {
inspector = aInspector;
markupView = inspector.markup;
inspector.once("inspector-updated", startTests);
});
}, content);
}, true);
content.location = TEST_PAGE;
}
function startTests() {
Task.spawn(function() {
yield prepareHighlighter();
yield hoverElement("#id1");
assertHighlighterShownOn("#id1");
yield hoverComment();
assertHighlighterHidden();
yield hoverElement("#id2");
assertHighlighterShownOn("#id2");
yield hoverElement("script");
assertHighlighterHidden();
yield hoverElement("#id3");
assertHighlighterShownOn("#id3");
yield hoverElement("#id4");
assertHighlighterHidden();
}).then(null, Cu.reportError).then(finishTest);
}
function finishTest() {
doc = inspector = markupView = null;
gBrowser.removeCurrentTab();
finish();
}
function prepareHighlighter() {
// Make sure the highlighter doesn't have transitions enabled
let deferred = promise.defer();
inspector.selection.setNode(doc.querySelector("p"), null);
inspector.once("inspector-updated", () => {
getHighlighterOutline().setAttribute("disable-transitions", "true");
deferred.resolve();
});
return deferred.promise;
}
function hoverContainer(container) {
let deferred = promise.defer();
EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
markupView.doc.defaultView);
inspector.markup.once("node-highlight", deferred.resolve);
return deferred.promise;
}
function hoverElement(selector) {
info("Hovering node " + selector + " in the markup view");
let container = getContainerForRawNode(markupView, doc.querySelector(selector));
return hoverContainer(container);
}
function hoverComment() {
info("Hovering the comment node in the markup view");
for (let [node, container] of markupView._containers) {
if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) {
return hoverContainer(container);
}
}
}
function assertHighlighterShownOn(selector) {
let node = doc.querySelector(selector);
let highlightNode = getHighlitNode();
is(node, highlightNode, "Highlighter is shown on the right node: " + selector);
}
function assertHighlighterHidden() {
ok(!isHighlighting(), "Highlighter is hidden");
}

View File

@ -54,7 +54,7 @@ function selectNode(aInspector)
inspector.sidebar.once("ruleview-ready", function() {
ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
inspector.sidebar.select("ruleview");
inspector.selection.setNode(div);
inspector.selection.setNode(div, "test");
inspector.once("inspector-updated", performTests);
});
}
@ -95,21 +95,21 @@ function performTests()
function testNavigate(callback)
{
inspector.selection.setNode(parentDiv);
inspector.selection.setNode(parentDiv, "test");
inspector.once("inspector-updated", () => {
// make sure it's still on after naving to parent
is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
"pseudo-class lock is still applied after inspecting ancestor");
inspector.selection.setNode(div2);
inspector.selection.setNode(div2, "test");
inspector.selection.once("pseudoclass", () => {
// make sure it's removed after naving to a non-hierarchy node
is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
"pseudo-class lock is removed after inspecting sibling node");
// toggle it back on
inspector.selection.setNode(div);
inspector.selection.setNode(div, "test");
inspector.once("inspector-updated", () => {
inspector.togglePseudoClass(pseudo);
inspector.once("computed-view-refreshed", callback);
@ -144,7 +144,7 @@ function testAdded(cb)
// infobar selector contains pseudo-class
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
cb();
inspector.toolbox.highlighter.hideBoxModel().then(cb);
});
}
@ -168,7 +168,7 @@ function testRemovedFromUI(cb)
showPickerOn(div, () => {
let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
cb();
inspector.toolbox.highlighter.hideBoxModel().then(cb);
});
}

View File

@ -282,6 +282,23 @@ MarkupView.prototype = {
}
},
/**
* Given the known reason, should the current selection be briefly highlighted
* In a few cases, we don't want to highlight the node:
* - If the reason is null (used to reset the selection),
* - if it's "inspector-open" (when the inspector opens up, let's not highlight
* the default node)
* - if it's "navigateaway" (since the page is being navigated away from)
* - if it's "test" (this is a special case for mochitest. In tests, we often
* need to select elements but don't necessarily want the highlighter to come
* and go after a delay as this might break test scenarios)
*/
_shouldNewSelectionBeHighlighted: function() {
let reason = this._inspector.selection.reason;
let unwantedReasons = ["inspector-open", "navigateaway", "test"];
return reason && unwantedReasons.indexOf(reason) === -1;
},
/**
* Highlight the inspector selected node.
*/
@ -291,8 +308,7 @@ MarkupView.prototype = {
this.htmlEditor.hide();
let done = this._inspector.updating("markup-view");
if (selection.isNode()) {
let reason = selection.reason;
if (reason && reason !== "inspector-open" && reason !== "navigateaway") {
if (this._shouldNewSelectionBeHighlighted()) {
this._brieflyShowBoxModel(selection.nodeFront, {
scrollIntoView: true
});

View File

@ -1490,14 +1490,10 @@ var Scratchpad = {
value: initialText,
lineNumbers: true,
showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
enableCodeFolding: Services.prefs.getBoolPref(ENABLE_CODE_FOLDING),
contextMenu: "scratchpad-text-popup"
};
if (Services.prefs.getBoolPref(ENABLE_CODE_FOLDING)) {
config.foldGutter = true;
config.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
}
this.editor = new Editor(config);
this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => {
var lines = initialText.split("\n");

View File

@ -3682,6 +3682,7 @@ Editable.prototype = {
*/
activate: function(e) {
if (!this.shouldActivate) {
this._onCleanup && this._onCleanup();
return;
}
@ -3739,6 +3740,7 @@ Editable.prototype = {
this._variable.locked = false;
this._variable.twisty = this._prevExpandable;
this._variable.expanded = this._prevExpanded;
this._onCleanup && this._onCleanup();
},
/**
@ -3867,9 +3869,6 @@ EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, {
let valueEditable = EditableValue.create(this._variable, {
onSave: aValue => {
this._onSave([key, aValue]);
},
onCleanup: () => {
this._onCleanup();
}
});
valueEditable._reset = () => {

View File

@ -17,11 +17,19 @@ To confirm the functionality run mochitests for the following components:
* styleditor
* netmonitor
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). The search addon (search.js)
was slightly modified to make search UI localizable. Other than that,
we don't have any Mozilla-specific patches applied to CodeMirror itself.
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). Be careful when updating
files test/codemirror.html and test/vimemacs.html; they were modified to
co-exist with Mozilla's testing infrastructure.
* In cm_comment_test.js comment out fallbackToBlock and fallbackToLine
tests.
* The search addon (search.js) was slightly modified to make search
UI localizable.
Other than that, we don't have any Mozilla-specific patches applied to
CodeMirror itself.
# Addons
@ -37,6 +45,7 @@ in the LICENSE file:
* codemirror.css
* codemirror.js
* comment.js
* activeline.js
* dialog/dialog.css
* dialog/dialog.js
* keymap/emacs.js
@ -50,6 +59,7 @@ in the LICENSE file:
* css.js
* javascript.js
* clike.js
* htmlmixed.js
* matchbrackets.js
* closebrackets.js
* trailingspace.js
@ -62,6 +72,8 @@ in the LICENSE file:
* test/cm_mode_javascript_test.js
* test/cm_mode_test.css
* test/cm_mode_test.js
* test/cm_vim_test.js
* test/cm_emacs_test.js
* test/cm_test.js
# Footnotes

View File

@ -1,22 +1,21 @@
/* 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/. */
// Because sometimes you need to style the cursor's line.
//
// Adds an option 'styleActiveLine' which, when enabled, gives the
// active line's wrapping <div> the CSS class "CodeMirror-activeline",
// and gives its background <div> the class "CodeMirror-activeline-background".
(function () {
(function() {
"use strict";
const WRAP_CLASS = "CodeMirror-activeline";
const BACK_CLASS = "CodeMirror-activeline-background";
var WRAP_CLASS = "CodeMirror-activeline";
var BACK_CLASS = "CodeMirror-activeline-background";
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
var prev = old && old != CodeMirror.Init;
if (val && !prev) {
updateActiveLine(cm);
cm.on("cursorActivity", updateActiveLine);
updateActiveLine(cm, cm.getCursor().line);
cm.on("beforeSelectionChange", selectionChange);
} else if (!val && prev) {
cm.off("cursorActivity", updateActiveLine);
cm.off("beforeSelectionChange", selectionChange);
clearActiveLine(cm);
delete cm.state.activeLine;
}
@ -29,12 +28,18 @@
}
}
function updateActiveLine(cm) {
var line = cm.getLineHandleVisualStart(cm.getCursor().line);
function updateActiveLine(cm, selectedLine) {
var line = cm.getLineHandleVisualStart(selectedLine);
if (cm.state.activeLine == line) return;
clearActiveLine(cm);
cm.addLineClass(line, "wrap", WRAP_CLASS);
cm.addLineClass(line, "background", BACK_CLASS);
cm.state.activeLine = line;
cm.operation(function() {
clearActiveLine(cm);
cm.addLineClass(line, "wrap", WRAP_CLASS);
cm.addLineClass(line, "background", BACK_CLASS);
cm.state.activeLine = line;
});
}
})();
function selectionChange(cm, sel) {
updateActiveLine(cm, sel.head.line);
}
})();

View File

@ -203,18 +203,34 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
return "string";
}
function mimes(ms, mode) {
for (var i = 0; i < ms.length; ++i) CodeMirror.defineMIME(ms[i], mode);
function def(mimes, mode) {
var words = [];
function add(obj) {
if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
words.push(prop);
}
add(mode.keywords);
add(mode.builtin);
add(mode.atoms);
if (words.length) {
mode.helperType = mimes[0];
CodeMirror.registerHelper("hintWords", mimes[0], words);
}
for (var i = 0; i < mimes.length; ++i)
CodeMirror.defineMIME(mimes[i], mode);
}
mimes(["text/x-csrc", "text/x-c", "text/x-chdr"], {
def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
name: "clike",
keywords: words(cKeywords),
blockKeywords: words("case do else for if switch while struct"),
atoms: words("null"),
hooks: {"#": cppHook}
hooks: {"#": cppHook},
modeProps: {fold: ["brace", "include"]}
});
mimes(["text/x-c++src", "text/x-c++hdr"], {
def(["text/x-c++src", "text/x-c++hdr"], {
name: "clike",
keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try bool explicit new " +
"static_cast typeid catch operator template typename class friend private " +
@ -222,7 +238,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
"wchar_t"),
blockKeywords: words("catch class do else finally for if struct switch try while"),
atoms: words("true false null"),
hooks: {"#": cppHook}
hooks: {"#": cppHook},
modeProps: {fold: ["brace", "include"]}
});
CodeMirror.defineMIME("text/x-java", {
name: "clike",
@ -238,7 +255,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
stream.eatWhile(/[\w\$_]/);
return "meta";
}
}
},
modeProps: {fold: ["brace", "import"]}
});
CodeMirror.defineMIME("text/x-csharp", {
name: "clike",
@ -303,7 +321,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
}
}
});
mimes(["x-shader/x-vertex", "x-shader/x-fragment"], {
def(["x-shader/x-vertex", "x-shader/x-fragment"], {
name: "clike",
keywords: words("float int bool void " +
"vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
@ -357,6 +375,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
"gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
"gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
"gl_MaxDrawBuffers"),
hooks: {"#": cppHook}
hooks: {"#": cppHook},
modeProps: {fold: ["brace", "include"]}
});
}());

View File

@ -28,7 +28,7 @@
var map = {
name : "autoCloseBrackets",
Backspace: function(cm) {
if (cm.somethingSelected()) return CodeMirror.Pass;
if (cm.somethingSelected() || cm.getOption("disableInput")) return CodeMirror.Pass;
var cur = cm.getCursor(), around = charsAround(cm, cur);
if (around && pairs.indexOf(around) % 2 == 0)
cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1));
@ -49,7 +49,8 @@
else cm.execCommand("goCharRight");
}
map["'" + left + "'"] = function(cm) {
if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment")
if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment" ||
cm.getOption("disableInput"))
return CodeMirror.Pass;
if (cm.somethingSelected()) return surround(cm);
if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return;
@ -70,7 +71,8 @@
function buildExplodeHandler(pairs) {
return function(cm) {
var cur = cm.getCursor(), around = charsAround(cm, cur);
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
if (!around || pairs.indexOf(around) % 2 != 0 || cm.getOption("disableInput"))
return CodeMirror.Pass;
cm.operation(function() {
var newPos = CodeMirror.Pos(cur.line + 1, 0);
cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input");

View File

@ -61,7 +61,7 @@
/* DEFAULT THEME */
.cm-s-default .cm-keyword {color: #708;}
.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;}

View File

@ -1,4 +1,4 @@
// CodeMirror version 3.20
// CodeMirror version 3.21
//
// CodeMirror is the only global var we claim
window.CodeMirror = (function() {
@ -12,10 +12,11 @@ window.CodeMirror = (function() {
// IE11 currently doesn't count as 'ie', since it has almost none of
// the same bugs as earlier versions. Use ie_gt10 to handle
// incompatibilities in that version.
var ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8);
var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
var old_ie = /MSIE \d/.test(navigator.userAgent);
var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8);
var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9);
var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent);
var ie = old_ie || ie_gt10;
var webkit = /WebKit\//.test(navigator.userAgent);
var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
var chrome = /Chrome\//.test(navigator.userAgent);
@ -37,7 +38,7 @@ window.CodeMirror = (function() {
if (opera_version && opera_version >= 15) { opera = false; webkit = true; }
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
var captureMiddleClick = gecko || (ie && !ie_lt9);
var captureMiddleClick = gecko || (old_ie && !ie_lt9);
// Optimize some code when these features are not used
var sawReadOnlySpans = false, sawCollapsedSpans = false;
@ -63,7 +64,8 @@ window.CodeMirror = (function() {
overlays: [],
modeGen: 0,
overwrite: false, focused: false,
suppressEdits: false, pasteIncoming: false,
suppressEdits: false,
pasteIncoming: false, cutIncoming: false,
draggingText: false,
highlight: new Delayed()};
@ -77,7 +79,7 @@ window.CodeMirror = (function() {
// Override magic textarea content restore that IE sometimes does
// on our hidden textarea on reload
if (ie) setTimeout(bind(resetInput, this, true), 20);
if (old_ie) setTimeout(bind(resetInput, this, true), 20);
registerEventHandlers(this);
// IE throws unspecified error in certain cases, when
@ -197,6 +199,10 @@ window.CodeMirror = (function() {
function loadMode(cm) {
cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
resetModeState(cm);
}
function resetModeState(cm) {
cm.doc.iter(function(line) {
if (line.stateAfter) line.stateAfter = null;
if (line.styles) line.styles = null;
@ -246,7 +252,6 @@ window.CodeMirror = (function() {
var map = keyMap[cm.options.keyMap], style = map.style;
cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
(style ? " cm-keymap-" + style : "");
cm.state.disableInput = map.disableInput;
}
function themeChanged(cm) {
@ -452,7 +457,7 @@ window.CodeMirror = (function() {
// updates.
function updateDisplayInner(cm, changes, visible, forced) {
var display = cm.display, doc = cm.doc;
if (!display.wrapper.clientWidth) {
if (!display.wrapper.offsetWidth) {
display.showingFrom = display.showingTo = doc.first;
display.viewOffset = 0;
return;
@ -537,6 +542,7 @@ window.CodeMirror = (function() {
}
display.showingFrom = from; display.showingTo = to;
display.gutters.style.height = "";
updateHeightsInViewport(cm);
updateViewOffset(cm);
@ -719,9 +725,9 @@ window.CodeMirror = (function() {
if (bgClass)
wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild);
if (cm.options.lineNumbers || markers) {
var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " +
var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " +
(cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"),
wrap.firstChild);
lineElement);
if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap);
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
wrap.lineNumber = gutterWrap.appendChild(
@ -1055,7 +1061,7 @@ window.CodeMirror = (function() {
// doesn't work when wrapping is on, but in that case the
// situation is slightly better, since IE does cache line-wrapping
// information and only recomputes per-line.
if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
var fragment = document.createDocumentFragment();
var chunk = 10, n = pre.childNodes.length;
for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) {
@ -1115,7 +1121,7 @@ window.CodeMirror = (function() {
}
}
if (!rect) rect = data[i] = measureRect(getRect(node));
if (cur.measureRight) rect.right = getRect(cur.measureRight).left;
if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left;
if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide));
}
removeChildren(cm.display.measure);
@ -1291,7 +1297,7 @@ window.CodeMirror = (function() {
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
var ch = x < fromX || x - fromX <= toX - x ? from : to;
var xDiff = x - (ch == from ? fromX : toX);
while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch;
while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
xDiff < 0 ? -1 : xDiff ? 1 : 0);
return pos;
@ -1483,7 +1489,7 @@ window.CodeMirror = (function() {
// supported or compatible enough yet to rely on.)
function readInput(cm) {
var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.state.disableInput) return false;
if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false;
if (cm.state.pasteIncoming && cm.state.fakedLastChar) {
input.value = input.value.substring(0, input.value.length - 1);
cm.state.fakedLastChar = false;
@ -1501,22 +1507,32 @@ window.CodeMirror = (function() {
var same = 0, l = Math.min(prevInput.length, text.length);
while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
var from = sel.from, to = sel.to;
var inserted = text.slice(same);
if (same < prevInput.length)
from = Pos(from.line, from.ch - (prevInput.length - same));
else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming)
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same)));
to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + inserted.length));
var updateInput = cm.curOp.updateInput;
var changeEvent = {from: from, to: to, text: splitLines(text.slice(same)),
origin: cm.state.pasteIncoming ? "paste" : "+input"};
var changeEvent = {from: from, to: to, text: splitLines(inserted),
origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"};
makeChange(cm.doc, changeEvent, "end");
cm.curOp.updateInput = updateInput;
signalLater(cm, "inputRead", cm, changeEvent);
if (inserted && !cm.state.pasteIncoming && cm.options.electricChars &&
cm.options.smartIndent && sel.head.ch < 100) {
var electric = cm.getModeAt(sel.head).electricChars;
if (electric) for (var i = 0; i < electric.length; i++)
if (inserted.indexOf(electric.charAt(i)) > -1) {
indentLine(cm, sel.head.line, "smart");
break;
}
}
if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = "";
else cm.display.prevInput = text;
if (withOp) endOperation(cm);
cm.state.pasteIncoming = false;
cm.state.pasteIncoming = cm.state.cutIncoming = false;
return true;
}
@ -1551,7 +1567,7 @@ window.CodeMirror = (function() {
function registerEventHandlers(cm) {
var d = cm.display;
on(d.scroller, "mousedown", operation(cm, onMouseDown));
if (ie)
if (old_ie)
on(d.scroller, "dblclick", operation(cm, function(e) {
if (signalDOMEvent(cm, e)) return;
var pos = posFromMouse(cm, e);
@ -1657,21 +1673,22 @@ window.CodeMirror = (function() {
fastPoll(cm);
});
function prepareCopy() {
function prepareCopy(e) {
if (d.inaccurateSelection) {
d.prevInput = "";
d.inaccurateSelection = false;
d.input.value = cm.getSelection();
selectInput(d.input);
}
if (e.type == "cut") cm.state.cutIncoming = true;
}
on(d.input, "cut", prepareCopy);
on(d.input, "copy", prepareCopy);
// Needed to handle Tab key in KHTML
if (khtml) on(d.sizer, "mouseup", function() {
if (document.activeElement == d.input) d.input.blur();
focusInput(cm);
if (document.activeElement == d.input) d.input.blur();
focusInput(cm);
});
}
@ -1755,6 +1772,9 @@ window.CodeMirror = (function() {
e_preventDefault(e2);
extendSelection(cm.doc, start);
focusInput(cm);
// Work around unexplainable focus problem in IE9 (#2127)
if (old_ie && !ie_lt9)
setTimeout(function() {document.body.focus(); focusInput(cm);}, 20);
}
});
// Let the drag handler handle this.
@ -1829,7 +1849,7 @@ window.CodeMirror = (function() {
}
var move = operation(cm, function(e) {
if (!ie && !e_button(e)) done(e);
if (!old_ie && !e_button(e)) done(e);
else extend(e);
});
var up = operation(cm, done);
@ -1974,7 +1994,7 @@ window.CodeMirror = (function() {
// know one. These don't have to be accurate -- the result of them
// being wrong would just be a slight flicker on the first wheel
// scroll (if it is large enough).
if (ie) wheelPixelsPerUnit = -.53;
if (old_ie) wheelPixelsPerUnit = -.53;
else if (gecko) wheelPixelsPerUnit = 15;
else if (chrome) wheelPixelsPerUnit = -.7;
else if (safari) wheelPixelsPerUnit = -1/3;
@ -2128,7 +2148,7 @@ window.CodeMirror = (function() {
var cm = this;
if (!cm.state.focused) onFocus(cm);
if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
if (ie && e.keyCode == 27) e.returnValue = false;
if (old_ie && e.keyCode == 27) e.returnValue = false;
var code = e.keyCode;
// IE does strange things with escape.
cm.doc.sel.shift = code == 16 || e.shiftKey;
@ -2149,10 +2169,6 @@ window.CodeMirror = (function() {
if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
if (this.options.electricChars && this.doc.mode.electricChars &&
this.options.smartIndent && !isReadOnly(this) &&
this.doc.mode.electricChars.indexOf(ch) > -1)
setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75);
if (handleCharBinding(cm, e, ch)) return;
if (ie && !ie_lt9) cm.display.inputHasSelection = null;
fastPoll(cm);
@ -2201,7 +2217,7 @@ window.CodeMirror = (function() {
var oldCSS = display.input.style.cssText;
display.inputDiv.style.position = "absolute";
display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" +
"px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" +
"border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
focusInput(cm);
resetInput(cm, true);
@ -2223,10 +2239,10 @@ window.CodeMirror = (function() {
// Try to detect the user choosing select-all
if (display.input.selectionStart != null) {
if (!ie || ie_lt9) prepareSelectAllHack();
if (!old_ie || ie_lt9) prepareSelectAllHack();
clearTimeout(detectingSelectAll);
var i = 0, poll = function(){
if (display.prevInput == " " && display.input.selectionStart == 0)
if (display.prevInput == "\u200b" && display.input.selectionStart == 0)
operation(cm, commands.selectAll)(cm);
else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
else resetInput(cm);
@ -2235,7 +2251,7 @@ window.CodeMirror = (function() {
}
}
if (ie && !ie_lt9) prepareSelectAllHack();
if (old_ie && !ie_lt9) prepareSelectAllHack();
if (captureMiddleClick) {
e_stop(e);
var mouseup = function() {
@ -2508,6 +2524,7 @@ window.CodeMirror = (function() {
function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
function cmp(a, b) {return a.line - b.line || a.ch - b.ch;}
function copyPos(x) {return Pos(x.line, x.ch);}
// SELECTION
@ -2652,14 +2669,13 @@ window.CodeMirror = (function() {
if (coords.top + box.top < 0) doScroll = true;
else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
if (doScroll != null && !phantom) {
var hidden = display.cursor.style.display == "none";
if (hidden) {
display.cursor.style.display = "";
display.cursor.style.left = coords.left + "px";
display.cursor.style.top = (coords.top - display.viewOffset) + "px";
}
display.cursor.scrollIntoView(doScroll);
if (hidden) display.cursor.style.display = "none";
var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
(coords.top - display.viewOffset) + "px; height: " +
(coords.bottom - coords.top + scrollerCutOff) + "px; left: " +
coords.left + "px; width: 2px;");
cm.display.lineSpace.appendChild(scrollNode);
scrollNode.scrollIntoView(doScroll);
cm.display.lineSpace.removeChild(scrollNode);
}
}
@ -2742,7 +2758,10 @@ window.CodeMirror = (function() {
var tabSize = cm.options.tabSize;
var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
var curSpaceString = line.text.match(/^\s*/)[0], indentation;
if (how == "smart") {
if (!aggressive && !/\S/.test(line.text)) {
indentation = 0;
how = "not";
} else if (how == "smart") {
indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
if (indentation == Pass) {
if (!aggressive) return;
@ -2924,7 +2943,7 @@ window.CodeMirror = (function() {
}),
indentSelection: operation(null, function(how) {
var sel = this.doc.sel;
if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how);
if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how, true);
var e = sel.to.line - (sel.to.ch ? 0 : 1);
for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how);
}),
@ -2969,11 +2988,31 @@ window.CodeMirror = (function() {
},
getHelper: function(pos, type) {
if (!helpers.hasOwnProperty(type)) return;
return this.getHelpers(pos, type)[0];
},
getHelpers: function(pos, type) {
var found = [];
if (!helpers.hasOwnProperty(type)) return helpers;
var help = helpers[type], mode = this.getModeAt(pos);
return mode[type] && help[mode[type]] ||
mode.helperType && help[mode.helperType] ||
help[mode.name];
if (typeof mode[type] == "string") {
if (help[mode[type]]) found.push(help[mode[type]]);
} else if (mode[type]) {
for (var i = 0; i < mode[type].length; i++) {
var val = help[mode[type][i]];
if (val) found.push(val);
}
} else if (mode.helperType && help[mode.helperType]) {
found.push(help[mode.helperType]);
} else if (help[mode.name]) {
found.push(help[mode.name]);
}
for (var i = 0; i < help._global.length; i++) {
var cur = help._global[i];
if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
found.push(cur.val);
}
return found;
},
getStateAfter: function(line, precise) {
@ -3120,7 +3159,10 @@ window.CodeMirror = (function() {
triggerOnKeyDown: operation(null, onKeyDown),
execCommand: function(cmd) {return commands[cmd](this);},
execCommand: function(cmd) {
if (commands.hasOwnProperty(cmd))
return commands[cmd](this);
},
findPosH: function(from, amount, unit, visually) {
var dir = 1;
@ -3162,14 +3204,18 @@ window.CodeMirror = (function() {
},
moveV: operation(null, function(dir, unit) {
var sel = this.doc.sel;
var pos = cursorCoords(this, sel.head, "div");
if (sel.goalColumn != null) pos.left = sel.goalColumn;
var target = findPosV(this, pos, dir, unit);
if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
var sel = this.doc.sel, target, goal;
if (sel.shift || sel.extend || posEq(sel.from, sel.to)) {
var pos = cursorCoords(this, sel.head, "div");
if (sel.goalColumn != null) pos.left = sel.goalColumn;
target = findPosV(this, pos, dir, unit);
if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
goal = pos.left;
} else {
target = dir < 0 ? sel.from : sel.to;
}
extendSelection(this.doc, target, target, dir);
sel.goalColumn = pos.left;
if (goal != null) sel.goalColumn = goal;
}),
toggleOverwrite: function(value) {
@ -3279,7 +3325,7 @@ window.CodeMirror = (function() {
option("indentWithTabs", false);
option("smartIndent", true);
option("tabSize", 4, function(cm) {
loadMode(cm);
resetModeState(cm);
clearCaches(cm);
regChange(cm);
}, true);
@ -3332,6 +3378,7 @@ window.CodeMirror = (function() {
if (!val) resetInput(cm, true);
}
});
option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true);
option("dragDrop", true);
option("cursorBlinkRate", 530);
@ -3339,12 +3386,13 @@ window.CodeMirror = (function() {
option("cursorHeight", 1);
option("workTime", 100);
option("workDelay", 100);
option("flattenSpans", true);
option("flattenSpans", true, resetModeState, true);
option("addModeClass", false, resetModeState, true);
option("pollInterval", 100);
option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;});
option("historyEventDelay", 500);
option("viewportMargin", 10, function(cm){cm.refresh();}, true);
option("maxHighlightLength", 10000, function(cm){loadMode(cm); cm.refresh();}, true);
option("maxHighlightLength", 10000, resetModeState, true);
option("crudeMeasuringFrom", 10000);
option("moveInputWithCursor", true, function(cm, val) {
if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0;
@ -3401,6 +3449,9 @@ window.CodeMirror = (function() {
}
}
modeObj.name = spec.name;
if (spec.helperType) modeObj.helperType = spec.helperType;
if (spec.modeProps) for (var prop in spec.modeProps)
modeObj[prop] = spec.modeProps[prop];
return modeObj;
};
@ -3431,9 +3482,13 @@ window.CodeMirror = (function() {
var helpers = CodeMirror.helpers = {};
CodeMirror.registerHelper = function(type, name, value) {
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {};
if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
helpers[type][name] = value;
};
CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
CodeMirror.registerHelper(type, name, value);
helpers[type]._global.push({pred: predicate, val: value});
};
// UTILITIES
@ -3538,7 +3593,9 @@ window.CodeMirror = (function() {
indentAuto: function(cm) {cm.indentSelection("smart");},
indentMore: function(cm) {cm.indentSelection("add");},
indentLess: function(cm) {cm.indentSelection("subtract");},
insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");},
insertTab: function(cm) {
cm.replaceSelection("\t", "end", "+input");
},
defaultTab: function(cm) {
if (cm.somethingSelected()) cm.indentSelection("add");
else cm.replaceSelection("\t", "end", "+input");
@ -3711,11 +3768,12 @@ window.CodeMirror = (function() {
this.string = string;
this.tabSize = tabSize || 8;
this.lastColumnPos = this.lastColumnValue = 0;
this.lineStart = 0;
}
StringStream.prototype = {
eol: function() {return this.pos >= this.string.length;},
sol: function() {return this.pos == 0;},
sol: function() {return this.pos == this.lineStart;},
peek: function() {return this.string.charAt(this.pos) || undefined;},
next: function() {
if (this.pos < this.string.length)
@ -3748,9 +3806,12 @@ window.CodeMirror = (function() {
this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
this.lastColumnPos = this.start;
}
return this.lastColumnValue;
return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
},
indentation: function() {
return countColumn(this.string, null, this.tabSize) -
(this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
},
indentation: function() {return countColumn(this.string, null, this.tabSize);},
match: function(pattern, consume, caseInsensitive) {
if (typeof pattern == "string") {
var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
@ -3766,7 +3827,12 @@ window.CodeMirror = (function() {
return match;
}
},
current: function(){return this.string.slice(this.start, this.pos);}
current: function(){return this.string.slice(this.start, this.pos);},
hideFirstChars: function(n, inner) {
this.lineStart += n;
try { return inner(); }
finally { this.lineStart -= n; }
}
};
CodeMirror.StringStream = StringStream;
@ -3818,7 +3884,7 @@ window.CodeMirror = (function() {
if (withOp) endOperation(cm);
};
TextMarker.prototype.find = function() {
TextMarker.prototype.find = function(bothSides) {
var from, to;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
@ -3829,7 +3895,7 @@ window.CodeMirror = (function() {
if (span.to != null) to = Pos(found, span.to);
}
}
if (this.type == "bookmark") return from;
if (this.type == "bookmark" && !bothSides) return from;
return from && {from: from, to: to};
};
@ -3866,39 +3932,40 @@ window.CodeMirror = (function() {
}
};
var nextMarkerId = 0;
function markText(doc, from, to, options, type) {
if (options && options.shared) return markTextShared(doc, from, to, options, type);
if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
var marker = new TextMarker(doc, type);
if (posLess(to, from) || posEq(from, to) && type == "range" &&
!(options.inclusiveLeft && options.inclusiveRight))
return marker;
if (options) copyObj(options, marker);
if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false)
return marker;
if (marker.replacedWith) {
marker.collapsed = true;
marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget");
if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true;
}
if (marker.collapsed) sawCollapsedSpans = true;
if (marker.collapsed) {
if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
throw new Error("Inserting collapsed marker partially overlapping an existing one");
sawCollapsedSpans = true;
}
if (marker.addToHistory)
addToHistory(doc, {from: from, to: to, origin: "markText"},
{head: doc.sel.head, anchor: doc.sel.anchor}, NaN);
var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine;
var curLine = from.line, cm = doc.cm, updateMaxLine;
doc.iter(curLine, to.line + 1, function(line) {
if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine)
updateMaxLine = true;
var span = {from: null, to: null, marker: marker};
size += line.text.length;
if (curLine == from.line) {span.from = from.ch; size -= from.ch;}
if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;}
if (marker.collapsed) {
if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch);
if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch);
else updateLineHeight(line, 0);
}
if (curLine == from.line) span.from = from.ch;
if (curLine == to.line) span.to = to.ch;
if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
addMarkedSpan(line, span);
++curLine;
});
@ -3914,9 +3981,7 @@ window.CodeMirror = (function() {
doc.clearHistory();
}
if (marker.collapsed) {
if (collapsedAtStart != collapsedAtEnd)
throw new Error("Inserting collapsed marker overlapping an existing one");
marker.size = size;
marker.id = ++nextMarkerId;
marker.atomic = true;
}
if (cm) {
@ -3989,9 +4054,7 @@ window.CodeMirror = (function() {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
if (startsBefore ||
(marker.inclusiveLeft && marker.inclusiveRight || marker.type == "bookmark") &&
span.from == startCh && (!isInsert || !span.marker.insertLeft)) {
if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
(nw || (nw = [])).push({from: span.from,
to: endsAfter ? null : span.to,
@ -4005,7 +4068,7 @@ window.CodeMirror = (function() {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) {
if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
(nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
to: span.to == null ? null : span.to - endCh,
@ -4055,13 +4118,9 @@ window.CodeMirror = (function() {
}
}
}
if (sameLine && first) {
// Make sure we didn't create any zero-length spans
for (var i = 0; i < first.length; ++i)
if (first[i].from != null && first[i].from == first[i].to && first[i].marker.type != "bookmark")
first.splice(i--, 1);
if (!first.length) first = null;
}
// Make sure we didn't create any zero-length spans
if (first) first = clearEmptySpans(first);
if (last && last != first) last = clearEmptySpans(last);
var newMarkers = [first];
if (!sameLine) {
@ -4078,6 +4137,16 @@ window.CodeMirror = (function() {
return newMarkers;
}
function clearEmptySpans(spans) {
for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
spans.splice(i--, 1);
}
if (!spans.length) return null;
return spans;
}
function mergeOldSpans(doc, change) {
var old = getOldSpans(doc, change);
var stretched = stretchSpansOverChange(doc, change);
@ -4128,20 +4197,48 @@ window.CodeMirror = (function() {
return parts;
}
function collapsedSpanAt(line, ch) {
function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
function compareCollapsedMarkers(a, b) {
var lenDiff = a.lines.length - b.lines.length;
if (lenDiff != 0) return lenDiff;
var aPos = a.find(), bPos = b.find();
var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
if (fromCmp) return -fromCmp;
var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
if (toCmp) return toCmp;
return b.id - a.id;
}
function collapsedSpanAtSide(line, start) {
var sps = sawCollapsedSpans && line.markedSpans, found;
if (sps) for (var sp, i = 0; i < sps.length; ++i) {
sp = sps[i];
if (!sp.marker.collapsed) continue;
if ((sp.from == null || sp.from < ch) &&
(sp.to == null || sp.to > ch) &&
(!found || found.width < sp.marker.width))
if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
(!found || compareCollapsedMarkers(found, sp.marker) < 0))
found = sp.marker;
}
return found;
}
function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); }
function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); }
function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
var line = getLine(doc, lineNo);
var sps = sawCollapsedSpans && line.markedSpans;
if (sps) for (var i = 0; i < sps.length; ++i) {
var sp = sps[i];
if (!sp.marker.collapsed) continue;
var found = sp.marker.find(true);
var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
if (fromCmp <= 0 && (cmp(found.to, from) || extraRight(sp.marker) - extraLeft(marker)) > 0 ||
fromCmp >= 0 && (cmp(found.from, to) || extraLeft(sp.marker) - extraRight(marker)) < 0)
return true;
}
}
function visualLine(doc, line) {
var merged;
@ -4171,6 +4268,7 @@ window.CodeMirror = (function() {
for (var sp, i = 0; i < line.markedSpans.length; ++i) {
sp = line.markedSpans[i];
if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to &&
(sp.to == null || sp.to != span.from) &&
(sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
lineIsHiddenInner(doc, line, sp)) return true;
}
@ -4300,6 +4398,10 @@ window.CodeMirror = (function() {
} else {
style = mode.token(stream, state);
}
if (cm.options.addModeClass) {
var mName = CodeMirror.innerMode(mode, state).mode.name;
if (mName) style = "m-" + (style ? mName + " " + style : mName);
}
if (!flattenSpans || curStyle != style) {
if (curStart < stream.start) f(stream.start, curStyle);
curStart = stream.start; curStyle = style;
@ -4371,7 +4473,7 @@ window.CodeMirror = (function() {
}
}
var styleToClassCache = {};
var styleToClassCache = {}, styleToClassCacheWithMode = {};
function interpretTokenStyle(style, builder) {
if (!style) return null;
for (;;) {
@ -4384,8 +4486,9 @@ window.CodeMirror = (function() {
else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop]))
builder[prop] += " " + lineClass[2];
}
return styleToClassCache[style] ||
(styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-"));
var cache = builder.cm.options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
return cache[style] ||
(cache[style] = "cm-" + style.replace(/ +/g, " cm-"));
}
function buildLineContent(cm, realLine, measure, copyWidgets) {
@ -4402,7 +4505,7 @@ window.CodeMirror = (function() {
builder.measure = line == realLine && measure;
builder.pos = 0;
builder.addToken = builder.measure ? buildTokenMeasure : buildToken;
if ((ie || webkit) && cm.getOption("lineWrapping"))
if ((old_ie || webkit) && cm.getOption("lineWrapping"))
builder.addToken = buildTokenSplitSpaces(builder.addToken);
var next = insertLineContent(line, builder, getLineStyles(cm, line));
if (measure && line == realLine && !builder.measuredSomething) {
@ -4421,7 +4524,7 @@ window.CodeMirror = (function() {
// Work around problem with the reported dimensions of single-char
// direction spans on IE (issue #1129). See also the comment in
// cursorCoords.
if (measure && (ie || ie_gt10) && (order = getOrder(line))) {
if (measure && ie && (order = getOrder(line))) {
var l = order.length - 1;
if (order[l].from == order[l].to) --l;
var last = order[l], prev = order[l - 1];
@ -4488,13 +4591,12 @@ window.CodeMirror = (function() {
function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
var wrapping = builder.cm.options.lineWrapping;
for (var i = 0; i < text.length; ++i) {
var ch = text.charAt(i), start = i == 0;
if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) {
ch = text.slice(i, i + 2);
++i;
} else if (i && wrapping && spanAffectsWrapping(text, i)) {
var start = i == 0, to = i + 1;
while (to < text.length && isExtendingChar(text.charAt(to))) ++to;
var ch = text.slice(i, to);
i = to - 1;
if (i && wrapping && spanAffectsWrapping(text, i))
builder.pre.appendChild(elt("wbr"));
}
var old = builder.measure[builder.pos];
var span = builder.measure[builder.pos] =
buildToken(builder, ch, style,
@ -4503,7 +4605,7 @@ window.CodeMirror = (function() {
// In IE single-space nodes wrap differently than spaces
// embedded in larger text nodes, except when set to
// white-space: normal (issue #1268).
if (ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
if (old_ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
i < text.length - 1 && !/\s/.test(text.charAt(i + 1)))
span.style.whiteSpace = "normal";
builder.pos += ch.length;
@ -4571,7 +4673,7 @@ window.CodeMirror = (function() {
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
if (m.title && !title) title = m.title;
if (m.collapsed && (!collapsed || collapsed.marker.size < m.size))
if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
collapsed = sp;
} else if (sp.from > pos && nextChange > sp.from) {
nextChange = sp.from;
@ -4904,10 +5006,11 @@ window.CodeMirror = (function() {
clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);},
markClean: function() {
this.cleanGeneration = this.changeGeneration();
this.cleanGeneration = this.changeGeneration(true);
},
changeGeneration: function() {
this.history.lastOp = this.history.lastOrigin = null;
changeGeneration: function(forceSplit) {
if (forceSplit)
this.history.lastOp = this.history.lastOrigin = null;
return this.history.generation;
},
isClean: function (gen) {
@ -4929,7 +5032,8 @@ window.CodeMirror = (function() {
},
setBookmark: function(pos, options) {
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
insertLeft: options && options.insertLeft};
insertLeft: options && options.insertLeft,
clearWhenEmpty: false};
pos = clipPos(this, pos);
return markText(this, pos, pos, realOpts, "bookmark");
},
@ -5210,10 +5314,10 @@ window.CodeMirror = (function() {
anchorBefore: doc.sel.anchor, headBefore: doc.sel.head,
anchorAfter: selAfter.anchor, headAfter: selAfter.head};
hist.done.push(cur);
hist.generation = ++hist.maxGeneration;
while (hist.done.length > hist.undoDepth)
hist.done.shift();
}
hist.generation = ++hist.maxGeneration;
hist.lastTime = time;
hist.lastOp = opId;
hist.lastOrigin = change.origin;
@ -5509,7 +5613,8 @@ window.CodeMirror = (function() {
return true;
}
var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\u1DC0\u1DFF\u20D0\u20FF\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff\uFE20\uFE2F]/;
var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
// DOM UTILITIES
@ -5582,7 +5687,7 @@ window.CodeMirror = (function() {
if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true;
if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false;
}
return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
};
var knownScrollbarWidth;
@ -5650,14 +5755,14 @@ window.CodeMirror = (function() {
var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete",
186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home",
63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"};
46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete",
173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"};
CodeMirror.keyNames = keyNames;
(function() {
// Number keys
for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
// Alphabetic keys
for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
// Function keys
@ -5714,29 +5819,29 @@ window.CodeMirror = (function() {
}
var bidiOther;
function getBidiPartAt(order, pos) {
bidiOther = null;
for (var i = 0, found; i < order.length; ++i) {
var cur = order[i];
if (cur.from < pos && cur.to > pos) { bidiOther = null; return i; }
if (cur.from == pos || cur.to == pos) {
if (cur.from < pos && cur.to > pos) return i;
if ((cur.from == pos || cur.to == pos)) {
if (found == null) {
found = i;
} else if (compareBidiLevel(order, cur.level, order[found].level)) {
bidiOther = found;
if (cur.from != cur.to) bidiOther = found;
return i;
} else {
bidiOther = i;
if (cur.from != cur.to) bidiOther = i;
return found;
}
}
}
bidiOther = null;
return found;
}
function moveInLine(line, pos, dir, byUnit) {
if (!byUnit) return pos + dir;
do pos += dir;
while (pos > 0 && isExtendingChar.test(line.text.charAt(pos)));
while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
return pos;
}
@ -5771,7 +5876,7 @@ window.CodeMirror = (function() {
function moveLogically(line, start, dir, byUnit) {
var target = start + dir;
if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir;
if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
return target < 0 || target > line.text.length ? null : target;
}
@ -5863,7 +5968,7 @@ window.CodeMirror = (function() {
if (type == ",") types[i] = "N";
else if (type == "%") {
for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N";
var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
}
@ -5888,7 +5993,7 @@ window.CodeMirror = (function() {
if (isNeutral.test(types[i])) {
for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
var before = (i ? types[i-1] : outerType) == "L";
var after = (end < len - 1 ? types[end] : outerType) == "L";
var after = (end < len ? types[end] : outerType) == "L";
var replace = before || after ? "L" : "R";
for (var j = i; j < end; ++j) types[j] = replace;
i = end - 1;
@ -5938,7 +6043,7 @@ window.CodeMirror = (function() {
// THE END
CodeMirror.version = "3.20.0";
CodeMirror.version = "3.21.0";
return CodeMirror;
})();

View File

@ -96,8 +96,9 @@
for (var i = start; i <= end; ++i) {
var line = self.getLine(i);
var found = line.indexOf(lineString);
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
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;
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
lines.push(line);
}
self.operation(function() {
@ -124,7 +125,10 @@
endLine = self.getLine(--end);
close = endLine.lastIndexOf(endString);
}
if (open == -1 || close == -1) return false;
if (open == -1 || close == -1 ||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
return false;
self.operation(function() {
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),

View File

@ -3,87 +3,80 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
var indentUnit = config.indentUnit || config.tabSize || 2,
hooks = parserConfig.hooks || {},
atMediaTypes = parserConfig.atMediaTypes || {},
atMediaFeatures = parserConfig.atMediaFeatures || {},
var indentUnit = config.indentUnit,
tokenHooks = parserConfig.tokenHooks,
mediaTypes = parserConfig.mediaTypes || {},
mediaFeatures = parserConfig.mediaFeatures || {},
propertyKeywords = parserConfig.propertyKeywords || {},
colorKeywords = parserConfig.colorKeywords || {},
valueKeywords = parserConfig.valueKeywords || {},
allowNested = !!parserConfig.allowNested,
type = null;
fontProperties = parserConfig.fontProperties || {},
allowNested = parserConfig.allowNested;
var type, override;
function ret(style, tp) { type = tp; return style; }
// Tokenizers
function tokenBase(stream, state) {
var ch = stream.next();
if (hooks[ch]) {
// result[0] is style and result[1] is type
var result = hooks[ch](stream, state);
if (tokenHooks[ch]) {
var result = tokenHooks[ch](stream, state);
if (result !== false) return result;
}
if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());}
else if (ch == "=") ret(null, "compare");
else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare");
else if (ch == "\"" || ch == "'") {
if (ch == "@") {
stream.eatWhile(/[\w\\\-]/);
return ret("def", stream.current());
} else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
return ret(null, "compare");
} else if (ch == "\"" || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
}
else if (ch == "#") {
} else if (ch == "#") {
stream.eatWhile(/[\w\\\-]/);
return ret("atom", "hash");
}
else if (ch == "!") {
} else if (ch == "!") {
stream.match(/^\s*\w*/);
return ret("keyword", "important");
}
else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
} else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
}
else if (ch === "-") {
if (/\d/.test(stream.peek())) {
} else if (ch === "-") {
if (/[\d.]/.test(stream.peek())) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (stream.match(/^[^-]+-/)) {
return ret("meta", "meta");
}
}
else if (/[,+>*\/]/.test(ch)) {
} else if (/[,+>*\/]/.test(ch)) {
return ret(null, "select-op");
}
else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
} else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
return ret("qualifier", "qualifier");
}
else if (ch == ":") {
return ret("operator", ch);
}
else if (/[;{}\[\]\(\)]/.test(ch)) {
} else if (/[:;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
}
else if (ch == "u" && stream.match("rl(")) {
} else if (ch == "u" && stream.match("rl(")) {
stream.backUp(1);
state.tokenize = tokenParenthesized;
return ret("property", "variable");
}
else {
return ret("property", "word");
} else if (/[\w\\\-]/.test(ch)) {
stream.eatWhile(/[\w\\\-]/);
return ret("property", "variable");
return ret("property", "word");
} else {
return ret(null, null);
}
}
function tokenString(quote, nonInclusive) {
function tokenString(quote) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
if (ch == quote && !escaped)
if (ch == quote && !escaped) {
if (quote == ")") stream.backUp(1);
break;
}
escaped = !escaped && ch == "\\";
}
if (!escaped) {
if (nonInclusive) stream.backUp(1);
state.tokenize = tokenBase;
}
if (ch == quote || !escaped && quote != ")") state.tokenize = null;
return ret("string", "string");
};
}
@ -91,218 +84,238 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
function tokenParenthesized(stream, state) {
stream.next(); // Must be '('
if (!stream.match(/\s*[\"\']/, false))
state.tokenize = tokenString(")", true);
state.tokenize = tokenString(")");
else
state.tokenize = tokenBase;
state.tokenize = null;
return ret(null, "(");
}
// Context management
function Context(type, indent, prev) {
this.type = type;
this.indent = indent;
this.prev = prev;
}
function pushContext(state, stream, type) {
state.context = new Context(type, stream.indentation() + indentUnit, state.context);
return type;
}
function popContext(state) {
state.context = state.context.prev;
return state.context.type;
}
function pass(type, stream, state) {
return states[state.context.type](type, stream, state);
}
function popAndPass(type, stream, state, n) {
for (var i = n || 1; i > 0; i--)
state.context = state.context.prev;
return pass(type, stream, state);
}
// Parser
function wordAsValue(stream) {
var word = stream.current().toLowerCase();
if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
override = "keyword";
else
override = "variable";
}
var states = {};
states.top = function(type, stream, state) {
if (type == "{") {
return pushContext(state, stream, "block");
} else if (type == "}" && state.context.prev) {
return popContext(state);
} else if (type == "@media") {
return pushContext(state, stream, "media");
} else if (type == "@font-face") {
return "font_face_before";
} else if (type && type.charAt(0) == "@") {
return pushContext(state, stream, "at");
} else if (type == "hash") {
override = "builtin";
} else if (type == "word") {
override = "tag";
} else if (type == "variable-definition") {
return "maybeprop";
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
} else if (type == ":") {
return "pseudo";
} else if (allowNested && type == "(") {
return pushContext(state, stream, "params");
}
return state.context.type;
};
states.block = function(type, stream, state) {
if (type == "word") {
if (propertyKeywords.hasOwnProperty(stream.current().toLowerCase())) {
override = "property";
return "maybeprop";
} else if (allowNested) {
override = stream.match(/^\s*:/, false) ? "property" : "tag";
return "block";
} else {
override += " error";
return "maybeprop";
}
} else if (type == "meta") {
return "block";
} else if (!allowNested && (type == "hash" || type == "qualifier")) {
override = "error";
return "block";
} else {
return states.top(type, stream, state);
}
};
states.maybeprop = function(type, stream, state) {
if (type == ":") return pushContext(state, stream, "prop");
return pass(type, stream, state);
};
states.prop = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
if (type == "}" || type == "{") return popAndPass(type, stream, state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "hash" && !/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
override += " error";
} else if (type == "word") {
wordAsValue(stream);
} else if (type == "interpolation") {
return pushContext(state, stream, "interpolation");
}
return "prop";
};
states.propBlock = function(type, _stream, state) {
if (type == "}") return popContext(state);
if (type == "word") { override = "property"; return "maybeprop"; }
return state.context.type;
};
states.parens = function(type, stream, state) {
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == ")") return popContext(state);
return "parens";
};
states.pseudo = function(type, stream, state) {
if (type == "word") {
override = "variable-3";
return state.context.type;
}
return pass(type, stream, state);
};
states.media = function(type, stream, state) {
if (type == "(") return pushContext(state, stream, "media_parens");
if (type == "}") return popAndPass(type, stream, state);
if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
if (type == "word") {
var word = stream.current().toLowerCase();
if (word == "only" || word == "not" || word == "and")
override = "keyword";
else if (mediaTypes.hasOwnProperty(word))
override = "attribute";
else if (mediaFeatures.hasOwnProperty(word))
override = "property";
else
override = "error";
}
return state.context.type;
};
states.media_parens = function(type, stream, state) {
if (type == ")") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
return states.media(type, stream, state);
};
states.font_face_before = function(type, stream, state) {
if (type == "{")
return pushContext(state, stream, "font_face");
return pass(type, stream, state);
};
states.font_face = function(type, stream, state) {
if (type == "}") return popContext(state);
if (type == "word") {
if (!fontProperties.hasOwnProperty(stream.current().toLowerCase()))
override = "error";
else
override = "property";
return "maybeprop";
}
return "font_face";
};
states.at = function(type, stream, state) {
if (type == ";") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == "word") override = "tag";
else if (type == "hash") override = "builtin";
return "at";
};
states.interpolation = function(type, stream, state) {
if (type == "}") return popContext(state);
if (type == "{" || type == ";") return popAndPass(type, stream, state);
if (type != "variable") override = "error";
return "interpolation";
};
states.params = function(type, stream, state) {
if (type == ")") return popContext(state);
if (type == "{" || type == "}") return popAndPass(type, stream, state);
if (type == "word") wordAsValue(stream);
return "params";
};
return {
startState: function(base) {
return {tokenize: tokenBase,
baseIndent: base || 0,
stack: [],
lastToken: null};
return {tokenize: null,
state: "top",
context: new Context("top", base || 0, null)};
},
token: function(stream, state) {
// Use these terms when applicable (see http://www.xanthir.com/blog/b4E50)
//
// rule** or **ruleset:
// A selector + braces combo, or an at-rule.
//
// declaration block:
// A sequence of declarations.
//
// declaration:
// A property + colon + value combo.
//
// property value:
// The entire value of a property.
//
// component value:
// A single piece of a property value. Like the 5px in
// text-shadow: 0 0 5px blue;. Can also refer to things that are
// multiple terms, like the 1-4 terms that make up the background-size
// portion of the background shorthand.
//
// term:
// The basic unit of author-facing CSS, like a single number (5),
// dimension (5px), string ("foo"), or function. Officially defined
// by the CSS 2.1 grammar (look for the 'term' production)
//
//
// simple selector:
// A single atomic selector, like a type selector, an attr selector, a
// class selector, etc.
//
// compound selector:
// One or more simple selectors without a combinator. div.example is
// compound, div > .example is not.
//
// complex selector:
// One or more compound selectors chained with combinators.
//
// combinator:
// The parts of selectors that express relationships. There are four
// currently - the space (descendant combinator), the greater-than
// bracket (child combinator), the plus sign (next sibling combinator),
// and the tilda (following sibling combinator).
//
// sequence of selectors:
// One or more of the named type of selector chained with commas.
state.tokenize = state.tokenize || tokenBase;
if (state.tokenize == tokenBase && stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (style && typeof style != "string") style = ret(style[0], style[1]);
// Changing style returned based on context
var context = state.stack[state.stack.length-1];
if (style == "variable") {
if (type == "variable-definition") state.stack.push("propertyValue");
return state.lastToken = "variable-2";
} else if (style == "property") {
var word = stream.current().toLowerCase();
if (context == "propertyValue") {
if (valueKeywords.hasOwnProperty(word)) {
style = "string-2";
} else if (colorKeywords.hasOwnProperty(word)) {
style = "keyword";
} else {
style = "variable-2";
}
} else if (context == "rule") {
if (!propertyKeywords.hasOwnProperty(word)) {
style += " error";
}
} else if (context == "block") {
// if a value is present in both property, value, or color, the order
// of preference is property -> color -> value
if (propertyKeywords.hasOwnProperty(word)) {
style = "property";
} else if (colorKeywords.hasOwnProperty(word)) {
style = "keyword";
} else if (valueKeywords.hasOwnProperty(word)) {
style = "string-2";
} else {
style = "tag";
}
} else if (!context || context == "@media{") {
style = "tag";
} else if (context == "@media") {
if (atMediaTypes[stream.current()]) {
style = "attribute"; // Known attribute
} else if (/^(only|not)$/.test(word)) {
style = "keyword";
} else if (word == "and") {
style = "error"; // "and" is only allowed in @mediaType
} else if (atMediaFeatures.hasOwnProperty(word)) {
style = "error"; // Known property, should be in @mediaType(
} else {
// Unknown, expecting keyword or attribute, assuming attribute
style = "attribute error";
}
} else if (context == "@mediaType") {
if (atMediaTypes.hasOwnProperty(word)) {
style = "attribute";
} else if (word == "and") {
style = "operator";
} else if (/^(only|not)$/.test(word)) {
style = "error"; // Only allowed in @media
} else {
// Unknown attribute or property, but expecting property (preceded
// by "and"). Should be in parentheses
style = "error";
}
} else if (context == "@mediaType(") {
if (propertyKeywords.hasOwnProperty(word)) {
// do nothing, remains "property"
} else if (atMediaTypes.hasOwnProperty(word)) {
style = "error"; // Known property, should be in parentheses
} else if (word == "and") {
style = "operator";
} else if (/^(only|not)$/.test(word)) {
style = "error"; // Only allowed in @media
} else {
style += " error";
}
} else if (context == "@import") {
style = "tag";
} else {
style = "error";
}
} else if (style == "atom") {
if(!context || context == "@media{" || context == "block") {
style = "builtin";
} else if (context == "propertyValue") {
if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
style += " error";
}
} else {
style = "error";
}
} else if (context == "@media" && type == "{") {
style = "error";
if (!state.tokenize && stream.eatSpace()) return null;
var style = (state.tokenize || tokenBase)(stream, state);
if (style && typeof style == "object") {
type = style[1];
style = style[0];
}
// Push/pop context stack
if (type == "{") {
if (context == "@media" || context == "@mediaType") {
state.stack[state.stack.length-1] = "@media{";
}
else {
var newContext = allowNested ? "block" : "rule";
state.stack.push(newContext);
}
}
else if (type == "}") {
if (context == "interpolation") style = "operator";
// Pop off end of array until { is reached
while(state.stack.length){
var removed = state.stack.pop();
if(removed.indexOf("{") > -1 || removed == "block" || removed == "rule"){
break;
}
}
}
else if (type == "interpolation") state.stack.push("interpolation");
else if (type == "@media") state.stack.push("@media");
else if (type == "@import") state.stack.push("@import");
else if (context == "@media" && /\b(keyword|attribute)\b/.test(style))
state.stack[state.stack.length-1] = "@mediaType";
else if (context == "@mediaType" && stream.current() == ",")
state.stack[state.stack.length-1] = "@media";
else if (type == "(") {
if (context == "@media" || context == "@mediaType") {
// Make sure @mediaType is used to avoid error on {
state.stack[state.stack.length-1] = "@mediaType";
state.stack.push("@mediaType(");
}
else state.stack.push("(");
}
else if (type == ")") {
// Pop off end of array until ( is reached
while(state.stack.length){
var removed = state.stack.pop();
if(removed.indexOf("(") > -1){
break;
}
}
}
else if (type == ":" && state.lastToken == "property") state.stack.push("propertyValue");
else if (context == "propertyValue" && type == ";") state.stack.pop();
else if (context == "@import" && type == ";") state.stack.pop();
return state.lastToken = style;
override = style;
state.state = states[state.state](type, stream, state);
return override;
},
indent: function(state, textAfter) {
var n = state.stack.length;
if (/^\}/.test(textAfter))
n -= state.stack[n-1] == "propertyValue" ? 2 : 1;
return state.baseIndent + n * indentUnit;
var cx = state.context, ch = textAfter && textAfter.charAt(0);
var indent = cx.indent;
if (cx.prev &&
(ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "font_face") ||
ch == ")" && (cx.type == "parens" || cx.type == "params" || cx.type == "media_parens") ||
ch == "{" && (cx.type == "at" || cx.type == "media"))) {
indent = cx.indent - indentUnit;
cx = cx.prev;
}
return indent;
},
electricChars: "}",
@ -321,12 +334,12 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
return keys;
}
var atMediaTypes = keySet([
var mediaTypes_ = [
"all", "aural", "braille", "handheld", "print", "projection", "screen",
"tty", "tv", "embossed"
]);
], mediaTypes = keySet(mediaTypes_);
var atMediaFeatures = keySet([
var mediaFeatures_ = [
"width", "min-width", "max-width", "height", "min-height", "max-height",
"device-width", "min-device-width", "max-device-width", "device-height",
"min-device-height", "max-device-height", "aspect-ratio",
@ -335,9 +348,9 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"max-color", "color-index", "min-color-index", "max-color-index",
"monochrome", "min-monochrome", "max-monochrome", "resolution",
"min-resolution", "max-resolution", "scan", "grid"
]);
], mediaFeatures = keySet(mediaFeatures_);
var propertyKeywords = keySet([
var propertyKeywords_ = [
"align-content", "align-items", "align-self", "alignment-adjust",
"alignment-baseline", "anchor-point", "animation", "animation-delay",
"animation-direction", "animation-duration", "animation-iteration-count",
@ -425,9 +438,9 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
"baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
"glyph-orientation-vertical", "kerning", "text-anchor", "writing-mode"
]);
], propertyKeywords = keySet(propertyKeywords_);
var colorKeywords = keySet([
var colorKeywords_ = [
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
@ -454,9 +467,9 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
"whitesmoke", "yellow", "yellowgreen"
]);
], colorKeywords = keySet(colorKeywords_);
var valueKeywords = keySet([
var valueKeywords_ = [
"above", "absolute", "activeborder", "activecaption", "afar",
"after-white-space", "ahead", "alias", "all", "all-scroll", "alternate",
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
@ -539,7 +552,15 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
"window", "windowframe", "windowtext", "x-large", "x-small", "xor",
"xx-large", "xx-small"
]);
], valueKeywords = keySet(valueKeywords_);
var fontProperties_ = [
"font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
"font-stretch", "font-weight", "font-style"
], fontProperties = keySet(fontProperties_);
var allWords = mediaTypes_.concat(mediaFeatures_).concat(propertyKeywords_).concat(colorKeywords_).concat(valueKeywords_);
CodeMirror.registerHelper("hintWords", "css", allWords);
function tokenCComment(stream, state) {
var maybeEnd = false, ch;
@ -553,67 +574,47 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
return ["comment", "comment"];
}
function tokenSGMLComment(stream, state) {
if (stream.skipTo("-->")) {
stream.match("-->");
state.tokenize = null;
} else {
stream.skipToEnd();
}
return ["comment", "comment"];
}
CodeMirror.defineMIME("text/css", {
atMediaTypes: atMediaTypes,
atMediaFeatures: atMediaFeatures,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
propertyKeywords: propertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
hooks: {
fontProperties: fontProperties,
tokenHooks: {
"<": function(stream, state) {
function tokenSGMLComment(stream, state) {
var dashes = 0, ch;
while ((ch = stream.next()) != null) {
if (dashes >= 2 && ch == ">") {
state.tokenize = null;
break;
}
dashes = (ch == "-") ? dashes + 1 : 0;
}
return ["comment", "comment"];
}
if (stream.eat("!")) {
state.tokenize = tokenSGMLComment;
return tokenSGMLComment(stream, state);
}
if (!stream.match("!--")) return false;
state.tokenize = tokenSGMLComment;
return tokenSGMLComment(stream, state);
},
"/": function(stream, state) {
if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
return false;
if (!stream.eat("*")) return false;
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}
},
name: "css"
});
CodeMirror.defineMIME("text/x-scss", {
atMediaTypes: atMediaTypes,
atMediaFeatures: atMediaFeatures,
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
propertyKeywords: propertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
hooks: {
":": function(stream) {
if (stream.match(/\s*{/)) {
return [null, "{"];
}
return false;
},
"$": function(stream) {
stream.match(/^[\w-]+/);
if (stream.peek() == ":") {
return ["variable", "variable-definition"];
}
return ["variable", "variable"];
},
",": function(stream, state) {
if (state.stack[state.stack.length - 1] == "propertyValue" && stream.match(/^ *\$/, false)) {
return ["operator", ";"];
}
},
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
@ -625,15 +626,58 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
return ["operator", "operator"];
}
},
":": function(stream) {
if (stream.match(/\s*{/))
return [null, "{"];
return false;
},
"$": function(stream) {
stream.match(/^[\w-]+/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"#": function(stream) {
if (stream.eat("{")) {
return ["operator", "interpolation"];
} else {
stream.eatWhile(/[\w\\\-]/);
return ["atom", "hash"];
}
if (!stream.eat("{")) return false;
return [null, "interpolation"];
}
},
name: "css"
name: "css",
helperType: "scss"
});
CodeMirror.defineMIME("text/x-less", {
mediaTypes: mediaTypes,
mediaFeatures: mediaFeatures,
propertyKeywords: propertyKeywords,
colorKeywords: colorKeywords,
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
stream.skipToEnd();
return ["comment", "comment"];
} else if (stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
} else {
return ["operator", "operator"];
}
},
"@": function(stream) {
if (stream.match(/^(charset|document|font-face|import|keyframes|media|namespace|page|supports)\b/, false)) return false;
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];
return ["variable-2", "variable"];
},
"&": function() {
return ["atom", "atom"];
}
},
name: "css",
helperType: "less"
});
})();

View File

@ -35,6 +35,7 @@
}
var inp = dialog.getElementsByTagName("input")[0], button;
if (inp) {
if (options && options.value) inp.value = options.value;
CodeMirror.on(inp, "keydown", function(e) {
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
if (e.keyCode == 13 || e.keyCode == 27) {

View File

@ -1,4 +1,6 @@
CodeMirror.registerHelper("fold", "comment", function(cm, start) {
CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
return mode.blockCommentStart && mode.blockCommentEnd;
}, function(cm, start) {
var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
if (!startToken || !endToken) return;
var line = start.line, lineText = cm.getLine(line);

View File

@ -3,8 +3,7 @@
function doFold(cm, pos, options, force) {
var finder = options && (options.call ? options : options.rangeFinder);
if (!finder) finder = cm.getHelper(pos, "fold");
if (!finder) return;
if (!finder) finder = CodeMirror.fold.auto;
if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
var minSize = options && options.minFoldSize || 0;
@ -63,6 +62,10 @@
doFold(this, pos, options, force);
});
CodeMirror.commands.fold = function(cm) {
cm.foldCode(cm.getCursor());
};
CodeMirror.registerHelper("fold", "combine", function() {
var funcs = Array.prototype.slice.call(arguments, 0);
return function(cm, start) {
@ -72,4 +75,12 @@
}
};
});
CodeMirror.registerHelper("fold", "auto", function(cm, start) {
var helpers = cm.getHelpers(start, "fold");
for (var i = 0; i < helpers.length; i++) {
var cur = helpers[i](cm, start);
if (cur) return cur;
}
});
})();

View File

@ -62,7 +62,7 @@
if (isFolded(cm, cur)) {
mark = marker(opts.indicatorFolded);
} else {
var pos = Pos(cur, 0), func = opts.rangeFinder || cm.getHelper(pos, "fold");
var pos = Pos(cur, 0), func = opts.rangeFinder || CodeMirror.fold.auto;
var range = func && func(cm, pos);
if (range && range.from.line + 1 < range.to.line)
mark = marker(opts.indicatorOpen);

View File

@ -164,4 +164,10 @@
if (close) return {open: open, close: close};
}
};
// Used by addon/edit/closetag.js
CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
return !!findMatchingClose(iter, name);
};
})();

View File

@ -93,8 +93,6 @@ CodeMirror.defineMode("htmlmixed", function(config, parserConfig) {
return CodeMirror.Pass;
},
electricChars: "/{}:",
innerMode: function(state) {
return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
}

View File

@ -15,7 +15,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
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,
"return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": 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"),
@ -54,14 +54,16 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var isOperatorChar = /[+\-*&%=<>!?|~^]/;
function nextUntilUnescaped(stream, end) {
var escaped = false, next;
function readRegexp(stream) {
var escaped = false, next, inSet = false;
while ((next = stream.next()) != null) {
if (next == end && !escaped)
return false;
if (!escaped) {
if (next == "/" && !inSet) return;
if (next == "[") inSet = true;
else if (inSet && next == "]") inSet = false;
}
escaped = !escaped && next == "\\";
}
return escaped;
}
// Used as scratch variables to communicate multiple values without
@ -83,7 +85,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
return ret(ch);
} else if (ch == "=" && stream.eat(">")) {
return ret("=>");
return ret("=>", "operator");
} else if (ch == "0" && stream.eat(/x/i)) {
stream.eatWhile(/[\da-f]/i);
return ret("number", "number");
@ -99,12 +101,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return ret("comment", "comment");
} else if (state.lastType == "operator" || state.lastType == "keyword c" ||
state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) {
nextUntilUnescaped(stream, "/");
readRegexp(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());
return ret("operator", "operator", stream.current());
}
} else if (ch == "`") {
state.tokenize = tokenQuasi;
@ -114,7 +116,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return ret("error", "error");
} else if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
return ret("operator", null, stream.current());
return ret("operator", "operator", stream.current());
} else {
stream.eatWhile(/[\w\$_]/);
var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
@ -125,8 +127,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function tokenString(quote) {
return function(stream, state) {
if (!nextUntilUnescaped(stream, quote))
state.tokenize = tokenBase;
var escaped = false, next;
while ((next = stream.next()) != null) {
if (next == quote && !escaped) break;
escaped = !escaped && next == "\\";
}
if (!escaped) state.tokenize = tokenBase;
return ret("string", "string");
};
}
@ -304,7 +310,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
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"), forspec, poplex, statement, poplex);
if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
if (type == "variable") return cont(pushlex("stat"), maybelabel);
if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
block, poplex, poplex);
@ -327,7 +333,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function expressionInner(type, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
if (type == "(") return cont(pushcontext, commasep(pattern, ")"), expect("=>"), body, popcontext);
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
@ -337,8 +343,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), expressionNoComma, maybeArrayComprehension, poplex, maybeop);
if (type == "{") return cont(commasep(objprop, "}"), maybeop);
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
return cont();
}
function maybeexpression(type) {
@ -365,12 +371,11 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
}
if (type == "quasi") { cx.cc.push(me); return quasi(value); }
if (type == ";") return;
if (type == "(") return cont(commasep(expressionNoComma, ")", "call"), me);
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
}
function quasi(value) {
if (!value) debugger;
if (value.slice(value.length - 2) != "${") return cont();
return cont(expression, continueQuasi);
}
@ -418,7 +423,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == ":") return cont(expressionNoComma);
if (type == "(") return pass(functiondef);
}
function commasep(what, end, info) {
function commasep(what, end) {
function proceed(type) {
if (type == ",") {
var lex = cx.state.lexical;
@ -430,10 +435,14 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
}
return function(type) {
if (type == end) return cont();
if (info === false) return pass(what, proceed);
return pass(pushlex(end, info), what, proceed, poplex);
return pass(what, proceed);
};
}
function contCommasep(what, end, info) {
for (var i = 3; i < arguments.length; i++)
cx.cc.push(arguments[i]);
return cont(pushlex(end, info), commasep(what, end), poplex);
}
function block(type) {
if (type == "}") return cont();
return pass(statement, block);
@ -449,8 +458,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
}
function pattern(type, value) {
if (type == "variable") { register(value); return cont(); }
if (type == "[") return cont(commasep(pattern, "]"));
if (type == "{") return cont(commasep(proppattern, "}"));
if (type == "[") return contCommasep(pattern, "]");
if (type == "{") return contCommasep(proppattern, "}");
}
function proppattern(type, value) {
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
@ -470,7 +479,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex);
}
function forspec(type) {
if (type == "(") return cont(pushlex(")"), forspec1, expect(")"));
if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef, expect(";"), forspec2);
@ -493,7 +502,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, commasep(funarg, ")"), statement, popcontext);
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext);
}
function funarg(type) {
if (type == "spread") return cont(funarg);
@ -506,7 +515,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (value == "extends") return cont(expression);
}
function objlit(type) {
if (type == "{") return cont(commasep(objprop, "}"));
if (type == "{") return contCommasep(objprop, "}");
}
function afterModule(type, value) {
if (type == "string") return cont(statement);
@ -522,17 +531,21 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return pass(importSpec, maybeFrom);
}
function importSpec(type, value) {
if (type == "{") return cont(commasep(importSpec, "}"));
if (type == "{") return contCommasep(importSpec, "}");
if (type == "variable") register(value);
return cont();
}
function maybeFrom(_type, value) {
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
}
function arrayLiteral(type) {
if (type == "]") return cont();
return pass(expressionNoComma, maybeArrayComprehension);
}
function maybeArrayComprehension(type) {
if (type == "for") return pass(comprehension);
if (type == ",") return cont(commasep(expressionNoComma, "]", false));
return pass(commasep(expressionNoComma, "]", false));
if (type == "for") return pass(comprehension, expect("]"));
if (type == ",") return cont(commasep(expressionNoComma, "]"));
return pass(commasep(expressionNoComma, "]"));
}
function comprehension(type) {
if (type == "for") return cont(forspec, comprehension);

View File

@ -201,6 +201,11 @@
cm.on("change", function() { cm.setExtending(false); });
}
function clearMark(cm) {
cm.setExtending(false);
cm.setCursor(cm.getCursor());
}
function getInput(cm, msg, f) {
if (cm.openDialog)
cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
@ -234,6 +239,11 @@
}
}
function quit(cm) {
cm.execCommand("clearSearch");
clearMark(cm);
}
// Actual keymap
var keyMap = CodeMirror.keyMap.emacs = {
@ -249,6 +259,7 @@
}),
"Alt-W": function(cm) {
addToRing(cm.getSelection());
clearMark(cm);
},
"Ctrl-Y": function(cm) {
var start = cm.getCursor();
@ -334,7 +345,7 @@
"Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
"Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
"Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
"Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace",
"Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
"Alt-/": "autocomplete",
"Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",

View File

@ -84,6 +84,7 @@
{ keys: ['<End>'], type: 'keyToKey', toKeys: ['$'] },
{ keys: ['<PageUp>'], type: 'keyToKey', toKeys: ['<C-b>'] },
{ keys: ['<PageDown>'], type: 'keyToKey', toKeys: ['<C-f>'] },
{ keys: ['<CR>'], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' },
// Motions
{ keys: ['H'], type: 'motion',
motion: 'moveToTopLine',
@ -247,6 +248,12 @@
actionArgs: { forward: true }},
{ keys: ['<C-o>'], type: 'action', action: 'jumpListWalk',
actionArgs: { forward: false }},
{ keys: ['<C-e>'], type: 'action',
action: 'scroll',
actionArgs: { forward: true, linewise: true }},
{ keys: ['<C-y>'], type: 'action',
action: 'scroll',
actionArgs: { forward: false, linewise: true }},
{ keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true,
actionArgs: { insertAt: 'charAfter' }},
{ keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true,
@ -324,12 +331,16 @@
CodeMirror.defineOption('vimMode', false, function(cm, val) {
if (val) {
cm.setOption('keyMap', 'vim');
cm.setOption('disableInput', true);
CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
cm.on('beforeSelectionChange', beforeSelectionChange);
maybeInitVimState(cm);
CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
} else if (cm.state.vim) {
cm.setOption('keyMap', 'default');
cm.setOption('disableInput', false);
cm.off('beforeSelectionChange', beforeSelectionChange);
CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
cm.state.vim = null;
}
});
@ -342,6 +353,18 @@
head.ch--;
}
}
function getOnPasteFn(cm) {
var vim = cm.state.vim;
if (!vim.onPasteFn) {
vim.onPasteFn = function() {
if (!vim.insertMode) {
cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
actions.enterInsertMode(cm, {}, vim);
}
};
}
return vim.onPasteFn;
}
var numberRegex = /[\d]/;
var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
@ -549,9 +572,9 @@
maybeInitVimState_: maybeInitVimState,
InsertModeKey: InsertModeKey,
map: function(lhs, rhs) {
map: function(lhs, rhs, ctx) {
// Add user defined key bindings.
exCommandDispatcher.map(lhs, rhs);
exCommandDispatcher.map(lhs, rhs, ctx);
},
defineEx: function(name, prefix, func){
if (name.indexOf(prefix) !== 0) {
@ -833,11 +856,17 @@
} else {
// Find the best match in the list of matchedCommands.
var context = vim.visualMode ? 'visual' : 'normal';
var bestMatch = matchedCommands[0]; // Default to first in the list.
var bestMatch; // Default to first in the list.
for (var i = 0; i < matchedCommands.length; i++) {
if (matchedCommands[i].context == context) {
bestMatch = matchedCommands[i];
var current = matchedCommands[i];
if (current.context == context) {
bestMatch = current;
break;
} else if (!bestMatch && !current.context) {
// Only set an imperfect match to best match if no best match is
// set and the imperfect match is not restricted to another
// context.
bestMatch = current;
}
}
return getFullyMatchedCommandOrNull(bestMatch);
@ -1636,6 +1665,43 @@
markPos = markPos ? markPos : cm.getCursor();
cm.setCursor(markPos);
},
scroll: function(cm, actionArgs, vim) {
if (vim.visualMode) {
return;
}
var repeat = actionArgs.repeat || 1;
var lineHeight = cm.defaultTextHeight();
var top = cm.getScrollInfo().top;
var delta = lineHeight * repeat;
var newPos = actionArgs.forward ? top + delta : top - delta;
var cursor = cm.getCursor();
var cursorCoords = cm.charCoords(cursor, 'local');
if (actionArgs.forward) {
if (newPos > cursorCoords.top) {
cursor.line += (newPos - cursorCoords.top) / lineHeight;
cursor.line = Math.ceil(cursor.line);
cm.setCursor(cursor);
cursorCoords = cm.charCoords(cursor, 'local');
cm.scrollTo(null, cursorCoords.top);
} else {
// Cursor stays within bounds. Just reposition the scroll window.
cm.scrollTo(null, newPos);
}
} else {
var newBottom = newPos + cm.getScrollInfo().clientHeight;
if (newBottom < cursorCoords.bottom) {
cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
cursor.line = Math.floor(cursor.line);
cm.setCursor(cursor);
cursorCoords = cm.charCoords(cursor, 'local');
cm.scrollTo(
null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
} else {
// Cursor stays within bounds. Just reposition the scroll window.
cm.scrollTo(null, newPos);
}
}
},
scrollToCursor: function(cm, actionArgs) {
var lineNum = cm.getCursor().line;
var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local');
@ -1691,6 +1757,7 @@
cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
}
cm.setOption('keyMap', 'vim-insert');
cm.setOption('disableInput', false);
if (actionArgs && actionArgs.replace) {
// Handle Replace-mode as a special case of insert mode.
cm.toggleOverwrite(true);
@ -2896,14 +2963,16 @@
// pair of commands that have a shared prefix, at least one of their
// shortNames must not match the prefix of the other command.
var defaultExCommandMap = [
{ name: 'map', type: 'builtIn' },
{ name: 'write', shortName: 'w', type: 'builtIn' },
{ name: 'undo', shortName: 'u', type: 'builtIn' },
{ name: 'redo', shortName: 'red', type: 'builtIn' },
{ name: 'sort', shortName: 'sor', type: 'builtIn'},
{ name: 'substitute', shortName: 's', type: 'builtIn'},
{ name: 'nohlsearch', shortName: 'noh', type: 'builtIn'},
{ name: 'delmarks', shortName: 'delm', type: 'builtin'}
{ name: 'map' },
{ name: 'nmap', shortName: 'nm' },
{ name: 'vmap', shortName: 'vm' },
{ name: 'write', shortName: 'w' },
{ name: 'undo', shortName: 'u' },
{ name: 'redo', shortName: 'red' },
{ name: 'sort', shortName: 'sor' },
{ name: 'substitute', shortName: 's' },
{ name: 'nohlsearch', shortName: 'noh' },
{ name: 'delmarks', shortName: 'delm' }
];
Vim.ExCommandDispatcher = function() {
this.buildCommandMap_();
@ -2955,6 +3024,7 @@
exCommands[commandName](cm, params);
} catch(e) {
showConfirm(cm, e);
throw e;
}
},
parseInput_: function(cm, inputStream, result) {
@ -3037,8 +3107,9 @@
this.commandMap_[key] = command;
}
},
map: function(lhs, rhs) {
map: function(lhs, rhs, ctx) {
if (lhs != ':' && lhs.charAt(0) == ':') {
if (ctx) { throw Error('Mode not supported for ex mappings'); }
var commandName = lhs.substring(1);
if (rhs != ':' && rhs.charAt(0) == ':') {
// Ex to Ex mapping
@ -3058,17 +3129,21 @@
} else {
if (rhs != ':' && rhs.charAt(0) == ':') {
// Key to Ex mapping.
defaultKeymap.unshift({
var mapping = {
keys: parseKeyString(lhs),
type: 'keyToEx',
exArgs: { input: rhs.substring(1) }});
exArgs: { input: rhs.substring(1) }};
if (ctx) { mapping.context = ctx; }
defaultKeymap.unshift(mapping);
} else {
// Key to key mapping
defaultKeymap.unshift({
var mapping = {
keys: parseKeyString(lhs),
type: 'keyToKey',
toKeys: parseKeyString(rhs)
});
};
if (ctx) { mapping.context = ctx; }
defaultKeymap.unshift(mapping);
}
}
}
@ -3090,7 +3165,7 @@
}
var exCommands = {
map: function(cm, params) {
map: function(cm, params, ctx) {
var mapArgs = params.args;
if (!mapArgs || mapArgs.length < 2) {
if (cm) {
@ -3098,8 +3173,10 @@
}
return;
}
exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm);
exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
},
nmap: function(cm, params) { this.map(cm, params, 'normal'); },
vmap: function(cm, params) { this.map(cm, params, 'visual'); },
move: function(cm, params) {
commandDispatcher.processCommand(cm, cm.state.vim, {
type: 'motion',
@ -3115,7 +3192,7 @@
var args = new CodeMirror.StringStream(params.argString);
if (args.eat('!')) { reverse = true; }
if (args.eol()) { return; }
if (!args.eatSpace()) { throw new Error('invalid arguments ' + args.match(/.*/)[0]); }
if (!args.eatSpace()) { return 'Invalid arguments'; }
var opts = args.match(/[a-z]+/);
if (opts) {
opts = opts[0];
@ -3124,13 +3201,17 @@
var decimal = opts.indexOf('d') != -1 && 1;
var hex = opts.indexOf('x') != -1 && 1;
var octal = opts.indexOf('o') != -1 && 1;
if (decimal + hex + octal > 1) { throw new Error('invalid arguments'); }
if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
}
if (args.eatSpace() && args.match(/\/.*\//)) { throw new Error('patterns not supported'); }
if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; }
}
}
parseArgs();
var err = parseArgs();
if (err) {
showConfirm(cm, err + ': ' + params.argString);
return;
}
var lineStart = params.line || cm.firstLine();
var lineEnd = params.lineEnd || params.line || cm.lastLine();
if (lineStart == lineEnd) { return; }
@ -3251,13 +3332,13 @@
clearSearchHighlight(cm);
},
delmarks: function(cm, params) {
if (!params.argString || !params.argString.trim()) {
if (!params.argString || !trim(params.argString)) {
showConfirm(cm, 'Argument required');
return;
}
var state = cm.state.vim;
var stream = new CodeMirror.StringStream(params.argString.trim());
var stream = new CodeMirror.StringStream(trim(params.argString));
while (!stream.eol()) {
stream.eatSpace();
@ -3394,7 +3475,8 @@
// Actually do replace.
next();
if (done) {
throw new Error('No matches for ' + query.source);
showConfirm(cm, 'No matches for ' + query.source);
return;
}
if (!confirm) {
replaceAll();
@ -3445,7 +3527,6 @@
var cmToVimKeymap = {
'nofallthrough': true,
'disableInput': true,
'style': 'fat-cursor'
};
function bindKeys(keys, modifier) {
@ -3492,6 +3573,7 @@
cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
vim.insertMode = false;
cm.setOption('keyMap', 'vim');
cm.setOption('disableInput', true);
cm.toggleOverwrite(false); // exit replace mode if we were in it.
CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
}

View File

@ -54,7 +54,7 @@
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.
// input stops going to the textarea whenever this fires.
if (ie_lt8 && cm.state.focused) cm.display.input.focus();
var clear = function() {
cm.operation(function() { one.clear(); two && two.clear(); });

View File

@ -74,14 +74,6 @@ selector in floating-scrollbar-light.css across all platforms. */
font: message-box;
}
.CodeMirror-code > div > div:first-child {
top: 50%;
}
.CodeMirror-gutter-elt {
transform: translate(0,-50%);
}
.cm-trailingspace {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==");
opacity: 0.75;

View File

@ -7,7 +7,15 @@
// Ctrl-G.
(function() {
function searchOverlay(query) {
function searchOverlay(query, caseInsensitive) {
var startChar;
if (typeof query == "string") {
startChar = query.charAt(0);
query = new RegExp("^" + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"),
caseInsensitive ? "i" : "");
} else {
query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : "");
}
if (typeof query == "string") return {token: function(stream) {
if (stream.match(query)) return "searching";
stream.next();
@ -17,6 +25,8 @@
if (stream.match(query)) return "searching";
while (!stream.eol()) {
stream.next();
if (startChar)
stream.skipTo(startChar) || stream.skipToEnd();
if (stream.match(query, false)) break;
}
}};
@ -29,13 +39,16 @@
function getSearchState(cm) {
return cm.state.search || (cm.state.search = new SearchState());
}
function queryCaseInsensitive(query) {
return typeof query == "string" && query == query.toLowerCase();
}
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());
return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
}
function dialog(cm, text, shortText, f) {
if (cm.openDialog) cm.openDialog(text, f);
else f(prompt(shortText, ""));
function dialog(cm, text, shortText, deflt, f) {
if (cm.openDialog) cm.openDialog(text, f, {value: deflt});
else f(prompt(shortText, deflt));
}
function confirmDialog(cm, text, shortText, fs) {
if (cm.openConfirm) cm.openConfirm(text, fs);
@ -45,29 +58,16 @@
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query;
}
var queryDialog;
var queryDialog =
'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
function doSearch(cm, rev) {
if (!queryDialog) {
let doc = cm.getWrapperElement().ownerDocument;
let inp = doc.createElement("input");
let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
inp.type = "text";
inp.style.width = "10em";
inp.style.MozMarginStart = "1em";
queryDialog = doc.createElement("div");
queryDialog.appendChild(txt);
queryDialog.appendChild(inp);
}
var state = getSearchState(cm);
if (state.query) return findNext(cm, rev);
dialog(cm, queryDialog, cm.l10n('findCmd.promptMessage'), function(query) {
dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
cm.operation(function() {
if (!query || state.query) return;
state.query = parseQuery(query);
cm.removeOverlay(state.overlay);
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
state.overlay = searchOverlay(state.query);
cm.addOverlay(state.overlay);
state.posFrom = state.posTo = cm.getCursor();
@ -98,10 +98,10 @@
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) {
dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) {
if (!query) return;
query = parseQuery(query);
dialog(cm, replacementQueryDialog, "Replace with:", function(text) {
dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
if (all) {
cm.operation(function() {
for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {

View File

@ -47,6 +47,7 @@
match: match};
};
} else { // String query
var origQuery = query;
if (caseFold) query = query.toLowerCase();
var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
var target = query.split("\n");
@ -58,33 +59,45 @@
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)};
if (reverse) {
var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
var match = line.lastIndexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match);
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
} else {
var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
var match = line.indexOf(query);
if (match > -1) {
match = adjustPos(orig, line, match) + pos.ch;
return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
}
}
};
}
} else {
var origTarget = origQuery.split("\n");
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};
var last = target.length - 1;
if (reverse) {
if (pos.line - (target.length - 1) < doc.firstLine()) return;
if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
var to = Pos(pos.line, origTarget[last].length);
for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
if (target[i] != fold(doc.getLine(ln))) return;
var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
return {from: Pos(ln, cut), to: to};
} else {
if (pos.line + (target.length - 1) > doc.lastLine()) return;
var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
if (fold(line.slice(cut)) != target[0]) return;
var from = Pos(pos.line, cut);
for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
if (target[i] != fold(doc.getLine(ln))) return;
if (doc.getLine(ln).slice(0, origTarget[last].length) != target[last]) return;
return {from: from, to: Pos(ln, origTarget[last].length)};
}
};
}
@ -106,7 +119,6 @@
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;
}
@ -134,6 +146,18 @@
}
};
// Maps a position in a case-folded line back to a position in the original line
// (compensating for codepoints increasing in number during folding)
function adjustPos(orig, folded, pos) {
if (orig.length == folded.length) return pos;
for (var pos1 = Math.min(pos, orig.length);;) {
var len1 = orig.slice(0, pos1).toLowerCase().length;
if (len1 < pos) ++pos1;
else if (len1 > pos) --pos1;
else return pos1;
}
}
CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
return new SearchCursor(this.doc, query, pos, caseFold);
});

View File

@ -1,11 +1,8 @@
(function() {
"use strict";
CodeMirror.defineOption("showTrailingSpace", true, function(cm, val, prev) {
if (prev == CodeMirror.Init) prev = false;
if (prev && !val)
CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
if (prev == CodeMirror.Init) prev = false;
if (prev && !val)
cm.removeOverlay("trailingspace");
else if (!prev && val)
else if (!prev && val)
cm.addOverlay({
token: function(stream) {
for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
@ -15,5 +12,4 @@
},
name: "trailingspace"
});
});
})();
});

View File

@ -45,7 +45,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
var alignCDATA = parserConfig.alignCDATA;
// Return variables for tokenizers
var tagName, type;
var tagName, type, setStyle;
function inText(stream, state) {
function chain(parser) {
@ -110,6 +110,8 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
return null;
} else if (ch == "<") {
state.tokenize = inText;
state.state = baseState;
state.tagName = state.tagStart = null;
var next = state.tokenize(stream, state);
return next ? next + " error" : "error";
} else if (/[\'\"]/.test(ch)) {
@ -169,139 +171,124 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
};
}
var curState, curStream, setStyle;
function pass() {
for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]);
function Context(state, tagName, startOfLine) {
this.prev = state.context;
this.tagName = tagName;
this.indent = state.indented;
this.startOfLine = startOfLine;
if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
this.noIndent = true;
}
function cont() {
pass.apply(null, arguments);
return true;
function popContext(state) {
if (state.context) state.context = state.context.prev;
}
function maybePopContext(state, nextTagName) {
var parentTagName;
while (true) {
if (!state.context) {
return;
}
parentTagName = state.context.tagName.toLowerCase();
if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
!Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
return;
}
popContext(state);
}
}
function pushContext(tagName, startOfLine) {
var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent);
curState.context = {
prev: curState.context,
tagName: tagName,
indent: curState.indented,
startOfLine: startOfLine,
noIndent: noIndent
};
}
function popContext() {
if (curState.context) curState.context = curState.context.prev;
}
function element(type) {
function baseState(type, stream, state) {
if (type == "openTag") {
curState.tagName = tagName;
curState.tagStart = curStream.column();
return cont(attributes, endtag(curState.startOfLine));
state.tagName = tagName;
state.tagStart = stream.column();
return attrState;
} else if (type == "closeTag") {
var err = false;
if (curState.context) {
if (curState.context.tagName != tagName) {
if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) {
popContext();
}
err = !curState.context || curState.context.tagName != tagName;
if (state.context) {
if (state.context.tagName != tagName) {
if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName.toLowerCase()))
popContext(state);
err = !state.context || state.context.tagName != tagName;
}
} else {
err = true;
}
if (err) setStyle = "error";
return cont(endclosetag(err));
return err ? closeStateErr : closeState;
} else {
return baseState;
}
return cont();
}
function endtag(startOfLine) {
return function(type) {
var tagName = curState.tagName;
curState.tagName = curState.tagStart = null;
if (type == "selfcloseTag" ||
(type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase()))) {
maybePopContext(tagName.toLowerCase());
return cont();
}
if (type == "endTag") {
maybePopContext(tagName.toLowerCase());
pushContext(tagName, startOfLine);
return cont();
}
return cont();
};
}
function endclosetag(err) {
return function(type) {
if (err) setStyle = "error";
if (type == "endTag") { popContext(); return cont(); }
function closeState(type, _stream, state) {
if (type != "endTag") {
setStyle = "error";
return cont(arguments.callee);
};
}
function maybePopContext(nextTagName) {
var parentTagName;
while (true) {
if (!curState.context) {
return;
}
parentTagName = curState.context.tagName.toLowerCase();
if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
!Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
return;
}
popContext();
return closeState;
}
popContext(state);
return baseState;
}
function closeStateErr(type, stream, state) {
setStyle = "error";
return closeState(type, stream, state);
}
function attributes(type) {
if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);}
if (type == "endTag" || type == "selfcloseTag") return pass();
function attrState(type, _stream, state) {
if (type == "word") {
setStyle = "attribute";
return attrEqState;
} else if (type == "endTag" || type == "selfcloseTag") {
var tagName = state.tagName, tagStart = state.tagStart;
state.tagName = state.tagStart = null;
if (type == "selfcloseTag" ||
Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase())) {
maybePopContext(state, tagName.toLowerCase());
} else {
maybePopContext(state, tagName.toLowerCase());
state.context = new Context(state, tagName, tagStart == state.indented);
}
return baseState;
}
setStyle = "error";
return cont(attributes);
return attrState;
}
function attribute(type) {
if (type == "equals") return cont(attvalue, attributes);
function attrEqState(type, stream, state) {
if (type == "equals") return attrValueState;
if (!Kludges.allowMissing) setStyle = "error";
else if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);}
return (type == "endTag" || type == "selfcloseTag") ? pass() : cont();
return attrState(type, stream, state);
}
function attvalue(type) {
if (type == "string") return cont(attvaluemaybe);
if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();}
function attrValueState(type, stream, state) {
if (type == "string") return attrContinuedState;
if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;}
setStyle = "error";
return (type == "endTag" || type == "selfCloseTag") ? pass() : cont();
return attrState(type, stream, state);
}
function attvaluemaybe(type) {
if (type == "string") return cont(attvaluemaybe);
else return pass();
function attrContinuedState(type, stream, state) {
if (type == "string") return attrContinuedState;
return attrState(type, stream, state);
}
return {
startState: function() {
return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, tagStart: null, context: null};
return {tokenize: inText,
state: baseState,
indented: 0,
tagName: null, tagStart: null,
context: null};
},
token: function(stream, state) {
if (!state.tagName && stream.sol()) {
state.startOfLine = true;
if (!state.tagName && stream.sol())
state.indented = stream.indentation();
}
if (stream.eatSpace()) return null;
setStyle = type = tagName = null;
if (stream.eatSpace()) return null;
tagName = type = null;
var style = state.tokenize(stream, state);
state.type = type;
if ((style || type) && style != "comment") {
curState = state; curStream = stream;
while (true) {
var comb = state.cc.pop() || element;
if (comb(type || style)) break;
}
setStyle = null;
state.state = state.state(type || style, stream, state);
if (setStyle)
style = setStyle == "error" ? style + " error" : setStyle;
}
state.startOfLine = false;
if (setStyle)
style = setStyle == "error" ? style + " error" : setStyle;
return style;
},
@ -311,8 +298,8 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
if (state.tokenize.isInAttribute) {
return state.stringStartCol + 1;
}
if ((state.tokenize != inTag && state.tokenize != inText) ||
context && context.noIndent)
if (context && context.noIndent) return CodeMirror.Pass;
if (state.tokenize != inTag && state.tokenize != inText)
return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
// Indent the starts of attribute names.
if (state.tagName) {

View File

@ -148,9 +148,9 @@ function Editor(config) {
};
// Additional shortcuts.
this.config.extraKeys[Editor.keyFor("jumpToLine")] = (cm) => this.jumpToLine(cm);
this.config.extraKeys[Editor.keyFor("moveLineUp")] = (cm) => this.moveLineUp();
this.config.extraKeys[Editor.keyFor("moveLineDown")] = (cm) => this.moveLineDown();
this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
this.config.extraKeys[Editor.keyFor("moveLineUp")] = () => this.moveLineUp();
this.config.extraKeys[Editor.keyFor("moveLineDown")] = () => this.moveLineDown();
this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
// Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
@ -176,6 +176,16 @@ function Editor(config) {
});
});
// Set the code folding gutter, if needed.
if (this.config.enableCodeFolding) {
this.config.foldGutter = true;
if (!this.config.gutters) {
this.config.gutters = this.config.lineNumbers ? ["CodeMirror-linenumbers"] : [];
this.config.gutters.push("CodeMirror-foldgutter");
}
}
// 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
@ -257,7 +267,9 @@ Editor.prototype = {
cm = win.CodeMirror(win.document.body, this.config);
cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
ev.preventDefault();
this.showContextMenu(el.ownerDocument, ev.screenX, ev.screenY);
if (!this.config.contextMenu) return;
let popup = el.ownerDocument.getElementById(this.config.contextMenu);
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
}, false);
cm.on("focus", () => this.emit("focus"));
@ -659,33 +671,12 @@ Editor.prototype = {
return cm.isClean(this.version);
},
/**
* True if the editor is in the read-only mode, false otherwise.
*/
isReadOnly: function () {
return this.getOption("readOnly");
},
/**
* 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 (cm) {
let doc = cm.getWrapperElement().ownerDocument;
jumpToLine: function () {
let doc = editors.get(this).getWrapperElement().ownerDocument;
let div = doc.createElement("div");
let inp = doc.createElement("input");
let txt = doc.createTextNode(L10N.GetStringFromName("gotoLineCmd.promptTitle"));
@ -933,7 +924,7 @@ function controller(ed) {
}
if (cmd == "cmd_gotoLine")
ed.jumpToLine(cm);
ed.jumpToLine();
},
onEvent: function () {}

View File

@ -29,11 +29,11 @@ namespace = "comment_";
}, simpleProg, simpleProg);
// test("fallbackToBlock", "css", function(cm) {
// cm.lineComment(Pos(0, 0), Pos(2, 1));
// 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));
// cm.blockComment(Pos(0, 0), Pos(1));
// }, "def blah()\n return hah\n", "# def blah()\n# return hah\n");
test("commentRange", "javascript", function(cm) {
@ -48,4 +48,16 @@ namespace = "comment_";
cm.setCursor(1);
cm.execCommand("toggleComment");
}, "a;\n\nb;", "a;\n// \nb;");
test("dontMessWithStrings", "javascript", function(cm) {
cm.execCommand("toggleComment");
}, "console.log(\"/*string*/\");", "// console.log(\"/*string*/\");");
test("dontMessWithStrings2", "javascript", function(cm) {
cm.execCommand("toggleComment");
}, "console.log(\"// string\");", "// console.log(\"// string\");");
test("dontMessWithStrings3", "javascript", function(cm) {
cm.execCommand("toggleComment");
}, "// console.log(\"// string\");", "console.log(\"// string\");");
})();

View File

@ -1,4 +1,4 @@
var tests = [], debug = null, debugUsed = new Array(), allNames = [];
var tests = [], filters = [], allNames = [];
function Failure(why) {this.message = why;}
Failure.prototype.toString = function() { return this.message; };
@ -32,7 +32,7 @@ function testCM(name, run, opts, expectedFail) {
run(cm);
successful = true;
} finally {
if ((debug && !successful) || verbose) {
if (!successful || verbose) {
place.style.visibility = "visible";
} else {
place.removeChild(cm.getWrapperElement());
@ -42,39 +42,23 @@ function testCM(name, run, opts, 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);
if (filters.length) {
for (var j = 0; j < filters.length; j++) {
if (test.name.match(filters[j])) {
break;
}
}
if (j == filters.length) {
callback("skipped", test.name, message);
return step(i + 1);
}
}
var threw = false;
try {
@ -84,7 +68,7 @@ function runTests(callback) {
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);
var pos = /(?:\bat |@).*?([^\/:]+):(\d+)/.exec(e.stack);
callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : ""));
}
}
@ -127,13 +111,21 @@ function is(a, msg) {
}
function countTests() {
if (!debug) return tests.length;
if (!filters.length) 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;
for (var j = 0; j < filters.length; j++) {
if (name.match(filters[j])) {
++sum;
break;
}
}
}
return sum;
}
function parseTestFilter(s) {
if (/_\*$/.test(s)) return new RegExp("^" + s.slice(0, s.length - 2), "i");
else return new RegExp(s, "i");
}

View File

@ -125,6 +125,9 @@
sim("transposeExpr", "do foo[bar] dah",
Pos(0, 6), "Ctrl-Alt-T", txt("do [bar]foo dah"));
sim("clearMark", "abcde", Pos(0, 2), "Ctrl-Space", "Ctrl-F", "Ctrl-F",
"Ctrl-G", "Ctrl-W", txt("abcde"));
testCM("save", function(cm) {
var saved = false;
CodeMirror.commands.save = function(cm) { saved = cm.getValue(); };

View File

@ -3,15 +3,15 @@
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]; }");
"[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
MT("comma-and-binop",
"[keyword function](){ [keyword var] [def x] = [number 1] + [number 2], [def y]; }");
"[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
MT("destructuring",
"([keyword function]([def a], [[[def b], [def c] ]]) {",
" [keyword let] {[def d], [property foo]: [def c]=[number 10], [def x]} = [variable foo]([variable-2 a]);",
" [[[variable-2 c], [variable y] ]] = [variable-2 c];",
" [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
" [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
"})();");
MT("class",
@ -19,13 +19,13 @@
" [[ [string-2 /expr/] ]]: [number 24],",
" [property constructor]([def x], [def y]) {",
" [keyword super]([string 'something']);",
" [keyword this].[property x] = [variable-2 x];",
" [keyword this].[property x] [operator =] [variable-2 x];",
" }",
"}");
MT("module",
"[keyword module] [string 'foo'] {",
" [keyword export] [keyword let] [def x] = [number 42];",
" [keyword export] [keyword let] [def x] [operator =] [number 42];",
" [keyword export] [keyword *] [keyword from] [string 'somewhere'];",
"}");
@ -38,7 +38,7 @@
MT("const",
"[keyword function] [variable f]() {",
" [keyword const] [[ [def a], [def b] ]] = [[ [number 1], [number 2] ]];",
" [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
"}");
MT("for/of",
@ -46,14 +46,14 @@
MT("generator",
"[keyword function*] [variable repeat]([def n]) {",
" [keyword for]([keyword var] [def i] = [number 0]; [variable-2 i] < [variable-2 n]; ++[variable-2 i])",
" [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
" [keyword yield] [variable-2 i];",
"}");
MT("fatArrow",
"[variable array].[property filter]([def a] => [variable-2 a] + [number 1]);",
"[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
"[variable a];", // No longer in scope
"[keyword let] [variable f] = ([[ [def a], [def b] ]], [def c]) => [variable-2 a] + [variable-2 c];",
"[keyword let] [variable f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
"[variable c];");
MT("spread",
@ -63,10 +63,51 @@
MT("comprehension",
"[keyword function] [variable f]() {",
" [[ [variable x] + [number 1] [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
" ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] === [string 'blue']));",
" [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
" ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
"}");
MT("quasi",
"[variable re][string-2 `fofdlakj${][variable x] + ([variable re][string-2 `foo`]) + [number 1][string-2 }fdsa`] + [number 2]");
"[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
MT("indent_statement",
"[keyword var] [variable x] [operator =] [number 10]",
"[variable x] [operator +=] [variable y] [operator +]",
" [atom Infinity]",
"[keyword debugger];");
MT("indent_if",
"[keyword if] ([number 1])",
" [keyword break];",
"[keyword else] [keyword if] ([number 2])",
" [keyword continue];",
"[keyword else]",
" [number 10];",
"[keyword if] ([number 1]) {",
" [keyword break];",
"} [keyword else] [keyword if] ([number 2]) {",
" [keyword continue];",
"} [keyword else] {",
" [number 10];",
"}");
MT("indent_for",
"[keyword for] ([keyword var] [variable i] [operator =] [number 0];",
" [variable i] [operator <] [number 100];",
" [variable i][operator ++])",
" [variable doSomething]([variable i]);",
"[keyword debugger];");
MT("indent_c_style",
"[keyword function] [variable foo]()",
"{",
" [keyword debugger];",
"}");
MT("multilinestring",
"[keyword var] [variable x] [operator =] [string 'foo\\]",
"[string bar'];");
MT("scary_regexp",
"[string-2 /foo[[/]]bar/];");
})();

View File

@ -59,13 +59,6 @@
return {tokens: tokens, plain: plain};
}
test.indentation = function(name, mode, tokens, modeName) {
var data = parseTokens(tokens);
return test((modeName || mode.name) + "_indent_" + name, function() {
return compare(data.plain, data.tokens, mode, true);
});
};
test.mode = function(name, mode, tokens, modeName) {
var data = parseTokens(tokens);
return test((modeName || mode.name) + "_" + name, function() {
@ -73,7 +66,11 @@
});
};
function compare(text, expected, mode, compareIndentation) {
function esc(str) {
return str.replace('&', '&amp;').replace('<', '&lt;');
}
function compare(text, expected, mode) {
var expectedOutput = [];
for (var i = 0; i < expected.length; i += 2) {
@ -82,61 +79,49 @@
expectedOutput.push(sty, expected[i + 1]);
}
var observedOutput = highlight(text, mode, compareIndentation);
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>';
var s = "";
var diff = highlightOutputsDifferent(expectedOutput, observedOutput);
if (diff != null) {
s += '<div class="mt-test mt-fail">';
s += '<pre>' + esc(text) + '</pre>';
s += '<div class="cm-s-default">';
s += 'expected:';
s += prettyPrintOutputTable(expectedOutput);
s += prettyPrintOutputTable(expectedOutput, diff);
s += 'observed:';
s += prettyPrintOutputTable(observedOutput);
s += prettyPrintOutputTable(observedOutput, diff);
s += '</div>';
s += '</div>';
throw s;
}
if (observedOutput.indentFailures) {
for (var i = 0; i < observedOutput.indentFailures.length; i++)
s += "<div class='mt-test mt-fail'>" + esc(observedOutput.indentFailures[i]) + "</div>";
}
if (s) throw new Failure(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, compareIndentation) {
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;
if (mode.indent) {
var ws = line.match(/^\s*/)[0];
var indent = mode.indent(state, line.slice(ws.length));
if (indent != CodeMirror.Pass && indent != ws.length)
(st.indentFailures || (st.indentFailures = [])).push(
"Indentation of line " + (i + 1) + " is " + indent + " (expected " + ws.length + ")");
}
var stream = new CodeMirror.StringStream(line);
if (line == "" && mode.blankLine) mode.blankLine(state);
/* Start copied code from CodeMirror.highlight */
while (!stream.eol()) {
var compare = mode.token(stream, state), substr = stream.current();
if(compareIndentation) compare = mode.indent(state) || null;
else if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
stream.start = stream.pos;
var compare = mode.token(stream, state), substr = stream.current();
if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
stream.start = stream.pos;
if (pos && st[pos-2] == compare && !newLine) {
st[pos-1] += substr;
} else if (substr) {
@ -154,39 +139,22 @@
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;
function highlightOutputsDifferent(o1, o2) {
var minLen = Math.min(o1.length, o2.length);
for (var i = 0; i < minLen; ++i)
if (o1[i] != o2[i]) return i >> 1;
if (o1.length > minLen || o2.length > minLen) return minLen;
}
/**
* 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) {
function prettyPrintOutputTable(output, diffAt) {
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;') +
'<td class="mt-token"' + (i == diffAt * 2 ? " style='background: pink'" : "") + '>' +
'<span class="cm-' + esc(String(style)) + '">' +
esc(val.replace(/ /g,'\xb7')) +
'</span>' +
'</td>';
}

View File

@ -436,6 +436,36 @@ testCM("markTextStayGone", function(cm) {
eq(m1.find(), null);
}, {value: "hello"});
testCM("markTextAllowEmpty", function(cm) {
var m1 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false});
is(m1.find());
cm.replaceRange("x", Pos(0, 0));
is(m1.find());
cm.replaceRange("y", Pos(0, 2));
is(m1.find());
cm.replaceRange("z", Pos(0, 3), Pos(0, 4));
is(!m1.find());
var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false,
inclusiveLeft: true,
inclusiveRight: true});
cm.replaceRange("q", Pos(0, 1), Pos(0, 2));
is(m2.find());
cm.replaceRange("", Pos(0, 0), Pos(0, 3));
is(!m2.find());
var m3 = cm.markText(Pos(0, 1), Pos(0, 1), {clearWhenEmpty: false});
cm.replaceRange("a", Pos(0, 3));
is(m3.find());
cm.replaceRange("b", Pos(0, 1));
is(!m3.find());
}, {value: "abcde"});
testCM("markTextStacked", function(cm) {
var m1 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
var m2 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
cm.replaceRange("B", Pos(0, 1));
is(m1.find() && m2.find());
}, {value: "A"});
testCM("undoPreservesNewMarks", function(cm) {
cm.markText(Pos(0, 3), Pos(0, 4));
cm.markText(Pos(1, 1), Pos(1, 3));
@ -712,8 +742,8 @@ testCM("collapsedRangeCoordsChar", function(cm) {
var m1 = cm.markText(Pos(0, 0), Pos(2, 0), opts);
eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
m1.clear();
var m1 = cm.markText(Pos(0, 0), Pos(1, 1), opts);
var m2 = cm.markText(Pos(1, 1), Pos(2, 0), opts);
var m1 = cm.markText(Pos(0, 0), Pos(1, 1), {collapsed: true, inclusiveLeft: true});
var m2 = cm.markText(Pos(1, 1), Pos(2, 0), {collapsed: true, inclusiveRight: true});
eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
m1.clear(); m2.clear();
var m1 = cm.markText(Pos(0, 0), Pos(1, 6), opts);
@ -841,6 +871,23 @@ testCM("badNestedFold", function(cm) {
is(/overlap/i.test(caught.message), "wrong error");
});
testCM("nestedFoldOnSide", function(cm) {
var m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true, inclusiveRight: true});
var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true});
cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true}).clear();
try { cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true, inclusiveLeft: true}); }
catch(e) { var caught = e; }
is(caught && /overlap/i.test(caught.message));
var m3 = cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true});
var m4 = cm.markText(Pos(2, 0), Pos(2, 1), {collapse: true, inclusiveRight: true});
m1.clear(); m4.clear();
m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true});
cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true}).clear();
try { cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true, inclusiveRight: true}); }
catch(e) { var caught = e; }
is(caught && /overlap/i.test(caught.message));
}, {value: "ab\ncd\ef"});
testCM("wrappingInlineWidget", function(cm) {
cm.setSize("11em");
var w = document.createElement("span");
@ -973,6 +1020,24 @@ testCM("moveVstuck", function(cm) {
eqPos(cm.getCursor(), Pos(0, 26));
}, {lineWrapping: true}, ie_lt8 || opera_lt10);
testCM("collapseOnMove", function(cm) {
cm.setSelection(Pos(0, 1), Pos(2, 4));
cm.execCommand("goLineUp");
is(!cm.somethingSelected());
eqPos(cm.getCursor(), Pos(0, 1));
cm.setSelection(Pos(0, 1), Pos(2, 4));
cm.execCommand("goPageDown");
is(!cm.somethingSelected());
eqPos(cm.getCursor(), Pos(2, 4));
cm.execCommand("goLineUp");
cm.execCommand("goLineUp");
eqPos(cm.getCursor(), Pos(0, 4));
cm.setSelection(Pos(0, 1), Pos(2, 4));
cm.execCommand("goCharLeft");
is(!cm.somethingSelected());
eqPos(cm.getCursor(), Pos(0, 1));
}, {value: "aaaaa\nb\nccccc"});
testCM("clickTab", function(cm) {
var p0 = cm.charCoords(Pos(0, 0));
eqPos(cm.coordsChar({left: p0.left + 5, top: p0.top + 5}), Pos(0, 0));
@ -1141,7 +1206,7 @@ testCM("verticalMovementCommandsWrapping", function(cm) {
testCM("rtlMovement", function(cm) {
forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج",
"خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر",
"خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق",
"<img src=\"/בדיקה3.jpg\">"], function(line) {
var inv = line.charAt(0) == "خ";
cm.setValue(line + "\n"); cm.execCommand(inv ? "goLineEnd" : "goLineStart");
@ -1414,6 +1479,21 @@ testCM("dirtyBit", function(cm) {
eq(cm.isClean(), true);
});
testCM("changeGeneration", function(cm) {
cm.replaceSelection("x", null, "+insert");
var softGen = cm.changeGeneration();
cm.replaceSelection("x", null, "+insert");
cm.undo();
eq(cm.getValue(), "");
is(!cm.isClean(softGen));
cm.replaceSelection("x", null, "+insert");
var hardGen = cm.changeGeneration(true);
cm.replaceSelection("x", null, "+insert");
cm.undo();
eq(cm.getValue(), "x");
is(cm.isClean(hardGen));
});
testCM("addKeyMap", function(cm) {
function sendKey(code) {
cm.triggerOnKeyDown({type: "keydown", keyCode: code,
@ -1560,3 +1640,22 @@ testCM("lineStyleFromMode", function(cm) {
eq(parenElts[0].nodeName, "DIV");
is(!/parens.*parens/.test(parenElts[0].className));
}, {value: "line1: [br] [br]\nline2: (par) (par)\nline3: nothing"});
CodeMirror.registerHelper("xxx", "a", "A");
CodeMirror.registerHelper("xxx", "b", "B");
CodeMirror.defineMode("yyy", function() {
return {
token: function(stream) { stream.skipToEnd(); },
xxx: ["a", "b", "q"]
};
});
CodeMirror.registerGlobalHelper("xxx", "c", function(m) { return m.enableC; }, "C");
testCM("helpers", function(cm) {
cm.setOption("mode", "yyy");
eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "A/B");
cm.setOption("mode", {name: "yyy", modeProps: {xxx: "b", enableC: true}});
eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "B/C");
cm.setOption("mode", "javascript");
eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "");
});

View File

@ -96,6 +96,12 @@ function copyCursor(cur) {
return { ch: cur.ch, line: cur.line };
}
function forEach(arr, func) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
function testVim(name, run, opts, expectedFail) {
var vimOpts = {
lineNumbers: true,
@ -189,7 +195,7 @@ function testVim(name, run, opts, expectedFail) {
run(cm, vim, helpers);
successful = true;
} finally {
if ((debug && !successful) || verbose) {
if (!successful || verbose) {
place.style.visibility = "visible";
} else {
place.removeChild(cm.getWrapperElement());
@ -1047,7 +1053,7 @@ testVim('ctrl-x', function(cm, vim, helpers) {
eq('-3', cm.getValue());
}, {value: '0'});
testVim('<C-x>/<C-a> search forward', function(cm, vim, helpers) {
['<C-x>', '<C-a>'].forEach(function(key) {
forEach(['<C-x>', '<C-a>'], function(key) {
cm.setCursor(0, 0);
helpers.doKeys(key);
helpers.assertCursorAt(0, 5);
@ -1997,6 +2003,33 @@ testVim('zt==z<CR>', function(cm, vim, helpers){
eq(zVals[2], zVals[5]);
});
var scrollMotionSandbox =
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
'\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
testVim('scrollMotion', function(cm, vim, helpers){
var prevCursor, prevScrollInfo;
cm.setCursor(0, 0);
// ctrl-y at the top of the file should have no effect.
helpers.doKeys('<C-y>');
eq(0, cm.getCursor().line);
prevScrollInfo = cm.getScrollInfo();
helpers.doKeys('<C-e>');
eq(1, cm.getCursor().line);
eq(true, prevScrollInfo.top < cm.getScrollInfo().top);
// Jump to the end of the sandbox.
cm.setCursor(1000, 0);
prevCursor = cm.getCursor();
// ctrl-e at the bottom of the file should have no effect.
helpers.doKeys('<C-e>');
eq(prevCursor.line, cm.getCursor().line);
prevScrollInfo = cm.getScrollInfo();
helpers.doKeys('<C-y>');
eq(prevCursor.line - 1, cm.getCursor().line);
eq(true, prevScrollInfo.top > cm.getScrollInfo().top);
}, { value: scrollMotionSandbox});
var squareBracketMotionSandbox = ''+
'({\n'+//0
' ({\n'+//11
@ -2080,7 +2113,7 @@ testVim('[(, ])', function(cm, vim, helpers) {
helpers.assertCursorAt(8,0);
}, { value: squareBracketMotionSandbox});
testVim('[*, ]*, [/, ]/', function(cm, vim, helpers) {
['*', '/'].forEach(function(key){
forEach(['*', '/'], function(key){
cm.setCursor(7, 0);
helpers.doKeys('2', '[', key);
helpers.assertCursorAt(2,2);
@ -2368,6 +2401,26 @@ testVim('ex_map_key2ex', function(cm, vim, helpers) {
eq(written, true);
eq(actualCm, cm);
});
testVim('ex_map_key2key_visual_api', function(cm, vim, helpers) {
CodeMirror.Vim.map('b', ':w', 'visual');
var tmp = CodeMirror.commands.save;
var written = false;
var actualCm;
CodeMirror.commands.save = function(cm) {
written = true;
actualCm = cm;
};
// Mapping should not work in normal mode.
helpers.doKeys('b');
eq(written, false);
// Mapping should work in visual mode.
helpers.doKeys('v', 'b');
eq(written, true);
eq(actualCm, cm);
CodeMirror.commands.save = tmp;
});
// Testing registration of functions as ex-commands and mapping to <Key>-keys
testVim('ex_api_test', function(cm, vim, helpers) {
var res=false;

View File

@ -100,12 +100,15 @@
progress = document.getElementById("progress"),
progressRan = document.getElementById("progress_ran").childNodes[0],
progressTotal = document.getElementById("progress_total").childNodes[0];
var count = 0,
failed = 0,
skipped = 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
quit = false, // Flag to quit tests ASAP
verbose = false, // Adds message for *every* test to output
phantom = false;
function runHarness(){
if (running) {
@ -114,19 +117,25 @@
setTimeout(function(){runHarness();}, 500);
return;
}
filters = [];
verbose = false;
if (window.location.hash.substr(1)){
debug = window.location.hash.substr(1).split(",");
} else {
debug = null;
var strings = window.location.hash.substr(1).split(",");
while (strings.length) {
var s = strings.shift();
if (s === "verbose")
verbose = true;
else
filters.push(parseTestFilter(decodeURIComponent(s)));
}
}
quit = false;
running = true;
setStatus("Loading tests...");
count = 0;
failed = 0;
skipped = 0;
bad = "";
verbose = false;
debugUsed = Array();
totalTests = countTests();
progressTotal.nodeValue = " of " + totalTests;
progressRan.nodeValue = count;
@ -159,12 +168,16 @@
}
function displayTest(type, name, customMessage) {
var message = "???";
if (type != "done") ++count;
if (type != "done" && type != "skipped") ++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 == "skipped") {
message = "Test '" + name + "' skipped";
++skipped;
if (!verbose) customMessage = false;
} else if (type == "expected") {
message = "Test '" + name + "' failed as expected";
if (!verbose) customMessage = false;
@ -182,15 +195,11 @@
} else {
type += " ok";
message = "All passed";
if (skipped) {
message += " (" + skipped + " skipped)";
}
}
if (debug && debug.length) {
var bogusTests = totalTests - count;
message += " — " + bogusTests + " nonexistent test" +
(bogusTests > 1 ? "s" : "") + " requested by location.hash: " +
"`" + debug.join("`, `") + "`";
} else {
progressTotal.nodeValue = '';
}
progressTotal.nodeValue = '';
customMessage = true; // Hack to avoid adding to output
}
if (verbose && !customMessage) customMessage = message;

View File

@ -100,8 +100,10 @@
progress = document.getElementById("progress"),
progressRan = document.getElementById("progress_ran").childNodes[0],
progressTotal = document.getElementById("progress_total").childNodes[0];
var count = 0,
failed = 0,
skipped = 0,
bad = "",
running = false, // Flag that states tests are running
quit = false, // Flag to quit tests ASAP
@ -115,19 +117,25 @@
setTimeout(function(){runHarness();}, 500);
return;
}
filters = [];
verbose = false;
if (window.location.hash.substr(1)){
debug = window.location.hash.substr(1).split(",");
} else {
debug = null;
var strings = window.location.hash.substr(1).split(",");
while (strings.length) {
var s = strings.shift();
if (s === "verbose")
verbose = true;
else
filters.push(parseTestFilter(decodeURIComponent(s)));
}
}
quit = false;
running = true;
setStatus("Loading tests...");
count = 0;
failed = 0;
skipped = 0;
bad = "";
verbose = false;
debugUsed = Array();
totalTests = countTests();
progressTotal.nodeValue = " of " + totalTests;
progressRan.nodeValue = count;
@ -160,12 +168,16 @@
}
function displayTest(type, name, customMessage) {
var message = "???";
if (type != "done") ++count;
if (type != "done" && type != "skipped") ++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 == "skipped") {
message = "Test '" + name + "' skipped";
++skipped;
if (!verbose) customMessage = false;
} else if (type == "expected") {
message = "Test '" + name + "' failed as expected";
if (!verbose) customMessage = false;
@ -183,15 +195,11 @@
} else {
type += " ok";
message = "All passed";
if (skipped) {
message += " (" + skipped + " skipped)";
}
}
if (debug && debug.length) {
var bogusTests = totalTests - count;
message += " — " + bogusTests + " nonexistent test" +
(bogusTests > 1 ? "s" : "") + " requested by location.hash: " +
"`" + debug.join("`, `") + "`";
} else {
progressTotal.nodeValue = '';
}
progressTotal.nodeValue = '';
customMessage = true; // Hack to avoid adding to output
}
if (verbose && !customMessage) customMessage = message;

View File

@ -113,7 +113,6 @@ support-files =
[browser_bug_871156_ctrlw_close_tab.js]
[browser_cached_messages.js]
[browser_console.js]
skip-if = os == "linux" # Intermittent failures, bug 952865
[browser_console_addonsdk_loader_exception.js]
[browser_console_clear_on_reload.js]
[browser_console_consolejsm_output.js]

View File

@ -50,50 +50,39 @@ function consoleOpened(hud)
xhr.open("get", TEST_URI, true);
xhr.send();
let chromeConsole = -1;
let contentConsole = -1;
let execValue = -1;
let exception = -1;
let xhrRequest = false;
let output = hud.outputNode;
function performChecks()
{
let text = output.textContent;
chromeConsole = text.indexOf("bug587757a");
contentConsole = text.indexOf("bug587757b");
execValue = text.indexOf("browser.xul");
exception = text.indexOf("foobarExceptionBug587757");
xhrRequest = text.indexOf("test-console.html");
}
function showResults()
{
isnot(chromeConsole, -1, "chrome window console.log() is displayed");
isnot(contentConsole, -1, "content window console.log() is displayed");
isnot(execValue, -1, "jsterm eval result is displayed");
isnot(exception, -1, "exception is displayed");
isnot(xhrRequest, -1, "xhr request is displayed");
}
waitForSuccess({
name: "messages displayed",
validatorFn: () => {
performChecks();
return chromeConsole > -1 &&
contentConsole > -1 &&
execValue > -1 &&
exception > -1 &&
xhrRequest > -1;
},
successFn: () => {
showResults();
executeSoon(finishTest);
},
failureFn: () => {
showResults();
info("output: " + output.textContent);
executeSoon(finishTest);
},
});
waitForMessages({
webconsole: hud,
messages: [
{
name: "chrome window console.log() is displayed",
text: "bug587757a",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
},
{
name: "content window console.log() is displayed",
text: "bug587757b",
category: CATEGORY_WEBDEV,
severity: SEVERITY_LOG,
},
{
name: "jsterm eval result",
text: "browser.xul",
category: CATEGORY_OUTPUT,
severity: SEVERITY_LOG,
},
{
name: "exception message",
text: "foobarExceptionBug587757",
category: CATEGORY_JS,
severity: SEVERITY_ERROR,
},
{
name: "network message",
text: "test-console.html",
category: CATEGORY_NETWORK,
severity: SEVERITY_LOG,
},
],
}).then(finishTest);
}

View File

@ -11,6 +11,8 @@ const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
function test()
{
requestLongerTimeout(2);
let webconsole, browserconsole;
addTab(TEST_URI);

View File

@ -41,11 +41,15 @@ var Bookmarks = {
});
},
_isMetroBookmark: function(aItemId) {
return PlacesUtils.bookmarks.getFolderIdForItem(aItemId) == Bookmarks.metroRoot;
},
isURIBookmarked: function bh_isURIBookmarked(aURI, callback) {
if (!callback)
return;
PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
callback(aItemIds && aItemIds.length > 0);
callback(aItemIds && aItemIds.length > 0 && aItemIds.some(this._isMetroBookmark));
}, this);
},
@ -53,10 +57,15 @@ var Bookmarks = {
// XXX blargle xpconnect! might not matter, but a method on
// nsINavBookmarksService that takes an array of items to
// delete would be faster. better yet, a method that takes a URI!
PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
aItemIds.forEach(PlacesUtils.bookmarks.removeItem);
PlacesUtils.asyncGetBookmarkIds(aURI, (aItemIds) => {
aItemIds.forEach((aItemId) => {
if (this._isMetroBookmark(aItemId)) {
PlacesUtils.bookmarks.removeItem(aItemId);
}
});
if (callback)
callback(aURI, aItemIds);
callback();
// XXX Used for browser-chrome tests
let event = document.createEvent("Events");

View File

@ -1104,6 +1104,9 @@ var BrowserUI = {
let message = bundle.GetStringFromName("clearPrivateData.message");
let clearbutton = bundle.GetStringFromName("clearPrivateData.clearButton");
let prefsClearButton = document.getElementById("prefs-clear-data");
prefsClearButton.disabled = true;
let buttonPressed = Services.prompt.confirmEx(
null,
title,
@ -1120,6 +1123,8 @@ var BrowserUI = {
if (buttonPressed === 0) {
SanitizeUI.onSanitize();
}
prefsClearButton.disabled = false;
},
};

View File

@ -35,7 +35,9 @@ gTests.push({
sendNativeTap(node);
}
yield waitForMs(100);
yield waitForCondition2(function () {
return Browser.selectedTab.browser.contentWindow.document.getElementById("opt9").selected;
}, "waiting for last option to select");
// check the menu state
for (let node of SelectHelperUI._listbox.childNodes) {
@ -53,7 +55,9 @@ gTests.push({
sendNativeTap(node);
}
yield waitForMs(100);
yield waitForCondition2(function () {
return !Browser.selectedTab.browser.contentWindow.document.getElementById("opt9").selected;
}, "waiting for last option to deselect");
// check the menu state
for (let node of SelectHelperUI._listbox.childNodes) {

View File

@ -1894,7 +1894,7 @@ chatbox {
}
#main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
padding: 2em;
padding: 0 2em 2em;
}
#main-window[customize-entered] #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -41,16 +41,6 @@
background-color: blue;
}
#placesList > treechildren::-moz-tree-cell(leaf) ,
#placesList > treechildren::-moz-tree-image(leaf) {
cursor: pointer;
}
#placesList > treechildren::-moz-tree-cell-text(leaf, hover) {
cursor: pointer;
text-decoration: underline;
}
#placesList > treechildren::-moz-tree-cell(separator) {
cursor: default;
}

View File

@ -282,6 +282,14 @@ toolbarpaletteitem[place="palette"] > toolbaritem > toolbarbutton {
box-shadow: none;
}
#PanelUI-quit:not([disabled]):hover {
background-color: #d94141;
}
#PanelUI-quit:not([disabled]):hover:active {
background-color: #ad3434;
}
#main-window[customize-entered] #PanelUI-customize {
color: white;
background-image: linear-gradient(rgb(41,123,204), rgb(40,133,203));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 849 B

After

Width:  |  Height:  |  Size: 568 B

View File

@ -2851,6 +2851,10 @@ nsDocumentViewer::SetTextZoom(float aTextZoom)
// And do the external resources
mDocument->EnumerateExternalResources(SetExtResourceTextZoom, &ZoomInfo);
nsContentUtils::DispatchChromeEvent(mDocument, static_cast<nsIDocument*>(mDocument),
NS_LITERAL_STRING("TextZoomChange"),
true, true);
return NS_OK;
}
@ -2954,6 +2958,10 @@ nsDocumentViewer::SetFullZoom(float aFullZoom)
// And do the external resources
mDocument->EnumerateExternalResources(SetExtResourceFullZoom, &ZoomInfo);
nsContentUtils::DispatchChromeEvent(mDocument, static_cast<nsIDocument*>(mDocument),
NS_LITERAL_STRING("FullZoomChange"),
true, true);
return NS_OK;
}

View File

@ -814,8 +814,9 @@ pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
// URL used to ping metrics with stats about which snippets have been shown
pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
// This pref requires a restart to take effect.
// These prefs require a restart to take effect.
pref("browser.snippets.enabled", false);
pref("browser.snippets.syncPromo.enabled", false);
#ifdef MOZ_ANDROID_SYNTHAPKS
// The URL of the APK factory from which we obtain APKs for webapps.

View File

@ -12,6 +12,7 @@ import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.LoadFaviconTask;
import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.GeckoLayerClient;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
@ -595,6 +596,9 @@ abstract public class BrowserApp extends GeckoApp
return true;
}
});
// Set the maximum bits-per-pixel the favicon system cares about.
IconDirectoryEntry.setMaxBPP(GeckoAppShell.getScreenDepth());
}
@Override
@ -788,10 +792,10 @@ abstract public class BrowserApp extends GeckoApp
final OnFaviconLoadedListener listener = new GeckoAppShell.CreateShortcutFaviconLoadedListener(url, title);
Favicons.getSizedFavicon(url,
tab.getFaviconURL(),
Integer.MAX_VALUE,
LoadFaviconTask.FLAG_PERSIST,
listener);
tab.getFaviconURL(),
Integer.MAX_VALUE,
LoadFaviconTask.FLAG_PERSIST,
listener);
return true;
}
@ -1417,14 +1421,6 @@ abstract public class BrowserApp extends GeckoApp
int id = Favicons.getSizedFavicon(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags, sFaviconLoadedListener);
tab.setFaviconLoadId(id);
final Tabs tabs = Tabs.getInstance();
if (id != Favicons.LOADED && tabs.isSelectedTab(tab)) {
// We're loading the current tab's favicon from somewhere
// other than the cache. Display the globe favicon until then.
tab.updateFavicon(Favicons.sDefaultFavicon);
tabs.notifyListeners(tab, Tabs.TabEvents.FAVICON);
}
}
private void maybeCancelFaviconLoad(Tab tab) {
@ -2050,6 +2046,8 @@ abstract public class BrowserApp extends GeckoApp
if (isDynamicToolbarEnabled()) {
mLayerView.getLayerMarginsAnimator().hideMargins(true);
mLayerView.getLayerMarginsAnimator().setMaxMargins(0, 0, 0, 0);
} else {
setToolbarMargin(0);
}
} else {
mViewFlipper.setVisibility(View.VISIBLE);

View File

@ -69,6 +69,7 @@ public class Tab {
private Context mAppContext;
private ErrorType mErrorType = ErrorType.NONE;
private static final int MAX_HISTORY_LIST_SIZE = 50;
private int mLoadProgress;
public static final int STATE_DELAYED = 0;
public static final int STATE_LOADING = 1;
@ -764,4 +765,22 @@ public class Tab {
public boolean isPrivate() {
return false;
}
/**
* Sets the tab load progress to the given percentage.
*
* @param progressPercentage Percentage to set progress to (0-100)
*/
void setLoadProgress(int progressPercentage) {
mLoadProgress = progressPercentage;
}
/**
* Gets the tab load progress percentage.
*
* @return Current progress percentage
*/
public int getLoadProgress() {
return mLoadProgress;
}
}

View File

@ -52,6 +52,10 @@ public class Tabs implements GeckoEventListener {
private AccountManager mAccountManager;
private OnAccountsUpdateListener mAccountListener = null;
private static final int LOAD_PROGRESS_START = 20;
private static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
private static final int LOAD_PROGRESS_LOADED = 80;
public static final int LOADURL_NONE = 0;
public static final int LOADURL_NEW_TAB = 1 << 0;
public static final int LOADURL_USER_ENTERED = 1 << 1;
@ -435,6 +439,7 @@ public class Tabs implements GeckoEventListener {
} else if (event.equals("Tab:Select")) {
selectTab(tab.getId());
} else if (event.equals("Content:LocationChange")) {
tab.setLoadProgress(LOAD_PROGRESS_LOCATION_CHANGE);
tab.handleLocationChange(message);
} else if (event.equals("Content:SecurityChange")) {
tab.updateIdentityData(message.getJSONObject("identity"));
@ -448,6 +453,7 @@ public class Tabs implements GeckoEventListener {
if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
boolean showProgress = message.getBoolean("showProgress");
tab.handleDocumentStart(showProgress, message.getString("uri"));
tab.setLoadProgress(LOAD_PROGRESS_START);
notifyListeners(tab, Tabs.TabEvents.START);
} else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
tab.handleDocumentStop(message.getBoolean("success"));
@ -455,6 +461,7 @@ public class Tabs implements GeckoEventListener {
}
}
} else if (event.equals("Content:LoadError")) {
tab.setLoadProgress(LOAD_PROGRESS_LOADED);
notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
} else if (event.equals("Content:PageShow")) {
notifyListeners(tab, TabEvents.PAGE_SHOW);
@ -467,6 +474,7 @@ public class Tabs implements GeckoEventListener {
tab.setBackgroundColor(Color.WHITE);
}
tab.setErrorType(message.optString("errorType"));
tab.setLoadProgress(LOAD_PROGRESS_LOADED);
notifyListeners(tab, Tabs.TabEvents.LOADED);
} else if (event.equals("DOMTitleChanged")) {
tab.updateTitle(message.getString("title"));

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.db;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.content.ContentResolver;
@ -94,11 +95,11 @@ public class BrowserDB {
public void removeReadingListItemWithURL(ContentResolver cr, String uri);
public Bitmap getFaviconForUrl(ContentResolver cr, String uri);
public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String uri);
public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri);
public void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri);
public void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
@ -257,7 +258,7 @@ public class BrowserDB {
sDb.removeReadingListItemWithURL(cr, uri);
}
public static Bitmap getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
public static LoadFaviconResult getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
return sDb.getFaviconForUrl(cr, faviconURL);
}
@ -265,8 +266,8 @@ public class BrowserDB {
return sDb.getFaviconUrlForHistoryUrl(cr, url);
}
public static void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri) {
sDb.updateFaviconForUrl(cr, pageUri, favicon, faviconUri);
public static void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri) {
sDb.updateFaviconForUrl(cr, pageUri, encodedFavicon, faviconUri);
}
public static void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail) {

View File

@ -20,7 +20,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
public class FormHistoryProvider extends PerProfileContentProvider {
public class FormHistoryProvider extends SQLiteBridgeContentProvider {
static final String TABLE_FORM_HISTORY = "moz_formhistory";
static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory";

View File

@ -15,6 +15,8 @@ import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.SyncColumns;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.gfx.BitmapUtils;
import android.content.ContentProviderOperation;
@ -708,7 +710,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
* @return The decoded Bitmap from the database, if any. null if none is stored.
*/
@Override
public Bitmap getFaviconForUrl(ContentResolver cr, String faviconURL) {
public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
Cursor c = null;
byte[] b = null;
@ -735,7 +737,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
return null;
}
return BitmapUtils.decodeByteArray(b);
return FaviconDecoder.decodeFavicon(b);
}
@Override
@ -761,19 +763,11 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
@Override
public void updateFaviconForUrl(ContentResolver cr, String pageUri,
Bitmap favicon, String faviconUri) {
byte[] encodedFavicon, String faviconUri) {
ContentValues values = new ContentValues();
values.put(Favicons.URL, faviconUri);
values.put(Favicons.PAGE_URL, pageUri);
byte[] data = null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
data = stream.toByteArray();
} else {
Log.w(LOGTAG, "Favicon compression failed.");
}
values.put(Favicons.DATA, data);
values.put(Favicons.DATA, encodedFavicon);
// Update or insert
Uri faviconsUri = getAllFaviconsUri().buildUpon().

View File

@ -25,7 +25,7 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class PasswordsProvider extends PerProfileContentProvider {
public class PasswordsProvider extends SQLiteBridgeContentProvider {
static final String TABLE_PASSWORDS = "moz_logins";
static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins";

View File

@ -34,12 +34,12 @@ import android.util.Log;
* public abstract void initGecko();
*/
public abstract class PerProfileContentProvider extends ContentProvider {
public abstract class SQLiteBridgeContentProvider extends ContentProvider {
private HashMap<String, SQLiteBridge> mDatabasePerProfile;
protected Context mContext = null;
private final String mLogTag;
protected PerProfileContentProvider(String logTag) {
protected SQLiteBridgeContentProvider(String logTag) {
mLogTag = logTag;
}

View File

@ -59,6 +59,9 @@ public class Favicons {
// The density-adjusted default Favicon dimensions.
public static int sDefaultFaviconSize;
// The density-adjusted maximum Favicon dimensions.
public static int sLargestFaviconSize;
private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>());
// Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
@ -290,10 +293,22 @@ public class Favicons {
sFaviconsCache.putSingleFavicon(pageUrl, image);
}
/**
* Adds the bitmaps given by the specified iterator to the cache associated with the url given.
* Future requests for images will be able to select the least larger image than the target
* size from this new set of images.
*
* @param pageUrl The URL to associate the new favicons with.
* @param images An iterator over the new favicons to put in the cache.
*/
public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) {
sFaviconsCache.putFavicons(pageUrl, images, permanently);
}
public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
putFaviconsInMemCache(pageUrl, images, false);
}
public static void clearMemCache() {
sFaviconsCache.evictAll();
sPageURLMappings.evictAll();
@ -366,7 +381,11 @@ public class Favicons {
}
sDefaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, res.getDimensionPixelSize(R.dimen.favicon_largest_interesting_size));
// Screen-density-adjusted upper limit on favicon size. Favicons larger than this are
// downscaled to this size or discarded.
sLargestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, sLargestFaviconSize);
// Initialize page mappings for each of our special pages.
for (String url : AboutPages.getDefaultIconPages()) {

View File

@ -15,10 +15,10 @@ import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.entity.BufferedHttpEntity;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
@ -32,7 +32,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class representing the asynchronous task to load a Favicon which is not currently in the in-memory
@ -50,6 +49,9 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
public static final int FLAG_PERSIST = 1;
public static final int FLAG_SCALE = 2;
private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
// The default size of the buffer to use for downloading Favicons in the event no size is given
// by the server.
private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
private int mId;
@ -88,19 +90,19 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
}
// Runs in background thread
private Bitmap loadFaviconFromDB() {
private LoadFaviconResult loadFaviconFromDb() {
ContentResolver resolver = sContext.getContentResolver();
return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
}
// Runs in background thread
private void saveFaviconToDb(final Bitmap favicon) {
private void saveFaviconToDb(final byte[] encodedFavicon) {
if ((mFlags & FLAG_PERSIST) == 0) {
return;
}
ContentResolver resolver = sContext.getContentResolver();
BrowserDB.updateFaviconForUrl(resolver, mPageUrl, favicon, mFaviconUrl);
BrowserDB.updateFaviconForUrl(resolver, mPageUrl, encodedFavicon, mFaviconUrl);
}
/**
@ -182,7 +184,7 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
// Runs in background thread.
// Does not attempt to fetch from JARs.
private Bitmap downloadFavicon(URI targetFaviconURI) {
private LoadFaviconResult downloadFavicon(URI targetFaviconURI) {
if (targetFaviconURI == null) {
return null;
}
@ -193,38 +195,85 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
return null;
}
Bitmap image = null;
LoadFaviconResult result = null;
// skia decoder sometimes returns null; workaround is to use BufferedHttpEntity
// http://groups.google.com/group/android-developers/browse_thread/thread/171b8bf35dbbed96/c3ec5f45436ceec8?lnk=raot
try {
// Try the URL we were given.
HttpResponse response = tryDownload(targetFaviconURI);
if (response == null) {
return null;
}
HttpEntity entity = response.getEntity();
if (entity == null) {
return null;
}
BufferedHttpEntity bufferedEntity = new BufferedHttpEntity(entity);
InputStream contentStream = null;
try {
contentStream = bufferedEntity.getContent();
image = BitmapUtils.decodeStream(contentStream);
contentStream.close();
} finally {
if (contentStream != null) {
contentStream.close();
}
}
result = downloadAndDecodeImage(targetFaviconURI);
} catch (Exception e) {
Log.e(LOGTAG, "Error reading favicon", e);
}
return image;
return result;
}
/**
* Download the Favicon from the given URL and pass it to the decoder function.
*
* @param targetFaviconURL URL of the favicon to download.
* @return A LoadFaviconResult containing the bitmap(s) extracted from the downloaded file, or
* null if no or corrupt data ware received.
* @throws IOException If attempts to fully read the stream result in such an exception, such as
* in the event of a transient connection failure.
* @throws URISyntaxException If the underlying call to tryDownload retries and raises such an
* exception trying a fallback URL.
*/
private LoadFaviconResult downloadAndDecodeImage(URI targetFaviconURL) throws IOException, URISyntaxException {
// Try the URL we were given.
HttpResponse response = tryDownload(targetFaviconURL);
if (response == null) {
return null;
}
HttpEntity entity = response.getEntity();
if (entity == null) {
return null;
}
// This may not be provided, but if it is, it's useful.
final long entityReportedLength = entity.getContentLength();
int bufferSize;
if (entityReportedLength > 0) {
// The size was reported and sane, so let's use that.
// Integer overflow should not be a problem for Favicon sizes...
bufferSize = (int) entityReportedLength + 1;
} else {
// No declared size, so guess and reallocate later if it turns out to be too small.
bufferSize = DEFAULT_FAVICON_BUFFER_SIZE;
}
// Allocate a buffer to hold the raw favicon data downloaded.
byte[] buffer = new byte[bufferSize];
// The offset of the start of the buffer's free space.
int bPointer = 0;
// The quantity of bytes the last call to read yielded.
int lastRead = 0;
InputStream contentStream = entity.getContent();
try {
// Fully read the entity into the buffer - decoding of streams is not supported
// (and questionably pointful - what would one do with a half-decoded Favicon?)
while (lastRead != -1) {
// Read as many bytes as are currently available into the buffer.
lastRead = contentStream.read(buffer, bPointer, buffer.length - bPointer);
bPointer += lastRead;
// If buffer has overflowed, double its size and carry on.
if (bPointer == buffer.length) {
bufferSize *= 2;
byte[] newBuffer = new byte[bufferSize];
// Copy the contents of the old buffer into the new buffer.
System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
buffer = newBuffer;
}
}
} finally {
contentStream.close();
}
// Having downloaded the image, decode it.
return FaviconDecoder.decodeFavicon(buffer, 0, bPointer + 1);
}
@Override
@ -299,9 +348,10 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
return null;
}
image = loadFaviconFromDB();
if (imageIsValid(image)) {
return image;
// If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
LoadFaviconResult loadedBitmaps = loadFaviconFromDb();
if (loadedBitmaps != null) {
return pushToCacheAndGetResult(loadedBitmaps);
}
if (mOnlyFromLocal || isCancelled()) {
@ -310,13 +360,14 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
// Let's see if it's in a JAR.
image = fetchJARFavicon(mFaviconUrl);
if (image != null) {
if (imageIsValid(image)) {
// We don't want to put this into the DB.
Favicons.putFaviconInMemCache(mFaviconUrl, image);
return image;
}
try {
image = downloadFavicon(new URI(mFaviconUrl));
loadedBitmaps = downloadFavicon(new URI(mFaviconUrl));
} catch (URISyntaxException e) {
Log.e(LOGTAG, "The provided favicon URL is not valid");
return null;
@ -324,9 +375,9 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
Log.e(LOGTAG, "Couldn't download favicon.", e);
}
if (imageIsValid(image)) {
saveFaviconToDb(image);
return image;
if (loadedBitmaps != null) {
saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
return pushToCacheAndGetResult(loadedBitmaps);
}
if (isUsingDefaultURL) {
@ -334,6 +385,10 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
return null;
}
if (isCancelled()) {
return null;
}
// If we're not already trying the default URL, try it now.
final String guessed = Favicons.guessDefaultFaviconURL(mPageUrl);
if (guessed == null) {
@ -344,24 +399,40 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
image = fetchJARFavicon(guessed);
if (imageIsValid(image)) {
// We don't want to put this into the DB.
Favicons.putFaviconInMemCache(mFaviconUrl, image);
return image;
}
try {
image = downloadFavicon(new URI(guessed));
loadedBitmaps = downloadFavicon(new URI(guessed));
} catch (Exception e) {
// Not interesting. It was an educated guess, anyway.
return null;
}
if (imageIsValid(image)) {
saveFaviconToDb(image);
return image;
if (loadedBitmaps != null) {
saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
return pushToCacheAndGetResult(loadedBitmaps);
}
return null;
}
/**
* Helper method to put the result of a favicon load into the memory cache and then query the
* cache for the particular bitmap we want for this request.
* This call is certain to succeed, provided there was enough memory to decode this favicon.
*
* @param loadedBitmaps LoadFaviconResult to store.
* @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if
* we are under extreme memory pressure and find ourselves dropping the cache immediately.
*/
private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) {
Favicons.putFaviconsInMemCache(mFaviconUrl, loadedBitmaps.getBitmaps());
Bitmap result = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
return result;
}
private static boolean imageIsValid(final Bitmap image) {
return image != null &&
image.getWidth() > 0 &&
@ -374,9 +445,6 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
return;
}
// Put what we got in the memcache.
Favicons.putFaviconInMemCache(mFaviconUrl, image);
// Process the result, scale for the listener, etc.
processResult(image);
@ -397,6 +465,8 @@ public class LoadFaviconTask extends UiAsyncTask<Void, Void, Bitmap> {
// Share the result with all chained tasks.
if (mChainees != null) {
for (LoadFaviconTask t : mChainees) {
// In the case that we just decoded multiple favicons, either we're passing the right
// image now, or the call into the cache in processResult will fetch the right one.
t.processResult(image);
}
}

View File

@ -43,8 +43,12 @@ public class FaviconsForURL {
FaviconCacheElement c = new FaviconCacheElement(favicon, isPrimary, imageSize, this);
int index = Collections.binarySearch(mFavicons, c);
// binarySearch returns -x - 1 where x is the insertion point of the element. Convert
// this to the actual insertion point..
if (index < 0) {
index = 0;
index++;
index = -index;
}
mFavicons.add(index, c);
@ -106,8 +110,11 @@ public class FaviconsForURL {
if (element.mIsPrimary) {
if (element.mInvalidated) {
// TODO: Replace with `return null` when ICO decoder is introduced.
break;
// We return null here, despite the possible existence of other primaries,
// because we know the most suitable primary for this request exists, but is
// no longer in the cache. By returning null, we cause the caller to load the
// missing primary from the database and call again.
return null;
}
return element;
}

View File

@ -0,0 +1,158 @@
/* 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/. */
package org.mozilla.gecko.favicons.decoders;
import android.graphics.Bitmap;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.gfx.BitmapUtils;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Class providing static utility methods for decoding favicons.
*/
public class FaviconDecoder {
static enum ImageMagicNumbers {
// It is irritating that Java bytes are signed...
PNG(new byte[] {(byte) (0x89 & 0xFF), 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}),
GIF(new byte[] {0x47, 0x49, 0x46, 0x38}),
JPEG(new byte[] {-0x1, -0x28, -0x1, -0x20}),
BMP(new byte[] {0x42, 0x4d}),
WEB(new byte[] {0x57, 0x45, 0x42, 0x50, 0x0a});
public byte[] value;
private ImageMagicNumbers(byte[] value) {
this.value = value;
}
}
/**
* Check for image format magic numbers of formats supported by Android.
* @param buffer Byte buffer to check for magic numbers
* @param offset Offset at which to look for magic numbers.
* @return true if the buffer contains a bitmap decodable by Android (Or at least, a sequence
* starting with the magic numbers thereof). false otherwise.
*/
private static boolean isDecodableByAndroid(byte[] buffer, int offset) {
for (ImageMagicNumbers m : ImageMagicNumbers.values()) {
if (bufferStartsWith(buffer, m.value, offset)) {
return true;
}
}
return false;
}
/**
* Utility function to check for the existence of a test byte sequence at a given offset in a
* buffer.
*
* @param buffer Byte buffer to search.
* @param test Byte sequence to search for.
* @param bufferOffset Index in input buffer to expect test sequence.
* @return true if buffer contains the byte sequence given in test at offset bufferOffset, false
* otherwise.
*/
static boolean bufferStartsWith(byte[] buffer, byte[] test, int bufferOffset) {
if (buffer.length < test.length) {
return false;
}
for (int i = 0; i < test.length; ++i) {
if (buffer[bufferOffset + i] != test[i]) {
return false;
}
}
return true;
}
/**
* Decode the favicon present in the region of the provided byte[] starting at offset and
* proceeding for length bytes, if any. Returns either the resulting LoadFaviconResult or null if the
* given range does not contain a bitmap we know how to decode.
*
* @param buffer Byte array containing the favicon to decode.
* @param offset The index of the first byte in the array of the region of interest.
* @param length The length of the region in the array to decode.
* @return The decoded version of the bitmap in the described region, or null if none can be
* decoded.
*/
public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) {
LoadFaviconResult result;
if (isDecodableByAndroid(buffer, offset)) {
result = new LoadFaviconResult();
result.mOffset = offset;
result.mLength = length;
result.mHasMultipleBitmaps = false;
// We assume here that decodeByteArray doesn't hold on to the entire supplied
// buffer -- worst case, each of our buffers will be twice the necessary size.
result.mBitmapsDecoded = new SingleBitmapIterator(BitmapUtils.decodeByteArray(buffer, offset, length));
result.mFaviconBytes = buffer;
return result;
}
// If it's not decodable by Android, it might be an ICO. Let's try.
ICODecoder decoder = new ICODecoder(buffer, offset, length);
result = decoder.decode();
if (result == null) {
return null;
}
return result;
}
public static LoadFaviconResult decodeFavicon(byte[] buffer) {
return decodeFavicon(buffer, 0, buffer.length);
}
/**
* Iterator to hold a single bitmap.
*/
static class SingleBitmapIterator implements Iterator<Bitmap> {
private Bitmap mBitmap;
public SingleBitmapIterator(Bitmap b) {
mBitmap = b;
}
/**
* Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
* places where the runtime type of the Iterator under consideration is known and
* destruction of it is discouraged.
*
* @return The bitmap carried by this SingleBitmapIterator.
*/
public Bitmap peek() {
return mBitmap;
}
@Override
public boolean hasNext() {
return mBitmap != null;
}
@Override
public Bitmap next() {
if (mBitmap == null) {
throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
}
Bitmap ret = mBitmap;
mBitmap = null;
return ret;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
}
}
}

View File

@ -0,0 +1,380 @@
/* 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/. */
package org.mozilla.gecko.favicons.decoders;
import android.graphics.Bitmap;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.gfx.BitmapUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* Utility class for determining the region of a provided array which contains the largest bitmap,
* assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
* unwanted entries from ICO files, if desired.
*
* An ICO file is a container format that may hold up to 255 images in either BMP or PNG format.
* A mixture of image types may not exist.
*
* The format consists of a header specifying the number, n, of images, followed by the Icon Directory.
*
* The Icon Directory consists of n Icon Directory Entries, each 16 bytes in length, specifying, for
* the corresponding image, the dimensions, colour information, payload size, and location in the file.
*
* All numerical fields follow a little-endian byte ordering.
*
* Header format:
*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Reserved field. Must be zero | Type (1 for ICO, 2 for CUR) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Image count (n) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* The type field is expected to always be 1. CUR format images should not be used for Favicons.
*
*
* Icon Directory Entry format:
*
* 0 1 2 3
* 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Image width | Image height | Palette size | Reserved (0) |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Colour plane count | Bits per pixel |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Size of image data, in bytes |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Start of image data, as an offset from start of file |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* Image dimensions of zero are to be interpreted as image dimensions of 256.
*
* The palette size field records the number of colours in the stored BMP, if a palette is used. Zero
* if the payload is a PNG or no palette is in use.
*
* The number of colour planes is, usually, 0 (Not in use) or 1. Values greater than 1 are to be
* interpreted not as a colour plane count, but as a multiplying factor on the bits per pixel field.
* (Apparently 65535 was not deemed a sufficiently large maximum value of bits per pixel.)
*
*
* The Icon Directory consists of n-many Icon Directory Entries in sequence, with no gaps.
*
* This class is not thread safe.
*/
public class ICODecoder implements Iterable<Bitmap> {
// The number of bytes that compacting will save for us to bother doing it.
public static final int COMPACT_THRESHOLD = 4000;
// Some geometry of an ICO file.
public static final int ICO_HEADER_LENGTH_BYTES = 6;
public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
// The buffer containing bytes to attempt to decode.
private byte[] mDecodand;
// The region of the decodand to decode.
private int mOffset;
private int mLen;
private IconDirectoryEntry[] mIconDirectory;
private boolean mIsValid;
private boolean mHasDecoded;
public ICODecoder(byte[] buffer, int offset, int len) {
mDecodand = buffer;
mOffset = offset;
mLen = len;
}
/**
* Decode the Icon Directory for this ICO and store the result in mIconDirectory.
*
* @return true if ICO decoding was considered to probably be a success, false if it certainly
* was a failure.
*/
private boolean decodeIconDirectoryAndPossiblyPrune() {
mHasDecoded = true;
// Fail if the end of the described range is out of bounds.
if (mOffset + mLen > mDecodand.length) {
return false;
}
// Fail if we don't have enough space for the header.
if (mLen < ICO_HEADER_LENGTH_BYTES) {
return false;
}
// Check that the reserved fields in the header are indeed zero, and that the type field
// specifies ICO. If not, we've probably been given something that isn't really an ICO.
if (mDecodand[mOffset] != 0 ||
mDecodand[mOffset + 1] != 0 ||
mDecodand[mOffset + 2] != 1 ||
mDecodand[mOffset + 3] != 0) {
return false;
}
// Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
// bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
// interpretation of the byte of interest, we do this.
int numEncodedImages = (mDecodand[mOffset + 4] & 0xFF) |
(mDecodand[mOffset + 5] & 0xFF) << 8;
// Fail if there are no images or the field is corrupt.
if (numEncodedImages <= 0) {
return false;
}
final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
// Fail if there is not enough space in the buffer for the stated number of icondir entries,
// let alone the data.
if (mLen < headerAndDirectorySize) {
return false;
}
// Put the pointer on the first byte of the first Icon Directory Entry.
int bufferIndex = mOffset + ICO_HEADER_LENGTH_BYTES;
// We now iterate over the Icon Directory, decoding each entry as we go. We also need to
// discard all entries except one >= the maximum interesting size.
// Size of the smallest image larger than the limit encountered.
int minimumMaximum = Integer.MAX_VALUE;
// Used to track the best entry for each size. The entries we want to keep.
HashMap<Integer, IconDirectoryEntry> preferenceMap = new HashMap<Integer, IconDirectoryEntry>();
for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
// Decode the Icon Directory Entry at this offset.
IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(mDecodand, mOffset, mLen, bufferIndex);
newEntry.mIndex = i;
if (newEntry.mIsErroneous) {
continue;
}
if (newEntry.mWidth > Favicons.sLargestFaviconSize) {
// If we already have a smaller image larger than the maximum size of interest, we
// don't care about the new one which is larger than the smallest image larger than
// the maximum size.
if (newEntry.mWidth >= minimumMaximum) {
continue;
}
// Remove the previous minimum-maximum.
if (preferenceMap.containsKey(minimumMaximum)) {
preferenceMap.remove(minimumMaximum);
}
minimumMaximum = newEntry.mWidth;
}
IconDirectoryEntry oldEntry = preferenceMap.get(newEntry.mWidth);
if (oldEntry == null) {
preferenceMap.put(newEntry.mWidth, newEntry);
continue;
}
if (oldEntry.compareTo(newEntry) < 0) {
preferenceMap.put(newEntry.mWidth, newEntry);
}
}
Collection<IconDirectoryEntry> entriesRetained = preferenceMap.values();
// Abort if no entries are desired (Perhaps all are corrupt?)
if (entriesRetained.isEmpty()) {
return false;
}
// Allocate space for the icon directory entries in the decoded directory.
mIconDirectory = new IconDirectoryEntry[entriesRetained.size()];
// The size of the data in the buffer that we find useful.
int retainedSpace = ICO_HEADER_LENGTH_BYTES;
int dirInd = 0;
for (IconDirectoryEntry e : entriesRetained) {
retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.mPayloadSize;
mIconDirectory[dirInd] = e;
dirInd++;
}
mIsValid = true;
// Set the number of images field in the buffer to reflect the number of retained entries.
mDecodand[mOffset + 4] = (byte) mIconDirectory.length;
mDecodand[mOffset + 5] = (byte) (mIconDirectory.length >>> 8);
if ((mLen - retainedSpace) > COMPACT_THRESHOLD) {
compactingCopy(retainedSpace);
}
return true;
}
/**
* Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
*/
private void compactingCopy(int spaceRetained) {
byte[] buf = new byte[spaceRetained];
// Copy the header.
System.arraycopy(mDecodand, mOffset, buf, 0, ICO_HEADER_LENGTH_BYTES);
int headerPtr = ICO_HEADER_LENGTH_BYTES;
int payloadPtr = ICO_HEADER_LENGTH_BYTES + (mIconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
int ind = 0;
for (IconDirectoryEntry entry : mIconDirectory) {
// Copy this entry.
System.arraycopy(mDecodand, mOffset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
// Copy its payload.
System.arraycopy(mDecodand, mOffset + entry.mPayloadOffset, buf, payloadPtr, entry.mPayloadSize);
// Update the offset field.
buf[headerPtr + 12] = (byte) payloadPtr;
buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
entry.mPayloadOffset = payloadPtr;
entry.mIndex = ind;
payloadPtr += entry.mPayloadSize;
headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
ind++;
}
mDecodand = buf;
mOffset = 0;
mLen = spaceRetained;
}
/**
* Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
*
* @param index The index into the Icon Directory of the image of interest.
* @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
* fails.
*/
public Bitmap decodeBitmapAtIndex(int index) {
final IconDirectoryEntry iconDirEntry = mIconDirectory[index];
if (iconDirEntry.mPayloadIsPNG) {
// PNG payload. Simply extract it and decode it.
return BitmapUtils.decodeByteArray(mDecodand, mOffset + iconDirEntry.mPayloadOffset, iconDirEntry.mPayloadSize);
}
// The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
// We construct an ICO containing just the image we want, and let Android do the rest.
byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.mPayloadSize];
// Set the type field in the ICO header.
decodeTarget[2] = 1;
// Set the num-images field in the header to 1.
decodeTarget[4] = 1;
// Copy the ICONDIRENTRY we need into the new buffer.
System.arraycopy(mDecodand, mOffset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
// Copy the payload into the new buffer.
final int singlePayloadOffset = ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
System.arraycopy(mDecodand, mOffset + iconDirEntry.mPayloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.mPayloadSize);
// Update the offset field of the ICONDIRENTRY to make the new ICO valid.
decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset;
decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16);
decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24);
// Decode the newly-constructed singleton-ICO.
return BitmapUtils.decodeByteArray(decodeTarget);
}
/**
* Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
*
* @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
*/
@Override
public ICOIterator iterator() {
// If a previous call to decode concluded this ICO is invalid, abort.
if (mHasDecoded && !mIsValid) {
return null;
}
// If we've not been decoded before, but now fail to make any sense of the ICO, abort.
if (!mHasDecoded) {
if (!decodeIconDirectoryAndPossiblyPrune()) {
return null;
}
}
// If decoding was a success, return an iterator over the images in this ICO.
return new ICOIterator();
}
/**
* Decode this ICO and return the result as a LoadFaviconResult.
* @return A LoadFaviconResult representing the decoded ICO.
*/
public LoadFaviconResult decode() {
// The call to iterator returns null if decoding fails.
Iterator<Bitmap> bitmaps = iterator();
if (bitmaps == null) {
return null;
}
LoadFaviconResult result = new LoadFaviconResult();
result.mBitmapsDecoded = bitmaps;
result.mFaviconBytes = mDecodand;
result.mOffset = mOffset;
result.mLength = mLen;
result.mHasMultipleBitmaps = mIconDirectory.length > 1;
return result;
}
/**
* Inner class to iterate over the elements in the ICO represented by the enclosing instance.
*/
private class ICOIterator implements Iterator<Bitmap> {
private int mIndex = 0;
@Override
public boolean hasNext() {
return mIndex < mIconDirectory.length;
}
@Override
public Bitmap next() {
if (mIndex > mIconDirectory.length) {
throw new NoSuchElementException("No more elements in this ICO.");
}
return decodeBitmapAtIndex(mIndex++);
}
@Override
public void remove() {
if (mIconDirectory[mIndex] == null) {
throw new IllegalStateException("Remove already called for element " + mIndex);
}
mIconDirectory[mIndex] = null;
}
}
}

View File

@ -0,0 +1,201 @@
/* 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/. */
package org.mozilla.gecko.favicons.decoders;
/**
* Representation of an ICO file ICONDIRENTRY structure.
*/
public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
public static int sMaxBPP;
int mWidth;
int mHeight;
int mPaletteSize;
int mBitsPerPixel;
int mPayloadSize;
int mPayloadOffset;
boolean mPayloadIsPNG;
// Tracks the index in the Icon Directory of this entry. Useful only for pruning.
int mIndex;
boolean mIsErroneous;
public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
mWidth = width;
mHeight = height;
mPaletteSize = paletteSize;
mBitsPerPixel = bitsPerPixel;
mPayloadSize = payloadSize;
mPayloadOffset = payloadOffset;
mPayloadIsPNG = payloadIsPNG;
}
/**
* Method to get a dummy Icon Directory Entry with the Erroneous bit set.
*
* @return An erroneous placeholder Icon Directory Entry.
*/
public static IconDirectoryEntry getErroneousEntry() {
IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false);
ret.mIsErroneous = true;
return ret;
}
/**
* Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given
* offset as an IconDirectoryEntry and returns the result.
*
* @param buffer Byte array containing the icon directory entry to decode.
* @param regionOffset Offset into the byte array of the valid region of the buffer.
* @param regionLength Length of the valid region in the buffer.
* @param entryOffset Offset of the icon directory entry to decode within the buffer.
* @return An IconDirectoryEntry object representing the entry specified, or null if the entry
* is obviously invalid.
*/
public static IconDirectoryEntry createFromBuffer(byte[] buffer, int regionOffset, int regionLength, int entryOffset) {
// Verify that the reserved field is really zero.
if (buffer[entryOffset + 3] != 0) {
return getErroneousEntry();
}
// Verify that the entry points to a region that actually exists in the buffer, else bin it.
int fieldPtr = entryOffset + 8;
int entryLength = (buffer[fieldPtr] & 0xFF) |
(buffer[fieldPtr + 1] & 0xFF) << 8 |
(buffer[fieldPtr + 2] & 0xFF) << 16 |
(buffer[fieldPtr + 3] & 0xFF) << 24;
// Advance to the offset field.
fieldPtr += 4;
int payloadOffset = (buffer[fieldPtr] & 0xFF) |
(buffer[fieldPtr + 1] & 0xFF) << 8 |
(buffer[fieldPtr + 2] & 0xFF) << 16 |
(buffer[fieldPtr + 3] & 0xFF) << 24;
// Fail if the entry describes a region outside the buffer.
if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) {
return getErroneousEntry();
}
// Extract the image dimensions.
int imageWidth = buffer[entryOffset] & 0xFF;
int imageHeight = buffer[entryOffset+1] & 0xFF;
// Because Microsoft, a size value of zero represents an image size of 256.
if (imageWidth == 0) {
imageWidth = 256;
}
if (imageHeight == 0) {
imageHeight = 256;
}
// If the image uses a colour palette, this is the number of colours, otherwise this is zero.
int paletteSize = buffer[entryOffset + 2] & 0xFF;
// The plane count - usually 0 or 1. When > 1, taken as multiplier on bitsPerPixel.
int colorPlanes = buffer[entryOffset + 4] & 0xFF;
int bitsPerPixel = (buffer[entryOffset + 6] & 0xFF) |
(buffer[entryOffset + 7] & 0xFF) << 8;
if (colorPlanes > 1) {
bitsPerPixel *= colorPlanes;
}
// Look for PNG magic numbers at the start of the payload.
boolean payloadIsPNG = FaviconDecoder.bufferStartsWith(buffer, FaviconDecoder.ImageMagicNumbers.PNG.value, regionOffset + payloadOffset);
return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG);
}
/**
* Get the number of bytes from the start of the ICO file to the beginning of this entry.
*/
public int getOffset() {
return ICODecoder.ICO_HEADER_LENGTH_BYTES + (mIndex * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
}
@Override
public int compareTo(IconDirectoryEntry another) {
if (mWidth > another.mWidth) {
return 1;
}
if (mWidth < another.mWidth) {
return -1;
}
// Where both images exceed the max BPP, take the smaller of the two BPP values.
if (mBitsPerPixel >= sMaxBPP && another.mBitsPerPixel >= sMaxBPP) {
if (mBitsPerPixel < another.mBitsPerPixel) {
return 1;
}
if (mBitsPerPixel > another.mBitsPerPixel) {
return -1;
}
}
// Otherwise, take the larger of the BPP values.
if (mBitsPerPixel > another.mBitsPerPixel) {
return 1;
}
if (mBitsPerPixel < another.mBitsPerPixel) {
return -1;
}
// Prefer large palettes.
if (mPaletteSize > another.mPaletteSize) {
return 1;
}
if (mPaletteSize < another.mPaletteSize) {
return -1;
}
// Prefer smaller payloads.
if (mPayloadSize < another.mPayloadSize) {
return 1;
}
if (mPayloadSize > another.mPayloadSize) {
return -1;
}
// If all else fails, prefer PNGs over BMPs. They tend to be smaller.
if (mPayloadIsPNG && !another.mPayloadIsPNG) {
return 1;
}
if (!mPayloadIsPNG && another.mPayloadIsPNG) {
return -1;
}
return 0;
}
public static void setMaxBPP(int maxBPP) {
sMaxBPP = maxBPP;
}
@Override
public String toString() {
return "IconDirectoryEntry{" +
"\nmWidth=" + mWidth +
", \nmHeight=" + mHeight +
", \nmPaletteSize=" + mPaletteSize +
", \nmBitsPerPixel=" + mBitsPerPixel +
", \nmPayloadSize=" + mPayloadSize +
", \nmPayloadOffset=" + mPayloadOffset +
", \nmPayloadIsPNG=" + mPayloadIsPNG +
", \nmIndex=" + mIndex +
'}';
}
}

View File

@ -0,0 +1,74 @@
/* 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/. */
package org.mozilla.gecko.favicons.decoders;
import android.graphics.Bitmap;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.util.Iterator;
/**
* Class representing the result of loading a favicon.
* This operation will produce either a collection of favicons, a single favicon, or no favicon.
* It is necessary to model single favicons differently to a collection of one favicon (An entity
* that may not exist with this scheme) since the in-database representation of these things differ.
* (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
* stored as decoded bitmap blobs.)
*/
public class LoadFaviconResult {
private static final String LOGTAG = "LoadFaviconResult";
byte[] mFaviconBytes;
int mOffset;
int mLength;
boolean mHasMultipleBitmaps;
Iterator<Bitmap> mBitmapsDecoded;
public Iterator<Bitmap> getBitmaps() {
return mBitmapsDecoded;
}
/**
* Return a representation of this result suitable for storing in the database.
* For
*
* @return A byte array containing the bytes from which this result was decoded.
*/
public byte[] getBytesForDatabaseStorage() {
// Begin by normalising the buffer.
if (mOffset != 0 || mLength != mFaviconBytes.length) {
final byte[] normalised = new byte[mLength];
System.arraycopy(mFaviconBytes, mOffset, normalised, 0, mLength);
mOffset = 0;
mFaviconBytes = normalised;
}
// For results containing a single image, we re-encode the result as a PNG in an effort to
// save space.
if (!mHasMultipleBitmaps) {
Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) mBitmapsDecoded).peek();
byte[] data = null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
data = stream.toByteArray();
} else {
Log.w(LOGTAG, "Favicon compression failed.");
}
return data;
}
// For results containing multiple images, we store the result verbatim. (But cutting the
// buffer to size first).
// We may instead want to consider re-encoding the entire ICO as a collection of efficiently
// encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image
// favicons may also not be worth the time/space tradeoff.).
return mFaviconBytes;
}
}

Some files were not shown because too many files have changed in this diff Show More