diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Controllers | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers')
45 files changed, 11872 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/AnalyzerManager.js b/Source/WebInspectorUI/UserInterface/Controllers/AnalyzerManager.js new file mode 100644 index 000000000..99c62738c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/AnalyzerManager.js @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.AnalyzerManager = class AnalyzerManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._eslintConfig = { + env: { + "browser": true, + "node": false + }, + globals: { + "document": true + }, + rules: { + "consistent-return": 2, + "curly": 0, + "eqeqeq": 0, + "new-parens": 0, + "no-comma-dangle": 0, + "no-console": 0, + "no-constant-condition": 0, + "no-extra-bind": 2, + "no-extra-semi": 2, + "no-proto": 0, + "no-return-assign": 2, + "no-trailing-spaces": 2, + "no-underscore-dangle": 0, + "no-unused-expressions": 2, + "no-wrap-func": 2, + "semi": 2, + "space-infix-ops": 2, + "space-return-throw-case": 2, + "strict": 0, + "valid-typeof": 2 + } + }; + + this._sourceCodeMessagesMap = new WeakMap; + + WebInspector.SourceCode.addEventListener(WebInspector.SourceCode.Event.ContentDidChange, this._handleSourceCodeContentDidChange, this); + } + + // Public + + getAnalyzerMessagesForSourceCode(sourceCode) + { + return new Promise(function(resolve, reject) { + var analyzer = WebInspector.AnalyzerManager._typeAnalyzerMap.get(sourceCode.type); + if (!analyzer) { + reject(new Error("This resource type cannot be analyzed.")); + return; + } + + if (this._sourceCodeMessagesMap.has(sourceCode)) { + resolve(this._sourceCodeMessagesMap.get(sourceCode)); + return; + } + + function retrieveAnalyzerMessages(properties) + { + var analyzerMessages = []; + var rawAnalyzerMessages = analyzer.verify(sourceCode.content, this._eslintConfig); + + // Raw line and column numbers are one-based. SourceCodeLocation expects them to be zero-based so we subtract 1 from each. + for (var rawAnalyzerMessage of rawAnalyzerMessages) + analyzerMessages.push(new WebInspector.AnalyzerMessage(new WebInspector.SourceCodeLocation(sourceCode, rawAnalyzerMessage.line - 1, rawAnalyzerMessage.column - 1), rawAnalyzerMessage.message, rawAnalyzerMessage.ruleId)); + + this._sourceCodeMessagesMap.set(sourceCode, analyzerMessages); + + resolve(analyzerMessages); + } + + sourceCode.requestContent().then(retrieveAnalyzerMessages.bind(this)).catch(handlePromiseException); + }.bind(this)); + } + + sourceCodeCanBeAnalyzed(sourceCode) + { + return sourceCode.type === WebInspector.Resource.Type.Script; + } + + // Private + + _handleSourceCodeContentDidChange(event) + { + var sourceCode = event.target; + + // Since sourceCode has changed, remove it and its messages from the map so getAnalyzerMessagesForSourceCode will have to reanalyze the next time it is called. + this._sourceCodeMessagesMap.delete(sourceCode); + } +}; + +WebInspector.AnalyzerManager._typeAnalyzerMap = new Map; + +// <https://webkit.org/b/136515> Web Inspector: JavaScript source text editor should have a linter +// WebInspector.AnalyzerManager._typeAnalyzerMap.set(WebInspector.Resource.Type.Script, eslint); diff --git a/Source/WebInspectorUI/UserInterface/Controllers/Annotator.js b/Source/WebInspectorUI/UserInterface/Controllers/Annotator.js new file mode 100644 index 000000000..3e87bb5a4 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/Annotator.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * Copyright (C) 2014 Saam Barati <saambarati1@gmail.com> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.Annotator = class Annotator extends WebInspector.Object +{ + constructor(sourceCodeTextEditor) + { + super(); + + console.assert(sourceCodeTextEditor instanceof WebInspector.SourceCodeTextEditor, sourceCodeTextEditor); + + this._sourceCodeTextEditor = sourceCodeTextEditor; + this._timeoutIdentifier = null; + this._isActive = false; + } + + // Public + + get sourceCodeTextEditor() + { + return this._sourceCodeTextEditor; + } + + isActive() + { + return this._isActive; + } + + pause() + { + this._clearTimeoutIfNeeded(); + this._isActive = false; + } + + resume() + { + this._clearTimeoutIfNeeded(); + this._isActive = true; + this.insertAnnotations(); + } + + refresh() + { + console.assert(this._isActive); + if (!this._isActive) + return; + + this._clearTimeoutIfNeeded(); + this.insertAnnotations(); + } + + reset() + { + this._clearTimeoutIfNeeded(); + this._isActive = true; + this.clearAnnotations(); + this.insertAnnotations(); + } + + clear() + { + this.pause(); + this.clearAnnotations(); + } + + // Protected + + insertAnnotations() + { + // Implemented by subclasses. + } + + clearAnnotations() + { + // Implemented by subclasses. + } + + // Private + + _clearTimeoutIfNeeded() + { + if (this._timeoutIdentifier) { + clearTimeout(this._timeoutIdentifier); + this._timeoutIdentifier = null; + } + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ApplicationCacheManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ApplicationCacheManager.js new file mode 100644 index 000000000..61a55a9f7 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/ApplicationCacheManager.js @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.ApplicationCacheManager = class ApplicationCacheManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.ApplicationCacheAgent) + ApplicationCacheAgent.enable(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this); + + this._online = true; + + this.initialize(); + } + + // Public + + initialize() + { + this._applicationCacheObjects = {}; + + if (window.ApplicationCacheAgent) + ApplicationCacheAgent.getFramesWithManifests(this._framesWithManifestsLoaded.bind(this)); + } + + get applicationCacheObjects() + { + var applicationCacheObjects = []; + for (var id in this._applicationCacheObjects) + applicationCacheObjects.push(this._applicationCacheObjects[id]); + return applicationCacheObjects; + } + + networkStateUpdated(isNowOnline) + { + this._online = isNowOnline; + + this.dispatchEventToListeners(WebInspector.ApplicationCacheManager.Event.NetworkStateUpdated, {online: this._online}); + } + + get online() + { + return this._online; + } + + applicationCacheStatusUpdated(frameId, manifestURL, status) + { + var frame = WebInspector.frameResourceManager.frameForIdentifier(frameId); + if (!frame) + return; + + this._frameManifestUpdated(frame, manifestURL, status); + } + + requestApplicationCache(frame, callback) + { + function callbackWrapper(error, applicationCache) + { + if (error) { + callback(null); + return; + } + + callback(applicationCache); + } + + ApplicationCacheAgent.getApplicationCacheForFrame(frame.id, callbackWrapper); + } + + // Private + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (event.target.isMainFrame()) { + // If we are dealing with the main frame, we want to clear our list of objects, because we are navigating to a new page. + this.initialize(); + + this.dispatchEventToListeners(WebInspector.ApplicationCacheManager.Event.Cleared); + + return; + } + + if (window.ApplicationCacheAgent) + ApplicationCacheAgent.getManifestForFrame(event.target.id, this._manifestForFrameLoaded.bind(this, event.target.id)); + } + + _childFrameWasRemoved(event) + { + this._frameManifestRemoved(event.data.childFrame); + } + + _manifestForFrameLoaded(frameId, error, manifestURL) + { + if (error) + return; + + var frame = WebInspector.frameResourceManager.frameForIdentifier(frameId); + if (!frame) + return; + + if (!manifestURL) + this._frameManifestRemoved(frame); + } + + _framesWithManifestsLoaded(error, framesWithManifests) + { + if (error) + return; + + for (var i = 0; i < framesWithManifests.length; ++i) { + var frame = WebInspector.frameResourceManager.frameForIdentifier(framesWithManifests[i].frameId); + if (!frame) + continue; + + this._frameManifestUpdated(frame, framesWithManifests[i].manifestURL, framesWithManifests[i].status); + } + } + + _frameManifestUpdated(frame, manifestURL, status) + { + if (status === WebInspector.ApplicationCacheManager.Status.Uncached) { + this._frameManifestRemoved(frame); + return; + } + + if (!manifestURL) + return; + + var manifestFrame = this._applicationCacheObjects[frame.id]; + if (manifestFrame && manifestURL !== manifestFrame.manifest.manifestURL) + this._frameManifestRemoved(frame); + + var oldStatus = manifestFrame ? manifestFrame.status : -1; + var statusChanged = manifestFrame && status !== oldStatus; + if (manifestFrame) + manifestFrame.status = status; + + if (!this._applicationCacheObjects[frame.id]) { + var cacheManifest = new WebInspector.ApplicationCacheManifest(manifestURL); + this._applicationCacheObjects[frame.id] = new WebInspector.ApplicationCacheFrame(frame, cacheManifest, status); + + this.dispatchEventToListeners(WebInspector.ApplicationCacheManager.Event.FrameManifestAdded, {frameManifest: this._applicationCacheObjects[frame.id]}); + } + + if (statusChanged) + this.dispatchEventToListeners(WebInspector.ApplicationCacheManager.Event.FrameManifestStatusChanged, {frameManifest: this._applicationCacheObjects[frame.id]}); + } + + _frameManifestRemoved(frame) + { + if (!this._applicationCacheObjects[frame.id]) + return; + + delete this._applicationCacheObjects[frame.id]; + + this.dispatchEventToListeners(WebInspector.ApplicationCacheManager.Event.FrameManifestRemoved, {frame}); + } +}; + +WebInspector.ApplicationCacheManager.Event = { + Cleared: "application-cache-manager-cleared", + FrameManifestAdded: "application-cache-manager-frame-manifest-added", + FrameManifestRemoved: "application-cache-manager-frame-manifest-removed", + FrameManifestStatusChanged: "application-cache-manager-frame-manifest-status-changed", + NetworkStateUpdated: "application-cache-manager-network-state-updated" +}; + +WebInspector.ApplicationCacheManager.Status = { + Uncached: 0, + Idle: 1, + Checking: 2, + Downloading: 3, + UpdateReady: 4, + Obsolete: 5 +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/BasicBlockAnnotator.js b/Source/WebInspectorUI/UserInterface/Controllers/BasicBlockAnnotator.js new file mode 100644 index 000000000..b4b86f9d3 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/BasicBlockAnnotator.js @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * Copyright (C) 2015 Saam Barati <saambarati1@gmail.com> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.BasicBlockAnnotator = class BasicBlockAnnotator extends WebInspector.Annotator +{ + constructor(sourceCodeTextEditor, script) + { + super(sourceCodeTextEditor); + + this._script = script; + this._basicBlockMarkers = new Map; // Only contains unexecuted basic blocks. + } + + // Protected + + clearAnnotations() + { + for (var key of this._basicBlockMarkers.keys()) + this._clearRangeForBasicBlockMarker(key); + } + + insertAnnotations() + { + if (!this.isActive()) + return; + this._annotateBasicBlockExecutionRanges(); + } + + // Private + + _annotateBasicBlockExecutionRanges() + { + var sourceID = this._script.id; + var startTime = Date.now(); + + this._script.target.RuntimeAgent.getBasicBlocks(sourceID, function(error, basicBlocks) { + if (error) { + console.error("Error in getting basic block locations: " + error); + return; + } + + if (!this.isActive()) + return; + + var {startOffset, endOffset} = this.sourceCodeTextEditor.visibleRangeOffsets(); + basicBlocks = basicBlocks.filter(function(block) { + // Viewport: [--] + // Block: [--] + if (block.startOffset > endOffset) + return false; + + // Viewport: [--] + // Block: [--] + if (block.endOffset < startOffset) + return false; + + return true; + }); + + for (var block of basicBlocks) { + var key = block.startOffset + ":" + block.endOffset; + var hasKey = this._basicBlockMarkers.has(key); + var hasExecuted = block.hasExecuted; + if (hasKey && hasExecuted) + this._clearRangeForBasicBlockMarker(key); + else if (!hasKey && !hasExecuted) { + var marker = this._highlightTextForBasicBlock(block); + this._basicBlockMarkers.set(key, marker); + } + } + + var totalTime = Date.now() - startTime; + var timeoutTime = Number.constrain(30 * totalTime, 500, 5000); + this._timeoutIdentifier = setTimeout(this.insertAnnotations.bind(this), timeoutTime); + }.bind(this)); + } + + _highlightTextForBasicBlock(basicBlock) + { + console.assert(basicBlock.startOffset <= basicBlock.endOffset && basicBlock.startOffset >= 0 && basicBlock.endOffset >= 0, "" + basicBlock.startOffset + ":" + basicBlock.endOffset); + console.assert(!basicBlock.hasExecuted); + + var startPosition = this.sourceCodeTextEditor.originalOffsetToCurrentPosition(basicBlock.startOffset); + var endPosition = this.sourceCodeTextEditor.originalOffsetToCurrentPosition(basicBlock.endOffset); + if (this._isTextRangeOnlyClosingBrace(startPosition, endPosition)) + return null; + + var marker = this.sourceCodeTextEditor.addStyleToTextRange(startPosition, endPosition, WebInspector.BasicBlockAnnotator.HasNotExecutedClassName); + return marker; + } + + _isTextRangeOnlyClosingBrace(startPosition, endPosition) + { + var isOnlyClosingBrace = /^\s*\}$/; + return isOnlyClosingBrace.test(this.sourceCodeTextEditor.getTextInRange(startPosition, endPosition)); + } + + _clearRangeForBasicBlockMarker(key) + { + console.assert(this._basicBlockMarkers.has(key)); + var marker = this._basicBlockMarkers.get(key); + if (marker) + marker.clear(); + this._basicBlockMarkers.delete(key); + } +}; + +WebInspector.BasicBlockAnnotator.HasNotExecutedClassName = "basic-block-has-not-executed"; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/BranchManager.js b/Source/WebInspectorUI/UserInterface/Controllers/BranchManager.js new file mode 100644 index 000000000..6dc8de1ca --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/BranchManager.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.BranchManager = class BranchManager extends WebInspector.Object +{ + constructor() + { + super(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + + this.initialize(); + } + + // Public + + initialize() + { + this._originalBranch = new WebInspector.Branch(WebInspector.UIString("Original"), null, true); + this._currentBranch = this._originalBranch.fork(WebInspector.UIString("Working Copy")); + this._branches = [this._originalBranch, this._currentBranch]; + } + + get branches() + { + return this._branches; + } + + get currentBranch() + { + return this._currentBranch; + } + + set currentBranch(branch) + { + console.assert(branch instanceof WebInspector.Branch); + if (!(branch instanceof WebInspector.Branch)) + return; + + this._currentBranch.revert(); + + this._currentBranch = branch; + + this._currentBranch.apply(); + } + + createBranch(displayName, fromBranch) + { + if (!fromBranch) + fromBranch = this._originalBranch; + + console.assert(fromBranch instanceof WebInspector.Branch); + if (!(fromBranch instanceof WebInspector.Branch)) + return null; + + var newBranch = fromBranch.fork(displayName); + this._branches.push(newBranch); + return newBranch; + } + + deleteBranch(branch) + { + console.assert(branch instanceof WebInspector.Branch); + if (!(branch instanceof WebInspector.Branch)) + return; + + console.assert(branch !== this._originalBranch); + if (branch === this._originalBranch) + return; + + this._branches.remove(branch); + + if (branch === this._currentBranch) + this._currentBranch = this._originalBranch; + } + + // Private + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (!event.target.isMainFrame()) + return; + + this.initialize(); + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/BreakpointLogMessageLexer.js b/Source/WebInspectorUI/UserInterface/Controllers/BreakpointLogMessageLexer.js new file mode 100644 index 000000000..071704a6b --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/BreakpointLogMessageLexer.js @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.BreakpointLogMessageLexer = class BreakpointLogMessageLexer extends WebInspector.Object +{ + constructor() + { + super(); + + this._stateFunctions = { + [WebInspector.BreakpointLogMessageLexer.State.Expression]: this._expression, + [WebInspector.BreakpointLogMessageLexer.State.PlainText]: this._plainText, + [WebInspector.BreakpointLogMessageLexer.State.PossiblePlaceholder]: this._possiblePlaceholder, + [WebInspector.BreakpointLogMessageLexer.State.RegExpOrStringLiteral]: this._regExpOrStringLiteral, + }; + + this.reset(); + } + + // Public + + tokenize(input) + { + this.reset(); + this._input = input; + + while (this._index < this._input.length) { + let stateFunction = this._stateFunctions[this._states.lastValue]; + console.assert(stateFunction); + if (!stateFunction) { + this.reset(); + return null; + } + + stateFunction.call(this); + } + + // Needed for trailing plain text. + this._finishPlainText(); + + return this._tokens; + } + + reset() + { + this._input = ""; + this._buffer = ""; + + this._index = 0; + this._states = [WebInspector.BreakpointLogMessageLexer.State.PlainText]; + this._literalStartCharacter = ""; + this._curlyBraceDepth = 0; + this._tokens = []; + } + + // Private + + _finishPlainText() + { + this._appendToken(WebInspector.BreakpointLogMessageLexer.TokenType.PlainText); + } + + _finishExpression() + { + this._appendToken(WebInspector.BreakpointLogMessageLexer.TokenType.Expression); + } + + _appendToken(type) + { + if (!this._buffer) + return; + + this._tokens.push({type, data: this._buffer}); + this._buffer = ""; + } + + _consume() + { + console.assert(this._index < this._input.length); + + let character = this._peek(); + this._index++; + return character; + } + + _peek() + { + return this._input[this._index] || null; + } + + // States + + _expression() + { + let character = this._consume(); + + if (character === "}") { + if (this._curlyBraceDepth === 0) { + this._finishExpression(); + + console.assert(this._states.lastValue === WebInspector.BreakpointLogMessageLexer.State.Expression); + this._states.pop(); + return; + } + + this._curlyBraceDepth--; + } + + this._buffer += character; + + if (character === "/" || character === "\"" || character === "'") { + this._literalStartCharacter = character; + this._states.push(WebInspector.BreakpointLogMessageLexer.State.RegExpOrStringLiteral); + } else if (character === "{") + this._curlyBraceDepth++; + } + + _plainText() + { + let character = this._peek(); + + if (character === "$") + this._states.push(WebInspector.BreakpointLogMessageLexer.State.PossiblePlaceholder); + else { + this._buffer += character; + this._consume(); + } + } + + _possiblePlaceholder() + { + let character = this._consume(); + console.assert(character === "$"); + let nextCharacter = this._peek(); + + console.assert(this._states.lastValue === WebInspector.BreakpointLogMessageLexer.State.PossiblePlaceholder); + this._states.pop(); + + if (nextCharacter === "{") { + this._finishPlainText(); + this._consume(); + this._states.push(WebInspector.BreakpointLogMessageLexer.State.Expression); + } else + this._buffer += character; + } + + _regExpOrStringLiteral() + { + let character = this._consume(); + this._buffer += character; + + if (character === "\\") { + if (this._peek() !== null) + this._buffer += this._consume(); + return; + } + + if (character === this._literalStartCharacter) { + console.assert(this._states.lastValue === WebInspector.BreakpointLogMessageLexer.State.RegExpOrStringLiteral); + this._states.pop(); + } + } +}; + +WebInspector.BreakpointLogMessageLexer.State = { + Expression: Symbol("expression"), + PlainText: Symbol("plain-text"), + PossiblePlaceholder: Symbol("possible-placeholder"), + RegExpOrStringLiteral: Symbol("regexp-or-string-literal"), +}; + +WebInspector.BreakpointLogMessageLexer.TokenType = { + PlainText: "token-type-plain-text", + Expression: "token-type-expression", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/BreakpointPopoverController.js b/Source/WebInspectorUI/UserInterface/Controllers/BreakpointPopoverController.js new file mode 100644 index 000000000..1b30fc98c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/BreakpointPopoverController.js @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.BreakpointPopoverController = class BreakpointPopoverController extends WebInspector.Object +{ + constructor() + { + super(); + + this._breakpoint = null; + this._popover = null; + this._popoverContentElement = null; + } + + // Public + + appendContextMenuItems(contextMenu, breakpoint, breakpointDisplayElement) + { + console.assert(document.body.contains(breakpointDisplayElement), "Breakpoint popover display element must be in the DOM."); + + const editBreakpoint = () => { + console.assert(!this._popover, "Breakpoint popover already exists."); + if (this._popover) + return; + + this._createPopoverContent(breakpoint); + this._popover = new WebInspector.Popover(this); + this._popover.content = this._popoverContentElement; + + let bounds = WebInspector.Rect.rectFromClientRect(breakpointDisplayElement.getBoundingClientRect()); + bounds.origin.x -= 1; // Move the anchor left one pixel so it looks more centered. + this._popover.present(bounds.pad(2), [WebInspector.RectEdge.MAX_Y]); + }; + + const removeBreakpoint = () => { + WebInspector.debuggerManager.removeBreakpoint(breakpoint); + }; + + const toggleBreakpoint = () => { + breakpoint.disabled = !breakpoint.disabled; + }; + + const toggleAutoContinue = () => { + breakpoint.autoContinue = !breakpoint.autoContinue; + }; + + const revealOriginalSourceCodeLocation = () => { + WebInspector.showOriginalOrFormattedSourceCodeLocation(breakpoint.sourceCodeLocation); + }; + + if (WebInspector.debuggerManager.isBreakpointEditable(breakpoint)) + contextMenu.appendItem(WebInspector.UIString("Edit Breakpoint…"), editBreakpoint); + + if (breakpoint.autoContinue && !breakpoint.disabled) { + contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), toggleBreakpoint); + contextMenu.appendItem(WebInspector.UIString("Cancel Automatic Continue"), toggleAutoContinue); + } else if (!breakpoint.disabled) + contextMenu.appendItem(WebInspector.UIString("Disable Breakpoint"), toggleBreakpoint); + else + contextMenu.appendItem(WebInspector.UIString("Enable Breakpoint"), toggleBreakpoint); + + if (!breakpoint.autoContinue && !breakpoint.disabled && breakpoint.actions.length) + contextMenu.appendItem(WebInspector.UIString("Set to Automatically Continue"), toggleAutoContinue); + + if (WebInspector.debuggerManager.isBreakpointRemovable(breakpoint)) { + contextMenu.appendSeparator(); + contextMenu.appendItem(WebInspector.UIString("Delete Breakpoint"), removeBreakpoint); + } + + if (breakpoint._sourceCodeLocation.hasMappedLocation()) { + contextMenu.appendSeparator(); + contextMenu.appendItem(WebInspector.UIString("Reveal in Original Resource"), revealOriginalSourceCodeLocation); + } + } + + // CodeMirrorCompletionController delegate + + completionControllerShouldAllowEscapeCompletion() + { + return false; + } + + // Private + + _createPopoverContent(breakpoint) + { + console.assert(!this._popoverContentElement, "Popover content element already exists."); + if (this._popoverContentElement) + return; + + this._breakpoint = breakpoint; + this._popoverContentElement = document.createElement("div"); + this._popoverContentElement.className = "edit-breakpoint-popover-content"; + + let checkboxElement = document.createElement("input"); + checkboxElement.type = "checkbox"; + checkboxElement.checked = !this._breakpoint.disabled; + checkboxElement.addEventListener("change", this._popoverToggleEnabledCheckboxChanged.bind(this)); + + let checkboxLabel = document.createElement("label"); + checkboxLabel.className = "toggle"; + checkboxLabel.appendChild(checkboxElement); + checkboxLabel.append(this._breakpoint.sourceCodeLocation.displayLocationString()); + + let table = document.createElement("table"); + + let conditionRow = table.appendChild(document.createElement("tr")); + let conditionHeader = conditionRow.appendChild(document.createElement("th")); + let conditionData = conditionRow.appendChild(document.createElement("td")); + let conditionLabel = conditionHeader.appendChild(document.createElement("label")); + conditionLabel.textContent = WebInspector.UIString("Condition"); + let conditionEditorElement = conditionData.appendChild(document.createElement("div")); + conditionEditorElement.classList.add("edit-breakpoint-popover-condition", WebInspector.SyntaxHighlightedStyleClassName); + + this._conditionCodeMirror = WebInspector.CodeMirrorEditor.create(conditionEditorElement, { + extraKeys: {Tab: false}, + lineWrapping: false, + mode: "text/javascript", + matchBrackets: true, + placeholder: WebInspector.UIString("Conditional expression"), + scrollbarStyle: null, + value: this._breakpoint.condition || "", + }); + + let conditionCodeMirrorInputField = this._conditionCodeMirror.getInputField(); + conditionCodeMirrorInputField.id = "codemirror-condition-input-field"; + conditionLabel.setAttribute("for", conditionCodeMirrorInputField.id); + + this._conditionCodeMirrorEscapeOrEnterKeyHandler = this._conditionCodeMirrorEscapeOrEnterKey.bind(this); + this._conditionCodeMirror.addKeyMap({ + "Esc": this._conditionCodeMirrorEscapeOrEnterKeyHandler, + "Enter": this._conditionCodeMirrorEscapeOrEnterKeyHandler, + }); + + this._conditionCodeMirror.on("change", this._conditionCodeMirrorChanged.bind(this)); + this._conditionCodeMirror.on("beforeChange", this._conditionCodeMirrorBeforeChange.bind(this)); + + let completionController = new WebInspector.CodeMirrorCompletionController(this._conditionCodeMirror, this); + completionController.addExtendedCompletionProvider("javascript", WebInspector.javaScriptRuntimeCompletionProvider); + + // CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear. + setTimeout(() => { + this._conditionCodeMirror.refresh(); + this._conditionCodeMirror.focus(); + }, 0); + + // COMPATIBILITY (iOS 7): Debugger.setBreakpoint did not support options. + if (DebuggerAgent.setBreakpoint.supports("options")) { + // COMPATIBILITY (iOS 9): Legacy backends don't support breakpoint ignore count. Since support + // can't be tested directly, check for CSS.getSupportedSystemFontFamilyNames. + // FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed. + if (CSSAgent.getSupportedSystemFontFamilyNames) { + let ignoreCountRow = table.appendChild(document.createElement("tr")); + let ignoreCountHeader = ignoreCountRow.appendChild(document.createElement("th")); + let ignoreCountLabel = ignoreCountHeader.appendChild(document.createElement("label")); + let ignoreCountData = ignoreCountRow.appendChild(document.createElement("td")); + this._ignoreCountInput = ignoreCountData.appendChild(document.createElement("input")); + this._ignoreCountInput.id = "edit-breakpoint-popover-ignore"; + this._ignoreCountInput.type = "number"; + this._ignoreCountInput.min = 0; + this._ignoreCountInput.value = 0; + this._ignoreCountInput.addEventListener("change", this._popoverIgnoreInputChanged.bind(this)); + + ignoreCountLabel.setAttribute("for", this._ignoreCountInput.id); + ignoreCountLabel.textContent = WebInspector.UIString("Ignore"); + + this._ignoreCountText = ignoreCountData.appendChild(document.createElement("span")); + this._updateIgnoreCountText(); + } + + let actionRow = table.appendChild(document.createElement("tr")); + let actionHeader = actionRow.appendChild(document.createElement("th")); + let actionData = this._actionsContainer = actionRow.appendChild(document.createElement("td")); + let actionLabel = actionHeader.appendChild(document.createElement("label")); + actionLabel.textContent = WebInspector.UIString("Action"); + + if (!this._breakpoint.actions.length) + this._popoverActionsCreateAddActionButton(); + else { + this._popoverContentElement.classList.add(WebInspector.BreakpointPopoverController.WidePopoverClassName); + for (let i = 0; i < this._breakpoint.actions.length; ++i) { + let breakpointActionView = new WebInspector.BreakpointActionView(this._breakpoint.actions[i], this, true); + this._popoverActionsInsertBreakpointActionView(breakpointActionView, i); + } + } + + let optionsRow = this._popoverOptionsRowElement = table.appendChild(document.createElement("tr")); + if (!this._breakpoint.actions.length) + optionsRow.classList.add(WebInspector.BreakpointPopoverController.HiddenStyleClassName); + let optionsHeader = optionsRow.appendChild(document.createElement("th")); + let optionsData = optionsRow.appendChild(document.createElement("td")); + let optionsLabel = optionsHeader.appendChild(document.createElement("label")); + let optionsCheckbox = this._popoverOptionsCheckboxElement = optionsData.appendChild(document.createElement("input")); + let optionsCheckboxLabel = optionsData.appendChild(document.createElement("label")); + optionsCheckbox.id = "edit-breakpoint-popoover-auto-continue"; + optionsCheckbox.type = "checkbox"; + optionsCheckbox.checked = this._breakpoint.autoContinue; + optionsCheckbox.addEventListener("change", this._popoverToggleAutoContinueCheckboxChanged.bind(this)); + optionsLabel.textContent = WebInspector.UIString("Options"); + optionsCheckboxLabel.setAttribute("for", optionsCheckbox.id); + optionsCheckboxLabel.textContent = WebInspector.UIString("Automatically continue after evaluating"); + } + + this._popoverContentElement.appendChild(checkboxLabel); + this._popoverContentElement.appendChild(table); + } + + _popoverToggleEnabledCheckboxChanged(event) + { + this._breakpoint.disabled = !event.target.checked; + } + + _conditionCodeMirrorChanged(codeMirror, change) + { + this._breakpoint.condition = (codeMirror.getValue() || "").trim(); + } + + _conditionCodeMirrorBeforeChange(codeMirror, change) + { + if (change.update) { + let newText = change.text.join("").replace(/\n/g, ""); + change.update(change.from, change.to, [newText]); + } + + return true; + } + + _conditionCodeMirrorEscapeOrEnterKey() + { + if (!this._popover) + return; + + this._popover.dismiss(); + } + + _popoverIgnoreInputChanged(event) + { + let ignoreCount = 0; + if (event.target.value) { + ignoreCount = parseInt(event.target.value, 10); + if (isNaN(ignoreCount) || ignoreCount < 0) + ignoreCount = 0; + } + + this._ignoreCountInput.value = ignoreCount; + this._breakpoint.ignoreCount = ignoreCount; + + this._updateIgnoreCountText(); + } + + _popoverToggleAutoContinueCheckboxChanged(event) + { + this._breakpoint.autoContinue = event.target.checked; + } + + _popoverActionsCreateAddActionButton() + { + this._popoverContentElement.classList.remove(WebInspector.BreakpointPopoverController.WidePopoverClassName); + this._actionsContainer.removeChildren(); + + let addActionButton = this._actionsContainer.appendChild(document.createElement("button")); + addActionButton.textContent = WebInspector.UIString("Add Action"); + addActionButton.addEventListener("click", this._popoverActionsAddActionButtonClicked.bind(this)); + } + + _popoverActionsAddActionButtonClicked(event) + { + this._popoverContentElement.classList.add(WebInspector.BreakpointPopoverController.WidePopoverClassName); + this._actionsContainer.removeChildren(); + + let newAction = this._breakpoint.createAction(WebInspector.Breakpoint.DefaultBreakpointActionType); + let newBreakpointActionView = new WebInspector.BreakpointActionView(newAction, this); + this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, -1); + this._popoverOptionsRowElement.classList.remove(WebInspector.BreakpointPopoverController.HiddenStyleClassName); + this._popover.update(); + } + + _popoverActionsInsertBreakpointActionView(breakpointActionView, index) + { + if (index === -1) + this._actionsContainer.appendChild(breakpointActionView.element); + else { + let nextElement = this._actionsContainer.children[index + 1] || null; + this._actionsContainer.insertBefore(breakpointActionView.element, nextElement); + } + } + + _updateIgnoreCountText() + { + if (this._breakpoint.ignoreCount === 1) + this._ignoreCountText.textContent = WebInspector.UIString("time before stopping"); + else + this._ignoreCountText.textContent = WebInspector.UIString("times before stopping"); + } + + breakpointActionViewAppendActionView(breakpointActionView, newAction) + { + let newBreakpointActionView = new WebInspector.BreakpointActionView(newAction, this); + + let index = 0; + let children = this._actionsContainer.children; + for (let i = 0; children.length; ++i) { + if (children[i] === breakpointActionView.element) { + index = i; + break; + } + } + + this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, index); + this._popoverOptionsRowElement.classList.remove(WebInspector.BreakpointPopoverController.HiddenStyleClassName); + + this._popover.update(); + } + + breakpointActionViewRemoveActionView(breakpointActionView) + { + breakpointActionView.element.remove(); + + if (!this._actionsContainer.children.length) { + this._popoverActionsCreateAddActionButton(); + this._popoverOptionsRowElement.classList.add(WebInspector.BreakpointPopoverController.HiddenStyleClassName); + this._popoverOptionsCheckboxElement.checked = false; + } + + this._popover.update(); + } + + breakpointActionViewResized(breakpointActionView) + { + this._popover.update(); + } + + willDismissPopover(popover) + { + console.assert(this._popover === popover); + this._popoverContentElement = null; + this._popoverOptionsRowElement = null; + this._popoverOptionsCheckboxElement = null; + this._actionsContainer = null; + this._popover = null; + } + + didDismissPopover(popover) + { + // Remove Evaluate and Probe actions that have no data. + let emptyActions = this._breakpoint.actions.filter(function(action) { + if (action.type !== WebInspector.BreakpointAction.Type.Evaluate && action.type !== WebInspector.BreakpointAction.Type.Probe) + return false; + return !(action.data && action.data.trim()); + }); + + for (let action of emptyActions) + this._breakpoint.removeAction(action); + + this._breakpoint = null; + } +}; + +WebInspector.BreakpointPopoverController.WidePopoverClassName = "wide"; +WebInspector.BreakpointPopoverController.HiddenStyleClassName = "hidden"; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CSSStyleManager.js b/Source/WebInspectorUI/UserInterface/Controllers/CSSStyleManager.js new file mode 100644 index 000000000..8e101e31f --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CSSStyleManager.js @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CSSStyleManager = class CSSStyleManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.CSSAgent) + CSSAgent.enable(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceAdded, this); + WebInspector.Resource.addEventListener(WebInspector.SourceCode.Event.ContentDidChange, this._resourceContentDidChange, this); + WebInspector.Resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this); + + WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeModified, this._nodeAttributesDidChange, this); + WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeRemoved, this._nodeAttributesDidChange, this); + WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this); + + this._colorFormatSetting = new WebInspector.Setting("default-color-format", WebInspector.Color.Format.Original); + + this._styleSheetIdentifierMap = new Map; + this._styleSheetFrameURLMap = new Map; + this._nodeStylesMap = {}; + + // COMPATIBILITY (iOS 9): Legacy backends did not send stylesheet + // added/removed events and must be fetched manually. + this._fetchedInitialStyleSheets = window.CSSAgent && window.CSSAgent.hasEvent("styleSheetAdded"); + } + + // Static + + static protocolStyleSheetOriginToEnum(origin) + { + switch (origin) { + case CSSAgent.StyleSheetOrigin.Regular: + return WebInspector.CSSStyleSheet.Type.Author; + case CSSAgent.StyleSheetOrigin.User: + return WebInspector.CSSStyleSheet.Type.User; + case CSSAgent.StyleSheetOrigin.UserAgent: + return WebInspector.CSSStyleSheet.Type.UserAgent; + case CSSAgent.StyleSheetOrigin.Inspector: + return WebInspector.CSSStyleSheet.Type.Inspector; + default: + console.assert(false, "Unknown CSS.StyleSheetOrigin", origin); + return CSSAgent.StyleSheetOrigin.Regular; + } + } + + static protocolMediaSourceToEnum(source) + { + switch (source) { + case CSSAgent.CSSMediaSource.MediaRule: + return WebInspector.CSSMedia.Type.MediaRule; + case CSSAgent.CSSMediaSource.ImportRule: + return WebInspector.CSSMedia.Type.ImportRule; + case CSSAgent.CSSMediaSource.LinkedSheet: + return WebInspector.CSSMedia.Type.LinkedStyleSheet; + case CSSAgent.CSSMediaSource.InlineSheet: + return WebInspector.CSSMedia.Type.InlineStyleSheet; + default: + console.assert(false, "Unknown CSS.CSSMediaSource", source); + return WebInspector.CSSMedia.Type.MediaRule; + } + } + + // Public + + get preferredColorFormat() + { + return this._colorFormatSetting.value; + } + + get styleSheets() + { + return [...this._styleSheetIdentifierMap.values()]; + } + + canForcePseudoClasses() + { + return window.CSSAgent && !!CSSAgent.forcePseudoState; + } + + propertyNameHasOtherVendorPrefix(name) + { + if (!name || name.length < 4 || name.charAt(0) !== "-") + return false; + + var match = name.match(/^(?:-moz-|-ms-|-o-|-epub-)/); + if (!match) + return false; + + return true; + } + + propertyValueHasOtherVendorKeyword(value) + { + var match = value.match(/(?:-moz-|-ms-|-o-|-epub-)[-\w]+/); + if (!match) + return false; + + return true; + } + + canonicalNameForPropertyName(name) + { + if (!name || name.length < 8 || name.charAt(0) !== "-") + return name; + + var match = name.match(/^(?:-webkit-|-khtml-|-apple-)(.+)/); + if (!match) + return name; + + return match[1]; + } + + fetchStyleSheetsIfNeeded() + { + if (this._fetchedInitialStyleSheets) + return; + + this._fetchInfoForAllStyleSheets(function() {}); + } + + styleSheetForIdentifier(id) + { + let styleSheet = this._styleSheetIdentifierMap.get(id); + if (styleSheet) + return styleSheet; + + styleSheet = new WebInspector.CSSStyleSheet(id); + this._styleSheetIdentifierMap.set(id, styleSheet); + return styleSheet; + } + + stylesForNode(node) + { + if (node.id in this._nodeStylesMap) + return this._nodeStylesMap[node.id]; + + var styles = new WebInspector.DOMNodeStyles(node); + this._nodeStylesMap[node.id] = styles; + return styles; + } + + preferredInspectorStyleSheetForFrame(frame, callback) + { + var inspectorStyleSheets = this._inspectorStyleSheetsForFrame(frame); + for (let styleSheet of inspectorStyleSheets) { + if (styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol]) { + callback(styleSheet); + return; + } + } + + if (CSSAgent.createStyleSheet) { + CSSAgent.createStyleSheet(frame.id, function(error, styleSheetId) { + let styleSheet = WebInspector.cssStyleManager.styleSheetForIdentifier(styleSheetId); + styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol] = true; + callback(styleSheet); + }); + return; + } + + // COMPATIBILITY (iOS 9): CSS.createStyleSheet did not exist. + // Legacy backends can only create the Inspector StyleSheet through CSS.addRule. + // Exploit that to create the Inspector StyleSheet for the document.body node in + // this frame, then get the StyleSheet for the new rule. + + let expression = appendWebInspectorSourceURL("document"); + let contextId = frame.pageExecutionContext.id; + RuntimeAgent.evaluate.invoke({expression, objectGroup: "", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, contextId, returnByValue: false, generatePreview: false}, documentAvailable); + + function documentAvailable(error, documentRemoteObjectPayload) + { + if (error) { + callback(null); + return; + } + + let remoteObject = WebInspector.RemoteObject.fromPayload(documentRemoteObjectPayload); + remoteObject.pushNodeToFrontend(documentNodeAvailable.bind(null, remoteObject)); + } + + function documentNodeAvailable(remoteObject, documentNodeId) + { + remoteObject.release(); + + if (!documentNodeId) { + callback(null); + return; + } + + DOMAgent.querySelector(documentNodeId, "body", bodyNodeAvailable); + } + + function bodyNodeAvailable(error, bodyNodeId) + { + if (error) { + console.error(error); + callback(null); + return; + } + + let selector = ""; // Intentionally empty. + CSSAgent.addRule(bodyNodeId, selector, cssRuleAvailable); + } + + function cssRuleAvailable(error, payload) + { + if (error || !payload.ruleId) { + callback(null); + return; + } + + let styleSheetId = payload.ruleId.styleSheetId; + let styleSheet = WebInspector.cssStyleManager.styleSheetForIdentifier(styleSheetId); + if (!styleSheet) { + callback(null); + return; + } + + styleSheet[WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol] = true; + + console.assert(styleSheet.isInspectorStyleSheet()); + console.assert(styleSheet.parentFrame === frame); + + callback(styleSheet); + } + } + + mediaTypeChanged() + { + // Act the same as if media queries changed. + this.mediaQueryResultChanged(); + } + + // Protected + + mediaQueryResultChanged() + { + // Called from WebInspector.CSSObserver. + + for (var key in this._nodeStylesMap) + this._nodeStylesMap[key].mediaQueryResultDidChange(); + } + + styleSheetChanged(styleSheetIdentifier) + { + // Called from WebInspector.CSSObserver. + var styleSheet = this.styleSheetForIdentifier(styleSheetIdentifier); + console.assert(styleSheet); + + // Do not observe inline styles + if (styleSheet.isInlineStyleAttributeStyleSheet()) + return; + + styleSheet.noteContentDidChange(); + this._updateResourceContent(styleSheet); + } + + styleSheetAdded(styleSheetInfo) + { + console.assert(!this._styleSheetIdentifierMap.has(styleSheetInfo.styleSheetId), "Attempted to add a CSSStyleSheet but identifier was already in use"); + let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId); + let parentFrame = WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId); + let origin = WebInspector.CSSStyleManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin); + styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, styleSheetInfo.isInline, styleSheetInfo.startLine, styleSheetInfo.startColumn); + + this.dispatchEventToListeners(WebInspector.CSSStyleManager.Event.StyleSheetAdded, {styleSheet}); + } + + styleSheetRemoved(styleSheetIdentifier) + { + let styleSheet = this._styleSheetIdentifierMap.get(styleSheetIdentifier); + console.assert(styleSheet, "Attempted to remove a CSSStyleSheet that was not tracked"); + if (!styleSheet) + return; + + this._styleSheetIdentifierMap.delete(styleSheetIdentifier); + + this.dispatchEventToListeners(WebInspector.CSSStyleManager.Event.StyleSheetRemoved, {styleSheet}); + } + + // Private + + _inspectorStyleSheetsForFrame(frame) + { + let styleSheets = []; + + for (let styleSheet of this.styleSheets) { + if (styleSheet.isInspectorStyleSheet() && styleSheet.parentFrame === frame) + styleSheets.push(styleSheet); + } + + return styleSheets; + } + + _nodePseudoClassesDidChange(event) + { + var node = event.target; + + for (var key in this._nodeStylesMap) { + var nodeStyles = this._nodeStylesMap[key]; + if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node)) + continue; + nodeStyles.pseudoClassesDidChange(node); + } + } + + _nodeAttributesDidChange(event) + { + var node = event.target; + + for (var key in this._nodeStylesMap) { + var nodeStyles = this._nodeStylesMap[key]; + if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node)) + continue; + nodeStyles.attributeDidChange(node, event.data.name); + } + } + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (!event.target.isMainFrame()) + return; + + // Clear our maps when the main frame navigates. + + this._fetchedInitialStyleSheets = window.CSSAgent && window.CSSAgent.hasEvent("styleSheetAdded"); + this._styleSheetIdentifierMap.clear(); + this._styleSheetFrameURLMap.clear(); + this._nodeStylesMap = {}; + } + + _resourceAdded(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + var resource = event.data.resource; + console.assert(resource); + + if (resource.type !== WebInspector.Resource.Type.Stylesheet) + return; + + this._clearStyleSheetsForResource(resource); + } + + _resourceTypeDidChange(event) + { + console.assert(event.target instanceof WebInspector.Resource); + + var resource = event.target; + if (resource.type !== WebInspector.Resource.Type.Stylesheet) + return; + + this._clearStyleSheetsForResource(resource); + } + + _clearStyleSheetsForResource(resource) + { + // Clear known stylesheets for this URL and frame. This will cause the stylesheets to + // be updated next time _fetchInfoForAllStyleSheets is called. + this._styleSheetIdentifierMap.delete(this._frameURLMapKey(resource.parentFrame, resource.url)); + } + + _frameURLMapKey(frame, url) + { + return frame.id + ":" + url; + } + + _lookupStyleSheetForResource(resource, callback) + { + this._lookupStyleSheet(resource.parentFrame, resource.url, callback); + } + + _lookupStyleSheet(frame, url, callback) + { + console.assert(frame instanceof WebInspector.Frame); + + let key = this._frameURLMapKey(frame, url); + + function styleSheetsFetched() + { + callback(this._styleSheetFrameURLMap.get(key) || null); + } + + let styleSheet = this._styleSheetFrameURLMap.get(key) || null; + if (styleSheet) + callback(styleSheet); + else + this._fetchInfoForAllStyleSheets(styleSheetsFetched.bind(this)); + } + + _fetchInfoForAllStyleSheets(callback) + { + console.assert(typeof callback === "function"); + + function processStyleSheets(error, styleSheets) + { + this._styleSheetFrameURLMap.clear(); + + if (error) { + callback(); + return; + } + + for (let styleSheetInfo of styleSheets) { + let parentFrame = WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId); + let origin = WebInspector.CSSStyleManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin); + + // COMPATIBILITY (iOS 9): The info did not have 'isInline', 'startLine', and 'startColumn', so make false and 0 in these cases. + let isInline = styleSheetInfo.isInline || false; + let startLine = styleSheetInfo.startLine || 0; + let startColumn = styleSheetInfo.startColumn || 0; + + let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId); + styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, isInline, startLine, startColumn); + + let key = this._frameURLMapKey(parentFrame, styleSheetInfo.sourceURL); + this._styleSheetFrameURLMap.set(key, styleSheet); + } + + callback(); + } + + CSSAgent.getAllStyleSheets(processStyleSheets.bind(this)); + } + + _resourceContentDidChange(event) + { + var resource = event.target; + if (resource === this._ignoreResourceContentDidChangeEventForResource) + return; + + // Ignore if it isn't a CSS stylesheet. + if (resource.type !== WebInspector.Resource.Type.Stylesheet || resource.syntheticMIMEType !== "text/css") + return; + + function applyStyleSheetChanges() + { + function styleSheetFound(styleSheet) + { + resource.__pendingChangeTimeout = undefined; + + console.assert(styleSheet); + if (!styleSheet) + return; + + // To prevent updating a TextEditor's content while the user is typing in it we want to + // ignore the next _updateResourceContent call. + resource.__ignoreNextUpdateResourceContent = true; + + WebInspector.branchManager.currentBranch.revisionForRepresentedObject(styleSheet).content = resource.content; + } + + this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this)); + } + + if (resource.__pendingChangeTimeout) + clearTimeout(resource.__pendingChangeTimeout); + resource.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500); + } + + _updateResourceContent(styleSheet) + { + console.assert(styleSheet); + + function fetchedStyleSheetContent(parameters) + { + var styleSheet = parameters.sourceCode; + var content = parameters.content; + + styleSheet.__pendingChangeTimeout = undefined; + + console.assert(styleSheet.url); + if (!styleSheet.url) + return; + + var resource = styleSheet.parentFrame.resourceForURL(styleSheet.url); + if (!resource) + return; + + // Only try to update stylesheet resources. Other resources, like documents, can contain + // multiple stylesheets and we don't have the source ranges to update those. + if (resource.type !== WebInspector.Resource.Type.Stylesheet) + return; + + if (resource.__ignoreNextUpdateResourceContent) { + resource.__ignoreNextUpdateResourceContent = false; + return; + } + + this._ignoreResourceContentDidChangeEventForResource = resource; + WebInspector.branchManager.currentBranch.revisionForRepresentedObject(resource).content = content; + this._ignoreResourceContentDidChangeEventForResource = null; + } + + function styleSheetReady() + { + styleSheet.requestContent().then(fetchedStyleSheetContent.bind(this)); + } + + function applyStyleSheetChanges() + { + if (styleSheet.url) + styleSheetReady.call(this); + else + this._fetchInfoForAllStyleSheets(styleSheetReady.bind(this)); + } + + if (styleSheet.__pendingChangeTimeout) + clearTimeout(styleSheet.__pendingChangeTimeout); + styleSheet.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500); + } +}; + +WebInspector.CSSStyleManager.Event = { + StyleSheetAdded: "css-style-manager-style-sheet-added", + StyleSheetRemoved: "css-style-manager-style-sheet-removed", +}; + +WebInspector.CSSStyleManager.PseudoElementNames = ["before", "after"]; +WebInspector.CSSStyleManager.ForceablePseudoClasses = ["active", "focus", "hover", "visited"]; +WebInspector.CSSStyleManager.PreferredInspectorStyleSheetSymbol = Symbol("css-style-manager-preferred-inspector-stylesheet"); diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorBezierEditingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorBezierEditingController.js new file mode 100644 index 000000000..e79da97f9 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorBezierEditingController.js @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorBezierEditingController = class CodeMirrorBezierEditingController extends WebInspector.CodeMirrorEditingController +{ + constructor(codeMirror, marker) + { + super(codeMirror, marker); + } + + // Public + + get initialValue() + { + return WebInspector.CubicBezier.fromString(this.text); + } + + get cssClassName() + { + return "cubic-bezier"; + } + + popoverWillPresent(popover) + { + this._bezierEditor = new WebInspector.BezierEditor; + this._bezierEditor.addEventListener(WebInspector.BezierEditor.Event.BezierChanged, this._bezierEditorBezierChanged, this); + popover.content = this._bezierEditor.element; + } + + popoverDidPresent(popover) + { + this._bezierEditor.bezier = this.value; + } + + popoverDidDismiss(popover) + { + this._bezierEditor.removeListeners(); + } + + // Private + + _bezierEditorBezierChanged(event) + { + this.value = event.data.bezier; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorColorEditingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorColorEditingController.js new file mode 100644 index 000000000..b45846fd5 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorColorEditingController.js @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorColorEditingController = class CodeMirrorColorEditingController extends WebInspector.CodeMirrorEditingController +{ + constructor(codeMirror, marker) + { + super(codeMirror, marker); + } + + // Public + + get initialValue() + { + return WebInspector.Color.fromString(this.text); + } + + get cssClassName() + { + return "color"; + } + + popoverWillPresent(popover) + { + this._colorPicker = new WebInspector.ColorPicker; + this._colorPicker.addEventListener(WebInspector.ColorPicker.Event.ColorChanged, this._colorPickerColorChanged, this); + this._colorPicker.addEventListener(WebInspector.ColorPicker.Event.FormatChanged, (event) => popover.update()); + popover.content = this._colorPicker.element; + } + + popoverDidPresent(popover) + { + this._colorPicker.color = this._value; + } + + // Private + + _colorPickerColorChanged(event) + { + this.value = event.target.color; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.css b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.css new file mode 100644 index 000000000..eff18cfb1 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.css @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +.CodeMirror .CodeMirror-lines .completion-hint { + text-decoration: none !important; + opacity: 0.4; +} diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.js new file mode 100644 index 000000000..33d7ccbd6 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorCompletionController.js @@ -0,0 +1,875 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorCompletionController = class CodeMirrorCompletionController extends WebInspector.Object +{ + constructor(codeMirror, delegate, stopCharactersRegex) + { + super(); + + console.assert(codeMirror); + + this._codeMirror = codeMirror; + this._stopCharactersRegex = stopCharactersRegex || null; + this._delegate = delegate || null; + + this._startOffset = NaN; + this._endOffset = NaN; + this._lineNumber = NaN; + this._prefix = ""; + this._noEndingSemicolon = false; + this._completions = []; + this._extendedCompletionProviders = {}; + + this._suggestionsView = new WebInspector.CompletionSuggestionsView(this); + + this._keyMap = { + "Up": this._handleUpKey.bind(this), + "Down": this._handleDownKey.bind(this), + "Right": this._handleRightOrEnterKey.bind(this), + "Esc": this._handleEscapeKey.bind(this), + "Enter": this._handleRightOrEnterKey.bind(this), + "Tab": this._handleTabKey.bind(this), + "Cmd-A": this._handleHideKey.bind(this), + "Cmd-Z": this._handleHideKey.bind(this), + "Shift-Cmd-Z": this._handleHideKey.bind(this), + "Cmd-Y": this._handleHideKey.bind(this) + }; + + this._handleChangeListener = this._handleChange.bind(this); + this._handleCursorActivityListener = this._handleCursorActivity.bind(this); + this._handleHideActionListener = this._handleHideAction.bind(this); + + this._codeMirror.addKeyMap(this._keyMap); + + this._codeMirror.on("change", this._handleChangeListener); + this._codeMirror.on("cursorActivity", this._handleCursorActivityListener); + this._codeMirror.on("blur", this._handleHideActionListener); + this._codeMirror.on("scroll", this._handleHideActionListener); + + this._updatePromise = null; + } + + // Public + + get delegate() + { + return this._delegate; + } + + addExtendedCompletionProvider(modeName, provider) + { + this._extendedCompletionProviders[modeName] = provider; + } + + updateCompletions(completions, implicitSuffix) + { + if (isNaN(this._startOffset) || isNaN(this._endOffset) || isNaN(this._lineNumber)) + return; + + if (!completions || !completions.length) { + this.hideCompletions(); + return; + } + + this._completions = completions; + + if (typeof implicitSuffix === "string") + this._implicitSuffix = implicitSuffix; + + var from = {line: this._lineNumber, ch: this._startOffset}; + var to = {line: this._lineNumber, ch: this._endOffset}; + + var firstCharCoords = this._codeMirror.cursorCoords(from); + var lastCharCoords = this._codeMirror.cursorCoords(to); + var bounds = new WebInspector.Rect(firstCharCoords.left, firstCharCoords.top, lastCharCoords.right - firstCharCoords.left, firstCharCoords.bottom - firstCharCoords.top); + + // Try to restore the previous selected index, otherwise just select the first. + var index = this._currentCompletion ? completions.indexOf(this._currentCompletion) : 0; + if (index === -1) + index = 0; + + if (this._forced || completions.length > 1 || completions[index] !== this._prefix) { + // Update and show the suggestion list. + this._suggestionsView.update(completions, index); + this._suggestionsView.show(bounds); + } else if (this._implicitSuffix) { + // The prefix and the completion exactly match, but there is an implicit suffix. + // Just hide the suggestion list and keep the completion hint for the implicit suffix. + this._suggestionsView.hide(); + } else { + // The prefix and the completion exactly match, hide the completions. Return early so + // the completion hint isn't updated. + this.hideCompletions(); + return; + } + + this._applyCompletionHint(completions[index]); + + this._resolveUpdatePromise(WebInspector.CodeMirrorCompletionController.UpdatePromise.CompletionsFound); + } + + isCompletionChange(change) + { + return this._ignoreChange || change.origin === WebInspector.CodeMirrorCompletionController.CompletionOrigin || change.origin === WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin; + } + + isShowingCompletions() + { + return this._suggestionsView.visible || (this._completionHintMarker && this._completionHintMarker.find()); + } + + isHandlingClickEvent() + { + return this._suggestionsView.isHandlingClickEvent(); + } + + hideCompletions() + { + this._suggestionsView.hide(); + + this._removeCompletionHint(); + + this._startOffset = NaN; + this._endOffset = NaN; + this._lineNumber = NaN; + this._prefix = ""; + this._completions = []; + this._implicitSuffix = ""; + this._forced = false; + + delete this._currentCompletion; + delete this._ignoreNextCursorActivity; + + this._resolveUpdatePromise(WebInspector.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound); + } + + close() + { + this._codeMirror.removeKeyMap(this._keyMap); + + this._codeMirror.off("change", this._handleChangeListener); + this._codeMirror.off("cursorActivity", this._handleCursorActivityListener); + this._codeMirror.off("blur", this._handleHideActionListener); + this._codeMirror.off("scroll", this._handleHideActionListener); + } + + completeAtCurrentPositionIfNeeded(force) + { + this._resolveUpdatePromise(WebInspector.CodeMirrorCompletionController.UpdatePromise.Canceled); + + var update = this._updatePromise = new WebInspector.WrappedPromise; + + this._completeAtCurrentPosition(force); + + return update.promise; + } + + // Protected + + completionSuggestionsSelectedCompletion(suggestionsView, completionText) + { + this._applyCompletionHint(completionText); + } + + completionSuggestionsClickedCompletion(suggestionsView, completionText) + { + // The clicked suggestion causes the editor to loose focus. Restore it so the user can keep typing. + this._codeMirror.focus(); + + this._applyCompletionHint(completionText); + this._commitCompletionHint(); + } + + set noEndingSemicolon(noEndingSemicolon) + { + this._noEndingSemicolon = noEndingSemicolon; + } + + // Private + + _resolveUpdatePromise(message) + { + if (!this._updatePromise) + return; + + this._updatePromise.resolve(message); + this._updatePromise = null; + } + + get _currentReplacementText() + { + return this._currentCompletion + this._implicitSuffix; + } + + _hasPendingCompletion() + { + return !isNaN(this._startOffset) && !isNaN(this._endOffset) && !isNaN(this._lineNumber); + } + + _notifyCompletionsHiddenSoon() + { + function notify() + { + if (this._completionHintMarker) + return; + + if (this._delegate && typeof this._delegate.completionControllerCompletionsHidden === "function") + this._delegate.completionControllerCompletionsHidden(this); + } + + if (this._notifyCompletionsHiddenIfNeededTimeout) + clearTimeout(this._notifyCompletionsHiddenIfNeededTimeout); + this._notifyCompletionsHiddenIfNeededTimeout = setTimeout(notify.bind(this), WebInspector.CodeMirrorCompletionController.CompletionsHiddenDelay); + } + + _createCompletionHintMarker(position, text) + { + var container = document.createElement("span"); + container.classList.add(WebInspector.CodeMirrorCompletionController.CompletionHintStyleClassName); + container.textContent = text; + + this._completionHintMarker = this._codeMirror.setUniqueBookmark(position, {widget: container, insertLeft: true}); + } + + _applyCompletionHint(completionText) + { + console.assert(completionText); + if (!completionText) + return; + + function update() + { + this._currentCompletion = completionText; + + this._removeCompletionHint(true, true); + + var replacementText = this._currentReplacementText; + + var from = {line: this._lineNumber, ch: this._startOffset}; + var cursor = {line: this._lineNumber, ch: this._endOffset}; + var currentText = this._codeMirror.getRange(from, cursor); + + this._createCompletionHintMarker(cursor, replacementText.replace(currentText, "")); + } + + this._ignoreChange = true; + this._ignoreNextCursorActivity = true; + + this._codeMirror.operation(update.bind(this)); + + delete this._ignoreChange; + } + + _commitCompletionHint() + { + function update() + { + this._removeCompletionHint(true, true); + + var replacementText = this._currentReplacementText; + + var from = {line: this._lineNumber, ch: this._startOffset}; + var cursor = {line: this._lineNumber, ch: this._endOffset}; + var to = {line: this._lineNumber, ch: this._startOffset + replacementText.length}; + + var lastChar = this._currentCompletion.charAt(this._currentCompletion.length - 1); + var isClosing = ")]}".indexOf(lastChar); + if (isClosing !== -1) + to.ch -= 1 + this._implicitSuffix.length; + + this._codeMirror.replaceRange(replacementText, from, cursor, WebInspector.CodeMirrorCompletionController.CompletionOrigin); + + // Don't call _removeLastChangeFromHistory here to allow the committed completion to be undone. + + this._codeMirror.setCursor(to); + + this.hideCompletions(); + } + + this._ignoreChange = true; + this._ignoreNextCursorActivity = true; + + this._codeMirror.operation(update.bind(this)); + + delete this._ignoreChange; + } + + _removeLastChangeFromHistory() + { + var history = this._codeMirror.getHistory(); + + // We don't expect a undone history. But if there is one clear it. If could lead to undefined behavior. + console.assert(!history.undone.length); + history.undone = []; + + // Pop the last item from the done history. + console.assert(history.done.length); + history.done.pop(); + + this._codeMirror.setHistory(history); + } + + _removeCompletionHint(nonatomic, dontRestorePrefix) + { + if (!this._completionHintMarker) + return; + + this._notifyCompletionsHiddenSoon(); + + function clearMarker(marker) + { + if (!marker) + return; + + var range = marker.find(); + if (range) + marker.clear(); + + return null; + } + + function update() + { + this._completionHintMarker = clearMarker(this._completionHintMarker); + + if (dontRestorePrefix) + return; + + console.assert(!isNaN(this._startOffset)); + console.assert(!isNaN(this._endOffset)); + console.assert(!isNaN(this._lineNumber)); + + var from = {line: this._lineNumber, ch: this._startOffset}; + var to = {line: this._lineNumber, ch: this._endOffset}; + + this._codeMirror.replaceRange(this._prefix, from, to, WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin); + this._removeLastChangeFromHistory(); + } + + if (nonatomic) { + update.call(this); + return; + } + + this._ignoreChange = true; + + this._codeMirror.operation(update.bind(this)); + + delete this._ignoreChange; + } + + _scanStringForExpression(modeName, string, startOffset, direction, allowMiddleAndEmpty, includeStopCharacter, ignoreInitialUnmatchedOpenBracket, stopCharactersRegex) + { + console.assert(direction === -1 || direction === 1); + + var stopCharactersRegex = stopCharactersRegex || this._stopCharactersRegex || WebInspector.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap[modeName] || WebInspector.CodeMirrorCompletionController.GenericStopCharactersRegex; + + function isStopCharacter(character) + { + return stopCharactersRegex.test(character); + } + + function isOpenBracketCharacter(character) + { + return WebInspector.CodeMirrorCompletionController.OpenBracketCharactersRegex.test(character); + } + + function isCloseBracketCharacter(character) + { + return WebInspector.CodeMirrorCompletionController.CloseBracketCharactersRegex.test(character); + } + + function matchingBracketCharacter(character) + { + return WebInspector.CodeMirrorCompletionController.MatchingBrackets[character]; + } + + var endOffset = Math.min(startOffset, string.length); + + var endOfLineOrWord = endOffset === string.length || isStopCharacter(string.charAt(endOffset)); + + if (!endOfLineOrWord && !allowMiddleAndEmpty) + return null; + + var bracketStack = []; + var bracketOffsetStack = []; + + var startOffset = endOffset; + var firstOffset = endOffset + direction; + for (var i = firstOffset; direction > 0 ? i < string.length : i >= 0; i += direction) { + var character = string.charAt(i); + + // Ignore stop characters when we are inside brackets. + if (isStopCharacter(character) && !bracketStack.length) + break; + + if (isCloseBracketCharacter(character)) { + bracketStack.push(character); + bracketOffsetStack.push(i); + } else if (isOpenBracketCharacter(character)) { + if ((!ignoreInitialUnmatchedOpenBracket || i !== firstOffset) && (!bracketStack.length || matchingBracketCharacter(character) !== bracketStack.lastValue)) + break; + + bracketOffsetStack.pop(); + bracketStack.pop(); + } + + startOffset = i + (direction > 0 ? 1 : 0); + } + + if (bracketOffsetStack.length) + startOffset = bracketOffsetStack.pop() + 1; + + if (includeStopCharacter && startOffset > 0 && startOffset < string.length) + startOffset += direction; + + if (direction > 0) { + var tempEndOffset = endOffset; + endOffset = startOffset; + startOffset = tempEndOffset; + } + + return {string: string.substring(startOffset, endOffset), startOffset, endOffset}; + } + + _completeAtCurrentPosition(force) + { + if (this._codeMirror.somethingSelected()) { + this.hideCompletions(); + return; + } + + this._removeCompletionHint(true, true); + + var cursor = this._codeMirror.getCursor(); + var token = this._codeMirror.getTokenAt(cursor); + + // Don't try to complete inside comments. + if (token.type && /\bcomment\b/.test(token.type)) { + this.hideCompletions(); + return; + } + + var mode = this._codeMirror.getMode(); + var innerMode = CodeMirror.innerMode(mode, token.state).mode; + var modeName = innerMode.alternateName || innerMode.name; + + var lineNumber = cursor.line; + var lineString = this._codeMirror.getLine(lineNumber); + + var backwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, -1, force); + if (!backwardScanResult) { + this.hideCompletions(); + return; + } + + var forwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, 1, true, true); + var suffix = forwardScanResult.string; + + this._ignoreNextCursorActivity = true; + + this._startOffset = backwardScanResult.startOffset; + this._endOffset = backwardScanResult.endOffset; + this._lineNumber = lineNumber; + this._prefix = backwardScanResult.string; + this._completions = []; + this._implicitSuffix = ""; + this._forced = force; + + var baseExpressionStopCharactersRegex = WebInspector.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap[modeName]; + if (baseExpressionStopCharactersRegex) + var baseScanResult = this._scanStringForExpression(modeName, lineString, this._startOffset, -1, true, false, true, baseExpressionStopCharactersRegex); + + if (!force && !backwardScanResult.string && (!baseScanResult || !baseScanResult.string)) { + this.hideCompletions(); + return; + } + + var defaultCompletions = []; + + switch (modeName) { + case "css": + defaultCompletions = this._generateCSSCompletions(token, baseScanResult ? baseScanResult.string : null, suffix); + break; + case "javascript": + defaultCompletions = this._generateJavaScriptCompletions(token, baseScanResult ? baseScanResult.string : null, suffix); + break; + } + + var extendedCompletionsProvider = this._extendedCompletionProviders[modeName]; + if (extendedCompletionsProvider) { + extendedCompletionsProvider.completionControllerCompletionsNeeded(this, defaultCompletions, baseScanResult ? baseScanResult.string : null, this._prefix, suffix, force); + return; + } + + if (this._delegate && typeof this._delegate.completionControllerCompletionsNeeded === "function") + this._delegate.completionControllerCompletionsNeeded(this, this._prefix, defaultCompletions, baseScanResult ? baseScanResult.string : null, suffix, force); + else + this.updateCompletions(defaultCompletions); + } + + _generateCSSCompletions(mainToken, base, suffix) + { + // We only support completion inside CSS block context. + if (mainToken.state.state === "media" || mainToken.state.state === "top" || mainToken.state.state === "parens") + return []; + + // Don't complete in the middle of a property name. + if (/^[a-z]/i.test(suffix)) + return []; + + var token = mainToken; + var lineNumber = this._lineNumber; + + // Scan backwards looking for the current property. + while (token.state.state === "prop") { + // Found the beginning of the line. Go to the previous line. + if (!token.start) { + --lineNumber; + + // No more lines, stop. + if (lineNumber < 0) + break; + } + + // Get the previous token. + token = this._codeMirror.getTokenAt({line: lineNumber, ch: token.start ? token.start : Number.MAX_VALUE}); + } + + // If we have a property token and it's not the main token, then we are working on + // the value for that property and should complete allowed values. + if (mainToken !== token && token.type && /\bproperty\b/.test(token.type)) { + var propertyName = token.string; + + // If there is a suffix and it isn't a semicolon, then we should use a space since + // the user is editing in the middle. Likewise if the suffix starts with an open + // paren we are changing a function name so don't add a suffix. + this._implicitSuffix = " "; + if (suffix === ";") + this._implicitSuffix = this._noEndingSemicolon ? "" : ";"; + else if (suffix.startsWith("(")) + this._implicitSuffix = ""; + + // Don't use an implicit suffix if it would be the same as the existing suffix. + if (this._implicitSuffix === suffix) + this._implicitSuffix = ""; + + let completions = WebInspector.CSSKeywordCompletions.forProperty(propertyName).startsWith(this._prefix); + + if (suffix.startsWith("(")) + completions = completions.map((x) => x.replace(/\(\)$/, "")); + + return completions; + } + + this._implicitSuffix = suffix !== ":" ? ": " : ""; + + // Complete property names. + return WebInspector.CSSCompletions.cssNameCompletions.startsWith(this._prefix); + } + + _generateJavaScriptCompletions(mainToken, base, suffix) + { + // If there is a base expression then we should not attempt to match any keywords or variables. + // Allow only open bracket characters at the end of the base, otherwise leave completions with + // a base up to the delegate to figure out. + if (base && !/[({[]$/.test(base)) + return []; + + var matchingWords = []; + + var prefix = this._prefix; + + var localState = mainToken.state.localState ? mainToken.state.localState : mainToken.state; + + var declaringVariable = localState.lexical.type === "vardef"; + var insideSwitch = localState.lexical.prev ? localState.lexical.prev.info === "switch" : false; + var insideBlock = localState.lexical.prev ? localState.lexical.prev.type === "}" : false; + var insideParenthesis = localState.lexical.type === ")"; + var insideBrackets = localState.lexical.type === "]"; + + // FIXME: Include module keywords if we know this is a module environment. + // var moduleKeywords = ["default", "export", "import"]; + + var allKeywords = [ + "break", "case", "catch", "class", "const", "continue", "debugger", "default", + "delete", "do", "else", "extends", "false", "finally", "for", "function", + "if", "in", "Infinity", "instanceof", "let", "NaN", "new", "null", "of", + "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "undefined", "var", "void", "while", "with", "yield" + ]; + var valueKeywords = ["false", "Infinity", "NaN", "null", "this", "true", "undefined"]; + + var allowedKeywordsInsideBlocks = allKeywords.keySet(); + var allowedKeywordsWhenDeclaringVariable = valueKeywords.keySet(); + var allowedKeywordsInsideParenthesis = valueKeywords.concat(["class", "function"]).keySet(); + var allowedKeywordsInsideBrackets = allowedKeywordsInsideParenthesis; + var allowedKeywordsOnlyInsideSwitch = ["case", "default"].keySet(); + + function matchKeywords(keywords) + { + matchingWords = matchingWords.concat(keywords.filter(function(word) { + if (!insideSwitch && word in allowedKeywordsOnlyInsideSwitch) + return false; + if (insideBlock && !(word in allowedKeywordsInsideBlocks)) + return false; + if (insideBrackets && !(word in allowedKeywordsInsideBrackets)) + return false; + if (insideParenthesis && !(word in allowedKeywordsInsideParenthesis)) + return false; + if (declaringVariable && !(word in allowedKeywordsWhenDeclaringVariable)) + return false; + return word.startsWith(prefix); + })); + } + + function matchVariables() + { + function filterVariables(variables) + { + for (var variable = variables; variable; variable = variable.next) { + // Don't match the variable if this token is in a variable declaration. + // Otherwise the currently typed text will always match and that isn't useful. + if (declaringVariable && variable.name === prefix) + continue; + + if (variable.name.startsWith(prefix) && !matchingWords.includes(variable.name)) + matchingWords.push(variable.name); + } + } + + var context = localState.context; + while (context) { + if (context.vars) + filterVariables(context.vars); + context = context.prev; + } + + if (localState.localVars) + filterVariables(localState.localVars); + if (localState.globalVars) + filterVariables(localState.globalVars); + } + + switch (suffix.substring(0, 1)) { + case "": + case " ": + matchVariables(); + matchKeywords(allKeywords); + break; + + case ".": + case "[": + matchVariables(); + matchKeywords(["false", "Infinity", "NaN", "this", "true"]); + break; + + case "(": + matchVariables(); + matchKeywords(["catch", "else", "for", "function", "if", "return", "switch", "throw", "while", "with", "yield"]); + break; + + case "{": + matchKeywords(["do", "else", "finally", "return", "try", "yield"]); + break; + + case ":": + if (insideSwitch) + matchKeywords(["case", "default"]); + break; + + case ";": + matchVariables(); + matchKeywords(valueKeywords); + matchKeywords(["break", "continue", "debugger", "return", "void"]); + break; + } + + return matchingWords; + } + + _handleUpKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._suggestionsView.selectPrevious(); + } + + _handleDownKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._suggestionsView.selectNext(); + } + + _handleRightOrEnterKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._commitCompletionHint(); + } + + _handleEscapeKey(codeMirror) + { + var delegateImplementsShouldAllowEscapeCompletion = this._delegate && typeof this._delegate.completionControllerShouldAllowEscapeCompletion === "function"; + if (this._hasPendingCompletion()) + this.hideCompletions(); + else if (this._codeMirror.getOption("readOnly")) + return CodeMirror.Pass; + else if (!delegateImplementsShouldAllowEscapeCompletion || this._delegate.completionControllerShouldAllowEscapeCompletion(this)) + this._completeAtCurrentPosition(true); + else + return CodeMirror.Pass; + } + + _handleTabKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + console.assert(this._completions.length); + if (!this._completions.length) + return; + + console.assert(this._currentCompletion); + if (!this._currentCompletion) + return; + + // Commit the current completion if there is only one suggestion. + if (this._completions.length === 1) { + this._commitCompletionHint(); + return; + } + + var prefixLength = this._prefix.length; + + var commonPrefix = this._completions[0]; + for (var i = 1; i < this._completions.length; ++i) { + var completion = this._completions[i]; + var lastIndex = Math.min(commonPrefix.length, completion.length); + for (var j = prefixLength; j < lastIndex; ++j) { + if (commonPrefix[j] !== completion[j]) { + commonPrefix = commonPrefix.substr(0, j); + break; + } + } + } + + // Commit the current completion if there is no common prefix that is longer. + if (commonPrefix === this._prefix) { + this._commitCompletionHint(); + return; + } + + // Set the prefix to the common prefix so _applyCompletionHint will insert the + // common prefix as commited text. Adjust _endOffset to match the new prefix. + this._prefix = commonPrefix; + this._endOffset = this._startOffset + commonPrefix.length; + + this._applyCompletionHint(this._currentCompletion); + } + + _handleChange(codeMirror, change) + { + if (this.isCompletionChange(change)) + return; + + this._ignoreNextCursorActivity = true; + + if (!change.origin || change.origin.charAt(0) !== "+") { + this.hideCompletions(); + return; + } + + // Only complete on delete if we are showing completions already. + if (change.origin === "+delete" && !this._hasPendingCompletion()) + return; + + this._completeAtCurrentPosition(false); + } + + _handleCursorActivity(codeMirror) + { + if (this._ignoreChange) + return; + + if (this._ignoreNextCursorActivity) { + delete this._ignoreNextCursorActivity; + return; + } + + this.hideCompletions(); + } + + _handleHideKey(codeMirror) + { + this.hideCompletions(); + + return CodeMirror.Pass; + } + + _handleHideAction(codeMirror) + { + // Clicking a suggestion causes the editor to blur. We don't want to hide completions in this case. + if (this.isHandlingClickEvent()) + return; + + this.hideCompletions(); + } +}; + +WebInspector.CodeMirrorCompletionController.UpdatePromise = { + Canceled: "code-mirror-completion-controller-canceled", + CompletionsFound: "code-mirror-completion-controller-completions-found", + NoCompletionsFound: "code-mirror-completion-controller-no-completions-found" +}; + +WebInspector.CodeMirrorCompletionController.GenericStopCharactersRegex = /[\s=:;,]/; +WebInspector.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap = {"css": /[\s:;,{}()]/, "javascript": /[\s=:;,!+\-*/%&|^~?<>.{}()[\]]/}; +WebInspector.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap = {"javascript": /[\s=:;,!+\-*/%&|^~?<>]/}; +WebInspector.CodeMirrorCompletionController.OpenBracketCharactersRegex = /[({[]/; +WebInspector.CodeMirrorCompletionController.CloseBracketCharactersRegex = /[)}\]]/; +WebInspector.CodeMirrorCompletionController.MatchingBrackets = {"{": "}", "(": ")", "[": "]", "}": "{", ")": "(", "]": "["}; +WebInspector.CodeMirrorCompletionController.CompletionHintStyleClassName = "completion-hint"; +WebInspector.CodeMirrorCompletionController.CompletionsHiddenDelay = 250; +WebInspector.CodeMirrorCompletionController.CompletionTypingDelay = 250; +WebInspector.CodeMirrorCompletionController.CompletionOrigin = "+completion"; +WebInspector.CodeMirrorCompletionController.DeleteCompletionOrigin = "+delete-completion"; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.css b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.css new file mode 100644 index 000000000..d67d6b361 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.css @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +.CodeMirror.drag-to-adjust .CodeMirror-lines { + cursor: col-resize; +} diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.js new file mode 100644 index 000000000..a8105998a --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorDragToAdjustNumberController.js @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorDragToAdjustNumberController = class CodeMirrorDragToAdjustNumberController extends WebInspector.Object +{ + constructor(codeMirror) + { + super(); + + this._codeMirror = codeMirror; + + this._dragToAdjustController = new WebInspector.DragToAdjustController(this); + } + + // Public + + get enabled() + { + return this._dragToAdjustController.enabled; + } + + set enabled(enabled) + { + if (this.enabled === enabled) + return; + + this._dragToAdjustController.element = this._codeMirror.getWrapperElement(); + this._dragToAdjustController.enabled = enabled; + } + + // Protected + + dragToAdjustControllerActiveStateChanged(dragToAdjustController) + { + if (!dragToAdjustController.active) + this._hoveredTokenInfo = null; + } + + dragToAdjustControllerCanBeActivated(dragToAdjustController) + { + return !this._codeMirror.getOption("readOnly"); + } + + dragToAdjustControllerCanBeAdjusted(dragToAdjustController) + { + + return this._hoveredTokenInfo && this._hoveredTokenInfo.containsNumber; + } + + dragToAdjustControllerWasAdjustedByAmount(dragToAdjustController, amount) + { + this._codeMirror.alterNumberInRange(amount, this._hoveredTokenInfo.startPosition, this._hoveredTokenInfo.endPosition, false); + } + + dragToAdjustControllerDidReset(dragToAdjustController) + { + this._hoveredTokenInfo = null; + } + + dragToAdjustControllerCanAdjustObjectAtPoint(dragToAdjustController, point) + { + var position = this._codeMirror.coordsChar({left: point.x, top: point.y}); + var token = this._codeMirror.getTokenAt(position); + + if (!token || !token.type || !token.string) { + if (this._hoveredTokenInfo) + dragToAdjustController.reset(); + return false; + } + + // Stop right here if we're hovering the same token as we were last time. + if (this._hoveredTokenInfo && this._hoveredTokenInfo.line === position.line && + this._hoveredTokenInfo.token.start === token.start && this._hoveredTokenInfo.token.end === token.end) + return this._hoveredTokenInfo.token.type.indexOf("number") !== -1; + + var containsNumber = token.type.indexOf("number") !== -1; + this._hoveredTokenInfo = { + token, + line: position.line, + containsNumber, + startPosition: { + ch: token.start, + line: position.line + }, + endPosition: { + ch: token.end, + line: position.line + } + }; + + return containsNumber; + } +}; + +CodeMirror.defineOption("dragToAdjustNumbers", true, function(codeMirror, value, oldValue) { + if (!codeMirror.dragToAdjustNumberController) + codeMirror.dragToAdjustNumberController = new WebInspector.CodeMirrorDragToAdjustNumberController(codeMirror); + codeMirror.dragToAdjustNumberController.enabled = value; +}); diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorEditingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorEditingController.js new file mode 100644 index 000000000..3496ac437 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorEditingController.js @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorEditingController = class CodeMirrorEditingController extends WebInspector.Object +{ + constructor(codeMirror, marker) + { + super(); + + this._codeMirror = codeMirror; + this._marker = marker; + this._delegate = null; + + this._range = marker.range; + + // The value must support .toString() and .copy() methods. + this._value = this.initialValue; + + this._keyboardShortcutEsc = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Escape); + } + + // Public + + get marker() + { + return this._marker; + } + + get range() + { + return this._range; + } + + get value() + { + return this._value; + } + + set value(value) + { + this.text = value.toString(); + this._value = value; + } + + get delegate() + { + return this._delegate; + } + + set delegate(delegate) + { + this._delegate = delegate; + } + + get text() + { + var from = {line: this._range.startLine, ch: this._range.startColumn}; + var to = {line: this._range.endLine, ch: this._range.endColumn}; + return this._codeMirror.getRange(from, to); + } + + set text(text) + { + var from = {line: this._range.startLine, ch: this._range.startColumn}; + var to = {line: this._range.endLine, ch: this._range.endColumn}; + this._codeMirror.replaceRange(text, from, to); + + var lines = text.split("\n"); + var endLine = this._range.startLine + lines.length - 1; + var endColumn = lines.length > 1 ? lines.lastValue.length : this._range.startColumn + text.length; + this._range = new WebInspector.TextRange(this._range.startLine, this._range.startColumn, endLine, endColumn); + } + + get initialValue() + { + // Implemented by subclasses. + return this.text; + } + + get cssClassName() + { + // Implemented by subclasses. + return ""; + } + + get popover() + { + return this._popover; + } + + get popoverPreferredEdges() + { + // Best to display the popover to the left or above the edited range since its end position may change, but not its start + // position. This way we minimize the chances of overlaying the edited range as it changes. + return [WebInspector.RectEdge.MIN_X, WebInspector.RectEdge.MIN_Y, WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MAX_X]; + } + + popoverTargetFrameWithRects(rects) + { + return WebInspector.Rect.unionOfRects(rects); + } + + presentHoverMenu() + { + if (!this.cssClassName) + return; + + this._hoverMenu = new WebInspector.HoverMenu(this); + this._hoverMenu.element.classList.add(this.cssClassName); + this._rects = this._marker.rects; + this._hoverMenu.present(this._rects); + } + + dismissHoverMenu(discrete) + { + if (!this._hoverMenu) + return; + + this._hoverMenu.dismiss(discrete); + } + + popoverWillPresent(popover) + { + // Implemented by subclasses. + } + + popoverDidPresent(popover) + { + // Implemented by subclasses. + } + + popoverDidDismiss(popover) + { + // Implemented by subclasses. + } + + // Protected + + handleKeydownEvent(event) + { + if (!this._keyboardShortcutEsc.matchesEvent(event) || !this._popover.visible) + return false; + + this.value = this._originalValue; + this._popover.dismiss(); + + return true; + } + + hoverMenuButtonWasPressed(hoverMenu) + { + this._popover = new WebInspector.Popover(this); + this.popoverWillPresent(this._popover); + this._popover.present(this.popoverTargetFrameWithRects(this._rects).pad(2), this.popoverPreferredEdges); + this.popoverDidPresent(this._popover); + + WebInspector.addWindowKeydownListener(this); + + hoverMenu.dismiss(); + + if (this._delegate && typeof this._delegate.editingControllerDidStartEditing === "function") + this._delegate.editingControllerDidStartEditing(this); + + this._originalValue = this._value.copy(); + } + + didDismissPopover(popover) + { + delete this._popover; + delete this._originalValue; + + WebInspector.removeWindowKeydownListener(this); + this.popoverDidDismiss(); + + if (this._delegate && typeof this._delegate.editingControllerDidFinishEditing === "function") + this._delegate.editingControllerDidFinishEditing(this); + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorGradientEditingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorGradientEditingController.js new file mode 100644 index 000000000..7ff38e0e7 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorGradientEditingController.js @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorGradientEditingController = class CodeMirrorGradientEditingController extends WebInspector.CodeMirrorEditingController +{ + constructor(codeMirror, marker) + { + super(codeMirror, marker); + } + + // Public + + get initialValue() + { + return WebInspector.Gradient.fromString(this.text); + } + + get cssClassName() + { + return "gradient"; + } + + get popoverPreferredEdges() + { + // Since the gradient editor can resize to be quite tall, let's avoid displaying the popover + // above the edited value so that it may not change which edge it attaches to upon editing a stop. + return [WebInspector.RectEdge.MIN_X, WebInspector.RectEdge.MAX_Y, WebInspector.RectEdge.MAX_X]; + } + + popoverTargetFrameWithRects(rects) + { + // If a gradient is defined across several lines, we probably want to use the first line only + // as a target frame for the editor since we may reformat the gradient value to fit on a single line. + return rects[0]; + } + + popoverWillPresent(popover) + { + function handleColorPickerToggled(event) + { + popover.update(); + } + + this._gradientEditor = new WebInspector.GradientEditor; + this._gradientEditor.addEventListener(WebInspector.GradientEditor.Event.GradientChanged, this._gradientEditorGradientChanged, this); + this._gradientEditor.addEventListener(WebInspector.GradientEditor.Event.ColorPickerToggled, handleColorPickerToggled, this); + popover.content = this._gradientEditor.element; + } + + popoverDidPresent(popover) + { + this._gradientEditor.gradient = this.value; + } + + // Private + + _gradientEditorGradientChanged(event) + { + this.value = event.data.gradient; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorSpringEditingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorSpringEditingController.js new file mode 100644 index 000000000..7f1f91f14 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorSpringEditingController.js @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016 Devin Rousso <dcrousso+webkit@gmail.com>. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorSpringEditingController = class CodeMirrorSpringEditingController extends WebInspector.CodeMirrorEditingController +{ + // Public + + get initialValue() + { + return WebInspector.Spring.fromString(this.text); + } + + get cssClassName() + { + return "spring"; + } + + popoverWillPresent(popover) + { + this._springEditor = new WebInspector.SpringEditor; + this._springEditor.addEventListener(WebInspector.SpringEditor.Event.SpringChanged, this._springEditorSpringChanged, this); + popover.content = this._springEditor.element; + } + + popoverDidPresent(popover) + { + this._springEditor.spring = this.value; + } + + popoverDidDismiss(popover) + { + this._springEditor.removeListeners(); + } + + // Private + + _springEditorSpringChanged(event) + { + this.value = event.data.spring; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTextKillController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTextKillController.js new file mode 100644 index 000000000..8cd0b7300 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTextKillController.js @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorTextKillController = class CodeMirrorTextKillController extends WebInspector.Object +{ + constructor(codeMirror) + { + super(); + + console.assert(codeMirror); + + this._codeMirror = codeMirror; + this._expectingChangeEventForKill = false; + this._nextKillStartsNewSequence = true; + this._shouldPrependToKillRing = false; + + this._handleTextChangeListener = this._handleTextChange.bind(this); + this._handleEditorBlurListener = this._handleEditorBlur.bind(this); + this._handleSelectionOrCaretChangeListener = this._handleSelectionOrCaretChange.bind(this); + + // FIXME: these keybindings match CodeMirror's default keymap for OS X. + // They should probably be altered for Windows / Linux someday. + this._codeMirror.addKeyMap({ + // Overrides for the 'emacsy' keymap. + "Ctrl-K": this._handleTextKillCommand.bind(this, "killLine", false), + "Alt-D": this._handleTextKillCommand.bind(this, "delWordAfter", false), + // Overrides for the 'macDefault' keymap. + "Alt-Delete": this._handleTextKillCommand.bind(this, "delGroupAfter", false), + "Cmd-Backspace": this._handleTextKillCommand.bind(this, "delWrappedLineLeft", true), + "Cmd-Delete": this._handleTextKillCommand.bind(this, "delWrappedLineRight", false), + "Alt-Backspace": this._handleTextKillCommand.bind(this, "delGroupBefore", true), + "Ctrl-Alt-Backspace": this._handleTextKillCommand.bind(this, "delGroupAfter", false), + }); + } + + _handleTextKillCommand(command, prependsToKillRing, codeMirror) + { + // Read-only mode is dynamic in some editors, so check every time + // and ignore the shortcut if in read-only mode. + if (this._codeMirror.getOption("readOnly")) + return; + + this._shouldPrependToKillRing = prependsToKillRing; + + // Don't add the listener if it's still registered because + // a previous empty kill didn't generate change events. + if (!this._expectingChangeEventForKill) + this._codeMirror.on("changes", this._handleTextChangeListener); + + this._expectingChangeEventForKill = true; + this._codeMirror.execCommand(command); + } + + _handleTextChange(codeMirror, changes) + { + this._codeMirror.off("changes", this._handleTextChangeListener); + + // Sometimes a second change event fires after removing the listener + // if you perform an "empty kill" and type after moving the caret. + if (!this._expectingChangeEventForKill) + return; + + this._expectingChangeEventForKill = false; + + // It doesn't make sense to get more than one change per kill. + console.assert(changes.length === 1); + let change = changes[0]; + + // If an "empty kill" is followed by up/down or typing, + // the empty kill won't fire a change event, then we'll get an + // unrelated change event that shouldn't be treated as a kill. + if (change.origin !== "+delete") + return; + + // When killed text includes a newline, CodeMirror returns + // strange change objects. Special-case for when this could happen. + let killedText; + if (change.to.line === change.from.line + 1 && change.removed.length === 2) { + // An entire line was deleted, including newline (deleteLine). + if (change.removed[0].length && !change.removed[1].length) + killedText = change.removed[0] + "\n"; + // A newline was killed by itself (Ctrl-K). + else + killedText = "\n"; + } else { + console.assert(change.removed.length === 1); + killedText = change.removed[0]; + } + + InspectorFrontendHost.killText(killedText, this._shouldPrependToKillRing, this._nextKillStartsNewSequence); + + // If the editor loses focus or the caret / selection changes + // (not as a result of the kill), then the next kill should + // start a new kill ring sequence. + this._nextKillStartsNewSequence = false; + this._codeMirror.on("blur", this._handleEditorBlurListener); + this._codeMirror.on("cursorActivity", this._handleSelectionOrCaretChangeListener); + } + + _handleEditorBlur(codeMirror) + { + this._nextKillStartsNewSequence = true; + this._codeMirror.off("blur", this._handleEditorBlurListener); + this._codeMirror.off("cursorActivity", this._handleSelectionOrCaretChangeListener); + } + + _handleSelectionOrCaretChange(codeMirror) + { + if (this._expectingChangeEventForKill) + return; + + this._nextKillStartsNewSequence = true; + this._codeMirror.off("blur", this._handleEditorBlurListener); + this._codeMirror.off("cursorActivity", this._handleSelectionOrCaretChangeListener); + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.css b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.css new file mode 100644 index 000000000..516755d1c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.css @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +.CodeMirror .jump-to-symbol-highlight { + color: blue !important; + text-decoration: underline !important; + cursor: pointer !important; + -webkit-text-stroke-width: 0 !important; +} diff --git a/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js new file mode 100644 index 000000000..4d4fdac99 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js @@ -0,0 +1,618 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.CodeMirrorTokenTrackingController = class CodeMirrorTokenTrackingController extends WebInspector.Object +{ + constructor(codeMirror, delegate) + { + super(); + + console.assert(codeMirror); + + this._codeMirror = codeMirror; + this._delegate = delegate || null; + this._mode = WebInspector.CodeMirrorTokenTrackingController.Mode.None; + + this._mouseOverDelayDuration = 0; + this._mouseOutReleaseDelayDuration = 0; + this._classNameForHighlightedRange = null; + + this._enabled = false; + this._tracking = false; + this._previousTokenInfo = null; + this._hoveredMarker = null; + + const hidePopover = this._hidePopover.bind(this); + + this._codeMirror.addKeyMap({ + "Cmd-Enter": this._handleCommandEnterKey.bind(this), + "Esc": hidePopover + }); + + this._codeMirror.on("cursorActivity", hidePopover); + } + + // Public + + get delegate() + { + return this._delegate; + } + + set delegate(x) + { + this._delegate = x; + } + + get enabled() + { + return this._enabled; + } + + set enabled(enabled) + { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + + var wrapper = this._codeMirror.getWrapperElement(); + if (enabled) { + wrapper.addEventListener("mouseenter", this); + wrapper.addEventListener("mouseleave", this); + this._updateHoveredTokenInfo({left: WebInspector.mouseCoords.x, top: WebInspector.mouseCoords.y}); + this._startTracking(); + } else { + wrapper.removeEventListener("mouseenter", this); + wrapper.removeEventListener("mouseleave", this); + this._stopTracking(); + } + } + + get mode() + { + return this._mode; + } + + set mode(mode) + { + var oldMode = this._mode; + + this._mode = mode || WebInspector.CodeMirrorTokenTrackingController.Mode.None; + + if (oldMode !== this._mode && this._tracking && this._previousTokenInfo) + this._processNewHoveredToken(this._previousTokenInfo); + } + + get mouseOverDelayDuration() + { + return this._mouseOverDelayDuration; + } + + set mouseOverDelayDuration(x) + { + console.assert(x >= 0); + this._mouseOverDelayDuration = Math.max(x, 0); + } + + get mouseOutReleaseDelayDuration() + { + return this._mouseOutReleaseDelayDuration; + } + + set mouseOutReleaseDelayDuration(x) + { + console.assert(x >= 0); + this._mouseOutReleaseDelayDuration = Math.max(x, 0); + } + + get classNameForHighlightedRange() + { + return this._classNameForHighlightedRange; + } + + set classNameForHighlightedRange(x) + { + this._classNameForHighlightedRange = x || null; + } + + get candidate() + { + return this._candidate; + } + + get hoveredMarker() + { + return this._hoveredMarker; + } + + set hoveredMarker(hoveredMarker) + { + this._hoveredMarker = hoveredMarker; + } + + highlightLastHoveredRange() + { + if (this._candidate) + this.highlightRange(this._candidate.hoveredTokenRange); + } + + highlightRange(range) + { + // Nothing to do if we're trying to highlight the same range. + if (this._codeMirrorMarkedText && this._codeMirrorMarkedText.className === this._classNameForHighlightedRange) { + var highlightedRange = this._codeMirrorMarkedText.find(); + if (!highlightedRange) + return; + if (WebInspector.compareCodeMirrorPositions(highlightedRange.from, range.start) === 0 && + WebInspector.compareCodeMirrorPositions(highlightedRange.to, range.end) === 0) + return; + } + + this.removeHighlightedRange(); + + var className = this._classNameForHighlightedRange || ""; + this._codeMirrorMarkedText = this._codeMirror.markText(range.start, range.end, {className}); + + window.addEventListener("mousemove", this, true); + } + + removeHighlightedRange() + { + if (!this._codeMirrorMarkedText) + return; + + this._codeMirrorMarkedText.clear(); + this._codeMirrorMarkedText = null; + + window.removeEventListener("mousemove", this, true); + } + + // Private + + _startTracking() + { + if (this._tracking) + return; + + this._tracking = true; + + var wrapper = this._codeMirror.getWrapperElement(); + wrapper.addEventListener("mousemove", this, true); + wrapper.addEventListener("mouseout", this, false); + wrapper.addEventListener("mousedown", this, false); + wrapper.addEventListener("mouseup", this, false); + window.addEventListener("blur", this, true); + } + + _stopTracking() + { + if (!this._tracking) + return; + + this._tracking = false; + this._candidate = null; + + var wrapper = this._codeMirror.getWrapperElement(); + wrapper.removeEventListener("mousemove", this, true); + wrapper.removeEventListener("mouseout", this, false); + wrapper.removeEventListener("mousedown", this, false); + wrapper.removeEventListener("mouseup", this, false); + window.removeEventListener("blur", this, true); + window.removeEventListener("mousemove", this, true); + + this._resetTrackingStates(); + } + + handleEvent(event) + { + switch (event.type) { + case "mouseenter": + this._mouseEntered(event); + break; + case "mouseleave": + this._mouseLeft(event); + break; + case "mousemove": + if (event.currentTarget === window) + this._mouseMovedWithMarkedText(event); + else + this._mouseMovedOverEditor(event); + break; + case "mouseout": + // Only deal with a mouseout event that has the editor wrapper as the target. + if (!event.currentTarget.contains(event.relatedTarget)) + this._mouseMovedOutOfEditor(event); + break; + case "mousedown": + this._mouseButtonWasPressedOverEditor(event); + break; + case "mouseup": + this._mouseButtonWasReleasedOverEditor(event); + break; + case "blur": + this._windowLostFocus(event); + break; + } + } + + _handleCommandEnterKey(codeMirror) + { + const tokenInfo = this._getTokenInfoForPosition(codeMirror.getCursor("head")); + tokenInfo.triggeredBy = WebInspector.CodeMirrorTokenTrackingController.TriggeredBy.Keyboard; + this._processNewHoveredToken(tokenInfo); + } + + _hidePopover() + { + if (!this._candidate) + return CodeMirror.Pass; + + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function") { + const forceHidePopover = true; + this._delegate.tokenTrackingControllerHighlightedRangeReleased(this, forceHidePopover); + } + } + + _mouseEntered(event) + { + if (!this._tracking) + this._startTracking(); + } + + _mouseLeft(event) + { + this._stopTracking(); + } + + _mouseMovedWithMarkedText(event) + { + if (this._candidate && this._candidate.triggeredBy === WebInspector.CodeMirrorTokenTrackingController.TriggeredBy.Keyboard) + return; + + var shouldRelease = !event.target.classList.contains(this._classNameForHighlightedRange); + if (shouldRelease && this._delegate && typeof this._delegate.tokenTrackingControllerCanReleaseHighlightedRange === "function") + shouldRelease = this._delegate.tokenTrackingControllerCanReleaseHighlightedRange(this, event.target); + + if (shouldRelease) { + if (!this._markedTextMouseoutTimer) + this._markedTextMouseoutTimer = setTimeout(this._markedTextIsNoLongerHovered.bind(this), this._mouseOutReleaseDelayDuration); + return; + } + + if (this._markedTextMouseoutTimer) + clearTimeout(this._markedTextMouseoutTimer); + + this._markedTextMouseoutTimer = 0; + } + + _markedTextIsNoLongerHovered() + { + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function") + this._delegate.tokenTrackingControllerHighlightedRangeReleased(this); + + this._markedTextMouseoutTimer = 0; + } + + _mouseMovedOverEditor(event) + { + this._updateHoveredTokenInfo({left: event.pageX, top: event.pageY}); + } + + _updateHoveredTokenInfo(mouseCoords) + { + // Get the position in the text and the token at that position. + var position = this._codeMirror.coordsChar(mouseCoords); + var token = this._codeMirror.getTokenAt(position); + + if (!token || !token.type || !token.string) { + if (this._hoveredMarker && this._delegate && typeof this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker === "function") { + if (!this._codeMirror.findMarksAt(position).includes(this._hoveredMarker.codeMirrorTextMarker)) + this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker(this, this._hoveredMarker); + } + + this._resetTrackingStates(); + return; + } + + // Stop right here if we're hovering the same token as we were last time. + if (this._previousTokenInfo && + this._previousTokenInfo.position.line === position.line && + this._previousTokenInfo.token.start === token.start && + this._previousTokenInfo.token.end === token.end) + return; + + // We have a new hovered token. + var tokenInfo = this._previousTokenInfo = this._getTokenInfoForPosition(position); + + if (/\bmeta\b/.test(token.type)) { + let nextTokenPosition = Object.shallowCopy(position); + nextTokenPosition.ch = tokenInfo.token.end + 1; + + let nextToken = this._codeMirror.getTokenAt(nextTokenPosition); + if (nextToken && nextToken.type && !/\bmeta\b/.test(nextToken.type)) { + console.assert(tokenInfo.token.end === nextToken.start); + + tokenInfo.token.type = nextToken.type; + tokenInfo.token.string = tokenInfo.token.string + nextToken.string; + tokenInfo.token.end = nextToken.end; + } + } else { + let previousTokenPosition = Object.shallowCopy(position); + previousTokenPosition.ch = tokenInfo.token.start - 1; + + let previousToken = this._codeMirror.getTokenAt(previousTokenPosition); + if (previousToken && previousToken.type && /\bmeta\b/.test(previousToken.type)) { + console.assert(tokenInfo.token.start === previousToken.end); + + tokenInfo.token.string = previousToken.string + tokenInfo.token.string; + tokenInfo.token.start = previousToken.start; + } + } + + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + + if (this._codeMirrorMarkedText || !this._mouseOverDelayDuration) + this._processNewHoveredToken(tokenInfo); + else + this._tokenHoverTimer = setTimeout(this._processNewHoveredToken.bind(this, tokenInfo), this._mouseOverDelayDuration); + } + + _getTokenInfoForPosition(position) + { + var token = this._codeMirror.getTokenAt(position); + var innerMode = CodeMirror.innerMode(this._codeMirror.getMode(), token.state); + var codeMirrorModeName = innerMode.mode.alternateName || innerMode.mode.name; + return { + token, + position, + innerMode, + modeName: codeMirrorModeName + }; + } + + _mouseMovedOutOfEditor(event) + { + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + this._previousTokenInfo = null; + this._selectionMayBeInProgress = false; + } + + _mouseButtonWasPressedOverEditor(event) + { + this._selectionMayBeInProgress = true; + } + + _mouseButtonWasReleasedOverEditor(event) + { + this._selectionMayBeInProgress = false; + this._mouseMovedOverEditor(event); + + if (this._codeMirrorMarkedText && this._previousTokenInfo) { + var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY}); + var marks = this._codeMirror.findMarksAt(position); + for (var i = 0; i < marks.length; ++i) { + if (marks[i] === this._codeMirrorMarkedText) { + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeWasClicked === "function") + this._delegate.tokenTrackingControllerHighlightedRangeWasClicked(this); + + break; + } + } + } + } + + _windowLostFocus(event) + { + this._resetTrackingStates(); + } + + _processNewHoveredToken(tokenInfo) + { + console.assert(tokenInfo); + + if (this._selectionMayBeInProgress) + return; + + this._candidate = null; + + switch (this._mode) { + case WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens: + this._candidate = this._processNonSymbolToken(tokenInfo); + break; + case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression: + case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation: + this._candidate = this._processJavaScriptExpression(tokenInfo); + break; + case WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens: + this._candidate = this._processMarkedToken(tokenInfo); + break; + } + + if (!this._candidate) + return; + + this._candidate.triggeredBy = tokenInfo.triggeredBy; + + if (this._markedTextMouseoutTimer) + clearTimeout(this._markedTextMouseoutTimer); + + this._markedTextMouseoutTimer = 0; + + if (this._delegate && typeof this._delegate.tokenTrackingControllerNewHighlightCandidate === "function") + this._delegate.tokenTrackingControllerNewHighlightCandidate(this, this._candidate); + } + + _processNonSymbolToken(tokenInfo) + { + // Ignore any symbol tokens. + var type = tokenInfo.token.type; + if (!type) + return null; + + var startPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + var endPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: {start: startPosition, end: endPosition}, + }; + } + + _processJavaScriptExpression(tokenInfo) + { + // Only valid within JavaScript. + if (tokenInfo.modeName !== "javascript") + return null; + + var startPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + var endPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + + function tokenIsInRange(token, range) + { + return token.line >= range.start.line && token.ch >= range.start.ch && + token.line <= range.end.line && token.ch <= range.end.ch; + } + + // If the hovered token is within a selection, use the selection as our expression. + if (this._codeMirror.somethingSelected()) { + var selectionRange = { + start: this._codeMirror.getCursor("start"), + end: this._codeMirror.getCursor("end") + }; + + if (tokenIsInRange(startPosition, selectionRange) || tokenIsInRange(endPosition, selectionRange)) { + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: selectionRange, + expression: this._codeMirror.getSelection(), + expressionRange: selectionRange, + }; + } + } + + // We only handle vars, definitions, properties, and the keyword 'this'. + var type = tokenInfo.token.type; + var isProperty = type.indexOf("property") !== -1; + var isKeyword = type.indexOf("keyword") !== -1; + if (!isProperty && !isKeyword && type.indexOf("variable") === -1 && type.indexOf("def") === -1) + return null; + + // Not object literal property names, but yes if an object literal shorthand property, which is a variable. + let state = tokenInfo.innerMode.state; + if (isProperty && state.lexical && state.lexical.type === "}") { + // Peek ahead to see if the next token is "}" or ",". If it is, we are a shorthand and therefore a variable. + let shorthand = false; + let mode = tokenInfo.innerMode.mode; + let position = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + WebInspector.walkTokens(this._codeMirror, mode, position, function(tokenType, string) { + if (tokenType) + return false; + if (string === "(") + return false; + if (string === "," || string === "}") { + shorthand = true; + return false; + } + return true; + }); + + if (!shorthand) + return null; + } + + // Only the "this" keyword. + if (isKeyword && tokenInfo.token.string !== "this") + return null; + + // Work out the full hovered expression. + var expression = tokenInfo.token.string; + var expressionStartPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + while (true) { + var token = this._codeMirror.getTokenAt(expressionStartPosition); + if (!token) + break; + + var isDot = !token.type && token.string === "."; + var isExpression = token.type && token.type.includes("m-javascript"); + if (!isDot && !isExpression) + break; + + // Disallow operators. We want the hovered expression to be just a single operand. + // Also, some operators can modify values, such as pre-increment and assignment operators. + if (isExpression && token.type.includes("operator")) + break; + + expression = token.string + expression; + expressionStartPosition.ch = token.start; + } + + // Return the candidate for this token and expression. + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: {start: startPosition, end: endPosition}, + expression, + expressionRange: {start: expressionStartPosition, end: endPosition}, + }; + } + + _processMarkedToken(tokenInfo) + { + return this._processNonSymbolToken(tokenInfo); + } + + _resetTrackingStates() + { + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + + this._selectionMayBeInProgress = false; + this._previousTokenInfo = null; + this.removeHighlightedRange(); + } +}; + +WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName = "jump-to-symbol-highlight"; + +WebInspector.CodeMirrorTokenTrackingController.Mode = { + None: "none", + NonSymbolTokens: "non-symbol-tokens", + JavaScriptExpression: "javascript-expression", + JavaScriptTypeInformation: "javascript-type-information", + MarkedTokens: "marked-tokens" +}; + +WebInspector.CodeMirrorTokenTrackingController.TriggeredBy = { + Keyboard: "keyboard", + Hover: "hover" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DOMTreeManager.js b/Source/WebInspectorUI/UserInterface/Controllers/DOMTreeManager.js new file mode 100644 index 000000000..c7c4003de --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/DOMTreeManager.js @@ -0,0 +1,795 @@ +/* + * Copyright (C) 2009, 2010 Google Inc. All rights reserved. + * Copyright (C) 2009 Joseph Pecoraro + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.DOMTreeManager = class DOMTreeManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._idToDOMNode = {}; + this._document = null; + this._attributeLoadNodeIds = {}; + this._flows = new Map; + this._contentNodesToFlowsMap = new Map; + this._restoreSelectedNodeIsAllowed = true; + this._loadNodeAttributesTimeout = 0; + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + } + + // Static + + static _flowPayloadHashKey(flowPayload) + { + // Use the flow node id, to avoid collisions when we change main document id. + return flowPayload.documentNodeId + ":" + flowPayload.name; + } + + // Public + + requestDocument(callback) + { + if (this._document) { + callback(this._document); + return; + } + + if (this._pendingDocumentRequestCallbacks) { + this._pendingDocumentRequestCallbacks.push(callback); + return; + } + + this._pendingDocumentRequestCallbacks = [callback]; + + function onDocumentAvailable(error, root) + { + if (!error) + this._setDocument(root); + + for (let callback of this._pendingDocumentRequestCallbacks) + callback(this._document); + + this._pendingDocumentRequestCallbacks = null; + } + + DOMAgent.getDocument(onDocumentAvailable.bind(this)); + } + + pushNodeToFrontend(objectId, callback) + { + this._dispatchWhenDocumentAvailable(DOMAgent.requestNode.bind(DOMAgent, objectId), callback); + } + + pushNodeByPathToFrontend(path, callback) + { + this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent, path), callback); + } + + // Private + + _wrapClientCallback(callback) + { + if (!callback) + return null; + + return function(error, result) { + if (error) + console.error("Error during DOMAgent operation: " + error); + callback(error ? null : result); + }; + } + + _dispatchWhenDocumentAvailable(func, callback) + { + var callbackWrapper = this._wrapClientCallback(callback); + + function onDocumentAvailable() + { + if (this._document) + func(callbackWrapper); + else { + if (callbackWrapper) + callbackWrapper("No document"); + } + } + this.requestDocument(onDocumentAvailable.bind(this)); + } + + _attributeModified(nodeId, name, value) + { + var node = this._idToDOMNode[nodeId]; + if (!node) + return; + + node._setAttribute(name, value); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.AttributeModified, {node, name}); + node.dispatchEventToListeners(WebInspector.DOMNode.Event.AttributeModified, {name}); + } + + _attributeRemoved(nodeId, name) + { + var node = this._idToDOMNode[nodeId]; + if (!node) + return; + + node._removeAttribute(name); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.AttributeRemoved, {node, name}); + node.dispatchEventToListeners(WebInspector.DOMNode.Event.AttributeRemoved, {name}); + } + + _inlineStyleInvalidated(nodeIds) + { + for (var nodeId of nodeIds) + this._attributeLoadNodeIds[nodeId] = true; + if (this._loadNodeAttributesTimeout) + return; + this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0); + } + + _loadNodeAttributes() + { + function callback(nodeId, error, attributes) + { + if (error) { + console.error("Error during DOMAgent operation: " + error); + return; + } + var node = this._idToDOMNode[nodeId]; + if (node) { + node._setAttributesPayload(attributes); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.AttributeModified, {node, name: "style"}); + node.dispatchEventToListeners(WebInspector.DOMNode.Event.AttributeModified, {name: "style"}); + } + } + + this._loadNodeAttributesTimeout = 0; + + for (var nodeId in this._attributeLoadNodeIds) { + var nodeIdAsNumber = parseInt(nodeId); + DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber)); + } + this._attributeLoadNodeIds = {}; + } + + _characterDataModified(nodeId, newValue) + { + var node = this._idToDOMNode[nodeId]; + node._nodeValue = newValue; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.CharacterDataModified, {node}); + } + + nodeForId(nodeId) + { + return this._idToDOMNode[nodeId]; + } + + _documentUpdated() + { + this._setDocument(null); + } + + _setDocument(payload) + { + this._idToDOMNode = {}; + if (payload && "nodeId" in payload) + this._document = new WebInspector.DOMNode(this, null, false, payload); + else + this._document = null; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.DocumentUpdated, this._document); + } + + _setDetachedRoot(payload) + { + new WebInspector.DOMNode(this, null, false, payload); + } + + _setChildNodes(parentId, payloads) + { + if (!parentId && payloads.length) { + this._setDetachedRoot(payloads[0]); + return; + } + + var parent = this._idToDOMNode[parentId]; + parent._setChildrenPayload(payloads); + } + + _childNodeCountUpdated(nodeId, newValue) + { + var node = this._idToDOMNode[nodeId]; + node.childNodeCount = newValue; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.ChildNodeCountUpdated, node); + } + + _childNodeInserted(parentId, prevId, payload) + { + var parent = this._idToDOMNode[parentId]; + var prev = this._idToDOMNode[prevId]; + var node = parent._insertChild(prev, payload); + this._idToDOMNode[node.id] = node; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.NodeInserted, {node, parent}); + } + + _childNodeRemoved(parentId, nodeId) + { + var parent = this._idToDOMNode[parentId]; + var node = this._idToDOMNode[nodeId]; + parent._removeChild(node); + this._unbind(node); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.NodeRemoved, {node, parent}); + } + + _customElementStateChanged(elementId, newState) + { + const node = this._idToDOMNode[elementId]; + node._customElementState = newState; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.CustomElementStateChanged, {node}); + } + + _pseudoElementAdded(parentId, pseudoElement) + { + var parent = this._idToDOMNode[parentId]; + if (!parent) + return; + + var node = new WebInspector.DOMNode(this, parent.ownerDocument, false, pseudoElement); + node.parentNode = parent; + this._idToDOMNode[node.id] = node; + console.assert(!parent.pseudoElements().get(node.pseudoType())); + parent.pseudoElements().set(node.pseudoType(), node); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.NodeInserted, {node, parent}); + } + + _pseudoElementRemoved(parentId, pseudoElementId) + { + var pseudoElement = this._idToDOMNode[pseudoElementId]; + if (!pseudoElement) + return; + + var parent = pseudoElement.parentNode; + console.assert(parent); + console.assert(parent.id === parentId); + if (!parent) + return; + + parent._removeChild(pseudoElement); + this._unbind(pseudoElement); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.NodeRemoved, {node: pseudoElement, parent}); + } + + _unbind(node) + { + this._removeContentNodeFromFlowIfNeeded(node); + + delete this._idToDOMNode[node.id]; + + for (let i = 0; node.children && i < node.children.length; ++i) + this._unbind(node.children[i]); + + let templateContent = node.templateContent(); + if (templateContent) + this._unbind(templateContent); + + for (let pseudoElement of node.pseudoElements().values()) + this._unbind(pseudoElement); + + // FIXME: Handle shadow roots. + } + + get restoreSelectedNodeIsAllowed() + { + return this._restoreSelectedNodeIsAllowed; + } + + inspectElement(nodeId) + { + var node = this._idToDOMNode[nodeId]; + if (!node || !node.ownerDocument) + return; + + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.DOMNodeWasInspected, {node}); + + this._inspectModeEnabled = false; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.InspectModeStateChanged); + } + + inspectNodeObject(remoteObject) + { + this._restoreSelectedNodeIsAllowed = false; + + function nodeAvailable(nodeId) + { + remoteObject.release(); + + console.assert(nodeId); + if (!nodeId) + return; + + this.inspectElement(nodeId); + + // Re-resolve the node in the console's object group when adding to the console. + let domNode = this.nodeForId(nodeId); + WebInspector.RemoteObject.resolveNode(domNode, WebInspector.RuntimeManager.ConsoleObjectGroup, function(remoteObject) { + if (!remoteObject) + return; + let specialLogStyles = true; + let shouldRevealConsole = false; + WebInspector.consoleLogViewController.appendImmediateExecutionWithResult(WebInspector.UIString("Selected Element"), remoteObject, specialLogStyles, shouldRevealConsole); + }); + } + + remoteObject.pushNodeToFrontend(nodeAvailable.bind(this)); + } + + performSearch(query, searchCallback) + { + this.cancelSearch(); + + function callback(error, searchId, resultsCount) + { + this._searchId = searchId; + searchCallback(resultsCount); + } + DOMAgent.performSearch(query, callback.bind(this)); + } + + searchResult(index, callback) + { + function mycallback(error, nodeIds) + { + if (error) { + console.error(error); + callback(null); + return; + } + if (nodeIds.length !== 1) + return; + + callback(this._idToDOMNode[nodeIds[0]]); + } + + if (this._searchId) + DOMAgent.getSearchResults(this._searchId, index, index + 1, mycallback.bind(this)); + else + callback(null); + } + + cancelSearch() + { + if (this._searchId) { + DOMAgent.discardSearchResults(this._searchId); + this._searchId = undefined; + } + } + + querySelector(nodeId, selectors, callback) + { + DOMAgent.querySelector(nodeId, selectors, this._wrapClientCallback(callback)); + } + + querySelectorAll(nodeId, selectors, callback) + { + DOMAgent.querySelectorAll(nodeId, selectors, this._wrapClientCallback(callback)); + } + + highlightDOMNode(nodeId, mode) + { + if (this._hideDOMNodeHighlightTimeout) { + clearTimeout(this._hideDOMNodeHighlightTimeout); + this._hideDOMNodeHighlightTimeout = undefined; + } + + this._highlightedDOMNodeId = nodeId; + if (nodeId) + DOMAgent.highlightNode.invoke({nodeId, highlightConfig: this._buildHighlightConfig(mode)}); + else + DOMAgent.hideHighlight(); + } + + highlightSelector(selectorText, frameId, mode) + { + // COMPATIBILITY (iOS 8): DOM.highlightSelector did not exist. + if (!DOMAgent.highlightSelector) + return; + + DOMAgent.highlightSelector(this._buildHighlightConfig(mode), selectorText, frameId); + } + + highlightRect(rect, usePageCoordinates) + { + DOMAgent.highlightRect.invoke({ + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + color: {r: 111, g: 168, b: 220, a: 0.66}, + outlineColor: {r: 255, g: 229, b: 153, a: 0.66}, + usePageCoordinates + }); + } + + hideDOMNodeHighlight() + { + this.highlightDOMNode(0); + } + + highlightDOMNodeForTwoSeconds(nodeId) + { + this.highlightDOMNode(nodeId); + this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000); + } + + get inspectModeEnabled() + { + return this._inspectModeEnabled; + } + + set inspectModeEnabled(enabled) + { + if (enabled === this._inspectModeEnabled) + return; + + DOMAgent.setInspectModeEnabled(enabled, this._buildHighlightConfig(), (error) => { + this._inspectModeEnabled = error ? false : enabled; + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.InspectModeStateChanged); + }); + } + + _buildHighlightConfig(mode = "all") + { + let highlightConfig = {showInfo: mode === "all"}; + + if (mode === "all" || mode === "content") + highlightConfig.contentColor = {r: 111, g: 168, b: 220, a: 0.66}; + + if (mode === "all" || mode === "padding") + highlightConfig.paddingColor = {r: 147, g: 196, b: 125, a: 0.66}; + + if (mode === "all" || mode === "border") + highlightConfig.borderColor = {r: 255, g: 229, b: 153, a: 0.66}; + + if (mode === "all" || mode === "margin") + highlightConfig.marginColor = {r: 246, g: 178, b: 107, a: 0.66}; + + return highlightConfig; + } + + _createContentFlowFromPayload(flowPayload) + { + // FIXME: Collect the regions from the payload. + var flow = new WebInspector.ContentFlow(flowPayload.documentNodeId, flowPayload.name, flowPayload.overset, flowPayload.content.map(this.nodeForId.bind(this))); + + for (var contentNode of flow.contentNodes) { + console.assert(!this._contentNodesToFlowsMap.has(contentNode.id)); + this._contentNodesToFlowsMap.set(contentNode.id, flow); + } + + return flow; + } + + _updateContentFlowFromPayload(contentFlow, flowPayload) + { + console.assert(contentFlow.contentNodes.length === flowPayload.content.length); + console.assert(contentFlow.contentNodes.every((node, i) => node.id === flowPayload.content[i])); + + // FIXME: Collect the regions from the payload. + contentFlow.overset = flowPayload.overset; + } + + getNamedFlowCollection(documentNodeIdentifier) + { + function onNamedFlowCollectionAvailable(error, flows) + { + if (error) + return; + this._contentNodesToFlowsMap.clear(); + var contentFlows = []; + for (var i = 0; i < flows.length; ++i) { + var flowPayload = flows[i]; + var flowKey = WebInspector.DOMTreeManager._flowPayloadHashKey(flowPayload); + var contentFlow = this._flows.get(flowKey); + if (contentFlow) + this._updateContentFlowFromPayload(contentFlow, flowPayload); + else { + contentFlow = this._createContentFlowFromPayload(flowPayload); + this._flows.set(flowKey, contentFlow); + } + contentFlows.push(contentFlow); + } + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.ContentFlowListWasUpdated, {documentNodeIdentifier, flows: contentFlows}); + } + + if (window.CSSAgent) + CSSAgent.getNamedFlowCollection(documentNodeIdentifier, onNamedFlowCollectionAvailable.bind(this)); + } + + namedFlowCreated(flowPayload) + { + var flowKey = WebInspector.DOMTreeManager._flowPayloadHashKey(flowPayload); + console.assert(!this._flows.has(flowKey)); + var contentFlow = this._createContentFlowFromPayload(flowPayload); + this._flows.set(flowKey, contentFlow); + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.ContentFlowWasAdded, {flow: contentFlow}); + } + + namedFlowRemoved(documentNodeIdentifier, flowName) + { + var flowKey = WebInspector.DOMTreeManager._flowPayloadHashKey({documentNodeId: documentNodeIdentifier, name: flowName}); + var contentFlow = this._flows.get(flowKey); + console.assert(contentFlow); + this._flows.delete(flowKey); + + // Remove any back links to this flow from the content nodes. + for (var contentNode of contentFlow.contentNodes) + this._contentNodesToFlowsMap.delete(contentNode.id); + + this.dispatchEventToListeners(WebInspector.DOMTreeManager.Event.ContentFlowWasRemoved, {flow: contentFlow}); + } + + _sendNamedFlowUpdateEvents(flowPayload) + { + var flowKey = WebInspector.DOMTreeManager._flowPayloadHashKey(flowPayload); + console.assert(this._flows.has(flowKey)); + this._updateContentFlowFromPayload(this._flows.get(flowKey), flowPayload); + } + + regionOversetChanged(flowPayload) + { + this._sendNamedFlowUpdateEvents(flowPayload); + } + + registeredNamedFlowContentElement(documentNodeIdentifier, flowName, contentNodeId, nextContentElementNodeId) + { + var flowKey = WebInspector.DOMTreeManager._flowPayloadHashKey({documentNodeId: documentNodeIdentifier, name: flowName}); + console.assert(this._flows.has(flowKey)); + console.assert(!this._contentNodesToFlowsMap.has(contentNodeId)); + + var flow = this._flows.get(flowKey); + var contentNode = this.nodeForId(contentNodeId); + + this._contentNodesToFlowsMap.set(contentNode.id, flow); + + if (nextContentElementNodeId) + flow.insertContentNodeBefore(contentNode, this.nodeForId(nextContentElementNodeId)); + else + flow.appendContentNode(contentNode); + } + + _removeContentNodeFromFlowIfNeeded(node) + { + if (!this._contentNodesToFlowsMap.has(node.id)) + return; + var flow = this._contentNodesToFlowsMap.get(node.id); + this._contentNodesToFlowsMap.delete(node.id); + flow.removeContentNode(node); + } + + unregisteredNamedFlowContentElement(documentNodeIdentifier, flowName, contentNodeId) + { + console.assert(this._contentNodesToFlowsMap.has(contentNodeId)); + + var flow = this._contentNodesToFlowsMap.get(contentNodeId); + console.assert(flow.id === WebInspector.DOMTreeManager._flowPayloadHashKey({documentNodeId: documentNodeIdentifier, name: flowName})); + + this._contentNodesToFlowsMap.delete(contentNodeId); + flow.removeContentNode(this.nodeForId(contentNodeId)); + } + + _coerceRemoteArrayOfDOMNodes(remoteObject, callback) + { + console.assert(remoteObject.type === "object"); + console.assert(remoteObject.subtype === "array"); + + let length = remoteObject.size; + if (!length) { + callback(null, []); + return; + } + + let nodes; + let received = 0; + let lastError = null; + let domTreeManager = this; + + function nodeRequested(index, error, nodeId) + { + if (error) + lastError = error; + else + nodes[index] = domTreeManager._idToDOMNode[nodeId]; + if (++received === length) + callback(lastError, nodes); + } + + WebInspector.runtimeManager.getPropertiesForRemoteObject(remoteObject.objectId, function(error, properties) { + if (error) { + callback(error); + return; + } + + nodes = new Array(length); + for (let i = 0; i < length; ++i) { + let nodeProperty = properties.get(String(i)); + console.assert(nodeProperty.value.type === "object"); + console.assert(nodeProperty.value.subtype === "node"); + DOMAgent.requestNode(nodeProperty.value.objectId, nodeRequested.bind(null, i)); + } + }); + } + + getNodeContentFlowInfo(domNode, resultReadyCallback) + { + DOMAgent.resolveNode(domNode.id, domNodeResolved.bind(this)); + + function domNodeResolved(error, remoteObject) + { + if (error) { + resultReadyCallback(error); + return; + } + + var evalParameters = { + objectId: remoteObject.objectId, + functionDeclaration: appendWebInspectorSourceURL(inspectedPage_node_getFlowInfo.toString()), + doNotPauseOnExceptionsAndMuteConsole: true, + returnByValue: false, + generatePreview: false + }; + RuntimeAgent.callFunctionOn.invoke(evalParameters, regionNodesAvailable.bind(this)); + } + + function regionNodesAvailable(error, remoteObject, wasThrown) + { + if (error) { + resultReadyCallback(error); + return; + } + + if (wasThrown) { + // We should never get here, but having the error is useful for debugging. + console.error("Error while executing backend function:", JSON.stringify(remoteObject)); + resultReadyCallback(null); + return; + } + + // The backend function can never return null. + console.assert(remoteObject.type === "object"); + console.assert(remoteObject.objectId); + WebInspector.runtimeManager.getPropertiesForRemoteObject(remoteObject.objectId, remoteObjectPropertiesAvailable.bind(this)); + } + + function remoteObjectPropertiesAvailable(error, properties) { + if (error) { + resultReadyCallback(error); + return; + } + + var result = { + regionFlow: null, + contentFlow: null, + regions: null + }; + + var regionFlowNameProperty = properties.get("regionFlowName"); + if (regionFlowNameProperty && regionFlowNameProperty.value && regionFlowNameProperty.value.value) { + console.assert(regionFlowNameProperty.value.type === "string"); + var regionFlowKey = WebInspector.DOMTreeManager._flowPayloadHashKey({documentNodeId: domNode.ownerDocument.id, name: regionFlowNameProperty.value.value}); + result.regionFlow = this._flows.get(regionFlowKey); + } + + var contentFlowNameProperty = properties.get("contentFlowName"); + if (contentFlowNameProperty && contentFlowNameProperty.value && contentFlowNameProperty.value.value) { + console.assert(contentFlowNameProperty.value.type === "string"); + var contentFlowKey = WebInspector.DOMTreeManager._flowPayloadHashKey({documentNodeId: domNode.ownerDocument.id, name: contentFlowNameProperty.value.value}); + result.contentFlow = this._flows.get(contentFlowKey); + } + + var regionsProperty = properties.get("regions"); + if (!regionsProperty || !regionsProperty.value.objectId) { + // The list of regions is null. + resultReadyCallback(null, result); + return; + } + + this._coerceRemoteArrayOfDOMNodes(regionsProperty.value, function(error, nodes) { + result.regions = nodes; + resultReadyCallback(error, result); + }); + } + + function inspectedPage_node_getFlowInfo() + { + function getComputedProperty(node, propertyName) + { + if (!node.ownerDocument || !node.ownerDocument.defaultView) + return null; + var computedStyle = node.ownerDocument.defaultView.getComputedStyle(node); + return computedStyle ? computedStyle[propertyName] : null; + } + + function getContentFlowName(node) + { + for (; node; node = node.parentNode) { + var flowName = getComputedProperty(node, "webkitFlowInto"); + if (flowName && flowName !== "none") + return flowName; + } + return null; + } + + var node = this; + + // Even detached nodes have an ownerDocument. + console.assert(node.ownerDocument); + + var result = { + regionFlowName: getComputedProperty(node, "webkitFlowFrom"), + contentFlowName: getContentFlowName(node), + regions: null + }; + + if (result.contentFlowName) { + var flowThread = node.ownerDocument.webkitGetNamedFlows().namedItem(result.contentFlowName); + if (flowThread) + result.regions = Array.from(flowThread.getRegionsByContent(node)); + } + + return result; + } + } + + // Private + + _mainResourceDidChange(event) + { + if (event.target.isMainFrame()) + this._restoreSelectedNodeIsAllowed = true; + } +}; + +WebInspector.DOMTreeManager.Event = { + AttributeModified: "dom-tree-manager-attribute-modified", + AttributeRemoved: "dom-tree-manager-attribute-removed", + CharacterDataModified: "dom-tree-manager-character-data-modified", + NodeInserted: "dom-tree-manager-node-inserted", + NodeRemoved: "dom-tree-manager-node-removed", + CustomElementStateChanged: "dom-tree-manager-custom-element-state-changed", + DocumentUpdated: "dom-tree-manager-document-updated", + ChildNodeCountUpdated: "dom-tree-manager-child-node-count-updated", + DOMNodeWasInspected: "dom-tree-manager-dom-node-was-inspected", + InspectModeStateChanged: "dom-tree-manager-inspect-mode-state-changed", + ContentFlowListWasUpdated: "dom-tree-manager-content-flow-list-was-updated", + ContentFlowWasAdded: "dom-tree-manager-content-flow-was-added", + ContentFlowWasRemoved: "dom-tree-manager-content-flow-was-removed", + RegionOversetChanged: "dom-tree-manager-region-overset-changed" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DashboardManager.js b/Source/WebInspectorUI/UserInterface/Controllers/DashboardManager.js new file mode 100644 index 000000000..48c4a8fd5 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/DashboardManager.js @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013, 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.DashboardManager = class DashboardManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._dashboards = {}; + this._dashboards.default = new WebInspector.DefaultDashboard; + this._dashboards.debugger = new WebInspector.DebuggerDashboard; + this._dashboards.replay = new WebInspector.ReplayDashboard; + } + + get dashboards() + { + return this._dashboards; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js b/Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js new file mode 100644 index 000000000..827263add --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/DebuggerManager.js @@ -0,0 +1,1217 @@ +/* + * Copyright (C) 2013-2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.DebuggerManager = class DebuggerManager extends WebInspector.Object +{ + constructor() + { + super(); + + DebuggerAgent.enable(); + + WebInspector.notifications.addEventListener(WebInspector.Notification.DebugUIEnabledDidChange, this._debugUIEnabledDidChange, this); + + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.DisplayLocationDidChange, this._breakpointDisplayLocationDidChange, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.DisabledStateDidChange, this._breakpointDisabledStateDidChange, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.ConditionDidChange, this._breakpointEditablePropertyDidChange, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.IgnoreCountDidChange, this._breakpointEditablePropertyDidChange, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.AutoContinueDidChange, this._breakpointEditablePropertyDidChange, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.ActionsDidChange, this._breakpointEditablePropertyDidChange, this); + + WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingWillStart, this._timelineCapturingWillStart, this); + WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._timelineCapturingStopped, this); + + WebInspector.targetManager.addEventListener(WebInspector.TargetManager.Event.TargetRemoved, this._targetRemoved, this); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + + this._breakpointsSetting = new WebInspector.Setting("breakpoints", []); + this._breakpointsEnabledSetting = new WebInspector.Setting("breakpoints-enabled", true); + this._allExceptionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-all-exceptions", false); + this._allUncaughtExceptionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-all-uncaught-exceptions", false); + this._assertionsBreakpointEnabledSetting = new WebInspector.Setting("break-on-assertions", false); + this._asyncStackTraceDepthSetting = new WebInspector.Setting("async-stack-trace-depth", 200); + + let specialBreakpointLocation = new WebInspector.SourceCodeLocation(null, Infinity, Infinity); + + this._allExceptionsBreakpoint = new WebInspector.Breakpoint(specialBreakpointLocation, !this._allExceptionsBreakpointEnabledSetting.value); + this._allExceptionsBreakpoint.resolved = true; + + this._allUncaughtExceptionsBreakpoint = new WebInspector.Breakpoint(specialBreakpointLocation, !this._allUncaughtExceptionsBreakpointEnabledSetting.value); + + this._assertionsBreakpoint = new WebInspector.Breakpoint(specialBreakpointLocation, !this._assertionsBreakpointEnabledSetting.value); + this._assertionsBreakpoint.resolved = true; + + this._breakpoints = []; + this._breakpointContentIdentifierMap = new Map; + this._breakpointScriptIdentifierMap = new Map; + this._breakpointIdMap = new Map; + + this._breakOnExceptionsState = "none"; + this._updateBreakOnExceptionsState(); + + this._nextBreakpointActionIdentifier = 1; + + this._activeCallFrame = null; + + this._internalWebKitScripts = []; + this._targetDebuggerDataMap = new Map; + this._targetDebuggerDataMap.set(WebInspector.mainTarget, new WebInspector.DebuggerData(WebInspector.mainTarget)); + + // Restore the correct breakpoints enabled setting if Web Inspector had + // previously been left in a state where breakpoints were temporarily disabled. + this._temporarilyDisabledBreakpointsRestoreSetting = new WebInspector.Setting("temporarily-disabled-breakpoints-restore", null); + if (this._temporarilyDisabledBreakpointsRestoreSetting.value !== null) { + this._breakpointsEnabledSetting.value = this._temporarilyDisabledBreakpointsRestoreSetting.value; + this._temporarilyDisabledBreakpointsRestoreSetting.value = null; + } + + DebuggerAgent.setBreakpointsActive(this._breakpointsEnabledSetting.value); + DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState); + + // COMPATIBILITY (iOS 10): DebuggerAgent.setPauseOnAssertions did not exist yet. + if (DebuggerAgent.setPauseOnAssertions) + DebuggerAgent.setPauseOnAssertions(this._assertionsBreakpointEnabledSetting.value); + + // COMPATIBILITY (iOS 10): Debugger.setAsyncStackTraceDepth did not exist yet. + if (DebuggerAgent.setAsyncStackTraceDepth) + DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value); + + this._ignoreBreakpointDisplayLocationDidChangeEvent = false; + + function restoreBreakpointsSoon() { + this._restoringBreakpoints = true; + for (let cookie of this._breakpointsSetting.value) + this.addBreakpoint(new WebInspector.Breakpoint(cookie)); + this._restoringBreakpoints = false; + } + + // Ensure that all managers learn about restored breakpoints, + // regardless of their initialization order. + setTimeout(restoreBreakpointsSoon.bind(this), 0); + } + + // Public + + get paused() + { + for (let [target, targetData] of this._targetDebuggerDataMap) { + if (targetData.paused) + return true; + } + + return false; + } + + get activeCallFrame() + { + return this._activeCallFrame; + } + + set activeCallFrame(callFrame) + { + if (callFrame === this._activeCallFrame) + return; + + this._activeCallFrame = callFrame || null; + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange); + } + + dataForTarget(target) + { + let targetData = this._targetDebuggerDataMap.get(target); + if (targetData) + return targetData; + + targetData = new WebInspector.DebuggerData(target); + this._targetDebuggerDataMap.set(target, targetData); + return targetData; + } + + get allExceptionsBreakpoint() + { + return this._allExceptionsBreakpoint; + } + + get allUncaughtExceptionsBreakpoint() + { + return this._allUncaughtExceptionsBreakpoint; + } + + get assertionsBreakpoint() + { + return this._assertionsBreakpoint; + } + + get breakpoints() + { + return this._breakpoints; + } + + breakpointForIdentifier(id) + { + return this._breakpointIdMap.get(id) || null; + } + + breakpointsForSourceCode(sourceCode) + { + console.assert(sourceCode instanceof WebInspector.Resource || sourceCode instanceof WebInspector.Script); + + if (sourceCode instanceof WebInspector.SourceMapResource) { + let originalSourceCodeBreakpoints = this.breakpointsForSourceCode(sourceCode.sourceMap.originalSourceCode); + return originalSourceCodeBreakpoints.filter(function(breakpoint) { + return breakpoint.sourceCodeLocation.displaySourceCode === sourceCode; + }); + } + + let contentIdentifierBreakpoints = this._breakpointContentIdentifierMap.get(sourceCode.contentIdentifier); + if (contentIdentifierBreakpoints) { + this._associateBreakpointsWithSourceCode(contentIdentifierBreakpoints, sourceCode); + return contentIdentifierBreakpoints; + } + + if (sourceCode instanceof WebInspector.Script) { + let scriptIdentifierBreakpoints = this._breakpointScriptIdentifierMap.get(sourceCode.id); + if (scriptIdentifierBreakpoints) { + this._associateBreakpointsWithSourceCode(scriptIdentifierBreakpoints, sourceCode); + return scriptIdentifierBreakpoints; + } + } + + return []; + } + + isBreakpointRemovable(breakpoint) + { + return breakpoint !== this._allExceptionsBreakpoint + && breakpoint !== this._allUncaughtExceptionsBreakpoint + && breakpoint !== this._assertionsBreakpoint; + } + + isBreakpointEditable(breakpoint) + { + return this.isBreakpointRemovable(breakpoint); + } + + get breakpointsEnabled() + { + return this._breakpointsEnabledSetting.value; + } + + set breakpointsEnabled(enabled) + { + if (this._breakpointsEnabledSetting.value === enabled) + return; + + console.assert(!(enabled && this.breakpointsDisabledTemporarily), "Should not enable breakpoints when we are temporarily disabling breakpoints."); + if (enabled && this.breakpointsDisabledTemporarily) + return; + + this._breakpointsEnabledSetting.value = enabled; + + this._updateBreakOnExceptionsState(); + + for (let target of WebInspector.targets) { + target.DebuggerAgent.setBreakpointsActive(enabled); + target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState); + } + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.BreakpointsEnabledDidChange); + } + + get breakpointsDisabledTemporarily() + { + return this._temporarilyDisabledBreakpointsRestoreSetting.value !== null; + } + + scriptForIdentifier(id, target) + { + console.assert(target instanceof WebInspector.Target); + return this.dataForTarget(target).scriptForIdentifier(id); + } + + scriptsForURL(url, target) + { + // FIXME: This may not be safe. A Resource's URL may differ from a Script's URL. + console.assert(target instanceof WebInspector.Target); + return this.dataForTarget(target).scriptsForURL(url); + } + + get searchableScripts() + { + return this.knownNonResourceScripts.filter((script) => !!script.contentIdentifier); + } + + get knownNonResourceScripts() + { + let knownScripts = []; + + for (let [target, targetData] of this._targetDebuggerDataMap) { + for (let script of targetData.scripts) { + if (script.resource) + continue; + if (!WebInspector.isDebugUIEnabled() && isWebKitInternalScript(script.sourceURL)) + continue; + knownScripts.push(script); + } + } + + return knownScripts; + } + + get asyncStackTraceDepth() + { + return this._asyncStackTraceDepthSetting.value; + } + + set asyncStackTraceDepth(x) + { + if (this._asyncStackTraceDepthSetting.value === x) + return; + + this._asyncStackTraceDepthSetting.value = x; + + for (let target of WebInspector.targets) + target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value); + } + + pause() + { + if (this.paused) + return Promise.resolve(); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.WaitingToPause); + + let listener = new WebInspector.EventListener(this, true); + + let managerResult = new Promise(function(resolve, reject) { + listener.connect(WebInspector.debuggerManager, WebInspector.DebuggerManager.Event.Paused, resolve); + }); + + let promises = []; + for (let [target, targetData] of this._targetDebuggerDataMap) + promises.push(targetData.pauseIfNeeded()); + + return Promise.all([managerResult, ...promises]); + } + + resume() + { + if (!this.paused) + return Promise.resolve(); + + let listener = new WebInspector.EventListener(this, true); + + let managerResult = new Promise(function(resolve, reject) { + listener.connect(WebInspector.debuggerManager, WebInspector.DebuggerManager.Event.Resumed, resolve); + }); + + let promises = []; + for (let [target, targetData] of this._targetDebuggerDataMap) + promises.push(targetData.resumeIfNeeded()); + + return Promise.all([managerResult, ...promises]); + } + + stepOver() + { + if (!this.paused) + return Promise.reject(new Error("Cannot step over because debugger is not paused.")); + + let listener = new WebInspector.EventListener(this, true); + + let managerResult = new Promise(function(resolve, reject) { + listener.connect(WebInspector.debuggerManager, WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, resolve); + }); + + let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepOver() + .catch(function(error) { + listener.disconnect(); + console.error("DebuggerManager.stepOver failed: ", error); + throw error; + }); + + return Promise.all([managerResult, protocolResult]); + } + + stepInto() + { + if (!this.paused) + return Promise.reject(new Error("Cannot step into because debugger is not paused.")); + + let listener = new WebInspector.EventListener(this, true); + + let managerResult = new Promise(function(resolve, reject) { + listener.connect(WebInspector.debuggerManager, WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, resolve); + }); + + let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepInto() + .catch(function(error) { + listener.disconnect(); + console.error("DebuggerManager.stepInto failed: ", error); + throw error; + }); + + return Promise.all([managerResult, protocolResult]); + } + + stepOut() + { + if (!this.paused) + return Promise.reject(new Error("Cannot step out because debugger is not paused.")); + + let listener = new WebInspector.EventListener(this, true); + + let managerResult = new Promise(function(resolve, reject) { + listener.connect(WebInspector.debuggerManager, WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, resolve); + }); + + let protocolResult = this._activeCallFrame.target.DebuggerAgent.stepOut() + .catch(function(error) { + listener.disconnect(); + console.error("DebuggerManager.stepOut failed: ", error); + throw error; + }); + + return Promise.all([managerResult, protocolResult]); + } + + continueUntilNextRunLoop(target) + { + return this.dataForTarget(target).continueUntilNextRunLoop(); + } + + continueToLocation(script, lineNumber, columnNumber) + { + return script.target.DebuggerAgent.continueToLocation({scriptId: script.id, lineNumber, columnNumber}); + } + + addBreakpoint(breakpoint, shouldSpeculativelyResolve) + { + console.assert(breakpoint instanceof WebInspector.Breakpoint); + if (!breakpoint) + return; + + if (breakpoint.contentIdentifier) { + let contentIdentifierBreakpoints = this._breakpointContentIdentifierMap.get(breakpoint.contentIdentifier); + if (!contentIdentifierBreakpoints) { + contentIdentifierBreakpoints = []; + this._breakpointContentIdentifierMap.set(breakpoint.contentIdentifier, contentIdentifierBreakpoints); + } + contentIdentifierBreakpoints.push(breakpoint); + } + + if (breakpoint.scriptIdentifier) { + let scriptIdentifierBreakpoints = this._breakpointScriptIdentifierMap.get(breakpoint.scriptIdentifier); + if (!scriptIdentifierBreakpoints) { + scriptIdentifierBreakpoints = []; + this._breakpointScriptIdentifierMap.set(breakpoint.scriptIdentifier, scriptIdentifierBreakpoints); + } + scriptIdentifierBreakpoints.push(breakpoint); + } + + this._breakpoints.push(breakpoint); + + if (!breakpoint.disabled) { + const specificTarget = undefined; + this._setBreakpoint(breakpoint, specificTarget, () => { + if (shouldSpeculativelyResolve) + breakpoint.resolved = true; + }); + } + + this._saveBreakpoints(); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.BreakpointAdded, {breakpoint}); + } + + removeBreakpoint(breakpoint) + { + console.assert(breakpoint instanceof WebInspector.Breakpoint); + if (!breakpoint) + return; + + console.assert(this.isBreakpointRemovable(breakpoint)); + if (!this.isBreakpointRemovable(breakpoint)) + return; + + this._breakpoints.remove(breakpoint); + + if (breakpoint.identifier) + this._removeBreakpoint(breakpoint); + + if (breakpoint.contentIdentifier) { + let contentIdentifierBreakpoints = this._breakpointContentIdentifierMap.get(breakpoint.contentIdentifier); + if (contentIdentifierBreakpoints) { + contentIdentifierBreakpoints.remove(breakpoint); + if (!contentIdentifierBreakpoints.length) + this._breakpointContentIdentifierMap.delete(breakpoint.contentIdentifier); + } + } + + if (breakpoint.scriptIdentifier) { + let scriptIdentifierBreakpoints = this._breakpointScriptIdentifierMap.get(breakpoint.scriptIdentifier); + if (scriptIdentifierBreakpoints) { + scriptIdentifierBreakpoints.remove(breakpoint); + if (!scriptIdentifierBreakpoints.length) + this._breakpointScriptIdentifierMap.delete(breakpoint.scriptIdentifier); + } + } + + // Disable the breakpoint first, so removing actions doesn't re-add the breakpoint. + breakpoint.disabled = true; + breakpoint.clearActions(); + + this._saveBreakpoints(); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.BreakpointRemoved, {breakpoint}); + } + + nextBreakpointActionIdentifier() + { + return this._nextBreakpointActionIdentifier++; + } + + initializeTarget(target) + { + let DebuggerAgent = target.DebuggerAgent; + let targetData = this.dataForTarget(target); + + // Initialize global state. + DebuggerAgent.enable(); + DebuggerAgent.setBreakpointsActive(this._breakpointsEnabledSetting.value); + DebuggerAgent.setPauseOnAssertions(this._assertionsBreakpointEnabledSetting.value); + DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState); + DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value); + + if (this.paused) + targetData.pauseIfNeeded(); + + // Initialize breakpoints. + this._restoringBreakpoints = true; + for (let breakpoint of this._breakpoints) { + if (breakpoint.disabled) + continue; + if (!breakpoint.contentIdentifier) + continue; + this._setBreakpoint(breakpoint, target); + } + this._restoringBreakpoints = false; + } + + // Protected (Called from WebInspector.DebuggerObserver) + + breakpointResolved(target, breakpointIdentifier, location) + { + // Called from WebInspector.DebuggerObserver. + + let breakpoint = this._breakpointIdMap.get(breakpointIdentifier); + console.assert(breakpoint); + if (!breakpoint) + return; + + console.assert(breakpoint.identifier === breakpointIdentifier); + + if (!breakpoint.sourceCodeLocation.sourceCode) { + let sourceCodeLocation = this._sourceCodeLocationFromPayload(target, location); + breakpoint.sourceCodeLocation.sourceCode = sourceCodeLocation.sourceCode; + } + + breakpoint.resolved = true; + } + + reset() + { + // Called from WebInspector.DebuggerObserver. + + let wasPaused = this.paused; + + WebInspector.Script.resetUniqueDisplayNameNumbers(); + + this._internalWebKitScripts = []; + this._targetDebuggerDataMap.clear(); + + this._ignoreBreakpointDisplayLocationDidChangeEvent = true; + + // Mark all the breakpoints as unresolved. They will be reported as resolved when + // breakpointResolved is called as the page loads. + for (let breakpoint of this._breakpoints) { + breakpoint.resolved = false; + if (breakpoint.sourceCodeLocation.sourceCode) + breakpoint.sourceCodeLocation.sourceCode = null; + } + + this._ignoreBreakpointDisplayLocationDidChangeEvent = false; + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.ScriptsCleared); + + if (wasPaused) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.Resumed); + } + + debuggerDidPause(target, callFramesPayload, reason, data, asyncStackTracePayload) + { + // Called from WebInspector.DebuggerObserver. + + if (this._delayedResumeTimeout) { + clearTimeout(this._delayedResumeTimeout); + this._delayedResumeTimeout = undefined; + } + + let wasPaused = this.paused; + let targetData = this._targetDebuggerDataMap.get(target); + + let callFrames = []; + let pauseReason = this._pauseReasonFromPayload(reason); + let pauseData = data || null; + + for (var i = 0; i < callFramesPayload.length; ++i) { + var callFramePayload = callFramesPayload[i]; + var sourceCodeLocation = this._sourceCodeLocationFromPayload(target, callFramePayload.location); + // FIXME: There may be useful call frames without a source code location (native callframes), should we include them? + if (!sourceCodeLocation) + continue; + if (!sourceCodeLocation.sourceCode) + continue; + + // Exclude the case where the call frame is in the inspector code. + if (!WebInspector.isDebugUIEnabled() && isWebKitInternalScript(sourceCodeLocation.sourceCode.sourceURL)) + continue; + + let scopeChain = this._scopeChainFromPayload(target, callFramePayload.scopeChain); + let callFrame = WebInspector.CallFrame.fromDebuggerPayload(target, callFramePayload, scopeChain, sourceCodeLocation); + callFrames.push(callFrame); + } + + let activeCallFrame = callFrames[0]; + + if (!activeCallFrame) { + // FIXME: This may not be safe for multiple threads/targets. + // This indicates we were pausing in internal scripts only (Injected Scripts). + // Just resume and skip past this pause. We should be fixing the backend to + // not send such pauses. + if (wasPaused) + target.DebuggerAgent.continueUntilNextRunLoop(); + else + target.DebuggerAgent.resume(); + this._didResumeInternal(target); + return; + } + + let asyncStackTrace = WebInspector.StackTrace.fromPayload(target, asyncStackTracePayload); + targetData.updateForPause(callFrames, pauseReason, pauseData, asyncStackTrace); + + // Pause other targets because at least one target has paused. + // FIXME: Should this be done on the backend? + for (let [otherTarget, otherTargetData] of this._targetDebuggerDataMap) + otherTargetData.pauseIfNeeded(); + + let activeCallFrameDidChange = this._activeCallFrame && this._activeCallFrame.target === target; + if (activeCallFrameDidChange) + this._activeCallFrame = activeCallFrame; + else if (!wasPaused) { + this._activeCallFrame = activeCallFrame; + activeCallFrameDidChange = true; + } + + if (!wasPaused) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.Paused); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.CallFramesDidChange, {target}); + + if (activeCallFrameDidChange) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange); + } + + debuggerDidResume(target) + { + // Called from WebInspector.DebuggerObserver. + + // COMPATIBILITY (iOS 10): Debugger.resumed event was ambiguous. When stepping + // we would receive a Debugger.resumed and we would not know if it really meant + // the backend resumed or would pause again due to a step. Legacy backends wait + // 50ms, and treat it as a real resume if we haven't paused in that time frame. + // This delay ensures the user interface does not flash between brief steps + // or successive breakpoints. + if (!DebuggerAgent.setPauseOnAssertions) { + this._delayedResumeTimeout = setTimeout(this._didResumeInternal.bind(this, target), 50); + return; + } + + this._didResumeInternal(target); + } + + playBreakpointActionSound(breakpointActionIdentifier) + { + // Called from WebInspector.DebuggerObserver. + + InspectorFrontendHost.beep(); + } + + scriptDidParse(target, scriptIdentifier, url, startLine, startColumn, endLine, endColumn, isModule, isContentScript, sourceURL, sourceMapURL) + { + // Called from WebInspector.DebuggerObserver. + + // Don't add the script again if it is already known. + let targetData = this.dataForTarget(target); + let existingScript = targetData.scriptForIdentifier(scriptIdentifier); + if (existingScript) { + console.assert(existingScript.url === (url || null)); + console.assert(existingScript.range.startLine === startLine); + console.assert(existingScript.range.startColumn === startColumn); + console.assert(existingScript.range.endLine === endLine); + console.assert(existingScript.range.endColumn === endColumn); + return; + } + + if (!WebInspector.isDebugUIEnabled() && isWebKitInternalScript(sourceURL)) + return; + + let range = new WebInspector.TextRange(startLine, startColumn, endLine, endColumn); + let sourceType = isModule ? WebInspector.Script.SourceType.Module : WebInspector.Script.SourceType.Program; + let script = new WebInspector.Script(target, scriptIdentifier, range, url, sourceType, isContentScript, sourceURL, sourceMapURL); + + targetData.addScript(script); + + if (target !== WebInspector.mainTarget && !target.mainResource) { + // FIXME: <https://webkit.org/b/164427> Web Inspector: WorkerTarget's mainResource should be a Resource not a Script + // We make the main resource of a WorkerTarget the Script instead of the Resource + // because the frontend may not be informed of the Resource. We should guarantee + // the frontend is informed of the Resource. + if (script.url === target.name) { + target.mainResource = script; + if (script.resource) + target.resourceCollection.remove(script.resource); + } + } + + if (isWebKitInternalScript(script.sourceURL)) { + this._internalWebKitScripts.push(script); + if (!WebInspector.isDebugUIEnabled()) + return; + } + + // Console expressions are not added to the UI by default. + if (isWebInspectorConsoleEvaluationScript(script.sourceURL)) + return; + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.ScriptAdded, {script}); + + if (target !== WebInspector.mainTarget && !script.isMainResource() && !script.resource) + target.addScript(script); + } + + // Private + + _sourceCodeLocationFromPayload(target, payload) + { + let targetData = this.dataForTarget(target); + let script = targetData.scriptForIdentifier(payload.scriptId); + if (!script) + return null; + + return script.createSourceCodeLocation(payload.lineNumber, payload.columnNumber); + } + + _scopeChainFromPayload(target, payload) + { + let scopeChain = []; + for (let i = 0; i < payload.length; ++i) + scopeChain.push(this._scopeChainNodeFromPayload(target, payload[i])); + return scopeChain; + } + + _scopeChainNodeFromPayload(target, payload) + { + var type = null; + switch (payload.type) { + case DebuggerAgent.ScopeType.Global: + type = WebInspector.ScopeChainNode.Type.Global; + break; + case DebuggerAgent.ScopeType.With: + type = WebInspector.ScopeChainNode.Type.With; + break; + case DebuggerAgent.ScopeType.Closure: + type = WebInspector.ScopeChainNode.Type.Closure; + break; + case DebuggerAgent.ScopeType.Catch: + type = WebInspector.ScopeChainNode.Type.Catch; + break; + case DebuggerAgent.ScopeType.FunctionName: + type = WebInspector.ScopeChainNode.Type.FunctionName; + break; + case DebuggerAgent.ScopeType.NestedLexical: + type = WebInspector.ScopeChainNode.Type.Block; + break; + case DebuggerAgent.ScopeType.GlobalLexicalEnvironment: + type = WebInspector.ScopeChainNode.Type.GlobalLexicalEnvironment; + break; + + // COMPATIBILITY (iOS 9): Debugger.ScopeType.Local used to be provided by the backend. + // Newer backends no longer send this enum value, it should be computed by the frontend. + // Map this to "Closure" type. The frontend can recalculate this when needed. + case DebuggerAgent.ScopeType.Local: + type = WebInspector.ScopeChainNode.Type.Closure; + break; + + default: + console.error("Unknown type: " + payload.type); + } + + let object = WebInspector.RemoteObject.fromPayload(payload.object, target); + return new WebInspector.ScopeChainNode(type, [object], payload.name, payload.location, payload.empty); + } + + _pauseReasonFromPayload(payload) + { + // FIXME: Handle other backend pause reasons. + switch (payload) { + case DebuggerAgent.PausedReason.Assert: + return WebInspector.DebuggerManager.PauseReason.Assertion; + case DebuggerAgent.PausedReason.Breakpoint: + return WebInspector.DebuggerManager.PauseReason.Breakpoint; + case DebuggerAgent.PausedReason.CSPViolation: + return WebInspector.DebuggerManager.PauseReason.CSPViolation; + case DebuggerAgent.PausedReason.DebuggerStatement: + return WebInspector.DebuggerManager.PauseReason.DebuggerStatement; + case DebuggerAgent.PausedReason.Exception: + return WebInspector.DebuggerManager.PauseReason.Exception; + case DebuggerAgent.PausedReason.PauseOnNextStatement: + return WebInspector.DebuggerManager.PauseReason.PauseOnNextStatement; + default: + return WebInspector.DebuggerManager.PauseReason.Other; + } + } + + _debuggerBreakpointActionType(type) + { + switch (type) { + case WebInspector.BreakpointAction.Type.Log: + return DebuggerAgent.BreakpointActionType.Log; + case WebInspector.BreakpointAction.Type.Evaluate: + return DebuggerAgent.BreakpointActionType.Evaluate; + case WebInspector.BreakpointAction.Type.Sound: + return DebuggerAgent.BreakpointActionType.Sound; + case WebInspector.BreakpointAction.Type.Probe: + return DebuggerAgent.BreakpointActionType.Probe; + default: + console.assert(false); + return DebuggerAgent.BreakpointActionType.Log; + } + } + + _debuggerBreakpointOptions(breakpoint) + { + const templatePlaceholderRegex = /\$\{.*?\}/; + + let options = breakpoint.options; + let invalidActions = []; + + for (let action of options.actions) { + if (action.type !== WebInspector.BreakpointAction.Type.Log) + continue; + + if (!templatePlaceholderRegex.test(action.data)) + continue; + + let lexer = new WebInspector.BreakpointLogMessageLexer; + let tokens = lexer.tokenize(action.data); + if (!tokens) { + invalidActions.push(action); + continue; + } + + let templateLiteral = tokens.reduce((text, token) => { + if (token.type === WebInspector.BreakpointLogMessageLexer.TokenType.PlainText) + return text + token.data.escapeCharacters("`\\"); + if (token.type === WebInspector.BreakpointLogMessageLexer.TokenType.Expression) + return text + "${" + token.data + "}"; + return text; + }, ""); + + action.data = "console.log(`" + templateLiteral + "`)"; + action.type = WebInspector.BreakpointAction.Type.Evaluate; + } + + const onlyFirst = true; + for (let invalidAction of invalidActions) + options.actions.remove(invalidAction, onlyFirst); + + return options; + } + + _setBreakpoint(breakpoint, specificTarget, callback) + { + console.assert(!breakpoint.disabled); + + if (breakpoint.disabled) + return; + + if (!this._restoringBreakpoints && !this.breakpointsDisabledTemporarily) { + // Enable breakpoints since a breakpoint is being set. This eliminates + // a multi-step process for the user that can be confusing. + this.breakpointsEnabled = true; + } + + function didSetBreakpoint(target, error, breakpointIdentifier, locations) + { + if (error) + return; + + this._breakpointIdMap.set(breakpointIdentifier, breakpoint); + + breakpoint.identifier = breakpointIdentifier; + + // Debugger.setBreakpoint returns a single location. + if (!(locations instanceof Array)) + locations = [locations]; + + for (let location of locations) + this.breakpointResolved(target, breakpointIdentifier, location); + + if (typeof callback === "function") + callback(); + } + + // The breakpoint will be resolved again by calling DebuggerAgent, so mark it as unresolved. + // If something goes wrong it will stay unresolved and show up as such in the user interface. + // When setting for a new target, don't change the resolved target. + if (!specificTarget) + breakpoint.resolved = false; + + // Convert BreakpointAction types to DebuggerAgent protocol types. + // NOTE: Breakpoint.options returns new objects each time, so it is safe to modify. + // COMPATIBILITY (iOS 7): Debugger.BreakpointActionType did not exist yet. + let options; + if (DebuggerAgent.BreakpointActionType) { + options = this._debuggerBreakpointOptions(breakpoint); + if (options.actions.length) { + for (let action of options.actions) + action.type = this._debuggerBreakpointActionType(action.type); + } + } + + // COMPATIBILITY (iOS 7): iOS 7 and earlier, DebuggerAgent.setBreakpoint* took a "condition" string argument. + // This has been replaced with an "options" BreakpointOptions object. + if (breakpoint.contentIdentifier) { + let targets = specificTarget ? [specificTarget] : WebInspector.targets; + for (let target of targets) { + target.DebuggerAgent.setBreakpointByUrl.invoke({ + lineNumber: breakpoint.sourceCodeLocation.lineNumber, + url: breakpoint.contentIdentifier, + urlRegex: undefined, + columnNumber: breakpoint.sourceCodeLocation.columnNumber, + condition: breakpoint.condition, + options + }, didSetBreakpoint.bind(this, target), target.DebuggerAgent); + } + } else if (breakpoint.scriptIdentifier) { + let target = breakpoint.target; + target.DebuggerAgent.setBreakpoint.invoke({ + location: {scriptId: breakpoint.scriptIdentifier, lineNumber: breakpoint.sourceCodeLocation.lineNumber, columnNumber: breakpoint.sourceCodeLocation.columnNumber}, + condition: breakpoint.condition, + options + }, didSetBreakpoint.bind(this, target), target.DebuggerAgent); + } + } + + _removeBreakpoint(breakpoint, callback) + { + if (!breakpoint.identifier) + return; + + function didRemoveBreakpoint(error) + { + if (error) + console.error(error); + + this._breakpointIdMap.delete(breakpoint.identifier); + + breakpoint.identifier = null; + + // Don't reset resolved here since we want to keep disabled breakpoints looking like they + // are resolved in the user interface. They will get marked as unresolved in reset. + + if (typeof callback === "function") + callback(); + } + + if (breakpoint.contentIdentifier) { + for (let target of WebInspector.targets) + target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this)); + } else if (breakpoint.scriptIdentifier) { + let target = breakpoint.target; + target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this)); + } + } + + _breakpointDisplayLocationDidChange(event) + { + if (this._ignoreBreakpointDisplayLocationDidChangeEvent) + return; + + let breakpoint = event.target; + if (!breakpoint.identifier || breakpoint.disabled) + return; + + // Remove the breakpoint with its old id. + this._removeBreakpoint(breakpoint, breakpointRemoved.bind(this)); + + function breakpointRemoved() + { + // Add the breakpoint at its new lineNumber and get a new id. + this._setBreakpoint(breakpoint); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.BreakpointMoved, {breakpoint}); + } + } + + _breakpointDisabledStateDidChange(event) + { + this._saveBreakpoints(); + + let breakpoint = event.target; + if (breakpoint === this._allExceptionsBreakpoint) { + if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily) + this.breakpointsEnabled = true; + this._allExceptionsBreakpointEnabledSetting.value = !breakpoint.disabled; + this._updateBreakOnExceptionsState(); + for (let target of WebInspector.targets) + target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState); + return; + } + + if (breakpoint === this._allUncaughtExceptionsBreakpoint) { + if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily) + this.breakpointsEnabled = true; + this._allUncaughtExceptionsBreakpointEnabledSetting.value = !breakpoint.disabled; + this._updateBreakOnExceptionsState(); + for (let target of WebInspector.targets) + target.DebuggerAgent.setPauseOnExceptions(this._breakOnExceptionsState); + return; + } + + if (breakpoint === this._assertionsBreakpoint) { + if (!breakpoint.disabled && !this.breakpointsDisabledTemporarily) + this.breakpointsEnabled = true; + this._assertionsBreakpointEnabledSetting.value = !breakpoint.disabled; + for (let target of WebInspector.targets) + target.DebuggerAgent.setPauseOnAssertions(this._assertionsBreakpointEnabledSetting.value); + return; + } + + if (breakpoint.disabled) + this._removeBreakpoint(breakpoint); + else + this._setBreakpoint(breakpoint); + } + + _breakpointEditablePropertyDidChange(event) + { + this._saveBreakpoints(); + + let breakpoint = event.target; + if (breakpoint.disabled) + return; + + console.assert(this.isBreakpointEditable(breakpoint)); + if (!this.isBreakpointEditable(breakpoint)) + return; + + // Remove the breakpoint with its old id. + this._removeBreakpoint(breakpoint, breakpointRemoved.bind(this)); + + function breakpointRemoved() + { + // Add the breakpoint with its new properties and get a new id. + this._setBreakpoint(breakpoint); + } + } + + _startDisablingBreakpointsTemporarily() + { + console.assert(!this.breakpointsDisabledTemporarily, "Already temporarily disabling breakpoints."); + if (this.breakpointsDisabledTemporarily) + return; + + this._temporarilyDisabledBreakpointsRestoreSetting.value = this._breakpointsEnabledSetting.value; + + this.breakpointsEnabled = false; + } + + _stopDisablingBreakpointsTemporarily() + { + console.assert(this.breakpointsDisabledTemporarily, "Was not temporarily disabling breakpoints."); + if (!this.breakpointsDisabledTemporarily) + return; + + let restoreState = this._temporarilyDisabledBreakpointsRestoreSetting.value; + this._temporarilyDisabledBreakpointsRestoreSetting.value = null; + + this.breakpointsEnabled = restoreState; + } + + _timelineCapturingWillStart(event) + { + this._startDisablingBreakpointsTemporarily(); + + if (this.paused) + this.resume(); + } + + _timelineCapturingStopped(event) + { + this._stopDisablingBreakpointsTemporarily(); + } + + _targetRemoved(event) + { + let wasPaused = this.paused; + + this._targetDebuggerDataMap.delete(event.data.target); + + if (!this.paused && wasPaused) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.Resumed); + } + + _mainResourceDidChange(event) + { + if (!event.target.isMainFrame()) + return; + + this._didResumeInternal(WebInspector.mainTarget); + } + + _didResumeInternal(target) + { + if (!this.paused) + return; + + if (this._delayedResumeTimeout) { + clearTimeout(this._delayedResumeTimeout); + this._delayedResumeTimeout = undefined; + } + + let activeCallFrameDidChange = false; + if (this._activeCallFrame && this._activeCallFrame.target === target) { + this._activeCallFrame = null; + activeCallFrameDidChange = true; + } + + this.dataForTarget(target).updateForResume(); + + if (!this.paused) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.Resumed); + + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.CallFramesDidChange, {target}); + + if (activeCallFrameDidChange) + this.dispatchEventToListeners(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange); + } + + _updateBreakOnExceptionsState() + { + let state = "none"; + + if (this._breakpointsEnabledSetting.value) { + if (!this._allExceptionsBreakpoint.disabled) + state = "all"; + else if (!this._allUncaughtExceptionsBreakpoint.disabled) + state = "uncaught"; + } + + this._breakOnExceptionsState = state; + + switch (state) { + case "all": + // Mark the uncaught breakpoint as unresolved since "all" includes "uncaught". + // That way it is clear in the user interface that the breakpoint is ignored. + this._allUncaughtExceptionsBreakpoint.resolved = false; + break; + case "uncaught": + case "none": + // Mark the uncaught breakpoint as resolved again. + this._allUncaughtExceptionsBreakpoint.resolved = true; + break; + } + } + + _saveBreakpoints() + { + if (this._restoringBreakpoints) + return; + + let breakpointsToSave = this._breakpoints.filter((breakpoint) => !!breakpoint.contentIdentifier); + let serializedBreakpoints = breakpointsToSave.map((breakpoint) => breakpoint.info); + this._breakpointsSetting.value = serializedBreakpoints; + } + + _associateBreakpointsWithSourceCode(breakpoints, sourceCode) + { + this._ignoreBreakpointDisplayLocationDidChangeEvent = true; + + for (let breakpoint of breakpoints) { + if (!breakpoint.sourceCodeLocation.sourceCode) + breakpoint.sourceCodeLocation.sourceCode = sourceCode; + // SourceCodes can be unequal if the SourceCodeLocation is associated with a Script and we are looking at the Resource. + console.assert(breakpoint.sourceCodeLocation.sourceCode === sourceCode || breakpoint.sourceCodeLocation.sourceCode.contentIdentifier === sourceCode.contentIdentifier); + } + + this._ignoreBreakpointDisplayLocationDidChangeEvent = false; + } + + _debugUIEnabledDidChange() + { + let eventType = WebInspector.isDebugUIEnabled() ? WebInspector.DebuggerManager.Event.ScriptAdded : WebInspector.DebuggerManager.Event.ScriptRemoved; + for (let script of this._internalWebKitScripts) + this.dispatchEventToListeners(eventType, {script}); + } +}; + +WebInspector.DebuggerManager.Event = { + BreakpointAdded: "debugger-manager-breakpoint-added", + BreakpointRemoved: "debugger-manager-breakpoint-removed", + BreakpointMoved: "debugger-manager-breakpoint-moved", + WaitingToPause: "debugger-manager-waiting-to-pause", + Paused: "debugger-manager-paused", + Resumed: "debugger-manager-resumed", + CallFramesDidChange: "debugger-manager-call-frames-did-change", + ActiveCallFrameDidChange: "debugger-manager-active-call-frame-did-change", + ScriptAdded: "debugger-manager-script-added", + ScriptRemoved: "debugger-manager-script-removed", + ScriptsCleared: "debugger-manager-scripts-cleared", + BreakpointsEnabledDidChange: "debugger-manager-breakpoints-enabled-did-change" +}; + +WebInspector.DebuggerManager.PauseReason = { + Assertion: "assertion", + Breakpoint: "breakpoint", + CSPViolation: "CSP-violation", + DebuggerStatement: "debugger-statement", + Exception: "exception", + PauseOnNextStatement: "pause-on-next-statement", + Other: "other", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/DragToAdjustController.js b/Source/WebInspectorUI/UserInterface/Controllers/DragToAdjustController.js new file mode 100644 index 000000000..4ad3a2ab3 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/DragToAdjustController.js @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014 Antoine Quint + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.DragToAdjustController = class DragToAdjustController +{ + constructor(delegate) + { + this._delegate = delegate; + + this._element = null; + this._active = false; + this._enabled = false; + this._dragging = false; + this._tracksMouseClickAndDrag = false; + } + + // Public + + get element() + { + return this._element; + } + + set element(element) + { + this._element = element; + } + + get enabled() + { + return this._enabled; + } + + set enabled(enabled) + { + if (this._enabled === enabled) + return; + + if (enabled) { + this._element.addEventListener("mouseenter", this); + this._element.addEventListener("mouseleave", this); + } else { + this._element.removeEventListener("mouseenter", this); + this._element.removeEventListener("mouseleave", this); + } + } + + get active() + { + return this._active; + } + + set active(active) + { + if (!this._element) + return; + + if (this._active === active) + return; + + if (active) { + WebInspector.notifications.addEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._modifiersDidChange, this); + this._element.addEventListener("mousemove", this); + } else { + WebInspector.notifications.removeEventListener(WebInspector.Notification.GlobalModifierKeysDidChange, this._modifiersDidChange, this); + this._element.removeEventListener("mousemove", this); + this._setTracksMouseClickAndDrag(false); + } + + this._active = active; + + if (this._delegate && typeof this._delegate.dragToAdjustControllerActiveStateChanged === "function") + this._delegate.dragToAdjustControllerActiveStateChanged(this); + } + + reset() + { + this._setTracksMouseClickAndDrag(false); + this._element.classList.remove(WebInspector.DragToAdjustController.StyleClassName); + + if (this._delegate && typeof this._delegate.dragToAdjustControllerDidReset === "function") + this._delegate.dragToAdjustControllerDidReset(this); + } + + // Protected + + handleEvent(event) + { + switch (event.type) { + case "mouseenter": + if (!this._dragging) { + if (this._delegate && typeof this._delegate.dragToAdjustControllerCanBeActivated === "function") + this.active = this._delegate.dragToAdjustControllerCanBeActivated(this); + else + this.active = true; + } + break; + case "mouseleave": + if (!this._dragging) + this.active = false; + break; + case "mousemove": + if (this._dragging) + this._mouseWasDragged(event); + else + this._mouseMoved(event); + break; + case "mousedown": + this._mouseWasPressed(event); + break; + case "mouseup": + this._mouseWasReleased(event); + break; + case "contextmenu": + event.preventDefault(); + break; + } + } + + // Private + + _setDragging(dragging) + { + if (this._dragging === dragging) + return; + + console.assert(window.event); + if (dragging) + WebInspector.elementDragStart(this._element, this, this, window.event, "col-resize", window); + else + WebInspector.elementDragEnd(window.event); + + this._dragging = dragging; + } + + _setTracksMouseClickAndDrag(tracksMouseClickAndDrag) + { + if (this._tracksMouseClickAndDrag === tracksMouseClickAndDrag) + return; + + if (tracksMouseClickAndDrag) { + this._element.classList.add(WebInspector.DragToAdjustController.StyleClassName); + window.addEventListener("mousedown", this, true); + window.addEventListener("contextmenu", this, true); + } else { + this._element.classList.remove(WebInspector.DragToAdjustController.StyleClassName); + window.removeEventListener("mousedown", this, true); + window.removeEventListener("contextmenu", this, true); + this._setDragging(false); + } + + this._tracksMouseClickAndDrag = tracksMouseClickAndDrag; + } + + _modifiersDidChange(event) + { + var canBeAdjusted = WebInspector.modifierKeys.altKey; + if (canBeAdjusted && this._delegate && typeof this._delegate.dragToAdjustControllerCanBeAdjusted === "function") + canBeAdjusted = this._delegate.dragToAdjustControllerCanBeAdjusted(this); + + this._setTracksMouseClickAndDrag(canBeAdjusted); + } + + _mouseMoved(event) + { + var canBeAdjusted = event.altKey; + if (canBeAdjusted && this._delegate && typeof this._delegate.dragToAdjustControllerCanAdjustObjectAtPoint === "function") + canBeAdjusted = this._delegate.dragToAdjustControllerCanAdjustObjectAtPoint(this, WebInspector.Point.fromEvent(event)); + + this._setTracksMouseClickAndDrag(canBeAdjusted); + } + + _mouseWasPressed(event) + { + this._lastX = event.screenX; + + this._setDragging(true); + + event.preventDefault(); + event.stopPropagation(); + } + + _mouseWasDragged(event) + { + var x = event.screenX; + var amount = x - this._lastX; + + if (Math.abs(amount) < 1) + return; + + this._lastX = x; + + if (event.ctrlKey) + amount /= 10; + else if (event.shiftKey) + amount *= 10; + + if (this._delegate && typeof this._delegate.dragToAdjustControllerWasAdjustedByAmount === "function") + this._delegate.dragToAdjustControllerWasAdjustedByAmount(this, amount); + + event.preventDefault(); + event.stopPropagation(); + } + + _mouseWasReleased(event) + { + this._setDragging(false); + + event.preventDefault(); + event.stopPropagation(); + + this.reset(); + } +}; + +WebInspector.DragToAdjustController.StyleClassName = "drag-to-adjust"; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/Formatter.js b/Source/WebInspectorUI/UserInterface/Controllers/Formatter.js new file mode 100644 index 000000000..2a1652733 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/Formatter.js @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.Formatter = class Formatter +{ + constructor(codeMirror, builder) + { + console.assert(codeMirror); + console.assert(builder); + + this._codeMirror = codeMirror; + this._builder = builder; + + this._lastToken = null; + this._lastContent = ""; + } + + // Public + + format(from, to) + { + console.assert(this._builder.originalContent === null); + if (this._builder.originalContent !== null) + return; + + var outerMode = this._codeMirror.getMode(); + var content = this._codeMirror.getRange(from, to); + var state = CodeMirror.copyState(outerMode, this._codeMirror.getTokenAt(from).state); + this._builder.setOriginalContent(content); + + var lineOffset = 0; + var lines = content.split("\n"); + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + var startOfNewLine = true; + var firstTokenOnLine = true; + var stream = new CodeMirror.StringStream(line); + while (!stream.eol()) { + var innerMode = CodeMirror.innerMode(outerMode, state); + var token = outerMode.token(stream, state); + var isWhiteSpace = token === null && /^\s*$/.test(stream.current()); + this._handleToken(innerMode.mode, token, state, stream, lineOffset + stream.start, isWhiteSpace, startOfNewLine, firstTokenOnLine); + stream.start = stream.pos; + startOfNewLine = false; + if (firstTokenOnLine && !isWhiteSpace) + firstTokenOnLine = false; + } + + if (firstTokenOnLine) + this._handleEmptyLine(); + + lineOffset += line.length + 1; // +1 for the "\n" removed in split. + this._handleLineEnding(lineOffset - 1); // -1 for the index of the "\n". + } + + this._builder.finish(); + } + + // Private + + _handleToken(mode, token, state, stream, originalPosition, isWhiteSpace, startOfNewLine, firstTokenOnLine) + { + // String content of the token. + var content = stream.current(); + + // Start of a new line. Insert a newline to be safe if code was not-ASI safe. These are collapsed. + if (startOfNewLine) + this._builder.appendNewline(); + + // Whitespace. Remove all spaces or collapse to a single space. + if (isWhiteSpace) { + this._builder.appendSpace(); + return; + } + + // Avoid some hooks for content in comments. + var isComment = token && /\bcomment\b/.test(token); + + if (mode.modifyStateForTokenPre) + mode.modifyStateForTokenPre(this._lastToken, this._lastContent, token, state, content, isComment); + + // Should we remove the last whitespace? + if (this._builder.lastTokenWasWhitespace && mode.removeLastWhitespace(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.removeLastWhitespace(); + + // Should we remove the last newline? + if (this._builder.lastTokenWasNewline && mode.removeLastNewline(this._lastToken, this._lastContent, token, state, content, isComment, firstTokenOnLine)) + this._builder.removeLastNewline(); + + // Add whitespace after the last token? + if (!this._builder.lastTokenWasWhitespace && mode.shouldHaveSpaceAfterLastToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendSpace(); + + // Add whitespace before this token? + if (!this._builder.lastTokenWasWhitespace && mode.shouldHaveSpaceBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendSpace(); + + // Should we dedent before this token? + var dedents = mode.dedentsBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment); + while (dedents-- > 0) + this._builder.dedent(); + + // Should we add a newline before this token? + if (mode.newlineBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendNewline(); + + // Should we indent before this token? + if (mode.indentBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.indent(); + + // Append token. + this._builder.appendToken(content, originalPosition); + + // Let the pretty printer update any state it keeps track of. + if (mode.modifyStateForTokenPost) + mode.modifyStateForTokenPost(this._lastToken, this._lastContent, token, state, content, isComment); + + // Should we indent or dedent after this token? + if (!isComment && mode.indentAfterToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.indent(); + + // Should we add newlines after this token? + var newlines = mode.newlinesAfterToken(this._lastToken, this._lastContent, token, state, content, isComment); + if (newlines) + this._builder.appendMultipleNewlines(newlines); + + // Record this token as the last token. + this._lastToken = token; + this._lastContent = content; + } + + _handleEmptyLine() + { + // Preserve original whitespace only lines by adding a newline. + // However, don't do this if the builder just added multiple newlines. + if (!(this._builder.lastTokenWasNewline && this._builder.lastNewlineAppendWasMultiple)) + this._builder.appendNewline(true); + } + + _handleLineEnding(originalNewLinePosition) + { + // Record the original line ending. + this._builder.addOriginalLineEnding(originalNewLinePosition); + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/FormatterSourceMap.js b/Source/WebInspectorUI/UserInterface/Controllers/FormatterSourceMap.js new file mode 100644 index 000000000..e320ba1ce --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/FormatterSourceMap.js @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.FormatterSourceMap = class FormatterSourceMap extends WebInspector.Object +{ + constructor(originalLineEndings, formattedLineEndings, mapping) + { + super(); + + this._originalLineEndings = originalLineEndings; + this._formattedLineEndings = formattedLineEndings; + this._mapping = mapping; + } + + // Static + + static fromSourceMapData({originalLineEndings, formattedLineEndings, mapping}) + { + return new WebInspector.FormatterSourceMap(originalLineEndings, formattedLineEndings, mapping); + } + + // Public + + originalToFormatted(lineNumber, columnNumber) + { + var originalPosition = this._locationToPosition(this._originalLineEndings, lineNumber || 0, columnNumber || 0); + return this.originalPositionToFormatted(originalPosition); + } + + originalPositionToFormatted(originalPosition) + { + var formattedPosition = this._convertPosition(this._mapping.original, this._mapping.formatted, originalPosition); + return this._positionToLocation(this._formattedLineEndings, formattedPosition); + } + + formattedToOriginal(lineNumber, columnNumber) + { + var originalPosition = this.formattedToOriginalOffset(lineNumber, columnNumber); + return this._positionToLocation(this._originalLineEndings, originalPosition); + } + + formattedToOriginalOffset(lineNumber, columnNumber) + { + var formattedPosition = this._locationToPosition(this._formattedLineEndings, lineNumber || 0, columnNumber || 0); + var originalPosition = this._convertPosition(this._mapping.formatted, this._mapping.original, formattedPosition); + return originalPosition; + } + + // Private + + _locationToPosition(lineEndings, lineNumber, columnNumber) + { + var lineOffset = lineNumber ? lineEndings[lineNumber - 1] + 1 : 0; + return lineOffset + columnNumber; + } + + _positionToLocation(lineEndings, position) + { + var lineNumber = lineEndings.upperBound(position - 1); + if (!lineNumber) + var columnNumber = position; + else + var columnNumber = position - lineEndings[lineNumber - 1] - 1; + return {lineNumber, columnNumber}; + } + + _convertPosition(positions1, positions2, positionInPosition1) + { + var index = positions1.upperBound(positionInPosition1) - 1; + var convertedPosition = positions2[index] + positionInPosition1 - positions1[index]; + if (index < positions2.length - 1 && convertedPosition > positions2[index + 1]) + convertedPosition = positions2[index + 1]; + return convertedPosition; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js b/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js new file mode 100644 index 000000000..2fa0c936f --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/FrameResourceManager.js @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.FrameResourceManager = class FrameResourceManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.PageAgent) + PageAgent.enable(); + if (window.NetworkAgent) + NetworkAgent.enable(); + + WebInspector.notifications.addEventListener(WebInspector.Notification.ExtraDomainsActivated, this._extraDomainsActivated, this); + + this.initialize(); + } + + // Public + + initialize() + { + var oldMainFrame = this._mainFrame; + + this._frameIdentifierMap = new Map; + this._mainFrame = null; + this._resourceRequestIdentifierMap = new Map; + this._orphanedResources = new Map; + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + + this._waitingForMainFrameResourceTreePayload = true; + if (window.PageAgent) + PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this)); + } + + get mainFrame() + { + return this._mainFrame; + } + + get frames() + { + return [...this._frameIdentifierMap.values()]; + } + + frameForIdentifier(frameId) + { + return this._frameIdentifierMap.get(frameId) || null; + } + + frameDidNavigate(framePayload) + { + // Called from WebInspector.PageObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var frameWasLoadedInstantly = false; + + var frame = this.frameForIdentifier(framePayload.id); + if (!frame) { + // If the frame wasn't known before now, then the main resource was loaded instantly (about:blank, etc.) + // Make a new resource (which will make the frame). Mark will mark it as loaded at the end too since we + // don't expect any more events about the load finishing for these frames. + var frameResource = this._addNewResourceToFrameOrTarget(null, framePayload.id, framePayload.loaderId, framePayload.url, null, null, null, null, null, framePayload.name, framePayload.securityOrigin); + frame = frameResource.parentFrame; + frameWasLoadedInstantly = true; + + console.assert(frame); + if (!frame) + return; + } + + if (framePayload.loaderId === frame.provisionalLoaderIdentifier) { + // There was a provisional load in progress, commit it. + frame.commitProvisionalLoad(framePayload.securityOrigin); + } else { + if (frame.mainResource.url !== framePayload.url || frame.loaderIdentifier !== framePayload.loaderId) { + // Navigations like back/forward do not have provisional loads, so create a new main resource here. + var mainResource = new WebInspector.Resource(framePayload.url, framePayload.mimeType, null, framePayload.loaderId); + } else { + // The main resource is already correct, so reuse it. + var mainResource = frame.mainResource; + } + + frame.initialize(framePayload.name, framePayload.securityOrigin, framePayload.loaderId, mainResource); + } + + var oldMainFrame = this._mainFrame; + + if (framePayload.parentId) { + var parentFrame = this.frameForIdentifier(framePayload.parentId); + console.assert(parentFrame); + + if (frame === this._mainFrame) + this._mainFrame = null; + + if (frame.parentFrame !== parentFrame) + parentFrame.addChildFrame(frame); + } else { + if (frame.parentFrame) + frame.parentFrame.removeChildFrame(frame); + this._mainFrame = frame; + } + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + + if (frameWasLoadedInstantly) + frame.mainResource.markAsFinished(); + } + + frameDidDetach(frameId) + { + // Called from WebInspector.PageObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var frame = this.frameForIdentifier(frameId); + if (!frame) + return; + + if (frame.parentFrame) + frame.parentFrame.removeChildFrame(frame); + + this._frameIdentifierMap.delete(frame.id); + + var oldMainFrame = this._mainFrame; + + if (frame === this._mainFrame) + this._mainFrame = null; + + frame.clearExecutionContexts(); + + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.FrameWasRemoved, {frame}); + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + } + + resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, initiator, targetId) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // COMPATIBILITY (iOS 8): Timeline timestamps for legacy backends are computed + // dynamically from the first backend timestamp received. For navigations we + // need to reset that base timestamp, and an appropriate timestamp to use is + // the new main resource's will be sent timestamp. So save this value on the + // resource in case it becomes a main resource. + var originalRequestWillBeSentTimestamp = timestamp; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + if (resource) { + // This is an existing request which is being redirected, update the resource. + console.assert(redirectResponse); + console.assert(!targetId); + resource.updateForRedirectResponse(request.url, request.headers, elapsedTime); + return; + } + + var initiatorSourceCodeLocation = this._initiatorSourceCodeLocationFromPayload(initiator); + + // This is a new request, make a new resource and add it to the right frame. + resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, request.url, type, request.method, request.headers, request.postData, elapsedTime, null, null, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId); + + // Associate the resource with the requestIdentifier so it can be found in future loading events. + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + } + + markResourceRequestAsServedFromMemoryCache(requestIdentifier) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up. + if (!resource) + return; + + resource.markAsCached(); + } + + resourceRequestWasServedFromMemoryCache(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload, timestamp, initiator) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + console.assert(!this._resourceRequestIdentifierMap.has(requestIdentifier)); + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + var initiatorSourceCodeLocation = this._initiatorSourceCodeLocationFromPayload(initiator); + var response = cachedResourcePayload.response; + var resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload.url, cachedResourcePayload.type, "GET", null, null, elapsedTime, null, null, initiatorSourceCodeLocation); + resource.markAsCached(); + resource.updateForResponse(cachedResourcePayload.url, response.mimeType, cachedResourcePayload.type, response.headers, response.status, response.statusText, elapsedTime, response.timing); + resource.increaseSize(cachedResourcePayload.bodySize, elapsedTime); + resource.increaseTransferSize(cachedResourcePayload.bodySize); + resource.markAsFinished(elapsedTime); + + if (cachedResourcePayload.sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(cachedResourcePayload.sourceMapURL, resource.url, resource); + + // No need to associate the resource with the requestIdentifier, since this is the only event + // sent for memory cache resource loads. + } + + resourceRequestDidReceiveResponse(requestIdentifier, frameIdentifier, loaderIdentifier, type, response, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up, but we can try to look it up by its URL. + if (!resource) { + var frame = this.frameForIdentifier(frameIdentifier); + if (frame) + resource = frame.resourceForURL(response.url); + + // If we find the resource this way we had marked it earlier as finished via PageAgent.getResourceTree. + // Associate the resource with the requestIdentifier so it can be found in future loading events. + // and roll it back to an unfinished state, we know now it is still loading. + if (resource) { + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + resource.revertMarkAsFinished(); + } + } + + // If we haven't found an existing Resource by now, then it is a resource that was loading when the inspector + // opened and we just missed the resourceRequestWillBeSent for it. So make a new resource and add it. + if (!resource) { + resource = this._addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, response.url, type, null, response.requestHeaders, null, elapsedTime, null, null, null); + + // Associate the resource with the requestIdentifier so it can be found in future loading events. + this._resourceRequestIdentifierMap.set(requestIdentifier, resource); + } + + if (response.fromDiskCache) + resource.markAsCached(); + + resource.updateForResponse(response.url, response.mimeType, type, response.headers, response.status, response.statusText, elapsedTime, response.timing); + } + + resourceRequestDidReceiveData(requestIdentifier, dataLength, encodedDataLength, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + + // We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called). + // We don't want to assert in this case since we do likely have the resource, via PageAgent.getResourceTree. The Resource + // just doesn't have a requestIdentifier for us to look it up. + if (!resource) + return; + + resource.increaseSize(dataLength, elapsedTime); + + if (encodedDataLength !== -1) + resource.increaseTransferSize(encodedDataLength); + } + + resourceRequestDidFinishLoading(requestIdentifier, timestamp, sourceMapURL) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // By now we should always have the Resource. Either it was fetched when the inspector first opened with + // PageAgent.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse. + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + console.assert(resource); + if (!resource) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + resource.markAsFinished(elapsedTime); + + if (sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(sourceMapURL, resource.url, resource); + + this._resourceRequestIdentifierMap.delete(requestIdentifier); + } + + resourceRequestDidFailLoading(requestIdentifier, canceled, timestamp) + { + // Called from WebInspector.NetworkObserver. + + // Ignore this while waiting for the whole frame/resource tree. + if (this._waitingForMainFrameResourceTreePayload) + return; + + // By now we should always have the Resource. Either it was fetched when the inspector first opened with + // PageAgent.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse. + let resource = this._resourceRequestIdentifierMap.get(requestIdentifier); + console.assert(resource); + if (!resource) + return; + + var elapsedTime = WebInspector.timelineManager.computeElapsedTime(timestamp); + resource.markAsFailed(canceled, elapsedTime); + + if (resource === resource.parentFrame.provisionalMainResource) + resource.parentFrame.clearProvisionalLoad(); + + this._resourceRequestIdentifierMap.delete(requestIdentifier); + } + + executionContextCreated(contextPayload) + { + // Called from WebInspector.RuntimeObserver. + + var frame = this.frameForIdentifier(contextPayload.frameId); + console.assert(frame); + if (!frame) + return; + + var displayName = contextPayload.name || frame.mainResource.displayName; + var executionContext = new WebInspector.ExecutionContext(WebInspector.mainTarget, contextPayload.id, displayName, contextPayload.isPageContext, frame); + frame.addExecutionContext(executionContext); + } + + resourceForURL(url) + { + if (!this._mainFrame) + return null; + + if (this._mainFrame.mainResource.url === url) + return this._mainFrame.mainResource; + + return this._mainFrame.resourceForURL(url, true); + } + + adoptOrphanedResourcesForTarget(target) + { + let resources = this._orphanedResources.take(target.identifier); + if (!resources) + return; + + for (let resource of resources) + target.adoptResource(resource); + } + + // Private + + _addNewResourceToFrameOrTarget(requestIdentifier, frameIdentifier, loaderIdentifier, url, type, requestMethod, requestHeaders, requestData, elapsedTime, frameName, frameSecurityOrigin, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp, targetId) + { + console.assert(!this._waitingForMainFrameResourceTreePayload); + + let resource = null; + + let frame = this.frameForIdentifier(frameIdentifier); + if (frame) { + // This is a new request for an existing frame, which might be the main resource or a new resource. + if (frame.mainResource.url === url && frame.loaderIdentifier === loaderIdentifier) + resource = frame.mainResource; + else if (frame.provisionalMainResource && frame.provisionalMainResource.url === url && frame.provisionalLoaderIdentifier === loaderIdentifier) + resource = frame.provisionalMainResource; + else { + resource = new WebInspector.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp); + if (resource.target === WebInspector.mainTarget) + this._addResourceToFrame(frame, resource); + else if (resource.target) + resource.target.addResource(resource); + else + this._addOrphanedResource(resource, targetId); + } + } else { + // This is a new request for a new frame, which is always the main resource. + console.assert(!targetId); + resource = new WebInspector.Resource(url, null, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, elapsedTime, initiatorSourceCodeLocation, originalRequestWillBeSentTimestamp); + frame = new WebInspector.Frame(frameIdentifier, frameName, frameSecurityOrigin, loaderIdentifier, resource); + this._frameIdentifierMap.set(frame.id, frame); + + // If we don't have a main frame, assume this is it. This can change later in + // frameDidNavigate when the parent frame is known. + if (!this._mainFrame) { + this._mainFrame = frame; + this._mainFrameDidChange(null); + } + + this._dispatchFrameWasAddedEvent(frame); + } + + console.assert(resource); + + return resource; + } + + _addResourceToFrame(frame, resource) + { + console.assert(!this._waitingForMainFrameResourceTreePayload); + if (this._waitingForMainFrameResourceTreePayload) + return; + + console.assert(frame); + console.assert(resource); + + if (resource.loaderIdentifier !== frame.loaderIdentifier && !frame.provisionalLoaderIdentifier) { + // This is the start of a provisional load which happens before frameDidNavigate is called. + // This resource will be the new mainResource if frameDidNavigate is called. + frame.startProvisionalLoad(resource); + return; + } + + // This is just another resource, either for the main loader or the provisional loader. + console.assert(resource.loaderIdentifier === frame.loaderIdentifier || resource.loaderIdentifier === frame.provisionalLoaderIdentifier); + frame.addResource(resource); + } + + _addResourceToTarget(target, resource) + { + console.assert(target !== WebInspector.mainTarget); + console.assert(resource); + + target.addResource(resource); + } + + _initiatorSourceCodeLocationFromPayload(initiatorPayload) + { + if (!initiatorPayload) + return null; + + var url = null; + var lineNumber = NaN; + var columnNumber = 0; + + if (initiatorPayload.stackTrace && initiatorPayload.stackTrace.length) { + var stackTracePayload = initiatorPayload.stackTrace; + for (var i = 0; i < stackTracePayload.length; ++i) { + var callFramePayload = stackTracePayload[i]; + if (!callFramePayload.url || callFramePayload.url === "[native code]") + continue; + + url = callFramePayload.url; + + // The lineNumber is 1-based, but we expect 0-based. + lineNumber = callFramePayload.lineNumber - 1; + + columnNumber = callFramePayload.columnNumber; + + break; + } + } else if (initiatorPayload.url) { + url = initiatorPayload.url; + + // The lineNumber is 1-based, but we expect 0-based. + lineNumber = initiatorPayload.lineNumber - 1; + } + + if (!url || isNaN(lineNumber) || lineNumber < 0) + return null; + + var sourceCode = WebInspector.frameResourceManager.resourceForURL(url); + if (!sourceCode) + sourceCode = WebInspector.debuggerManager.scriptsForURL(url, WebInspector.mainTarget)[0]; + + if (!sourceCode) + return null; + + return sourceCode.createSourceCodeLocation(lineNumber, columnNumber); + } + + _processMainFrameResourceTreePayload(error, mainFramePayload) + { + console.assert(this._waitingForMainFrameResourceTreePayload); + delete this._waitingForMainFrameResourceTreePayload; + + if (error) { + console.error(JSON.stringify(error)); + return; + } + + console.assert(mainFramePayload); + console.assert(mainFramePayload.frame); + + this._resourceRequestIdentifierMap = new Map; + this._frameIdentifierMap = new Map; + + var oldMainFrame = this._mainFrame; + + this._mainFrame = this._addFrameTreeFromFrameResourceTreePayload(mainFramePayload, true); + + if (this._mainFrame !== oldMainFrame) + this._mainFrameDidChange(oldMainFrame); + } + + _createFrame(payload) + { + // If payload.url is missing or empty then this page is likely the special empty page. In that case + // we will just say it is "about:blank" so we have a URL, which is required for resources. + var mainResource = new WebInspector.Resource(payload.url || "about:blank", payload.mimeType, null, payload.loaderId); + var frame = new WebInspector.Frame(payload.id, payload.name, payload.securityOrigin, payload.loaderId, mainResource); + + this._frameIdentifierMap.set(frame.id, frame); + + mainResource.markAsFinished(); + + return frame; + } + + _createResource(payload, framePayload) + { + var resource = new WebInspector.Resource(payload.url, payload.mimeType, payload.type, framePayload.loaderId, payload.targetId); + + if (payload.sourceMapURL) + WebInspector.sourceMapManager.downloadSourceMap(payload.sourceMapURL, resource.url, resource); + + return resource; + } + + _addFrameTreeFromFrameResourceTreePayload(payload, isMainFrame) + { + var frame = this._createFrame(payload.frame); + if (isMainFrame) + frame.markAsMainFrame(); + + for (var i = 0; payload.childFrames && i < payload.childFrames.length; ++i) + frame.addChildFrame(this._addFrameTreeFromFrameResourceTreePayload(payload.childFrames[i], false)); + + for (var i = 0; payload.resources && i < payload.resources.length; ++i) { + var resourcePayload = payload.resources[i]; + + // The main resource is included as a resource. We can skip it since we already created + // a main resource when we created the Frame. The resource payload does not include anything + // didn't already get from the frame payload. + if (resourcePayload.type === "Document" && resourcePayload.url === payload.frame.url) + continue; + + var resource = this._createResource(resourcePayload, payload); + if (resource.target === WebInspector.mainTarget) + frame.addResource(resource); + else if (resource.target) + resource.target.addResource(resource); + else + this._addOrphanedResource(resource, resourcePayload.targetId); + + if (resourcePayload.failed || resourcePayload.canceled) + resource.markAsFailed(resourcePayload.canceled); + else + resource.markAsFinished(); + } + + this._dispatchFrameWasAddedEvent(frame); + + return frame; + } + + _addOrphanedResource(resource, targetId) + { + let resources = this._orphanedResources.get(targetId); + if (!resources) { + resources = []; + this._orphanedResources.set(targetId, resources); + } + + resources.push(resource); + } + + _dispatchFrameWasAddedEvent(frame) + { + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.FrameWasAdded, {frame}); + } + + _mainFrameDidChange(oldMainFrame) + { + if (oldMainFrame) + oldMainFrame.unmarkAsMainFrame(); + if (this._mainFrame) + this._mainFrame.markAsMainFrame(); + + this.dispatchEventToListeners(WebInspector.FrameResourceManager.Event.MainFrameDidChange, {oldMainFrame}); + } + + _extraDomainsActivated(event) + { + if (event.data.domains.includes("Page") && window.PageAgent) + PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this)); + } +}; + +WebInspector.FrameResourceManager.Event = { + FrameWasAdded: "frame-resource-manager-frame-was-added", + FrameWasRemoved: "frame-resource-manager-frame-was-removed", + MainFrameDidChange: "frame-resource-manager-main-frame-did-change", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/HeapManager.js b/Source/WebInspectorUI/UserInterface/Controllers/HeapManager.js new file mode 100644 index 000000000..cc8449cf3 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/HeapManager.js @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.HeapManager = class HeapManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.HeapAgent) + HeapAgent.enable(); + } + + // Public + + garbageCollected(target, payload) + { + // Called from WebInspector.HeapObserver. + + // FIXME: <https://webkit.org/b/167323> Web Inspector: Enable Memory profiling in Workers + if (target !== WebInspector.mainTarget) + return; + + let collection = WebInspector.GarbageCollection.fromPayload(payload); + this.dispatchEventToListeners(WebInspector.HeapManager.Event.GarbageCollected, {collection}); + } +}; + +WebInspector.HeapManager.Event = { + GarbageCollected: "heap-manager-garbage-collected" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/IssueManager.js b/Source/WebInspectorUI/UserInterface/Controllers/IssueManager.js new file mode 100644 index 000000000..a589adb38 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/IssueManager.js @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.IssueManager = class IssueManager extends WebInspector.Object +{ + constructor() + { + super(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._logCleared, this); + + this.initialize(); + } + + static issueMatchSourceCode(issue, sourceCode) + { + if (sourceCode instanceof WebInspector.SourceMapResource) + return issue.sourceCodeLocation && issue.sourceCodeLocation.displaySourceCode === sourceCode; + if (sourceCode instanceof WebInspector.Resource) + return issue.url === sourceCode.url && (!issue.sourceCodeLocation || issue.sourceCodeLocation.sourceCode === sourceCode); + if (sourceCode instanceof WebInspector.Script) + return issue.sourceCodeLocation && issue.sourceCodeLocation.sourceCode === sourceCode; + return false; + } + + // Public + + initialize() + { + this._issues = []; + + this.dispatchEventToListeners(WebInspector.IssueManager.Event.Cleared); + } + + issueWasAdded(consoleMessage) + { + let issue = new WebInspector.IssueMessage(consoleMessage); + + this._issues.push(issue); + + this.dispatchEventToListeners(WebInspector.IssueManager.Event.IssueWasAdded, {issue}); + } + + issuesForSourceCode(sourceCode) + { + var issues = []; + + for (var i = 0; i < this._issues.length; ++i) { + var issue = this._issues[i]; + if (WebInspector.IssueManager.issueMatchSourceCode(issue, sourceCode)) + issues.push(issue); + } + + return issues; + } + + // Private + + _logCleared(event) + { + this.initialize(); + } + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (!event.target.isMainFrame()) + return; + + this.initialize(); + } +}; + +WebInspector.IssueManager.Event = { + IssueWasAdded: "issue-manager-issue-was-added", + Cleared: "issue-manager-cleared" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js new file mode 100644 index 000000000..72621324c --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptLogViewController.js @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.JavaScriptLogViewController = class JavaScriptLogViewController extends WebInspector.Object +{ + constructor(element, scrollElement, textPrompt, delegate, historySettingIdentifier) + { + super(); + + console.assert(textPrompt instanceof WebInspector.ConsolePrompt); + console.assert(historySettingIdentifier); + + this._element = element; + this._scrollElement = scrollElement; + + this._promptHistorySetting = new WebInspector.Setting(historySettingIdentifier, null); + + this._prompt = textPrompt; + this._prompt.delegate = this; + this._prompt.history = this._promptHistorySetting.value; + + this.delegate = delegate; + + this._cleared = true; + this._previousMessageView = null; + this._lastCommitted = ""; + this._repeatCountWasInterrupted = false; + + this._sessions = []; + + this.messagesAlternateClearKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._element); + + this._messagesFindNextKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._element); + this._messagesFindPreviousKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._element); + + this._promptAlternateClearKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._prompt.element); + this._promptFindNextKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._prompt.element); + this._promptFindPreviousKeyboardShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.CommandOrControl | WebInspector.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._prompt.element); + + this._pendingMessages = []; + this._scheduledRenderIdentifier = 0; + + this.startNewSession(); + } + + // Public + + get prompt() + { + return this._prompt; + } + + get currentConsoleGroup() + { + return this._currentConsoleGroup; + } + + clear() + { + this._cleared = true; + + const clearPreviousSessions = true; + this.startNewSession(clearPreviousSessions, {newSessionReason: WebInspector.ConsoleSession.NewSessionReason.ConsoleCleared}); + } + + startNewSession(clearPreviousSessions = false, data = {}) + { + if (this._sessions.length && clearPreviousSessions) { + for (var i = 0; i < this._sessions.length; ++i) + this._element.removeChild(this._sessions[i].element); + + this._sessions = []; + this._currentConsoleGroup = null; + } + + // First session shows the time when the console was opened. + if (!this._sessions.length) + data.timestamp = Date.now(); + + let lastSession = this._sessions.lastValue; + + // Remove empty session. + if (lastSession && !lastSession.hasMessages()) { + this._sessions.pop(); + lastSession.element.remove(); + } + + let consoleSession = new WebInspector.ConsoleSession(data); + + this._previousMessageView = null; + this._lastCommitted = ""; + this._repeatCountWasInterrupted = false; + + this._sessions.push(consoleSession); + this._currentConsoleGroup = consoleSession; + + this._element.appendChild(consoleSession.element); + + // Make sure the new session is visible. + consoleSession.element.scrollIntoView(); + } + + appendImmediateExecutionWithResult(text, result, addSpecialUserLogClass, shouldRevealConsole) + { + console.assert(result instanceof WebInspector.RemoteObject); + + var commandMessageView = new WebInspector.ConsoleCommandView(text, addSpecialUserLogClass ? "special-user-log" : null); + this._appendConsoleMessageView(commandMessageView, true); + + function saveResultCallback(savedResultIndex) + { + let commandResultMessage = new WebInspector.ConsoleCommandResultMessage(result.target, result, false, savedResultIndex, shouldRevealConsole); + let commandResultMessageView = new WebInspector.ConsoleMessageView(commandResultMessage); + this._appendConsoleMessageView(commandResultMessageView, true); + } + + WebInspector.runtimeManager.saveResult(result, saveResultCallback.bind(this)); + } + + appendConsoleMessage(consoleMessage) + { + var consoleMessageView = new WebInspector.ConsoleMessageView(consoleMessage); + this._appendConsoleMessageView(consoleMessageView); + return consoleMessageView; + } + + updatePreviousMessageRepeatCount(count) + { + console.assert(this._previousMessageView); + if (!this._previousMessageView) + return false; + + var previousIgnoredCount = this._previousMessageView[WebInspector.JavaScriptLogViewController.IgnoredRepeatCount] || 0; + var previousVisibleCount = this._previousMessageView.repeatCount; + + if (!this._repeatCountWasInterrupted) { + this._previousMessageView.repeatCount = count - previousIgnoredCount; + return true; + } + + var consoleMessage = this._previousMessageView.message; + var duplicatedConsoleMessageView = new WebInspector.ConsoleMessageView(consoleMessage); + duplicatedConsoleMessageView[WebInspector.JavaScriptLogViewController.IgnoredRepeatCount] = previousIgnoredCount + previousVisibleCount; + duplicatedConsoleMessageView.repeatCount = 1; + this._appendConsoleMessageView(duplicatedConsoleMessageView); + + return true; + } + + isScrolledToBottom() + { + // Lie about being scrolled to the bottom if we have a pending request to scroll to the bottom soon. + return this._scrollToBottomTimeout || this._scrollElement.isScrolledToBottom(); + } + + scrollToBottom() + { + if (this._scrollToBottomTimeout) + return; + + function delayedWork() + { + this._scrollToBottomTimeout = null; + this._scrollElement.scrollTop = this._scrollElement.scrollHeight; + } + + // Don't scroll immediately so we are not causing excessive layouts when there + // are many messages being added at once. + this._scrollToBottomTimeout = setTimeout(delayedWork.bind(this), 0); + } + + requestClearMessages() + { + WebInspector.logManager.requestClearMessages(); + } + + // Protected + + consolePromptHistoryDidChange(prompt) + { + this._promptHistorySetting.value = this.prompt.history; + } + + consolePromptShouldCommitText(prompt, text, cursorIsAtLastPosition, handler) + { + // Always commit the text if we are not at the last position. + if (!cursorIsAtLastPosition) { + handler(true); + return; + } + + function parseFinished(error, result, message, range) + { + handler(result !== RuntimeAgent.SyntaxErrorType.Recoverable); + } + + WebInspector.runtimeManager.activeExecutionContext.target.RuntimeAgent.parse(text, parseFinished.bind(this)); + } + + consolePromptTextCommitted(prompt, text) + { + console.assert(text); + + if (this._lastCommitted !== text) { + let commandMessageView = new WebInspector.ConsoleCommandView(text); + this._appendConsoleMessageView(commandMessageView, true); + this._lastCommitted = text; + } + + function printResult(result, wasThrown, savedResultIndex) + { + if (!result || this._cleared) + return; + + let shouldRevealConsole = true; + let commandResultMessage = new WebInspector.ConsoleCommandResultMessage(result.target, result, wasThrown, savedResultIndex, shouldRevealConsole); + let commandResultMessageView = new WebInspector.ConsoleMessageView(commandResultMessage); + this._appendConsoleMessageView(commandResultMessageView, true); + } + + let options = { + objectGroup: WebInspector.RuntimeManager.ConsoleObjectGroup, + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: false, + returnByValue: false, + generatePreview: true, + saveResult: true, + sourceURLAppender: appendWebInspectorConsoleEvaluationSourceURL, + }; + + WebInspector.runtimeManager.evaluateInInspectedWindow(text, options, printResult.bind(this)); + } + + // Private + + _handleFindNextShortcut() + { + this.delegate.highlightNextSearchMatch(); + } + + _handleFindPreviousShortcut() + { + this.delegate.highlightPreviousSearchMatch(); + } + + _appendConsoleMessageView(messageView, repeatCountWasInterrupted) + { + this._pendingMessages.push(messageView); + + this._cleared = false; + this._repeatCountWasInterrupted = repeatCountWasInterrupted || false; + + if (!repeatCountWasInterrupted) + this._previousMessageView = messageView; + + if (messageView.message && messageView.message.source !== WebInspector.ConsoleMessage.MessageSource.JS) + this._lastCommitted = ""; + + if (WebInspector.consoleContentView.visible) + this.renderPendingMessagesSoon(); + + if (!WebInspector.isShowingConsoleTab() && messageView.message && messageView.message.shouldRevealConsole) + WebInspector.showSplitConsole(); + } + + renderPendingMessages() + { + if (this._scheduledRenderIdentifier) { + cancelAnimationFrame(this._scheduledRenderIdentifier); + this._scheduledRenderIdentifier = 0; + } + + if (this._pendingMessages.length === 0) + return; + + const maxMessagesPerFrame = 100; + let messages = this._pendingMessages.splice(0, maxMessagesPerFrame); + + let lastMessageView = messages.lastValue; + let isCommandView = lastMessageView instanceof WebInspector.ConsoleCommandView; + let shouldScrollToBottom = isCommandView || lastMessageView.message.type === WebInspector.ConsoleMessage.MessageType.Result || this.isScrolledToBottom(); + + for (let messageView of messages) { + messageView.render(); + this._didRenderConsoleMessageView(messageView); + } + + if (shouldScrollToBottom) + this.scrollToBottom(); + + WebInspector.quickConsole.needsLayout(); + + if (this._pendingMessages.length > 0) + this.renderPendingMessagesSoon(); + } + + renderPendingMessagesSoon() + { + if (this._scheduledRenderIdentifier) + return; + + this._scheduledRenderIdentifier = requestAnimationFrame(() => this.renderPendingMessages()); + } + + _didRenderConsoleMessageView(messageView) + { + var type = messageView instanceof WebInspector.ConsoleCommandView ? null : messageView.message.type; + if (type === WebInspector.ConsoleMessage.MessageType.EndGroup) { + var parentGroup = this._currentConsoleGroup.parentGroup; + if (parentGroup) + this._currentConsoleGroup = parentGroup; + } else { + if (type === WebInspector.ConsoleMessage.MessageType.StartGroup || type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) { + var group = new WebInspector.ConsoleGroup(this._currentConsoleGroup); + var groupElement = group.render(messageView); + this._currentConsoleGroup.append(groupElement); + this._currentConsoleGroup = group; + } else + this._currentConsoleGroup.addMessageView(messageView); + } + + if (this.delegate && typeof this.delegate.didAppendConsoleMessageView === "function") + this.delegate.didAppendConsoleMessageView(messageView); + } +}; + +WebInspector.JavaScriptLogViewController.CachedPropertiesDuration = 30000; +WebInspector.JavaScriptLogViewController.IgnoredRepeatCount = Symbol("ignored-repeat-count"); diff --git a/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js new file mode 100644 index 000000000..134b2f032 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/JavaScriptRuntimeCompletionProvider.js @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +Object.defineProperty(WebInspector, "javaScriptRuntimeCompletionProvider", +{ + get: function() + { + if (!WebInspector.JavaScriptRuntimeCompletionProvider._instance) + WebInspector.JavaScriptRuntimeCompletionProvider._instance = new WebInspector.JavaScriptRuntimeCompletionProvider; + return WebInspector.JavaScriptRuntimeCompletionProvider._instance; + } +}); + +WebInspector.JavaScriptRuntimeCompletionProvider = class JavaScriptRuntimeCompletionProvider extends WebInspector.Object +{ + constructor() + { + super(); + + console.assert(!WebInspector.JavaScriptRuntimeCompletionProvider._instance); + + WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this); + } + + // Protected + + completionControllerCompletionsNeeded(completionController, defaultCompletions, base, prefix, suffix, forced) + { + // Don't allow non-forced empty prefix completions unless the base is that start of property access. + if (!forced && !prefix && !/[.[]$/.test(base)) { + completionController.updateCompletions(null); + return; + } + + // If the base ends with an open parentheses or open curly bracket then treat it like there is + // no base so we get global object completions. + if (/[({]$/.test(base)) + base = ""; + + var lastBaseIndex = base.length - 1; + var dotNotation = base[lastBaseIndex] === "."; + var bracketNotation = base[lastBaseIndex] === "["; + + if (dotNotation || bracketNotation) { + base = base.substring(0, lastBaseIndex); + + // Don't suggest anything for an empty base that is using dot notation. + // Bracket notation with an empty base will be treated as an array. + if (!base && dotNotation) { + completionController.updateCompletions(defaultCompletions); + return; + } + + // Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float. + // But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties. + if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) { + completionController.updateCompletions(null); + return; + } + + // An empty base with bracket notation is not property access, it is an array. + // Clear the bracketNotation flag so completions are not quoted. + if (!base && bracketNotation) + bracketNotation = false; + } + + // If the base is the same as the last time, we can reuse the property names we have already gathered. + // Doing this eliminates delay caused by the async nature of the code below and it only calls getters + // and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated, + // but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the + // debugger to make sure we don't use stale properties in most cases. + if (this._lastBase === base && this._lastPropertyNames) { + receivedPropertyNames.call(this, this._lastPropertyNames); + return; + } + + this._lastBase = base; + this._lastPropertyNames = null; + + var activeCallFrame = WebInspector.debuggerManager.activeCallFrame; + if (!base && activeCallFrame && !this._alwaysEvaluateInWindowContext) + activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this)); + else { + let options = {objectGroup: "completion", includeCommandLineAPI: true, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: false, saveResult: false}; + WebInspector.runtimeManager.evaluateInInspectedWindow(base, options, evaluated.bind(this)); + } + + function updateLastPropertyNames(propertyNames) + { + if (this._clearLastPropertiesTimeout) + clearTimeout(this._clearLastPropertiesTimeout); + this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WebInspector.JavaScriptLogViewController.CachedPropertiesDuration); + + this._lastPropertyNames = propertyNames || {}; + } + + function evaluated(result, wasThrown) + { + if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) { + WebInspector.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion"); + + updateLastPropertyNames.call(this, {}); + completionController.updateCompletions(defaultCompletions); + + return; + } + + function inspectedPage_evalResult_getArrayCompletions(primitiveType) + { + var array = this; + var arrayLength; + + var resultSet = {}; + for (var o = array; o; o = o.__proto__) { + try { + if (o === array && o.length) { + // If the array type has a length, don't include a list of all the indexes. + // Include it at the end and the frontend can build the list. + arrayLength = o.length; + } else { + var names = Object.getOwnPropertyNames(o); + for (var i = 0; i < names.length; ++i) + resultSet[names[i]] = true; + } + } catch (e) { + // Ignore + } + } + + if (arrayLength) + resultSet["length"] = arrayLength; + + return resultSet; + } + + function inspectedPage_evalResult_getCompletions(primitiveType) + { + var object; + if (primitiveType === "string") + object = new String(""); + else if (primitiveType === "number") + object = new Number(0); + else if (primitiveType === "boolean") + object = new Boolean(false); + else if (primitiveType === "symbol") + object = Symbol(); + else + object = this; + + var resultSet = {}; + for (var o = object; o; o = o.__proto__) { + try { + var names = Object.getOwnPropertyNames(o); + for (var i = 0; i < names.length; ++i) + resultSet[names[i]] = true; + } catch (e) { + // Ignore + } + } + + return resultSet; + } + + if (result.subtype === "array") + result.callFunctionJSON(inspectedPage_evalResult_getArrayCompletions, undefined, receivedArrayPropertyNames.bind(this)); + else if (result.type === "object" || result.type === "function") + result.callFunctionJSON(inspectedPage_evalResult_getCompletions, undefined, receivedPropertyNames.bind(this)); + else if (result.type === "string" || result.type === "number" || result.type === "boolean" || result.type === "symbol") { + let options = {objectGroup: "completion", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: false, saveResult: false}; + WebInspector.runtimeManager.evaluateInInspectedWindow("(" + inspectedPage_evalResult_getCompletions + ")(\"" + result.type + "\")", options, receivedPropertyNamesFromEvaluate.bind(this)); + } else + console.error("Unknown result type: " + result.type); + } + + function receivedPropertyNamesFromEvaluate(object, wasThrown, result) + { + receivedPropertyNames.call(this, result && !wasThrown ? result.value : null); + } + + function receivedArrayPropertyNames(propertyNames) + { + // FIXME: <https://webkit.org/b/143589> Web Inspector: Better handling for large collections in Object Trees + // If there was an array like object, we generate autocompletion up to 1000 indexes, but this should + // handle a list with arbitrary length. + if (propertyNames && typeof propertyNames.length === "number") { + var max = Math.min(propertyNames.length, 1000); + for (var i = 0; i < max; ++i) + propertyNames[i] = true; + } + + receivedPropertyNames.call(this, propertyNames); + } + + function receivedPropertyNames(propertyNames) + { + propertyNames = propertyNames || {}; + + updateLastPropertyNames.call(this, propertyNames); + + WebInspector.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion"); + + if (!base) { + var commandLineAPI = ["$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", "getEventListeners", "$0", "$_"]; + if (WebInspector.debuggerManager.paused) { + let targetData = WebInspector.debuggerManager.dataForTarget(WebInspector.runtimeManager.activeExecutionContext.target); + if (targetData.pauseReason === WebInspector.DebuggerManager.PauseReason.Exception) + commandLineAPI.push("$exception"); + } + for (var i = 0; i < commandLineAPI.length; ++i) + propertyNames[commandLineAPI[i]] = true; + + // FIXME: Due to caching, sometimes old $n values show up as completion results even though they are not available. We should clear that proactively. + for (var i = 1; i <= WebInspector.ConsoleCommandResultMessage.maximumSavedResultIndex; ++i) + propertyNames["$" + i] = true; + } + + propertyNames = Object.keys(propertyNames); + + var implicitSuffix = ""; + if (bracketNotation) { + var quoteUsed = prefix[0] === "'" ? "'" : "\""; + if (suffix !== "]" && suffix !== quoteUsed) + implicitSuffix = "]"; + } + + var completions = defaultCompletions; + var knownCompletions = completions.keySet(); + + for (var i = 0; i < propertyNames.length; ++i) { + var property = propertyNames[i]; + + if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property)) + continue; + + if (bracketNotation) { + if (parseInt(property) != property) + property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : ""); + } + + if (!property.startsWith(prefix) || property in knownCompletions) + continue; + + completions.push(property); + knownCompletions[property] = true; + } + + function compare(a, b) + { + // Try to sort in numerical order first. + let numericCompareResult = a - b; + if (!isNaN(numericCompareResult)) + return numericCompareResult; + + // Sort __defineGetter__, __lookupGetter__, and friends last. + let aRareProperty = a.startsWith("__") && a.endsWith("__"); + let bRareProperty = b.startsWith("__") && b.endsWith("__"); + if (aRareProperty && !bRareProperty) + return 1; + if (!aRareProperty && bRareProperty) + return -1; + + // Not numbers, sort as strings. + return a.localeCompare(b); + } + + completions.sort(compare); + + completionController.updateCompletions(completions, implicitSuffix); + } + } + + // Private + + _clearLastProperties() + { + if (this._clearLastPropertiesTimeout) { + clearTimeout(this._clearLastPropertiesTimeout); + delete this._clearLastPropertiesTimeout; + } + + // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same + // expression is evaluated again. + this._lastPropertyNames = null; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/LayerTreeManager.js b/Source/WebInspectorUI/UserInterface/Controllers/LayerTreeManager.js new file mode 100644 index 000000000..48ec5f1d4 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/LayerTreeManager.js @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.LayerTreeManager = class LayerTreeManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._supported = !!window.LayerTreeAgent; + + if (this._supported) + LayerTreeAgent.enable(); + } + + // Public + + get supported() + { + return this._supported; + } + + layerTreeMutations(previousLayers, newLayers) + { + console.assert(this.supported); + + if (isEmptyObject(previousLayers)) { + return { + preserved: [], + additions: newLayers, + removals: [] + }; + } + + function nodeIdForLayer(layer) + { + return layer.isGeneratedContent ? layer.pseudoElementId : layer.nodeId; + } + + var layerIdsInPreviousLayers = []; + var nodeIdsInPreviousLayers = []; + var nodeIdsForReflectionsInPreviousLayers = []; + + previousLayers.forEach(function(layer) { + layerIdsInPreviousLayers.push(layer.layerId); + + var nodeId = nodeIdForLayer(layer); + if (!nodeId) + return; + + if (layer.isReflection) + nodeIdsForReflectionsInPreviousLayers.push(nodeId); + else + nodeIdsInPreviousLayers.push(nodeId); + }); + + var preserved = []; + var additions = []; + + var layerIdsInNewLayers = []; + var nodeIdsInNewLayers = []; + var nodeIdsForReflectionsInNewLayers = []; + + newLayers.forEach(function(layer) { + layerIdsInNewLayers.push(layer.layerId); + + var existed = layerIdsInPreviousLayers.includes(layer.layerId); + + var nodeId = nodeIdForLayer(layer); + if (!nodeId) + return; + + if (layer.isReflection) { + nodeIdsForReflectionsInNewLayers.push(nodeId); + existed = existed || nodeIdsForReflectionsInPreviousLayers.includes(nodeId); + } else { + nodeIdsInNewLayers.push(nodeId); + existed = existed || nodeIdsInPreviousLayers.includes(nodeId); + } + + if (existed) + preserved.push(layer); + else + additions.push(layer); + }); + + var removals = previousLayers.filter(function(layer) { + var nodeId = nodeIdForLayer(layer); + + if (layer.isReflection) + return !nodeIdsForReflectionsInNewLayers.includes(nodeId); + else + return !nodeIdsInNewLayers.includes(nodeId) && !layerIdsInNewLayers.includes(layer.layerId); + }); + + return {preserved, additions, removals}; + } + + layersForNode(node, callback) + { + console.assert(this.supported); + + LayerTreeAgent.layersForNode(node.id, function(error, layers) { + if (error || isEmptyObject(layers)) { + callback(null, []); + return; + } + + var firstLayer = layers[0]; + var layerForNode = firstLayer.nodeId === node.id && !firstLayer.isGeneratedContent ? layers.shift() : null; + callback(layerForNode, layers); + }); + } + + reasonsForCompositingLayer(layer, callback) + { + console.assert(this.supported); + + LayerTreeAgent.reasonsForCompositingLayer(layer.layerId, function(error, reasons) { + callback(error ? 0 : reasons); + }); + } + + layerTreeDidChange() + { + this.dispatchEventToListeners(WebInspector.LayerTreeManager.Event.LayerTreeDidChange); + } +}; + +WebInspector.LayerTreeManager.Event = { + LayerTreeDidChange: "layer-tree-did-change" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/LogManager.js b/Source/WebInspectorUI/UserInterface/Controllers/LogManager.js new file mode 100644 index 000000000..3b917bbbd --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/LogManager.js @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.LogManager = class LogManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._clearMessagesRequested = false; + this._isNewPageOrReload = false; + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + } + + // Public + + messageWasAdded(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, requestId) + { + // Called from WebInspector.ConsoleObserver. + + // FIXME: Get a request from request ID. + + if (parameters) + parameters = parameters.map((x) => WebInspector.RemoteObject.fromPayload(x, target)); + + let message = new WebInspector.ConsoleMessage(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, null); + + this.dispatchEventToListeners(WebInspector.LogManager.Event.MessageAdded, {message}); + + if (message.level === "warning" || message.level === "error") + WebInspector.issueManager.issueWasAdded(message); + } + + messagesCleared() + { + // Called from WebInspector.ConsoleObserver. + + WebInspector.ConsoleCommandResultMessage.clearMaximumSavedResultIndex(); + + if (this._clearMessagesRequested) { + // Frontend requested "clear console" and Backend successfully completed the request. + this._clearMessagesRequested = false; + this.dispatchEventToListeners(WebInspector.LogManager.Event.Cleared); + } else { + // Received an unrequested clear console event. + // This could be for a navigation or other reasons (like console.clear()). + // If this was a reload, we may not want to dispatch WebInspector.LogManager.Event.Cleared. + // To detect if this is a reload we wait a turn and check if there was a main resource change reload. + setTimeout(this._delayedMessagesCleared.bind(this), 0); + } + } + + _delayedMessagesCleared() + { + if (this._isNewPageOrReload) { + this._isNewPageOrReload = false; + + if (!WebInspector.settings.clearLogOnNavigate.value) + return; + } + + // A console.clear() or command line clear() happened. + this.dispatchEventToListeners(WebInspector.LogManager.Event.Cleared); + } + + messageRepeatCountUpdated(count) + { + // Called from WebInspector.ConsoleObserver. + + this.dispatchEventToListeners(WebInspector.LogManager.Event.PreviousMessageRepeatCountUpdated, {count}); + } + + requestClearMessages() + { + this._clearMessagesRequested = true; + + for (let target of WebInspector.targets) + target.ConsoleAgent.clearMessages(); + } + + // Private + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (!event.target.isMainFrame()) + return; + + this._isNewPageOrReload = true; + + let timestamp = Date.now(); + let wasReloaded = event.data.oldMainResource && event.data.oldMainResource.url === event.target.mainResource.url; + this.dispatchEventToListeners(WebInspector.LogManager.Event.SessionStarted, {timestamp, wasReloaded}); + + WebInspector.ConsoleCommandResultMessage.clearMaximumSavedResultIndex(); + } +}; + +WebInspector.LogManager.Event = { + SessionStarted: "log-manager-session-was-started", + Cleared: "log-manager-cleared", + MessageAdded: "log-manager-message-added", + PreviousMessageRepeatCountUpdated: "log-manager-previous-message-repeat-count-updated" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/MemoryManager.js b/Source/WebInspectorUI/UserInterface/Controllers/MemoryManager.js new file mode 100644 index 000000000..ecb07eacb --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/MemoryManager.js @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.MemoryManager = class MemoryManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.MemoryAgent) + MemoryAgent.enable(); + } + + // Public + + memoryPressure(timestamp, protocolSeverity) + { + // Called from WebInspector.MemoryObserver. + + let memoryPressureEvent = WebInspector.MemoryPressureEvent.fromPayload(timestamp, protocolSeverity); + this.dispatchEventToListeners(WebInspector.MemoryManager.Event.MemoryPressure, {memoryPressureEvent}); + } +}; + +WebInspector.MemoryManager.Event = { + MemoryPressure: "memory-manager-memory-pressure", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ProbeManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ProbeManager.js new file mode 100644 index 000000000..b7f100fba --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/ProbeManager.js @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2013 University of Washington. All rights reserved. + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.ProbeManager = class ProbeManager extends WebInspector.Object +{ + constructor() + { + super(); + + // Used to detect deleted probe actions. + this._knownProbeIdentifiersForBreakpoint = new Map; + + // Main lookup tables for probes and probe sets. + this._probesByIdentifier = new Map; + this._probeSetsByBreakpoint = new Map; + + WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointAdded, this._breakpointAdded, this); + WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.BreakpointRemoved, this._breakpointRemoved, this); + WebInspector.Breakpoint.addEventListener(WebInspector.Breakpoint.Event.ActionsDidChange, this._breakpointActionsChanged, this); + + // Saved breakpoints should not be restored on the first event loop turn, because it + // makes manager initialization order very fragile. No breakpoints should be available. + console.assert(!WebInspector.debuggerManager.breakpoints.length, "No breakpoints should exist before all the managers are constructed."); + } + + // Public + + get probeSets() + { + return [...this._probeSetsByBreakpoint.values()]; + } + + probeForIdentifier(identifier) + { + return this._probesByIdentifier.get(identifier); + } + + // Protected (called by WebInspector.DebuggerObserver) + + didSampleProbe(target, sample) + { + console.assert(this._probesByIdentifier.has(sample.probeId), "Unknown probe identifier specified for sample: ", sample); + let probe = this._probesByIdentifier.get(sample.probeId); + let elapsedTime = WebInspector.timelineManager.computeElapsedTime(sample.timestamp); + let object = WebInspector.RemoteObject.fromPayload(sample.payload, target); + probe.addSample(new WebInspector.ProbeSample(sample.sampleId, sample.batchId, elapsedTime, object)); + } + + // Private + + _breakpointAdded(breakpointOrEvent) + { + var breakpoint; + if (breakpointOrEvent instanceof WebInspector.Breakpoint) + breakpoint = breakpointOrEvent; + else + breakpoint = breakpointOrEvent.data.breakpoint; + + console.assert(breakpoint instanceof WebInspector.Breakpoint, "Unknown object passed as breakpoint: ", breakpoint); + + if (this._knownProbeIdentifiersForBreakpoint.has(breakpoint)) + return; + + this._knownProbeIdentifiersForBreakpoint.set(breakpoint, new Set); + + this._breakpointActionsChanged(breakpoint); + } + + _breakpointRemoved(event) + { + var breakpoint = event.data.breakpoint; + console.assert(this._knownProbeIdentifiersForBreakpoint.has(breakpoint)); + + this._breakpointActionsChanged(breakpoint); + this._knownProbeIdentifiersForBreakpoint.delete(breakpoint); + } + + _breakpointActionsChanged(breakpointOrEvent) + { + var breakpoint; + if (breakpointOrEvent instanceof WebInspector.Breakpoint) + breakpoint = breakpointOrEvent; + else + breakpoint = breakpointOrEvent.target; + + console.assert(breakpoint instanceof WebInspector.Breakpoint, "Unknown object passed as breakpoint: ", breakpoint); + + // Sometimes actions change before the added breakpoint is fully dispatched. + if (!this._knownProbeIdentifiersForBreakpoint.has(breakpoint)) { + this._breakpointAdded(breakpoint); + return; + } + + var knownProbeIdentifiers = this._knownProbeIdentifiersForBreakpoint.get(breakpoint); + var seenProbeIdentifiers = new Set; + + breakpoint.probeActions.forEach(function(probeAction) { + var probeIdentifier = probeAction.id; + console.assert(probeIdentifier, "Probe added without breakpoint action identifier: ", breakpoint); + + seenProbeIdentifiers.add(probeIdentifier); + if (!knownProbeIdentifiers.has(probeIdentifier)) { + // New probe; find or create relevant probe set. + knownProbeIdentifiers.add(probeIdentifier); + var probeSet = this._probeSetForBreakpoint(breakpoint); + var newProbe = new WebInspector.Probe(probeIdentifier, breakpoint, probeAction.data); + this._probesByIdentifier.set(probeIdentifier, newProbe); + probeSet.addProbe(newProbe); + return; + } + + var probe = this._probesByIdentifier.get(probeIdentifier); + console.assert(probe, "Probe known but couldn't be found by identifier: ", probeIdentifier); + // Update probe expression; if it differed, change events will fire. + probe.expression = probeAction.data; + }, this); + + // Look for missing probes based on what we saw last. + knownProbeIdentifiers.forEach(function(probeIdentifier) { + if (seenProbeIdentifiers.has(probeIdentifier)) + return; + + // The probe has gone missing, remove it. + var probeSet = this._probeSetForBreakpoint(breakpoint); + var probe = this._probesByIdentifier.get(probeIdentifier); + this._probesByIdentifier.delete(probeIdentifier); + knownProbeIdentifiers.delete(probeIdentifier); + probeSet.removeProbe(probe); + + // Remove the probe set if it has become empty. + if (!probeSet.probes.length) { + this._probeSetsByBreakpoint.delete(probeSet.breakpoint); + probeSet.willRemove(); + this.dispatchEventToListeners(WebInspector.ProbeManager.Event.ProbeSetRemoved, {probeSet}); + } + }, this); + } + + _probeSetForBreakpoint(breakpoint) + { + if (this._probeSetsByBreakpoint.has(breakpoint)) + return this._probeSetsByBreakpoint.get(breakpoint); + + var newProbeSet = new WebInspector.ProbeSet(breakpoint); + this._probeSetsByBreakpoint.set(breakpoint, newProbeSet); + this.dispatchEventToListeners(WebInspector.ProbeManager.Event.ProbeSetAdded, {probeSet: newProbeSet}); + return newProbeSet; + } +}; + +WebInspector.ProbeManager.Event = { + ProbeSetAdded: "probe-manager-probe-set-added", + ProbeSetRemoved: "probe-manager-probe-set-removed", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js b/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js new file mode 100644 index 000000000..ab04ddf10 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/ReplayManager.js @@ -0,0 +1,678 @@ +/* + * Copyright (C) 2013 University of Washington. All rights reserved. + * Copyright (C) 2014 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.ReplayManager = class ReplayManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._sessionState = WebInspector.ReplayManager.SessionState.Inactive; + this._segmentState = WebInspector.ReplayManager.SegmentState.Unloaded; + + this._activeSessionIdentifier = null; + this._activeSegmentIdentifier = null; + this._currentPosition = new WebInspector.ReplayPosition(0, 0); + this._initialized = false; + + // These hold actual instances of sessions and segments. + this._sessions = new Map; + this._segments = new Map; + // These hold promises that resolve when the instance data is recieved. + this._sessionPromises = new Map; + this._segmentPromises = new Map; + + // Playback speed is specified in replayToPosition commands, and persists + // for the duration of the playback command until another playback begins. + this._playbackSpeed = WebInspector.ReplayManager.PlaybackSpeed.RealTime; + + if (window.ReplayAgent) { + var instance = this; + this._initializationPromise = ReplayAgent.currentReplayState() + .then(function(payload) { + console.assert(payload.sessionState in WebInspector.ReplayManager.SessionState, "Unknown session state: " + payload.sessionState); + console.assert(payload.segmentState in WebInspector.ReplayManager.SegmentState, "Unknown segment state: " + payload.segmentState); + + instance._activeSessionIdentifier = payload.sessionIdentifier; + instance._activeSegmentIdentifier = payload.segmentIdentifier; + instance._sessionState = WebInspector.ReplayManager.SessionState[payload.sessionState]; + instance._segmentState = WebInspector.ReplayManager.SegmentState[payload.segmentState]; + instance._currentPosition = payload.replayPosition; + + instance._initialized = true; + }).then(function() { + return ReplayAgent.getAvailableSessions(); + }).then(function(payload) { + for (var sessionId of payload.ids) + instance.sessionCreated(sessionId); + }).catch(function(error) { + console.error("ReplayManager initialization failed: ", error); + throw error; + }); + } + } + + // Public + + // The following state is invalid unless called from a function that's chained + // to the (resolved) ReplayManager.waitUntilInitialized promise. + get sessionState() + { + console.assert(this._initialized); + return this._sessionState; + } + + get segmentState() + { + console.assert(this._initialized); + return this._segmentState; + } + + get activeSessionIdentifier() + { + console.assert(this._initialized); + return this._activeSessionIdentifier; + } + + get activeSegmentIdentifier() + { + console.assert(this._initialized); + return this._activeSegmentIdentifier; + } + + get playbackSpeed() + { + console.assert(this._initialized); + return this._playbackSpeed; + } + + set playbackSpeed(value) + { + console.assert(this._initialized); + this._playbackSpeed = value; + } + + get currentPosition() + { + console.assert(this._initialized); + return this._currentPosition; + } + + // These return promises even if the relevant instance is already created. + waitUntilInitialized() // --> () + { + return this._initializationPromise; + } + + // Return a promise that resolves to a session, if it exists. + getSession(sessionId) // --> (WebInspector.ReplaySession) + { + if (this._sessionPromises.has(sessionId)) + return this._sessionPromises.get(sessionId); + + var newPromise = ReplayAgent.getSessionData(sessionId) + .then(function(payload) { + return Promise.resolve(WebInspector.ReplaySession.fromPayload(sessionId, payload)); + }); + + this._sessionPromises.set(sessionId, newPromise); + return newPromise; + } + + // Return a promise that resolves to a session segment, if it exists. + getSegment(segmentId) // --> (WebInspector.ReplaySessionSegment) + { + if (this._segmentPromises.has(segmentId)) + return this._segmentPromises.get(segmentId); + + var newPromise = ReplayAgent.getSegmentData(segmentId) + .then(function(payload) { + return Promise.resolve(new WebInspector.ReplaySessionSegment(segmentId, payload)); + }); + + this._segmentPromises.set(segmentId, newPromise); + return newPromise; + } + + // Switch to the specified session. + // Returns a promise that resolves when the switch completes. + switchSession(sessionId) // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) { + result = result.then(function() { + return WebInspector.replayManager.cancelPlayback(); + }); + } + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return manager.getSession(sessionId); + }).then(function ensureSessionDataIsLoaded(session) { + return ReplayAgent.switchSession(session.identifier); + }).catch(function(error) { + console.error("Failed to switch to session: ", error); + throw error; + }); + + return result; + } + + // Start capturing into the current session as soon as possible. + // Returns a promise that resolves when capturing begins. + startCapturing() // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) + return result; // Already capturing. + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Replaying) { + result = result.then(function() { + return WebInspector.replayManager.cancelPlayback(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Inactive); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return ReplayAgent.startCapturing(); + }).catch(function(error) { + console.error("Failed to start capturing: ", error); + throw error; + }); + + return result; + } + + // Stop capturing into the current session as soon as possible. + // Returns a promise that resolves when capturing ends. + stopCapturing() // --> () + { + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop capturing unless capture is active."); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.stopCapturing() + .catch(function(error) { + console.error("Failed to stop capturing: ", error); + throw error; + }); + } + + // Pause playback as soon as possible. + // Returns a promise that resolves when playback is paused. + pausePlayback() // --> () + { + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot pause playback while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + return result; // Already stopped. + + if (this.segmentState !== WebInspector.ReplayManager.SegmentState.Dispatching) + return result; // Already stopped. + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching); + + return ReplayAgent.pausePlayback(); + }).catch(function(error) { + console.error("Failed to pause playback: ", error); + throw error; + }); + + return result; + } + + // Pause playback and unload the current session segment as soon as possible. + // Returns a promise that resolves when the current segment is unloaded. + cancelPlayback() // --> () + { + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing, "Cannot stop playback while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + return result; // Already stopped. + + result = result.then(function() { + console.assert(manager.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.cancelPlayback(); + }).catch(function(error) { + console.error("Failed to stop playback: ", error); + throw error; + }); + + return result; + } + + // Replay to the specified position as soon as possible using the current replay speed. + // Returns a promise that resolves when replay has begun (NOT when the position is reached). + replayToPosition(replayPosition) // --> () + { + console.assert(replayPosition instanceof WebInspector.ReplayPosition, "Cannot replay to a position while capturing."); + + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + console.assert(manager.segmentState !== WebInspector.ReplayManager.SegmentState.Appending); + + return ReplayAgent.replayToPosition(replayPosition, manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward); + }).catch(function(error) { + console.error("Failed to start playback to position: ", replayPosition, error); + throw error; + }); + + return result; + } + + // Replay to the end of the session as soon as possible using the current replay speed. + // Returns a promise that resolves when replay has begun (NOT when the end is reached). + replayToCompletion() // --> () + { + var manager = this; + var result = this.waitUntilInitialized(); + + if (this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching) + return result; // Already running. + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Capturing) { + result = result.then(function() { + return WebInspector.replayManager.stopCapturing(); + }); + } + + result = result.then(this._suppressBreakpointsAndResumeIfNeeded()); + + result = result.then(function() { + console.assert(manager.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + console.assert(manager.segmentState === WebInspector.ReplayManager.SegmentState.Loaded || manager.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + return ReplayAgent.replayToCompletion(manager.playbackSpeed === WebInspector.ReplayManager.PlaybackSpeed.FastForward); + }).catch(function(error) { + console.error("Failed to start playback to completion: ", error); + throw error; + }); + + return result; + } + + // Protected (called by ReplayObserver) + + // Since these methods update session and segment state, they depend on the manager + // being properly initialized. So, each function body is prepended with a retry guard. + // This makes call sites simpler and avoids an extra event loop turn in the common case. + + captureStarted() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.captureStarted.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Capturing); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStarted); + } + + captureStopped() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.captureStopped.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.CaptureStopped); + } + + playbackStarted() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackStarted.bind(this)); + + if (this.sessionState === WebInspector.ReplayManager.SessionState.Inactive) + this._changeSessionState(WebInspector.ReplayManager.SessionState.Replaying); + + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Dispatching); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackStarted); + } + + playbackHitPosition(replayPosition, timestamp) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackHitPosition.bind(this, replayPosition, timestamp)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Dispatching); + console.assert(replayPosition instanceof WebInspector.ReplayPosition); + + this._currentPosition = replayPosition; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPositionChanged); + } + + playbackPaused(position) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackPaused.bind(this, position)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackPaused); + } + + playbackFinished() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.playbackFinished.bind(this)); + + this._changeSessionState(WebInspector.ReplayManager.SessionState.Inactive); + console.assert(this.segmentState === WebInspector.ReplayManager.SegmentState.Unloaded); + + if (this._breakpointsWereSuppressed) { + delete this._breakpointsWereSuppressed; + WebInspector.debuggerManager.breakpointsEnabled = true; + } + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.PlaybackFinished); + } + + sessionCreated(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionCreated.bind(this, sessionId)); + + console.assert(!this._sessions.has(sessionId), "Tried to add duplicate session identifier:", sessionId); + var sessionMap = this._sessions; + this.getSession(sessionId) + .then(function(session) { + sessionMap.set(sessionId, session); + }).catch(function(error) { + console.error("Error obtaining session data: ", error); + throw error; + }); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionAdded, {sessionId}); + } + + sessionModified(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionModified.bind(this, sessionId)); + + this.getSession(sessionId).then(function(session) { + session.segmentsChanged(); + }); + } + + sessionRemoved(sessionId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.sessionRemoved.bind(this, sessionId)); + + console.assert(this._sessions.has(sessionId), "Unknown session identifier:", sessionId); + + if (!this._sessionPromises.has(sessionId)) + return; + + var manager = this; + + this.getSession(sessionId) + .catch(function(error) { + // Wait for any outstanding promise to settle so it doesn't get re-added. + }).then(function() { + manager._sessionPromises.delete(sessionId); + var removedSession = manager._sessions.take(sessionId); + console.assert(removedSession); + manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionRemoved, {removedSession}); + }); + } + + segmentCreated(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentCreated.bind(this, segmentId)); + + console.assert(!this._segments.has(segmentId), "Tried to add duplicate segment identifier:", segmentId); + + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Appending); + + // Create a dummy segment, and don't try to load any data for it. It will + // be removed once the segment is complete, and then its data will be fetched. + var incompleteSegment = new WebInspector.IncompleteSessionSegment(segmentId); + this._segments.set(segmentId, incompleteSegment); + this._segmentPromises.set(segmentId, Promise.resolve(incompleteSegment)); + + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentAdded, {segmentIdentifier: segmentId}); + } + + segmentCompleted(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentCompleted.bind(this, segmentId)); + + var placeholderSegment = this._segments.take(segmentId); + console.assert(placeholderSegment instanceof WebInspector.IncompleteSessionSegment); + this._segmentPromises.delete(segmentId); + + var segmentMap = this._segments; + this.getSegment(segmentId) + .then(function(segment) { + segmentMap.set(segmentId, segment); + }).catch(function(error) { + console.error("Error obtaining segment data: ", error); + throw error; + }); + } + + segmentRemoved(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentRemoved.bind(this, segmentId)); + + console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId); + + if (!this._segmentPromises.has(segmentId)) + return; + + var manager = this; + + // Wait for any outstanding promise to settle so it doesn't get re-added. + this.getSegment(segmentId) + .catch(function(error) { + return Promise.resolve(); + }).then(function() { + manager._segmentPromises.delete(segmentId); + var removedSegment = manager._segments.take(segmentId); + console.assert(removedSegment); + manager.dispatchEventToListeners(WebInspector.ReplayManager.Event.SessionSegmentRemoved, {removedSegment}); + }); + } + + segmentLoaded(segmentId) + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentLoaded.bind(this, segmentId)); + + console.assert(this._segments.has(segmentId), "Unknown segment identifier:", segmentId); + + console.assert(this.sessionState !== WebInspector.ReplayManager.SessionState.Capturing); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Loaded); + + var previousIdentifier = this._activeSegmentIdentifier; + this._activeSegmentIdentifier = segmentId; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier}); + } + + segmentUnloaded() + { + if (!this._initialized) + return this.waitUntilInitialized().then(this.segmentUnloaded.bind(this)); + + console.assert(this.sessionState === WebInspector.ReplayManager.SessionState.Replaying); + this._changeSegmentState(WebInspector.ReplayManager.SegmentState.Unloaded); + + var previousIdentifier = this._activeSegmentIdentifier; + this._activeSegmentIdentifier = null; + this.dispatchEventToListeners(WebInspector.ReplayManager.Event.ActiveSegmentChanged, {previousSegmentIdentifier: previousIdentifier}); + } + + // Private + + _changeSessionState(newState) + { + // Warn about no-op state changes. We shouldn't be seeing them. + var isAllowed = this._sessionState !== newState; + + switch (this._sessionState) { + case WebInspector.ReplayManager.SessionState.Capturing: + isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive; + break; + + case WebInspector.ReplayManager.SessionState.Replaying: + isAllowed &= newState === WebInspector.ReplayManager.SessionState.Inactive; + break; + } + + console.assert(isAllowed, "Invalid session state change: ", this._sessionState, " to ", newState); + if (isAllowed) + this._sessionState = newState; + } + + _changeSegmentState(newState) + { + // Warn about no-op state changes. We shouldn't be seeing them. + var isAllowed = this._segmentState !== newState; + + switch (this._segmentState) { + case WebInspector.ReplayManager.SegmentState.Appending: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded; + break; + case WebInspector.ReplayManager.SegmentState.Unloaded: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Appending || newState === WebInspector.ReplayManager.SegmentState.Loaded; + break; + case WebInspector.ReplayManager.SegmentState.Loaded: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Unloaded || newState === WebInspector.ReplayManager.SegmentState.Dispatching; + break; + case WebInspector.ReplayManager.SegmentState.Dispatching: + isAllowed &= newState === WebInspector.ReplayManager.SegmentState.Loaded; + break; + } + + console.assert(isAllowed, "Invalid segment state change: ", this._segmentState, " to ", newState); + if (isAllowed) + this._segmentState = newState; + } + + _suppressBreakpointsAndResumeIfNeeded() + { + var manager = this; + + return new Promise(function(resolve, reject) { + manager._breakpointsWereSuppressed = WebInspector.debuggerManager.breakpointsEnabled; + WebInspector.debuggerManager.breakpointsEnabled = false; + + return WebInspector.debuggerManager.resume(); + }); + } +}; + +WebInspector.ReplayManager.Event = { + CaptureStarted: "replay-manager-capture-started", + CaptureStopped: "replay-manager-capture-stopped", + + PlaybackStarted: "replay-manager-playback-started", + PlaybackPaused: "replay-manager-playback-paused", + PlaybackFinished: "replay-manager-playback-finished", + PlaybackPositionChanged: "replay-manager-play-back-position-changed", + + ActiveSessionChanged: "replay-manager-active-session-changed", + ActiveSegmentChanged: "replay-manager-active-segment-changed", + + SessionSegmentAdded: "replay-manager-session-segment-added", + SessionSegmentRemoved: "replay-manager-session-segment-removed", + + SessionAdded: "replay-manager-session-added", + SessionRemoved: "replay-manager-session-removed", +}; + +WebInspector.ReplayManager.SessionState = { + Capturing: "replay-manager-session-state-capturing", + Inactive: "replay-manager-session-state-inactive", + Replaying: "replay-manager-session-state-replaying", +}; + +WebInspector.ReplayManager.SegmentState = { + Appending: "replay-manager-segment-state-appending", + Unloaded: "replay-manager-segment-state-unloaded", + Loaded: "replay-manager-segment-state-loaded", + Dispatching: "replay-manager-segment-state-dispatching", +}; + +WebInspector.ReplayManager.PlaybackSpeed = { + RealTime: "replay-manager-playback-speed-real-time", + FastForward: "replay-manager-playback-speed-fast-forward", +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/ResourceQueryController.js b/Source/WebInspectorUI/UserInterface/Controllers/ResourceQueryController.js new file mode 100644 index 000000000..171165f32 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/ResourceQueryController.js @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.ResourceQueryController = class ResourceQueryController extends WebInspector.Object +{ + constructor() + { + super(); + + this._resourceDataMap = new Map; + } + + // Public + + addResource(resource) + { + this._resourceDataMap.set(resource, {}); + } + + removeResource(resource) + { + this._resourceDataMap.delete(resource); + } + + reset() + { + this._resourceDataMap.clear(); + } + + executeQuery(query) + { + if (!query || !this._resourceDataMap.size) + return []; + + query = query.removeWhitespace().toLowerCase(); + + let cookie = null; + if (query.includes(":")) { + let [newQuery, lineNumber, columnNumber] = query.split(":"); + query = newQuery; + lineNumber = lineNumber ? parseInt(lineNumber, 10) - 1 : 0; + columnNumber = columnNumber ? parseInt(columnNumber, 10) - 1 : 0; + cookie = {lineNumber, columnNumber}; + } + + let results = []; + for (let [resource, cachedData] of this._resourceDataMap) { + if (!cachedData.searchString) { + let displayName = resource.displayName; + cachedData.searchString = displayName.toLowerCase(); + cachedData.specialCharacterIndices = this._findSpecialCharacterIndices(displayName); + } + + let matches = this._findQueryMatches(query, cachedData.searchString, cachedData.specialCharacterIndices); + if (matches.length) + results.push(new WebInspector.ResourceQueryResult(resource, matches, cookie)); + } + + // Resources are sorted in descending order by rank. Resources of equal + // rank are sorted by display name. + return results.sort((a, b) => { + if (a.rank === b.rank) + return a.resource.displayName.localeCompare(b.resource.displayName); + return b.rank - a.rank; + }); + } + + // Private + + _findQueryMatches(query, searchString, specialCharacterIndices) + { + let matches = []; + let queryIndex = 0; + let searchIndex = 0; + let specialIndex = 0; + let deadBranches = new Array(query.length).fill(Infinity); + let type = WebInspector.ResourceQueryMatch.Type.Special; + + function pushMatch(index) + { + matches.push(new WebInspector.ResourceQueryMatch(type, index, queryIndex)); + searchIndex = index + 1; + queryIndex++; + } + + function matchNextSpecialCharacter() + { + if (specialIndex >= specialCharacterIndices.length) + return false; + + let originalSpecialIndex = specialIndex; + while (specialIndex < specialCharacterIndices.length) { + // Normal character matching can move past special characters, + // so advance the special character index if it's before the + // current search string position. + let index = specialCharacterIndices[specialIndex++]; + if (index < searchIndex) + continue; + + if (query[queryIndex] === searchString[index]) { + pushMatch(index); + return true; + } + } + + specialIndex = originalSpecialIndex; + return false; + } + + function backtrack() + { + while (matches.length) { + queryIndex--; + + let lastMatch = matches.pop(); + if (lastMatch.type !== WebInspector.ResourceQueryMatch.Type.Special) + continue; + + deadBranches[lastMatch.queryIndex] = lastMatch.index; + searchIndex = matches.lastValue ? matches.lastValue.index + 1 : 0; + return true; + } + + return false; + } + + while (queryIndex < query.length && searchIndex < searchString.length) { + if (type === WebInspector.ResourceQueryMatch.Type.Special && !matchNextSpecialCharacter()) + type = WebInspector.ResourceQueryMatch.Type.Normal; + + if (type === WebInspector.ResourceQueryMatch.Type.Normal) { + let index = searchString.indexOf(query[queryIndex], searchIndex); + if (index >= 0 && index < deadBranches[queryIndex]) { + pushMatch(index); + type = WebInspector.ResourceQueryMatch.Type.Special; + } else if (!backtrack()) + return []; + } + } + + if (queryIndex < query.length) + return []; + + return matches; + } + + _findSpecialCharacterIndices(string) + { + if (!string.length) + return []; + + const filenameSeparators = "_.-"; + + // Special characters include the following: + // 1. The first character. + // 2. Uppercase characters that follow a lowercase letter. + // 3. Filename separators and the first character following the separator. + let indices = [0]; + + for (let i = 1; i < string.length; ++i) { + let character = string[i]; + let isSpecial = false; + + if (filenameSeparators.includes(character)) + isSpecial = true; + else { + let previousCharacter = string[i - 1]; + let previousCharacterIsSeparator = filenameSeparators.includes(previousCharacter); + if (previousCharacterIsSeparator) + isSpecial = true; + else if (character.isUpperCase() && previousCharacter.isLowerCase()) + isSpecial = true; + } + + if (isSpecial) + indices.push(i); + } + + return indices; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/RuntimeManager.js b/Source/WebInspectorUI/UserInterface/Controllers/RuntimeManager.js new file mode 100644 index 000000000..a10541422 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/RuntimeManager.js @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.RuntimeManager = class RuntimeManager extends WebInspector.Object +{ + constructor() + { + super(); + + // Enable the RuntimeAgent to receive notification of execution contexts. + RuntimeAgent.enable(); + + this._activeExecutionContext = WebInspector.mainTarget.executionContext; + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ExecutionContextsCleared, this._frameExecutionContextsCleared, this); + } + + // Public + + get activeExecutionContext() + { + return this._activeExecutionContext; + } + + set activeExecutionContext(executionContext) + { + if (this._activeExecutionContext === executionContext) + return; + + this._activeExecutionContext = executionContext; + + this.dispatchEventToListeners(WebInspector.RuntimeManager.Event.ActiveExecutionContextChanged); + } + + evaluateInInspectedWindow(expression, options, callback) + { + let {objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, saveResult, sourceURLAppender} = options; + + includeCommandLineAPI = includeCommandLineAPI || false; + doNotPauseOnExceptionsAndMuteConsole = doNotPauseOnExceptionsAndMuteConsole || false; + returnByValue = returnByValue || false; + generatePreview = generatePreview || false; + saveResult = saveResult || false; + sourceURLAppender = sourceURLAppender || appendWebInspectorSourceURL; + + console.assert(objectGroup, "RuntimeManager.evaluateInInspectedWindow should always be called with an objectGroup"); + console.assert(typeof sourceURLAppender === "function"); + + if (!expression) { + // There is no expression, so the completion should happen against global properties. + expression = "this"; + } else if (/^\s*\{/.test(expression) && /\}\s*$/.test(expression)) { + // Transform {a:1} to ({a:1}) so it is treated like an object literal instead of a block with a label. + expression = "(" + expression + ")"; + } else if (/\bawait\b/.test(expression)) { + // Transform `await <expr>` into an async function assignment. + expression = this._tryApplyAwaitConvenience(expression); + } + + expression = sourceURLAppender(expression); + + let target = this._activeExecutionContext.target; + let executionContextId = this._activeExecutionContext.id; + + if (WebInspector.debuggerManager.activeCallFrame) { + target = WebInspector.debuggerManager.activeCallFrame.target; + executionContextId = target.executionContext.id; + } + + function evalCallback(error, result, wasThrown, savedResultIndex) + { + this.dispatchEventToListeners(WebInspector.RuntimeManager.Event.DidEvaluate, {objectGroup}); + + if (error) { + console.error(error); + callback(null, false); + return; + } + + if (returnByValue) + callback(null, wasThrown, wasThrown ? null : result, savedResultIndex); + else + callback(WebInspector.RemoteObject.fromPayload(result, target), wasThrown, savedResultIndex); + } + + if (WebInspector.debuggerManager.activeCallFrame) { + // COMPATIBILITY (iOS 8): "saveResult" did not exist. + target.DebuggerAgent.evaluateOnCallFrame.invoke({callFrameId: WebInspector.debuggerManager.activeCallFrame.id, expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, saveResult}, evalCallback.bind(this), target.DebuggerAgent); + return; + } + + // COMPATIBILITY (iOS 8): "saveResult" did not exist. + target.RuntimeAgent.evaluate.invoke({expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, contextId: executionContextId, returnByValue, generatePreview, saveResult}, evalCallback.bind(this), target.RuntimeAgent); + } + + saveResult(remoteObject, callback) + { + console.assert(remoteObject instanceof WebInspector.RemoteObject); + + // COMPATIBILITY (iOS 8): Runtime.saveResult did not exist. + if (!RuntimeAgent.saveResult) { + callback(undefined); + return; + } + + function mycallback(error, savedResultIndex) + { + callback(savedResultIndex); + } + + let target = this._activeExecutionContext.target; + let executionContextId = this._activeExecutionContext.id; + + if (remoteObject.objectId) + target.RuntimeAgent.saveResult(remoteObject.asCallArgument(), mycallback); + else + target.RuntimeAgent.saveResult(remoteObject.asCallArgument(), executionContextId, mycallback); + } + + getPropertiesForRemoteObject(objectId, callback) + { + this._activeExecutionContext.target.RuntimeAgent.getProperties(objectId, function(error, result) { + if (error) { + callback(error); + return; + } + + let properties = new Map; + for (let property of result) + properties.set(property.name, property); + + callback(null, properties); + }); + } + + // Private + + _frameExecutionContextsCleared(event) + { + let contexts = event.data.contexts || []; + + let currentContextWasDestroyed = contexts.some((context) => context.id === this._activeExecutionContext.id); + if (currentContextWasDestroyed) + this.activeExecutionContext = WebInspector.mainTarget.executionContext; + } + + _tryApplyAwaitConvenience(originalExpression) + { + let esprimaSyntaxTree; + + // Do not transform if the original code parses just fine. + try { + esprima.parse(originalExpression); + return originalExpression; + } catch (error) { } + + // Do not transform if the async function version does not parse. + try { + esprimaSyntaxTree = esprima.parse("(async function(){" + originalExpression + "})"); + } catch (error) { + return originalExpression; + } + + // Assert expected AST produced by our wrapping code. + console.assert(esprimaSyntaxTree.type === "Program"); + console.assert(esprimaSyntaxTree.body.length === 1); + console.assert(esprimaSyntaxTree.body[0].type === "ExpressionStatement"); + console.assert(esprimaSyntaxTree.body[0].expression.type === "FunctionExpression"); + console.assert(esprimaSyntaxTree.body[0].expression.async); + console.assert(esprimaSyntaxTree.body[0].expression.body.type === "BlockStatement"); + + // Do not transform if there is more than one statement. + let asyncFunctionBlock = esprimaSyntaxTree.body[0].expression.body; + if (asyncFunctionBlock.body.length !== 1) + return originalExpression; + + // Extract the variable name for transformation. + let variableName; + let anonymous = false; + let declarationKind = "var"; + let awaitPortion; + let statement = asyncFunctionBlock.body[0]; + if (statement.type === "ExpressionStatement" + && statement.expression.type === "AwaitExpression") { + // await <expr> + anonymous = true; + } else if (statement.type === "ExpressionStatement" + && statement.expression.type === "AssignmentExpression" + && statement.expression.right.type === "AwaitExpression" + && statement.expression.left.type === "Identifier") { + // x = await <expr> + variableName = statement.expression.left.name; + awaitPortion = originalExpression.substring(originalExpression.indexOf("await")); + } else if (statement.type === "VariableDeclaration" + && statement.declarations.length === 1 + && statement.declarations[0].init.type === "AwaitExpression" + && statement.declarations[0].id.type === "Identifier") { + // var x = await <expr> + variableName = statement.declarations[0].id.name; + declarationKind = statement.kind; + awaitPortion = originalExpression.substring(originalExpression.indexOf("await")); + } else { + // Do not transform if this was not one of the simple supported syntaxes. + return originalExpression; + } + + if (anonymous) { + return ` +(async function() { + try { + let result = ${originalExpression}; + console.info("%o", result); + } catch (e) { + console.error(e); + } +})(); +undefined`; + } + + return `${declarationKind} ${variableName}; +(async function() { + try { + ${variableName} = ${awaitPortion}; + console.info("%o", ${variableName}); + } catch (e) { + console.error(e); + } +})(); +undefined;`; + } +}; + +WebInspector.RuntimeManager.ConsoleObjectGroup = "console"; +WebInspector.RuntimeManager.TopLevelExecutionContextIdentifier = undefined; + +WebInspector.RuntimeManager.Event = { + DidEvaluate: Symbol("runtime-manager-did-evaluate"), + DefaultExecutionContextChanged: Symbol("runtime-manager-default-execution-context-changed"), + ActiveExecutionContextChanged: Symbol("runtime-manager-active-execution-context-changed"), +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/SourceMapManager.js b/Source/WebInspectorUI/UserInterface/Controllers/SourceMapManager.js new file mode 100644 index 000000000..6bb8cefba --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/SourceMapManager.js @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.SourceMapManager = class SourceMapManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._sourceMapURLMap = {}; + this._downloadingSourceMaps = {}; + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + } + + // Public + + sourceMapForURL(sourceMapURL) + { + return this._sourceMapURLMap[sourceMapURL]; + } + + downloadSourceMap(sourceMapURL, baseURL, originalSourceCode) + { + // The baseURL could have come from a "//# sourceURL". Attempt to get a + // reasonable absolute URL for the base by using the main resource's URL. + if (WebInspector.frameResourceManager.mainFrame) + baseURL = absoluteURL(baseURL, WebInspector.frameResourceManager.mainFrame.url); + + if (sourceMapURL.startsWith("data:")) { + this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode); + return; + } + + sourceMapURL = absoluteURL(sourceMapURL, baseURL); + if (!sourceMapURL) + return; + + console.assert(originalSourceCode.url); + if (!originalSourceCode.url) + return; + + // FIXME: <rdar://problem/13265694> Source Maps: Better handle when multiple resources reference the same SourceMap + + if (sourceMapURL in this._sourceMapURLMap) + return; + + if (sourceMapURL in this._downloadingSourceMaps) + return; + + function loadAndParseSourceMap() + { + this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode); + } + + if (!WebInspector.frameResourceManager.mainFrame) { + // If we don't have a main frame, then we are likely in the middle of building the resource tree. + // Delaying until the next runloop is enough in this case to then start loading the source map. + setTimeout(loadAndParseSourceMap.bind(this), 0); + return; + } + + loadAndParseSourceMap.call(this); + } + + // Private + + _loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode) + { + this._downloadingSourceMaps[sourceMapURL] = true; + + function sourceMapLoaded(error, content, mimeType, statusCode) + { + if (error || statusCode >= 400) { + this._loadAndParseFailed(sourceMapURL); + return; + } + + if (content.slice(0, 3) === ")]}") { + var firstNewlineIndex = content.indexOf("\n"); + if (firstNewlineIndex === -1) { + this._loadAndParseFailed(sourceMapURL); + return; + } + + content = content.substring(firstNewlineIndex); + } + + try { + var payload = JSON.parse(content); + var baseURL = sourceMapURL.startsWith("data:") ? originalSourceCode.url : sourceMapURL; + var sourceMap = new WebInspector.SourceMap(baseURL, payload, originalSourceCode); + this._loadAndParseSucceeded(sourceMapURL, sourceMap); + } catch (e) { + this._loadAndParseFailed(sourceMapURL); + } + } + + if (sourceMapURL.startsWith("data:")) { + let {mimeType, base64, data} = parseDataURL(sourceMapURL); + let content = base64 ? atob(data) : data; + sourceMapLoaded.call(this, null, content, mimeType, 0); + return; + } + + // COMPATIBILITY (iOS 7): Network.loadResource did not exist. + // Also, JavaScript Debuggable may reach this. + if (!window.NetworkAgent || !NetworkAgent.loadResource) { + this._loadAndParseFailed(sourceMapURL); + return; + } + + var frameIdentifier = null; + if (originalSourceCode instanceof WebInspector.Resource && originalSourceCode.parentFrame) + frameIdentifier = originalSourceCode.parentFrame.id; + + if (!frameIdentifier) + frameIdentifier = WebInspector.frameResourceManager.mainFrame.id; + + NetworkAgent.loadResource(frameIdentifier, sourceMapURL, sourceMapLoaded.bind(this)); + } + + _loadAndParseFailed(sourceMapURL) + { + delete this._downloadingSourceMaps[sourceMapURL]; + } + + _loadAndParseSucceeded(sourceMapURL, sourceMap) + { + if (!(sourceMapURL in this._downloadingSourceMaps)) + return; + + delete this._downloadingSourceMaps[sourceMapURL]; + + this._sourceMapURLMap[sourceMapURL] = sourceMap; + + var sources = sourceMap.sources(); + for (var i = 0; i < sources.length; ++i) { + var sourceMapResource = new WebInspector.SourceMapResource(sources[i], sourceMap); + sourceMap.addResource(sourceMapResource); + } + + // Associate the SourceMap with the originalSourceCode. + sourceMap.originalSourceCode.addSourceMap(sourceMap); + + // If the originalSourceCode was not a Resource, be sure to also associate with the Resource if one exists. + // FIXME: We should try to use the right frame instead of a global lookup by URL. + if (!(sourceMap.originalSourceCode instanceof WebInspector.Resource)) { + console.assert(sourceMap.originalSourceCode instanceof WebInspector.Script); + var resource = sourceMap.originalSourceCode.resource; + if (resource) + resource.addSourceMap(sourceMap); + } + } + + _mainResourceDidChange(event) + { + if (!event.target.isMainFrame()) + return; + + this._sourceMapURLMap = {}; + this._downloadingSourceMaps = {}; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/StorageManager.js b/Source/WebInspectorUI/UserInterface/Controllers/StorageManager.js new file mode 100644 index 000000000..a267dcb85 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/StorageManager.js @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * Copyright (C) 2013 Samsung Electronics. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.StorageManager = class StorageManager extends WebInspector.Object +{ + constructor() + { + super(); + + if (window.DOMStorageAgent) + DOMStorageAgent.enable(); + if (window.DatabaseAgent) + DatabaseAgent.enable(); + if (window.IndexedDBAgent) + IndexedDBAgent.enable(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.SecurityOriginDidChange, this._securityOriginDidChange, this); + + this.initialize(); + } + + // Public + + initialize() + { + this._domStorageObjects = []; + this._databaseObjects = []; + this._indexedDatabases = []; + this._cookieStorageObjects = {}; + } + + get domStorageObjects() + { + return this._domStorageObjects; + } + + get databases() + { + return this._databaseObjects; + } + + get indexedDatabases() + { + return this._indexedDatabases; + } + + get cookieStorageObjects() + { + var cookieStorageObjects = []; + for (var host in this._cookieStorageObjects) + cookieStorageObjects.push(this._cookieStorageObjects[host]); + return cookieStorageObjects; + } + + domStorageWasAdded(id, host, isLocalStorage) + { + var domStorage = new WebInspector.DOMStorageObject(id, host, isLocalStorage); + + this._domStorageObjects.push(domStorage); + this.dispatchEventToListeners(WebInspector.StorageManager.Event.DOMStorageObjectWasAdded, {domStorage}); + } + + databaseWasAdded(id, host, name, version) + { + var database = new WebInspector.DatabaseObject(id, host, name, version); + + this._databaseObjects.push(database); + this.dispatchEventToListeners(WebInspector.StorageManager.Event.DatabaseWasAdded, {database}); + } + + itemsCleared(storageId) + { + let domStorage = this._domStorageForIdentifier(storageId); + if (domStorage) + domStorage.itemsCleared(storageId); + } + + itemRemoved(storageId, key) + { + let domStorage = this._domStorageForIdentifier(storageId); + if (domStorage) + domStorage.itemRemoved(key); + } + + itemAdded(storageId, key, value) + { + let domStorage = this._domStorageForIdentifier(storageId); + if (domStorage) + domStorage.itemAdded(key, value); + } + + itemUpdated(storageId, key, oldValue, value) + { + let domStorage = this._domStorageForIdentifier(storageId); + if (domStorage) + domStorage.itemUpdated(key, oldValue, value); + } + + inspectDatabase(id) + { + var database = this._databaseForIdentifier(id); + console.assert(database); + if (!database) + return; + this.dispatchEventToListeners(WebInspector.StorageManager.Event.DatabaseWasInspected, {database}); + } + + inspectDOMStorage(id) + { + var domStorage = this._domStorageForIdentifier(id); + console.assert(domStorage); + if (!domStorage) + return; + this.dispatchEventToListeners(WebInspector.StorageManager.Event.DOMStorageObjectWasInspected, {domStorage}); + } + + requestIndexedDatabaseData(objectStore, objectStoreIndex, startEntryIndex, maximumEntryCount, callback) + { + console.assert(window.IndexedDBAgent); + console.assert(objectStore); + console.assert(callback); + + function processData(error, entryPayloads, moreAvailable) + { + if (error) { + callback(null, false); + return; + } + + var entries = []; + + for (var entryPayload of entryPayloads) { + var entry = {}; + entry.primaryKey = WebInspector.RemoteObject.fromPayload(entryPayload.primaryKey); + entry.key = WebInspector.RemoteObject.fromPayload(entryPayload.key); + entry.value = WebInspector.RemoteObject.fromPayload(entryPayload.value); + entries.push(entry); + } + + callback(entries, moreAvailable); + } + + var requestArguments = { + securityOrigin: objectStore.parentDatabase.securityOrigin, + databaseName: objectStore.parentDatabase.name, + objectStoreName: objectStore.name, + indexName: objectStoreIndex && objectStoreIndex.name || "", + skipCount: startEntryIndex || 0, + pageSize: maximumEntryCount || 100 + }; + + IndexedDBAgent.requestData.invoke(requestArguments, processData); + } + + clearObjectStore(objectStore) + { + let securityOrigin = objectStore.parentDatabase.securityOrigin; + let databaseName = objectStore.parentDatabase.name; + let objectStoreName = objectStore.name; + + IndexedDBAgent.clearObjectStore(securityOrigin, databaseName, objectStoreName); + } + + // Private + + _domStorageForIdentifier(id) + { + for (var storageObject of this._domStorageObjects) { + // The id is an object, so we need to compare the properties using Object.shallowEqual. + if (Object.shallowEqual(storageObject.id, id)) + return storageObject; + } + + return null; + } + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + if (event.target.isMainFrame()) { + // If we are dealing with the main frame, we want to clear our list of objects, because we are navigating to a new page. + this.initialize(); + this.dispatchEventToListeners(WebInspector.StorageManager.Event.Cleared); + + this._addDOMStorageIfNeeded(event.target); + this._addIndexedDBDatabasesIfNeeded(event.target); + } + + // Add the host of the frame that changed the main resource to the list of hosts there could be cookies for. + var host = parseURL(event.target.url).host; + if (!host) + return; + + if (this._cookieStorageObjects[host]) + return; + + this._cookieStorageObjects[host] = new WebInspector.CookieStorageObject(host); + this.dispatchEventToListeners(WebInspector.StorageManager.Event.CookieStorageObjectWasAdded, {cookieStorage: this._cookieStorageObjects[host]}); + } + + _addDOMStorageIfNeeded(frame) + { + if (!window.DOMStorageAgent) + return; + + // Don't show storage if we don't have a security origin (about:blank). + if (!frame.securityOrigin || frame.securityOrigin === "://") + return; + + // FIXME: Consider passing the other parts of the origin along to domStorageWasAdded. + + var localStorageIdentifier = {securityOrigin: frame.securityOrigin, isLocalStorage: true}; + if (!this._domStorageForIdentifier(localStorageIdentifier)) + this.domStorageWasAdded(localStorageIdentifier, frame.mainResource.urlComponents.host, true); + + var sessionStorageIdentifier = {securityOrigin: frame.securityOrigin, isLocalStorage: false}; + if (!this._domStorageForIdentifier(sessionStorageIdentifier)) + this.domStorageWasAdded(sessionStorageIdentifier, frame.mainResource.urlComponents.host, false); + } + + _addIndexedDBDatabasesIfNeeded(frame) + { + if (!window.IndexedDBAgent) + return; + + var securityOrigin = frame.securityOrigin; + + // Don't show storage if we don't have a security origin (about:blank). + if (!securityOrigin || securityOrigin === "://") + return; + + function processDatabaseNames(error, names) + { + if (error || !names) + return; + + for (var name of names) + IndexedDBAgent.requestDatabase(securityOrigin, name, processDatabase.bind(this)); + } + + function processDatabase(error, databasePayload) + { + if (error || !databasePayload) + return; + + var objectStores = databasePayload.objectStores.map(processObjectStore); + var indexedDatabase = new WebInspector.IndexedDatabase(databasePayload.name, securityOrigin, databasePayload.version, objectStores); + + this._indexedDatabases.push(indexedDatabase); + this.dispatchEventToListeners(WebInspector.StorageManager.Event.IndexedDatabaseWasAdded, {indexedDatabase}); + } + + function processKeyPath(keyPathPayload) + { + switch (keyPathPayload.type) { + case IndexedDBAgent.KeyPathType.Null: + return null; + case IndexedDBAgent.KeyPathType.String: + return keyPathPayload.string; + case IndexedDBAgent.KeyPathType.Array: + return keyPathPayload.array; + default: + console.error("Unknown KeyPath type:", keyPathPayload.type); + return null; + } + } + + function processObjectStore(objectStorePayload) + { + var keyPath = processKeyPath(objectStorePayload.keyPath); + var indexes = objectStorePayload.indexes.map(processObjectStoreIndex); + return new WebInspector.IndexedDatabaseObjectStore(objectStorePayload.name, keyPath, objectStorePayload.autoIncrement, indexes); + } + + function processObjectStoreIndex(objectStoreIndexPayload) + { + var keyPath = processKeyPath(objectStoreIndexPayload.keyPath); + return new WebInspector.IndexedDatabaseObjectStoreIndex(objectStoreIndexPayload.name, keyPath, objectStoreIndexPayload.unique, objectStoreIndexPayload.multiEntry); + } + + IndexedDBAgent.requestDatabaseNames(securityOrigin, processDatabaseNames.bind(this)); + } + + _securityOriginDidChange(event) + { + console.assert(event.target instanceof WebInspector.Frame); + + this._addDOMStorageIfNeeded(event.target); + this._addIndexedDBDatabasesIfNeeded(event.target); + } + + _databaseForIdentifier(id) + { + for (var i = 0; i < this._databaseObjects.length; ++i) { + if (this._databaseObjects[i].id === id) + return this._databaseObjects[i]; + } + + return null; + } +}; + +WebInspector.StorageManager.Event = { + CookieStorageObjectWasAdded: "storage-manager-cookie-storage-object-was-added", + DOMStorageObjectWasAdded: "storage-manager-dom-storage-object-was-added", + DOMStorageObjectWasInspected: "storage-dom-object-was-inspected", + DatabaseWasAdded: "storage-manager-database-was-added", + DatabaseWasInspected: "storage-object-was-inspected", + IndexedDatabaseWasAdded: "storage-manager-indexed-database-was-added", + Cleared: "storage-manager-cleared" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/TargetManager.js b/Source/WebInspectorUI/UserInterface/Controllers/TargetManager.js new file mode 100644 index 000000000..7e987d4dd --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/TargetManager.js @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.TargetManager = class TargetManager extends WebInspector.Object +{ + constructor() + { + super(); + + console.assert(WebInspector.mainTarget); + + this._targets = new Set([WebInspector.mainTarget]); + } + + // Public + + get targets() + { + return this._targets; + } + + targetForIdentifier(targetId) + { + if (!targetId) + return null; + + for (let target of this._targets) { + if (target.identifier === targetId) + return target; + } + + return null; + } + + addTarget(target) + { + this._targets.add(target); + + this.dispatchEventToListeners(WebInspector.TargetManager.Event.TargetAdded, {target}); + } + + removeTarget(target) + { + this._targets.delete(target); + + this.dispatchEventToListeners(WebInspector.TargetManager.Event.TargetRemoved, {target}); + } +}; + +WebInspector.TargetManager.Event = { + TargetAdded: Symbol("target-manager-target-added"), + TargetRemoved: Symbol("target-manager-target-removed"), +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js b/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js new file mode 100644 index 000000000..e03ba9ba2 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js @@ -0,0 +1,1091 @@ +/* + * Copyright (C) 2013, 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.TimelineManager = class TimelineManager extends WebInspector.Object +{ + constructor() + { + super(); + + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this); + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this); + WebInspector.Target.addEventListener(WebInspector.Target.Event.ResourceAdded, this._resourceWasAdded, this); + + WebInspector.heapManager.addEventListener(WebInspector.HeapManager.Event.GarbageCollected, this._garbageCollected, this); + WebInspector.memoryManager.addEventListener(WebInspector.MemoryManager.Event.MemoryPressure, this._memoryPressure, this); + + this._enabledTimelineTypesSetting = new WebInspector.Setting("enabled-instrument-types", WebInspector.TimelineManager.defaultTimelineTypes()); + this._updateAutoCaptureInstruments(); + + this._persistentNetworkTimeline = new WebInspector.NetworkTimeline; + + this._isCapturing = false; + this._initiatedByBackendStart = false; + this._initiatedByBackendStop = false; + this._waitingForCapturingStartedEvent = false; + this._isCapturingPageReload = false; + this._autoCaptureOnPageLoad = false; + this._mainResourceForAutoCapturing = null; + this._shouldSetAutoCapturingMainResource = false; + this._boundStopCapturing = this.stopCapturing.bind(this); + + this._webTimelineScriptRecordsExpectingScriptProfilerEvents = null; + this._scriptProfilerRecords = null; + + this._stopCapturingTimeout = undefined; + this._deadTimeTimeout = undefined; + this._lastDeadTimeTickle = 0; + + this.reset(); + } + + // Static + + static defaultTimelineTypes() + { + if (WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript) { + let defaultTypes = [WebInspector.TimelineRecord.Type.Script]; + if (WebInspector.HeapAllocationsInstrument.supported()) + defaultTypes.push(WebInspector.TimelineRecord.Type.HeapAllocations); + return defaultTypes; + } + + let defaultTypes = [ + WebInspector.TimelineRecord.Type.Network, + WebInspector.TimelineRecord.Type.Layout, + WebInspector.TimelineRecord.Type.Script, + ]; + + if (WebInspector.FPSInstrument.supported()) + defaultTypes.push(WebInspector.TimelineRecord.Type.RenderingFrame); + + return defaultTypes; + } + + static availableTimelineTypes() + { + let types = WebInspector.TimelineManager.defaultTimelineTypes(); + if (WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript) + return types; + + if (WebInspector.MemoryInstrument.supported()) + types.push(WebInspector.TimelineRecord.Type.Memory); + + if (WebInspector.HeapAllocationsInstrument.supported()) + types.push(WebInspector.TimelineRecord.Type.HeapAllocations); + + return types; + } + + // Public + + reset() + { + if (this._isCapturing) + this.stopCapturing(); + + this._recordings = []; + this._activeRecording = null; + this._nextRecordingIdentifier = 1; + + this._loadNewRecording(); + } + + // The current recording that new timeline records will be appended to, if any. + get activeRecording() + { + console.assert(this._activeRecording || !this._isCapturing); + return this._activeRecording; + } + + get persistentNetworkTimeline() + { + return this._persistentNetworkTimeline; + } + + get recordings() + { + return this._recordings.slice(); + } + + get autoCaptureOnPageLoad() + { + return this._autoCaptureOnPageLoad; + } + + set autoCaptureOnPageLoad(autoCapture) + { + autoCapture = !!autoCapture; + + if (this._autoCaptureOnPageLoad === autoCapture) + return; + + this._autoCaptureOnPageLoad = autoCapture; + + if (window.TimelineAgent && TimelineAgent.setAutoCaptureEnabled) + TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad); + } + + get enabledTimelineTypes() + { + let availableTimelineTypes = WebInspector.TimelineManager.availableTimelineTypes(); + return this._enabledTimelineTypesSetting.value.filter((type) => availableTimelineTypes.includes(type)); + } + + set enabledTimelineTypes(x) + { + this._enabledTimelineTypesSetting.value = x || []; + + this._updateAutoCaptureInstruments(); + } + + isCapturing() + { + return this._isCapturing; + } + + isCapturingPageReload() + { + return this._isCapturingPageReload; + } + + startCapturing(shouldCreateRecording) + { + console.assert(!this._isCapturing, "TimelineManager is already capturing."); + + if (!this._activeRecording || shouldCreateRecording) + this._loadNewRecording(); + + this._waitingForCapturingStartedEvent = true; + + this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingWillStart); + + this._activeRecording.start(this._initiatedByBackendStart); + } + + stopCapturing() + { + console.assert(this._isCapturing, "TimelineManager is not capturing."); + + this._activeRecording.stop(this._initiatedByBackendStop); + + // NOTE: Always stop immediately instead of waiting for a Timeline.recordingStopped event. + // This way the UI feels as responsive to a stop as possible. + // FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records + this.capturingStopped(); + } + + unloadRecording() + { + if (!this._activeRecording) + return; + + if (this._isCapturing) + this.stopCapturing(); + + this._activeRecording.unloaded(); + this._activeRecording = null; + } + + computeElapsedTime(timestamp) + { + if (!this._activeRecording) + return 0; + + return this._activeRecording.computeElapsedTime(timestamp); + } + + scriptProfilerIsTracking() + { + return this._scriptProfilerRecords !== null; + } + + // Protected + + capturingStarted(startTime) + { + // Called from WebInspector.TimelineObserver. + + if (this._isCapturing) + return; + + this._waitingForCapturingStartedEvent = false; + this._isCapturing = true; + + this._lastDeadTimeTickle = 0; + + if (startTime) + this.activeRecording.initializeTimeBoundsIfNecessary(startTime); + + this._webTimelineScriptRecordsExpectingScriptProfilerEvents = []; + + this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStarted, {startTime}); + } + + capturingStopped(endTime) + { + // Called from WebInspector.TimelineObserver. + + if (!this._isCapturing) + return; + + if (this._stopCapturingTimeout) { + clearTimeout(this._stopCapturingTimeout); + this._stopCapturingTimeout = undefined; + } + + if (this._deadTimeTimeout) { + clearTimeout(this._deadTimeTimeout); + this._deadTimeTimeout = undefined; + } + + this._isCapturing = false; + this._isCapturingPageReload = false; + this._shouldSetAutoCapturingMainResource = false; + this._mainResourceForAutoCapturing = null; + this._initiatedByBackendStart = false; + this._initiatedByBackendStop = false; + + this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStopped, {endTime}); + } + + autoCaptureStarted() + { + // Called from WebInspector.TimelineObserver. + + if (this._isCapturing) + this.stopCapturing(); + + this._initiatedByBackendStart = true; + + // We may already have an fresh TimelineRecording created if autoCaptureStarted is received + // between sending the Timeline.start command and receiving Timeline.capturingStarted event. + // In that case, there is no need to call startCapturing again. Reuse the fresh recording. + if (!this._waitingForCapturingStartedEvent) { + const createNewRecording = true; + this.startCapturing(createNewRecording); + } + + this._shouldSetAutoCapturingMainResource = true; + } + + programmaticCaptureStarted() + { + // Called from WebInspector.TimelineObserver. + + this._initiatedByBackendStart = true; + + this._activeRecording.addScriptInstrumentForProgrammaticCapture(); + + const createNewRecording = false; + this.startCapturing(createNewRecording); + } + + programmaticCaptureStopped() + { + // Called from WebInspector.TimelineObserver. + + this._initiatedByBackendStop = true; + + // FIXME: This is purely to avoid a noisy assert. Previously + // it was impossible to stop without stopping from the UI. + console.assert(!this._isCapturing); + this._isCapturing = true; + + this.stopCapturing(); + } + + eventRecorded(recordPayload) + { + // Called from WebInspector.TimelineObserver. + + if (!this._isCapturing) + return; + + var records = []; + + // Iterate over the records tree using a stack. Doing this recursively has + // been known to cause a call stack overflow. https://webkit.org/b/79106 + var stack = [{array: [recordPayload], parent: null, parentRecord: null, index: 0}]; + while (stack.length) { + var entry = stack.lastValue; + var recordPayloads = entry.array; + + if (entry.index < recordPayloads.length) { + var recordPayload = recordPayloads[entry.index]; + var record = this._processEvent(recordPayload, entry.parent); + if (record) { + record.parent = entry.parentRecord; + records.push(record); + if (entry.parentRecord) + entry.parentRecord.children.push(record); + } + + if (recordPayload.children && recordPayload.children.length) + stack.push({array: recordPayload.children, parent: recordPayload, parentRecord: record || entry.parentRecord, index: 0}); + ++entry.index; + } else + stack.pop(); + } + + for (var record of records) { + if (record.type === WebInspector.TimelineRecord.Type.RenderingFrame) { + if (!record.children.length) + continue; + record.setupFrameIndex(); + } + + this._addRecord(record); + } + } + + // Protected + + pageDOMContentLoadedEventFired(timestamp) + { + // Called from WebInspector.PageObserver. + + console.assert(this._activeRecording); + console.assert(isNaN(WebInspector.frameResourceManager.mainFrame.domContentReadyEventTimestamp)); + + let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp); + + WebInspector.frameResourceManager.mainFrame.markDOMContentReadyEvent(computedTimestamp); + + let eventMarker = new WebInspector.TimelineMarker(computedTimestamp, WebInspector.TimelineMarker.Type.DOMContentEvent); + this._activeRecording.addEventMarker(eventMarker); + } + + pageLoadEventFired(timestamp) + { + // Called from WebInspector.PageObserver. + + console.assert(this._activeRecording); + console.assert(isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp)); + + let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp); + + WebInspector.frameResourceManager.mainFrame.markLoadEvent(computedTimestamp); + + let eventMarker = new WebInspector.TimelineMarker(computedTimestamp, WebInspector.TimelineMarker.Type.LoadEvent); + this._activeRecording.addEventMarker(eventMarker); + + this._stopAutoRecordingSoon(); + } + + memoryTrackingStart(timestamp) + { + // Called from WebInspector.MemoryObserver. + + this.capturingStarted(timestamp); + } + + memoryTrackingUpdate(event) + { + // Called from WebInspector.MemoryObserver. + + if (!this._isCapturing) + return; + + this._addRecord(new WebInspector.MemoryTimelineRecord(event.timestamp, event.categories)); + } + + memoryTrackingComplete() + { + // Called from WebInspector.MemoryObserver. + } + + heapTrackingStarted(timestamp, snapshot) + { + // Called from WebInspector.HeapObserver. + + this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot)); + + this.capturingStarted(timestamp); + } + + heapTrackingCompleted(timestamp, snapshot) + { + // Called from WebInspector.HeapObserver. + + this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot)); + } + + heapSnapshotAdded(timestamp, snapshot) + { + // Called from WebInspector.HeapAllocationsInstrument. + + this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot)); + } + + // Private + + _processRecord(recordPayload, parentRecordPayload) + { + var startTime = this.activeRecording.computeElapsedTime(recordPayload.startTime); + var endTime = this.activeRecording.computeElapsedTime(recordPayload.endTime); + var callFrames = this._callFramesFromPayload(recordPayload.stackTrace); + + var significantCallFrame = null; + if (callFrames) { + for (var i = 0; i < callFrames.length; ++i) { + if (callFrames[i].nativeCode) + continue; + significantCallFrame = callFrames[i]; + break; + } + } + + var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation; + + switch (recordPayload.type) { + case TimelineAgent.EventType.ScheduleStyleRecalculation: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation); + + case TimelineAgent.EventType.RecalculateStyles: + return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation); + + case TimelineAgent.EventType.InvalidateLayout: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation); + + case TimelineAgent.EventType.Layout: + var layoutRecordType = sourceCodeLocation ? WebInspector.LayoutTimelineRecord.EventType.ForcedLayout : WebInspector.LayoutTimelineRecord.EventType.Layout; + var quad = new WebInspector.Quad(recordPayload.data.root); + return new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation, quad); + + case TimelineAgent.EventType.Paint: + var quad = new WebInspector.Quad(recordPayload.data.clip); + return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, quad); + + case TimelineAgent.EventType.Composite: + return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Composite, startTime, endTime, callFrames, sourceCodeLocation); + + case TimelineAgent.EventType.RenderingFrame: + if (!recordPayload.children || !recordPayload.children.length) + return null; + + return new WebInspector.RenderingFrameTimelineRecord(startTime, endTime); + + case TimelineAgent.EventType.EvaluateScript: + if (!sourceCodeLocation) { + var mainFrame = WebInspector.frameResourceManager.mainFrame; + var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true); + if (scriptResource) { + // The lineNumber is 1-based, but we expect 0-based. + var lineNumber = recordPayload.data.lineNumber - 1; + + // FIXME: No column number is provided. + sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0); + } + } + + var profileData = recordPayload.data.profile; + + var record; + switch (parentRecordPayload && parentRecordPayload.type) { + case TimelineAgent.EventType.TimerFire: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData); + break; + default: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null, profileData); + break; + } + + this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record); + return record; + + case TimelineAgent.EventType.ConsoleProfile: + var profileData = recordPayload.data.profile; + // COMPATIBILITY (iOS 9): With the Sampling Profiler, profiles no longer include legacy profile data. + console.assert(profileData || TimelineAgent.setInstruments); + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ConsoleProfileRecorded, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.title, profileData); + + case TimelineAgent.EventType.TimerFire: + case TimelineAgent.EventType.EventDispatch: + case TimelineAgent.EventType.FireAnimationFrame: + // These are handled when the parent of FunctionCall or EvaluateScript. + break; + + case TimelineAgent.EventType.FunctionCall: + // FunctionCall always happens as a child of another record, and since the FunctionCall record + // has useful info we just make the timeline record here (combining the data from both records). + if (!parentRecordPayload) { + console.warn("Unexpectedly received a FunctionCall timeline record without a parent record"); + break; + } + + if (!sourceCodeLocation) { + var mainFrame = WebInspector.frameResourceManager.mainFrame; + var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true); + if (scriptResource) { + // The lineNumber is 1-based, but we expect 0-based. + var lineNumber = recordPayload.data.scriptLine - 1; + + // FIXME: No column number is provided. + sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0); + } + } + + var profileData = recordPayload.data.profile; + + var record; + switch (parentRecordPayload.type) { + case TimelineAgent.EventType.TimerFire: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData); + break; + case TimelineAgent.EventType.EventDispatch: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type, profileData); + break; + case TimelineAgent.EventType.FireAnimationFrame: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData); + break; + case TimelineAgent.EventType.FunctionCall: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData); + break; + case TimelineAgent.EventType.RenderingFrame: + record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData); + break; + + default: + console.assert(false, "Missed FunctionCall embedded inside of: " + parentRecordPayload.type); + break; + } + + if (record) { + this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record); + return record; + } + break; + + case TimelineAgent.EventType.ProbeSample: + // Pass the startTime as the endTime since this record type has no duration. + sourceCodeLocation = WebInspector.probeManager.probeForIdentifier(recordPayload.data.probeId).breakpoint.sourceCodeLocation; + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ProbeSampleRecorded, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.probeId); + + case TimelineAgent.EventType.TimerInstall: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + var timerDetails = {timerId: recordPayload.data.timerId, timeout: recordPayload.data.timeout, repeating: !recordPayload.data.singleShot}; + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, timerDetails); + + case TimelineAgent.EventType.TimerRemove: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId); + + case TimelineAgent.EventType.RequestAnimationFrame: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.id); + + case TimelineAgent.EventType.CancelAnimationFrame: + console.assert(isNaN(endTime)); + + // Pass the startTime as the endTime since this record type has no duration. + return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.id); + + default: + console.error("Missing handling of Timeline Event Type: " + recordPayload.type); + } + + return null; + } + + _processEvent(recordPayload, parentRecordPayload) + { + switch (recordPayload.type) { + case TimelineAgent.EventType.TimeStamp: + var timestamp = this.activeRecording.computeElapsedTime(recordPayload.startTime); + var eventMarker = new WebInspector.TimelineMarker(timestamp, WebInspector.TimelineMarker.Type.TimeStamp, recordPayload.data.message); + this._activeRecording.addEventMarker(eventMarker); + break; + + case TimelineAgent.EventType.Time: + case TimelineAgent.EventType.TimeEnd: + // FIXME: <https://webkit.org/b/150690> Web Inspector: Show console.time/timeEnd ranges in Timeline + // FIXME: Make use of "message" payload properties. + break; + + default: + return this._processRecord(recordPayload, parentRecordPayload); + } + + return null; + } + + _loadNewRecording() + { + if (this._activeRecording && this._activeRecording.isEmpty()) + return; + + let instruments = this.enabledTimelineTypes.map((type) => WebInspector.Instrument.createForTimelineType(type)); + let identifier = this._nextRecordingIdentifier++; + let newRecording = new WebInspector.TimelineRecording(identifier, WebInspector.UIString("Timeline Recording %d").format(identifier), instruments); + + this._recordings.push(newRecording); + this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingCreated, {recording: newRecording}); + + if (this._isCapturing) + this.stopCapturing(); + + var oldRecording = this._activeRecording; + if (oldRecording) + oldRecording.unloaded(); + + this._activeRecording = newRecording; + + // COMPATIBILITY (iOS 8): When using Legacy timestamps, a navigation will have computed + // the main resource's will send request timestamp in terms of the last page's base timestamp. + // Now that we have navigated, we should reset the legacy base timestamp and the + // will send request timestamp for the new main resource. This way, all new timeline + // records will be computed relative to the new navigation. + if (this._mainResourceForAutoCapturing && WebInspector.TimelineRecording.isLegacy) { + console.assert(this._mainResourceForAutoCapturing.originalRequestWillBeSentTimestamp); + this._activeRecording.setLegacyBaseTimestamp(this._mainResourceForAutoCapturing.originalRequestWillBeSentTimestamp); + this._mainResourceForAutoCapturing._requestSentTimestamp = 0; + } + + this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingLoaded, {oldRecording}); + } + + _callFramesFromPayload(payload) + { + if (!payload) + return null; + + return payload.map((x) => WebInspector.CallFrame.fromPayload(WebInspector.assumingMainTarget(), x)); + } + + _addRecord(record) + { + this._activeRecording.addRecord(record); + + // Only worry about dead time after the load event. + if (WebInspector.frameResourceManager.mainFrame && isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp)) + this._resetAutoRecordingDeadTimeTimeout(); + } + + _attemptAutoCapturingForFrame(frame) + { + if (!this._autoCaptureOnPageLoad) + return false; + + if (!frame.isMainFrame()) + return false; + + // COMPATIBILITY (iOS 9): Timeline.setAutoCaptureEnabled did not exist. + // Perform auto capture in the frontend. + if (!TimelineAgent.setAutoCaptureEnabled) + return this._legacyAttemptStartAutoCapturingForFrame(frame); + + if (!this._shouldSetAutoCapturingMainResource) + return false; + + console.assert(this._isCapturing, "We saw autoCaptureStarted so we should already be capturing"); + + let mainResource = frame.provisionalMainResource || frame.mainResource; + if (mainResource === this._mainResourceForAutoCapturing) + return false; + + let oldMainResource = frame.mainResource || null; + this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url; + + this._mainResourceForAutoCapturing = mainResource; + + this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource)); + + this._resetAutoRecordingMaxTimeTimeout(); + + this._shouldSetAutoCapturingMainResource = false; + + return true; + } + + _legacyAttemptStartAutoCapturingForFrame(frame) + { + if (this._isCapturing && !this._mainResourceForAutoCapturing) + return false; + + let mainResource = frame.provisionalMainResource || frame.mainResource; + if (mainResource === this._mainResourceForAutoCapturing) + return false; + + let oldMainResource = frame.mainResource || null; + this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url; + + if (this._isCapturing) + this.stopCapturing(); + + this._mainResourceForAutoCapturing = mainResource; + + this._loadNewRecording(); + + this.startCapturing(); + + this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource)); + + this._resetAutoRecordingMaxTimeTimeout(); + + return true; + } + + _stopAutoRecordingSoon() + { + // Only auto stop when auto capturing. + if (!this._isCapturing || !this._mainResourceForAutoCapturing) + return; + + if (this._stopCapturingTimeout) + clearTimeout(this._stopCapturingTimeout); + this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent); + } + + _resetAutoRecordingMaxTimeTimeout() + { + if (this._stopCapturingTimeout) + clearTimeout(this._stopCapturingTimeout); + this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDuration); + } + + _resetAutoRecordingDeadTimeTimeout() + { + // Only monitor dead time when auto capturing. + if (!this._isCapturing || !this._mainResourceForAutoCapturing) + return; + + // Avoid unnecessary churning of timeout identifier by not tickling until 10ms have passed. + let now = Date.now(); + if (now <= this._lastDeadTimeTickle) + return; + this._lastDeadTimeTickle = now + 10; + + if (this._deadTimeTimeout) + clearTimeout(this._deadTimeTimeout); + this._deadTimeTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly); + } + + _provisionalLoadStarted(event) + { + this._attemptAutoCapturingForFrame(event.target); + } + + _mainResourceDidChange(event) + { + let frame = event.target; + if (frame.isMainFrame() && WebInspector.settings.clearNetworkOnNavigate.value) + this._persistentNetworkTimeline.reset(); + + let mainResource = frame.mainResource; + let record = new WebInspector.ResourceTimelineRecord(mainResource); + if (!isNaN(record.startTime)) + this._persistentNetworkTimeline.addRecord(record); + + // Ignore resource events when there isn't a main frame yet. Those events are triggered by + // loading the cached resources when the inspector opens, and they do not have timing information. + if (!WebInspector.frameResourceManager.mainFrame) + return; + + if (this._attemptAutoCapturingForFrame(frame)) + return; + + if (!this._isCapturing) + return; + + if (mainResource === this._mainResourceForAutoCapturing) + return; + + this._addRecord(record); + } + + _resourceWasAdded(event) + { + var record = new WebInspector.ResourceTimelineRecord(event.data.resource); + if (!isNaN(record.startTime)) + this._persistentNetworkTimeline.addRecord(record); + + // Ignore resource events when there isn't a main frame yet. Those events are triggered by + // loading the cached resources when the inspector opens, and they do not have timing information. + if (!WebInspector.frameResourceManager.mainFrame) + return; + + if (!this._isCapturing) + return; + + this._addRecord(record); + } + + _garbageCollected(event) + { + if (!this._isCapturing) + return; + + let collection = event.data.collection; + this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.GarbageCollected, collection.startTime, collection.endTime, null, null, collection)); + } + + _memoryPressure(event) + { + if (!this._isCapturing) + return; + + this.activeRecording.addMemoryPressureEvent(event.data.memoryPressureEvent); + } + + _scriptProfilerTypeToScriptTimelineRecordType(type) + { + switch (type) { + case ScriptProfilerAgent.EventType.API: + return WebInspector.ScriptTimelineRecord.EventType.APIScriptEvaluated; + case ScriptProfilerAgent.EventType.Microtask: + return WebInspector.ScriptTimelineRecord.EventType.MicrotaskDispatched; + case ScriptProfilerAgent.EventType.Other: + return WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated; + } + } + + scriptProfilerProgrammaticCaptureStarted() + { + // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection + console.assert(WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript); + console.assert(!this._isCapturing); + + this.programmaticCaptureStarted(); + } + + scriptProfilerProgrammaticCaptureStopped() + { + // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection + console.assert(WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript); + console.assert(this._isCapturing); + + this.programmaticCaptureStopped(); + } + + scriptProfilerTrackingStarted(timestamp) + { + this._scriptProfilerRecords = []; + + this.capturingStarted(timestamp); + } + + scriptProfilerTrackingUpdated(event) + { + let {startTime, endTime, type} = event; + let scriptRecordType = this._scriptProfilerTypeToScriptTimelineRecordType(type); + let record = new WebInspector.ScriptTimelineRecord(scriptRecordType, startTime, endTime, null, null, null, null); + record.__scriptProfilerType = type; + this._scriptProfilerRecords.push(record); + + // "Other" events, generated by Web content, will have wrapping Timeline records + // and need to be merged. Non-Other events, generated purely by the JavaScript + // engine or outside of the page via APIs, will not have wrapping Timeline + // records, so these records can just be added right now. + if (type !== ScriptProfilerAgent.EventType.Other) + this._addRecord(record); + } + + scriptProfilerTrackingCompleted(samples) + { + console.assert(!this._webTimelineScriptRecordsExpectingScriptProfilerEvents || this._scriptProfilerRecords.length >= this._webTimelineScriptRecordsExpectingScriptProfilerEvents.length); + + if (samples) { + let {stackTraces} = samples; + let topDownCallingContextTree = this.activeRecording.topDownCallingContextTree; + let bottomUpCallingContextTree = this.activeRecording.bottomUpCallingContextTree; + let topFunctionsTopDownCallingContextTree = this.activeRecording.topFunctionsTopDownCallingContextTree; + let topFunctionsBottomUpCallingContextTree = this.activeRecording.topFunctionsBottomUpCallingContextTree; + + // Calculate a per-sample duration. + let timestampIndex = 0; + let timestampCount = stackTraces.length; + let sampleDurations = new Array(timestampCount); + let sampleDurationIndex = 0; + const defaultDuration = 1 / 1000; // 1ms. + for (let i = 0; i < this._scriptProfilerRecords.length; ++i) { + let record = this._scriptProfilerRecords[i]; + + // Use a default duration for timestamps recorded outside of ScriptProfiler events. + while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.startTime) { + sampleDurations[sampleDurationIndex++] = defaultDuration; + timestampIndex++; + } + + // Average the duration per sample across all samples during the record. + let samplesInRecord = 0; + while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.endTime) { + timestampIndex++; + samplesInRecord++; + } + if (samplesInRecord) { + let averageDuration = (record.endTime - record.startTime) / samplesInRecord; + sampleDurations.fill(averageDuration, sampleDurationIndex, sampleDurationIndex + samplesInRecord); + sampleDurationIndex += samplesInRecord; + } + } + + // Use a default duration for timestamps recorded outside of ScriptProfiler events. + if (timestampIndex < timestampCount) + sampleDurations.fill(defaultDuration, sampleDurationIndex); + + for (let i = 0; i < stackTraces.length; i++) { + topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]); + bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]); + topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]); + topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]); + } + + // FIXME: This transformation should not be needed after introducing ProfileView. + // Once we eliminate ProfileNodeTreeElements and ProfileNodeDataGridNodes. + // <https://webkit.org/b/154973> Web Inspector: Timelines UI redesign: Remove TimelineSidebarPanel + for (let i = 0; i < this._scriptProfilerRecords.length; ++i) { + let record = this._scriptProfilerRecords[i]; + record.profilePayload = topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime); + } + } + + // Associate the ScriptProfiler created records with Web Timeline records. + // Filter out the already added ScriptProfiler events which should not have been wrapped. + if (WebInspector.debuggableType !== WebInspector.DebuggableType.JavaScript) { + this._scriptProfilerRecords = this._scriptProfilerRecords.filter((x) => x.__scriptProfilerType === ScriptProfilerAgent.EventType.Other); + this._mergeScriptProfileRecords(); + } + + this._scriptProfilerRecords = null; + + let timeline = this.activeRecording.timelineForRecordType(WebInspector.TimelineRecord.Type.Script); + timeline.refresh(); + } + + _mergeScriptProfileRecords() + { + let nextRecord = function(list) { return list.shift() || null; }; + let nextWebTimelineRecord = nextRecord.bind(null, this._webTimelineScriptRecordsExpectingScriptProfilerEvents); + let nextScriptProfilerRecord = nextRecord.bind(null, this._scriptProfilerRecords); + let recordEnclosesRecord = function(record1, record2) { + return record1.startTime <= record2.startTime && record1.endTime >= record2.endTime; + }; + + let webRecord = nextWebTimelineRecord(); + let profilerRecord = nextScriptProfilerRecord(); + + while (webRecord && profilerRecord) { + // Skip web records with parent web records. For example an EvaluateScript with an EvaluateScript parent. + if (webRecord.parent instanceof WebInspector.ScriptTimelineRecord) { + console.assert(recordEnclosesRecord(webRecord.parent, webRecord), "Timeline Record incorrectly wrapping another Timeline Record"); + webRecord = nextWebTimelineRecord(); + continue; + } + + // Normal case of a Web record wrapping a Script record. + if (recordEnclosesRecord(webRecord, profilerRecord)) { + webRecord.profilePayload = profilerRecord.profilePayload; + profilerRecord = nextScriptProfilerRecord(); + + // If there are more script profile records in the same time interval, add them + // as individual script evaluated records with profiles. This can happen with + // web microtask checkpoints that are technically inside of other web records. + // FIXME: <https://webkit.org/b/152903> Web Inspector: Timeline Cleanup: Better Timeline Record for Microtask Checkpoints + while (profilerRecord && recordEnclosesRecord(webRecord, profilerRecord)) { + this._addRecord(profilerRecord); + profilerRecord = nextScriptProfilerRecord(); + } + + webRecord = nextWebTimelineRecord(); + continue; + } + + // Profiler Record is entirely after the Web Record. This would mean an empty web record. + if (profilerRecord.startTime > webRecord.endTime) { + console.warn("Unexpected case of a Timeline record not containing a ScriptProfiler event and profile data"); + webRecord = nextWebTimelineRecord(); + continue; + } + + // Non-wrapped profiler record. + console.warn("Unexpected case of a ScriptProfiler event not being contained by a Timeline record"); + this._addRecord(profilerRecord); + profilerRecord = nextScriptProfilerRecord(); + } + + // Skipping the remaining ScriptProfiler events to match the current UI for handling Timeline records. + // However, the remaining ScriptProfiler records are valid and could be shown. + // FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records + } + + _updateAutoCaptureInstruments() + { + if (!window.TimelineAgent) + return; + + if (!TimelineAgent.setInstruments) + return; + + let instrumentSet = new Set; + let enabledTimelineTypes = this._enabledTimelineTypesSetting.value; + + for (let timelineType of enabledTimelineTypes) { + switch (timelineType) { + case WebInspector.TimelineRecord.Type.Script: + instrumentSet.add(TimelineAgent.Instrument.ScriptProfiler); + break; + case WebInspector.TimelineRecord.Type.HeapAllocations: + instrumentSet.add(TimelineAgent.Instrument.Heap); + break; + case WebInspector.TimelineRecord.Type.Network: + case WebInspector.TimelineRecord.Type.RenderingFrame: + case WebInspector.TimelineRecord.Type.Layout: + instrumentSet.add(TimelineAgent.Instrument.Timeline); + break; + case WebInspector.TimelineRecord.Type.Memory: + instrumentSet.add(TimelineAgent.Instrument.Memory); + break; + } + } + + TimelineAgent.setInstruments([...instrumentSet]); + } +}; + +WebInspector.TimelineManager.Event = { + RecordingCreated: "timeline-manager-recording-created", + RecordingLoaded: "timeline-manager-recording-loaded", + CapturingWillStart: "timeline-manager-capturing-will-start", + CapturingStarted: "timeline-manager-capturing-started", + CapturingStopped: "timeline-manager-capturing-stopped" +}; + +WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds +WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds +WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds diff --git a/Source/WebInspectorUI/UserInterface/Controllers/TypeTokenAnnotator.js b/Source/WebInspectorUI/UserInterface/Controllers/TypeTokenAnnotator.js new file mode 100644 index 000000000..7b820bbec --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/TypeTokenAnnotator.js @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014, 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.TypeTokenAnnotator = class TypeTokenAnnotator extends WebInspector.Annotator +{ + constructor(sourceCodeTextEditor, script) + { + super(sourceCodeTextEditor); + + this._script = script; + this._typeTokenNodes = []; + this._typeTokenBookmarks = []; + } + + // Protected + + insertAnnotations() + { + if (!this.isActive()) + return; + + var scriptSyntaxTree = this._script.scriptSyntaxTree; + + if (!scriptSyntaxTree) { + this._script.requestScriptSyntaxTree((syntaxTree) => { + // After requesting the tree, we still might get a null tree from a parse error. + if (syntaxTree) + this.insertAnnotations(); + }); + return; + } + + if (!scriptSyntaxTree.parsedSuccessfully) + return; + + var {startOffset, endOffset} = this.sourceCodeTextEditor.visibleRangeOffsets(); + + var startTime = Date.now(); + var allNodesInRange = scriptSyntaxTree.filterByRange(startOffset, endOffset); + scriptSyntaxTree.updateTypes(allNodesInRange, (nodesWithUpdatedTypes) => { + // Because this is an asynchronous call, we could have been deactivated before the callback function is called. + if (!this.isActive()) + return; + + nodesWithUpdatedTypes.forEach(this._insertTypeToken, this); + + let totalTime = Date.now() - startTime; + let timeoutTime = Number.constrain(8 * totalTime, 500, 2000); + this._timeoutIdentifier = setTimeout(() => { + this._timeoutIdentifier = null; + this.insertAnnotations(); + }, timeoutTime); + }); + } + + clearAnnotations() + { + this._clearTypeTokens(); + } + + // Private + + _insertTypeToken(node) + { + if (node.type === WebInspector.ScriptSyntaxTree.NodeType.Identifier) { + if (!node.attachments.__typeToken && node.attachments.types && node.attachments.types.valid) + this._insertToken(node.range[0], node, false, WebInspector.TypeTokenView.TitleType.Variable, node.name); + + if (node.attachments.__typeToken) + node.attachments.__typeToken.update(node.attachments.types); + + return; + } + + console.assert(node.type === WebInspector.ScriptSyntaxTree.NodeType.FunctionDeclaration || node.type === WebInspector.ScriptSyntaxTree.NodeType.FunctionExpression || node.type === WebInspector.ScriptSyntaxTree.NodeType.ArrowFunctionExpression); + + var functionReturnType = node.attachments.returnTypes; + if (!functionReturnType || !functionReturnType.valid) + return; + + // If a function does not have an explicit return statement with an argument (i.e, "return x;" instead of "return;") + // then don't show a return type unless we think it's a constructor. + var scriptSyntaxTree = this._script._scriptSyntaxTree; + if (!node.attachments.__typeToken && (scriptSyntaxTree.containsNonEmptyReturnStatement(node.body) || !functionReturnType.typeSet.isContainedIn(WebInspector.TypeSet.TypeBit.Undefined))) { + var functionName = node.id ? node.id.name : null; + this._insertToken(node.typeProfilingReturnDivot, node, true, WebInspector.TypeTokenView.TitleType.ReturnStatement, functionName); + } + + if (node.attachments.__typeToken) + node.attachments.__typeToken.update(node.attachments.returnTypes); + } + + _insertToken(originalOffset, node, shouldTranslateOffsetToAfterParameterList, typeTokenTitleType, functionOrVariableName) + { + var tokenPosition = this.sourceCodeTextEditor.originalOffsetToCurrentPosition(originalOffset); + var currentOffset = this.sourceCodeTextEditor.currentPositionToCurrentOffset(tokenPosition); + var sourceString = this.sourceCodeTextEditor.string; + + if (shouldTranslateOffsetToAfterParameterList) { + // Translate the position to the closing parenthesis of the function arguments: + // translate from: [type-token] function foo() {} => to: function foo() [type-token] {} + currentOffset = this._translateToOffsetAfterFunctionParameterList(node, currentOffset, sourceString); + tokenPosition = this.sourceCodeTextEditor.currentOffsetToCurrentPosition(currentOffset); + } + + // Note: bookmarks render to the left of the character they're being displayed next to. + // This is why right margin checks the current offset. And this is okay to do because JavaScript can't be written right-to-left. + var isSpaceRegexp = /\s/; + var shouldHaveLeftMargin = currentOffset !== 0 && !isSpaceRegexp.test(sourceString[currentOffset - 1]); + var shouldHaveRightMargin = !isSpaceRegexp.test(sourceString[currentOffset]); + var typeToken = new WebInspector.TypeTokenView(this, shouldHaveRightMargin, shouldHaveLeftMargin, typeTokenTitleType, functionOrVariableName); + var bookmark = this.sourceCodeTextEditor.setInlineWidget(tokenPosition, typeToken.element); + node.attachments.__typeToken = typeToken; + this._typeTokenNodes.push(node); + this._typeTokenBookmarks.push(bookmark); + } + + _translateToOffsetAfterFunctionParameterList(node, offset, sourceString) + { + // The assumption here is that we get the offset starting at the function keyword (or after the get/set keywords). + // We will return the offset for the closing parenthesis in the function declaration. + // All this code is just a way to find this parenthesis while ignoring comments. + + var isMultiLineComment = false; + var isSingleLineComment = false; + var shouldIgnore = false; + const isArrowFunction = node.type === WebInspector.ScriptSyntaxTree.NodeType.ArrowFunctionExpression; + + function isLineTerminator(char) + { + // Reference EcmaScript 5 grammar for single line comments and line terminators: + // http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 + // http://www.ecma-international.org/ecma-262/5.1/#sec-7.4 + return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029"; + } + + while (((!isArrowFunction && sourceString[offset] !== ")") + || (isArrowFunction && sourceString[offset] !== ">") + || shouldIgnore) + && offset < sourceString.length) { + if (isSingleLineComment && isLineTerminator(sourceString[offset])) { + isSingleLineComment = false; + shouldIgnore = false; + } else if (isMultiLineComment && sourceString[offset] === "*" && sourceString[offset + 1] === "/") { + isMultiLineComment = false; + shouldIgnore = false; + offset++; + } else if (!shouldIgnore && sourceString[offset] === "/") { + offset++; + if (sourceString[offset] === "*") + isMultiLineComment = true; + else if (sourceString[offset] === "/") + isSingleLineComment = true; + else + throw new Error("Bad parsing. Couldn't parse comment preamble."); + shouldIgnore = true; + } + + offset++; + } + + return offset + 1; + } + + _clearTypeTokens() + { + this._typeTokenNodes.forEach(function(node) { + node.attachments.__typeToken = null; + }); + this._typeTokenBookmarks.forEach(function(bookmark) { + bookmark.clear(); + }); + + this._typeTokenNodes = []; + this._typeTokenBookmarks = []; + } +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/VisualStyleCompletionsController.js b/Source/WebInspectorUI/UserInterface/Controllers/VisualStyleCompletionsController.js new file mode 100644 index 000000000..d2a40b9d4 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/VisualStyleCompletionsController.js @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.VisualStyleCompletionsController = class VisualStyleCompletionsController extends WebInspector.Object +{ + constructor(delegate) + { + super(); + + this._delegate = delegate || null; + this._suggestionsView = new WebInspector.CompletionSuggestionsView(this); + this._completions = null; + this._currentCompletions = []; + this._selectedCompletionIndex = 0; + } + + // Public + + get visible() + { + return this._completions && this._currentCompletions.length && this._suggestionsView.visible; + } + + get hasCompletions() + { + return !!this._completions; + } + + get currentCompletion() + { + if (!this.hasCompletions) + return null; + + return this._currentCompletions[this._selectedCompletionIndex] || null; + } + + set completions(completions) + { + this._completions = completions || null; + } + + completionSuggestionsViewCustomizeCompletionElement(suggestionsView, element, item) + { + if (this._delegate && typeof this._delegate.visualStyleCompletionsControllerCustomizeCompletionElement === "function") + this._delegate.visualStyleCompletionsControllerCustomizeCompletionElement(suggestionsView, element, item); + } + + completionSuggestionsClickedCompletion(suggestionsView, text) + { + suggestionsView.hide(); + this.dispatchEventToListeners(WebInspector.VisualStyleCompletionsController.Event.CompletionSelected, {text}); + } + + previous() + { + this._suggestionsView.selectPrevious(); + this._selectedCompletionIndex = this._suggestionsView.selectedIndex; + } + + next() + { + this._suggestionsView.selectNext(); + this._selectedCompletionIndex = this._suggestionsView.selectedIndex; + } + + update(value) + { + if (!this.hasCompletions) + return false; + + this._currentCompletions = this._completions.startsWith(value); + + var currentCompletionsLength = this._currentCompletions.length; + if (currentCompletionsLength) { + if (currentCompletionsLength === 1 && this._currentCompletions[0] === value) { + this.hide(); + return false; + } + + if (this._selectedCompletionIndex >= currentCompletionsLength) + this._selectedCompletionIndex = 0; + + this._suggestionsView.update(this._currentCompletions, this._selectedCompletionIndex); + return true; + } + + this.hide(); + return false; + } + + show(bounds, padding) + { + if (!bounds) + return; + + this._suggestionsView.show(bounds.pad(padding || 0)); + } + + hide() + { + if (this._suggestionsView.isHandlingClickEvent()) + return; + + this._suggestionsView.hide(); + } +}; + +WebInspector.VisualStyleCompletionsController.Event = { + CompletionSelected: "visual-style-completions-controller-completion-selected" +}; diff --git a/Source/WebInspectorUI/UserInterface/Controllers/WorkerManager.js b/Source/WebInspectorUI/UserInterface/Controllers/WorkerManager.js new file mode 100644 index 000000000..99cd3f6ee --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Controllers/WorkerManager.js @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +WebInspector.WorkerManager = class WorkerManager extends WebInspector.Object +{ + constructor() + { + super(); + + this._connections = new Map; + + if (window.WorkerAgent) + WorkerAgent.enable(); + } + + // Public + + workerCreated(workerId, url) + { + let connection = new InspectorBackend.WorkerConnection(workerId); + let workerTarget = new WebInspector.WorkerTarget(workerId, url, connection); + WebInspector.targetManager.addTarget(workerTarget); + + this._connections.set(workerId, connection); + + // Unpause the worker now that we have sent all initialization messages. + WorkerAgent.initialized(workerId); + } + + workerTerminated(workerId) + { + let connection = this._connections.take(workerId); + + WebInspector.targetManager.removeTarget(connection.target); + } + + dispatchMessageFromWorker(workerId, message) + { + let connection = this._connections.get(workerId); + + console.assert(connection); + if (!connection) + return; + + connection.dispatch(message); + } +}; |