2 * Copyright (C) 2013 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
26 WebInspector.CodeMirrorTokenTrackingController = class CodeMirrorTokenTrackingController extends WebInspector.Object
28 constructor(codeMirror, delegate)
32 console.assert(codeMirror);
34 this._codeMirror = codeMirror;
35 this._delegate = delegate || null;
36 this._mode = WebInspector.CodeMirrorTokenTrackingController.Mode.None;
38 this._mouseOverDelayDuration = 0;
39 this._mouseOutReleaseDelayDuration = 0;
40 this._classNameForHighlightedRange = null;
42 this._enabled = false;
43 this._tracking = false;
44 this._hoveredTokenInfo = null;
45 this._hoveredMarker = null;
52 return this._delegate;
67 if (this._enabled === enabled)
70 this._enabled = enabled;
72 var wrapper = this._codeMirror.getWrapperElement();
74 wrapper.addEventListener("mouseenter", this);
75 wrapper.addEventListener("mouseleave", this);
76 this._updateHoveredTokenInfo({left: WebInspector.mouseCoords.x, top: WebInspector.mouseCoords.y});
77 this._startTracking();
79 wrapper.removeEventListener("mouseenter", this);
80 wrapper.removeEventListener("mouseleave", this);
92 var oldMode = this._mode;
94 this._mode = mode || WebInspector.CodeMirrorTokenTrackingController.Mode.None;
96 if (oldMode !== this._mode && this._tracking && this._hoveredTokenInfo)
97 this._processNewHoveredToken();
100 get mouseOverDelayDuration()
102 return this._mouseOverDelayDuration;
105 set mouseOverDelayDuration(x)
107 console.assert(x >= 0);
108 this._mouseOverDelayDuration = Math.max(x, 0);
111 get mouseOutReleaseDelayDuration()
113 return this._mouseOutReleaseDelayDuration;
116 set mouseOutReleaseDelayDuration(x)
118 console.assert(x >= 0);
119 this._mouseOutReleaseDelayDuration = Math.max(x, 0);
122 get classNameForHighlightedRange()
124 return this._classNameForHighlightedRange;
127 set classNameForHighlightedRange(x)
129 this._classNameForHighlightedRange = x || null;
134 return this._candidate;
139 return this._hoveredMarker;
142 set hoveredMarker(hoveredMarker)
144 this._hoveredMarker = hoveredMarker;
147 highlightLastHoveredRange()
150 this.highlightRange(this._candidate.hoveredTokenRange);
153 highlightRange(range)
155 // Nothing to do if we're trying to highlight the same range.
156 if (this._codeMirrorMarkedText && this._codeMirrorMarkedText.className === this._classNameForHighlightedRange) {
157 var highlightedRange = this._codeMirrorMarkedText.find();
158 if (WebInspector.compareCodeMirrorPositions(highlightedRange.from, range.start) === 0 &&
159 WebInspector.compareCodeMirrorPositions(highlightedRange.to, range.end) === 0)
163 this.removeHighlightedRange();
165 var className = this._classNameForHighlightedRange || "";
166 this._codeMirrorMarkedText = this._codeMirror.markText(range.start, range.end, {className});
168 window.addEventListener("mousemove", this, true);
171 removeHighlightedRange()
173 if (!this._codeMirrorMarkedText)
176 this._codeMirrorMarkedText.clear();
177 delete this._codeMirrorMarkedText;
179 window.removeEventListener("mousemove", this, true);
186 console.assert(!this._tracking);
190 this._tracking = true;
192 var wrapper = this._codeMirror.getWrapperElement();
193 wrapper.addEventListener("mousemove", this, true);
194 wrapper.addEventListener("mouseout", this, false);
195 wrapper.addEventListener("mousedown", this, false);
196 wrapper.addEventListener("mouseup", this, false);
197 window.addEventListener("blur", this, true);
202 console.assert(this._tracking);
206 this._tracking = false;
207 this._candidate = null;
209 var wrapper = this._codeMirror.getWrapperElement();
210 wrapper.removeEventListener("mousemove", this, true);
211 wrapper.removeEventListener("mouseout", this, false);
212 wrapper.removeEventListener("mousedown", this, false);
213 wrapper.removeEventListener("mouseup", this, false);
214 window.removeEventListener("blur", this, true);
215 window.removeEventListener("mousemove", this, true);
217 this._resetTrackingStates();
222 switch (event.type) {
224 this._mouseEntered(event);
227 this._mouseLeft(event);
230 if (event.currentTarget === window)
231 this._mouseMovedWithMarkedText(event);
233 this._mouseMovedOverEditor(event);
236 // Only deal with a mouseout event that has the editor wrapper as the target.
237 if (!event.currentTarget.contains(event.relatedTarget))
238 this._mouseMovedOutOfEditor(event);
241 this._mouseButtonWasPressedOverEditor(event);
244 this._mouseButtonWasReleasedOverEditor(event);
247 this._windowLostFocus(event);
255 this._startTracking();
260 this._stopTracking();
263 _mouseMovedWithMarkedText(event)
265 var shouldRelease = !event.target.classList.contains(this._classNameForHighlightedRange);
266 if (shouldRelease && this._delegate && typeof this._delegate.tokenTrackingControllerCanReleaseHighlightedRange === "function")
267 shouldRelease = this._delegate.tokenTrackingControllerCanReleaseHighlightedRange(this, event.target);
270 if (!this._markedTextMouseoutTimer)
271 this._markedTextMouseoutTimer = setTimeout(this._markedTextIsNoLongerHovered.bind(this), this._mouseOutReleaseDelayDuration);
275 clearTimeout(this._markedTextMouseoutTimer);
276 delete this._markedTextMouseoutTimer;
279 _markedTextIsNoLongerHovered()
281 if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function")
282 this._delegate.tokenTrackingControllerHighlightedRangeReleased(this);
283 delete this._markedTextMouseoutTimer;
286 _mouseMovedOverEditor(event)
288 this._updateHoveredTokenInfo({left: event.pageX, top: event.pageY});
291 _updateHoveredTokenInfo(mouseCoords)
293 // Get the position in the text and the token at that position.
294 var position = this._codeMirror.coordsChar(mouseCoords);
295 var token = this._codeMirror.getTokenAt(position);
297 if (!token || !token.type || !token.string) {
298 if (this._hoveredMarker && this._delegate && typeof this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker === "function") {
299 if (!this._codeMirror.findMarksAt(position).includes(this._hoveredMarker.codeMirrorTextMarker))
300 this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker(this, this._hoveredMarker);
303 this._resetTrackingStates();
307 // Stop right here if we're hovering the same token as we were last time.
308 if (this._hoveredTokenInfo &&
309 this._hoveredTokenInfo.position.line === position.line &&
310 this._hoveredTokenInfo.token.start === token.start &&
311 this._hoveredTokenInfo.token.end === token.end)
314 // We have a new hovered token.
315 var innerMode = CodeMirror.innerMode(this._codeMirror.getMode(), token.state);
316 var codeMirrorModeName = innerMode.mode.alternateName || innerMode.mode.name;
317 this._hoveredTokenInfo = {
321 modeName: codeMirrorModeName
324 clearTimeout(this._tokenHoverTimer);
326 if (this._codeMirrorMarkedText || !this._mouseOverDelayDuration)
327 this._processNewHoveredToken();
329 this._tokenHoverTimer = setTimeout(this._processNewHoveredToken.bind(this), this._mouseOverDelayDuration);
332 _mouseMovedOutOfEditor(event)
334 clearTimeout(this._tokenHoverTimer);
335 delete this._hoveredTokenInfo;
336 delete this._selectionMayBeInProgress;
339 _mouseButtonWasPressedOverEditor(event)
341 this._selectionMayBeInProgress = true;
344 _mouseButtonWasReleasedOverEditor(event)
346 delete this._selectionMayBeInProgress;
347 this._mouseMovedOverEditor(event);
349 if (this._codeMirrorMarkedText && this._hoveredTokenInfo) {
350 var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY});
351 var marks = this._codeMirror.findMarksAt(position);
352 for (var i = 0; i < marks.length; ++i) {
353 if (marks[i] === this._codeMirrorMarkedText) {
354 if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeWasClicked === "function") {
355 // Trigger the clicked delegate asynchronously, letting the editor complete handling of the click.
356 setTimeout(function() { this._delegate.tokenTrackingControllerHighlightedRangeWasClicked(this); }.bind(this), 0);
364 _windowLostFocus(event)
366 this._resetTrackingStates();
369 _processNewHoveredToken()
371 console.assert(this._hoveredTokenInfo);
373 if (this._selectionMayBeInProgress)
376 this._candidate = null;
378 switch (this._mode) {
379 case WebInspector.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens:
380 this._candidate = this._processNonSymbolToken();
382 case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression:
383 case WebInspector.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation:
384 this._candidate = this._processJavaScriptExpression();
386 case WebInspector.CodeMirrorTokenTrackingController.Mode.MarkedTokens:
387 this._candidate = this._processMarkedToken();
391 if (!this._candidate)
394 clearTimeout(this._markedTextMouseoutTimer);
395 delete this._markedTextMouseoutTimer;
397 if (this._delegate && typeof this._delegate.tokenTrackingControllerNewHighlightCandidate === "function")
398 this._delegate.tokenTrackingControllerNewHighlightCandidate(this, this._candidate);
401 _processNonSymbolToken()
403 // Ignore any symbol tokens.
404 var type = this._hoveredTokenInfo.token.type;
408 var startPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
409 var endPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.end};
412 hoveredToken: this._hoveredTokenInfo.token,
413 hoveredTokenRange: {start: startPosition, end: endPosition},
417 _processJavaScriptExpression()
419 // Only valid within JavaScript.
420 if (this._hoveredTokenInfo.modeName !== "javascript")
423 var startPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
424 var endPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.end};
426 function tokenIsInRange(token, range)
428 return token.line >= range.start.line && token.ch >= range.start.ch &&
429 token.line <= range.end.line && token.ch <= range.end.ch;
432 // If the hovered token is within a selection, use the selection as our expression.
433 if (this._codeMirror.somethingSelected()) {
434 var selectionRange = {
435 start: this._codeMirror.getCursor("start"),
436 end: this._codeMirror.getCursor("end")
439 if (tokenIsInRange(startPosition, selectionRange) || tokenIsInRange(endPosition, selectionRange)) {
441 hoveredToken: this._hoveredTokenInfo.token,
442 hoveredTokenRange: selectionRange,
443 expression: this._codeMirror.getSelection(),
444 expressionRange: selectionRange,
449 // We only handle vars, definitions, properties, and the keyword 'this'.
450 var type = this._hoveredTokenInfo.token.type;
451 var isProperty = type.indexOf("property") !== -1;
452 var isKeyword = type.indexOf("keyword") !== -1;
453 if (!isProperty && !isKeyword && type.indexOf("variable") === -1 && type.indexOf("def") === -1)
456 // Not object literal properties.
457 var state = this._hoveredTokenInfo.innerMode.state;
458 if (isProperty && state.lexical && state.lexical.type === "}")
461 // Only the "this" keyword.
462 if (isKeyword && this._hoveredTokenInfo.token.string !== "this")
465 // Work out the full hovered expression.
466 var expression = this._hoveredTokenInfo.token.string;
467 var expressionStartPosition = {line: this._hoveredTokenInfo.position.line, ch: this._hoveredTokenInfo.token.start};
469 var token = this._codeMirror.getTokenAt(expressionStartPosition);
473 var isDot = !token.type && token.string === ".";
474 var isExpression = token.type && token.type.includes("m-javascript");
475 if (!isDot && !isExpression)
478 // Disallow operators. We want the hovered expression to be just a single operand.
479 // Also, some operators can modify values, such as pre-increment and assignment operators.
480 if (isExpression && token.type.includes("operator"))
483 expression = token.string + expression;
484 expressionStartPosition.ch = token.start;
487 // Return the candidate for this token and expression.
489 hoveredToken: this._hoveredTokenInfo.token,
490 hoveredTokenRange: {start: startPosition, end: endPosition},
492 expressionRange: {start: expressionStartPosition, end: endPosition},
496 _processMarkedToken()
498 return this._processNonSymbolToken();
501 _resetTrackingStates()
503 clearTimeout(this._tokenHoverTimer);
504 delete this._selectionMayBeInProgress;
505 delete this._hoveredTokenInfo;
506 this.removeHighlightedRange();
510 WebInspector.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName = "jump-to-symbol-highlight";
512 WebInspector.CodeMirrorTokenTrackingController.Mode = {
514 NonSymbolTokens: "non-symbol-tokens",
515 JavaScriptExpression: "javascript-expression",
516 JavaScriptTypeInformation: "javascript-type-information",
517 MarkedTokens: "marked-tokens"