diff options
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js')
-rw-r--r-- | Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js | 618 |
1 files changed, 618 insertions, 0 deletions
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" +}; |