summaryrefslogtreecommitdiff
path: root/Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
commit1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch)
tree46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js
parent32761a6cee1d0dee366b885b7b9c777e67885688 (diff)
downloadWebKitGtk-tarball-master.tar.gz
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js')
-rw-r--r--Source/WebInspectorUI/UserInterface/Controllers/CodeMirrorTokenTrackingController.js618
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"
+};