merge m-c to fx-team
2
CLOBBER
@ -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.
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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)"/>
|
||||
|
@ -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]
|
||||
));
|
||||
|
@ -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));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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]
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
@ -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]
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
@ -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]
|
||||
|
@ -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>
|
@ -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");
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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");
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
@ -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"]}
|
||||
});
|
||||
}());
|
||||
|
@ -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");
|
||||
|
@ -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;}
|
||||
|
@ -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;
|
||||
})();
|
||||
|
@ -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)),
|
||||
|
@ -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"
|
||||
});
|
||||
})();
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
};
|
||||
})();
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
|
||||
|
@ -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"});
|
||||
}
|
||||
|
@ -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(); });
|
||||
|
@ -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;
|
||||
|
@ -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();) {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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"
|
||||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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 () {}
|
||||
|
@ -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\");");
|
||||
})();
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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(); };
|
||||
|
@ -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/];");
|
||||
})();
|
||||
|
@ -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('&', '&').replace('<', '<');
|
||||
}
|
||||
|
||||
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('&', '&').replace('<', '<') + '</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('&', '&').replace('<', '<') + '</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 (·).
|
||||
*
|
||||
* @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('&', '&').replace('<', '<') +
|
||||
'<td class="mt-token"' + (i == diffAt * 2 ? " style='background: pink'" : "") + '>' +
|
||||
'<span class="cm-' + esc(String(style)) + '">' +
|
||||
esc(val.replace(/ /g,'\xb7')) +
|
||||
'</span>' +
|
||||
'</td>';
|
||||
}
|
||||
|
@ -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("/"), "");
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 568 B |
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 811 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.6 KiB |
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
|
Before Width: | Height: | Size: 849 B After Width: | Height: | Size: 568 B |
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
|
@ -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) {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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().
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
158
mobile/android/base/favicons/decoders/FaviconDecoder.java
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
380
mobile/android/base/favicons/decoders/ICODecoder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
201
mobile/android/base/favicons/decoders/IconDirectoryEntry.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
74
mobile/android/base/favicons/decoders/LoadFaviconResult.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|